diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11d073e7e3..6c89712e05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,9 +193,10 @@ jobs: needs: - common # PR-branch cache saves are invisible to other PRs, so dev pushes seed - # the shared entry test-esp8266-native restores. Only dev: the composite - # action saves nowhere else, so a beta/release push would download the - # toolchain and discard it. + # the shared entry the component matrix, the memory impact jobs and + # test-esp8266-native restore. Only dev: the composite action saves + # nowhere else, so a beta/release push would download the toolchain and + # discard it. if: github.event_name == 'push' && github.ref == 'refs/heads/dev' timeout-minutes: 15 steps: @@ -1127,6 +1128,11 @@ jobs: uses: ./.github/actions/cache-sdk-nrf with: restore-only: true + - name: Cache the native ESP8266 toolchain + # Only batches whose test platforms include esp8266; never saves + # here, it reuses the install the dev seed job cached. + if: matrix.batch.needs_arduino8266 + uses: ./.github/actions/cache-arduino8266 - name: Validate and compile components with intelligent grouping run: | . venv/bin/activate @@ -1479,12 +1485,9 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - name: Cache platformio - if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.platformio - key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} + - name: Cache the native ESP8266 toolchain + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266 + uses: ./.github/actions/cache-arduino8266 - name: Build, compile, and analyze memory if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' @@ -1569,11 +1572,9 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - name: Cache platformio - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.platformio - key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} + - name: Cache the native ESP8266 toolchain + if: fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266 + uses: ./.github/actions/cache-arduino8266 - name: Build, compile, and analyze memory id: extract run: | diff --git a/script/determine-jobs.py b/script/determine-jobs.py index aa93074581..a45951a52a 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -724,11 +724,17 @@ def _esp8266_native_path_or_file_trigger(files: list[str]) -> bool: def esp8266_native_components_to_test(branch: str | None = None) -> list[str]: - """Subset of ``ESP8266_NATIVE_TEST_COMPONENTS`` the job needs to - compile (same narrowing as ``esp32_platformio_components_to_test``).""" - return _toolchain_components_to_test( - branch, ESP8266_NATIVE_TEST_COMPONENTS, _esp8266_native_path_or_file_trigger - ) + """The smoke set on a native-build change, nothing otherwise. + + Unlike the esp32 PlatformIO job, this one does not narrow to the changed + components: the component matrix already compiles every esp8266 fixture + with this toolchain, so the only gap left is a change to the native build + itself that brings no component along. + """ + files = changed_files(branch) + if core_changed(files) or _esp8266_native_path_or_file_trigger(files): + return sorted(ESP8266_NATIVE_TEST_COMPONENTS) + return [] def determine_cpp_unit_tests( @@ -1286,6 +1292,7 @@ def detect_memory_impact_config( "components": compatible_components, "platform": platform, "use_merged_config": "true", + "needs_arduino8266": platform.startswith("esp8266"), } @@ -1516,12 +1523,20 @@ def main() -> None: for batch in batches: platforms: set[str] = set() for component in batch: - platforms.update(get_component_test_platforms(component)) + # Variants included: the compile stage builds them, so a + # component tested only by test-..yaml + # still needs that platform's toolchain + platforms.update( + get_component_test_platforms(component, base_only=False) + ) component_test_batches.append( { "components": " ".join(batch), "needs_idf": any(p.startswith("esp32") for p in platforms), "needs_nrf": any(p.startswith("nrf52") for p in platforms), + "needs_arduino8266": any( + p.startswith("esp8266") for p in platforms + ), } ) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 4903f1a88a..c3f30b8ed1 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -250,6 +250,82 @@ def test_main_all_tests_should_run( assert len(batch["components"]) > 0 assert isinstance(batch["needs_idf"], bool) assert isinstance(batch["needs_nrf"], bool) + assert isinstance(batch["needs_arduino8266"], bool) + + +def test_main_batch_flags_count_variant_tests( + mock_determine_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, + mock_esp32_platformio_components_to_test: Mock, + mock_esp8266_native_components_to_test: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The compile stage builds test-..yaml too, so a + component tested on esp8266 only by a variant still needs the toolchain.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + mock_determine_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_should_run_import_time.return_value = False + mock_should_run_device_builder.return_value = False + mock_esp32_platformio_components_to_test.return_value = [] + mock_esp8266_native_components_to_test.return_value = [] + mock_determine_cpp_unit_tests.return_value = (False, []) + mock_changed_files.return_value = ["esphome/components/safe_mode/__init__.py"] + + def platforms(component: str, *, base_only: bool = True) -> set[str]: + return set() if base_only else {"esp8266-ard"} + + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, "get_changed_components", return_value=["safe_mode"] + ), + 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", + return_value=["safe_mode"], + ), + patch.object(determine_jobs, "_component_has_tests", return_value=True), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["safe_mode"]], {}), + ), + patch.object( + determine_jobs, "get_component_test_platforms", side_effect=platforms + ), + ): + determine_jobs.main() + + output = json.loads(capsys.readouterr().out) + assert output["component_test_batches"] == [ + { + "components": "safe_mode", + "needs_idf": False, + "needs_nrf": False, + "needs_arduino8266": True, + } + ] def test_main_no_tests_should_run( @@ -1530,6 +1606,7 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non assert set(result["components"]) == {"wifi", "api"} assert result["platform"] == "esp32-idf" # Common platform assert result["use_merged_config"] == "true" + assert result["needs_arduino8266"] is False @pytest.mark.usefixtures("mock_target_branch_dev") @@ -1634,6 +1711,8 @@ def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None: assert result["platform"] == "esp8266-ard" assert result["components"] == ["logger"] assert result["use_merged_config"] == "true" + # The esp8266 build is native, so the job restores that toolchain + assert result["needs_arduino8266"] is True @pytest.mark.usefixtures("mock_target_branch_dev") @@ -3212,13 +3291,13 @@ def test_esp8266_native_components_full_list_on_infra_change(changed: str) -> No @pytest.mark.parametrize( ("changed_files", "dependency_closure", "expected"), [ - # Tested component changed -- narrow to the intersection. + # A tested component alone does not schedule this job: the component + # matrix already compiles its esp8266 fixtures with this toolchain. ( ["esphome/components/mqtt/mqtt_client.cpp"], ["mqtt", "json"], - ["mqtt"], + [], ), - # Components outside the test set return an empty list (job skipped). ( ["esphome/components/wifi/wifi_component.cpp"], ["wifi", "network"], @@ -3236,7 +3315,7 @@ def test_esp8266_native_components_to_test_narrowing( dependency_closure: list[str], expected: list[str], ) -> None: - """Component changes narrow the native-ESP8266 test list.""" + """Only a native-build change schedules the native-ESP8266 job.""" with ( patch.object(determine_jobs, "changed_files", return_value=changed_files), patch.object( diff --git a/tests/test_build_components/build_components_base.esp8266-ard.yaml b/tests/test_build_components/build_components_base.esp8266-ard.yaml index 1e2d614392..a20aa573ae 100644 --- a/tests/test_build_components/build_components_base.esp8266-ard.yaml +++ b/tests/test_build_components/build_components_base.esp8266-ard.yaml @@ -4,6 +4,7 @@ esphome: esp8266: board: d1_mini_pro + toolchain: arduino logger: level: VERY_VERBOSE