mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 12:17:23 +00:00
[ci] Skip native ESP-IDF compile test when no relevant files changed (#16395)
This commit is contained in:
@@ -70,6 +70,17 @@ def mock_should_run_device_builder() -> Generator[Mock, None, None]:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_native_idf_components_to_test() -> Generator[Mock, None, None]:
|
||||
"""Mock native_idf_components_to_test from determine_jobs.
|
||||
|
||||
main() drives both the ``native_idf`` boolean output and the
|
||||
``native_idf_components`` CSV from this one function.
|
||||
"""
|
||||
with patch.object(determine_jobs, "native_idf_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."""
|
||||
@@ -107,6 +118,7 @@ def test_main_all_tests_should_run(
|
||||
mock_should_run_python_linters: Mock,
|
||||
mock_should_run_import_time: Mock,
|
||||
mock_should_run_device_builder: Mock,
|
||||
mock_native_idf_components_to_test: Mock,
|
||||
mock_changed_files: Mock,
|
||||
mock_determine_cpp_unit_tests: Mock,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
@@ -122,6 +134,7 @@ def test_main_all_tests_should_run(
|
||||
mock_should_run_python_linters.return_value = True
|
||||
mock_should_run_import_time.return_value = True
|
||||
mock_should_run_device_builder.return_value = True
|
||||
mock_native_idf_components_to_test.return_value = ["api", "esp32"]
|
||||
mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"])
|
||||
|
||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||
@@ -203,6 +216,8 @@ def test_main_all_tests_should_run(
|
||||
assert output["python_linters"] is True
|
||||
assert output["import_time"] is True
|
||||
assert output["device_builder"] is True
|
||||
assert output["native_idf"] is True
|
||||
assert output["native_idf_components"] == "api,esp32"
|
||||
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
|
||||
@@ -236,6 +251,7 @@ def test_main_no_tests_should_run(
|
||||
mock_should_run_python_linters: Mock,
|
||||
mock_should_run_import_time: Mock,
|
||||
mock_should_run_device_builder: Mock,
|
||||
mock_native_idf_components_to_test: Mock,
|
||||
mock_changed_files: Mock,
|
||||
mock_determine_cpp_unit_tests: Mock,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
@@ -251,6 +267,7 @@ def test_main_no_tests_should_run(
|
||||
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_native_idf_components_to_test.return_value = []
|
||||
mock_determine_cpp_unit_tests.return_value = (False, [])
|
||||
|
||||
# Mock changed_files to return no component files
|
||||
@@ -291,6 +308,8 @@ def test_main_no_tests_should_run(
|
||||
assert output["python_linters"] is False
|
||||
assert output["import_time"] is False
|
||||
assert output["device_builder"] is False
|
||||
assert output["native_idf"] is False
|
||||
assert output["native_idf_components"] == ""
|
||||
assert output["changed_components"] == []
|
||||
assert output["changed_components_with_tests"] == []
|
||||
assert output["component_test_count"] == 0
|
||||
@@ -313,6 +332,7 @@ def test_main_with_branch_argument(
|
||||
mock_should_run_python_linters: Mock,
|
||||
mock_should_run_import_time: Mock,
|
||||
mock_should_run_device_builder: Mock,
|
||||
mock_native_idf_components_to_test: Mock,
|
||||
mock_changed_files: Mock,
|
||||
mock_determine_cpp_unit_tests: Mock,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
@@ -328,6 +348,7 @@ def test_main_with_branch_argument(
|
||||
mock_should_run_python_linters.return_value = True
|
||||
mock_should_run_import_time.return_value = True
|
||||
mock_should_run_device_builder.return_value = True
|
||||
mock_native_idf_components_to_test.return_value = ["esp32"]
|
||||
mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"])
|
||||
|
||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||
@@ -366,6 +387,7 @@ def test_main_with_branch_argument(
|
||||
mock_should_run_python_linters.assert_called_once_with("main")
|
||||
mock_should_run_import_time.assert_called_once_with("main")
|
||||
mock_should_run_device_builder.assert_called_once_with("main")
|
||||
mock_native_idf_components_to_test.assert_called_once_with("main")
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
@@ -379,6 +401,8 @@ def test_main_with_branch_argument(
|
||||
assert output["python_linters"] is True
|
||||
assert output["import_time"] is True
|
||||
assert output["device_builder"] is True
|
||||
assert output["native_idf"] is True
|
||||
assert output["native_idf_components"] == "esp32"
|
||||
assert output["changed_components"] == ["mqtt"]
|
||||
# changed_components_with_tests will only include components that actually have test files
|
||||
assert "changed_components_with_tests" in output
|
||||
@@ -827,6 +851,142 @@ def test_should_run_device_builder_skips_beta_release(target_branch: str) -> Non
|
||||
mock_changed.assert_not_called()
|
||||
|
||||
|
||||
_NATIVE_IDF_FULL_LIST_FILES = [
|
||||
# Core C++/Python changes -- caught by core_changed()
|
||||
["esphome/core/component.cpp"],
|
||||
["esphome/core/config.py"],
|
||||
# Native IDF infrastructure paths
|
||||
["esphome/espidf/framework.py"],
|
||||
["esphome/espidf/component.py"],
|
||||
["esphome/espidf/api.py"],
|
||||
["esphome/build_gen/espidf.py"],
|
||||
# Workflow / harness files
|
||||
["script/test_build_components.py"],
|
||||
[".github/workflows/ci.yml"],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("changed_files", _NATIVE_IDF_FULL_LIST_FILES)
|
||||
def test_native_idf_components_to_test_returns_full_list_on_infrastructure(
|
||||
changed_files: list[str],
|
||||
) -> None:
|
||||
"""Infrastructure / core / harness changes fall back to the full component list."""
|
||||
with (
|
||||
patch.object(determine_jobs, "changed_files", return_value=changed_files),
|
||||
# The dep-closure path shouldn't be consulted at all -- if it is,
|
||||
# the obviously-wrong "wifi" sneaks in and the assertion catches it.
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=["wifi"]
|
||||
),
|
||||
):
|
||||
result = determine_jobs.native_idf_components_to_test()
|
||||
assert result == sorted(determine_jobs.NATIVE_IDF_TEST_COMPONENTS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("changed_files", "dependency_closure", "expected"),
|
||||
[
|
||||
# Single tested component changed -- narrow to just that component.
|
||||
(
|
||||
["esphome/components/esp32/__init__.py"],
|
||||
["esp32"],
|
||||
["esp32"],
|
||||
),
|
||||
# Dependency closure: multiple BLE components in the changed set
|
||||
# are all intersected with the test list and returned sorted.
|
||||
(
|
||||
["esphome/components/esp32_ble/ble.cpp"],
|
||||
["esp32_ble", "esp32_ble_tracker", "ble_scanner"],
|
||||
["ble_scanner", "esp32_ble", "esp32_ble_tracker"],
|
||||
),
|
||||
# api in the test set -- narrow to [api] even though the closure
|
||||
# has other (unrelated to native-IDF coverage) entries.
|
||||
(
|
||||
["esphome/components/api/api_connection.cpp"],
|
||||
["api", "logger"],
|
||||
["api"],
|
||||
),
|
||||
# Components outside the test set return an empty list (job skipped).
|
||||
(
|
||||
["esphome/components/wifi/wifi_component.cpp"],
|
||||
["wifi", "network"],
|
||||
[],
|
||||
),
|
||||
# Pure Python-only change outside trigger paths -> empty.
|
||||
(["esphome/yaml_util.py"], [], []),
|
||||
# Non-IDF files in esphome/build_gen/ do NOT trigger the full
|
||||
# list -- only esphome/build_gen/espidf.py is a trigger.
|
||||
(["esphome/build_gen/platformio.py"], [], []),
|
||||
# Docs / unrelated files -> empty.
|
||||
(["README.md"], [], []),
|
||||
([], [], []),
|
||||
],
|
||||
)
|
||||
def test_native_idf_components_to_test_narrowing(
|
||||
changed_files: list[str],
|
||||
dependency_closure: list[str],
|
||||
expected: list[str],
|
||||
) -> None:
|
||||
"""Component changes narrow the test list to the intersection."""
|
||||
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.native_idf_components_to_test()
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_native_idf_components_to_test_with_branch() -> None:
|
||||
"""native_idf_components_to_test passes branch argument through.
|
||||
|
||||
Regression test: an earlier version called ``get_changed_components()``,
|
||||
which silently ignored the branch argument because that helper re-runs
|
||||
``changed_files()`` with its own default. The current implementation
|
||||
derives the closure from ``files = changed_files(branch)`` directly,
|
||||
so a branch arg has to flow through ``changed_files``.
|
||||
"""
|
||||
with (
|
||||
patch.object(determine_jobs, "changed_files") as mock_changed,
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=[]
|
||||
),
|
||||
):
|
||||
mock_changed.return_value = []
|
||||
determine_jobs.native_idf_components_to_test("release")
|
||||
mock_changed.assert_called_once_with("release")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("components_to_test", "expected"),
|
||||
[
|
||||
([], False),
|
||||
(["esp32"], True),
|
||||
(["esp32", "api"], True),
|
||||
],
|
||||
)
|
||||
def test_should_run_native_idf(components_to_test: list[str], expected: bool) -> None:
|
||||
"""should_run_native_idf is a thin wrapper around the component list."""
|
||||
with patch.object(
|
||||
determine_jobs,
|
||||
"native_idf_components_to_test",
|
||||
return_value=components_to_test,
|
||||
):
|
||||
assert determine_jobs.should_run_native_idf() is expected
|
||||
|
||||
|
||||
def test_should_run_native_idf_with_branch() -> None:
|
||||
"""Test should_run_native_idf passes branch argument through."""
|
||||
with patch.object(
|
||||
determine_jobs, "native_idf_components_to_test", return_value=[]
|
||||
) as mock_inner:
|
||||
determine_jobs.should_run_native_idf("release")
|
||||
mock_inner.assert_called_once_with("release")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("changed_files", "expected_result"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user