From f30802757ee0c896ab3046fb759e6ed6fd62b9af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Aug 2026 17:06:59 -0500 Subject: [PATCH] Stamp the compile-DB freshness, defer the silent-component failure past the summary, mark every undecoded address, honor all-platform fixtures --- esphome/arduino8266/toolchain.py | 8 +++++++- esphome/build_helpers/idedata.py | 4 ++-- esphome/components/esp8266/__init__.py | 19 +++++++++++++------ script/determine-jobs.py | 3 +++ script/test_build_components.py | 19 ++++++++++++------- tests/unit_tests/test_main.py | 3 ++- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/esphome/arduino8266/toolchain.py b/esphome/arduino8266/toolchain.py index d73e9aad4e..dec3e7caf7 100644 --- a/esphome/arduino8266/toolchain.py +++ b/esphome/arduino8266/toolchain.py @@ -97,13 +97,19 @@ def run_compile(config: ConfigType, verbose: bool) -> int: # (an interrupted previous run may have rewritten the manifest without # regenerating the DB). compdb = build_dir / "compile_commands.json" + compdb_stamp = build_dir / ".compile_commands.stamp" ninja_file = build_dir / "build.ninja" + # Freshness rides a stamp: the DB itself is written through + # write_file_if_changed (its mtime feeds get_idedata's cache), so a + # regeneration with identical content would stay "stale" forever if ( ninja_changed or not compdb.is_file() - or compdb.stat().st_mtime < ninja_file.stat().st_mtime + or not compdb_stamp.is_file() + or compdb_stamp.stat().st_mtime < ninja_file.stat().st_mtime ): _write_compile_commands(paths.ninja, build_dir, env) + compdb_stamp.touch() cmd = [str(paths.ninja)] if verbose: diff --git a/esphome/build_helpers/idedata.py b/esphome/build_helpers/idedata.py index 0ede397c55..4b57b5e983 100644 --- a/esphome/build_helpers/idedata.py +++ b/esphome/build_helpers/idedata.py @@ -22,6 +22,8 @@ import subprocess from esphome.core import EsphomeError from esphome.helpers import write_file +_LOGGER = logging.getLogger(__name__) + # Everything idedata generation may raise after a successful link; idedata # is a bonus artifact, so consumers warn instead of failing the build IDEDATA_BEST_EFFORT_ERRORS = ( @@ -57,8 +59,6 @@ def warn_if_idedata_missing(get_idedata: Callable[[], dict | None]) -> None: _LOGGER.warning("Idedata failure detail", exc_info=True) -_LOGGER = logging.getLogger(__name__) - # C++ translation-unit suffixes used to identify ESPHome source files. _CXX_SUFFIXES = (".cpp", ".cc") # Suffixes of input/output files that appear bare on the command line (and so diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index b5852abc46..97b9ad8acd 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -627,15 +627,17 @@ ESP8266_EXCEPTION_CODES = { _DECODE_WARNED_AT: dict[str, float] = {} -def _warn_decode_problem(key: str, message: str, *args) -> None: +def _warn_decode_problem(key: str, message: str, *args) -> bool: """Warn, deduplicated briefly so a burst of stack-dump addresses warns - once but a later dump warns again.""" + once but a later dump warns again; returns whether it warned so the + caller can mark suppressed addresses individually.""" now = time.monotonic() last = _DECODE_WARNED_AT.get(key) if last is not None and now - last < 30: - return + return False _DECODE_WARNED_AT[key] = now _LOGGER.warning(message, *args) + return True def _decode_pc(config: ConfigType, addr: str) -> None: @@ -647,6 +649,8 @@ def _decode_pc(config: ConfigType, addr: str) -> None: _warn_decode_problem( str(path), "Cannot decode crash addresses: %s missing", path ) + # The detailed warning names no address, so mark each one + _LOGGER.warning("Not decoded %s (toolchain file missing)", addr) return addr2line, elf = str(addr2line), str(elf) else: @@ -658,6 +662,7 @@ def _decode_pc(config: ConfigType, addr: str) -> None: "no-addr2line", "Cannot decode crash addresses: no addr2line or ELF in idedata", ) + _LOGGER.warning("Not decoded %s (no addr2line or ELF)", addr) return addr2line, elf = idedata.addr2line_path, idedata.firmware_elf_path command = [addr2line, "-pfiaC", "-e", elf, addr] @@ -667,10 +672,12 @@ def _decode_pc(config: ConfigType, addr: str) -> None: # 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( + if not _warn_decode_problem( "addr2line-failed", "Could not decode crash address %s (%s)", addr, err - ) - _LOGGER.warning("Not decoded %s (addr2line failed)", addr) + ): + # The detailed warning already named this address; mark only + # the addresses whose warning was rate-limited away + _LOGGER.warning("Not decoded %s (addr2line failed)", addr) _LOGGER.debug("Caught exception for command %s", command, exc_info=1) return diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 7b6ffd9412..41fdff265a 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -692,6 +692,9 @@ def _esp8266_native_path_or_file_trigger(files: list[str]) -> bool: # base_python_changed covers the top-level esphome/*.py modules the # native backend imports directly (framework_helpers, helpers, writer, # __main__); without it a change there would silently skip this job. + # base_python_changed is deliberately broad (any top-level esphome/*.py) + # as belt-and-braces while the backend is new; narrow it to the modules + # the backend imports once the toolchain has soaked a few releases return base_python_changed(files) or _path_or_file_trigger( files, ESP8266_NATIVE_TRIGGER_FILES, ESP8266_NATIVE_TRIGGER_PATH_PREFIXES ) diff --git a/script/test_build_components.py b/script/test_build_components.py index 06a2ea09f5..28026d5775 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -1086,8 +1086,11 @@ def test_components( # check is per platform: a component whose fixture exists only for other # platforms contributes nothing to this leg. def _has_platform_test(component: str) -> bool: + # "all" fixtures build on every platform (mirrors the run loop) return any( - not platform_filter or test.stem.split(".")[-1].startswith(platform_filter) + not platform_filter + or (suffix := test.stem.split(".")[-1]) == "all" + or suffix.startswith(platform_filter) for test in all_tests.get(component, []) ) @@ -1214,19 +1217,21 @@ def test_components( toolchain=toolchain, ) + silent: list[str] = [] if fail_on_no_tests: # A green run that built nothing for a requested component (renamed # fixture, missing base file, version-suffix mismatch) must not pass # CI. Per component: an all-or-nothing check would let one silent # component hide behind the others. Opt-in: some legs (the esp32-ard - # smoke subset) legitimately match nothing. + # smoke subset) legitimately match nothing. Failing is deferred past + # the summary so a real failure's reproduce commands still print. built = {c for r in test_results for c in r.components} - if silent := [ + silent = [ p for p in component_patterns if p and "*" not in p and p not in built - ]: + ] + if silent: print(f"No tests ran for requested component(s): {', '.join(silent)}") - return 1 - if not test_results: + elif not test_results: print("No tests matched the requested components/platform") return 1 @@ -1261,7 +1266,7 @@ def test_components( if os.environ.get("GITHUB_STEP_SUMMARY"): write_github_summary(test_results, toolchain=toolchain) - if failed_results: + if failed_results or silent: return 1 return 0 diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index e190fb3283..c660feca0e 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -7125,7 +7125,8 @@ def test_upload_using_esptool_arduino_toolchain( mock_run_external_command_main: Mock, ) -> None: """The native ESP8266 Arduino toolchain flashes its factory image at - 0x0, dispatched through the platform's native_toolchain_module hook.""" + 0x0, resolved from the toolchain-keyed backend table (deliberately not + the platform hook: that import would break the upload fast path).""" setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test") CORE.toolchain = Toolchain.ARDUINO from esphome.arduino8266 import toolchain as native