mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 14:55:05 +00:00
[ci] Add validate.*.yaml for config-only component tests (#16384)
This commit is contained in:
@@ -2215,3 +2215,230 @@ def test_should_run_benchmarks_with_branch() -> None:
|
||||
mock_changed.return_value = []
|
||||
determine_jobs.should_run_benchmarks("release")
|
||||
mock_changed.assert_called_with("release")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _component_change_is_validate_only
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("component", "changed", "expected"),
|
||||
[
|
||||
# Only a base validate file changed.
|
||||
(
|
||||
"foo",
|
||||
["tests/components/foo/validate.esp32-idf.yaml"],
|
||||
True,
|
||||
),
|
||||
# Only a validate variant changed.
|
||||
(
|
||||
"foo",
|
||||
["tests/components/foo/validate-legacy.esp32-idf.yaml"],
|
||||
True,
|
||||
),
|
||||
# Multiple validate files (all validate).
|
||||
(
|
||||
"foo",
|
||||
[
|
||||
"tests/components/foo/validate.esp32-idf.yaml",
|
||||
"tests/components/foo/validate-legacy.esp32-idf.yaml",
|
||||
],
|
||||
True,
|
||||
),
|
||||
# Mixed: validate + regular test must NOT be classified as validate-only.
|
||||
(
|
||||
"foo",
|
||||
[
|
||||
"tests/components/foo/validate.esp32-idf.yaml",
|
||||
"tests/components/foo/test.esp32-idf.yaml",
|
||||
],
|
||||
False,
|
||||
),
|
||||
# Regular test only.
|
||||
(
|
||||
"foo",
|
||||
["tests/components/foo/test.esp32-idf.yaml"],
|
||||
False,
|
||||
),
|
||||
# Source change disqualifies even if a validate file is also touched.
|
||||
(
|
||||
"foo",
|
||||
[
|
||||
"esphome/components/foo/foo.cpp",
|
||||
"tests/components/foo/validate.esp32-idf.yaml",
|
||||
],
|
||||
False,
|
||||
),
|
||||
# No matching files at all.
|
||||
("foo", ["esphome/core/helpers.cpp"], False),
|
||||
# Filenames merely starting with "validate" but not following the
|
||||
# grammar must not match (defensive against accidental classification).
|
||||
(
|
||||
"foo",
|
||||
["tests/components/foo/validatesomething.yaml"],
|
||||
False,
|
||||
),
|
||||
# An unrelated component's validate change doesn't affect this one.
|
||||
(
|
||||
"foo",
|
||||
["tests/components/bar/validate.esp32-idf.yaml"],
|
||||
False,
|
||||
),
|
||||
# common.yaml change in the component dir disqualifies.
|
||||
(
|
||||
"foo",
|
||||
[
|
||||
"tests/components/foo/common.yaml",
|
||||
"tests/components/foo/validate.esp32-idf.yaml",
|
||||
],
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_component_change_is_validate_only(
|
||||
component: str, changed: list[str], expected: bool
|
||||
) -> None:
|
||||
"""The validate-only classifier rejects anything beyond validate.* edits."""
|
||||
assert (
|
||||
determine_jobs._component_change_is_validate_only(component, changed)
|
||||
is expected
|
||||
)
|
||||
|
||||
|
||||
def test_main_emits_validate_only_components(
|
||||
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_changed_files: Mock,
|
||||
mock_determine_cpp_unit_tests: Mock,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Directly-changed components whose only edits are validate.*.yaml are
|
||||
listed in `validate_only_components` so CI can skip their compile stage.
|
||||
"""
|
||||
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_determine_cpp_unit_tests.return_value = (False, [])
|
||||
|
||||
# foo: only validate file changed (qualifies)
|
||||
# bar: test file changed (does not qualify)
|
||||
mock_changed_files.return_value = [
|
||||
"tests/components/foo/validate.esp32-idf.yaml",
|
||||
"tests/components/bar/test.esp32-idf.yaml",
|
||||
]
|
||||
|
||||
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=["foo", "bar"],
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_and_test_files",
|
||||
side_effect=lambda f: f.startswith("tests/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
side_effect=lambda files, deps: ["foo", "bar"],
|
||||
),
|
||||
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=([["foo", "bar"]], {}),
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
output = json.loads(capsys.readouterr().out)
|
||||
assert output["validate_only_components"] == ["foo"]
|
||||
|
||||
|
||||
def test_main_validate_only_excludes_transitive_components(
|
||||
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_changed_files: Mock,
|
||||
mock_determine_cpp_unit_tests: Mock,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A component pulled in only as a dependency must NOT be considered
|
||||
validate-only, even if it has no source changes -- its dependency moved,
|
||||
so the compile is still required.
|
||||
"""
|
||||
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_determine_cpp_unit_tests.return_value = (False, [])
|
||||
|
||||
# Only foo's validate file changed directly. bar is a transitive dep.
|
||||
mock_changed_files.return_value = [
|
||||
"tests/components/foo/validate.esp32-idf.yaml",
|
||||
]
|
||||
|
||||
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=["foo", "bar"], # bar pulled in via dependencies
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_and_test_files",
|
||||
side_effect=lambda f: f.startswith("tests/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
# deps=False -> directly_changed = [foo]; deps=True -> [foo, bar]
|
||||
side_effect=lambda files, deps: ["foo", "bar"] if deps else ["foo"],
|
||||
),
|
||||
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=([["foo", "bar"]], {}),
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
output = json.loads(capsys.readouterr().out)
|
||||
# Only foo (directly changed, validate-only). bar is a transitive dep
|
||||
# and still needs compile despite no source change of its own.
|
||||
assert output["validate_only_components"] == ["foo"]
|
||||
|
||||
@@ -1624,3 +1624,171 @@ def test_split_conflicting_groups_preserves_original_signature_for_first_bucket(
|
||||
platform, signature = next(iter(extra))
|
||||
assert platform == "esp32"
|
||||
assert signature.startswith("i2c__conflict")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_component_test_files / is_validate_only_file
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_component_tests(tmp_path: Path) -> Path:
|
||||
"""Create a fake tests/components/ tree and return the repo root.
|
||||
|
||||
Layout for component "demo":
|
||||
test.esp32-idf.yaml
|
||||
test.esp8266-ard.yaml
|
||||
test-variant.esp32-idf.yaml
|
||||
validate.esp32-idf.yaml
|
||||
validate-legacy.esp32-idf.yaml
|
||||
|
||||
Layout for component "validate_only":
|
||||
validate.esp32-idf.yaml (only validate files)
|
||||
|
||||
Layout for component "no_tests":
|
||||
common.yaml (no test/validate files at all)
|
||||
"""
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
|
||||
demo = tests_dir / "demo"
|
||||
demo.mkdir(parents=True)
|
||||
(demo / "test.esp32-idf.yaml").write_text("")
|
||||
(demo / "test.esp8266-ard.yaml").write_text("")
|
||||
(demo / "test-variant.esp32-idf.yaml").write_text("")
|
||||
(demo / "validate.esp32-idf.yaml").write_text("")
|
||||
(demo / "validate-legacy.esp32-idf.yaml").write_text("")
|
||||
|
||||
validate_only = tests_dir / "validate_only"
|
||||
validate_only.mkdir(parents=True)
|
||||
(validate_only / "validate.esp32-idf.yaml").write_text("")
|
||||
|
||||
no_tests = tests_dir / "no_tests"
|
||||
no_tests.mkdir(parents=True)
|
||||
(no_tests / "common.yaml").write_text("")
|
||||
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _names(paths: list[Path]) -> set[str]:
|
||||
return {p.name for p in paths}
|
||||
|
||||
|
||||
def test_get_component_test_files_default_excludes_validate(
|
||||
fake_component_tests: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""Default behaviour: only base test.*.yaml; no variants, no validate."""
|
||||
monkeypatch.setattr(helpers, "root_path", str(fake_component_tests))
|
||||
|
||||
files = helpers.get_component_test_files("demo")
|
||||
|
||||
assert _names(files) == {"test.esp32-idf.yaml", "test.esp8266-ard.yaml"}
|
||||
|
||||
|
||||
def test_get_component_test_files_all_variants_excludes_validate(
|
||||
fake_component_tests: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""all_variants=True picks up test variants but still skips validate."""
|
||||
monkeypatch.setattr(helpers, "root_path", str(fake_component_tests))
|
||||
|
||||
files = helpers.get_component_test_files("demo", all_variants=True)
|
||||
|
||||
assert _names(files) == {
|
||||
"test.esp32-idf.yaml",
|
||||
"test.esp8266-ard.yaml",
|
||||
"test-variant.esp32-idf.yaml",
|
||||
}
|
||||
|
||||
|
||||
def test_get_component_test_files_include_validate_base_only(
|
||||
fake_component_tests: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""include_validate=True with base-only adds validate.*.yaml only."""
|
||||
monkeypatch.setattr(helpers, "root_path", str(fake_component_tests))
|
||||
|
||||
files = helpers.get_component_test_files("demo", include_validate=True)
|
||||
|
||||
assert _names(files) == {
|
||||
"test.esp32-idf.yaml",
|
||||
"test.esp8266-ard.yaml",
|
||||
"validate.esp32-idf.yaml",
|
||||
}
|
||||
|
||||
|
||||
def test_get_component_test_files_include_validate_all_variants(
|
||||
fake_component_tests: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""include_validate=True with all_variants adds validate variants too."""
|
||||
monkeypatch.setattr(helpers, "root_path", str(fake_component_tests))
|
||||
|
||||
files = helpers.get_component_test_files(
|
||||
"demo", all_variants=True, include_validate=True
|
||||
)
|
||||
|
||||
assert _names(files) == {
|
||||
"test.esp32-idf.yaml",
|
||||
"test.esp8266-ard.yaml",
|
||||
"test-variant.esp32-idf.yaml",
|
||||
"validate.esp32-idf.yaml",
|
||||
"validate-legacy.esp32-idf.yaml",
|
||||
}
|
||||
|
||||
|
||||
def test_get_component_test_files_validate_only_component(
|
||||
fake_component_tests: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""A component with only validate files is invisible without the flag."""
|
||||
monkeypatch.setattr(helpers, "root_path", str(fake_component_tests))
|
||||
|
||||
assert helpers.get_component_test_files("validate_only") == []
|
||||
assert helpers.get_component_test_files("validate_only", all_variants=True) == []
|
||||
|
||||
files = helpers.get_component_test_files(
|
||||
"validate_only", all_variants=True, include_validate=True
|
||||
)
|
||||
assert _names(files) == {"validate.esp32-idf.yaml"}
|
||||
|
||||
|
||||
def test_get_component_test_files_missing_component(
|
||||
fake_component_tests: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""Unknown components return an empty list, regardless of flags."""
|
||||
monkeypatch.setattr(helpers, "root_path", str(fake_component_tests))
|
||||
|
||||
assert (
|
||||
helpers.get_component_test_files(
|
||||
"does_not_exist", all_variants=True, include_validate=True
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_get_component_test_files_component_without_tests(
|
||||
fake_component_tests: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""A component with only common.yaml and no test/validate files returns []."""
|
||||
monkeypatch.setattr(helpers, "root_path", str(fake_component_tests))
|
||||
|
||||
assert (
|
||||
helpers.get_component_test_files(
|
||||
"no_tests", all_variants=True, include_validate=True
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("filename", "expected"),
|
||||
[
|
||||
("validate.esp32-idf.yaml", True),
|
||||
("validate-legacy.esp32-idf.yaml", True),
|
||||
("validate.host.yaml", True),
|
||||
("test.esp32-idf.yaml", False),
|
||||
("test-variant.esp32-idf.yaml", False),
|
||||
("common.yaml", False),
|
||||
# Defensive: a hypothetical name starting with "validate" but not
|
||||
# following the grammar must not be classified as a validate file.
|
||||
("validatesomething.yaml", False),
|
||||
],
|
||||
)
|
||||
def test_is_validate_only_file(filename: str, expected: bool, tmp_path: Path) -> None:
|
||||
assert helpers.is_validate_only_file(tmp_path / filename) is expected
|
||||
|
||||
Reference in New Issue
Block a user