From 985a784f5edd9a365246f413c6bae3c5bf28c02f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 20:53:55 -0500 Subject: [PATCH] Point analyze-memory at the platformio escape hatch, test the backend-table invariant instead of asserting at import, fail blank component lists under the flag, clean the arduino idedata cache --- esphome/__main__.py | 5 +---- esphome/writer.py | 14 ++++++++------ script/test_build_components.py | 7 +++++++ tests/script/test_test_build_components.py | 12 ++++++++++++ tests/unit_tests/test_main.py | 9 +++++++++ tests/unit_tests/test_writer.py | 6 +++++- 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c4f4b1d4e6..4f035e2ce3 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -48,7 +48,6 @@ from esphome.const import ( ENV_NOGITIGNORE, KEY_ESP32, KEY_VARIANT, - NATIVE_TOOLCHAINS, SECRETS_FILES, Toolchain, ) @@ -1932,8 +1931,6 @@ _NATIVE_TOOLCHAIN_MODULES = { ("esp32", Toolchain.ESP_IDF): "esphome.espidf.toolchain", ("esp8266", Toolchain.ARDUINO): "esphome.arduino8266.toolchain", } -# Structure over prose: a native toolchain the table does not serve is a bug -assert {tc for _, tc in _NATIVE_TOOLCHAIN_MODULES} == set(NATIVE_TOOLCHAINS) def _native_toolchain_module(): @@ -2002,7 +1999,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: if native_toolchain is None and not CORE.using_toolchain_platformio: _LOGGER.error( "analyze-memory is not supported with the '%s' toolchain on %s; " - "it requires a PlatformIO, ESP-IDF, or native Arduino build", + "re-run with --toolchain platformio", CORE.toolchain.value if CORE.toolchain else "unresolved", CORE.target_platform, ) diff --git a/esphome/writer.py b/esphome/writer.py index 0b9e7669ef..37114f84e5 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -610,12 +610,14 @@ def clean_build(clear_pio_cache: bool = True, *, full: bool = False): _LOGGER.info("Deleting %s", idf_path) rmtree(idf_path) - # The idedata cache is derived from the build but lives under the data dir, - # not the build path, so it must be removed separately in both modes. - idedata_cache = CORE.relative_internal_path("idedata", f"{CORE.name}.json") - if idedata_cache.is_file(): - _LOGGER.info("Deleting %s", idedata_cache) - idedata_cache.unlink() + # The idedata caches are derived from the build but live under the data + # dir, not the build path, so they must be removed separately in both + # modes (the .arduino.json variant is the native esp8266 toolchain's). + for cache_name in (f"{CORE.name}.json", f"{CORE.name}.arduino.json"): + idedata_cache = CORE.relative_internal_path("idedata", cache_name) + if idedata_cache.is_file(): + _LOGGER.info("Deleting %s", idedata_cache) + idedata_cache.unlink() if not clear_pio_cache: return diff --git a/script/test_build_components.py b/script/test_build_components.py index c5b2edc978..26c49cf60f 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -1074,6 +1074,13 @@ def test_components( ) ) + # The flag's contract is "no test matched fails": a fully blank pattern + # list would otherwise slide into the reference-baseline fallback and + # exit green while building nothing a caller asked for + if fail_on_no_tests and not any(component_patterns): + print("No components requested (blank component list)") + return 1 + # Renamed or removed fixtures must shrink coverage loudly, and the # reference-baseline fallback below must not mask an empty match. The # check is per platform: a component whose fixture exists only for other diff --git a/tests/script/test_test_build_components.py b/tests/script/test_test_build_components.py index b005ffe8c0..a293ebcb16 100644 --- a/tests/script/test_test_build_components.py +++ b/tests/script/test_test_build_components.py @@ -273,6 +273,18 @@ def test_components_component_with_no_base_file_fails_with_flag( ) +def test_components_blank_list_fails_with_flag( + capsys: pytest.CaptureFixture[str], +) -> None: + """A fully blank component list must not slide into the baseline + fallback and exit green under the flag.""" + rc = tbc.test_components( + [""], "esp8266-ard", "compile", False, fail_on_no_tests=True + ) + assert rc == 1 + assert "blank component list" 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.""" diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 07698d68cf..e190fb3283 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -7390,6 +7390,15 @@ def test_compile_program_espidf_idedata_none_warns( assert "No idedata was generated" in caplog.text +def test_native_toolchain_table_serves_every_native_toolchain() -> None: + """Every member of NATIVE_TOOLCHAINS has a backend entry; a gap would + surface as a targeted EsphomeError on the one affected config, and this + pin keeps the table from drifting when a toolchain is added.""" + from esphome.const import NATIVE_TOOLCHAINS + + assert {tc for _, tc in main._NATIVE_TOOLCHAIN_MODULES} == set(NATIVE_TOOLCHAINS) + + def test_native_toolchain_module_missing_backend_raises(tmp_path: Path) -> None: """A native toolchain missing from the backend table is a bug and must fail, not silently degrade to the PlatformIO path.""" diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 0e2941a1b2..22882824f7 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -517,10 +517,13 @@ def test_clean_build( dependencies_lock = tmp_path / "dependencies.lock" dependencies_lock.write_text("lock file") - # idedata cache lives under the data dir, not the build path. + # idedata caches live under the data dir, not the build path; the + # .arduino.json variant is the native esp8266 toolchain's. idedata_cache = tmp_path / "idedata" / "test.json" idedata_cache.parent.mkdir() idedata_cache.write_text("{}") + arduino_idedata_cache = tmp_path / "idedata" / "test.arduino.json" + arduino_idedata_cache.write_text("{}") # Native ESP-IDF toolchain artifacts. idf_build_dir = tmp_path / "build" @@ -581,6 +584,7 @@ def test_clean_build( assert not piolibdeps_dir.exists() assert not dependencies_lock.exists() assert not idedata_cache.exists() + assert not arduino_idedata_cache.exists() assert not idf_build_dir.exists() assert not managed_components_dir.exists() assert not pio_components_dir.exists()