[ci] Add the native ESP8266 compile smoke test (#19714)

This commit is contained in:
J. Nick Koston
2026-09-26 21:00:48 +01:00
committed by GitHub
parent 6c5010032b
commit b5b6d0bacd
6 changed files with 478 additions and 34 deletions
+98
View File
@@ -78,6 +78,17 @@ def mock_esp32_platformio_components_to_test() -> Generator[Mock, None, None]:
yield mock
@pytest.fixture
def mock_esp8266_native_components_to_test() -> Generator[Mock, None, None]:
"""Mock esp8266_native_components_to_test from determine_jobs.
main() drives both the ``esp8266_native`` boolean output and the
``esp8266_native_components`` CSV from this one function.
"""
with patch.object(determine_jobs, "esp8266_native_components_to_test") as mock:
yield mock
@pytest.fixture
def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]:
"""Mock determine_cpp_unit_tests from helpers."""
@@ -106,6 +117,7 @@ def clear_determine_jobs_caches() -> None:
"""Clear all cached functions before each test."""
determine_jobs._is_clang_tidy_full_scan.cache_clear()
determine_jobs._component_has_tests.cache_clear()
determine_jobs._cached_components_closure.cache_clear()
def test_main_all_tests_should_run(
@@ -116,6 +128,7 @@ def test_main_all_tests_should_run(
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],
@@ -132,6 +145,7 @@ def test_main_all_tests_should_run(
mock_should_run_import_time.return_value = True
mock_should_run_device_builder.return_value = True
mock_esp32_platformio_components_to_test.return_value = ["api", "esp32"]
mock_esp8266_native_components_to_test.return_value = ["api", "logger"]
mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"])
# Mock changed_files to return non-component files (to avoid memory impact)
@@ -208,6 +222,8 @@ def test_main_all_tests_should_run(
assert output["device_builder"] is True
assert output["esp32_platformio"] is True
assert output["esp32_platformio_components"] == "api,esp32"
assert output["esp8266_native"] is True
assert output["esp8266_native_components"] == "api,logger"
assert output["changed_components"] == ["wifi", "api", "sensor"]
# changed_components_with_tests will only include components that actually have test files
assert "changed_components_with_tests" in output
@@ -244,6 +260,7 @@ def test_main_no_tests_should_run(
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],
@@ -260,6 +277,7 @@ def test_main_no_tests_should_run(
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 to return no component files
@@ -302,6 +320,8 @@ def test_main_no_tests_should_run(
assert output["device_builder"] is False
assert output["esp32_platformio"] is False
assert output["esp32_platformio_components"] == ""
assert output["esp8266_native"] is False
assert output["esp8266_native_components"] == ""
assert output["changed_components"] == []
assert output["changed_components_with_tests"] == []
assert output["component_test_count"] == 0
@@ -997,6 +1017,9 @@ _ESP32_PLATFORMIO_FULL_LIST_FILES = [
# Workflow / harness files
["script/test_build_components.py"],
[".github/workflows/ci.yml"],
# The base config and bus packages every generated build includes
["tests/test_build_components/build_components_base.esp32-idf.yaml"],
["tests/test_build_components/common/uart/esp32-idf.yaml"],
]
@@ -3151,6 +3174,81 @@ def test_memory_impact_elf_layouts_are_found(tmp_path: Path) -> None:
assert find_elf_path(build_path) == elf, f"{platform} ELF not found"
@pytest.mark.parametrize(
"changed",
[
"esphome/arduino8266/framework.py",
"esphome/build_gen/arduino8266.py",
"esphome/components/esp8266/build_surgery.py",
# Shared modules the native build depends on
"esphome/build_helpers/idedata.py",
"esphome/platformio/library.py",
# Top-level esphome/*.py modules the backend imports directly
"esphome/framework_helpers.py",
"esphome/writer.py",
# esp8266/__init__.py imports copy_ccache_script from it
"esphome/platformio/toolchain.py",
# The composite cache action must not ship unexercised
".github/actions/cache-arduino8266/action.yml",
# The base config and bus packages every generated build includes
"tests/test_build_components/build_components_base.esp8266-ard.yaml",
"tests/test_build_components/common/uart/esp8266-ard.yaml",
],
)
def test_esp8266_native_components_full_list_on_infra_change(changed: str) -> None:
"""Native-ESP8266 infrastructure changes run the full test list."""
with (
patch.object(determine_jobs, "changed_files", return_value=[changed]),
patch.object(
determine_jobs,
"get_components_with_dependencies",
return_value=["wifi"],
),
):
result = determine_jobs.esp8266_native_components_to_test()
assert result == sorted(determine_jobs.ESP8266_NATIVE_TEST_COMPONENTS)
@pytest.mark.parametrize(
("changed_files", "dependency_closure", "expected"),
[
# Tested component changed -- narrow to the intersection.
(
["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"],
[],
),
# espidf infrastructure is not an esp8266-native trigger; the
# native backend depends on esphome/build_helpers/ instead.
(["esphome/build_gen/espidf.py"], [], []),
(["esphome/espidf/toolchain.py"], [], []),
(["README.md"], [], []),
],
)
def test_esp8266_native_components_to_test_narrowing(
changed_files: list[str],
dependency_closure: list[str],
expected: list[str],
) -> None:
"""Component changes narrow the native-ESP8266 test list."""
with (
patch.object(determine_jobs, "changed_files", return_value=changed_files),
patch.object(
determine_jobs,
"get_components_with_dependencies",
return_value=dependency_closure,
),
):
result = determine_jobs.esp8266_native_components_to_test()
assert result == expected
def test_compute_integration_test_buckets_no_durations_full_fanout() -> None:
"""Without recorded durations the fan-out stays at the maximum."""
files = [f"tests/integration/test_{i:03d}.py" for i in range(15)]
@@ -236,3 +236,98 @@ def test_run_grouped_test_closes_group_when_subprocess_raises(
)
assert "::endgroup::" in capsys.readouterr().out
def test_components_empty_match_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""Under --fail-on-no-tests, a real component filtered to a platform
with no matching test file must not pass CI as a green zero-component
compile."""
rc = tbc.test_components(
["logger"],
"zz-none",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No tests ran for requested pattern(s): logger" in (capsys.readouterr().out)
def test_components_component_with_no_base_file_fails_with_flag(
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A component whose fixture matches the platform but whose platform has
no base file builds nothing; under the flag that silent zero fails by
component name instead of hiding behind other components."""
monkeypatch.setattr(tbc, "get_platform_base_files", lambda base_dir: {})
rc = tbc.test_components(
["logger"],
"esp8266-ard",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No tests ran for requested pattern(s): logger" in (capsys.readouterr().out)
def test_components_blank_list_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""A fully blank component list must not slide into the baseline
fallback and exit green under the flag."""
rc = tbc.test_components(
[""], "esp8266-ard", "compile", False, fail_on_no_tests=True
)
assert rc == 1
assert "blank component list" in capsys.readouterr().out
def test_components_wildcard_no_match_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""A wildcard matching nothing must not degrade to the synthetic
baseline build and exit green under the flag."""
rc = tbc.test_components(
["zz_no_such*"],
"esp8266-ard",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No components found matching" in capsys.readouterr().out
def test_components_empty_match_tolerated_without_flag() -> None:
"""The esp32-ard smoke leg deliberately builds only the subset with a
matching fixture; without the flag an empty match stays green."""
assert (
tbc.test_components(
["logger"], "zz-none", "compile", False, enable_grouping=False
)
== 0
)
def test_components_unknown_component_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""A renamed smoke-test component must shrink coverage loudly, not fall
into the reference-baseline build."""
rc = tbc.test_components(
["no_such_component_xyz"],
"esp8266-ard",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No components found matching" in capsys.readouterr().out