mirror of
https://github.com/esphome/esphome.git
synced 2025-11-15 14:25:45 +00:00
Merge branch 'dev' into delete_redundant_c3_tests
This commit is contained in:
@@ -3,6 +3,8 @@ import re
|
|||||||
from esphome import config_validation as cv
|
from esphome import config_validation as cv
|
||||||
from esphome.const import CONF_ARGS, CONF_FORMAT
|
from esphome.const import CONF_ARGS, CONF_FORMAT
|
||||||
|
|
||||||
|
CONF_IF_NAN = "if_nan"
|
||||||
|
|
||||||
lv_uses = {
|
lv_uses = {
|
||||||
"USER_DATA",
|
"USER_DATA",
|
||||||
"LOG",
|
"LOG",
|
||||||
@@ -21,23 +23,48 @@ lv_fonts_used = set()
|
|||||||
esphome_fonts_used = set()
|
esphome_fonts_used = set()
|
||||||
lvgl_components_required = set()
|
lvgl_components_required = set()
|
||||||
|
|
||||||
|
# noqa
|
||||||
def validate_printf(value):
|
f_regex = re.compile(
|
||||||
cfmt = r"""
|
r"""
|
||||||
( # start of capture group 1
|
( # start of capture group 1
|
||||||
% # literal "%"
|
% # literal "%"
|
||||||
(?:[-+0 #]{0,5}) # optional flags
|
[-+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
|
||||||
(?:\d+|\*)? # width
|
(?:\d+|\*)? # width
|
||||||
(?:\.(?:\d+|\*))? # precision
|
(?:\.(?:\d+|\*))? # precision
|
||||||
(?:h|l|ll|w|I|I32|I64)? # size
|
(?:h|l|ll|w|I|I32|I64)? # size
|
||||||
[cCdiouxXeEfgGaAnpsSZ] # type
|
[cCdiouxXeEfgGaAnpsSZ] # type
|
||||||
)
|
)
|
||||||
""" # noqa
|
""",
|
||||||
matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE)
|
flags=re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_printf(value):
|
||||||
|
format_string = value[CONF_FORMAT]
|
||||||
|
matches = c_regex.findall(format_string)
|
||||||
if len(matches) != len(value[CONF_ARGS]):
|
if len(matches) != len(value[CONF_ARGS]):
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!"
|
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
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ from .defines import (
|
|||||||
call_lambda,
|
call_lambda,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component
|
from .helpers import (
|
||||||
|
CONF_IF_NAN,
|
||||||
|
add_lv_use,
|
||||||
|
esphome_fonts_used,
|
||||||
|
lv_fonts_used,
|
||||||
|
requires_component,
|
||||||
|
)
|
||||||
from .types import lv_font_t, lv_gradient_t
|
from .types import lv_font_t, lv_gradient_t
|
||||||
|
|
||||||
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
||||||
@@ -412,7 +418,13 @@ class TextValidator(LValidator):
|
|||||||
str_args = [str(x) for x in value[CONF_ARGS]]
|
str_args = [str(x) for x in value[CONF_ARGS]]
|
||||||
arg_expr = cg.RawExpression(",".join(str_args))
|
arg_expr = cg.RawExpression(",".join(str_args))
|
||||||
format_str = cpp_string_escape(format_str)
|
format_str = cpp_string_escape(format_str)
|
||||||
return literal(f"str_sprintf({format_str}, {arg_expr}).c_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)
|
||||||
if time_format := value.get(CONF_TIME_FORMAT):
|
if time_format := value.get(CONF_TIME_FORMAT):
|
||||||
source = value[CONF_TIME]
|
source = value[CONF_TIME]
|
||||||
if isinstance(source, Lambda):
|
if isinstance(source, Lambda):
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from esphome.core.config import StartupTrigger
|
|||||||
|
|
||||||
from . import defines as df, lv_validation as lvalid
|
from . import defines as df, lv_validation as lvalid
|
||||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
||||||
from .helpers import requires_component, validate_printf
|
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
||||||
from .layout import (
|
from .layout import (
|
||||||
FLEX_OBJ_SCHEMA,
|
FLEX_OBJ_SCHEMA,
|
||||||
GRID_CELL_SCHEMA,
|
GRID_CELL_SCHEMA,
|
||||||
@@ -54,6 +54,7 @@ PRINTF_TEXT_SCHEMA = cv.All(
|
|||||||
{
|
{
|
||||||
cv.Required(CONF_FORMAT): cv.string,
|
cv.Required(CONF_FORMAT): cv.string,
|
||||||
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
|
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
|
||||||
|
cv.Optional(CONF_IF_NAN): cv.string,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
validate_printf,
|
validate_printf,
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ from helpers import (
|
|||||||
get_components_from_integration_fixtures,
|
get_components_from_integration_fixtures,
|
||||||
get_components_with_dependencies,
|
get_components_with_dependencies,
|
||||||
get_cpp_changed_components,
|
get_cpp_changed_components,
|
||||||
|
get_target_branch,
|
||||||
git_ls_files,
|
git_ls_files,
|
||||||
parse_test_filename,
|
parse_test_filename,
|
||||||
root_path,
|
root_path,
|
||||||
@@ -93,6 +94,7 @@ class Platform(StrEnum):
|
|||||||
# Memory impact analysis constants
|
# Memory impact analysis constants
|
||||||
MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes
|
MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes
|
||||||
MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform
|
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
|
# Platform-specific components that can only be built on their respective platforms
|
||||||
# These components contain platform-specific code and cannot be cross-compiled
|
# These components contain platform-specific code and cannot be cross-compiled
|
||||||
@@ -471,6 +473,20 @@ def detect_memory_impact_config(
|
|||||||
- platform: platform name for the merged build
|
- platform: platform name for the merged build
|
||||||
- use_merged_config: "true" (always use merged config)
|
- use_merged_config: "true" (always use merged config)
|
||||||
"""
|
"""
|
||||||
|
# Skip memory impact analysis for release* or beta* branches
|
||||||
|
# These branches typically contain many merged changes from dev, and building
|
||||||
|
# all components at once would produce nonsensical memory impact results.
|
||||||
|
# Memory impact analysis is most useful for focused PRs targeting dev.
|
||||||
|
target_branch = get_target_branch()
|
||||||
|
if target_branch and (
|
||||||
|
target_branch.startswith("release") or target_branch.startswith("beta")
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
f"Memory impact: Skipping analysis for target branch {target_branch} "
|
||||||
|
f"(would try to build all components at once, giving nonsensical results)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return {"should_run": "false"}
|
||||||
|
|
||||||
# Get actually changed files (not dependencies)
|
# Get actually changed files (not dependencies)
|
||||||
files = changed_files(branch)
|
files = changed_files(branch)
|
||||||
@@ -540,6 +556,17 @@ def detect_memory_impact_config(
|
|||||||
if not components_with_tests:
|
if not components_with_tests:
|
||||||
return {"should_run": "false"}
|
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
|
# Find common platforms supported by ALL components
|
||||||
# This ensures we can build all components together in a merged config
|
# This ensures we can build all components together in a merged config
|
||||||
common_platforms = set(MEMORY_IMPACT_PLATFORM_PREFERENCE)
|
common_platforms = set(MEMORY_IMPACT_PLATFORM_PREFERENCE)
|
||||||
|
|||||||
@@ -196,6 +196,20 @@ def splitlines_no_ends(string: str) -> list[str]:
|
|||||||
return [s.strip() for s in string.splitlines()]
|
return [s.strip() for s in string.splitlines()]
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _get_github_event_data() -> dict | None:
|
||||||
|
"""Read and parse GitHub event file (cached).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed event data dictionary, or None if not available
|
||||||
|
"""
|
||||||
|
github_event_path = os.environ.get("GITHUB_EVENT_PATH")
|
||||||
|
if github_event_path and os.path.exists(github_event_path):
|
||||||
|
with open(github_event_path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_pr_number_from_github_env() -> str | None:
|
def _get_pr_number_from_github_env() -> str | None:
|
||||||
"""Extract PR number from GitHub environment variables.
|
"""Extract PR number from GitHub environment variables.
|
||||||
|
|
||||||
@@ -208,13 +222,30 @@ def _get_pr_number_from_github_env() -> str | None:
|
|||||||
return github_ref.split("/pull/")[1].split("/")[0]
|
return github_ref.split("/pull/")[1].split("/")[0]
|
||||||
|
|
||||||
# Fallback to GitHub event file
|
# Fallback to GitHub event file
|
||||||
github_event_path = os.environ.get("GITHUB_EVENT_PATH")
|
if event_data := _get_github_event_data():
|
||||||
if github_event_path and os.path.exists(github_event_path):
|
pr_data = event_data.get("pull_request", {})
|
||||||
with open(github_event_path) as f:
|
if pr_number := pr_data.get("number"):
|
||||||
event_data = json.load(f)
|
return str(pr_number)
|
||||||
pr_data = event_data.get("pull_request", {})
|
|
||||||
if pr_number := pr_data.get("number"):
|
return None
|
||||||
return str(pr_number)
|
|
||||||
|
|
||||||
|
def get_target_branch() -> str | None:
|
||||||
|
"""Get the target branch from GitHub environment variables.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Target branch name (e.g., "dev", "release", "beta"), or None if not in PR context
|
||||||
|
"""
|
||||||
|
# First try GITHUB_BASE_REF (set for pull_request events)
|
||||||
|
if base_ref := os.environ.get("GITHUB_BASE_REF"):
|
||||||
|
return base_ref
|
||||||
|
|
||||||
|
# Fallback to GitHub event file
|
||||||
|
if event_data := _get_github_event_data():
|
||||||
|
pr_data = event_data.get("pull_request", {})
|
||||||
|
base_data = pr_data.get("base", {})
|
||||||
|
if ref := base_data.get("ref"):
|
||||||
|
return ref
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -726,6 +726,12 @@ lvgl:
|
|||||||
- logger.log:
|
- logger.log:
|
||||||
format: "Spinbox value is %f"
|
format: "Spinbox value is %f"
|
||||||
args: [x]
|
args: [x]
|
||||||
|
- lvgl.label.update:
|
||||||
|
id: hello_label
|
||||||
|
text:
|
||||||
|
format: "value is %.1f now"
|
||||||
|
args: [x]
|
||||||
|
if_nan: "Value unknown"
|
||||||
- button:
|
- button:
|
||||||
styles: spin_button
|
styles: spin_button
|
||||||
id: spin_down
|
id: spin_down
|
||||||
|
|||||||
@@ -71,9 +71,10 @@ def mock_changed_files() -> Generator[Mock, None, None]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def clear_clang_tidy_cache() -> None:
|
def clear_determine_jobs_caches() -> None:
|
||||||
"""Clear the clang-tidy full scan cache before each test."""
|
"""Clear all cached functions before each test."""
|
||||||
determine_jobs._is_clang_tidy_full_scan.cache_clear()
|
determine_jobs._is_clang_tidy_full_scan.cache_clear()
|
||||||
|
determine_jobs._component_has_tests.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
def test_main_all_tests_should_run(
|
def test_main_all_tests_should_run(
|
||||||
@@ -565,7 +566,6 @@ def test_main_filters_components_without_tests(
|
|||||||
patch.object(determine_jobs, "changed_files", return_value=[]),
|
patch.object(determine_jobs, "changed_files", return_value=[]),
|
||||||
):
|
):
|
||||||
# Clear the cache since we're mocking root_path
|
# Clear the cache since we're mocking root_path
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
determine_jobs.main()
|
determine_jobs.main()
|
||||||
|
|
||||||
# Check output
|
# Check output
|
||||||
@@ -665,7 +665,6 @@ def test_main_detects_components_with_variant_tests(
|
|||||||
patch.object(determine_jobs, "changed_files", return_value=[]),
|
patch.object(determine_jobs, "changed_files", return_value=[]),
|
||||||
):
|
):
|
||||||
# Clear the cache since we're mocking root_path
|
# Clear the cache since we're mocking root_path
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
determine_jobs.main()
|
determine_jobs.main()
|
||||||
|
|
||||||
# Check output
|
# Check output
|
||||||
@@ -714,7 +713,6 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non
|
|||||||
"esphome/components/wifi/wifi.cpp",
|
"esphome/components/wifi/wifi.cpp",
|
||||||
"esphome/components/api/api.cpp",
|
"esphome/components/api/api.cpp",
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -744,7 +742,6 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
|||||||
"esphome/core/application.cpp",
|
"esphome/core/application.cpp",
|
||||||
"esphome/core/component.h",
|
"esphome/core/component.h",
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -775,7 +772,6 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) ->
|
|||||||
"esphome/config.py",
|
"esphome/config.py",
|
||||||
"esphome/core/config.py",
|
"esphome/core/config.py",
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -808,7 +804,6 @@ def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
|
|||||||
"esphome/components/wifi/wifi.cpp",
|
"esphome/components/wifi/wifi.cpp",
|
||||||
"esphome/components/logger/logger.cpp",
|
"esphome/components/logger/logger.cpp",
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -830,7 +825,6 @@ def test_detect_memory_impact_config_no_changes(tmp_path: Path) -> None:
|
|||||||
patch.object(determine_jobs, "changed_files") as mock_changed_files,
|
patch.object(determine_jobs, "changed_files") as mock_changed_files,
|
||||||
):
|
):
|
||||||
mock_changed_files.return_value = []
|
mock_changed_files.return_value = []
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -855,7 +849,6 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) ->
|
|||||||
mock_changed_files.return_value = [
|
mock_changed_files.return_value = [
|
||||||
"esphome/components/my_custom_component/component.cpp",
|
"esphome/components/my_custom_component/component.cpp",
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -895,7 +888,6 @@ def test_detect_memory_impact_config_includes_base_bus_components(
|
|||||||
"esphome/components/uart/automation.h", # Header file with inline code
|
"esphome/components/uart/automation.h", # Header file with inline code
|
||||||
"esphome/components/wifi/wifi.cpp",
|
"esphome/components/wifi/wifi.cpp",
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -938,7 +930,6 @@ def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
|
|||||||
"esphome/components/improv_serial/improv_serial.cpp",
|
"esphome/components/improv_serial/improv_serial.cpp",
|
||||||
"esphome/components/ethernet/ethernet.cpp",
|
"esphome/components/ethernet/ethernet.cpp",
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -1168,7 +1159,6 @@ def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266(
|
|||||||
"tests/components/esp8266/test.esp8266-ard.yaml",
|
"tests/components/esp8266/test.esp8266-ard.yaml",
|
||||||
"esphome/core/helpers_esp8266.h", # ESP8266-specific file to hint platform
|
"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()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -1222,7 +1212,6 @@ def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32(
|
|||||||
"esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint
|
"esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint
|
||||||
"esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint
|
"esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint
|
||||||
]
|
]
|
||||||
determine_jobs._component_has_tests.cache_clear()
|
|
||||||
|
|
||||||
result = determine_jobs.detect_memory_impact_config()
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
@@ -1240,3 +1229,127 @@ def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["use_merged_config"] == "true"
|
assert result["use_merged_config"] == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_memory_impact_config_skips_release_branch(tmp_path: Path) -> None:
|
||||||
|
"""Test that memory impact analysis is skipped for release* branches."""
|
||||||
|
# Create test directory structure with components that have tests
|
||||||
|
tests_dir = tmp_path / "tests" / "components"
|
||||||
|
wifi_dir = tests_dir / "wifi"
|
||||||
|
wifi_dir.mkdir(parents=True)
|
||||||
|
(wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi")
|
||||||
|
|
||||||
|
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="release"),
|
||||||
|
):
|
||||||
|
mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"]
|
||||||
|
|
||||||
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
|
# Memory impact should be skipped for release branch
|
||||||
|
assert result["should_run"] == "false"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_memory_impact_config_skips_beta_branch(tmp_path: Path) -> None:
|
||||||
|
"""Test that memory impact analysis is skipped for beta* branches."""
|
||||||
|
# Create test directory structure with components that have tests
|
||||||
|
tests_dir = tmp_path / "tests" / "components"
|
||||||
|
wifi_dir = tests_dir / "wifi"
|
||||||
|
wifi_dir.mkdir(parents=True)
|
||||||
|
(wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi")
|
||||||
|
|
||||||
|
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="beta"),
|
||||||
|
):
|
||||||
|
mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"]
|
||||||
|
|
||||||
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
|
# Memory impact should be skipped for beta branch
|
||||||
|
assert result["should_run"] == "false"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_memory_impact_config_runs_for_dev_branch(tmp_path: Path) -> None:
|
||||||
|
"""Test that memory impact analysis runs for dev branch."""
|
||||||
|
# Create test directory structure with components that have tests
|
||||||
|
tests_dir = tmp_path / "tests" / "components"
|
||||||
|
wifi_dir = tests_dir / "wifi"
|
||||||
|
wifi_dir.mkdir(parents=True)
|
||||||
|
(wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi")
|
||||||
|
|
||||||
|
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 = ["esphome/components/wifi/wifi.cpp"]
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ print_file_list = helpers.print_file_list
|
|||||||
get_all_dependencies = helpers.get_all_dependencies
|
get_all_dependencies = helpers.get_all_dependencies
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clear_helpers_cache() -> None:
|
||||||
|
"""Clear cached functions before each test."""
|
||||||
|
helpers._get_github_event_data.cache_clear()
|
||||||
|
helpers._get_changed_files_github_actions.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("github_ref", "expected_pr_number"),
|
("github_ref", "expected_pr_number"),
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user