mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'dev' into select_options_fixed
This commit is contained in:
		| @@ -1 +1 @@ | |||||||
| d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248 | 3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -53,6 +53,7 @@ jobs: | |||||||
|               'new-target-platform', |               'new-target-platform', | ||||||
|               'merging-to-release', |               'merging-to-release', | ||||||
|               'merging-to-beta', |               'merging-to-beta', | ||||||
|  |               'chained-pr', | ||||||
|               'core', |               'core', | ||||||
|               'small-pr', |               'small-pr', | ||||||
|               'dashboard', |               'dashboard', | ||||||
| @@ -140,6 +141,8 @@ jobs: | |||||||
|                 labels.add('merging-to-release'); |                 labels.add('merging-to-release'); | ||||||
|               } else if (baseRef === 'beta') { |               } else if (baseRef === 'beta') { | ||||||
|                 labels.add('merging-to-beta'); |                 labels.add('merging-to-beta'); | ||||||
|  |               } else if (baseRef !== 'dev') { | ||||||
|  |                 labels.add('chained-pr'); | ||||||
|               } |               } | ||||||
|  |  | ||||||
|               return labels; |               return labels; | ||||||
| @@ -528,8 +531,8 @@ jobs: | |||||||
|             const apiData = await fetchApiData(); |             const apiData = await fetchApiData(); | ||||||
|             const baseRef = context.payload.pull_request.base.ref; |             const baseRef = context.payload.pull_request.base.ref; | ||||||
|  |  | ||||||
|             // Early exit for non-dev branches |             // Early exit for release and beta branches only | ||||||
|             if (baseRef !== 'dev') { |             if (baseRef === 'release' || baseRef === 'beta') { | ||||||
|               const branchLabels = await detectMergeBranch(); |               const branchLabels = await detectMergeBranch(); | ||||||
|               const finalLabels = Array.from(branchLabels); |               const finalLabels = Array.from(branchLabels); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -178,6 +178,8 @@ jobs: | |||||||
|       component-test-count: ${{ steps.determine.outputs.component-test-count }} |       component-test-count: ${{ steps.determine.outputs.component-test-count }} | ||||||
|       changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }} |       changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }} | ||||||
|       memory_impact: ${{ steps.determine.outputs.memory-impact }} |       memory_impact: ${{ steps.determine.outputs.memory-impact }} | ||||||
|  |       cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} | ||||||
|  |       cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
| @@ -210,6 +212,8 @@ jobs: | |||||||
|           echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT |           echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT | ||||||
|           echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT |           echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT | ||||||
|           echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT |           echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT | ||||||
|  |           echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT | ||||||
|  |           echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT | ||||||
|  |  | ||||||
|   integration-tests: |   integration-tests: | ||||||
|     name: Run integration tests |     name: Run integration tests | ||||||
| @@ -247,6 +251,33 @@ jobs: | |||||||
|           . venv/bin/activate |           . venv/bin/activate | ||||||
|           pytest -vv --no-cov --tb=native -n auto tests/integration/ |           pytest -vv --no-cov --tb=native -n auto tests/integration/ | ||||||
|  |  | ||||||
|  |   cpp-unit-tests: | ||||||
|  |     name: Run C++ unit tests | ||||||
|  |     runs-on: ubuntu-24.04 | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |       - determine-jobs | ||||||
|  |     if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|  |  | ||||||
|  |       - name: Restore Python | ||||||
|  |         uses: ./.github/actions/restore-python | ||||||
|  |         with: | ||||||
|  |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|  |           cache-key: ${{ needs.common.outputs.cache-key }} | ||||||
|  |  | ||||||
|  |       - name: Run cpp_unit_test.py | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then | ||||||
|  |             script/cpp_unit_test.py --all | ||||||
|  |           else | ||||||
|  |             ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs) | ||||||
|  |             script/cpp_unit_test.py $ARGS | ||||||
|  |           fi | ||||||
|  |  | ||||||
|   clang-tidy-single: |   clang-tidy-single: | ||||||
|     name: ${{ matrix.name }} |     name: ${{ matrix.name }} | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,6 +14,7 @@ jobs: | |||||||
|         label: |         label: | ||||||
|           - needs-docs |           - needs-docs | ||||||
|           - merge-after-release |           - merge-after-release | ||||||
|  |           - chained-pr | ||||||
|     steps: |     steps: | ||||||
|       - name: Check for ${{ matrix.label }} label |       - name: Check for ${{ matrix.label }} label | ||||||
|         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 |         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | ||||||
|   | |||||||
| @@ -550,6 +550,32 @@ CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | |||||||
| CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" | CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" | ||||||
| CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" | CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" | ||||||
| CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" | CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" | ||||||
|  | CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" | ||||||
|  | CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" | ||||||
|  | CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" | ||||||
|  |  | ||||||
|  | # VFS requirement tracking | ||||||
|  | # Components that need VFS features can call require_vfs_select() or require_vfs_dir() | ||||||
|  | KEY_VFS_SELECT_REQUIRED = "vfs_select_required" | ||||||
|  | KEY_VFS_DIR_REQUIRED = "vfs_dir_required" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def require_vfs_select() -> None: | ||||||
|  |     """Mark that VFS select support is required by a component. | ||||||
|  |  | ||||||
|  |     Call this from components that use esp_vfs_eventfd or other VFS select features. | ||||||
|  |     This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled. | ||||||
|  |     """ | ||||||
|  |     CORE.data[KEY_VFS_SELECT_REQUIRED] = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def require_vfs_dir() -> None: | ||||||
|  |     """Mark that VFS directory support is required by a component. | ||||||
|  |  | ||||||
|  |     Call this from components that use directory functions (opendir, readdir, mkdir, etc.). | ||||||
|  |     This prevents CONFIG_VFS_SUPPORT_DIR from being disabled. | ||||||
|  |     """ | ||||||
|  |     CORE.data[KEY_VFS_DIR_REQUIRED] = True | ||||||
|  |  | ||||||
|  |  | ||||||
| def _validate_idf_component(config: ConfigType) -> ConfigType: | def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||||
| @@ -615,6 +641,13 @@ FRAMEWORK_SCHEMA = cv.All( | |||||||
|                     cv.Optional( |                     cv.Optional( | ||||||
|                         CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True |                         CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True | ||||||
|                     ): cv.boolean, |                     ): cv.boolean, | ||||||
|  |                     cv.Optional( | ||||||
|  |                         CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True | ||||||
|  |                     ): cv.boolean, | ||||||
|  |                     cv.Optional( | ||||||
|  |                         CONF_DISABLE_VFS_SUPPORT_SELECT, default=True | ||||||
|  |                     ): cv.boolean, | ||||||
|  |                     cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, | ||||||
|                     cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, |                     cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, | ||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
| @@ -962,6 +995,43 @@ async def to_code(config): | |||||||
|     if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): |     if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): | ||||||
|         add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) |         add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) | ||||||
|  |  | ||||||
|  |     # Disable VFS support for termios (terminal I/O functions) | ||||||
|  |     # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). | ||||||
|  |     # Saves approximately 1.8KB of flash when disabled (default). | ||||||
|  |     add_idf_sdkconfig_option( | ||||||
|  |         "CONFIG_VFS_SUPPORT_TERMIOS", | ||||||
|  |         not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Disable VFS support for select() with file descriptors | ||||||
|  |     # ESPHome only uses select() with sockets via lwip_select(), which still works. | ||||||
|  |     # VFS select is only needed for UART/eventfd file descriptors. | ||||||
|  |     # Components that need it (e.g., openthread) call require_vfs_select(). | ||||||
|  |     # Saves approximately 2.7KB of flash when disabled (default). | ||||||
|  |     if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False): | ||||||
|  |         # Component requires VFS select - force enable regardless of user setting | ||||||
|  |         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True) | ||||||
|  |     else: | ||||||
|  |         # No component needs it - allow user to control (default: disabled) | ||||||
|  |         add_idf_sdkconfig_option( | ||||||
|  |             "CONFIG_VFS_SUPPORT_SELECT", | ||||||
|  |             not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) | ||||||
|  |     # ESPHome doesn't use directory functions on ESP32. | ||||||
|  |     # Components that need it (e.g., storage components) call require_vfs_dir(). | ||||||
|  |     # Saves approximately 0.5KB+ of flash when disabled (default). | ||||||
|  |     if CORE.data.get(KEY_VFS_DIR_REQUIRED, False): | ||||||
|  |         # Component requires VFS directory support - force enable regardless of user setting | ||||||
|  |         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True) | ||||||
|  |     else: | ||||||
|  |         # No component needs it - allow user to control (default: disabled) | ||||||
|  |         add_idf_sdkconfig_option( | ||||||
|  |             "CONFIG_VFS_SUPPORT_DIR", | ||||||
|  |             not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     cg.add_platformio_option("board_build.partitions", "partitions.csv") |     cg.add_platformio_option("board_build.partitions", "partitions.csv") | ||||||
|     if CONF_PARTITIONS in config: |     if CONF_PARTITIONS in config: | ||||||
|         add_extra_build_file( |         add_extra_build_file( | ||||||
|   | |||||||
| @@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) { | |||||||
|   this->pin_->digital_write(state); |   this->pin_->digital_write(state); | ||||||
|   this->publish_state(state); |   this->publish_state(state); | ||||||
| } | } | ||||||
| void GPIOSwitch::set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; } | void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; } | ||||||
|  |  | ||||||
| }  // namespace gpio | }  // namespace gpio | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -2,10 +2,9 @@ | |||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/components/switch/switch.h" | #include "esphome/components/switch/switch.h" | ||||||
|  |  | ||||||
| #include <vector> |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace gpio { | namespace gpio { | ||||||
|  |  | ||||||
| @@ -19,14 +18,14 @@ class GPIOSwitch : public switch_::Switch, public Component { | |||||||
|  |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void set_interlock(const std::vector<Switch *> &interlock); |   void set_interlock(const std::initializer_list<Switch *> &interlock); | ||||||
|   void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } |   void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void write_state(bool state) override; |   void write_state(bool state) override; | ||||||
|  |  | ||||||
|   GPIOPin *pin_; |   GPIOPin *pin_; | ||||||
|   std::vector<Switch *> interlock_; |   FixedVector<Switch *> interlock_; | ||||||
|   uint32_t interlock_wait_time_{0}; |   uint32_t interlock_wait_time_{0}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,19 +17,19 @@ class ESPColorCorrection { | |||||||
|                  this->color_correct_blue(color.blue), this->color_correct_white(color.white)); |                  this->color_correct_blue(color.blue), this->color_correct_white(color.white)); | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); |     uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); |     uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); |     uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); |     uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { |   inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ from esphome.components.esp32 import ( | |||||||
|     VARIANT_ESP32H2, |     VARIANT_ESP32H2, | ||||||
|     add_idf_sdkconfig_option, |     add_idf_sdkconfig_option, | ||||||
|     only_on_variant, |     only_on_variant, | ||||||
|  |     require_vfs_select, | ||||||
| ) | ) | ||||||
| from esphome.components.mdns import MDNSComponent, enable_mdns_storage | from esphome.components.mdns import MDNSComponent, enable_mdns_storage | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| @@ -106,6 +107,14 @@ _CONNECTION_SCHEMA = cv.Schema( | |||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _require_vfs_select(config): | ||||||
|  |     """Register VFS select requirement during config validation.""" | ||||||
|  |     # OpenThread uses esp_vfs_eventfd which requires VFS select support | ||||||
|  |     require_vfs_select() | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -122,6 +131,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), |     cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), | ||||||
|     cv.only_with_esp_idf, |     cv.only_with_esp_idf, | ||||||
|     only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), |     only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), | ||||||
|  |     _require_vfs_select, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -56,6 +56,13 @@ uint32_t ESP8266UartComponent::get_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void ESP8266UartComponent::setup() { | void ESP8266UartComponent::setup() { | ||||||
|  |   if (this->rx_pin_) { | ||||||
|  |     this->rx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { | ||||||
|  |     this->tx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Use Arduino HardwareSerial UARTs if all used pins match the ones |   // Use Arduino HardwareSerial UARTs if all used pins match the ones | ||||||
|   // preconfigured by the platform. For example if RX disabled but TX pin |   // preconfigured by the platform. For example if RX disabled but TX pin | ||||||
|   // is 1 we still want to use Serial. |   // is 1 we still want to use Serial. | ||||||
|   | |||||||
| @@ -6,6 +6,9 @@ | |||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/gpio.h" | ||||||
|  | #include "driver/gpio.h" | ||||||
|  | #include "soc/gpio_num.h" | ||||||
|  |  | ||||||
| #ifdef USE_LOGGER | #ifdef USE_LOGGER | ||||||
| #include "esphome/components/logger/logger.h" | #include "esphome/components/logger/logger.h" | ||||||
| @@ -104,6 +107,13 @@ void IDFUARTComponent::load_settings(bool dump_config) { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (this->rx_pin_) { | ||||||
|  |     this->rx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { | ||||||
|  |     this->tx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; |   int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; | ||||||
|   int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; |   int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; | ||||||
|   int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; |   int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; | ||||||
|   | |||||||
| @@ -46,6 +46,13 @@ uint16_t LibreTinyUARTComponent::get_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void LibreTinyUARTComponent::setup() { | void LibreTinyUARTComponent::setup() { | ||||||
|  |   if (this->rx_pin_) { | ||||||
|  |     this->rx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { | ||||||
|  |     this->tx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); |   int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); | ||||||
|   int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); |   int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); | ||||||
|   bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); |   bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); | ||||||
|   | |||||||
| @@ -52,6 +52,13 @@ uint16_t RP2040UartComponent::get_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void RP2040UartComponent::setup() { | void RP2040UartComponent::setup() { | ||||||
|  |   if (this->rx_pin_) { | ||||||
|  |     this->rx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { | ||||||
|  |     this->tx_pin_->setup(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   uint16_t config = get_config(); |   uint16_t config = get_config(); | ||||||
|  |  | ||||||
|   constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); |   constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); | ||||||
|   | |||||||
| @@ -14,6 +14,15 @@ inline static constexpr uint8_t esp_scale8(uint8_t i, uint8_t scale) { | |||||||
|   return (uint16_t(i) * (1 + uint16_t(scale))) / 256; |   return (uint16_t(i) * (1 + uint16_t(scale))) / 256; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Scale an 8-bit value by two 8-bit scale factors with improved precision. | ||||||
|  | /// This is more accurate than calling esp_scale8() twice because it delays | ||||||
|  | /// truncation until after both multiplications, preserving intermediate precision. | ||||||
|  | /// For example: esp_scale8_twice(value, max_brightness, local_brightness) | ||||||
|  | /// gives better results than esp_scale8(esp_scale8(value, max_brightness), local_brightness) | ||||||
|  | inline static constexpr uint8_t esp_scale8_twice(uint8_t i, uint8_t scale1, uint8_t scale2) { | ||||||
|  |   return (uint32_t(i) * (1 + uint32_t(scale1)) * (1 + uint32_t(scale2))) >> 16; | ||||||
|  | } | ||||||
|  |  | ||||||
| struct Color { | struct Color { | ||||||
|   union { |   union { | ||||||
|     struct { |     struct { | ||||||
|   | |||||||
| @@ -46,6 +46,10 @@ lib_deps = | |||||||
|     ; This is using the repository until a new release is published to PlatformIO |     ; This is using the repository until a new release is published to PlatformIO | ||||||
|     https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library |     https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library | ||||||
|     lvgl/lvgl@8.4.0                                       ; lvgl |     lvgl/lvgl@8.4.0                                       ; lvgl | ||||||
|  |     ; This dependency is used only in unit tests. | ||||||
|  |     ; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py | ||||||
|  |     ; See scripts/cpp_unit_test.py and tests/components/README.md | ||||||
|  |     google/googletest@^1.15.2 | ||||||
| build_flags = | build_flags = | ||||||
|     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE |     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||||
|     -std=gnu++20 |     -std=gnu++20 | ||||||
|   | |||||||
							
								
								
									
										172
									
								
								script/cpp_unit_test.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										172
									
								
								script/cpp_unit_test.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  | import argparse | ||||||
|  | import hashlib | ||||||
|  | import os | ||||||
|  | from pathlib import Path | ||||||
|  | import subprocess | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | from helpers import get_all_components, get_all_dependencies, root_path | ||||||
|  |  | ||||||
|  | from esphome.__main__ import command_compile, parse_args | ||||||
|  | from esphome.config import validate_config | ||||||
|  | from esphome.core import CORE | ||||||
|  | from esphome.platformio_api import get_idedata | ||||||
|  |  | ||||||
|  | # This must coincide with the version in /platformio.ini | ||||||
|  | PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" | ||||||
|  |  | ||||||
|  | # Path to /tests/components | ||||||
|  | COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def hash_components(components: list[str]) -> str: | ||||||
|  |     key = ",".join(components) | ||||||
|  |     return hashlib.sha256(key.encode()).hexdigest()[:16] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def filter_components_without_tests(components: list[str]) -> list[str]: | ||||||
|  |     """Filter out components that do not have a corresponding test file. | ||||||
|  |  | ||||||
|  |     This is done by checking if the component's directory contains at | ||||||
|  |     least a .cpp file. | ||||||
|  |     """ | ||||||
|  |     filtered_components: list[str] = [] | ||||||
|  |     for component in components: | ||||||
|  |         test_dir = COMPONENTS_TESTS_DIR / component | ||||||
|  |         if test_dir.is_dir() and any(test_dir.glob("*.cpp")): | ||||||
|  |             filtered_components.append(component) | ||||||
|  |         else: | ||||||
|  |             print( | ||||||
|  |                 f"WARNING: No tests found for component '{component}', skipping.", | ||||||
|  |                 file=sys.stderr, | ||||||
|  |             ) | ||||||
|  |     return filtered_components | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_test_config(config_name: str, includes: list[str]) -> dict: | ||||||
|  |     """Create ESPHome test configuration for C++ unit tests. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         config_name: Unique name for this test configuration | ||||||
|  |         includes: List of include folders for the test build | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         Configuration dict for ESPHome | ||||||
|  |     """ | ||||||
|  |     return { | ||||||
|  |         "esphome": { | ||||||
|  |             "name": config_name, | ||||||
|  |             "friendly_name": "CPP Unit Tests", | ||||||
|  |             "libraries": PLATFORMIO_GOOGLE_TEST_LIB, | ||||||
|  |             "platformio_options": { | ||||||
|  |                 "build_type": "debug", | ||||||
|  |                 "build_unflags": [ | ||||||
|  |                     "-Os",  # remove size-opt flag | ||||||
|  |                 ], | ||||||
|  |                 "build_flags": [ | ||||||
|  |                     "-Og",  # optimize for debug | ||||||
|  |                 ], | ||||||
|  |                 "debug_build_flags": [  # only for debug builds | ||||||
|  |                     "-g3",  # max debug info | ||||||
|  |                     "-ggdb3", | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |             "includes": includes, | ||||||
|  |         }, | ||||||
|  |         "host": {}, | ||||||
|  |         "logger": {"level": "DEBUG"}, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def run_tests(selected_components: list[str]) -> int: | ||||||
|  |     # Skip tests on Windows | ||||||
|  |     if os.name == "nt": | ||||||
|  |         print("Skipping esphome tests on Windows", file=sys.stderr) | ||||||
|  |         return 1 | ||||||
|  |  | ||||||
|  |     # Remove components that do not have tests | ||||||
|  |     components = filter_components_without_tests(selected_components) | ||||||
|  |  | ||||||
|  |     if len(components) == 0: | ||||||
|  |         print( | ||||||
|  |             "No components specified or no tests found for the specified components.", | ||||||
|  |             file=sys.stderr, | ||||||
|  |         ) | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     components = sorted(components) | ||||||
|  |  | ||||||
|  |     # Obtain possible dependencies for the requested components: | ||||||
|  |     components_with_dependencies = sorted(get_all_dependencies(set(components))) | ||||||
|  |  | ||||||
|  |     # Build a list of include folders, one folder per component containing tests. | ||||||
|  |     # A special replacement main.cpp is located in /tests/components/main.cpp | ||||||
|  |     includes: list[str] = ["main.cpp"] + components | ||||||
|  |  | ||||||
|  |     # Create a unique name for this config based on the actual components being tested | ||||||
|  |     # to maximize cache during testing | ||||||
|  |     config_name: str = "cpptests-" + hash_components(components) | ||||||
|  |  | ||||||
|  |     config = create_test_config(config_name, includes) | ||||||
|  |  | ||||||
|  |     CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" | ||||||
|  |     CORE.dashboard = None | ||||||
|  |  | ||||||
|  |     # Validate config will expand the above with defaults: | ||||||
|  |     config = validate_config(config, {}) | ||||||
|  |  | ||||||
|  |     # Add all components and dependencies to the base configuration after validation, so their files | ||||||
|  |     # are added to the build. | ||||||
|  |     config.update({key: {} for key in components_with_dependencies}) | ||||||
|  |  | ||||||
|  |     print(f"Testing components: {', '.join(components)}") | ||||||
|  |     CORE.config = config | ||||||
|  |     args = parse_args(["program", "compile", str(CORE.config_path)]) | ||||||
|  |     try: | ||||||
|  |         exit_code: int = command_compile(args, config) | ||||||
|  |  | ||||||
|  |         if exit_code != 0: | ||||||
|  |             print(f"Error compiling unit tests for {', '.join(components)}") | ||||||
|  |             return exit_code | ||||||
|  |     except Exception as e: | ||||||
|  |         print( | ||||||
|  |             f"Error compiling unit tests for {', '.join(components)}. Check path. : {e}" | ||||||
|  |         ) | ||||||
|  |         return 2 | ||||||
|  |  | ||||||
|  |     # After a successful compilation, locate the executable and run it: | ||||||
|  |     idedata = get_idedata(config) | ||||||
|  |     if idedata is None: | ||||||
|  |         print("Cannot find executable") | ||||||
|  |         return 1 | ||||||
|  |  | ||||||
|  |     program_path: str = idedata.raw["prog_path"] | ||||||
|  |     run_cmd: list[str] = [program_path] | ||||||
|  |     run_proc = subprocess.run(run_cmd, check=False) | ||||||
|  |     return run_proc.returncode | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main() -> None: | ||||||
|  |     parser = argparse.ArgumentParser( | ||||||
|  |         description="Run C++ unit tests for ESPHome components." | ||||||
|  |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "components", | ||||||
|  |         nargs="*", | ||||||
|  |         help="List of components to test. Use --all to test all known components.", | ||||||
|  |     ) | ||||||
|  |     parser.add_argument("--all", action="store_true", help="Test all known components.") | ||||||
|  |  | ||||||
|  |     args = parser.parse_args() | ||||||
|  |  | ||||||
|  |     if args.all: | ||||||
|  |         components: list[str] = get_all_components() | ||||||
|  |     else: | ||||||
|  |         components: list[str] = args.components | ||||||
|  |  | ||||||
|  |     sys.exit(run_tests(components)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
| @@ -52,13 +52,16 @@ from helpers import ( | |||||||
|     CPP_FILE_EXTENSIONS, |     CPP_FILE_EXTENSIONS, | ||||||
|     PYTHON_FILE_EXTENSIONS, |     PYTHON_FILE_EXTENSIONS, | ||||||
|     changed_files, |     changed_files, | ||||||
|     filter_component_files, |     core_changed, | ||||||
|  |     filter_component_and_test_cpp_files, | ||||||
|  |     filter_component_and_test_files, | ||||||
|     get_all_dependencies, |     get_all_dependencies, | ||||||
|     get_changed_components, |     get_changed_components, | ||||||
|     get_component_from_path, |     get_component_from_path, | ||||||
|     get_component_test_files, |     get_component_test_files, | ||||||
|     get_components_from_integration_fixtures, |     get_components_from_integration_fixtures, | ||||||
|     get_components_with_dependencies, |     get_components_with_dependencies, | ||||||
|  |     get_cpp_changed_components, | ||||||
|     git_ls_files, |     git_ls_files, | ||||||
|     parse_test_filename, |     parse_test_filename, | ||||||
|     root_path, |     root_path, | ||||||
| @@ -143,10 +146,9 @@ def should_run_integration_tests(branch: str | None = None) -> bool: | |||||||
|     """ |     """ | ||||||
|     files = changed_files(branch) |     files = changed_files(branch) | ||||||
|  |  | ||||||
|     # Check if any core files changed (esphome/core/*) |     if core_changed(files): | ||||||
|     for file in files: |         # If any core files changed, run integration tests | ||||||
|         if file.startswith("esphome/core/"): |         return True | ||||||
|             return True |  | ||||||
|  |  | ||||||
|     # Check if any integration test files changed |     # Check if any integration test files changed | ||||||
|     if any("tests/integration" in file for file in files): |     if any("tests/integration" in file for file in files): | ||||||
| @@ -283,6 +285,40 @@ def should_run_python_linters(branch: str | None = None) -> bool: | |||||||
|     return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS) |     return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def determine_cpp_unit_tests( | ||||||
|  |     branch: str | None = None, | ||||||
|  | ) -> tuple[bool, list[str]]: | ||||||
|  |     """Determine if C++ unit tests should run based on changed files. | ||||||
|  |  | ||||||
|  |     This function is used by the CI workflow to skip C++ unit tests when | ||||||
|  |     no relevant files have changed, saving CI time and resources. | ||||||
|  |  | ||||||
|  |     C++ unit tests will run when any of the following conditions are met: | ||||||
|  |  | ||||||
|  |     1. Any C++ core source files changed (esphome/core/*), in which case | ||||||
|  |        all cpp unit tests run. | ||||||
|  |     2. A test file for a component changed, which triggers tests for that | ||||||
|  |        component. | ||||||
|  |     3. The code for a component changed, which triggers tests for that | ||||||
|  |        component and all components that depend on it. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         branch: Branch to compare against. If None, uses default. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         Tuple of (run_all, components) where: | ||||||
|  |         - run_all: True if all tests should run, False otherwise | ||||||
|  |         - components: List of specific components to test (empty if run_all) | ||||||
|  |     """ | ||||||
|  |     files = changed_files(branch) | ||||||
|  |     if core_changed(files): | ||||||
|  |         return (True, []) | ||||||
|  |  | ||||||
|  |     # Filter to only C++ files | ||||||
|  |     cpp_files = list(filter(filter_component_and_test_cpp_files, files)) | ||||||
|  |     return (False, get_cpp_changed_components(cpp_files)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool: | def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool: | ||||||
|     """Check if a changed file ends with any of the specified extensions.""" |     """Check if a changed file ends with any of the specified extensions.""" | ||||||
|     return any(file.endswith(extensions) for file in changed_files(branch)) |     return any(file.endswith(extensions) for file in changed_files(branch)) | ||||||
| @@ -579,7 +615,7 @@ def main() -> None: | |||||||
|     else: |     else: | ||||||
|         # Get both directly changed and all changed (with dependencies) |         # Get both directly changed and all changed (with dependencies) | ||||||
|         changed = changed_files(args.branch) |         changed = changed_files(args.branch) | ||||||
|         component_files = [f for f in changed if filter_component_files(f)] |         component_files = [f for f in changed if filter_component_and_test_files(f)] | ||||||
|  |  | ||||||
|         directly_changed_components = get_components_with_dependencies( |         directly_changed_components = get_components_with_dependencies( | ||||||
|             component_files, False |             component_files, False | ||||||
| @@ -646,6 +682,9 @@ def main() -> None: | |||||||
|         files_to_check_count = 0 |         files_to_check_count = 0 | ||||||
|  |  | ||||||
|     # Build output |     # Build output | ||||||
|  |     # Determine which C++ unit tests to run | ||||||
|  |     cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) | ||||||
|  |  | ||||||
|     output: dict[str, Any] = { |     output: dict[str, Any] = { | ||||||
|         "integration_tests": run_integration, |         "integration_tests": run_integration, | ||||||
|         "clang_tidy": run_clang_tidy, |         "clang_tidy": run_clang_tidy, | ||||||
| @@ -661,6 +700,8 @@ def main() -> None: | |||||||
|         "dependency_only_count": len(dependency_only_components), |         "dependency_only_count": len(dependency_only_components), | ||||||
|         "changed_cpp_file_count": changed_cpp_file_count, |         "changed_cpp_file_count": changed_cpp_file_count, | ||||||
|         "memory_impact": memory_impact, |         "memory_impact": memory_impact, | ||||||
|  |         "cpp_unit_tests_run_all": cpp_run_all, | ||||||
|  |         "cpp_unit_tests_components": cpp_components, | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     # Output as JSON |     # Output as JSON | ||||||
|   | |||||||
| @@ -2,19 +2,14 @@ | |||||||
|  |  | ||||||
| import json | import json | ||||||
|  |  | ||||||
| from helpers import git_ls_files | from helpers import get_all_component_files, get_components_with_dependencies | ||||||
|  |  | ||||||
| from esphome.automation import ACTION_REGISTRY, CONDITION_REGISTRY | from esphome.automation import ACTION_REGISTRY, CONDITION_REGISTRY | ||||||
| from esphome.pins import PIN_SCHEMA_REGISTRY | from esphome.pins import PIN_SCHEMA_REGISTRY | ||||||
|  |  | ||||||
| list_components = __import__("list-components") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     files = git_ls_files() |     files = get_all_component_files() | ||||||
|     files = filter(list_components.filter_component_files, files) |     components = get_components_with_dependencies(files, True) | ||||||
|  |  | ||||||
|     components = list_components.get_components(files, True) |  | ||||||
|  |  | ||||||
|     dump = { |     dump = { | ||||||
|         "actions": sorted(list(ACTION_REGISTRY.keys())), |         "actions": sorted(list(ACTION_REGISTRY.keys())), | ||||||
|   | |||||||
| @@ -25,12 +25,21 @@ CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc") | |||||||
| # Python file extensions | # Python file extensions | ||||||
| PYTHON_FILE_EXTENSIONS = (".py", ".pyi") | PYTHON_FILE_EXTENSIONS = (".py", ".pyi") | ||||||
|  |  | ||||||
|  | # Combined C++ and Python file extensions for convenience | ||||||
|  | CPP_AND_PYTHON_FILE_EXTENSIONS = (*CPP_FILE_EXTENSIONS, *PYTHON_FILE_EXTENSIONS) | ||||||
|  |  | ||||||
| # YAML file extensions | # YAML file extensions | ||||||
| YAML_FILE_EXTENSIONS = (".yaml", ".yml") | YAML_FILE_EXTENSIONS = (".yaml", ".yml") | ||||||
|  |  | ||||||
| # Component path prefix | # Component path prefix | ||||||
| ESPHOME_COMPONENTS_PATH = "esphome/components/" | ESPHOME_COMPONENTS_PATH = "esphome/components/" | ||||||
|  |  | ||||||
|  | # Test components path prefix | ||||||
|  | ESPHOME_TESTS_COMPONENTS_PATH = "tests/components/" | ||||||
|  |  | ||||||
|  | # Tuple of component and test paths for efficient startswith checks | ||||||
|  | COMPONENT_AND_TESTS_PATHS = (ESPHOME_COMPONENTS_PATH, ESPHOME_TESTS_COMPONENTS_PATH) | ||||||
|  |  | ||||||
| # Base bus components - these ARE the bus implementations and should not | # Base bus components - these ARE the bus implementations and should not | ||||||
| # be flagged as needing migration since they are the platform/base components | # be flagged as needing migration since they are the platform/base components | ||||||
| BASE_BUS_COMPONENTS = { | BASE_BUS_COMPONENTS = { | ||||||
| @@ -658,17 +667,32 @@ def get_components_from_integration_fixtures() -> set[str]: | |||||||
|     return components |     return components | ||||||
|  |  | ||||||
|  |  | ||||||
| def filter_component_files(file_path: str) -> bool: | def filter_component_and_test_files(file_path: str) -> bool: | ||||||
|     """Check if a file path is a component file. |     """Check if a file path is a component or test file. | ||||||
|  |  | ||||||
|     Args: |     Args: | ||||||
|         file_path: Path to check |         file_path: Path to check | ||||||
|  |  | ||||||
|     Returns: |     Returns: | ||||||
|         True if the file is in a component directory |         True if the file is in a component or test directory | ||||||
|     """ |     """ | ||||||
|     return file_path.startswith("esphome/components/") or file_path.startswith( |     return file_path.startswith(COMPONENT_AND_TESTS_PATHS) or ( | ||||||
|         "tests/components/" |         file_path.startswith(ESPHOME_TESTS_COMPONENTS_PATH) | ||||||
|  |         and file_path.endswith(YAML_FILE_EXTENSIONS) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def filter_component_and_test_cpp_files(file_path: str) -> bool: | ||||||
|  |     """Check if a file is a C++ source file in component or test directories. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         file_path: Path to check | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         True if the file is a C++ source/header file in component or test directories | ||||||
|  |     """ | ||||||
|  |     return file_path.endswith(CPP_FILE_EXTENSIONS) and file_path.startswith( | ||||||
|  |         COMPONENT_AND_TESTS_PATHS | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -740,7 +764,7 @@ def create_components_graph() -> dict[str, list[str]]: | |||||||
|  |  | ||||||
|     # The root directory of the repo |     # The root directory of the repo | ||||||
|     root = Path(__file__).parent.parent |     root = Path(__file__).parent.parent | ||||||
|     components_dir = root / "esphome" / "components" |     components_dir = root / ESPHOME_COMPONENTS_PATH | ||||||
|     # Fake some directory so that get_component works |     # Fake some directory so that get_component works | ||||||
|     CORE.config_path = root |     CORE.config_path = root | ||||||
|     # Various configuration to capture different outcomes used by `AUTO_LOAD` function. |     # Various configuration to capture different outcomes used by `AUTO_LOAD` function. | ||||||
| @@ -873,3 +897,81 @@ def get_components_with_dependencies( | |||||||
|         return sorted(all_changed_components) |         return sorted(all_changed_components) | ||||||
|  |  | ||||||
|     return sorted(components) |     return sorted(components) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_all_component_files() -> list[str]: | ||||||
|  |     """Get all component and test files from git. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         List of all component and test file paths | ||||||
|  |     """ | ||||||
|  |     files = git_ls_files() | ||||||
|  |     return list(filter(filter_component_and_test_files, files)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_all_components() -> list[str]: | ||||||
|  |     """Get all component names. | ||||||
|  |  | ||||||
|  |     This function uses git to find all component files and extracts the component names. | ||||||
|  |     It returns the same list as calling list-components.py without arguments. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         List of all component names | ||||||
|  |     """ | ||||||
|  |     return get_components_with_dependencies(get_all_component_files(), False) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def core_changed(files: list[str]) -> bool: | ||||||
|  |     """Check if any core C++ or Python files have changed. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         files: List of file paths to check | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         True if any core C++ or Python files have changed | ||||||
|  |     """ | ||||||
|  |     return any( | ||||||
|  |         f.startswith("esphome/core/") and f.endswith(CPP_AND_PYTHON_FILE_EXTENSIONS) | ||||||
|  |         for f in files | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_cpp_changed_components(files: list[str]) -> list[str]: | ||||||
|  |     """Get components that have changed C++ files or tests. | ||||||
|  |  | ||||||
|  |     This function analyzes a list of changed files and determines which components | ||||||
|  |     are affected. It handles two scenarios: | ||||||
|  |  | ||||||
|  |     1. Test files changed (tests/components/<component>/*.cpp): | ||||||
|  |        - Adds the component to the affected list | ||||||
|  |        - Only that component needs to be tested | ||||||
|  |  | ||||||
|  |     2. Component C++ files changed (esphome/components/<component>/*): | ||||||
|  |        - Adds the component to the affected list | ||||||
|  |        - Also adds all components that depend on this component (recursively) | ||||||
|  |        - This ensures that changes propagate to dependent components | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         files: List of file paths to analyze (should be C++ files) | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         Sorted list of component names that need C++ unit tests run | ||||||
|  |     """ | ||||||
|  |     components_graph = create_components_graph() | ||||||
|  |     affected: set[str] = set() | ||||||
|  |     for file in files: | ||||||
|  |         if not file.endswith(CPP_FILE_EXTENSIONS): | ||||||
|  |             continue | ||||||
|  |         if file.startswith(ESPHOME_TESTS_COMPONENTS_PATH): | ||||||
|  |             parts = file.split("/") | ||||||
|  |             if len(parts) >= 4: | ||||||
|  |                 component_dir = Path(ESPHOME_TESTS_COMPONENTS_PATH) / parts[2] | ||||||
|  |                 if component_dir.is_dir(): | ||||||
|  |                     affected.add(parts[2]) | ||||||
|  |         elif file.startswith(ESPHOME_COMPONENTS_PATH): | ||||||
|  |             parts = file.split("/") | ||||||
|  |             if len(parts) >= 4: | ||||||
|  |                 component = parts[2] | ||||||
|  |                 affected.update(find_children_of_component(components_graph, component)) | ||||||
|  |                 affected.add(component) | ||||||
|  |     return sorted(affected) | ||||||
|   | |||||||
| @@ -3,18 +3,14 @@ import argparse | |||||||
|  |  | ||||||
| from helpers import ( | from helpers import ( | ||||||
|     changed_files, |     changed_files, | ||||||
|     filter_component_files, |     filter_component_and_test_cpp_files, | ||||||
|  |     filter_component_and_test_files, | ||||||
|  |     get_all_component_files, | ||||||
|     get_components_with_dependencies, |     get_components_with_dependencies, | ||||||
|     git_ls_files, |     get_cpp_changed_components, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_all_component_files() -> list[str]: |  | ||||||
|     """Get all component files from git.""" |  | ||||||
|     files = git_ls_files() |  | ||||||
|     return list(filter(filter_component_files, files)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     parser = argparse.ArgumentParser() |     parser = argparse.ArgumentParser() | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
| @@ -39,16 +35,29 @@ def main(): | |||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
|         "-b", "--branch", help="Branch to compare changed files against" |         "-b", "--branch", help="Branch to compare changed files against" | ||||||
|     ) |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "--cpp-changed", | ||||||
|  |         action="store_true", | ||||||
|  |         help="List components with changed C++ files", | ||||||
|  |     ) | ||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
|  |  | ||||||
|     if args.branch and not ( |     if args.branch and not ( | ||||||
|         args.changed or args.changed_direct or args.changed_with_deps |         args.changed | ||||||
|  |         or args.changed_direct | ||||||
|  |         or args.changed_with_deps | ||||||
|  |         or args.cpp_changed | ||||||
|     ): |     ): | ||||||
|         parser.error( |         parser.error( | ||||||
|             "--branch requires --changed, --changed-direct, or --changed-with-deps" |             "--branch requires --changed, --changed-direct, --changed-with-deps, or --cpp-changed" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     if args.changed or args.changed_direct or args.changed_with_deps: |     if ( | ||||||
|  |         args.changed | ||||||
|  |         or args.changed_direct | ||||||
|  |         or args.changed_with_deps | ||||||
|  |         or args.cpp_changed | ||||||
|  |     ): | ||||||
|         # When --changed* is passed, only get the changed files |         # When --changed* is passed, only get the changed files | ||||||
|         changed = changed_files(args.branch) |         changed = changed_files(args.branch) | ||||||
|  |  | ||||||
| @@ -68,6 +77,11 @@ def main(): | |||||||
|         # - --changed-with-deps: Used by CI test determination (script/determine-jobs.py) |         # - --changed-with-deps: Used by CI test determination (script/determine-jobs.py) | ||||||
|         #   Returns: Components with code changes + their dependencies (not infrastructure) |         #   Returns: Components with code changes + their dependencies (not infrastructure) | ||||||
|         #   Reason: CI needs to test changed components and their dependents |         #   Reason: CI needs to test changed components and their dependents | ||||||
|  |         # | ||||||
|  |         # - --cpp-changed: Used by CI to determine if any C++ files changed (script/determine-jobs.py) | ||||||
|  |         #   Returns: Only components with changed C++ files | ||||||
|  |         #   Reason: Only components with C++ changes need C++ testing | ||||||
|  |  | ||||||
|         base_test_changed = any( |         base_test_changed = any( | ||||||
|             "tests/test_build_components" in file for file in changed |             "tests/test_build_components" in file for file in changed | ||||||
|         ) |         ) | ||||||
| @@ -80,7 +94,7 @@ def main(): | |||||||
|             # Only look at changed component files (ignore infrastructure changes) |             # Only look at changed component files (ignore infrastructure changes) | ||||||
|             # For --changed-direct: only actual component code changes matter (for isolation) |             # For --changed-direct: only actual component code changes matter (for isolation) | ||||||
|             # For --changed-with-deps: only actual component code changes matter (for testing) |             # For --changed-with-deps: only actual component code changes matter (for testing) | ||||||
|             files = [f for f in changed if filter_component_files(f)] |             files = [f for f in changed if filter_component_and_test_files(f)] | ||||||
|     else: |     else: | ||||||
|         # Get all component files |         # Get all component files | ||||||
|         files = get_all_component_files() |         files = get_all_component_files() | ||||||
| @@ -100,6 +114,11 @@ def main(): | |||||||
|         # Return only directly changed components (without dependencies) |         # Return only directly changed components (without dependencies) | ||||||
|         for c in get_components_with_dependencies(files, False): |         for c in get_components_with_dependencies(files, False): | ||||||
|             print(c) |             print(c) | ||||||
|  |     elif args.cpp_changed: | ||||||
|  |         # Only look at changed cpp files | ||||||
|  |         files = list(filter(filter_component_and_test_cpp_files, changed)) | ||||||
|  |         for c in get_cpp_changed_components(files): | ||||||
|  |             print(c) | ||||||
|     else: |     else: | ||||||
|         # Return all changed components (with dependencies) - default behavior |         # Return all changed components (with dependencies) - default behavior | ||||||
|         for c in get_components_with_dependencies(files, args.changed): |         for c in get_components_with_dependencies(files, args.changed): | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								tests/components/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | # Gitignore settings for ESPHome | ||||||
|  | # This is an example and may include too much for your use-case. | ||||||
|  | # You can modify this file to suit your needs. | ||||||
|  | /.esphome/ | ||||||
|  | /secrets.yaml | ||||||
							
								
								
									
										32
									
								
								tests/components/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								tests/components/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | # How to write C++ ESPHome unit tests | ||||||
|  |  | ||||||
|  | 1. Locate the folder with your component or create a new one with the same name as the component. | ||||||
|  | 2. Write the tests. You can add as many `.cpp` and `.h` files as you need to organize your tests. | ||||||
|  |  | ||||||
|  | **IMPORTANT**: wrap all your testing code in a unique namespace to avoid linker collisions when compiling | ||||||
|  | testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing` | ||||||
|  | (where "component" is the component under test), for example: `esphome::uart::testing`. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Running component unit tests | ||||||
|  |  | ||||||
|  | (from the repository root) | ||||||
|  | ```bash | ||||||
|  | ./script/cpp_unit_test.py component1 component2 ... | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The above will compile and run the provided components and their tests. | ||||||
|  |  | ||||||
|  | To run all tests, you can invoke `cpp_unit_test.py` with the special `--all` flag: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | ./script/cpp_unit_test.py --all | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | To run a specific test suite, you can provide a Google Test filter: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | GTEST_FILTER='UART*' ./script/cpp_unit_test.py uart modbus | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The process will return `0` for success or nonzero for failure. In case of failure, the errors will be printed out to the console. | ||||||
| @@ -70,3 +70,69 @@ binary_sensor: | |||||||
|           - delay: 10s |           - delay: 10s | ||||||
|             time_off: 200ms |             time_off: 200ms | ||||||
|             time_on: 800ms |             time_on: 800ms | ||||||
|  |  | ||||||
|  |   # Test on_multi_click with single click | ||||||
|  |   - platform: template | ||||||
|  |     id: multi_click_single | ||||||
|  |     name: "Multi Click Single" | ||||||
|  |     on_multi_click: | ||||||
|  |       - timing: | ||||||
|  |           - state: true | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |         then: | ||||||
|  |           - logger.log: "Single click detected" | ||||||
|  |  | ||||||
|  |   # Test on_multi_click with double click | ||||||
|  |   - platform: template | ||||||
|  |     id: multi_click_double | ||||||
|  |     name: "Multi Click Double" | ||||||
|  |     on_multi_click: | ||||||
|  |       - timing: | ||||||
|  |           - state: true | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |           - state: false | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |           - state: true | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |         then: | ||||||
|  |           - logger.log: "Double click detected" | ||||||
|  |  | ||||||
|  |   # Test on_multi_click with complex pattern (5 events) | ||||||
|  |   - platform: template | ||||||
|  |     id: multi_click_complex | ||||||
|  |     name: "Multi Click Complex" | ||||||
|  |     on_multi_click: | ||||||
|  |       - timing: | ||||||
|  |           - state: true | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |           - state: false | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |           - state: true | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |           - state: false | ||||||
|  |             min_length: 50ms | ||||||
|  |             max_length: 350ms | ||||||
|  |           - state: true | ||||||
|  |             min_length: 50ms | ||||||
|  |         then: | ||||||
|  |           - logger.log: "Complex pattern detected" | ||||||
|  |  | ||||||
|  |   # Test on_multi_click with custom invalid_cooldown | ||||||
|  |   - platform: template | ||||||
|  |     id: multi_click_cooldown | ||||||
|  |     name: "Multi Click Cooldown" | ||||||
|  |     on_multi_click: | ||||||
|  |       - timing: | ||||||
|  |           - state: true | ||||||
|  |             min_length: 100ms | ||||||
|  |             max_length: 500ms | ||||||
|  |         invalid_cooldown: 2s | ||||||
|  |         then: | ||||||
|  |           - logger.log: "Click with custom cooldown" | ||||||
|   | |||||||
| @@ -12,3 +12,20 @@ switch: | |||||||
|   - platform: gpio |   - platform: gpio | ||||||
|     pin: ${switch_pin} |     pin: ${switch_pin} | ||||||
|     id: gpio_switch |     id: gpio_switch | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: ${switch_pin_2} | ||||||
|  |     id: gpio_switch_interlock_1 | ||||||
|  |     interlock: [gpio_switch_interlock_2, gpio_switch_interlock_3] | ||||||
|  |     interlock_wait_time: 100ms | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: ${switch_pin_3} | ||||||
|  |     id: gpio_switch_interlock_2 | ||||||
|  |     interlock: [gpio_switch_interlock_1, gpio_switch_interlock_3] | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: ${switch_pin_4} | ||||||
|  |     id: gpio_switch_interlock_3 | ||||||
|  |     interlock: [gpio_switch_interlock_1, gpio_switch_interlock_2] | ||||||
|  |     interlock_wait_time: 50ms | ||||||
|   | |||||||
| @@ -2,5 +2,8 @@ substitutions: | |||||||
|   binary_sensor_pin: GPIO2 |   binary_sensor_pin: GPIO2 | ||||||
|   output_pin: GPIO3 |   output_pin: GPIO3 | ||||||
|   switch_pin: GPIO4 |   switch_pin: GPIO4 | ||||||
|  |   switch_pin_2: GPIO5 | ||||||
|  |   switch_pin_3: GPIO6 | ||||||
|  |   switch_pin_4: GPIO7 | ||||||
|  |  | ||||||
| <<: !include common.yaml | <<: !include common.yaml | ||||||
|   | |||||||
| @@ -2,5 +2,8 @@ substitutions: | |||||||
|   binary_sensor_pin: GPIO12 |   binary_sensor_pin: GPIO12 | ||||||
|   output_pin: GPIO13 |   output_pin: GPIO13 | ||||||
|   switch_pin: GPIO14 |   switch_pin: GPIO14 | ||||||
|  |   switch_pin_2: GPIO15 | ||||||
|  |   switch_pin_3: GPIO16 | ||||||
|  |   switch_pin_4: GPIO17 | ||||||
|  |  | ||||||
| <<: !include common.yaml | <<: !include common.yaml | ||||||
|   | |||||||
| @@ -2,5 +2,8 @@ substitutions: | |||||||
|   binary_sensor_pin: GPIO0 |   binary_sensor_pin: GPIO0 | ||||||
|   output_pin: GPIO2 |   output_pin: GPIO2 | ||||||
|   switch_pin: GPIO15 |   switch_pin: GPIO15 | ||||||
|  |   switch_pin_2: GPIO12 | ||||||
|  |   switch_pin_3: GPIO13 | ||||||
|  |   switch_pin_4: GPIO14 | ||||||
|  |  | ||||||
| <<: !include common.yaml | <<: !include common.yaml | ||||||
|   | |||||||
| @@ -12,3 +12,20 @@ switch: | |||||||
|   - platform: gpio |   - platform: gpio | ||||||
|     pin: P1.2 |     pin: P1.2 | ||||||
|     id: gpio_switch |     id: gpio_switch | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: P1.3 | ||||||
|  |     id: gpio_switch_interlock_1 | ||||||
|  |     interlock: [gpio_switch_interlock_2, gpio_switch_interlock_3] | ||||||
|  |     interlock_wait_time: 100ms | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: P1.4 | ||||||
|  |     id: gpio_switch_interlock_2 | ||||||
|  |     interlock: [gpio_switch_interlock_1, gpio_switch_interlock_3] | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: P1.5 | ||||||
|  |     id: gpio_switch_interlock_3 | ||||||
|  |     interlock: [gpio_switch_interlock_1, gpio_switch_interlock_2] | ||||||
|  |     interlock_wait_time: 50ms | ||||||
|   | |||||||
| @@ -12,3 +12,20 @@ switch: | |||||||
|   - platform: gpio |   - platform: gpio | ||||||
|     pin: P1.2 |     pin: P1.2 | ||||||
|     id: gpio_switch |     id: gpio_switch | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: P1.3 | ||||||
|  |     id: gpio_switch_interlock_1 | ||||||
|  |     interlock: [gpio_switch_interlock_2, gpio_switch_interlock_3] | ||||||
|  |     interlock_wait_time: 100ms | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: P1.4 | ||||||
|  |     id: gpio_switch_interlock_2 | ||||||
|  |     interlock: [gpio_switch_interlock_1, gpio_switch_interlock_3] | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: P1.5 | ||||||
|  |     id: gpio_switch_interlock_3 | ||||||
|  |     interlock: [gpio_switch_interlock_1, gpio_switch_interlock_2] | ||||||
|  |     interlock_wait_time: 50ms | ||||||
|   | |||||||
| @@ -2,5 +2,8 @@ substitutions: | |||||||
|   binary_sensor_pin: GPIO2 |   binary_sensor_pin: GPIO2 | ||||||
|   output_pin: GPIO3 |   output_pin: GPIO3 | ||||||
|   switch_pin: GPIO4 |   switch_pin: GPIO4 | ||||||
|  |   switch_pin_2: GPIO5 | ||||||
|  |   switch_pin_3: GPIO6 | ||||||
|  |   switch_pin_4: GPIO7 | ||||||
|  |  | ||||||
| <<: !include common.yaml | <<: !include common.yaml | ||||||
|   | |||||||
| @@ -123,3 +123,43 @@ light: | |||||||
|       red: 100% |       red: 100% | ||||||
|       green: 50% |       green: 50% | ||||||
|       blue: 50% |       blue: 50% | ||||||
|  |   # Test StrobeLightEffect with multiple colors | ||||||
|  |   - platform: monochromatic | ||||||
|  |     id: test_strobe_multiple | ||||||
|  |     name: Strobe Multiple Colors | ||||||
|  |     output: test_ledc_1 | ||||||
|  |     effects: | ||||||
|  |       - strobe: | ||||||
|  |           name: Strobe Multi | ||||||
|  |           colors: | ||||||
|  |             - state: true | ||||||
|  |               brightness: 100% | ||||||
|  |               duration: 500ms | ||||||
|  |             - state: false | ||||||
|  |               duration: 250ms | ||||||
|  |             - state: true | ||||||
|  |               brightness: 50% | ||||||
|  |               duration: 500ms | ||||||
|  |   # Test StrobeLightEffect with transition | ||||||
|  |   - platform: rgb | ||||||
|  |     id: test_strobe_transition | ||||||
|  |     name: Strobe With Transition | ||||||
|  |     red: test_ledc_1 | ||||||
|  |     green: test_ledc_2 | ||||||
|  |     blue: test_ledc_3 | ||||||
|  |     effects: | ||||||
|  |       - strobe: | ||||||
|  |           name: Strobe Transition | ||||||
|  |           colors: | ||||||
|  |             - state: true | ||||||
|  |               red: 100% | ||||||
|  |               green: 0% | ||||||
|  |               blue: 0% | ||||||
|  |               duration: 1s | ||||||
|  |               transition_length: 500ms | ||||||
|  |             - state: true | ||||||
|  |               red: 0% | ||||||
|  |               green: 100% | ||||||
|  |               blue: 0% | ||||||
|  |               duration: 1s | ||||||
|  |               transition_length: 500ms | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								tests/components/main.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								tests/components/main.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | #include <gtest/gtest.h> | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | This special main.cpp replaces the default one. | ||||||
|  | It will run all the Google Tests found in all compiled cpp files and then exit with the result | ||||||
|  | See README.md for more information | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | // Auto generated code by esphome | ||||||
|  | // ========== AUTO GENERATED INCLUDE BLOCK BEGIN =========== | ||||||
|  | // ========== AUTO GENERATED INCLUDE BLOCK END ===========" | ||||||
|  |  | ||||||
|  | void original_setup() { | ||||||
|  |   // This function won't be run. | ||||||
|  |  | ||||||
|  |   // ========== AUTO GENERATED CODE BEGIN =========== | ||||||
|  |   // =========== AUTO GENERATED CODE END ============ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void setup() { | ||||||
|  |   ::testing::InitGoogleTest(); | ||||||
|  |   int exit_code = RUN_ALL_TESTS(); | ||||||
|  |   exit(exit_code); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void loop() {} | ||||||
							
								
								
									
										37
									
								
								tests/components/uart/common.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								tests/components/uart/common.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <vector> | ||||||
|  | #include <cstdint> | ||||||
|  | #include <cstring> | ||||||
|  | #include <gmock/gmock.h> | ||||||
|  | #include <gtest/gtest.h> | ||||||
|  | #include "esphome/components/uart/uart_component.h" | ||||||
|  |  | ||||||
|  | namespace esphome::uart::testing { | ||||||
|  |  | ||||||
|  | using ::testing::_; | ||||||
|  | using ::testing::Return; | ||||||
|  | using ::testing::SaveArg; | ||||||
|  | using ::testing::DoAll; | ||||||
|  | using ::testing::Invoke; | ||||||
|  | using ::testing::SetArgPointee; | ||||||
|  |  | ||||||
|  | // Derive a mock from UARTComponent to test the wrapper implementations. | ||||||
|  | class MockUARTComponent : public UARTComponent { | ||||||
|  |  public: | ||||||
|  |   using UARTComponent::write_array; | ||||||
|  |   using UARTComponent::write_byte; | ||||||
|  |  | ||||||
|  |   // NOTE: std::vector is used here for test convenience. For production code, | ||||||
|  |   // consider using StaticVector or FixedVector from esphome/core/helpers.h instead. | ||||||
|  |   std::vector<uint8_t> written_data; | ||||||
|  |  | ||||||
|  |   void write_array(const uint8_t *data, size_t len) override { written_data.assign(data, data + len); } | ||||||
|  |  | ||||||
|  |   MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); | ||||||
|  |   MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); | ||||||
|  |   MOCK_METHOD(int, available, (), (override)); | ||||||
|  |   MOCK_METHOD(void, flush, (), (override)); | ||||||
|  |   MOCK_METHOD(void, check_logger_conflict, (), (override)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace esphome::uart::testing | ||||||
							
								
								
									
										73
									
								
								tests/components/uart/uart_component.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								tests/components/uart/uart_component.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | #include "common.h" | ||||||
|  |  | ||||||
|  | namespace esphome::uart::testing { | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, SetGetBaudRate) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   mock.set_baud_rate(38400); | ||||||
|  |   EXPECT_EQ(mock.get_baud_rate(), 38400); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, SetGetStopBits) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   mock.set_stop_bits(2); | ||||||
|  |   EXPECT_EQ(mock.get_stop_bits(), 2); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, SetGetDataBits) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   mock.set_data_bits(7); | ||||||
|  |   EXPECT_EQ(mock.get_data_bits(), 7); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, SetGetParity) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   mock.set_parity(UARTParityOptions::UART_CONFIG_PARITY_EVEN); | ||||||
|  |   EXPECT_EQ(mock.get_parity(), UARTParityOptions::UART_CONFIG_PARITY_EVEN); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, SetGetRxBufferSize) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   mock.set_rx_buffer_size(128); | ||||||
|  |   EXPECT_EQ(mock.get_rx_buffer_size(), 128); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, WriteArrayVector) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   std::vector<uint8_t> data = {10, 20, 30}; | ||||||
|  |   mock.write_array(data); | ||||||
|  |   EXPECT_EQ(mock.written_data, data); | ||||||
|  | } | ||||||
|  | TEST(UARTComponentTest, WriteByte) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   uint8_t byte = 0x79; | ||||||
|  |   mock.write_byte(byte); | ||||||
|  |   EXPECT_EQ(mock.written_data.size(), 1); | ||||||
|  |   EXPECT_EQ(mock.written_data[0], byte); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, WriteStr) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   const char *str = "Hello"; | ||||||
|  |   std::vector<uint8_t> captured; | ||||||
|  |   mock.write_str(str); | ||||||
|  |   EXPECT_EQ(mock.written_data.size(), strlen(str)); | ||||||
|  |   EXPECT_EQ(0, strncmp(str, (const char *) mock.written_data.data(), mock.written_data.size())); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Tests for wrapper methods forwarding to pure virtual read_array | ||||||
|  | TEST(UARTComponentTest, ReadByteSuccess) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   uint8_t value = 0; | ||||||
|  |   EXPECT_CALL(mock, read_array(&value, 1)).WillOnce(Return(true)); | ||||||
|  |   EXPECT_TRUE(mock.read_byte(&value)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTComponentTest, ReadByteFailure) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   uint8_t value = 0xFF; | ||||||
|  |   EXPECT_CALL(mock, read_array(&value, 1)).WillOnce(Return(false)); | ||||||
|  |   EXPECT_FALSE(mock.read_byte(&value)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome::uart::testing | ||||||
							
								
								
									
										108
									
								
								tests/components/uart/uart_device.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								tests/components/uart/uart_device.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | #include "common.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  |  | ||||||
|  | namespace esphome::uart::testing { | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, ReadByteSuccess) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   uint8_t value = 0; | ||||||
|  |   EXPECT_CALL(mock, read_array(_, 1)).WillOnce(DoAll(SetArgPointee<0>(0x5A), Return(true))); | ||||||
|  |   bool result = dev.read_byte(&value); | ||||||
|  |   EXPECT_TRUE(result); | ||||||
|  |   EXPECT_EQ(value, 0x5A); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, ReadByteFailure) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   uint8_t value = 0xFF; | ||||||
|  |   EXPECT_CALL(mock, read_array(_, 1)).WillOnce(Return(false)); | ||||||
|  |   bool result = dev.read_byte(&value); | ||||||
|  |   EXPECT_FALSE(result); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, PeekByteSuccess) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   uint8_t value = 0; | ||||||
|  |   EXPECT_CALL(mock, peek_byte(_)).WillOnce(DoAll(SetArgPointee<0>(0xA5), Return(true))); | ||||||
|  |   bool result = dev.peek_byte(&value); | ||||||
|  |   EXPECT_TRUE(result); | ||||||
|  |   EXPECT_EQ(value, 0xA5); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, PeekByteFailure) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   uint8_t value = 0; | ||||||
|  |   EXPECT_CALL(mock, peek_byte(_)).WillOnce(Return(false)); | ||||||
|  |   bool result = dev.peek_byte(&value); | ||||||
|  |   EXPECT_FALSE(result); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, Available) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   EXPECT_CALL(mock, available()).WillOnce(Return(5)); | ||||||
|  |   EXPECT_EQ(dev.available(), 5); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, FlushCallsParent) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   EXPECT_CALL(mock, flush()).Times(1); | ||||||
|  |   dev.flush(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, WriteByteForwardsToWriteArray) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   dev.write_byte(0xAB); | ||||||
|  |   EXPECT_EQ(mock.written_data.size(), 1); | ||||||
|  |   EXPECT_EQ(mock.written_data[0], 0xAB); | ||||||
|  | } | ||||||
|  | TEST(UARTDeviceTest, WriteArrayPointer) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   uint8_t data[3] = {1, 2, 3}; | ||||||
|  |   dev.write_array(data, 3); | ||||||
|  |   EXPECT_EQ(mock.written_data.size(), 3); | ||||||
|  |   EXPECT_EQ(mock.written_data, std::vector(data, data + 3)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, WriteArrayVector) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   std::vector<uint8_t> data = {4, 5, 6}; | ||||||
|  |   dev.write_array(data); | ||||||
|  |   EXPECT_EQ(mock.written_data, data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, WriteArrayStdArray) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   std::array<uint8_t, 4> data = {7, 8, 9, 10}; | ||||||
|  |   dev.write_array(data); | ||||||
|  |   EXPECT_EQ(mock.written_data.size(), data.size()); | ||||||
|  |   EXPECT_EQ(mock.written_data, std::vector(data.begin(), data.end())); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, WriteStrForwardsToWriteArray) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   const char *str = "ESPHome"; | ||||||
|  |   dev.write_str(str); | ||||||
|  |   EXPECT_EQ(mock.written_data.size(), strlen(str)); | ||||||
|  |   EXPECT_EQ(0, strncmp(str, (const char *) mock.written_data.data(), mock.written_data.size())); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TEST(UARTDeviceTest, WriteStrEmptyString) { | ||||||
|  |   MockUARTComponent mock; | ||||||
|  |   UARTDevice dev(&mock); | ||||||
|  |   const char *str = ""; | ||||||
|  |   dev.write_str(str); | ||||||
|  |   EXPECT_EQ(mock.written_data.size(), 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome::uart::testing | ||||||
| @@ -5,7 +5,6 @@ import importlib.util | |||||||
| import json | import json | ||||||
| import os | import os | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import subprocess |  | ||||||
| import sys | import sys | ||||||
| from unittest.mock import Mock, call, patch | from unittest.mock import Mock, call, patch | ||||||
|  |  | ||||||
| @@ -56,9 +55,9 @@ def mock_should_run_python_linters() -> Generator[Mock, None, None]: | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| def mock_subprocess_run() -> Generator[Mock, None, None]: | def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]: | ||||||
|     """Mock subprocess.run for list-components.py calls.""" |     """Mock determine_cpp_unit_tests from helpers.""" | ||||||
|     with patch.object(determine_jobs.subprocess, "run") as mock: |     with patch.object(determine_jobs, "determine_cpp_unit_tests") as mock: | ||||||
|         yield mock |         yield mock | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -82,8 +81,8 @@ def test_main_all_tests_should_run( | |||||||
|     mock_should_run_clang_tidy: Mock, |     mock_should_run_clang_tidy: Mock, | ||||||
|     mock_should_run_clang_format: Mock, |     mock_should_run_clang_format: Mock, | ||||||
|     mock_should_run_python_linters: Mock, |     mock_should_run_python_linters: Mock, | ||||||
|     mock_subprocess_run: Mock, |  | ||||||
|     mock_changed_files: Mock, |     mock_changed_files: Mock, | ||||||
|  |     mock_determine_cpp_unit_tests: Mock, | ||||||
|     capsys: pytest.CaptureFixture[str], |     capsys: pytest.CaptureFixture[str], | ||||||
|     monkeypatch: pytest.MonkeyPatch, |     monkeypatch: pytest.MonkeyPatch, | ||||||
| ) -> None: | ) -> None: | ||||||
| @@ -95,6 +94,7 @@ def test_main_all_tests_should_run( | |||||||
|     mock_should_run_clang_tidy.return_value = True |     mock_should_run_clang_tidy.return_value = True | ||||||
|     mock_should_run_clang_format.return_value = True |     mock_should_run_clang_format.return_value = True | ||||||
|     mock_should_run_python_linters.return_value = True |     mock_should_run_python_linters.return_value = True | ||||||
|  |     mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"]) | ||||||
|  |  | ||||||
|     # Mock changed_files to return non-component files (to avoid memory impact) |     # Mock changed_files to return non-component files (to avoid memory impact) | ||||||
|     # Memory impact only runs when component C++ files change |     # Memory impact only runs when component C++ files change | ||||||
| @@ -114,15 +114,15 @@ def test_main_all_tests_should_run( | |||||||
|         ), |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, |             determine_jobs, | ||||||
|             "filter_component_files", |             "filter_component_and_test_files", | ||||||
|             side_effect=lambda f: f.startswith("esphome/components/"), |             side_effect=lambda f: f.startswith("esphome/components/"), | ||||||
|         ), |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, |             determine_jobs, | ||||||
|             "get_components_with_dependencies", |             "get_components_with_dependencies", | ||||||
|             side_effect=lambda files, deps: ["wifi", "api"] |             side_effect=lambda files, deps: ( | ||||||
|             if not deps |                 ["wifi", "api"] if not deps else ["wifi", "api", "sensor"] | ||||||
|             else ["wifi", "api", "sensor"], |             ), | ||||||
|         ), |         ), | ||||||
|     ): |     ): | ||||||
|         determine_jobs.main() |         determine_jobs.main() | ||||||
| @@ -150,6 +150,8 @@ def test_main_all_tests_should_run( | |||||||
|     # memory_impact should be false (no component C++ files changed) |     # memory_impact should be false (no component C++ files changed) | ||||||
|     assert "memory_impact" in output |     assert "memory_impact" in output | ||||||
|     assert output["memory_impact"]["should_run"] == "false" |     assert output["memory_impact"]["should_run"] == "false" | ||||||
|  |     assert output["cpp_unit_tests_run_all"] is False | ||||||
|  |     assert output["cpp_unit_tests_components"] == ["wifi", "api", "sensor"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_main_no_tests_should_run( | def test_main_no_tests_should_run( | ||||||
| @@ -157,8 +159,8 @@ def test_main_no_tests_should_run( | |||||||
|     mock_should_run_clang_tidy: Mock, |     mock_should_run_clang_tidy: Mock, | ||||||
|     mock_should_run_clang_format: Mock, |     mock_should_run_clang_format: Mock, | ||||||
|     mock_should_run_python_linters: Mock, |     mock_should_run_python_linters: Mock, | ||||||
|     mock_subprocess_run: Mock, |  | ||||||
|     mock_changed_files: Mock, |     mock_changed_files: Mock, | ||||||
|  |     mock_determine_cpp_unit_tests: Mock, | ||||||
|     capsys: pytest.CaptureFixture[str], |     capsys: pytest.CaptureFixture[str], | ||||||
|     monkeypatch: pytest.MonkeyPatch, |     monkeypatch: pytest.MonkeyPatch, | ||||||
| ) -> None: | ) -> None: | ||||||
| @@ -170,6 +172,7 @@ def test_main_no_tests_should_run( | |||||||
|     mock_should_run_clang_tidy.return_value = False |     mock_should_run_clang_tidy.return_value = False | ||||||
|     mock_should_run_clang_format.return_value = False |     mock_should_run_clang_format.return_value = False | ||||||
|     mock_should_run_python_linters.return_value = False |     mock_should_run_python_linters.return_value = False | ||||||
|  |     mock_determine_cpp_unit_tests.return_value = (False, []) | ||||||
|  |  | ||||||
|     # Mock changed_files to return no component files |     # Mock changed_files to return no component files | ||||||
|     mock_changed_files.return_value = [] |     mock_changed_files.return_value = [] | ||||||
| @@ -178,7 +181,9 @@ def test_main_no_tests_should_run( | |||||||
|     with ( |     with ( | ||||||
|         patch("sys.argv", ["determine-jobs.py"]), |         patch("sys.argv", ["determine-jobs.py"]), | ||||||
|         patch.object(determine_jobs, "get_changed_components", return_value=[]), |         patch.object(determine_jobs, "get_changed_components", return_value=[]), | ||||||
|         patch.object(determine_jobs, "filter_component_files", return_value=False), |         patch.object( | ||||||
|  |             determine_jobs, "filter_component_and_test_files", return_value=False | ||||||
|  |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, "get_components_with_dependencies", return_value=[] |             determine_jobs, "get_components_with_dependencies", return_value=[] | ||||||
|         ), |         ), | ||||||
| @@ -202,31 +207,8 @@ def test_main_no_tests_should_run( | |||||||
|     # memory_impact should be present |     # memory_impact should be present | ||||||
|     assert "memory_impact" in output |     assert "memory_impact" in output | ||||||
|     assert output["memory_impact"]["should_run"] == "false" |     assert output["memory_impact"]["should_run"] == "false" | ||||||
|  |     assert output["cpp_unit_tests_run_all"] is False | ||||||
|  |     assert output["cpp_unit_tests_components"] == [] | ||||||
| def test_main_list_components_fails( |  | ||||||
|     mock_should_run_integration_tests: Mock, |  | ||||||
|     mock_should_run_clang_tidy: Mock, |  | ||||||
|     mock_should_run_clang_format: Mock, |  | ||||||
|     mock_should_run_python_linters: Mock, |  | ||||||
|     mock_subprocess_run: Mock, |  | ||||||
|     capsys: pytest.CaptureFixture[str], |  | ||||||
| ) -> None: |  | ||||||
|     """Test when list-components.py fails.""" |  | ||||||
|     mock_should_run_integration_tests.return_value = True |  | ||||||
|     mock_should_run_clang_tidy.return_value = True |  | ||||||
|     mock_should_run_clang_format.return_value = True |  | ||||||
|     mock_should_run_python_linters.return_value = True |  | ||||||
|  |  | ||||||
|     # Mock list-components.py failure |  | ||||||
|     mock_subprocess_run.side_effect = subprocess.CalledProcessError(1, "cmd") |  | ||||||
|  |  | ||||||
|     # Run main function with mocked argv - should raise |  | ||||||
|     with ( |  | ||||||
|         patch("sys.argv", ["determine-jobs.py"]), |  | ||||||
|         pytest.raises(subprocess.CalledProcessError), |  | ||||||
|     ): |  | ||||||
|         determine_jobs.main() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_main_with_branch_argument( | def test_main_with_branch_argument( | ||||||
| @@ -234,8 +216,8 @@ def test_main_with_branch_argument( | |||||||
|     mock_should_run_clang_tidy: Mock, |     mock_should_run_clang_tidy: Mock, | ||||||
|     mock_should_run_clang_format: Mock, |     mock_should_run_clang_format: Mock, | ||||||
|     mock_should_run_python_linters: Mock, |     mock_should_run_python_linters: Mock, | ||||||
|     mock_subprocess_run: Mock, |  | ||||||
|     mock_changed_files: Mock, |     mock_changed_files: Mock, | ||||||
|  |     mock_determine_cpp_unit_tests: Mock, | ||||||
|     capsys: pytest.CaptureFixture[str], |     capsys: pytest.CaptureFixture[str], | ||||||
|     monkeypatch: pytest.MonkeyPatch, |     monkeypatch: pytest.MonkeyPatch, | ||||||
| ) -> None: | ) -> None: | ||||||
| @@ -247,6 +229,7 @@ def test_main_with_branch_argument( | |||||||
|     mock_should_run_clang_tidy.return_value = True |     mock_should_run_clang_tidy.return_value = True | ||||||
|     mock_should_run_clang_format.return_value = False |     mock_should_run_clang_format.return_value = False | ||||||
|     mock_should_run_python_linters.return_value = True |     mock_should_run_python_linters.return_value = True | ||||||
|  |     mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"]) | ||||||
|  |  | ||||||
|     # Mock changed_files to return non-component files (to avoid memory impact) |     # Mock changed_files to return non-component files (to avoid memory impact) | ||||||
|     # Memory impact only runs when component C++ files change |     # Memory impact only runs when component C++ files change | ||||||
| @@ -258,7 +241,7 @@ def test_main_with_branch_argument( | |||||||
|         patch.object(determine_jobs, "get_changed_components", return_value=["mqtt"]), |         patch.object(determine_jobs, "get_changed_components", return_value=["mqtt"]), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, |             determine_jobs, | ||||||
|             "filter_component_files", |             "filter_component_and_test_files", | ||||||
|             side_effect=lambda f: f.startswith("esphome/components/"), |             side_effect=lambda f: f.startswith("esphome/components/"), | ||||||
|         ), |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
| @@ -296,6 +279,8 @@ def test_main_with_branch_argument( | |||||||
|     # memory_impact should be false (no component C++ files changed) |     # memory_impact should be false (no component C++ files changed) | ||||||
|     assert "memory_impact" in output |     assert "memory_impact" in output | ||||||
|     assert output["memory_impact"]["should_run"] == "false" |     assert output["memory_impact"]["should_run"] == "false" | ||||||
|  |     assert output["cpp_unit_tests_run_all"] is False | ||||||
|  |     assert output["cpp_unit_tests_components"] == ["mqtt"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_should_run_integration_tests( | def test_should_run_integration_tests( | ||||||
| @@ -506,7 +491,6 @@ def test_main_filters_components_without_tests( | |||||||
|     mock_should_run_clang_tidy: Mock, |     mock_should_run_clang_tidy: Mock, | ||||||
|     mock_should_run_clang_format: Mock, |     mock_should_run_clang_format: Mock, | ||||||
|     mock_should_run_python_linters: Mock, |     mock_should_run_python_linters: Mock, | ||||||
|     mock_subprocess_run: Mock, |  | ||||||
|     mock_changed_files: Mock, |     mock_changed_files: Mock, | ||||||
|     capsys: pytest.CaptureFixture[str], |     capsys: pytest.CaptureFixture[str], | ||||||
|     tmp_path: Path, |     tmp_path: Path, | ||||||
| @@ -556,16 +540,17 @@ def test_main_filters_components_without_tests( | |||||||
|         ), |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, |             determine_jobs, | ||||||
|             "filter_component_files", |             "filter_component_and_test_files", | ||||||
|             side_effect=lambda f: f.startswith("esphome/components/"), |             side_effect=lambda f: f.startswith("esphome/components/"), | ||||||
|         ), |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, |             determine_jobs, | ||||||
|             "get_components_with_dependencies", |             "get_components_with_dependencies", | ||||||
|             side_effect=lambda files, deps: ["wifi", "sensor"] |             side_effect=lambda files, deps: ( | ||||||
|             if not deps |                 ["wifi", "sensor"] if not deps else ["wifi", "sensor", "airthings_ble"] | ||||||
|             else ["wifi", "sensor", "airthings_ble"], |             ), | ||||||
|         ), |         ), | ||||||
|  |         patch.object(determine_jobs, "changed_files", return_value=[]), | ||||||
|     ): |     ): | ||||||
|         # Clear the cache since we're mocking root_path |         # Clear the cache since we're mocking root_path | ||||||
|         determine_jobs._component_has_tests.cache_clear() |         determine_jobs._component_has_tests.cache_clear() | ||||||
| @@ -808,7 +793,6 @@ def test_clang_tidy_mode_full_scan( | |||||||
|     mock_should_run_clang_tidy: Mock, |     mock_should_run_clang_tidy: Mock, | ||||||
|     mock_should_run_clang_format: Mock, |     mock_should_run_clang_format: Mock, | ||||||
|     mock_should_run_python_linters: Mock, |     mock_should_run_python_linters: Mock, | ||||||
|     mock_subprocess_run: Mock, |  | ||||||
|     mock_changed_files: Mock, |     mock_changed_files: Mock, | ||||||
|     capsys: pytest.CaptureFixture[str], |     capsys: pytest.CaptureFixture[str], | ||||||
|     monkeypatch: pytest.MonkeyPatch, |     monkeypatch: pytest.MonkeyPatch, | ||||||
| @@ -829,7 +813,9 @@ def test_clang_tidy_mode_full_scan( | |||||||
|         patch("sys.argv", ["determine-jobs.py"]), |         patch("sys.argv", ["determine-jobs.py"]), | ||||||
|         patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=True), |         patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=True), | ||||||
|         patch.object(determine_jobs, "get_changed_components", return_value=[]), |         patch.object(determine_jobs, "get_changed_components", return_value=[]), | ||||||
|         patch.object(determine_jobs, "filter_component_files", return_value=False), |         patch.object( | ||||||
|  |             determine_jobs, "filter_component_and_test_files", return_value=False | ||||||
|  |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, "get_components_with_dependencies", return_value=[] |             determine_jobs, "get_components_with_dependencies", return_value=[] | ||||||
|         ), |         ), | ||||||
| @@ -873,7 +859,6 @@ def test_clang_tidy_mode_targeted_scan( | |||||||
|     mock_should_run_clang_tidy: Mock, |     mock_should_run_clang_tidy: Mock, | ||||||
|     mock_should_run_clang_format: Mock, |     mock_should_run_clang_format: Mock, | ||||||
|     mock_should_run_python_linters: Mock, |     mock_should_run_python_linters: Mock, | ||||||
|     mock_subprocess_run: Mock, |  | ||||||
|     mock_changed_files: Mock, |     mock_changed_files: Mock, | ||||||
|     capsys: pytest.CaptureFixture[str], |     capsys: pytest.CaptureFixture[str], | ||||||
|     monkeypatch: pytest.MonkeyPatch, |     monkeypatch: pytest.MonkeyPatch, | ||||||
| @@ -912,7 +897,7 @@ def test_clang_tidy_mode_targeted_scan( | |||||||
|         patch.object(determine_jobs, "get_changed_components", return_value=components), |         patch.object(determine_jobs, "get_changed_components", return_value=components), | ||||||
|         patch.object( |         patch.object( | ||||||
|             determine_jobs, |             determine_jobs, | ||||||
|             "filter_component_files", |             "filter_component_and_test_files", | ||||||
|             side_effect=lambda f: f.startswith("esphome/components/"), |             side_effect=lambda f: f.startswith("esphome/components/"), | ||||||
|         ), |         ), | ||||||
|         patch.object( |         patch.object( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user