mirror of
https://github.com/esphome/esphome.git
synced 2025-08-02 16:37:46 +00:00
Merge branch 'scheduler_opt' into integration
This commit is contained in:
commit
36ca3546f5
@ -1 +1 @@
|
|||||||
a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a
|
07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a
|
||||||
|
92
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
92
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
name: Report an issue with ESPHome
|
||||||
|
description: Report an issue with ESPHome.
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for reporting bugs only!
|
||||||
|
|
||||||
|
If you have a feature request or enhancement, please [request them here instead][fr].
|
||||||
|
|
||||||
|
[fr]: https://github.com/orgs/esphome/discussions
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: The problem
|
||||||
|
description: >-
|
||||||
|
Describe the issue you are experiencing here to communicate to the
|
||||||
|
maintainers. Tell us what you were trying to do and what happened.
|
||||||
|
|
||||||
|
Provide a clear and concise description of what the problem is.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Environment
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which version of ESPHome has the issue?
|
||||||
|
description: >
|
||||||
|
ESPHome version like 1.19, 2025.6.0 or 2025.XX.X-dev.
|
||||||
|
- type: dropdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
id: installation
|
||||||
|
attributes:
|
||||||
|
label: What type of installation are you using?
|
||||||
|
options:
|
||||||
|
- Home Assistant Add-on
|
||||||
|
- Docker
|
||||||
|
- pip
|
||||||
|
- type: dropdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: What platform are you using?
|
||||||
|
options:
|
||||||
|
- ESP8266
|
||||||
|
- ESP32
|
||||||
|
- RP2040
|
||||||
|
- BK72XX
|
||||||
|
- RTL87XX
|
||||||
|
- LN882X
|
||||||
|
- Host
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: component_name
|
||||||
|
attributes:
|
||||||
|
label: Component causing the issue
|
||||||
|
description: >
|
||||||
|
The name of the component or platform. For example, api/i2c or ultrasonic.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Details
|
||||||
|
- type: textarea
|
||||||
|
id: config
|
||||||
|
attributes:
|
||||||
|
label: YAML Config
|
||||||
|
description: |
|
||||||
|
Include a complete YAML configuration file demonstrating the problem here. Preferably post the *entire* file - don't make assumptions about what is unimportant. However, if it's a large or complicated config then you will need to reduce it to the smallest possible file *that still demonstrates the problem*. If you don't provide enough information to *easily* reproduce the problem, it's unlikely your bug report will get any attention. Logs do not belong here, attach them below.
|
||||||
|
render: yaml
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Anything in the logs that might be useful for us?
|
||||||
|
description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs.
|
||||||
|
render: txt
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: >
|
||||||
|
If you have any additional information for us, use the field below.
|
||||||
|
Please note, you can attach screenshots or screen recordings here, by
|
||||||
|
dragging and dropping files in the field below.
|
26
.github/ISSUE_TEMPLATE/config.yml
vendored
26
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,15 +1,21 @@
|
|||||||
---
|
---
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Issue Tracker
|
- name: Report an issue with the ESPHome documentation
|
||||||
url: https://github.com/esphome/issues
|
url: https://github.com/esphome/esphome-docs/issues/new/choose
|
||||||
about: Please create bug reports in the dedicated issue tracker.
|
about: Report an issue with the ESPHome documentation.
|
||||||
- name: Feature Request Tracker
|
- name: Report an issue with the ESPHome web server
|
||||||
url: https://github.com/esphome/feature-requests
|
url: https://github.com/esphome/esphome-webserver/issues/new/choose
|
||||||
about: |
|
about: Report an issue with the ESPHome web server.
|
||||||
Please create feature requests in the dedicated feature request tracker.
|
- name: Report an issue with the ESPHome Builder / Dashboard
|
||||||
|
url: https://github.com/esphome/dashboard/issues/new/choose
|
||||||
|
about: Report an issue with the ESPHome Builder / Dashboard.
|
||||||
|
- name: Report an issue with the ESPHome API client
|
||||||
|
url: https://github.com/esphome/aioesphomeapi/issues/new/choose
|
||||||
|
about: Report an issue with the ESPHome API client.
|
||||||
|
- name: Make a Feature Request
|
||||||
|
url: https://github.com/orgs/esphome/discussions
|
||||||
|
about: Please create feature requests in the dedicated feature request tracker.
|
||||||
- name: Frequently Asked Question
|
- name: Frequently Asked Question
|
||||||
url: https://esphome.io/guides/faq.html
|
url: https://esphome.io/guides/faq.html
|
||||||
about: |
|
about: Please view the FAQ for common questions and what to include in a bug report.
|
||||||
Please view the FAQ for common questions and what
|
|
||||||
to include in a bug report.
|
|
||||||
|
@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
|||||||
|
|
||||||
#ifdef USE_WEBSERVER_SORTING
|
#ifdef USE_WEBSERVER_SORTING
|
||||||
for (auto &group : ws->sorting_groups_) {
|
for (auto &group : ws->sorting_groups_) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
message = json::build_json([group](JsonObject root) {
|
message = json::build_json([group](JsonObject root) {
|
||||||
root["name"] = group.second.name;
|
root["name"] = group.second.name;
|
||||||
root["sorting_weight"] = group.second.weight;
|
root["sorting_weight"] = group.second.weight;
|
||||||
});
|
});
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
|
|
||||||
// a (very) large number of these should be able to be queued initially without defer
|
// a (very) large number of these should be able to be queued initially without defer
|
||||||
// since the only thing in the send buffer at this point is the initial ping/config
|
// since the only thing in the send buffer at this point is the initial ping/config
|
||||||
|
@ -71,7 +71,7 @@ void Application::setup() {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
uint8_t new_app_state = STATUS_LED_WARNING;
|
uint8_t new_app_state = STATUS_LED_WARNING;
|
||||||
this->scheduler.call();
|
this->scheduler.call(millis());
|
||||||
this->feed_wdt();
|
this->feed_wdt();
|
||||||
for (uint32_t j = 0; j <= i; j++) {
|
for (uint32_t j = 0; j <= i; j++) {
|
||||||
// Update loop_component_start_time_ right before calling each component
|
// Update loop_component_start_time_ right before calling each component
|
||||||
@ -97,11 +97,11 @@ void Application::setup() {
|
|||||||
void Application::loop() {
|
void Application::loop() {
|
||||||
uint8_t new_app_state = 0;
|
uint8_t new_app_state = 0;
|
||||||
|
|
||||||
this->scheduler.call();
|
|
||||||
|
|
||||||
// Get the initial loop time at the start
|
// Get the initial loop time at the start
|
||||||
uint32_t last_op_end_time = millis();
|
uint32_t last_op_end_time = millis();
|
||||||
|
|
||||||
|
this->scheduler.call(last_op_end_time);
|
||||||
|
|
||||||
// Feed WDT with time
|
// Feed WDT with time
|
||||||
this->feed_wdt(last_op_end_time);
|
this->feed_wdt(last_op_end_time);
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ void Application::loop() {
|
|||||||
this->yield_with_select_(0);
|
this->yield_with_select_(0);
|
||||||
} else {
|
} else {
|
||||||
uint32_t delay_time = this->loop_interval_ - elapsed;
|
uint32_t delay_time = this->loop_interval_ - elapsed;
|
||||||
uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time);
|
uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time);
|
||||||
// next_schedule is max 0.5*delay_time
|
// next_schedule is max 0.5*delay_time
|
||||||
// otherwise interval=0 schedules result in constant looping with almost no sleep
|
// otherwise interval=0 schedules result in constant looping with almost no sleep
|
||||||
next_schedule = std::max(next_schedule, delay_time / 2);
|
next_schedule = std::max(next_schedule, delay_time / 2);
|
||||||
|
@ -267,6 +267,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
|
|||||||
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||||
bool Component::is_ready() const {
|
bool Component::is_ready() const {
|
||||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
||||||
|
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
|
||||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||||
}
|
}
|
||||||
bool Component::can_proceed() { return true; }
|
bool Component::can_proceed() { return true; }
|
||||||
|
@ -91,7 +91,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const auto now = this->millis_();
|
const auto now = this->millis_64_(millis());
|
||||||
|
|
||||||
// Type-specific setup
|
// Type-specific setup
|
||||||
if (type == SchedulerItem::INTERVAL) {
|
if (type == SchedulerItem::INTERVAL) {
|
||||||
@ -193,9 +193,7 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin
|
|||||||
name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor);
|
name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor);
|
||||||
|
|
||||||
if (backoff_increase_factor < 0.0001) {
|
if (backoff_increase_factor < 0.0001) {
|
||||||
ESP_LOGE(TAG,
|
ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0", backoff_increase_factor);
|
||||||
"set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead",
|
|
||||||
name.c_str(), backoff_increase_factor);
|
|
||||||
backoff_increase_factor = 1;
|
backoff_increase_factor = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,19 +213,19 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name)
|
|||||||
return this->cancel_timeout(component, "retry$" + name);
|
return this->cancel_timeout(component, "retry$" + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
optional<uint32_t> HOT Scheduler::next_schedule_in() {
|
optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
||||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||||
// It calls empty_() and accesses items_[0] without holding a lock, which is only
|
// It calls empty_() and accesses items_[0] without holding a lock, which is only
|
||||||
// safe when called from the main thread. Other threads must not call this method.
|
// safe when called from the main thread. Other threads must not call this method.
|
||||||
if (this->empty_())
|
if (this->empty_())
|
||||||
return {};
|
return {};
|
||||||
auto &item = this->items_[0];
|
auto &item = this->items_[0];
|
||||||
const auto now = this->millis_();
|
const auto now_64 = this->millis_64_(now);
|
||||||
if (item->next_execution_ < now)
|
if (item->next_execution_ < now_64)
|
||||||
return 0;
|
return 0;
|
||||||
return item->next_execution_ - now;
|
return item->next_execution_ - now_64;
|
||||||
}
|
}
|
||||||
void HOT Scheduler::call() {
|
void HOT Scheduler::call(uint32_t now) {
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
||||||
// Process defer queue first to guarantee FIFO execution order for deferred items.
|
// Process defer queue first to guarantee FIFO execution order for deferred items.
|
||||||
// Previously, defer() used the heap which gave undefined order for equal timestamps,
|
// Previously, defer() used the heap which gave undefined order for equal timestamps,
|
||||||
@ -256,22 +254,22 @@ void HOT Scheduler::call() {
|
|||||||
// Execute callback without holding lock to prevent deadlocks
|
// Execute callback without holding lock to prevent deadlocks
|
||||||
// if the callback tries to call defer() again
|
// if the callback tries to call defer() again
|
||||||
if (!this->should_skip_item_(item.get())) {
|
if (!this->should_skip_item_(item.get())) {
|
||||||
this->execute_item_(item.get());
|
this->execute_item_(item.get(), now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const auto now = this->millis_();
|
const auto now_64 = this->millis_64_(now);
|
||||||
this->process_to_add();
|
this->process_to_add();
|
||||||
|
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
static uint64_t last_print = 0;
|
static uint64_t last_print = 0;
|
||||||
|
|
||||||
if (now - last_print > 2000) {
|
if (now_64 - last_print > 2000) {
|
||||||
last_print = now;
|
last_print = now_64;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
||||||
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_,
|
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
|
||||||
this->last_millis_);
|
this->millis_major_, this->last_millis_);
|
||||||
while (!this->empty_()) {
|
while (!this->empty_()) {
|
||||||
std::unique_ptr<SchedulerItem> item;
|
std::unique_ptr<SchedulerItem> item;
|
||||||
{
|
{
|
||||||
@ -283,7 +281,7 @@ void HOT Scheduler::call() {
|
|||||||
const char *name = item->get_name();
|
const char *name = item->get_name();
|
||||||
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
|
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
|
||||||
item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval,
|
item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval,
|
||||||
item->next_execution_ - now, item->next_execution_);
|
item->next_execution_ - now_64, item->next_execution_);
|
||||||
|
|
||||||
old_items.push_back(std::move(item));
|
old_items.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
@ -328,7 +326,7 @@ void HOT Scheduler::call() {
|
|||||||
{
|
{
|
||||||
// Don't copy-by value yet
|
// Don't copy-by value yet
|
||||||
auto &item = this->items_[0];
|
auto &item = this->items_[0];
|
||||||
if (item->next_execution_ > now) {
|
if (item->next_execution_ > now_64) {
|
||||||
// Not reached timeout yet, done for this call
|
// Not reached timeout yet, done for this call
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -342,13 +340,13 @@ void HOT Scheduler::call() {
|
|||||||
const char *item_name = item->get_name();
|
const char *item_name = item->get_name();
|
||||||
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
||||||
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
|
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
|
||||||
item->next_execution_, now);
|
item->next_execution_, now_64);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Warning: During callback(), a lot of stuff can happen, including:
|
// Warning: During callback(), a lot of stuff can happen, including:
|
||||||
// - timeouts/intervals get added, potentially invalidating vector pointers
|
// - timeouts/intervals get added, potentially invalidating vector pointers
|
||||||
// - timeouts/intervals get cancelled
|
// - timeouts/intervals get cancelled
|
||||||
this->execute_item_(item.get());
|
this->execute_item_(item.get(), now);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -367,7 +365,7 @@ void HOT Scheduler::call() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item->type == SchedulerItem::INTERVAL) {
|
if (item->type == SchedulerItem::INTERVAL) {
|
||||||
item->next_execution_ = now + item->interval;
|
item->next_execution_ = now_64 + item->interval;
|
||||||
// Add new item directly to to_add_
|
// Add new item directly to to_add_
|
||||||
// since we have the lock held
|
// since we have the lock held
|
||||||
this->to_add_.push_back(std::move(item));
|
this->to_add_.push_back(std::move(item));
|
||||||
@ -423,11 +421,9 @@ void HOT Scheduler::pop_raw_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to execute a scheduler item
|
// Helper to execute a scheduler item
|
||||||
void HOT Scheduler::execute_item_(SchedulerItem *item) {
|
void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
|
||||||
App.set_current_component(item->component);
|
App.set_current_component(item->component);
|
||||||
|
WarnIfComponentBlockingGuard guard{item->component, now};
|
||||||
uint32_t now_ms = millis();
|
|
||||||
WarnIfComponentBlockingGuard guard{item->component, now_ms};
|
|
||||||
item->callback();
|
item->callback();
|
||||||
guard.finish();
|
guard.finish();
|
||||||
}
|
}
|
||||||
@ -486,15 +482,15 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
|||||||
return total_cancelled > 0;
|
return total_cancelled > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t Scheduler::millis_() {
|
uint64_t Scheduler::millis_64_(uint32_t now) {
|
||||||
// Get the current 32-bit millis value
|
|
||||||
const uint32_t now = millis();
|
|
||||||
// Check for rollover by comparing with last value
|
// Check for rollover by comparing with last value
|
||||||
if (now < this->last_millis_) {
|
if (now < this->last_millis_) {
|
||||||
// Detected rollover (happens every ~49.7 days)
|
// Detected rollover (happens every ~49.7 days)
|
||||||
this->millis_major_++;
|
this->millis_major_++;
|
||||||
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms",
|
ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms",
|
||||||
now + (static_cast<uint64_t>(this->millis_major_) << 32));
|
now + (static_cast<uint64_t>(this->millis_major_) << 32));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
this->last_millis_ = now;
|
this->last_millis_ = now;
|
||||||
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
||||||
|
@ -52,9 +52,9 @@ class Scheduler {
|
|||||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
||||||
bool cancel_retry(Component *component, const std::string &name);
|
bool cancel_retry(Component *component, const std::string &name);
|
||||||
|
|
||||||
optional<uint32_t> next_schedule_in();
|
optional<uint32_t> next_schedule_in(uint32_t now);
|
||||||
|
|
||||||
void call();
|
void call(uint32_t now);
|
||||||
|
|
||||||
void process_to_add();
|
void process_to_add();
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ class Scheduler {
|
|||||||
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
|
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
|
||||||
uint32_t delay, std::function<void()> func);
|
uint32_t delay, std::function<void()> func);
|
||||||
|
|
||||||
uint64_t millis_();
|
uint64_t millis_64_(uint32_t now);
|
||||||
void cleanup_();
|
void cleanup_();
|
||||||
void pop_raw_();
|
void pop_raw_();
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ class Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to execute a scheduler item
|
// Helper to execute a scheduler item
|
||||||
void execute_item_(SchedulerItem *item);
|
void execute_item_(SchedulerItem *item, uint32_t now);
|
||||||
|
|
||||||
// Helper to check if item should be skipped
|
// Helper to check if item should be skipped
|
||||||
bool should_skip_item_(const SchedulerItem *item) const {
|
bool should_skip_item_(const SchedulerItem *item) const {
|
||||||
|
@ -163,7 +163,7 @@ def get_ini_content():
|
|||||||
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
||||||
|
|
||||||
# Add extra script for C++ flags
|
# Add extra script for C++ flags
|
||||||
CORE.add_platformio_option("extra_scripts", ["pre:cxx_flags.py"])
|
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||||
|
|
||||||
content = "[platformio]\n"
|
content = "[platformio]\n"
|
||||||
content += f"description = ESPHome {__version__}\n"
|
content += f"description = ESPHome {__version__}\n"
|
||||||
@ -402,14 +402,18 @@ def write_gitignore():
|
|||||||
f.write(GITIGNORE_CONTENT)
|
f.write(GITIGNORE_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
CXX_FLAGS_SCRIPT = """# Auto-generated ESPHome script for C++ specific compiler flags
|
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
|
||||||
|
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
|
||||||
Import("env")
|
Import("env")
|
||||||
|
|
||||||
# Add C++ specific warning flags
|
# Add C++ specific flags
|
||||||
env.Append(CXXFLAGS=["-Wno-volatile"])
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def write_cxx_flags_script() -> None:
|
def write_cxx_flags_script() -> None:
|
||||||
path = CORE.relative_build_path("cxx_flags.py")
|
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||||
write_file_if_changed(path, CXX_FLAGS_SCRIPT)
|
contents = CXX_FLAGS_FILE_CONTENTS
|
||||||
|
if not CORE.is_host:
|
||||||
|
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||||
|
contents += "\n"
|
||||||
|
write_file_if_changed(path, contents)
|
||||||
|
@ -62,26 +62,6 @@ def get_clang_tidy_version_from_requirements() -> str:
|
|||||||
return "clang-tidy version not found"
|
return "clang-tidy version not found"
|
||||||
|
|
||||||
|
|
||||||
def extract_platformio_flags() -> str:
|
|
||||||
"""Extract clang-tidy related flags from platformio.ini"""
|
|
||||||
flags: list[str] = []
|
|
||||||
in_clangtidy_section = False
|
|
||||||
|
|
||||||
platformio_path = Path(__file__).parent.parent / "platformio.ini"
|
|
||||||
lines = read_file_lines(platformio_path)
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith("[flags:clangtidy]"):
|
|
||||||
in_clangtidy_section = True
|
|
||||||
continue
|
|
||||||
elif line.startswith("[") and in_clangtidy_section:
|
|
||||||
break
|
|
||||||
elif in_clangtidy_section and line and not line.startswith("#"):
|
|
||||||
flags.append(line)
|
|
||||||
|
|
||||||
return "\n".join(sorted(flags))
|
|
||||||
|
|
||||||
|
|
||||||
def read_file_bytes(path: Path) -> bytes:
|
def read_file_bytes(path: Path) -> bytes:
|
||||||
"""Read bytes from a file."""
|
"""Read bytes from a file."""
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
@ -101,9 +81,10 @@ def calculate_clang_tidy_hash() -> str:
|
|||||||
version = get_clang_tidy_version_from_requirements()
|
version = get_clang_tidy_version_from_requirements()
|
||||||
hasher.update(version.encode())
|
hasher.update(version.encode())
|
||||||
|
|
||||||
# Hash relevant platformio.ini sections
|
# Hash the entire platformio.ini file
|
||||||
pio_flags = extract_platformio_flags()
|
platformio_path = Path(__file__).parent.parent / "platformio.ini"
|
||||||
hasher.update(pio_flags.encode())
|
platformio_content = read_file_bytes(platformio_path)
|
||||||
|
hasher.update(platformio_content)
|
||||||
|
|
||||||
return hasher.hexdigest()
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
|
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/tests"]
|
CODEOWNERS = ["@esphome/tests"]
|
||||||
|
|
||||||
@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon
|
|||||||
LoopTestISRComponent = loop_test_component_ns.class_(
|
LoopTestISRComponent = loop_test_component_ns.class_(
|
||||||
"LoopTestISRComponent", cg.Component
|
"LoopTestISRComponent", cg.Component
|
||||||
)
|
)
|
||||||
|
LoopTestUpdateComponent = loop_test_component_ns.class_(
|
||||||
|
"LoopTestUpdateComponent", cg.PollingComponent
|
||||||
|
)
|
||||||
|
|
||||||
CONF_DISABLE_AFTER = "disable_after"
|
CONF_DISABLE_AFTER = "disable_after"
|
||||||
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
||||||
CONF_ISR_COMPONENTS = "isr_components"
|
CONF_ISR_COMPONENTS = "isr_components"
|
||||||
|
CONF_UPDATE_COMPONENTS = "update_components"
|
||||||
|
CONF_DISABLE_LOOP_AFTER = "disable_loop_after"
|
||||||
|
|
||||||
COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent),
|
||||||
|
cv.Required(CONF_NAME): cv.string,
|
||||||
|
cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_,
|
||||||
|
cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
||||||
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
|
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
|
||||||
cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA),
|
cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA),
|
||||||
|
cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list(
|
||||||
|
UPDATE_COMPONENT_CONFIG_SCHEMA
|
||||||
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
@ -94,3 +111,12 @@ async def to_code(config):
|
|||||||
var = cg.new_Pvariable(isr_config[CONF_ID])
|
var = cg.new_Pvariable(isr_config[CONF_ID])
|
||||||
await cg.register_component(var, isr_config)
|
await cg.register_component(var, isr_config)
|
||||||
cg.add(var.set_name(isr_config[CONF_NAME]))
|
cg.add(var.set_name(isr_config[CONF_NAME]))
|
||||||
|
|
||||||
|
# Create update test components
|
||||||
|
for update_config in config.get(CONF_UPDATE_COMPONENTS, []):
|
||||||
|
var = cg.new_Pvariable(update_config[CONF_ID])
|
||||||
|
await cg.register_component(var, update_config)
|
||||||
|
|
||||||
|
cg.add(var.set_name(update_config[CONF_NAME]))
|
||||||
|
cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER]))
|
||||||
|
cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL]))
|
||||||
|
@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() {
|
|||||||
this->disable_loop();
|
this->disable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoopTestUpdateComponent implementation
|
||||||
|
void LoopTestUpdateComponent::setup() {
|
||||||
|
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoopTestUpdateComponent::loop() {
|
||||||
|
this->loop_count_++;
|
||||||
|
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_);
|
||||||
|
|
||||||
|
// Disable loop after specified count to test component.update when loop is disabled
|
||||||
|
if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) {
|
||||||
|
ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_);
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoopTestUpdateComponent::update() {
|
||||||
|
this->update_count_++;
|
||||||
|
// Check if loop is disabled by testing component state
|
||||||
|
bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE;
|
||||||
|
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(),
|
||||||
|
this->update_count_, loop_disabled ? "YES" : "NO");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace loop_test_component
|
} // namespace loop_test_component
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace loop_test_component {
|
namespace loop_test_component {
|
||||||
@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> {
|
|||||||
LoopTestComponent *parent_;
|
LoopTestComponent *parent_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Component with update() method to test component.update action
|
||||||
|
class LoopTestUpdateComponent : public PollingComponent {
|
||||||
|
public:
|
||||||
|
LoopTestUpdateComponent() : PollingComponent(1000) {} // Default 1s update interval
|
||||||
|
|
||||||
|
void set_name(const std::string &name) { this->name_ = name; }
|
||||||
|
void set_disable_loop_after(int count) { this->disable_loop_after_ = count; }
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
int get_update_count() const { return this->update_count_; }
|
||||||
|
int get_loop_count() const { return this->loop_count_; }
|
||||||
|
|
||||||
|
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string name_;
|
||||||
|
int loop_count_{0};
|
||||||
|
int update_count_{0};
|
||||||
|
int disable_loop_after_{0};
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace loop_test_component
|
} // namespace loop_test_component
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -40,6 +40,13 @@ loop_test_component:
|
|||||||
- id: isr_test
|
- id: isr_test
|
||||||
name: "isr_test"
|
name: "isr_test"
|
||||||
|
|
||||||
|
# Update test component to test component.update when loop is disabled
|
||||||
|
update_components:
|
||||||
|
- id: update_test_component
|
||||||
|
name: "update_test"
|
||||||
|
disable_loop_after: 3 # Disable loop after 3 iterations
|
||||||
|
update_interval: 0.1s # Fast update interval for testing
|
||||||
|
|
||||||
# Interval to re-enable the self_disable_10 component after some time
|
# Interval to re-enable the self_disable_10 component after some time
|
||||||
interval:
|
interval:
|
||||||
- interval: 0.5s
|
- interval: 0.5s
|
||||||
@ -51,3 +58,28 @@ interval:
|
|||||||
- logger.log: "Re-enabling self_disable_10 via service"
|
- logger.log: "Re-enabling self_disable_10 via service"
|
||||||
- loop_test_component.enable:
|
- loop_test_component.enable:
|
||||||
id: self_disable_10
|
id: self_disable_10
|
||||||
|
|
||||||
|
# Test component.update on a component with disabled loop
|
||||||
|
- interval: 0.1s
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
static bool manual_update_done = false;
|
||||||
|
if (!manual_update_done &&
|
||||||
|
id(update_test_component).get_loop_count() == 3 &&
|
||||||
|
id(update_test_component).get_update_count() >= 3) {
|
||||||
|
ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop");
|
||||||
|
manual_update_done = true;
|
||||||
|
}
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
static bool manual_update_triggered = false;
|
||||||
|
if (!manual_update_triggered &&
|
||||||
|
id(update_test_component).get_loop_count() == 3 &&
|
||||||
|
id(update_test_component).get_update_count() >= 3) {
|
||||||
|
manual_update_triggered = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
then:
|
||||||
|
- component.update: update_test_component
|
||||||
|
@ -45,11 +45,18 @@ async def test_loop_disable_enable(
|
|||||||
isr_component_disabled = asyncio.Event()
|
isr_component_disabled = asyncio.Event()
|
||||||
isr_component_re_enabled = asyncio.Event()
|
isr_component_re_enabled = asyncio.Event()
|
||||||
isr_component_pure_re_enabled = asyncio.Event()
|
isr_component_pure_re_enabled = asyncio.Event()
|
||||||
|
# Events for update component testing
|
||||||
|
update_component_loop_disabled = asyncio.Event()
|
||||||
|
update_component_manual_update_called = asyncio.Event()
|
||||||
|
|
||||||
# Track loop counts for components
|
# Track loop counts for components
|
||||||
self_disable_10_counts: list[int] = []
|
self_disable_10_counts: list[int] = []
|
||||||
normal_component_counts: list[int] = []
|
normal_component_counts: list[int] = []
|
||||||
isr_component_counts: list[int] = []
|
isr_component_counts: list[int] = []
|
||||||
|
# Track update component behavior
|
||||||
|
update_component_loop_count = 0
|
||||||
|
update_component_update_count = 0
|
||||||
|
update_component_manual_update_count = 0
|
||||||
|
|
||||||
def on_log_line(line: str) -> None:
|
def on_log_line(line: str) -> None:
|
||||||
"""Process each log line from the process output."""
|
"""Process each log line from the process output."""
|
||||||
@ -59,6 +66,7 @@ async def test_loop_disable_enable(
|
|||||||
if (
|
if (
|
||||||
"loop_test_component" not in clean_line
|
"loop_test_component" not in clean_line
|
||||||
and "loop_test_isr_component" not in clean_line
|
and "loop_test_isr_component" not in clean_line
|
||||||
|
and "Manually calling component.update" not in clean_line
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -112,6 +120,23 @@ async def test_loop_disable_enable(
|
|||||||
elif "Running after pure ISR re-enable!" in clean_line:
|
elif "Running after pure ISR re-enable!" in clean_line:
|
||||||
isr_component_pure_re_enabled.set()
|
isr_component_pure_re_enabled.set()
|
||||||
|
|
||||||
|
# Update component events
|
||||||
|
elif "[update_test]" in clean_line:
|
||||||
|
if "LoopTestUpdateComponent loop count:" in clean_line:
|
||||||
|
nonlocal update_component_loop_count
|
||||||
|
update_component_loop_count = int(
|
||||||
|
clean_line.split("LoopTestUpdateComponent loop count: ")[1]
|
||||||
|
)
|
||||||
|
elif "LoopTestUpdateComponent update() called" in clean_line:
|
||||||
|
nonlocal update_component_update_count
|
||||||
|
update_component_update_count += 1
|
||||||
|
if "Manually calling component.update" in " ".join(log_messages[-5:]):
|
||||||
|
nonlocal update_component_manual_update_count
|
||||||
|
update_component_manual_update_count += 1
|
||||||
|
update_component_manual_update_called.set()
|
||||||
|
elif "Disabling loop after" in clean_line:
|
||||||
|
update_component_loop_disabled.set()
|
||||||
|
|
||||||
# Write, compile and run the ESPHome device with log callback
|
# Write, compile and run the ESPHome device with log callback
|
||||||
async with (
|
async with (
|
||||||
run_compiled(yaml_config, line_callback=on_log_line),
|
run_compiled(yaml_config, line_callback=on_log_line),
|
||||||
@ -205,3 +230,28 @@ async def test_loop_disable_enable(
|
|||||||
assert final_count > 10, (
|
assert final_count > 10, (
|
||||||
f"Component didn't run after pure ISR enable: got {final_count} counts total"
|
f"Component didn't run after pure ISR enable: got {final_count} counts total"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test component.update functionality when loop is disabled
|
||||||
|
# Wait for update component to disable its loop
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("Update component did not disable its loop within 3 seconds")
|
||||||
|
|
||||||
|
# Verify it ran exactly 3 loops before disabling
|
||||||
|
assert update_component_loop_count == 3, (
|
||||||
|
f"Expected 3 loop iterations before disable, got {update_component_loop_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for manual component.update to be called
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(
|
||||||
|
update_component_manual_update_called.wait(), timeout=5.0
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("Manual component.update was not called within 5 seconds")
|
||||||
|
|
||||||
|
# The key test: verify that manual component.update worked after loop was disabled
|
||||||
|
assert update_component_manual_update_count >= 1, (
|
||||||
|
"component.update did not fire after loop was disabled"
|
||||||
|
)
|
||||||
|
@ -44,67 +44,36 @@ def test_get_clang_tidy_version_from_requirements(
|
|||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("platformio_content", "expected_flags"),
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"[env:esp32]\n"
|
|
||||||
"platform = espressif32\n"
|
|
||||||
"\n"
|
|
||||||
"[flags:clangtidy]\n"
|
|
||||||
"build_flags = -Wall\n"
|
|
||||||
"extra_flags = -Wextra\n"
|
|
||||||
"\n"
|
|
||||||
"[env:esp8266]\n",
|
|
||||||
"build_flags = -Wall\nextra_flags = -Wextra",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"[flags:clangtidy]\n# Comment line\nbuild_flags = -O2\n\n[next_section]\n",
|
|
||||||
"build_flags = -O2",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"[flags:clangtidy]\nflag_c = -std=c99\nflag_b = -Wall\nflag_a = -O2\n",
|
|
||||||
"flag_a = -O2\nflag_b = -Wall\nflag_c = -std=c99", # Sorted
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"[env:esp32]\nplatform = espressif32\n", # No clangtidy section
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_extract_platformio_flags(platformio_content: str, expected_flags: str) -> None:
|
|
||||||
"""Test extracting clang-tidy flags from platformio.ini."""
|
|
||||||
# Mock read_file_lines to return our test content
|
|
||||||
with patch("clang_tidy_hash.read_file_lines") as mock_read:
|
|
||||||
mock_read.return_value = platformio_content.splitlines(keepends=True)
|
|
||||||
|
|
||||||
result = clang_tidy_hash.extract_platformio_flags()
|
|
||||||
|
|
||||||
assert result == expected_flags
|
|
||||||
|
|
||||||
|
|
||||||
def test_calculate_clang_tidy_hash() -> None:
|
def test_calculate_clang_tidy_hash() -> None:
|
||||||
"""Test calculating hash from all configuration sources."""
|
"""Test calculating hash from all configuration sources."""
|
||||||
clang_tidy_content = b"Checks: '-*,readability-*'\n"
|
clang_tidy_content = b"Checks: '-*,readability-*'\n"
|
||||||
requirements_version = "clang-tidy==18.1.5"
|
requirements_version = "clang-tidy==18.1.5"
|
||||||
pio_flags = "build_flags = -Wall"
|
platformio_content = b"[env:esp32]\nplatform = espressif32\n"
|
||||||
|
|
||||||
# Expected hash calculation
|
# Expected hash calculation
|
||||||
expected_hasher = hashlib.sha256()
|
expected_hasher = hashlib.sha256()
|
||||||
expected_hasher.update(clang_tidy_content)
|
expected_hasher.update(clang_tidy_content)
|
||||||
expected_hasher.update(requirements_version.encode())
|
expected_hasher.update(requirements_version.encode())
|
||||||
expected_hasher.update(pio_flags.encode())
|
expected_hasher.update(platformio_content)
|
||||||
expected_hash = expected_hasher.hexdigest()
|
expected_hash = expected_hasher.hexdigest()
|
||||||
|
|
||||||
# Mock the dependencies
|
# Mock the dependencies
|
||||||
with (
|
with (
|
||||||
patch("clang_tidy_hash.read_file_bytes", return_value=clang_tidy_content),
|
patch("clang_tidy_hash.read_file_bytes") as mock_read_bytes,
|
||||||
patch(
|
patch(
|
||||||
"clang_tidy_hash.get_clang_tidy_version_from_requirements",
|
"clang_tidy_hash.get_clang_tidy_version_from_requirements",
|
||||||
return_value=requirements_version,
|
return_value=requirements_version,
|
||||||
),
|
),
|
||||||
patch("clang_tidy_hash.extract_platformio_flags", return_value=pio_flags),
|
|
||||||
):
|
):
|
||||||
|
# Set up mock to return different content based on the file being read
|
||||||
|
def read_file_mock(path: Path) -> bytes:
|
||||||
|
if ".clang-tidy" in str(path):
|
||||||
|
return clang_tidy_content
|
||||||
|
elif "platformio.ini" in str(path):
|
||||||
|
return platformio_content
|
||||||
|
return b""
|
||||||
|
|
||||||
|
mock_read_bytes.side_effect = read_file_mock
|
||||||
result = clang_tidy_hash.calculate_clang_tidy_hash()
|
result = clang_tidy_hash.calculate_clang_tidy_hash()
|
||||||
|
|
||||||
assert result == expected_hash
|
assert result == expected_hash
|
||||||
|
Loading…
x
Reference in New Issue
Block a user