From b718ca079fb5bda86ab99098c158d87cea40fc4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 16:17:05 -0500 Subject: [PATCH] Name the unreadable report's file, tighten the number and region checks The corrupt-report arm names its file with wording distinct from the backstop, so tests can tell them apart (and now do, plus the non-UTF-8 half). _is_number rejects bool and non-finite floats, and the RAM region selects by key presence, so a falsy-but-present DRAM value is malformed rather than absent -- the size report is the one input nothing upstream validates, so these stay hardened. The not-app_size comment carries the zero case it also swallows. --- esphome/espidf/size_summary.py | 22 ++++++++++++++++++---- tests/unit_tests/test_size_summary.py | 11 +++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/espidf/size_summary.py b/esphome/espidf/size_summary.py index d6006a188c..6a34505dc2 100644 --- a/esphome/espidf/size_summary.py +++ b/esphome/espidf/size_summary.py @@ -26,6 +26,7 @@ from __future__ import annotations import csv import json import logging +import math from pathlib import Path _LOGGER = logging.getLogger(__name__) @@ -104,7 +105,7 @@ def _print_summary(size_json: Path, partitions_csv: Path | None) -> None: data = json.loads(size_json.read_text(encoding="utf-8")) except (OSError, ValueError) as e: # ValueError covers JSONDecodeError and a non-UTF-8 (truncated) file - _LOGGER.warning("Skipping size summary: %s", e) + _LOGGER.warning("Skipping size summary: cannot read %s: %s", size_json, e) return if not isinstance(data, dict): # Non-object JSON has no .get @@ -127,13 +128,25 @@ def _present_but_not_dict(value: object) -> bool: def _is_number(value: object) -> bool: - return isinstance(value, (int, float)) + # bool subclasses int; NaN/Infinity are valid JSON for json.loads + return ( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and math.isfinite(value) + ) def _ram_line(data: dict, size_json: Path) -> str | None: """The formatted RAM line, or None (already logged) to skip it.""" memory_types = data.get("memory_types") - ram_region = _dict_get(memory_types, "DRAM") or _dict_get(memory_types, "DIRAM") + ram_region = None + if isinstance(memory_types, dict): + # Key presence, not truthiness: a falsy DRAM value is corrupt, not + # absent, and must not fall through to DIRAM + for key in ("DRAM", "DIRAM"): + if key in memory_types: + ram_region = memory_types[key] + break used = _dict_get(ram_region, "used") total = _dict_get(ram_region, "size") if _is_number(used) and _is_number(total) and total > 0: @@ -170,7 +183,8 @@ def _flash_line(data: dict, size_json: Path, partitions_csv: Path | None) -> str return None app_size = _find_app_partition_size(partitions_csv) if not app_size: - # No table or no qualifying row: legitimate for non-app layouts + # No qualifying row (a zero-size row has nothing to report either): + # legitimate for non-app layouts _LOGGER.debug("Skipping Flash summary: no app partition in %s", partitions_csv) return None return f"Flash: {_format_bar(int(image_size), app_size)}" diff --git a/tests/unit_tests/test_size_summary.py b/tests/unit_tests/test_size_summary.py index 23365704b7..7617a80a2d 100644 --- a/tests/unit_tests/test_size_summary.py +++ b/tests/unit_tests/test_size_summary.py @@ -133,7 +133,8 @@ def test_print_summary_handles_missing_json( """Missing size json is non-fatal and prints nothing.""" print_summary(tmp_path / "does_not_exist.json", partitions_csv=None) assert capsys.readouterr().out == "" - assert "Skipping size summary" in caplog.text + assert "cannot read" in caplog.text + assert "Skipping size summary for" not in caplog.text def test_print_summary_handles_no_memory_types( @@ -196,6 +197,8 @@ def test_print_summary_happy_path_prints_both_bars( {"memory_types": []}, {"memory_types": {"DRAM": 5}}, {"memory_types": {"DRAM": {"used": "x", "size": "y"}}, "image_size": 1}, + {"memory_types": {"DRAM": []}}, + {"memory_types": {"DRAM": {"used": True, "size": True}}}, ], ) def test_print_summary_nested_bad_shapes_never_raise( @@ -303,8 +306,12 @@ def test_print_summary_corrupt_size_json_warns( size_json = tmp_path / "size.json" size_json.write_text("not json {{{") print_summary(size_json, None) + size_json.write_bytes(b"\xff\xfe\x00") + print_summary(size_json, None) assert capsys.readouterr().out == "" - assert "Skipping size summary" in caplog.text + # The named arm, not the blanket, for both damage classes + assert caplog.text.count("cannot read") == 2 + assert "Skipping size summary for" not in caplog.text def test_print_summary_missing_partitions_named_in_backstop(