Files
esphome/tests/script/test_test_build_components.py
T
J. Nick Koston b5b6d0bacd
CI / Create common environment (push) Canceled after 0s
CI / Seed apt package cache (push) Canceled after 0s
CI / Determine which jobs to run (push) Canceled after 0s
CI / Seed the esp8266 native toolchain cache (push) Canceled after 0s
CI / Run script/ci-custom (push) Canceled after 0s
CI / Check pylint (push) Canceled after 0s
CI / Check lint and formatting (push) Canceled after 0s
CI / Run pytest (macOS-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (macOS-latest, 3.14) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.13) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.14) (push) Canceled after 0s
CI / Run pytest (windows-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (windows-latest, 3.14) (push) Canceled after 0s
CI / Report no coverage to Codecov (push) Canceled after 0s
CI / Run integration tests () (push) Canceled after 0s
CI / Check import esphome.__main__ time (push) Canceled after 0s
CI / Run CodSpeed benchmarks (push) Canceled after 0s
CI / Run C++ unit tests (push) Canceled after 0s
CI / Run script/clang-tidy for LibreTiny (push) Canceled after 0s
CI / Run script/clang-tidy for ESP8266 (push) Canceled after 0s
CI / Run script/clang-tidy for RP2 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 Arduino (push) Canceled after 0s
CI / Run script/clang-tidy for ZEPHYR (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 1/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 2/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 3/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 C6 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 P4 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 S3 (push) Canceled after 0s
CI / Test components batch () (push) Canceled after 0s
CI / Test esp32 components with PlatformIO (push) Canceled after 0s
CI / Test esp8266 components with the native toolchain (push) Canceled after 0s
CI / Test downstream esphome/device-builder (push) Canceled after 0s
CI / Build target branch for memory impact (push) Canceled after 0s
CI / Build PR branch for memory impact (push) Canceled after 0s
CI / Comment memory impact (push) Canceled after 0s
CI / CI Status (push) Canceled after 0s
[ci] Add the native ESP8266 compile smoke test (#19714)
2026-09-26 21:00:48 +01:00

334 lines
11 KiB
Python

"""Unit tests for script/test_build_components.py logging helpers."""
from pathlib import Path
import sys
import pytest
# Add the script directory to the path so we can import the module under test.
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "script"))
import test_build_components as tbc # noqa: E402
class _FakeCompleted:
"""Minimal stand-in for subprocess.CompletedProcess."""
def __init__(self, returncode: int) -> None:
self.returncode = returncode
@pytest.fixture
def _no_ci(monkeypatch: pytest.MonkeyPatch) -> None:
"""Ensure GITHUB_ACTIONS is unset so group markers are suppressed."""
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
@pytest.fixture
def _ci(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pretend we are running inside GitHub Actions."""
monkeypatch.setenv("GITHUB_ACTIONS", "true")
def test_start_log_group_outside_ci_is_silent(
_no_ci: None, capsys: pytest.CaptureFixture[str]
) -> None:
tbc.start_log_group("hello")
assert capsys.readouterr().out == ""
def test_end_log_group_outside_ci_is_silent(
_no_ci: None, capsys: pytest.CaptureFixture[str]
) -> None:
tbc.end_log_group()
assert capsys.readouterr().out == ""
def test_start_log_group_in_ci_emits_marker(
_ci: None, capsys: pytest.CaptureFixture[str]
) -> None:
tbc.start_log_group("hello")
assert capsys.readouterr().out == "::group::hello\n"
def test_end_log_group_in_ci_emits_marker(
_ci: None, capsys: pytest.CaptureFixture[str]
) -> None:
tbc.end_log_group()
assert capsys.readouterr().out == "::endgroup::\n"
def _make_base_file(tmp_path: Path) -> Path:
base_file = tmp_path / "base.yaml"
base_file.write_text("esphome:\n name: $component_test_file\n")
return base_file
def test_run_esphome_test_wraps_output_in_group(
_ci: None,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""A passing single-component test is bracketed by group markers."""
monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(0))
repo_root = Path(tbc.__file__).parent.parent
test_file = repo_root / "tests" / "components" / "foo" / "test.esp32-idf.yaml"
result = tbc.run_esphome_test(
component="foo",
test_file=test_file,
platform="esp32-idf",
platform_with_version="esp32-idf",
base_file=_make_base_file(tmp_path),
build_dir=tmp_path,
esphome_command="config",
continue_on_fail=True,
)
out = capsys.readouterr().out
assert result.success is True
assert "::group::[foo] [test] [esp32-idf]" in out
assert "::endgroup::" in out
# The header line is printed inside the group.
assert out.index("::group::") < out.index("> [foo]") < out.index("::endgroup::")
def test_run_esphome_test_closes_group_before_failure_report(
_ci: None,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""On a fail-fast failure the group closes before the reproduce report."""
monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(1))
repo_root = Path(tbc.__file__).parent.parent
test_file = repo_root / "tests" / "components" / "foo" / "test.esp32-idf.yaml"
# continue_on_fail=False makes the failure raise after printing the
# reproduce block, which is the path that must stay outside the group.
with pytest.raises(tbc.subprocess.CalledProcessError):
tbc.run_esphome_test(
component="foo",
test_file=test_file,
platform="esp32-idf",
platform_with_version="esp32-idf",
base_file=_make_base_file(tmp_path),
build_dir=tmp_path,
esphome_command="config",
continue_on_fail=False,
)
out = capsys.readouterr().out
assert "::endgroup::" in out
assert "FAILED - Command to reproduce:" in out
# The group must be closed before the failure report is printed.
assert out.index("::endgroup::") < out.index("FAILED - Command to reproduce:")
def test_run_esphome_test_closes_group_when_subprocess_raises(
_ci: None,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""If the subprocess raises, the group is still closed (via finally)."""
def _boom(*a: object, **k: object) -> None:
raise OSError("boom")
monkeypatch.setattr(tbc.subprocess, "run", _boom)
repo_root = Path(tbc.__file__).parent.parent
test_file = repo_root / "tests" / "components" / "foo" / "test.esp32-idf.yaml"
with pytest.raises(OSError, match="boom"):
tbc.run_esphome_test(
component="foo",
test_file=test_file,
platform="esp32-idf",
platform_with_version="esp32-idf",
base_file=_make_base_file(tmp_path),
build_dir=tmp_path,
esphome_command="config",
continue_on_fail=True,
)
assert "::endgroup::" in capsys.readouterr().out
def test_run_grouped_test_wraps_output_in_group(
_ci: None,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""A grouped test is bracketed by group markers listing its components."""
monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(0))
monkeypatch.setattr(tbc, "merge_component_configs", lambda **k: None)
result = tbc.run_grouped_test(
components=["foo", "bar"],
platform="esp32-idf",
platform_with_version="esp32-idf",
base_file=_make_base_file(tmp_path),
build_dir=tmp_path,
tests_dir=tmp_path,
esphome_command="config",
continue_on_fail=True,
)
out = capsys.readouterr().out
assert result.success is True
assert "::group::[GROUPED: foo, bar] [esp32-idf]" in out
assert out.index("::group::") < out.index("> [GROUPED") < out.index("::endgroup::")
def test_run_grouped_test_closes_group_before_failure_report(
_ci: None,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""A fail-fast grouped failure closes the group before the report."""
monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(1))
monkeypatch.setattr(tbc, "merge_component_configs", lambda **k: None)
with pytest.raises(tbc.subprocess.CalledProcessError):
tbc.run_grouped_test(
components=["foo", "bar"],
platform="esp32-idf",
platform_with_version="esp32-idf",
base_file=_make_base_file(tmp_path),
build_dir=tmp_path,
tests_dir=tmp_path,
esphome_command="config",
continue_on_fail=False,
)
out = capsys.readouterr().out
assert out.index("::endgroup::") < out.index("FAILED - Command to reproduce:")
def test_run_grouped_test_closes_group_when_subprocess_raises(
_ci: None,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""If the grouped subprocess raises, the group is still closed (finally)."""
def _boom(*a: object, **k: object) -> None:
raise OSError("boom")
monkeypatch.setattr(tbc.subprocess, "run", _boom)
monkeypatch.setattr(tbc, "merge_component_configs", lambda **k: None)
with pytest.raises(OSError, match="boom"):
tbc.run_grouped_test(
components=["foo", "bar"],
platform="esp32-idf",
platform_with_version="esp32-idf",
base_file=_make_base_file(tmp_path),
build_dir=tmp_path,
tests_dir=tmp_path,
esphome_command="config",
continue_on_fail=True,
)
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