mirror of
https://github.com/esphome/esphome.git
synced 2025-10-27 13:13:50 +00:00
Merge branch 'enum_mask_helper' into integration
This commit is contained in:
@@ -524,13 +524,23 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
|||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
call.set_target_humidity(this->target_humidity);
|
call.set_target_humidity(this->target_humidity);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
|
if (this->uses_custom_fan_mode) {
|
||||||
|
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||||
|
call.fan_mode_.reset();
|
||||||
|
call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
|
||||||
|
}
|
||||||
|
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||||
call.set_fan_mode(this->fan_mode);
|
call.set_fan_mode(this->fan_mode);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
if (this->uses_custom_preset) {
|
||||||
|
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||||
|
call.preset_.reset();
|
||||||
|
call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
|
||||||
|
}
|
||||||
|
} else if (traits.supports_preset(this->preset)) {
|
||||||
call.set_preset(this->preset);
|
call.set_preset(this->preset);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes()) {
|
if (traits.supports_swing_mode(this->swing_mode)) {
|
||||||
call.set_swing_mode(this->swing_mode);
|
call.set_swing_mode(this->swing_mode);
|
||||||
}
|
}
|
||||||
return call;
|
return call;
|
||||||
@@ -549,41 +559,25 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
|||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
climate->target_humidity = this->target_humidity;
|
climate->target_humidity = this->target_humidity;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
|
if (this->uses_custom_fan_mode) {
|
||||||
|
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||||
|
climate->fan_mode.reset();
|
||||||
|
climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
|
||||||
|
}
|
||||||
|
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||||
climate->fan_mode = this->fan_mode;
|
climate->fan_mode = this->fan_mode;
|
||||||
|
climate->custom_fan_mode.reset();
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
|
if (this->uses_custom_preset) {
|
||||||
// std::set has consistent order (lexicographic for strings)
|
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||||
const auto &modes = traits.get_supported_custom_fan_modes();
|
climate->preset.reset();
|
||||||
if (custom_fan_mode < modes.size()) {
|
climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
|
||||||
size_t i = 0;
|
|
||||||
for (const auto &mode : modes) {
|
|
||||||
if (i == this->custom_fan_mode) {
|
|
||||||
climate->custom_fan_mode = mode;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else if (traits.supports_preset(this->preset)) {
|
||||||
if (traits.get_supports_presets() && !this->uses_custom_preset) {
|
|
||||||
climate->preset = this->preset;
|
climate->preset = this->preset;
|
||||||
|
climate->custom_preset.reset();
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
|
if (traits.supports_swing_mode(this->swing_mode)) {
|
||||||
// std::set has consistent order (lexicographic for strings)
|
|
||||||
const auto &presets = traits.get_supported_custom_presets();
|
|
||||||
if (custom_preset < presets.size()) {
|
|
||||||
size_t i = 0;
|
|
||||||
for (const auto &preset : presets) {
|
|
||||||
if (i == this->custom_preset) {
|
|
||||||
climate->custom_preset = preset;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (traits.get_supports_swing_modes()) {
|
|
||||||
climate->swing_mode = this->swing_mode;
|
climate->swing_mode = this->swing_mode;
|
||||||
}
|
}
|
||||||
climate->publish_state();
|
climate->publish_state();
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class Climate;
|
|||||||
class ClimateCall {
|
class ClimateCall {
|
||||||
public:
|
public:
|
||||||
explicit ClimateCall(Climate *parent) : parent_(parent) {}
|
explicit ClimateCall(Climate *parent) : parent_(parent) {}
|
||||||
|
friend struct ClimateDeviceRestoreState;
|
||||||
|
|
||||||
/// Set the mode of the climate device.
|
/// Set the mode of the climate device.
|
||||||
ClimateCall &set_mode(ClimateMode mode);
|
ClimateCall &set_mode(ClimateMode mode);
|
||||||
|
|||||||
@@ -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_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "esphome/core/enum_bitmask.h"
|
#include "esphome/core/finite_set_mask.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace light {
|
namespace light {
|
||||||
@@ -105,7 +105,7 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
|
|||||||
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
|
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type alias for raw color mode bitmask values (retained for compatibility)
|
// Type alias for raw color mode bitmask values
|
||||||
using color_mode_bitmask_t = uint16_t;
|
using color_mode_bitmask_t = uint16_t;
|
||||||
|
|
||||||
// Number of ColorMode enum values
|
// Number of ColorMode enum values
|
||||||
@@ -127,8 +127,8 @@ constexpr ColorMode COLOR_MODE_LOOKUP[COLOR_MODE_BITMASK_SIZE] = {
|
|||||||
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
|
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type alias for ColorMode bitmask using generic EnumBitmask template
|
// Type alias for ColorMode bitmask using generic FiniteSetMask template
|
||||||
using ColorModeMask = EnumBitmask<ColorMode, COLOR_MODE_BITMASK_SIZE>;
|
using ColorModeMask = FiniteSetMask<ColorMode, COLOR_MODE_BITMASK_SIZE>;
|
||||||
|
|
||||||
// Number of ColorCapability enum values
|
// Number of ColorCapability enum values
|
||||||
constexpr int COLOR_CAPABILITY_COUNT = 6;
|
constexpr int COLOR_CAPABILITY_COUNT = 6;
|
||||||
@@ -159,16 +159,21 @@ constexpr uint16_t CAPABILITY_BITMASKS[] = {
|
|||||||
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
|
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Check if any mode in the bitmask has a specific capability
|
/**
|
||||||
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
|
* @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS
|
||||||
inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) {
|
* lookup.
|
||||||
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
|
*
|
||||||
// ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
|
* This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5).
|
||||||
// We need to convert the power-of-2 value to an index
|
* Used to index into the CAPABILITY_BITMASKS lookup table.
|
||||||
|
*
|
||||||
|
* @param capability A ColorCapability enum value (must be a power of 2).
|
||||||
|
* @return The corresponding array index (0-based).
|
||||||
|
*/
|
||||||
|
inline int capability_to_index(ColorCapability capability) {
|
||||||
uint8_t cap_val = static_cast<uint8_t>(capability);
|
uint8_t cap_val = static_cast<uint8_t>(capability);
|
||||||
#if defined(__GNUC__) || defined(__clang__)
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
|
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
|
||||||
int index = __builtin_ctz(cap_val);
|
return __builtin_ctz(cap_val);
|
||||||
#else
|
#else
|
||||||
// Fallback for compilers without __builtin_ctz
|
// Fallback for compilers without __builtin_ctz
|
||||||
int index = 0;
|
int index = 0;
|
||||||
@@ -176,8 +181,15 @@ inline bool has_capability(const ColorModeMask &mask, ColorCapability capability
|
|||||||
cap_val >>= 1;
|
cap_val >>= 1;
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
|
return index;
|
||||||
#endif
|
#endif
|
||||||
return (mask.get_mask() & CAPABILITY_BITMASKS[index]) != 0;
|
}
|
||||||
|
|
||||||
|
/// Check if any mode in the bitmask has a specific capability
|
||||||
|
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
|
||||||
|
inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) {
|
||||||
|
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
|
||||||
|
return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace light
|
} // namespace light
|
||||||
@@ -186,7 +198,7 @@ inline bool has_capability(const ColorModeMask &mask, ColorCapability capability
|
|||||||
// Template specializations for ColorMode must be in global namespace
|
// Template specializations for ColorMode must be in global namespace
|
||||||
//
|
//
|
||||||
// C++ requires template specializations to be declared in the same namespace as the
|
// C++ requires template specializations to be declared in the same namespace as the
|
||||||
// original template. Since EnumBitmask is in the esphome namespace (not esphome::light),
|
// original template. Since FiniteSetMask is in the esphome namespace (not esphome::light),
|
||||||
// we must provide these specializations at global scope with fully-qualified names.
|
// we must provide these specializations at global scope with fully-qualified names.
|
||||||
//
|
//
|
||||||
// These specializations define how ColorMode enum values map to/from bit positions.
|
// These specializations define how ColorMode enum values map to/from bit positions.
|
||||||
@@ -194,7 +206,7 @@ inline bool has_capability(const ColorModeMask &mask, ColorCapability capability
|
|||||||
/// Map ColorMode enum values to bit positions (0-9)
|
/// Map ColorMode enum values to bit positions (0-9)
|
||||||
/// Bit positions follow the enum declaration order
|
/// Bit positions follow the enum declaration order
|
||||||
template<>
|
template<>
|
||||||
constexpr int esphome::EnumBitmask<esphome::light::ColorMode, esphome::light::COLOR_MODE_BITMASK_SIZE>::enum_to_bit(
|
constexpr int esphome::FiniteSetMask<esphome::light::ColorMode, esphome::light::COLOR_MODE_BITMASK_SIZE>::value_to_bit(
|
||||||
esphome::light::ColorMode mode) {
|
esphome::light::ColorMode mode) {
|
||||||
// Linear search through COLOR_MODE_LOOKUP array
|
// Linear search through COLOR_MODE_LOOKUP array
|
||||||
// Compiler optimizes this to efficient code since array is constexpr
|
// Compiler optimizes this to efficient code since array is constexpr
|
||||||
@@ -208,8 +220,8 @@ constexpr int esphome::EnumBitmask<esphome::light::ColorMode, esphome::light::CO
|
|||||||
/// Map bit positions (0-9) to ColorMode enum values
|
/// Map bit positions (0-9) to ColorMode enum values
|
||||||
/// Bit positions follow the enum declaration order
|
/// Bit positions follow the enum declaration order
|
||||||
template<>
|
template<>
|
||||||
inline esphome::light::ColorMode esphome::EnumBitmask<esphome::light::ColorMode,
|
inline esphome::light::ColorMode esphome::FiniteSetMask<
|
||||||
esphome::light::COLOR_MODE_BITMASK_SIZE>::bit_to_enum(int bit) {
|
esphome::light::ColorMode, esphome::light::COLOR_MODE_BITMASK_SIZE>::bit_to_value(int bit) {
|
||||||
return (bit >= 0 && bit < esphome::light::COLOR_MODE_BITMASK_SIZE) ? esphome::light::COLOR_MODE_LOOKUP[bit]
|
return (bit >= 0 && bit < esphome::light::COLOR_MODE_BITMASK_SIZE) ? esphome::light::COLOR_MODE_LOOKUP[bit]
|
||||||
: esphome::light::ColorMode::UNKNOWN;
|
: esphome::light::ColorMode::UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <initializer_list>
|
|
||||||
#include <iterator>
|
|
||||||
#include <type_traits>
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
|
|
||||||
/// Generic bitmask for storing a set of enum values efficiently.
|
|
||||||
/// Replaces std::set<EnumType> to eliminate red-black tree overhead (~586 bytes per instantiation).
|
|
||||||
///
|
|
||||||
/// Template parameters:
|
|
||||||
/// EnumType: The enum type to store (must be uint8_t-based)
|
|
||||||
/// MaxBits: Maximum number of bits needed (auto-selects uint8_t/uint16_t/uint32_t)
|
|
||||||
///
|
|
||||||
/// Requirements:
|
|
||||||
/// - EnumType must be an enum with sequential values starting from 0
|
|
||||||
/// - Specialization must provide enum_to_bit() and bit_to_enum() static methods
|
|
||||||
/// - MaxBits must be sufficient to hold all enum values
|
|
||||||
///
|
|
||||||
/// Example usage:
|
|
||||||
/// using ClimateModeMask = EnumBitmask<ClimateMode, 8>;
|
|
||||||
/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
|
|
||||||
/// if (modes.count(CLIMATE_MODE_HEAT)) { ... }
|
|
||||||
/// for (auto mode : modes) { ... } // Iterate over set bits
|
|
||||||
///
|
|
||||||
/// For complete usage examples with template specializations, see:
|
|
||||||
/// - esphome/components/light/color_mode.h (ColorMode example)
|
|
||||||
///
|
|
||||||
/// Design notes:
|
|
||||||
/// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t)
|
|
||||||
/// - Iterator converts bit positions to actual enum values during traversal
|
|
||||||
/// - All operations are constexpr-compatible for compile-time initialization
|
|
||||||
/// - Drop-in replacement for std::set<EnumType> with simpler API
|
|
||||||
///
|
|
||||||
template<typename EnumType, int MaxBits = 16> class EnumBitmask {
|
|
||||||
public:
|
|
||||||
// Automatic bitmask type selection based on MaxBits
|
|
||||||
// ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t
|
|
||||||
using bitmask_t =
|
|
||||||
typename std::conditional<(MaxBits <= 8), uint8_t,
|
|
||||||
typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type;
|
|
||||||
|
|
||||||
constexpr EnumBitmask() = default;
|
|
||||||
|
|
||||||
/// Construct from initializer list: {VALUE1, VALUE2, ...}
|
|
||||||
constexpr EnumBitmask(std::initializer_list<EnumType> values) {
|
|
||||||
for (auto value : values) {
|
|
||||||
this->insert(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a single enum value to the set (std::set compatibility)
|
|
||||||
constexpr void insert(EnumType value) { this->mask_ |= (static_cast<bitmask_t>(1) << enum_to_bit(value)); }
|
|
||||||
|
|
||||||
/// Add multiple enum values from initializer list
|
|
||||||
constexpr void insert(std::initializer_list<EnumType> values) {
|
|
||||||
for (auto value : values) {
|
|
||||||
this->insert(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove an enum value from the set (std::set compatibility)
|
|
||||||
constexpr void erase(EnumType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << enum_to_bit(value)); }
|
|
||||||
|
|
||||||
/// Clear all values from the set
|
|
||||||
constexpr void clear() { this->mask_ = 0; }
|
|
||||||
|
|
||||||
/// Check if the set contains a specific enum value (std::set compatibility)
|
|
||||||
/// Returns 1 if present, 0 if not (same as std::set for unique elements)
|
|
||||||
constexpr size_t count(EnumType value) const {
|
|
||||||
return (this->mask_ & (static_cast<bitmask_t>(1) << enum_to_bit(value))) != 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count the number of enum values in the set
|
|
||||||
constexpr size_t size() const {
|
|
||||||
// Brian Kernighan's algorithm - efficient for sparse bitmasks
|
|
||||||
// Typical case: 2-4 modes out of 10 possible
|
|
||||||
bitmask_t n = this->mask_;
|
|
||||||
size_t count = 0;
|
|
||||||
while (n) {
|
|
||||||
n &= n - 1; // Clear the least significant set bit
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the set is empty
|
|
||||||
constexpr bool empty() const { return this->mask_ == 0; }
|
|
||||||
|
|
||||||
/// Iterator support for range-based for loops and API encoding
|
|
||||||
/// Iterates over set bits and converts bit positions to enum values
|
|
||||||
class Iterator {
|
|
||||||
public:
|
|
||||||
using iterator_category = std::forward_iterator_tag;
|
|
||||||
using value_type = EnumType;
|
|
||||||
using difference_type = std::ptrdiff_t;
|
|
||||||
using pointer = const EnumType *;
|
|
||||||
using reference = EnumType;
|
|
||||||
|
|
||||||
constexpr Iterator(bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
|
|
||||||
|
|
||||||
constexpr EnumType operator*() const { return bit_to_enum(bit_); }
|
|
||||||
|
|
||||||
constexpr Iterator &operator++() {
|
|
||||||
++bit_;
|
|
||||||
advance_to_next_set_bit_();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
|
|
||||||
|
|
||||||
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
constexpr void advance_to_next_set_bit_() { bit_ = find_next_set_bit(mask_, bit_); }
|
|
||||||
|
|
||||||
bitmask_t mask_;
|
|
||||||
int bit_;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr Iterator begin() const { return Iterator(mask_, 0); }
|
|
||||||
constexpr Iterator end() const { return Iterator(mask_, MaxBits); }
|
|
||||||
|
|
||||||
/// Get the raw bitmask value for optimized operations
|
|
||||||
constexpr bitmask_t get_mask() const { return this->mask_; }
|
|
||||||
|
|
||||||
/// Check if a specific enum value is present in a raw bitmask
|
|
||||||
/// Useful for checking intersection results without creating temporary objects
|
|
||||||
static constexpr bool mask_contains(bitmask_t mask, EnumType value) {
|
|
||||||
return (mask & (static_cast<bitmask_t>(1) << enum_to_bit(value))) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the first enum value from a raw bitmask
|
|
||||||
/// 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)); }
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) {
|
|
||||||
int bit = start_bit;
|
|
||||||
while (bit < MaxBits && !(mask & (static_cast<bitmask_t>(1) << bit))) {
|
|
||||||
++bit;
|
|
||||||
}
|
|
||||||
return bit;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
// Must be provided by template specialization
|
|
||||||
// These convert between enum values and bit positions (0, 1, 2, ...)
|
|
||||||
static constexpr int enum_to_bit(EnumType value);
|
|
||||||
static EnumType bit_to_enum(int bit); // Not constexpr due to static array limitation in C++20
|
|
||||||
|
|
||||||
bitmask_t mask_{0};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace esphome
|
|
||||||
165
esphome/core/finite_set_mask.h
Normal file
165
esphome/core/finite_set_mask.h
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <iterator>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
|
||||||
|
/// Generic bitmask for storing a finite set of discrete values efficiently.
|
||||||
|
/// Replaces std::set<ValueType> to eliminate red-black tree overhead (~586 bytes per instantiation).
|
||||||
|
///
|
||||||
|
/// Template parameters:
|
||||||
|
/// 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)
|
||||||
|
///
|
||||||
|
/// Requirements:
|
||||||
|
/// - ValueType must have a bounded discrete range that maps to bit positions
|
||||||
|
/// - For 1:1 mappings (contiguous enums starting at 0), no specialization needed
|
||||||
|
/// - For custom mappings (like ColorMode), specialize value_to_bit() and/or bit_to_value()
|
||||||
|
/// - MaxBits must be sufficient to hold all possible values
|
||||||
|
///
|
||||||
|
/// Example usage (1:1 mapping - climate enums):
|
||||||
|
/// // For enums with contiguous values starting at 0, no specialization needed!
|
||||||
|
/// using ClimateModeMask = FiniteSetMask<ClimateMode, CLIMATE_MODE_AUTO + 1>;
|
||||||
|
/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
|
||||||
|
/// if (modes.count(CLIMATE_MODE_HEAT)) { ... }
|
||||||
|
/// for (auto mode : modes) { ... } // Iterate over set bits
|
||||||
|
///
|
||||||
|
/// Example usage (custom mapping - ColorMode):
|
||||||
|
/// // For non-contiguous enums or custom mappings, specialize value_to_bit() and/or bit_to_value()
|
||||||
|
/// // See esphome/components/light/color_mode.h for complete example
|
||||||
|
///
|
||||||
|
/// Design notes:
|
||||||
|
/// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t)
|
||||||
|
/// - Iterator converts bit positions to actual values during traversal
|
||||||
|
/// - All operations are constexpr-compatible for compile-time initialization
|
||||||
|
/// - Drop-in replacement for std::set<ValueType> with simpler API
|
||||||
|
/// - Despite the name, works with any discrete bounded type, not just enums
|
||||||
|
///
|
||||||
|
template<typename ValueType, int MaxBits = 16> class FiniteSetMask {
|
||||||
|
public:
|
||||||
|
// Automatic bitmask type selection based on MaxBits
|
||||||
|
// ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t
|
||||||
|
using bitmask_t =
|
||||||
|
typename std::conditional<(MaxBits <= 8), uint8_t,
|
||||||
|
typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type;
|
||||||
|
|
||||||
|
constexpr FiniteSetMask() = default;
|
||||||
|
|
||||||
|
/// Construct from initializer list: {VALUE1, VALUE2, ...}
|
||||||
|
constexpr FiniteSetMask(std::initializer_list<ValueType> values) {
|
||||||
|
for (auto value : values) {
|
||||||
|
this->insert(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a single value to the set (std::set compatibility)
|
||||||
|
constexpr void insert(ValueType value) { this->mask_ |= (static_cast<bitmask_t>(1) << value_to_bit(value)); }
|
||||||
|
|
||||||
|
/// Add multiple values from initializer list
|
||||||
|
constexpr void insert(std::initializer_list<ValueType> values) {
|
||||||
|
for (auto value : values) {
|
||||||
|
this->insert(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a value from the set (std::set compatibility)
|
||||||
|
constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << value_to_bit(value)); }
|
||||||
|
|
||||||
|
/// Clear all values from the set
|
||||||
|
constexpr void clear() { this->mask_ = 0; }
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
constexpr size_t count(ValueType value) const {
|
||||||
|
return (this->mask_ & (static_cast<bitmask_t>(1) << value_to_bit(value))) != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count the number of values in the set
|
||||||
|
constexpr size_t size() const {
|
||||||
|
// Brian Kernighan's algorithm - efficient for sparse bitmasks
|
||||||
|
// Typical case: 2-4 modes out of 10 possible
|
||||||
|
bitmask_t n = this->mask_;
|
||||||
|
size_t count = 0;
|
||||||
|
while (n) {
|
||||||
|
n &= n - 1; // Clear the least significant set bit
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the set is empty
|
||||||
|
constexpr bool empty() const { return this->mask_ == 0; }
|
||||||
|
|
||||||
|
/// Iterator support for range-based for loops and API encoding
|
||||||
|
/// Iterates over set bits and converts bit positions to values
|
||||||
|
/// Optimization: removes bits from mask as we iterate
|
||||||
|
class Iterator {
|
||||||
|
public:
|
||||||
|
using iterator_category = std::forward_iterator_tag;
|
||||||
|
using value_type = ValueType;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using pointer = const ValueType *;
|
||||||
|
using reference = ValueType;
|
||||||
|
|
||||||
|
constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {}
|
||||||
|
|
||||||
|
constexpr ValueType operator*() const {
|
||||||
|
// Return value for the first set bit
|
||||||
|
return bit_to_value(find_next_set_bit(mask_, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Iterator &operator++() {
|
||||||
|
// Clear the lowest set bit (Brian Kernighan's algorithm)
|
||||||
|
mask_ &= mask_ - 1;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; }
|
||||||
|
|
||||||
|
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bitmask_t mask_;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr Iterator begin() const { return Iterator(mask_); }
|
||||||
|
constexpr Iterator end() const { return Iterator(0); }
|
||||||
|
|
||||||
|
/// Get the raw bitmask value for optimized operations
|
||||||
|
constexpr bitmask_t get_mask() const { return this->mask_; }
|
||||||
|
|
||||||
|
/// Check if a specific value is present in a raw bitmask
|
||||||
|
/// Useful for checking intersection results without creating temporary objects
|
||||||
|
static constexpr bool mask_contains(bitmask_t mask, ValueType value) {
|
||||||
|
return (mask & (static_cast<bitmask_t>(1) << value_to_bit(value))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first value from a raw bitmask
|
||||||
|
/// Used for optimizing intersection logic (e.g., "pick first suitable mode")
|
||||||
|
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
|
||||||
|
/// Returns the bit position, or MaxBits if no more bits are set
|
||||||
|
static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) {
|
||||||
|
int bit = start_bit;
|
||||||
|
while (bit < MaxBits && !(mask & (static_cast<bitmask_t>(1) << bit))) {
|
||||||
|
++bit;
|
||||||
|
}
|
||||||
|
return bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Default implementations for 1:1 mapping (enum value = bit position)
|
||||||
|
// For enums with contiguous values starting at 0, these defaults work as-is.
|
||||||
|
// If you need custom mapping (like ColorMode), provide specializations.
|
||||||
|
static constexpr int value_to_bit(ValueType value) { return static_cast<int>(value); }
|
||||||
|
static constexpr ValueType bit_to_value(int bit) { return static_cast<ValueType>(bit); }
|
||||||
|
|
||||||
|
bitmask_t mask_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome
|
||||||
@@ -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