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

This commit is contained in:
J. Nick Koston
2026-08-27 08:32:13 -05:00
8 changed files with 195 additions and 53 deletions
+82 -2
View File
@@ -1240,8 +1240,11 @@ def test_prepare_pch_probe_rejection_latches_and_degrades(
def rejecting(cmd, **kwargs):
if "-fsyntax-only" in cmd:
if "-include" not in cmd:
# Baseline without the pch passes: the pch is to blame
return subprocess.CompletedProcess(cmd, 0, "", "")
return subprocess.CompletedProcess(
cmd, 0, "", "warning: esphome_pch.h.gch: had text segment "
cmd, 1, "", "error: esphome_pch.h.gch: had text segment "
)
gch.write_bytes(b"gch")
return subprocess.CompletedProcess(cmd, 0, "", "")
@@ -1289,8 +1292,10 @@ def test_prepare_pch_strict_reprobes_cached_gch(
def reject(cmd, **kwargs):
assert "-fsyntax-only" in cmd, "cached path must not recompile"
if "-include" not in cmd:
return subprocess.CompletedProcess(cmd, 0, "", "")
return subprocess.CompletedProcess(
cmd, 0, "", "warning: esphome_pch.h.gch: had text segment "
cmd, 1, "", "error: esphome_pch.h.gch: had text segment "
)
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
@@ -1304,3 +1309,78 @@ def test_prepare_pch_strict_reprobes_cached_gch(
# Per-process rejection may not reproduce: the cached path must not
# latch the pch off for later non-strict builds
assert not (dev / "build" / "esphome_pch.h.gch.failed").exists()
def test_prepare_pch_unexpected_command_shape_degrades(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A tail the probe slice cannot trust discards and degrades."""
from esphome.build_gen.espidf import prepare_pch
from esphome.core import EsphomeError
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
dev = _make_pch_device(tmp_path, "dev_sh")
CORE.build_path = dev
gch = dev / "build" / "esphome_pch.h.gch"
def ok(cmd, **kwargs):
gch.write_bytes(b"gch")
return subprocess.CompletedProcess(cmd, 0, "", "")
with (
patch.object(CORE, "name", "test"),
patch("esphome.build_helpers.pch.subprocess.run", side_effect=ok),
patch(
"esphome.build_helpers.pch.pch_compile_command",
return_value=(
["g++", "-DX=1", "-c", "x", "-o", "y", "extra"],
dev / "build",
),
),
pytest.raises(EsphomeError, match="command shape"),
):
prepare_pch()
assert not gch.exists()
# Non-strict: same shape problem degrades without raising
monkeypatch.delenv("ESPHOME_PCH_STRICT")
with (
patch.object(CORE, "name", "test"),
patch("esphome.build_helpers.pch.subprocess.run", side_effect=ok),
patch(
"esphome.build_helpers.pch.pch_compile_command",
return_value=(
["g++", "-DX=1", "-c", "x", "-o", "y", "extra"],
dev / "build",
),
),
):
prepare_pch()
assert not gch.exists()
def test_prepare_pch_probe_baseline_spawn_failure_is_transient(
tmp_path: Path,
) -> None:
"""A baseline that cannot spawn is environmental: no marker."""
from esphome.build_gen.espidf import prepare_pch
dev = _make_pch_device(tmp_path, "dev_bs")
CORE.build_path = dev
gch = dev / "build" / "esphome_pch.h.gch"
def flaky(cmd, **kwargs):
if "-fsyntax-only" in cmd:
if "-include" in cmd:
return subprocess.CompletedProcess(cmd, 1, "", "boom")
raise OSError("baseline spawn failed")
gch.write_bytes(b"gch")
return subprocess.CompletedProcess(cmd, 0, "", "")
with (
patch.object(CORE, "name", "test"),
patch("esphome.build_helpers.pch.subprocess.run", side_effect=flaky),
):
prepare_pch()
assert not gch.exists()
assert not (dev / "build" / "esphome_pch.h.gch.failed").exists()
@@ -253,3 +253,14 @@ def test_pch_extra_scripts_strict_raises_when_disabled(
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
with pytest.raises(EsphomeError, match="disabled"):
pch.pch_extra_scripts()
def test_pch_strict_rejects_unrecognized_values(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A typo must not silently disable the gate."""
from esphome.core import EsphomeError
monkeypatch.setenv("ESPHOME_PCH_STRICT", "yolo")
with pytest.raises(EsphomeError, match="Unrecognized ESPHOME_PCH_STRICT"):
pch.pch_strict()
@@ -100,12 +100,14 @@ def _fake_cxx(
body += f"echo {fail_msg or 'boom'} >&2\nexit 1\n"
else:
# Only the c++-header compile has a -o; the load probe has none
body += 'out=""; prev=""; mf=0; dep=0\nfor a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; [ "$a" = "-MF" ] && mf=1; case "$a" in -M|-MM|-MD|-MMD) dep=1;; esac; done\n'
body += 'out=""; prev=""; mf=0; dep=0; inc=0\nfor a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; [ "$a" = "-MF" ] && mf=1; [ "$a" = "-include" ] && inc=1; case "$a" in -M|-MM|-MD|-MMD) dep=1;; esac; done\n'
# Real cc1plus rejects -MF without a dependency flag
body += 'if [ "$mf" = 1 ] && [ "$dep" = 0 ]; then echo "cc1plus: error: to generate dependencies you must specify either \x27-M\x27 or \x27-MM\x27" >&2; exit 1; fi\n'
body += '[ -n "$out" ] && echo gch > "$out"\n'
if reject_pch:
body += 'case " $* " in *c++-header*) ;; *) echo "warning: esphome_pch.h.gch: had text segment at different address" >&2;; esac\n'
# -Werror=invalid-pch makes rejection a nonzero exit; the
# baseline (no -include) still passes
body += 'case " $* " in *c++-header*) ;; *) if [ "$inc" = 1 ]; then echo "error: esphome_pch.h.gch: had text segment at different address" >&2; exit 1; fi;; esac\n'
body += f'case " $* " in *c++-header*) exit 0;; *) exit {probe_exit};; esac\n'
cxx.write_text("#!/bin/sh\n" + body)
cxx.chmod(cxx.stat().st_mode | stat.S_IEXEC)
@@ -293,12 +295,14 @@ def test_pch_script_spawn_failure_is_transient(
assert "did not run" in capsys.readouterr().out
def test_pch_script_probe_nonzero_exit_falls_back(tmp_path: Path) -> None:
"""A probe failure whose stderr never mentions .gch must still count."""
def test_pch_script_probe_environment_failure_does_not_latch(
tmp_path: Path,
) -> None:
"""Probe AND baseline failing is environmental: no marker, retry."""
scons_env = _run_script(tmp_path, probe_exit=1)
proj = tmp_path / "dev"
assert not (proj / "esphome_pch.h.gch").exists()
assert (proj / "esphome_pch.h.gch.failed").is_file()
assert not (proj / "esphome_pch.h.gch.failed").exists()
assert scons_env.prepended == []