mirror of
https://github.com/esphome/esphome.git
synced 2026-08-31 10:06:03 +00:00
Harden the skip path against stale artifacts, tolerate corrupt linker scripts, guard the baseline degrade, mark undecoded addresses
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user