mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	revert
This commit is contained in:
		| @@ -877,6 +877,11 @@ async def to_code(config): | |||||||
|     for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): |     for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): | ||||||
|         os.environ.pop(clean_var, None) |         os.environ.pop(clean_var, None) | ||||||
|  |  | ||||||
|  |     # Set the location of the IDF component manager cache | ||||||
|  |     os.environ["IDF_COMPONENT_CACHE_PATH"] = str( | ||||||
|  |         CORE.relative_internal_path(".espressif") | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     add_extra_script( |     add_extra_script( | ||||||
|         "post", |         "post", | ||||||
|         "post_build.py", |         "post_build.py", | ||||||
|   | |||||||
| @@ -61,6 +61,10 @@ void AddressableLightTransformer::start() { | |||||||
|   this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); |   this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) { | ||||||
|  |   return uint8_t(int32_t(a) - (((int32_t(a) - int32_t(b)) * scale) / 256)); | ||||||
|  | } | ||||||
|  |  | ||||||
| optional<LightColorValues> AddressableLightTransformer::apply() { | optional<LightColorValues> AddressableLightTransformer::apply() { | ||||||
|   float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_()); |   float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_()); | ||||||
|  |  | ||||||
| @@ -74,38 +78,37 @@ optional<LightColorValues> AddressableLightTransformer::apply() { | |||||||
|   // all LEDs, we use the current state of each LED as the start. |   // all LEDs, we use the current state of each LED as the start. | ||||||
|  |  | ||||||
|   // We can't use a direct lerp smoothing here though - that would require creating a copy of the original |   // We can't use a direct lerp smoothing here though - that would require creating a copy of the original | ||||||
|   // state of each LED at the start of the transition. |   // state of each LED at the start of the transition. Instead, we "fake" the look of lerp by calculating | ||||||
|   // Instead, we "fake" the look of the LERP by using an exponential average over time and using |   // the delta between the current state and the target state, assuming that the delta represents the rest | ||||||
|   // dynamically-calculated alpha values to match the look. |   // of the transition that was to be applied as of the previous transition step, and scaling the delta for | ||||||
|  |   // what should be left after the current transition step. In this manner, the delta decays to zero as the | ||||||
|  |   // transition progresses. | ||||||
|  |   // | ||||||
|  |   // Here's an example of how the algorithm progresses in discrete steps: | ||||||
|  |   // | ||||||
|  |   // At time = 0.00, 0% complete, 100% remaining, 100% will remain after this step, so the scale is 100% / 100% = 100%. | ||||||
|  |   // At time = 0.10, 0% complete, 100% remaining, 90% will remain after this step, so the scale is 90% / 100% = 90%. | ||||||
|  |   // At time = 0.20, 10% complete, 90% remaining, 80% will remain after this step, so the scale is 80% / 90% = 88.9%. | ||||||
|  |   // At time = 0.50, 20% complete, 80% remaining, 50% will remain after this step, so the scale is 50% / 80% = 62.5%. | ||||||
|  |   // At time = 0.90, 50% complete, 50% remaining, 10% will remain after this step, so the scale is 10% / 50% = 20%. | ||||||
|  |   // At time = 0.91, 90% complete, 10% remaining, 9% will remain after this step, so the scale is 9% / 10% = 90%. | ||||||
|  |   // At time = 1.00, 91% complete, 9% remaining, 0% will remain after this step, so the scale is 0% / 9% = 0%. | ||||||
|  |   // | ||||||
|  |   // Because the color values are quantized to 8 bit resolution after each step, the transition may appear | ||||||
|  |   // non-linear when applying small deltas. | ||||||
|  |  | ||||||
|   float denom = (1.0f - smoothed_progress); |   if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) { | ||||||
|   float alpha = denom == 0.0f ? 1.0f : (smoothed_progress - this->last_transition_progress_) / denom; |     int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); | ||||||
|  |     for (auto led : this->light_) { | ||||||
|   // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length |       led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), | ||||||
|   // We solve this by accumulating the fractional part of the alpha over time. |                    subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), | ||||||
|   float alpha255 = alpha * 255.0f; |                    subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), | ||||||
|   float alpha255int = floorf(alpha255); |                    subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); | ||||||
|   float alpha255remainder = alpha255 - alpha255int; |     } | ||||||
|  |     this->last_transition_progress_ = smoothed_progress; | ||||||
|   this->accumulated_alpha_ += alpha255remainder; |     this->light_.schedule_show(); | ||||||
|   float alpha_add = floorf(this->accumulated_alpha_); |  | ||||||
|   this->accumulated_alpha_ -= alpha_add; |  | ||||||
|  |  | ||||||
|   alpha255 += alpha_add; |  | ||||||
|   alpha255 = clamp(alpha255, 0.0f, 255.0f); |  | ||||||
|   auto alpha8 = static_cast<uint8_t>(alpha255); |  | ||||||
|  |  | ||||||
|   if (alpha8 != 0) { |  | ||||||
|     uint8_t inv_alpha8 = 255 - alpha8; |  | ||||||
|     Color add = this->target_color_ * alpha8; |  | ||||||
|  |  | ||||||
|     for (auto led : this->light_) |  | ||||||
|       led.set(add + led.get() * inv_alpha8); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->last_transition_progress_ = smoothed_progress; |  | ||||||
|   this->light_.schedule_show(); |  | ||||||
|  |  | ||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -113,7 +113,6 @@ class AddressableLightTransformer : public LightTransformer { | |||||||
|  protected: |  protected: | ||||||
|   AddressableLight &light_; |   AddressableLight &light_; | ||||||
|   float last_transition_progress_{0.0f}; |   float last_transition_progress_{0.0f}; | ||||||
|   float accumulated_alpha_{0.0f}; |  | ||||||
|   Color target_color_{}; |   Color target_color_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,34 +8,35 @@ | |||||||
| 
 | 
 | ||||||
| namespace esphome { | namespace esphome { | ||||||
| 
 | 
 | ||||||
| /// Generic bitmask for storing a set of enum values efficiently.
 | /// Generic bitmask for storing a finite set of discrete values efficiently.
 | ||||||
| /// Replaces std::set<EnumType> to eliminate red-black tree overhead (~586 bytes per instantiation).
 | /// Replaces std::set<ValueType> to eliminate red-black tree overhead (~586 bytes per instantiation).
 | ||||||
| ///
 | ///
 | ||||||
| /// Template parameters:
 | /// Template parameters:
 | ||||||
| ///   EnumType: The enum type to store (must be uint8_t-based)
 | ///   ValueType: The type to store (typically enum, but can be any discrete bounded type)
 | ||||||
| ///   MaxBits: Maximum number of bits needed (auto-selects uint8_t/uint16_t/uint32_t)
 | ///   MaxBits: Maximum number of bits needed (auto-selects uint8_t/uint16_t/uint32_t)
 | ||||||
| ///
 | ///
 | ||||||
| /// Requirements:
 | /// Requirements:
 | ||||||
| ///   - EnumType must be an enum with sequential values starting from 0
 | ///   - ValueType must have a bounded discrete range that maps to bit positions
 | ||||||
| ///   - Specialization must provide enum_to_bit() and bit_to_enum() static methods
 | ///   - Specialization must provide value_to_bit() and bit_to_value() static methods
 | ||||||
| ///   - MaxBits must be sufficient to hold all enum values
 | ///   - MaxBits must be sufficient to hold all possible values
 | ||||||
| ///
 | ///
 | ||||||
| /// Example usage:
 | /// Example usage:
 | ||||||
| ///   using ClimateModeMask = EnumBitmask<ClimateMode, 8>;
 | ///   using ClimateModeMask = FiniteSetMask<ClimateMode, 8>;
 | ||||||
| ///   ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
 | ///   ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
 | ||||||
| ///   if (modes.count(CLIMATE_MODE_HEAT)) { ... }
 | ///   if (modes.count(CLIMATE_MODE_HEAT)) { ... }
 | ||||||
| ///   for (auto mode : modes) { ... }  // Iterate over set bits
 | ///   for (auto mode : modes) { ... }  // Iterate over set bits
 | ||||||
| ///
 | ///
 | ||||||
| /// For complete usage examples with template specializations, see:
 | /// For complete usage examples with template specializations, see:
 | ||||||
| ///   - esphome/components/light/color_mode.h (ColorMode example)
 | ///   - esphome/components/light/color_mode.h (ColorMode enum example)
 | ||||||
| ///
 | ///
 | ||||||
| /// Design notes:
 | /// Design notes:
 | ||||||
| ///   - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t)
 | ///   - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t)
 | ||||||
| ///   - Iterator converts bit positions to actual enum values during traversal
 | ///   - Iterator converts bit positions to actual values during traversal
 | ||||||
| ///   - All operations are constexpr-compatible for compile-time initialization
 | ///   - All operations are constexpr-compatible for compile-time initialization
 | ||||||
| ///   - Drop-in replacement for std::set<EnumType> with simpler API
 | ///   - Drop-in replacement for std::set<ValueType> with simpler API
 | ||||||
|  | ///   - Despite the name, works with any discrete bounded type, not just enums
 | ||||||
| ///
 | ///
 | ||||||
| template<typename EnumType, int MaxBits = 16> class EnumBitmask { | template<typename ValueType, int MaxBits = 16> class FiniteSetMask { | ||||||
|  public: |  public: | ||||||
|   // Automatic bitmask type selection based on MaxBits
 |   // Automatic bitmask type selection based on MaxBits
 | ||||||
|   // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t
 |   // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t
 | ||||||
| @@ -43,38 +44,38 @@ template<typename EnumType, int MaxBits = 16> class EnumBitmask { | |||||||
|       typename std::conditional<(MaxBits <= 8), uint8_t, |       typename std::conditional<(MaxBits <= 8), uint8_t, | ||||||
|                                 typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; |                                 typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; | ||||||
| 
 | 
 | ||||||
|   constexpr EnumBitmask() = default; |   constexpr FiniteSetMask() = default; | ||||||
| 
 | 
 | ||||||
|   /// Construct from initializer list: {VALUE1, VALUE2, ...}
 |   /// Construct from initializer list: {VALUE1, VALUE2, ...}
 | ||||||
|   constexpr EnumBitmask(std::initializer_list<EnumType> values) { |   constexpr FiniteSetMask(std::initializer_list<ValueType> values) { | ||||||
|     for (auto value : values) { |     for (auto value : values) { | ||||||
|       this->insert(value); |       this->insert(value); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Add a single enum value to the set (std::set compatibility)
 |   /// Add a single value to the set (std::set compatibility)
 | ||||||
|   constexpr void insert(EnumType value) { this->mask_ |= (static_cast<bitmask_t>(1) << enum_to_bit(value)); } |   constexpr void insert(ValueType value) { this->mask_ |= (static_cast<bitmask_t>(1) << value_to_bit(value)); } | ||||||
| 
 | 
 | ||||||
|   /// Add multiple enum values from initializer list
 |   /// Add multiple values from initializer list
 | ||||||
|   constexpr void insert(std::initializer_list<EnumType> values) { |   constexpr void insert(std::initializer_list<ValueType> values) { | ||||||
|     for (auto value : values) { |     for (auto value : values) { | ||||||
|       this->insert(value); |       this->insert(value); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Remove an enum value from the set (std::set compatibility)
 |   /// Remove a value from the set (std::set compatibility)
 | ||||||
|   constexpr void erase(EnumType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << enum_to_bit(value)); } |   constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << value_to_bit(value)); } | ||||||
| 
 | 
 | ||||||
|   /// Clear all values from the set
 |   /// Clear all values from the set
 | ||||||
|   constexpr void clear() { this->mask_ = 0; } |   constexpr void clear() { this->mask_ = 0; } | ||||||
| 
 | 
 | ||||||
|   /// Check if the set contains a specific enum value (std::set compatibility)
 |   /// Check if the set contains a specific value (std::set compatibility)
 | ||||||
|   /// Returns 1 if present, 0 if not (same as std::set for unique elements)
 |   /// Returns 1 if present, 0 if not (same as std::set for unique elements)
 | ||||||
|   constexpr size_t count(EnumType value) const { |   constexpr size_t count(ValueType value) const { | ||||||
|     return (this->mask_ & (static_cast<bitmask_t>(1) << enum_to_bit(value))) != 0 ? 1 : 0; |     return (this->mask_ & (static_cast<bitmask_t>(1) << value_to_bit(value))) != 0 ? 1 : 0; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Count the number of enum values in the set
 |   /// Count the number of values in the set
 | ||||||
|   constexpr size_t size() const { |   constexpr size_t size() const { | ||||||
|     // Brian Kernighan's algorithm - efficient for sparse bitmasks
 |     // Brian Kernighan's algorithm - efficient for sparse bitmasks
 | ||||||
|     // Typical case: 2-4 modes out of 10 possible
 |     // Typical case: 2-4 modes out of 10 possible
 | ||||||
| @@ -91,51 +92,52 @@ template<typename EnumType, int MaxBits = 16> class EnumBitmask { | |||||||
|   constexpr bool empty() const { return this->mask_ == 0; } |   constexpr bool empty() const { return this->mask_ == 0; } | ||||||
| 
 | 
 | ||||||
|   /// Iterator support for range-based for loops and API encoding
 |   /// Iterator support for range-based for loops and API encoding
 | ||||||
|   /// Iterates over set bits and converts bit positions to enum values
 |   /// Iterates over set bits and converts bit positions to values
 | ||||||
|  |   /// Optimization: removes bits from mask as we iterate
 | ||||||
|   class Iterator { |   class Iterator { | ||||||
|    public: |    public: | ||||||
|     using iterator_category = std::forward_iterator_tag; |     using iterator_category = std::forward_iterator_tag; | ||||||
|     using value_type = EnumType; |     using value_type = ValueType; | ||||||
|     using difference_type = std::ptrdiff_t; |     using difference_type = std::ptrdiff_t; | ||||||
|     using pointer = const EnumType *; |     using pointer = const ValueType *; | ||||||
|     using reference = EnumType; |     using reference = ValueType; | ||||||
| 
 | 
 | ||||||
|     constexpr Iterator(bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } |     constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {} | ||||||
| 
 | 
 | ||||||
|     constexpr EnumType operator*() const { return bit_to_enum(bit_); } |     constexpr ValueType operator*() const { | ||||||
|  |       // Return value for the first set bit
 | ||||||
|  |       return bit_to_value(find_next_set_bit(mask_, 0)); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     constexpr Iterator &operator++() { |     constexpr Iterator &operator++() { | ||||||
|       ++bit_; |       // Clear the lowest set bit (Brian Kernighan's algorithm)
 | ||||||
|       advance_to_next_set_bit_(); |       mask_ &= mask_ - 1; | ||||||
|       return *this; |       return *this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } |     constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; } | ||||||
| 
 | 
 | ||||||
|     constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } |     constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } | ||||||
| 
 | 
 | ||||||
|    private: |    private: | ||||||
|     constexpr void advance_to_next_set_bit_() { bit_ = find_next_set_bit(mask_, bit_); } |  | ||||||
| 
 |  | ||||||
|     bitmask_t mask_; |     bitmask_t mask_; | ||||||
|     int bit_; |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   constexpr Iterator begin() const { return Iterator(mask_, 0); } |   constexpr Iterator begin() const { return Iterator(mask_); } | ||||||
|   constexpr Iterator end() const { return Iterator(mask_, MaxBits); } |   constexpr Iterator end() const { return Iterator(0); } | ||||||
| 
 | 
 | ||||||
|   /// Get the raw bitmask value for optimized operations
 |   /// Get the raw bitmask value for optimized operations
 | ||||||
|   constexpr bitmask_t get_mask() const { return this->mask_; } |   constexpr bitmask_t get_mask() const { return this->mask_; } | ||||||
| 
 | 
 | ||||||
|   /// Check if a specific enum value is present in a raw bitmask
 |   /// Check if a specific value is present in a raw bitmask
 | ||||||
|   /// Useful for checking intersection results without creating temporary objects
 |   /// Useful for checking intersection results without creating temporary objects
 | ||||||
|   static constexpr bool mask_contains(bitmask_t mask, EnumType value) { |   static constexpr bool mask_contains(bitmask_t mask, ValueType value) { | ||||||
|     return (mask & (static_cast<bitmask_t>(1) << enum_to_bit(value))) != 0; |     return (mask & (static_cast<bitmask_t>(1) << value_to_bit(value))) != 0; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Get the first enum value from a raw bitmask
 |   /// Get the first value from a raw bitmask
 | ||||||
|   /// Used for optimizing intersection logic (e.g., "pick first suitable mode")
 |   /// Used for optimizing intersection logic (e.g., "pick first suitable mode")
 | ||||||
|   static constexpr EnumType first_value_from_mask(bitmask_t mask) { return bit_to_enum(find_next_set_bit(mask, 0)); } |   static constexpr ValueType first_value_from_mask(bitmask_t mask) { return bit_to_value(find_next_set_bit(mask, 0)); } | ||||||
| 
 | 
 | ||||||
|   /// Find the next set bit in a bitmask starting from a given position
 |   /// Find the next set bit in a bitmask starting from a given position
 | ||||||
|   /// Returns the bit position, or MaxBits if no more bits are set
 |   /// Returns the bit position, or MaxBits if no more bits are set
 | ||||||
| @@ -149,9 +151,9 @@ template<typename EnumType, int MaxBits = 16> class EnumBitmask { | |||||||
| 
 | 
 | ||||||
|  protected: |  protected: | ||||||
|   // Must be provided by template specialization
 |   // Must be provided by template specialization
 | ||||||
|   // These convert between enum values and bit positions (0, 1, 2, ...)
 |   // These convert between values and bit positions (0, 1, 2, ...)
 | ||||||
|   static constexpr int enum_to_bit(EnumType value); |   static constexpr int value_to_bit(ValueType value); | ||||||
|   static EnumType bit_to_enum(int bit);  // Not constexpr due to static array limitation in C++20
 |   static ValueType bit_to_value(int bit);  // Not constexpr: array indexing with runtime bounds checking
 | ||||||
| 
 | 
 | ||||||
|   bitmask_t mask_{0}; |   bitmask_t mask_{0}; | ||||||
| }; | }; | ||||||
| @@ -336,7 +336,7 @@ def _component_has_tests(component: str) -> bool: | |||||||
|     Returns: |     Returns: | ||||||
|         True if the component has test YAML files |         True if the component has test YAML files | ||||||
|     """ |     """ | ||||||
|     return bool(get_component_test_files(component)) |     return bool(get_component_test_files(component, all_variants=True)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _select_platform_by_preference( | def _select_platform_by_preference( | ||||||
| @@ -496,7 +496,7 @@ def detect_memory_impact_config( | |||||||
|  |  | ||||||
|     for component in sorted(changed_component_set): |     for component in sorted(changed_component_set): | ||||||
|         # Look for test files on preferred platforms |         # Look for test files on preferred platforms | ||||||
|         test_files = get_component_test_files(component) |         test_files = get_component_test_files(component, all_variants=True) | ||||||
|         if not test_files: |         if not test_files: | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|   | |||||||
| @@ -49,9 +49,9 @@ def has_test_files(component_name: str, tests_dir: Path) -> bool: | |||||||
|         tests_dir: Path to tests/components directory (unused, kept for compatibility) |         tests_dir: Path to tests/components directory (unused, kept for compatibility) | ||||||
|  |  | ||||||
|     Returns: |     Returns: | ||||||
|         True if the component has test.*.yaml files |         True if the component has test.*.yaml or test-*.yaml files | ||||||
|     """ |     """ | ||||||
|     return bool(get_component_test_files(component_name)) |     return bool(get_component_test_files(component_name, all_variants=True)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_intelligent_batches( | def create_intelligent_batches( | ||||||
|   | |||||||
| @@ -574,6 +574,105 @@ def test_main_filters_components_without_tests( | |||||||
|     assert output["memory_impact"]["should_run"] == "false" |     assert output["memory_impact"]["should_run"] == "false" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_main_detects_components_with_variant_tests( | ||||||
|  |     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_changed_files: Mock, | ||||||
|  |     capsys: pytest.CaptureFixture[str], | ||||||
|  |     tmp_path: Path, | ||||||
|  |     monkeypatch: pytest.MonkeyPatch, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that components with only variant test files (test-*.yaml) are detected. | ||||||
|  |  | ||||||
|  |     This test verifies the fix for components like improv_serial, ethernet, mdns, | ||||||
|  |     improv_base, and safe_mode which only have variant test files (test-*.yaml) | ||||||
|  |     instead of base test files (test.*.yaml). | ||||||
|  |     """ | ||||||
|  |     # Ensure we're not in GITHUB_ACTIONS mode for this test | ||||||
|  |     monkeypatch.delenv("GITHUB_ACTIONS", raising=False) | ||||||
|  |  | ||||||
|  |     mock_should_run_integration_tests.return_value = False | ||||||
|  |     mock_should_run_clang_tidy.return_value = False | ||||||
|  |     mock_should_run_clang_format.return_value = False | ||||||
|  |     mock_should_run_python_linters.return_value = False | ||||||
|  |  | ||||||
|  |     # Mock changed_files to return component files | ||||||
|  |     mock_changed_files.return_value = [ | ||||||
|  |         "esphome/components/improv_serial/improv_serial.cpp", | ||||||
|  |         "esphome/components/ethernet/ethernet.cpp", | ||||||
|  |         "esphome/components/no_tests/component.cpp", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     # Create test directory structure | ||||||
|  |     tests_dir = tmp_path / "tests" / "components" | ||||||
|  |  | ||||||
|  |     # improv_serial has only variant tests (like the real component) | ||||||
|  |     improv_serial_dir = tests_dir / "improv_serial" | ||||||
|  |     improv_serial_dir.mkdir(parents=True) | ||||||
|  |     (improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: config") | ||||||
|  |     (improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: config") | ||||||
|  |     (improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: config") | ||||||
|  |  | ||||||
|  |     # ethernet also has only variant tests | ||||||
|  |     ethernet_dir = tests_dir / "ethernet" | ||||||
|  |     ethernet_dir.mkdir(parents=True) | ||||||
|  |     (ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: config") | ||||||
|  |     (ethernet_dir / "test-dhcp.esp32-idf.yaml").write_text("test: config") | ||||||
|  |  | ||||||
|  |     # no_tests component has no test files at all | ||||||
|  |     no_tests_dir = tests_dir / "no_tests" | ||||||
|  |     no_tests_dir.mkdir(parents=True) | ||||||
|  |  | ||||||
|  |     # Mock root_path to use tmp_path (need to patch both determine_jobs and helpers) | ||||||
|  |     with ( | ||||||
|  |         patch.object(determine_jobs, "root_path", str(tmp_path)), | ||||||
|  |         patch.object(helpers, "root_path", str(tmp_path)), | ||||||
|  |         patch("sys.argv", ["determine-jobs.py"]), | ||||||
|  |         patch.object( | ||||||
|  |             determine_jobs, | ||||||
|  |             "get_changed_components", | ||||||
|  |             return_value=["improv_serial", "ethernet", "no_tests"], | ||||||
|  |         ), | ||||||
|  |         patch.object( | ||||||
|  |             determine_jobs, | ||||||
|  |             "filter_component_and_test_files", | ||||||
|  |             side_effect=lambda f: f.startswith("esphome/components/"), | ||||||
|  |         ), | ||||||
|  |         patch.object( | ||||||
|  |             determine_jobs, | ||||||
|  |             "get_components_with_dependencies", | ||||||
|  |             side_effect=lambda files, deps: ( | ||||||
|  |                 ["improv_serial", "ethernet"] | ||||||
|  |                 if not deps | ||||||
|  |                 else ["improv_serial", "ethernet", "no_tests"] | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |         patch.object(determine_jobs, "changed_files", return_value=[]), | ||||||
|  |     ): | ||||||
|  |         # Clear the cache since we're mocking root_path | ||||||
|  |         determine_jobs._component_has_tests.cache_clear() | ||||||
|  |         determine_jobs.main() | ||||||
|  |  | ||||||
|  |     # Check output | ||||||
|  |     captured = capsys.readouterr() | ||||||
|  |     output = json.loads(captured.out) | ||||||
|  |  | ||||||
|  |     # changed_components should have all components | ||||||
|  |     assert set(output["changed_components"]) == { | ||||||
|  |         "improv_serial", | ||||||
|  |         "ethernet", | ||||||
|  |         "no_tests", | ||||||
|  |     } | ||||||
|  |     # changed_components_with_tests should include components with variant tests | ||||||
|  |     assert set(output["changed_components_with_tests"]) == {"improv_serial", "ethernet"} | ||||||
|  |     # component_test_count should be 2 (improv_serial and ethernet) | ||||||
|  |     assert output["component_test_count"] == 2 | ||||||
|  |     # no_tests should be excluded since it has no test files | ||||||
|  |     assert "no_tests" not in output["changed_components_with_tests"] | ||||||
|  |  | ||||||
|  |  | ||||||
| # Tests for detect_memory_impact_config function | # Tests for detect_memory_impact_config function | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -785,6 +884,51 @@ def test_detect_memory_impact_config_skips_base_bus_components(tmp_path: Path) - | |||||||
|     assert "i2c" not in result["components"] |     assert "i2c" not in result["components"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: | ||||||
|  |     """Test memory impact detection for components with only variant test files. | ||||||
|  |  | ||||||
|  |     This verifies that memory impact analysis works correctly for components like | ||||||
|  |     improv_serial, ethernet, mdns, etc. which only have variant test files | ||||||
|  |     (test-*.yaml) instead of base test files (test.*.yaml). | ||||||
|  |     """ | ||||||
|  |     # Create test directory structure | ||||||
|  |     tests_dir = tmp_path / "tests" / "components" | ||||||
|  |  | ||||||
|  |     # improv_serial with only variant tests | ||||||
|  |     improv_serial_dir = tests_dir / "improv_serial" | ||||||
|  |     improv_serial_dir.mkdir(parents=True) | ||||||
|  |     (improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: improv") | ||||||
|  |     (improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: improv") | ||||||
|  |     (improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: improv") | ||||||
|  |  | ||||||
|  |     # ethernet with only variant tests | ||||||
|  |     ethernet_dir = tests_dir / "ethernet" | ||||||
|  |     ethernet_dir.mkdir(parents=True) | ||||||
|  |     (ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: ethernet") | ||||||
|  |     (ethernet_dir / "test-dhcp.esp32-c3-idf.yaml").write_text("test: ethernet") | ||||||
|  |  | ||||||
|  |     # Mock changed_files to return both components | ||||||
|  |     with ( | ||||||
|  |         patch.object(determine_jobs, "root_path", str(tmp_path)), | ||||||
|  |         patch.object(helpers, "root_path", str(tmp_path)), | ||||||
|  |         patch.object(determine_jobs, "changed_files") as mock_changed_files, | ||||||
|  |     ): | ||||||
|  |         mock_changed_files.return_value = [ | ||||||
|  |             "esphome/components/improv_serial/improv_serial.cpp", | ||||||
|  |             "esphome/components/ethernet/ethernet.cpp", | ||||||
|  |         ] | ||||||
|  |         determine_jobs._component_has_tests.cache_clear() | ||||||
|  |  | ||||||
|  |         result = determine_jobs.detect_memory_impact_config() | ||||||
|  |  | ||||||
|  |     # Should detect both components even though they only have variant tests | ||||||
|  |     assert result["should_run"] == "true" | ||||||
|  |     assert set(result["components"]) == {"improv_serial", "ethernet"} | ||||||
|  |     # Both components support esp32-idf | ||||||
|  |     assert result["platform"] == "esp32-idf" | ||||||
|  |     assert result["use_merged_config"] == "true" | ||||||
|  |  | ||||||
|  |  | ||||||
| # Tests for clang-tidy split mode logic | # Tests for clang-tidy split mode logic | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user