Harden the skip path against stale artifacts, tolerate corrupt linker scripts, guard the baseline degrade, mark undecoded addresses

This commit is contained in:
J. Nick Koston
2026-08-24 13:49:50 -05:00
parent 0556c212b2
commit 634dfe6f3c
5 changed files with 89 additions and 3 deletions
+13 -1
View File
@@ -151,10 +151,20 @@ def run_compile(config: ConfigType, verbose: bool) -> int:
get_factory_firmware_path(),
build_dir / "firmware.ota.bin",
)
ninja_mtime = ninja_file.stat().st_mtime
for artifact in build_dir_artifacts:
if not artifact.is_file():
_LOGGER.error("Build produced no %s", artifact)
return 1
if artifact.stat().st_mtime < ninja_mtime:
# A leftover from an older manifest must not pass as this
# build's output (a defective manifest with no default targets
# reports "no work to do" while building nothing)
_LOGGER.error(
"%s is older than build.ninja; run 'esphome clean-all' and retry",
artifact,
)
return 1
if not _print_size_summary(build_dir, paths):
# The cause was already warned; name the consequence so a build
@@ -214,7 +224,9 @@ def _parse_app_size(build_dir: Path, paths: framework.InstalledPaths) -> int | N
ld_path = get_flash_ld_path(build_dir, paths)
try:
ld_text = ld_path.read_text(encoding="utf-8")
except OSError as err:
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError: a truncated/corrupt script must degrade to
# the same warning, never abort an already-linked build
_LOGGER.warning("Cannot read linker script for the Flash summary: %s", err)
return None
app_size = segment_length(ld_text, "irom0_0_seg")
+4 -1
View File
@@ -664,10 +664,13 @@ def _decode_pc(config: ConfigType, addr: str) -> None:
try:
translation = subprocess.check_output(command, close_fds=False).decode().strip()
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
# Warn, not debug: a failing addr2line must be visible
# Warn, not debug: a failing addr2line must be visible. The warning
# is rate-limited across a dump, so mark every undecoded address
# inline or the rest read as merely unmappable
_warn_decode_problem(
"addr2line-failed", "Could not decode crash address %s (%s)", addr, err
)
_LOGGER.warning("Not decoded %s (addr2line failed)", addr)
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
return
+5
View File
@@ -1106,6 +1106,11 @@ def test_components(
# If no components found, build a reference configuration for baseline comparison
# Create a synthetic "empty" component test that will build just the base config
if fail_on_no_tests and not all_tests:
# The synthetic baseline would report success having built nothing
# the caller asked for (e.g. a wildcard that matched no component)
print(f"No components found matching: {component_patterns}")
return 1
if not all_tests:
print(f"No components found matching: {component_patterns}")
print(
@@ -285,6 +285,18 @@ def test_components_blank_list_fails_with_flag(
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, 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."""
+55 -1
View File
@@ -39,9 +39,11 @@ def _setup_core(tmp_path: Path) -> None:
CORE.build_path = tmp_path
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)}
# run_compile verifies the produced artifacts; give every test a build
# that "produced" them (tests for the guard delete them again)
# that "produced" them (tests for the guard delete them again). The
# manifest comes first: artifacts must not be older than build.ninja.
build_dir = CORE.relative_pioenvs_path("test8266")
build_dir.mkdir(parents=True, exist_ok=True)
(build_dir / "build.ninja").write_text("# manifest")
for artifact in (
"firmware.elf",
"firmware.bin",
@@ -51,6 +53,18 @@ def _setup_core(tmp_path: Path) -> None:
(build_dir / artifact).write_bytes(b"")
def _touch_artifacts(build_dir: Path) -> None:
"""Re-date the fixture artifacts after a test rewrites build.ninja, so
the freshness guard sees them as this manifest's outputs."""
for artifact in (
"firmware.elf",
"firmware.bin",
"firmware.factory.bin",
"firmware.ota.bin",
):
(build_dir / artifact).touch()
def _paths(tmp_path: Path) -> framework.InstalledPaths:
return framework.InstalledPaths(
framework=tmp_path / "framework",
@@ -150,6 +164,7 @@ def test_run_compile_regenerates_stale_compdb(tmp_path: Path) -> None:
build_dir = toolchain.get_build_dir()
build_dir.mkdir(parents=True, exist_ok=True)
(build_dir / "build.ninja").write_text("")
_touch_artifacts(build_dir)
compdb = build_dir / "compile_commands.json"
compdb.write_text("[]")
os.utime(compdb, ((build_dir / "build.ninja").stat().st_mtime - 5,) * 2)
@@ -422,6 +437,7 @@ def test_run_compile_skips_compdb_when_ninja_unchanged(tmp_path: Path) -> None:
build_dir.mkdir(parents=True, exist_ok=True)
# write_project (stubbed below) always leaves a build.ninja behind
(build_dir / "build.ninja").write_text("# manifest")
_touch_artifacts(build_dir)
def run(regenerate_expected: bool) -> None:
with (
@@ -571,6 +587,44 @@ def test_run_compile_skipped_size_summary_names_consequence(
assert "Firmware size summary unavailable for this build" in caplog.text
def test_run_compile_stale_artifact_fails(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""An artifact older than build.ninja is a leftover, not this build's
output; a defective manifest with no default targets must not pass on
it."""
build_dir = toolchain.get_build_dir()
os.utime(build_dir / "firmware.bin", (0, 0))
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project", return_value=False),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout="", stderr=""),
),
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary", return_value=True),
patch.object(toolchain, "get_idedata", return_value={}),
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 1
assert "older than build.ninja" in caplog.text
def test_parse_app_size_non_utf8_ld_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A corrupt (non-UTF-8) linker script degrades to the same warning as
an unreadable one, never a traceback after a successful link."""
paths = _paths(tmp_path)
ld = tmp_path / "corrupt.ld"
ld.write_bytes(b"\xff\xfe not utf8")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path, paths) is None
assert "Cannot read linker script" in caplog.text
def test_get_idedata_accepts_preresolved_ccache() -> None:
"""run_compile threads its resolved ccache through; the probe must not
run again."""