Compare commits

...

2 Commits

Author SHA1 Message Date
J. Nick Koston 5b1618331f DO NOT MERGE: prove memory-impact no-common-platform fix in CI
Chained on top of the memory-impact fix to exercise it in real CI.

Changed components and their base-test platforms:
- const: variant-only test (test-display.esp32-s3-idf.yaml, no base test).
  --base-only cannot build it, so the fix must exclude it; it only injects
  an esp32-s3-idf platform hint.
- shelly_dimmer: base test esp8266-ard (real C++).
- daikin: base test esp8266-ard (real C++), the working third component.

Without the fix, the esp32-s3-idf hint (and const's variant counted as a
supported platform) selects esp32-s3-idf, which none of the three can build
under --base-only, yielding 0 builds and a failed memory extraction. With the
fix, const is excluded, the unbuildable hint is ignored in favor of the common
esp8266-ard platform, and shelly_dimmer + daikin generate memory impact.
2026-06-03 21:47:52 -05:00
J. Nick Koston 3ad851993f [ci] Fix memory impact build selecting unbuildable platform
Memory impact analysis gathered candidate platforms from all test files
(including variant test-*.yaml) but the CI build runs
test_build_components.py with --base-only, which only compiles base
test.<platform>.yaml files. It could therefore select a platform that no
changed component has a base test for, leaving the merged build with
nothing to compile and failing memory extraction.

Detect platforms using the same base-test discovery as the runner via a
shared get_component_test_platforms helper, and keep only components that
have a base test on the selected platform, falling back to the platform
supported by the most components when a hint picks an unbuildable one.
2026-06-03 21:18:09 -05:00
8 changed files with 213 additions and 73 deletions
+2
View File
@@ -1,3 +1,5 @@
// DNM: do not merge. Trivial touch to mark daikin changed; this is the working
// esp8266-ard component that should generate memory impact (see PR #16788).
#include "daikin.h"
#include "esphome/components/remote_base/remote_base.h"
@@ -1,3 +1,5 @@
// DNM: do not merge. Trivial touch to mark shelly_dimmer changed so the
// memory-impact CI job exercises the no-common-platform fix (see PR #16788).
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
+56 -41
View File
@@ -70,6 +70,7 @@ from helpers import (
get_changed_components,
get_component_from_path,
get_component_test_files,
get_component_test_platforms,
get_components_with_dependencies,
get_cpp_changed_components,
get_fixture_to_test_files,
@@ -77,7 +78,6 @@ from helpers import (
get_target_branch,
git_ls_files,
is_validate_only_file,
parse_test_filename,
root_path,
)
from split_components_for_ci import create_intelligent_batches
@@ -169,24 +169,6 @@ MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core ch
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
# Regular components (wifi, logger, api, etc.) are cross-platform and not listed here
PLATFORM_SPECIFIC_COMPONENTS = frozenset(
{
"esp32", # ESP32 platform implementation
"esp8266", # ESP8266 platform implementation
"rp2040", # Raspberry Pi Pico / RP2040 platform implementation
"libretiny", # LibreTiny base platform implementation
"bk72xx", # Beken BK72xx platform implementation (uses LibreTiny)
"rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny)
"ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny)
"host", # Host platform (for testing on development machine)
"nrf52", # Nordic nRF52 platform implementation (uses Zephyr)
"zephyr", # Zephyr RTOS platform implementation
}
)
# Platform preference order for memory impact analysis
# This order is used when no platform-specific hints are detected from filenames
# Priority rationale:
@@ -1006,23 +988,24 @@ def detect_memory_impact_config(
] = {} # Track which platforms each component supports
for component in sorted(changed_component_set):
# Look for test files on preferred platforms
test_files = get_component_test_files(component, all_variants=True)
if not test_files:
continue
# Check if component has tests for any preferred platform
available_platforms = [
platform
for test_file in test_files
if (platform := parse_test_filename(test_file)[1]) != "all"
and platform in MEMORY_IMPACT_PLATFORM_PREFERENCE
]
# Discover the platforms this component has BASE tests for, using the
# same logic as the build runner (get_component_test_platforms wraps the
# shared get_component_test_files + parse_test_filename helpers). Base
# tests only: the memory impact CI build runs test_build_components.py
# with --base-only, which compiles base test.<platform>.yaml files but
# never variant test-<variant>.<platform>.yaml files. Counting
# variant-only platforms here would let us select a platform the build
# then has nothing to compile for, producing no memory output.
available_platforms = {
Platform(platform)
for platform in get_component_test_platforms(component)
if platform in MEMORY_IMPACT_PLATFORM_PREFERENCE
}
if not available_platforms:
continue
component_platforms_map[component] = set(available_platforms)
component_platforms_map[component] = available_platforms
components_with_tests.append(component)
# If no components have tests, don't run memory impact
@@ -1084,20 +1067,52 @@ def detect_memory_impact_config(
)
platform = _select_platform_by_count(platform_counts)
# Filter out platform-specific components that are incompatible with selected platform
# Platform components (esp32, esp8266, rp2040, etc.) can only build on their own platform
# Other components (wifi, logger, etc.) are cross-platform and can build anywhere
compatible_components = [
component
for component in components_with_tests
if component not in PLATFORM_SPECIFIC_COMPONENTS
or platform in component_platforms_map.get(component, set())
]
# Keep only components that have a base test on the selected platform.
# The merged build runs test_build_components.py -t <platform> --base-only,
# so a component without a base test.<platform>.yaml compiles nothing and
# contributes no memory output. This also covers platform-specific
# components (esp32, esp8266, etc.), which only have tests on their own
# platform. When components don't share a common platform we build the
# largest subset that does, dropping the rest.
def components_supporting(target: Platform) -> list[str]:
return [
component
for component in components_with_tests
if target in component_platforms_map.get(component, set())
]
compatible_components = components_supporting(platform)
# A platform hint (or no-common-platform fallback) can pick a platform that
# no changed component actually has a base test for, leaving nothing to
# build. In that case fall back to the platform supported by the most
# components, which by construction is backed by at least one base test.
if not compatible_components:
platform = _select_platform_by_count(
Counter(
p for platforms in component_platforms_map.values() for p in platforms
)
)
compatible_components = components_supporting(platform)
# If no components are compatible with the selected platform, don't run
if not compatible_components:
return {"should_run": "false"}
# Log components dropped because they lack a base test on the selected
# platform so partial-subset builds are visible in CI logs.
dropped_components = [
component
for component in components_with_tests
if component not in compatible_components
]
if dropped_components:
print(
f"Memory impact: Dropping components without a base test on "
f"{platform}: {dropped_components}",
file=sys.stderr,
)
# Debug output
print("Memory impact analysis:", file=sys.stderr)
print(f" Changed components: {sorted(changed_component_set)}", file=sys.stderr)
+25
View File
@@ -149,6 +149,31 @@ def get_component_test_files(
return files
def get_component_test_platforms(component: str, *, base_only: bool = True) -> set[str]:
"""Return the set of platforms a component has compilable test files for.
Uses the same discovery as ``test_build_components.py`` (``get_component_test_files``
+ ``parse_test_filename``) so callers agree with what the build runner would
actually compile. With ``base_only=True`` (the default, matching the
memory-impact build's ``--base-only``), only base ``test.<platform>.yaml``
files are considered; variant ``test-<variant>.<platform>.yaml`` files are
excluded. The ``"all"`` platform sentinel is excluded.
Args:
component: Component name (e.g. "wifi")
base_only: If True, only consider base test files (default).
Returns:
Set of platform identifiers (e.g. {"esp32-idf", "esp8266-ard"}).
"""
platforms: set[str] = set()
for test_file in get_component_test_files(component, all_variants=not base_only):
platform = parse_test_filename(test_file)[1]
if platform != "all":
platforms.add(platform)
return platforms
def is_validate_only_file(test_file: Path) -> bool:
"""Return True if the given path is a config-only validate file.
+1 -15
View File
@@ -42,6 +42,7 @@ from script.analyze_component_buses import (
from script.helpers import (
get_component_test_files,
is_validate_only_file,
parse_test_filename,
split_conflicting_groups,
)
from script.merge_component_configs import merge_component_configs
@@ -122,21 +123,6 @@ def find_component_tests(
return dict(component_tests)
def parse_test_filename(test_file: Path) -> tuple[str, str]:
"""Parse test filename to extract test name and platform.
Args:
test_file: Path to test file
Returns:
Tuple of (test_name, platform)
"""
parts = test_file.stem.split(".")
if len(parts) == 2:
return parts[0], parts[1] # test, platform
return parts[0], "all"
def get_platform_base_files(base_dir: Path) -> dict[str, list[Path]]:
"""Get all platform base files.
+37
View File
@@ -0,0 +1,37 @@
display:
- platform: qspi_dbi
model: RM690B0
data_rate: 80MHz
spi_mode: mode0
dimensions:
width: 450
height: 600
offset_width: 16
color_order: rgb
invert_colors: false
brightness: 255
cs_pin: 11
reset_pin: 13
enable_pin: 9
- platform: qspi_dbi
model: CUSTOM
id: main_lcd
draw_from_origin: true
dimensions:
height: 240
width: 536
transform:
mirror_x: true
swap_xy: true
color_order: rgb
brightness: 255
cs_pin: 6
reset_pin: 17
enable_pin: 38
init_sequence:
- [0x3A, 0x66]
- [0x11]
- delay 120ms
- [0x29]
- delay 20ms
@@ -0,0 +1,7 @@
# DNM proof: this is a VARIANT test (test-display.*) with no base test.*.yaml.
# The memory-impact build runs --base-only, so const has nothing buildable and
# must be excluded; it only contributes an esp32-s3-idf platform hint.
packages:
qspi: !include ../../test_build_components/common/qspi/esp32-s3-idf.yaml
<<: !include common.yaml
+83 -17
View File
@@ -1426,7 +1426,15 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) ->
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
"""Test memory impact detection when components have no common platform."""
"""Test memory impact detection when components have no common platform.
The merged build runs with --base-only on a single platform, so components
without a base test on the selected platform cannot be built and must be
dropped. We build the largest subset that shares the selected platform
rather than handing the runner components it has nothing to compile for
(which previously produced "0 passed, 0 failed" and a failed memory
extraction).
"""
# Create test directory structure
tests_dir = tmp_path / "tests" / "components"
@@ -1453,12 +1461,70 @@ def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
result = determine_jobs.detect_memory_impact_config()
# Should pick the most frequently supported platform
# No common platform: pick the most preferred platform among those supported
# (esp8266-ard outranks esp32-idf in the preference list) and build only the
# components that have a base test on it. wifi (esp32-idf only) is dropped.
assert result["should_run"] == "true"
assert set(result["components"]) == {"wifi", "logger"}
# When no common platform, picks most commonly supported
# esp8266-ard is preferred over esp32-idf in the preference list
assert result["platform"] in ["esp32-idf", "esp8266-ard"]
assert result["platform"] == "esp8266-ard"
assert result["components"] == ["logger"]
assert result["use_merged_config"] == "true"
def test_detect_memory_impact_config_variant_only_platform_excluded(
tmp_path: Path,
) -> None:
"""Regression test for the const + shelly_dimmer memory-impact failure.
Reproduces https://github.com/esphome/esphome/actions/runs/26746938473
where a platform hint selected esp32-idf even though neither changed
component had a base test.esp32-idf.yaml. The merged --base-only build then
found nothing to compile ("0 passed, 0 failed") and memory extraction
failed. Also covers a component whose only esp32-idf test is a *variant*
(test-*.esp32-idf.yaml): --base-only never compiles variants, so it must
not count toward platform availability.
"""
tests_dir = tmp_path / "tests" / "components"
# const: base test only on esp32-s3-idf
const_dir = tests_dir / "const"
const_dir.mkdir(parents=True)
(const_dir / "test.esp32-s3-idf.yaml").write_text("test: const")
# shelly_dimmer: base test only on esp8266-ard
shelly_dir = tests_dir / "shelly_dimmer"
shelly_dir.mkdir(parents=True)
(shelly_dir / "test.esp8266-ard.yaml").write_text("test: shelly_dimmer")
# mdns: only a VARIANT test on esp32-idf (no base test.esp32-idf.yaml).
# --base-only would never build it, so it must be excluded entirely.
mdns_dir = tests_dir / "mdns"
mdns_dir.mkdir(parents=True)
(mdns_dir / "test-min.esp32-idf.yaml").write_text("test: mdns")
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,
):
# The "_esp32" filename yields an esp32-idf platform hint, reproducing
# the original bug where the hint picked a platform no component could
# build as a base test.
mock_changed_files.return_value = [
"esphome/components/const/const.cpp",
"esphome/components/shelly_dimmer/shelly_dimmer_esp32.cpp",
"esphome/components/mdns/mdns.cpp",
]
result = determine_jobs.detect_memory_impact_config()
# The esp32-idf hint is unbuildable (no base test), so we fall back to the
# platform supported by the most components, broken by preference order:
# esp8266-ard (shelly_dimmer) outranks esp32-s3-idf (const). Only the
# component with a base test on the selected platform is returned; the
# variant-only mdns is excluded entirely.
assert result["should_run"] == "true"
assert result["platform"] == "esp8266-ard"
assert result["components"] == ["shelly_dimmer"]
assert result["use_merged_config"] == "true"
@@ -1545,12 +1611,16 @@ def test_detect_memory_impact_config_includes_base_bus_components(
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
"""Test memory impact detection for components with only variant test files.
def test_detect_memory_impact_config_variant_only_components_skipped(
tmp_path: Path,
) -> None:
"""Components with only variant tests are skipped for memory impact.
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).
Components like improv_serial and ethernet only have variant test files
(test-*.yaml), no base test.<platform>.yaml. The memory-impact build runs
test_build_components.py with --base-only, which never compiles variants, so
these components have nothing buildable and must not be selected. Selecting
them previously produced "0 passed, 0 failed" and a failed memory extraction.
"""
# Create test directory structure
tests_dir = tmp_path / "tests" / "components"
@@ -1581,12 +1651,8 @@ def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
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"
# Neither component has a base test, so nothing is buildable under --base-only
assert result["should_run"] == "false"
# Tests for clang-tidy split mode logic