mirror of
https://github.com/esphome/esphome.git
synced 2025-11-06 01:51:49 +00:00
Compare commits
4 Commits
dev
...
api_servic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fded918b7 | ||
|
|
ce4f9db778 | ||
|
|
ab6cb2dee6 | ||
|
|
bd0705cdc0 |
@@ -51,79 +51,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
* **Naming Conventions:**
|
||||
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
|
||||
* **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions):
|
||||
- Function, method, and variable names: `lower_snake_case`
|
||||
- Class/struct/enum names: `UpperCamelCase`
|
||||
- Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE`
|
||||
- Function-local constants: `lower_snake_case`
|
||||
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
||||
- Favor descriptive names over abbreviations
|
||||
|
||||
* **C++ Field Visibility:**
|
||||
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
|
||||
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
|
||||
1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references.
|
||||
```cpp
|
||||
// Helper to find matching string in vector and return its pointer
|
||||
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
|
||||
for (const char *item : vec) {
|
||||
if (strcmp(item, value) == 0)
|
||||
return item;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
class ClimateDevice {
|
||||
public:
|
||||
void set_custom_fan_modes(std::initializer_list<const char *> modes) {
|
||||
this->custom_fan_modes_ = modes;
|
||||
this->active_custom_fan_mode_ = nullptr; // Reset when modes change
|
||||
}
|
||||
bool set_custom_fan_mode(const char *mode) {
|
||||
// Find mode in supported list and store that pointer (not the input pointer)
|
||||
const char *validated_mode = vector_find(this->custom_fan_modes_, mode);
|
||||
if (validated_mode != nullptr) {
|
||||
this->active_custom_fan_mode_ = validated_mode;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
std::vector<const char *> custom_fan_modes_; // Pointers to string literals in flash
|
||||
const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_
|
||||
};
|
||||
```
|
||||
2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption.
|
||||
```cpp
|
||||
class Buffer {
|
||||
public:
|
||||
void resize(size_t new_size) {
|
||||
auto new_data = std::make_unique<uint8_t[]>(new_size);
|
||||
if (this->data_) {
|
||||
std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size));
|
||||
}
|
||||
this->data_ = std::move(new_data);
|
||||
this->size_ = new_size; // Must stay in sync with data_
|
||||
}
|
||||
private:
|
||||
std::unique_ptr<uint8_t[]> data_;
|
||||
size_t size_{0}; // Must match allocated size of data_
|
||||
};
|
||||
```
|
||||
3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip.
|
||||
* **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members.
|
||||
|
||||
* **C++ Preprocessor Directives:**
|
||||
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
|
||||
* **Use `#define` only for:**
|
||||
- Conditional compilation (`#ifdef`, `#ifndef`)
|
||||
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
|
||||
|
||||
* **C++ Additional Conventions:**
|
||||
* **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`)
|
||||
* **Indentation:** Use spaces (two per indentation level), not tabs
|
||||
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
|
||||
* **Line length:** Wrap lines at no more than 120 characters
|
||||
* **C++:** Follows the Google C++ Style Guide.
|
||||
|
||||
* **Component Structure:**
|
||||
* **Standard Files:**
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> {
|
||||
public:
|
||||
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
|
||||
void (T::*callback)(Ts...))
|
||||
: UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||
|
||||
protected:
|
||||
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
||||
|
||||
@@ -23,11 +23,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
||||
|
||||
template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||
|
||||
// Base class for YAML-defined services (most common case)
|
||||
// Stores only pointers to string literals in flash - no heap allocation
|
||||
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
public:
|
||||
UserServiceBase(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||
: name_(std::move(name)), arg_names_(arg_names) {
|
||||
this->key_ = fnv1_hash(this->name_);
|
||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: name_(name), arg_names_(arg_names) {
|
||||
this->key_ = fnv1_hash(name);
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
@@ -47,7 +49,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||
if (req.key != this->key_)
|
||||
return false;
|
||||
if (req.args.size() != this->arg_names_.size())
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
@@ -59,14 +61,60 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
std::string name_;
|
||||
// Pointers to string literals in flash - no heap allocation
|
||||
const char *name_;
|
||||
std::array<const char *, sizeof...(Ts)> arg_names_;
|
||||
uint32_t key_{0};
|
||||
};
|
||||
|
||||
// Separate class for custom_api_device services (rare case)
|
||||
// Stores copies of runtime-generated names
|
||||
template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor {
|
||||
public:
|
||||
UserServiceDynamic(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||
: name_(std::move(name)), arg_names_(arg_names) {
|
||||
this->key_ = fnv1_hash(this->name_.c_str());
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.key = this->key_;
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
msg.args.init(sizeof...(Ts));
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.set_name(StringRef(this->arg_names_[i]));
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||
if (req.key != this->key_)
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
// Heap-allocated strings for runtime-generated names
|
||||
std::string name_;
|
||||
std::array<std::string, sizeof...(Ts)> arg_names_;
|
||||
uint32_t key_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||
// Constructor for static names (YAML-defined services - used by code generator)
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names) {}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -3,8 +3,6 @@ import re
|
||||
from esphome import config_validation as cv
|
||||
from esphome.const import CONF_ARGS, CONF_FORMAT
|
||||
|
||||
CONF_IF_NAN = "if_nan"
|
||||
|
||||
lv_uses = {
|
||||
"USER_DATA",
|
||||
"LOG",
|
||||
@@ -23,48 +21,23 @@ lv_fonts_used = set()
|
||||
esphome_fonts_used = set()
|
||||
lvgl_components_required = set()
|
||||
|
||||
# noqa
|
||||
f_regex = re.compile(
|
||||
r"""
|
||||
|
||||
def validate_printf(value):
|
||||
cfmt = r"""
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:h|l|ll|w|I|I32|I64)? # size
|
||||
f # type
|
||||
)
|
||||
""",
|
||||
flags=re.VERBOSE,
|
||||
)
|
||||
# noqa
|
||||
c_regex = re.compile(
|
||||
r"""
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:[-+0 #]{0,5}) # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:h|l|ll|w|I|I32|I64)? # size
|
||||
[cCdiouxXeEfgGaAnpsSZ] # type
|
||||
)
|
||||
""",
|
||||
flags=re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def validate_printf(value):
|
||||
format_string = value[CONF_FORMAT]
|
||||
matches = c_regex.findall(format_string)
|
||||
""" # noqa
|
||||
matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE)
|
||||
if len(matches) != len(value[CONF_ARGS]):
|
||||
raise cv.Invalid(
|
||||
f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!"
|
||||
)
|
||||
|
||||
if value.get(CONF_IF_NAN) and len(f_regex.findall(format_string)) != 1:
|
||||
raise cv.Invalid(
|
||||
"Use of 'if_nan' requires a single valid printf-pattern of type %f"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -33,13 +33,7 @@ from .defines import (
|
||||
call_lambda,
|
||||
literal,
|
||||
)
|
||||
from .helpers import (
|
||||
CONF_IF_NAN,
|
||||
add_lv_use,
|
||||
esphome_fonts_used,
|
||||
lv_fonts_used,
|
||||
requires_component,
|
||||
)
|
||||
from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component
|
||||
from .types import lv_font_t, lv_gradient_t
|
||||
|
||||
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
||||
@@ -418,13 +412,7 @@ class TextValidator(LValidator):
|
||||
str_args = [str(x) for x in value[CONF_ARGS]]
|
||||
arg_expr = cg.RawExpression(",".join(str_args))
|
||||
format_str = cpp_string_escape(format_str)
|
||||
sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()"
|
||||
if nanval := value.get(CONF_IF_NAN):
|
||||
nanval = cpp_string_escape(nanval)
|
||||
return literal(
|
||||
f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})"
|
||||
)
|
||||
return literal(sprintf_str)
|
||||
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
|
||||
if time_format := value.get(CONF_TIME_FORMAT):
|
||||
source = value[CONF_TIME]
|
||||
if isinstance(source, Lambda):
|
||||
|
||||
@@ -20,7 +20,7 @@ from esphome.core.config import StartupTrigger
|
||||
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
||||
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
||||
from .helpers import requires_component, validate_printf
|
||||
from .layout import (
|
||||
FLEX_OBJ_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
@@ -54,7 +54,6 @@ PRINTF_TEXT_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_FORMAT): cv.string,
|
||||
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
|
||||
cv.Optional(CONF_IF_NAN): cv.string,
|
||||
},
|
||||
),
|
||||
validate_printf,
|
||||
|
||||
@@ -94,7 +94,6 @@ class Platform(StrEnum):
|
||||
# Memory impact analysis constants
|
||||
MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes
|
||||
MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform
|
||||
MEMORY_IMPACT_MAX_COMPONENTS = 40 # Max components before results become nonsensical
|
||||
|
||||
# Platform-specific components that can only be built on their respective platforms
|
||||
# These components contain platform-specific code and cannot be cross-compiled
|
||||
@@ -556,17 +555,6 @@ def detect_memory_impact_config(
|
||||
if not components_with_tests:
|
||||
return {"should_run": "false"}
|
||||
|
||||
# Skip memory impact analysis if too many components changed
|
||||
# Building 40+ components at once produces nonsensical memory impact results
|
||||
# This typically happens with large refactorings or batch updates
|
||||
if len(components_with_tests) > MEMORY_IMPACT_MAX_COMPONENTS:
|
||||
print(
|
||||
f"Memory impact: Skipping analysis for {len(components_with_tests)} components "
|
||||
f"(limit is {MEMORY_IMPACT_MAX_COMPONENTS}, would give nonsensical results)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return {"should_run": "false"}
|
||||
|
||||
# Find common platforms supported by ALL components
|
||||
# This ensures we can build all components together in a merged config
|
||||
common_platforms = set(MEMORY_IMPACT_PLATFORM_PREFERENCE)
|
||||
|
||||
@@ -726,12 +726,6 @@ lvgl:
|
||||
- logger.log:
|
||||
format: "Spinbox value is %f"
|
||||
args: [x]
|
||||
- lvgl.label.update:
|
||||
id: hello_label
|
||||
text:
|
||||
format: "value is %.1f now"
|
||||
args: [x]
|
||||
if_nan: "Value unknown"
|
||||
- button:
|
||||
styles: spin_button
|
||||
id: spin_down
|
||||
|
||||
@@ -11,6 +11,28 @@ api:
|
||||
then:
|
||||
- logger.log: "YAML service called"
|
||||
|
||||
# Test YAML service with arguments (tests UserServiceBase with const char* array)
|
||||
- action: test_yaml_service_with_args
|
||||
variables:
|
||||
my_int: int
|
||||
my_string: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "YAML service with args: %d, %s"
|
||||
args: [my_int, my_string.c_str()]
|
||||
|
||||
# Test YAML service with multiple arguments
|
||||
- action: test_yaml_service_many_args
|
||||
variables:
|
||||
arg1: int
|
||||
arg2: float
|
||||
arg3: bool
|
||||
arg4: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "YAML service many args: %d, %.2f, %d, %s"
|
||||
args: [arg1, arg2, arg3, arg4.c_str()]
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
|
||||
@@ -33,12 +33,16 @@ async def test_api_custom_services(
|
||||
|
||||
# Track log messages
|
||||
yaml_service_future = loop.create_future()
|
||||
yaml_args_future = loop.create_future()
|
||||
yaml_many_args_future = loop.create_future()
|
||||
custom_service_future = loop.create_future()
|
||||
custom_args_future = loop.create_future()
|
||||
custom_arrays_future = loop.create_future()
|
||||
|
||||
# Patterns to match in logs
|
||||
yaml_service_pattern = re.compile(r"YAML service called")
|
||||
yaml_args_pattern = re.compile(r"YAML service with args: 123, test_value")
|
||||
yaml_many_args_pattern = re.compile(r"YAML service many args: 42, 3\.14, 1, hello")
|
||||
custom_service_pattern = re.compile(r"Custom test service called!")
|
||||
custom_args_pattern = re.compile(
|
||||
r"Custom service called with: test_string, 456, 1, 78\.90"
|
||||
@@ -51,6 +55,10 @@ async def test_api_custom_services(
|
||||
"""Check log output for expected messages."""
|
||||
if not yaml_service_future.done() and yaml_service_pattern.search(line):
|
||||
yaml_service_future.set_result(True)
|
||||
elif not yaml_args_future.done() and yaml_args_pattern.search(line):
|
||||
yaml_args_future.set_result(True)
|
||||
elif not yaml_many_args_future.done() and yaml_many_args_pattern.search(line):
|
||||
yaml_many_args_future.set_result(True)
|
||||
elif not custom_service_future.done() and custom_service_pattern.search(line):
|
||||
custom_service_future.set_result(True)
|
||||
elif not custom_args_future.done() and custom_args_pattern.search(line):
|
||||
@@ -71,11 +79,13 @@ async def test_api_custom_services(
|
||||
# List services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Should have 4 services: 1 YAML + 3 CustomAPIDevice
|
||||
assert len(services) == 4, f"Expected 4 services, found {len(services)}"
|
||||
# Should have 6 services: 3 YAML + 3 CustomAPIDevice
|
||||
assert len(services) == 6, f"Expected 6 services, found {len(services)}"
|
||||
|
||||
# Find our services
|
||||
yaml_service: UserService | None = None
|
||||
yaml_args_service: UserService | None = None
|
||||
yaml_many_args_service: UserService | None = None
|
||||
custom_service: UserService | None = None
|
||||
custom_args_service: UserService | None = None
|
||||
custom_arrays_service: UserService | None = None
|
||||
@@ -83,6 +93,10 @@ async def test_api_custom_services(
|
||||
for service in services:
|
||||
if service.name == "test_yaml_service":
|
||||
yaml_service = service
|
||||
elif service.name == "test_yaml_service_with_args":
|
||||
yaml_args_service = service
|
||||
elif service.name == "test_yaml_service_many_args":
|
||||
yaml_many_args_service = service
|
||||
elif service.name == "custom_test_service":
|
||||
custom_service = service
|
||||
elif service.name == "custom_service_with_args":
|
||||
@@ -91,6 +105,10 @@ async def test_api_custom_services(
|
||||
custom_arrays_service = service
|
||||
|
||||
assert yaml_service is not None, "test_yaml_service not found"
|
||||
assert yaml_args_service is not None, "test_yaml_service_with_args not found"
|
||||
assert yaml_many_args_service is not None, (
|
||||
"test_yaml_service_many_args not found"
|
||||
)
|
||||
assert custom_service is not None, "custom_test_service not found"
|
||||
assert custom_args_service is not None, "custom_service_with_args not found"
|
||||
assert custom_arrays_service is not None, "custom_service_with_arrays not found"
|
||||
@@ -99,6 +117,44 @@ async def test_api_custom_services(
|
||||
client.execute_service(yaml_service, {})
|
||||
await asyncio.wait_for(yaml_service_future, timeout=5.0)
|
||||
|
||||
# Verify YAML service with args arguments
|
||||
assert len(yaml_args_service.args) == 2
|
||||
yaml_args_types = {arg.name: arg.type for arg in yaml_args_service.args}
|
||||
assert yaml_args_types["my_int"] == UserServiceArgType.INT
|
||||
assert yaml_args_types["my_string"] == UserServiceArgType.STRING
|
||||
|
||||
# Test YAML service with arguments
|
||||
client.execute_service(
|
||||
yaml_args_service,
|
||||
{
|
||||
"my_int": 123,
|
||||
"my_string": "test_value",
|
||||
},
|
||||
)
|
||||
await asyncio.wait_for(yaml_args_future, timeout=5.0)
|
||||
|
||||
# Verify YAML service with many args arguments
|
||||
assert len(yaml_many_args_service.args) == 4
|
||||
yaml_many_args_types = {
|
||||
arg.name: arg.type for arg in yaml_many_args_service.args
|
||||
}
|
||||
assert yaml_many_args_types["arg1"] == UserServiceArgType.INT
|
||||
assert yaml_many_args_types["arg2"] == UserServiceArgType.FLOAT
|
||||
assert yaml_many_args_types["arg3"] == UserServiceArgType.BOOL
|
||||
assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING
|
||||
|
||||
# Test YAML service with many arguments
|
||||
client.execute_service(
|
||||
yaml_many_args_service,
|
||||
{
|
||||
"arg1": 42,
|
||||
"arg2": 3.14,
|
||||
"arg3": True,
|
||||
"arg4": "hello",
|
||||
},
|
||||
)
|
||||
await asyncio.wait_for(yaml_many_args_future, timeout=5.0)
|
||||
|
||||
# Test simple CustomAPIDevice service
|
||||
client.execute_service(custom_service, {})
|
||||
await asyncio.wait_for(custom_service_future, timeout=5.0)
|
||||
|
||||
@@ -71,10 +71,9 @@ def mock_changed_files() -> Generator[Mock, None, None]:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_determine_jobs_caches() -> None:
|
||||
"""Clear all cached functions before each test."""
|
||||
def clear_clang_tidy_cache() -> None:
|
||||
"""Clear the clang-tidy full scan cache before each test."""
|
||||
determine_jobs._is_clang_tidy_full_scan.cache_clear()
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
|
||||
def test_main_all_tests_should_run(
|
||||
@@ -566,6 +565,7 @@ def test_main_filters_components_without_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
|
||||
@@ -665,6 +665,7 @@ def test_main_detects_components_with_variant_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
|
||||
@@ -713,6 +714,7 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non
|
||||
"esphome/components/wifi/wifi.cpp",
|
||||
"esphome/components/api/api.cpp",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -742,6 +744,7 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
||||
"esphome/core/application.cpp",
|
||||
"esphome/core/component.h",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -772,6 +775,7 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) ->
|
||||
"esphome/config.py",
|
||||
"esphome/core/config.py",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -804,6 +808,7 @@ def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
|
||||
"esphome/components/wifi/wifi.cpp",
|
||||
"esphome/components/logger/logger.cpp",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -825,6 +830,7 @@ def test_detect_memory_impact_config_no_changes(tmp_path: Path) -> None:
|
||||
patch.object(determine_jobs, "changed_files") as mock_changed_files,
|
||||
):
|
||||
mock_changed_files.return_value = []
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -849,6 +855,7 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) ->
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/components/my_custom_component/component.cpp",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -888,6 +895,7 @@ def test_detect_memory_impact_config_includes_base_bus_components(
|
||||
"esphome/components/uart/automation.h", # Header file with inline code
|
||||
"esphome/components/wifi/wifi.cpp",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -930,6 +938,7 @@ def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
|
||||
"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()
|
||||
|
||||
@@ -1159,6 +1168,7 @@ def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266(
|
||||
"tests/components/esp8266/test.esp8266-ard.yaml",
|
||||
"esphome/core/helpers_esp8266.h", # ESP8266-specific file to hint platform
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -1212,6 +1222,7 @@ def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32(
|
||||
"esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint
|
||||
"esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -1246,6 +1257,7 @@ def test_detect_memory_impact_config_skips_release_branch(tmp_path: Path) -> Non
|
||||
patch.object(determine_jobs, "get_target_branch", return_value="release"),
|
||||
):
|
||||
mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -1268,6 +1280,7 @@ def test_detect_memory_impact_config_skips_beta_branch(tmp_path: Path) -> None:
|
||||
patch.object(determine_jobs, "get_target_branch", return_value="beta"),
|
||||
):
|
||||
mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
@@ -1290,66 +1303,10 @@ def test_detect_memory_impact_config_runs_for_dev_branch(tmp_path: Path) -> None
|
||||
patch.object(determine_jobs, "get_target_branch", return_value="dev"),
|
||||
):
|
||||
mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
# Memory impact should run for dev branch
|
||||
assert result["should_run"] == "true"
|
||||
assert result["components"] == ["wifi"]
|
||||
|
||||
|
||||
def test_detect_memory_impact_config_skips_too_many_components(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that memory impact analysis is skipped when more than 40 components changed."""
|
||||
# Create test directory structure with 41 components
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
component_names = [f"component_{i}" for i in range(41)]
|
||||
|
||||
for component_name in component_names:
|
||||
comp_dir = tests_dir / component_name
|
||||
comp_dir.mkdir(parents=True)
|
||||
(comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}")
|
||||
|
||||
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,
|
||||
patch.object(determine_jobs, "get_target_branch", return_value="dev"),
|
||||
):
|
||||
mock_changed_files.return_value = [
|
||||
f"esphome/components/{name}/{name}.cpp" for name in component_names
|
||||
]
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
# Memory impact should be skipped for too many components (41 > 40)
|
||||
assert result["should_run"] == "false"
|
||||
|
||||
|
||||
def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> None:
|
||||
"""Test that memory impact analysis runs with exactly 40 components (at limit)."""
|
||||
# Create test directory structure with exactly 40 components
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
component_names = [f"component_{i}" for i in range(40)]
|
||||
|
||||
for component_name in component_names:
|
||||
comp_dir = tests_dir / component_name
|
||||
comp_dir.mkdir(parents=True)
|
||||
(comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}")
|
||||
|
||||
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,
|
||||
patch.object(determine_jobs, "get_target_branch", return_value="dev"),
|
||||
):
|
||||
mock_changed_files.return_value = [
|
||||
f"esphome/components/{name}/{name}.cpp" for name in component_names
|
||||
]
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
# Memory impact should run at exactly 40 components (at limit but not over)
|
||||
assert result["should_run"] == "true"
|
||||
assert len(result["components"]) == 40
|
||||
|
||||
Reference in New Issue
Block a user