Merge branch 'pch-strict-ci' into host-pch

This commit is contained in:
J. Nick Koston
2026-08-27 09:13:55 -05:00
5 changed files with 89 additions and 28 deletions
+29 -4
View File
@@ -289,13 +289,15 @@ def test_discard_pch_raises_when_gch_survives(
pch.discard_pch(tmp_path)
def test_discard_pch_warns_when_only_sidecar_unlink_fails(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
def test_discard_pch_raises_when_sum_survives(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The .sum is ccache's pch identity; one that survives is as unsafe
as a surviving .gch."""
from pathlib import Path as _P
from esphome.core import EsphomeError
(tmp_path / "esphome_pch.h").write_text("")
(tmp_path / "esphome_pch.h.gch").write_bytes(b"gch")
(tmp_path / "esphome_pch.h.gch.sum").write_text("x")
@@ -307,5 +309,28 @@ def test_discard_pch_warns_when_only_sidecar_unlink_fails(
return real_unlink(self, missing_ok=missing_ok)
monkeypatch.setattr(_P, "unlink", failing_unlink)
with pytest.raises(EsphomeError, match="Could not discard"):
pch.discard_pch(tmp_path)
def test_discard_pch_warns_when_file_vanished_concurrently(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""An unlink error on a file that is nonetheless gone only warns."""
from pathlib import Path as _P
(tmp_path / "esphome_pch.h").write_text("")
(tmp_path / "esphome_pch.h.gch").write_bytes(b"gch")
real_unlink = _P.unlink
def racing_unlink(self, missing_ok=False):
if self.name.endswith(".sum"):
# Racer removed it, then our unlink errored
raise OSError("stale handle")
return real_unlink(self, missing_ok=missing_ok)
monkeypatch.setattr(_P, "unlink", racing_unlink)
pch.discard_pch(tmp_path)
assert "Could not discard the pch sidecars" in caplog.text
+31 -4
View File
@@ -295,15 +295,17 @@ def test_pch_script_spawn_failure_is_transient(
assert "did not run" in capsys.readouterr().out
def test_pch_script_probe_environment_failure_does_not_latch(
tmp_path: Path,
def test_pch_script_probe_baseline_failure_latches_with_honest_label(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Probe AND baseline failing is environmental: no marker, retry."""
"""Probe AND baseline failing is a deterministic environment problem:
latch, but blame the environment rather than the pch."""
scons_env = _run_script(tmp_path, probe_exit=1)
proj = tmp_path / "dev"
assert not (proj / "esphome_pch.h.gch").exists()
assert not (proj / "esphome_pch.h.gch.failed").exists()
assert (proj / "esphome_pch.h.gch.failed").is_file()
assert scons_env.prepended == []
assert "probe cannot run at all" in capsys.readouterr().out
def test_pch_script_unresolved_package_version_skips_pch(tmp_path: Path) -> None:
@@ -464,6 +466,27 @@ def test_pch_script_unreadable_local_header_skips_pch(
assert "skipping precompiled header" in capsys.readouterr().out
def test_pch_script_rejects_unrecognized_strict_value(tmp_path: Path) -> None:
"""A typo'd knob must fail the build, not silently disable the gate."""
with pytest.raises(RuntimeError, match="Unrecognized ESPHOME_PCH_STRICT"):
_run_script(tmp_path, env_vars={"ESPHOME_PCH_STRICT": "yolo"})
def test_pch_script_strict_tables_match_helpers(tmp_path: Path) -> None:
"""The script's mirrored spelling tables must not drift."""
from esphome.helpers import FALSY_ENV_STRINGS, TRUTHY_ENV_STRINGS
proj = tmp_path / "dev"
(proj / "src").mkdir(parents=True)
env = _FakeSConsEnv(proj, proj / "src", "g++", ["-DX=1"])
namespace = {"Import": lambda *_names: None, "env": env, "projenv": env}
with patch.dict(os.environ, {}, clear=True):
exec(compile(_SCRIPT.read_text(), "pch.py", "exec"), namespace) # noqa: S102
assert set(namespace["_TRUTHY"]) == set(TRUTHY_ENV_STRINGS)
# parse_enable_env handles the empty string separately
assert set(namespace["_FALSY"]) - {""} == set(FALSY_ENV_STRINGS)
def test_pch_script_strict_reprobes_cached_gch(tmp_path: Path) -> None:
"""Rejection is per-process: strict re-proves a cached .gch loads."""
_run_script(tmp_path)
@@ -491,8 +514,12 @@ def test_pch_script_strict_fails_without_scons(tmp_path: Path) -> None:
if "projenv" in names:
raise RuntimeError("Import of non-existent variable 'projenv'")
import sys
env = _FakeSConsEnv(proj, proj / "src", "g++", ["-DX=1"])
with (
# None forces ImportError even where SCons is installed
patch.dict(sys.modules, {"SCons.Script": None}),
patch.dict(os.environ, {"ESPHOME_PCH_STRICT": "1"}, clear=True),
pytest.raises(RuntimeError, match="not used"),
):