mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		| @@ -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. |  | ||||||
|   | |||||||
| @@ -31,7 +31,6 @@ APIServer::APIServer() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void APIServer::setup() { | void APIServer::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Running setup"); |  | ||||||
|   this->setup_controller(); |   this->setup_controller(); | ||||||
|  |  | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
| @@ -205,16 +204,16 @@ void APIServer::loop() { | |||||||
|  |  | ||||||
| void APIServer::dump_config() { | void APIServer::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
|                 "API Server:\n" |                 "Server:\n" | ||||||
|                 "  Address: %s:%u", |                 "  Address: %s:%u", | ||||||
|                 network::get_use_address().c_str(), this->port_); |                 network::get_use_address().c_str(), this->port_); | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
|   ESP_LOGCONFIG(TAG, "  Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); |   ESP_LOGCONFIG(TAG, "  Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); | ||||||
|   if (!this->noise_ctx_->has_psk()) { |   if (!this->noise_ctx_->has_psk()) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Supports noise encryption: YES"); |     ESP_LOGCONFIG(TAG, "  Supports encryption: YES"); | ||||||
|   } |   } | ||||||
| #else | #else | ||||||
|   ESP_LOGCONFIG(TAG, "  Using noise encryption: NO"); |   ESP_LOGCONFIG(TAG, "  Noise encryption: NO"); | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user