mirror of
https://github.com/esphome/esphome.git
synced 2026-10-05 18:41:32 +00:00
Warn on the build's own broken artifacts, buffer the report, pin the blank-cell path
Skips caused by the build's own outputs (missing/unparsable/misshapen size report, absent RAM region, broken-but-present partition table via a typed _MalformedPartitionRow) log at warning; only the legitimately quiet cases (no table given, no qualifying partition) stay at debug. The report is buffered so a late failure prints nothing instead of half a report. The blank-cell test now asserts the logged row and path, so reverting the ValueError fails it (mutation-checked), and the nested- shape tests assert the named guard fired rather than the blanket.
This commit is contained in:
@@ -44,6 +44,11 @@ def _parse_size(token: str) -> int:
|
||||
return int(token)
|
||||
|
||||
|
||||
class _MalformedPartitionRow(ValueError):
|
||||
"""A matched app row whose size cell cannot be parsed; warned, since the
|
||||
table is present but broken."""
|
||||
|
||||
|
||||
def _find_app_partition_size(partitions_csv: Path) -> int:
|
||||
"""Return the size of the firmware's app partition.
|
||||
|
||||
@@ -66,7 +71,7 @@ def _find_app_partition_size(partitions_csv: Path) -> int:
|
||||
try:
|
||||
return _parse_size(psize)
|
||||
except ValueError as err:
|
||||
raise ValueError(
|
||||
raise _MalformedPartitionRow(
|
||||
f"{err} for partition {cells[0]} in {partitions_csv}"
|
||||
) from err
|
||||
raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}")
|
||||
@@ -96,35 +101,69 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
|
||||
|
||||
def _print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
# The build's own POST_BUILD step writes this file; its absence or an
|
||||
# unexpected shape is a regression signal, so these skips warn
|
||||
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
|
||||
|
||||
if not isinstance(data, dict):
|
||||
# Non-object JSON has no .get
|
||||
_LOGGER.debug("Skipping size summary: unexpected shape in %s", size_json)
|
||||
_LOGGER.warning("Skipping size summary: unexpected shape in %s", size_json)
|
||||
return
|
||||
|
||||
memory_types = data.get("memory_types", {})
|
||||
# Buffered so a late failure prints nothing instead of half a report
|
||||
lines: list[str] = []
|
||||
memory_types = data.get("memory_types")
|
||||
if not isinstance(memory_types, dict):
|
||||
memory_types = {}
|
||||
ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {}
|
||||
if not isinstance(ram_region, dict):
|
||||
ram_region = {}
|
||||
ram_used = ram_region.get("used")
|
||||
ram_total = ram_region.get("size")
|
||||
if ram_total and ram_used is not None:
|
||||
print(f"RAM: {_format_bar(ram_used, ram_total)}")
|
||||
if (
|
||||
isinstance(ram_used, (int, float))
|
||||
and isinstance(ram_total, (int, float))
|
||||
and ram_total > 0
|
||||
):
|
||||
lines.append(f"RAM: {_format_bar(int(ram_used), int(ram_total))}")
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Skipping RAM summary: no usable DRAM/DIRAM region in %s", size_json
|
||||
)
|
||||
|
||||
app_size = _resolve_app_size(data, partitions_csv)
|
||||
if app_size is not None:
|
||||
lines.append(f"Flash: {_format_bar(data['image_size'], app_size)}")
|
||||
for line in lines:
|
||||
print(line)
|
||||
|
||||
|
||||
def _resolve_app_size(data: dict, partitions_csv: Path | None) -> int | None:
|
||||
"""The Flash bar's denominator, or None (already logged) to skip it."""
|
||||
image_size = data.get("image_size")
|
||||
if image_size is None or partitions_csv is None:
|
||||
return
|
||||
if image_size is None:
|
||||
_LOGGER.warning("Skipping Flash summary: no image_size in the size report")
|
||||
return None
|
||||
if partitions_csv is None:
|
||||
_LOGGER.debug("Skipping Flash summary: no partition table given")
|
||||
return None
|
||||
try:
|
||||
app_size = _find_app_partition_size(partitions_csv)
|
||||
except (ValueError, OSError) as e:
|
||||
except (_MalformedPartitionRow, OSError) as e:
|
||||
# The table is there but broken/unreadable: visible, like size 0
|
||||
_LOGGER.warning("Skipping Flash summary: %s", e)
|
||||
return None
|
||||
except ValueError as e:
|
||||
# Missing file or no qualifying partition: legitimate for
|
||||
# non-app layouts, keep quiet
|
||||
_LOGGER.debug("Skipping Flash summary: %s", e)
|
||||
return
|
||||
return None
|
||||
if app_size <= 0:
|
||||
# Skipping also fails CI's Flash extraction, the right outcome here
|
||||
_LOGGER.warning(
|
||||
@@ -132,5 +171,5 @@ def _print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
app_size,
|
||||
partitions_csv,
|
||||
)
|
||||
return
|
||||
print(f"Flash: {_format_bar(image_size, app_size)}")
|
||||
return None
|
||||
return app_size
|
||||
|
||||
@@ -201,17 +201,42 @@ def test_print_summary_happy_path_prints_both_bars(
|
||||
],
|
||||
)
|
||||
def test_print_summary_nested_bad_shapes_never_raise(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], payload: dict
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
payload: dict,
|
||||
) -> None:
|
||||
"""The blanket guard keeps unexpected nested shapes from raising."""
|
||||
"""Bad nested shapes hit the named RAM guard, not the blanket backstop."""
|
||||
size_json = _write_size_json(tmp_path, payload)
|
||||
print_summary(size_json, None)
|
||||
# No half-formed bar for CI to scrape; every payload fails before printing
|
||||
assert capsys.readouterr().out == ""
|
||||
assert "Skipping RAM summary" in caplog.text
|
||||
assert "Skipping size summary for" not in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_blanket_guard_catches_the_rest(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Shapes the named guards miss (non-numeric image_size) warn via the
|
||||
blanket backstop, and the buffered report prints nothing at all."""
|
||||
size_json = _write_size_json(
|
||||
tmp_path,
|
||||
{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": "x"},
|
||||
)
|
||||
partitions = tmp_path / "partitions.csv"
|
||||
partitions.write_text("app0, app, ota_0, 0x10000, 0x100000,\n")
|
||||
print_summary(size_json, partitions)
|
||||
assert capsys.readouterr().out == ""
|
||||
assert "Skipping size summary for" in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_blank_size_cell_names_the_row(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A blank size cell raises ValueError instead of parsing to 0."""
|
||||
size_json = _write_size_json(
|
||||
@@ -223,3 +248,7 @@ def test_print_summary_blank_size_cell_names_the_row(
|
||||
print_summary(size_json, partitions)
|
||||
out = capsys.readouterr().out
|
||||
assert "RAM:" in out and "Flash:" not in out
|
||||
# Pins the ValueError path: pre-diff, "" parsed to 0 and the size-0
|
||||
# warning fired instead
|
||||
assert "blank partition size cell" in caplog.text
|
||||
assert "app0" in caplog.text and str(partitions) in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user