From 6220084fe684f357c99f4261832f09c8cf7a76c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 19:23:04 -0600 Subject: [PATCH] [ci] Fix memory impact analysis to filter incompatible platform components (#11706) --- script/determine-jobs.py | 33 ++++++++- tests/script/test_determine_jobs.py | 108 ++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 4a0edebb0d..6f908b7150 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -94,6 +94,22 @@ class Platform(StrEnum): MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform +# 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 + "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 + } +) + # Platform preference order for memory impact analysis # This order is used when no platform-specific hints are detected from filenames # Priority rationale: @@ -568,6 +584,20 @@ 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()) + ] + + # If no components are compatible with the selected platform, don't run + if not compatible_components: + return {"should_run": "false"} + # Debug output print("Memory impact analysis:", file=sys.stderr) print(f" Changed components: {sorted(changed_component_set)}", file=sys.stderr) @@ -579,10 +609,11 @@ def detect_memory_impact_config( print(f" Platform hints from filenames: {platform_hints}", file=sys.stderr) print(f" Common platforms: {sorted(common_platforms)}", file=sys.stderr) print(f" Selected platform: {platform}", file=sys.stderr) + print(f" Compatible components: {compatible_components}", file=sys.stderr) return { "should_run": "true", - "components": components_with_tests, + "components": compatible_components, "platform": platform, "use_merged_config": "true", } diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index e73c134151..a33eca5b19 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1130,3 +1130,111 @@ def test_main_core_files_changed_still_detects_components( assert "select" in output["changed_components"] assert "api" in output["changed_components"] assert len(output["changed_components"]) > 0 + + +def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266( + tmp_path: Path, +) -> None: + """Test that ESP32 components are filtered out when ESP8266 platform is selected. + + This test verifies the fix for the issue where ESP32 components were being included + when ESP8266 was selected as the platform, causing build failures in PR 10387. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include esp8266-specific filename to trigger esp8266 platform hint + 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 = [ + "tests/components/esp32/common.yaml", + "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() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp8266-ard (due to ESP8266 filename hint) + assert result["platform"] == "esp8266-ard" + + # CRITICAL: Only esp8266 component should be included, not esp32 + # This prevents trying to build ESP32 components on ESP8266 platform + assert result["components"] == ["esp8266"], ( + "When esp8266-ard platform is selected, only esp8266 component should be included, " + "not esp32. This prevents trying to build ESP32 components on ESP8266 platform." + ) + + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( + tmp_path: Path, +) -> None: + """Test that ESP8266 components are filtered out when ESP32 platform is selected. + + This is the inverse of the ESP8266 test - ensures filtering works both ways. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include MORE esp32-specific filenames to ensure esp32-idf wins the hint count + 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 = [ + "tests/components/esp32/common.yaml", + "tests/components/esp8266/test.esp8266-ard.yaml", + "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() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp32-idf (due to more ESP32-IDF hints) + assert result["platform"] == "esp32-idf" + + # CRITICAL: Only esp32 component should be included, not esp8266 + # This prevents trying to build ESP8266 components on ESP32 platform + assert result["components"] == ["esp32"], ( + "When esp32-idf platform is selected, only esp32 component should be included, " + "not esp8266. This prevents trying to build ESP8266 components on ESP32 platform." + ) + + assert result["use_merged_config"] == "true"