diff --git a/esphome/espidf/size_summary.py b/esphome/espidf/size_summary.py index 2be3634c69..572e2b0eff 100644 --- a/esphome/espidf/size_summary.py +++ b/esphome/espidf/size_summary.py @@ -73,15 +73,16 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None: """Print PlatformIO-shaped RAM and Flash one-liners. Failures are non-fatal: the build has already succeeded, we just couldn't - summarize. Logs the cause at debug level. + summarize. Logs the cause at warning level, so a missing RAM/Flash line + (which CI's memory-impact extraction greps for) is diagnosable. """ if not size_json.is_file(): - _LOGGER.debug("Skipping size summary: %s not found", size_json) + _LOGGER.warning("Skipping size summary: %s not found", size_json) return try: data = json.loads(size_json.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as e: - _LOGGER.debug("Skipping size summary: %s", e) + _LOGGER.warning("Skipping size summary: %s", e) return memory_types = data.get("memory_types", {}) @@ -90,6 +91,8 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None: ram_total = ram_region.get("size") if ram_total and ram_used is not None: print_size_line("RAM", ram_used, ram_total) + else: + _LOGGER.warning("Skipping RAM summary: no DRAM/DIRAM region in %s", size_json) image_size = data.get("image_size") if image_size is None or partitions_csv is None: @@ -97,6 +100,6 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None: try: app_size = _find_app_partition_size(partitions_csv) except ValueError as e: - _LOGGER.debug("Skipping Flash summary: %s", e) + _LOGGER.warning("Skipping Flash summary: %s", e) return print_size_line("Flash", image_size, app_size) diff --git a/esphome/platformio/extra_script.py b/esphome/platformio/extra_script.py index ef75421671..6b21100031 100644 --- a/esphome/platformio/extra_script.py +++ b/esphome/platformio/extra_script.py @@ -173,9 +173,9 @@ def run_extra_script( process CWD so relative-path lookups (``join``, ``realpath``, ``open``) resolve against the library tree. - On any exception inside the script we log at debug level and return - an empty result — extra-scripts are best-effort, and an unsupported - script shouldn't block the build. + On any exception inside the script we warn and return whatever the + script captured before failing — extra-scripts are best-effort, and an + unsupported script shouldn't block the build. """ env = _FakeSConsEnv( board_mcu=board_mcu, @@ -196,8 +196,15 @@ def run_extra_script( }, ) except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught - _LOGGER.warning("PIO extra-script %s raised %s; skipping", script_path, e) - return ExtraScriptResult() + # Keep what the script captured before failing: dropping flags it + # already appended would fail later at link time, far from the cause + _LOGGER.warning( + "PIO extra-script %s (in %s) raised %s; keeping the partial capture", + script_path, + library_dir.name, + e, + ) + return env.result finally: os.chdir(old_cwd) return env.result diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 22da9cfc87..e347e1a901 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -564,7 +564,9 @@ def split_flag_entry(entry: str, owner: str) -> list[str]: """``shlex.split`` with a clean error naming the offending flags entry.""" try: return shlex.split(entry) - except ValueError as err: + except (ValueError, AttributeError, TypeError) as err: + # AttributeError/TypeError: a dict or number from a third-party + # manifest; name the entry instead of an opaque shlex traceback raise EsphomeError(f"Malformed build flag {entry!r} in {owner}: {err}") from err diff --git a/tests/unit_tests/test_platformio_extra_script.py b/tests/unit_tests/test_platformio_extra_script.py index 1cc9413679..ec62b67f08 100644 --- a/tests/unit_tests/test_platformio_extra_script.py +++ b/tests/unit_tests/test_platformio_extra_script.py @@ -224,7 +224,7 @@ def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None: c.data = {"build": {"extraScript": "extra.py"}} apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266") assert "flags" not in c.data["build"] - assert "skipping" in caplog.text + assert "keeping the partial capture" in caplog.text def test_apply_extra_script_pio_platform(tmp_path) -> None: @@ -250,3 +250,16 @@ def test_apply_extra_script_missing_script_logged(tmp_path, caplog) -> None: c.data = {"build": {"extraScript": "nope.py"}} apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266") assert "not found" in caplog.text + + +def test_run_extra_script_keeps_partial_capture(tmp_path, caplog) -> None: + """Flags appended before a script fails are kept, not dropped.""" + from esphome.platformio.extra_script import run_extra_script + + script = tmp_path / "extra.py" + script.write_text("env.Append(LIBS=['algobsec'])\nraise RuntimeError('boom')\n") + result = run_extra_script( + script, library_dir=tmp_path, board_mcu="esp32", pio_platform="espressif32" + ) + assert result.libs == ["algobsec"] + assert "keeping the partial capture" in caplog.text diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index c8f994bb04..670f676902 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -564,3 +564,16 @@ def test_lex_build_flags_dangling_flag_does_not_cross_entries( assert lex_build_flags(["-Wall -I", "-DFOO=1"], "lib x") == ["-Wall", "-DFOO=1"] assert "Ignoring trailing '-I'" in caplog.text + + +def test_split_flag_entry_non_string_is_clean( # type: ignore[no-untyped-def] +) -> None: + """A dict or number from a third-party manifest fails naming the entry, + not with an opaque shlex traceback.""" + from esphome.core import EsphomeError + from esphome.platformio.library import split_flag_entry + + with pytest.raises(EsphomeError, match="Malformed build flag"): + split_flag_entry({"esp32": ["-DX"]}, "lib x") + with pytest.raises(EsphomeError, match="Malformed build flag 5"): + split_flag_entry(5, "lib x") diff --git a/tests/unit_tests/test_size_summary.py b/tests/unit_tests/test_size_summary.py index 816e65be18..dc937147ad 100644 --- a/tests/unit_tests/test_size_summary.py +++ b/tests/unit_tests/test_size_summary.py @@ -149,3 +149,32 @@ def test_print_summary_flash_line( out = capsys.readouterr().out assert "RAM: [===== ] 50.0% (used 100 bytes from 200 bytes)" in out assert "Flash: [ ] 0.0% (used 500 bytes from 1048576 bytes)" in out + + +def test_print_summary_missing_ram_region_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A missing RAM line is diagnosable, not a silently absent CI metric.""" + size_json = _write_size_json(tmp_path, {"memory_types": {}, "image_size": 100}) + print_summary(size_json, partitions_csv=None) + assert "Skipping RAM summary" in caplog.text + + +def test_print_summary_bad_partitions_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """An unparseable partition table skips the Flash line with a warning.""" + size_json = _write_size_json(tmp_path, _esp32_size_data()) + partitions = tmp_path / "partitions.csv" + partitions.write_text("not,a,valid,partition,table\n") + print_summary(size_json, partitions_csv=partitions) + assert "Skipping Flash summary" in caplog.text + + +def test_print_summary_corrupt_json_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + size_json = tmp_path / "size.json" + size_json.write_text("{not json") + print_summary(size_json, partitions_csv=None) + assert "Skipping size summary" in caplog.text