From 8e4e541d7e84183e394c4328c1e14ca0113e4ac1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 09:40:35 -0500 Subject: [PATCH] Unlink a damaged build_info.json so regeneration completes, and log the replacement The widened except left non-UTF-8 damage half-fixed: the staleness branch fired, but write_file_if_changed then re-read the damaged file and raised EsphomeError. Unlinking in the handler makes the recovery self-sufficient, and the warning makes a repeatedly unreadable file visible instead of silently costing a rebuild each run. The non-UTF-8 case now runs end to end as a third row of the parametrized damage test, replacing the stubbed decision-only test. --- esphome/writer.py | 9 +++++++-- tests/unit_tests/test_writer.py | 33 ++------------------------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 42e5751530..2b6ed929f0 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,3 +1,4 @@ +from contextlib import suppress import importlib import json import logging @@ -343,8 +344,12 @@ def copy_src_tree(): ): # Non-object JSON is stale like every other damage case sources_changed = True - except (ValueError, OSError): - # ValueError covers both JSONDecodeError and UnicodeDecodeError + except (ValueError, OSError) as err: + # ValueError covers both JSONDecodeError and UnicodeDecodeError; + # unlink so the regenerating write never re-reads the damage + _LOGGER.warning("Replacing damaged build_info.json: %s", err) + with suppress(OSError): + build_info_json_path.unlink(missing_ok=True) sources_changed = True # Write build_info header and JSON metadata diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index cb12ae79bd..0551090e2c 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -2118,8 +2118,8 @@ def test_copy_src_tree_detects_version_change( @pytest.mark.parametrize( "damage", - (b"invalid json {{{", b"[]"), - ids=("invalid-json", "non-object"), + (b"invalid json {{{", b"[]", b'\xff{"config_hash": 1}'), + ids=("invalid-json", "non-object", "non-utf8"), ) @patch("esphome.writer.CORE") @patch("esphome.writer.iter_components") @@ -2143,35 +2143,6 @@ def test_copy_src_tree_regenerates_damaged_build_info( assert new_json["config_hash"] == 0xDEADBEEF -@patch("esphome.writer.write_file_if_changed", return_value=False) -@patch("esphome.writer.CORE") -@patch("esphome.writer.iter_components") -@patch("esphome.writer.walk_files") -def test_copy_src_tree_non_utf8_build_info_reads_as_stale( - mock_walk_files: MagicMock, - mock_iter_components: MagicMock, - mock_core: MagicMock, - mock_write: MagicMock, - tmp_path: Path, -) -> None: - """Non-UTF-8 build_info.json fires the staleness branch instead of raising. - - Writes are stubbed (returning False, so nothing else can set - sources_changed): regenerating over the damaged file needs - write_file_if_changed's own recovery, which lands separately. - """ - build_info_json_path = _setup_build_info_mocks( - mock_core, mock_iter_components, mock_walk_files, tmp_path - ) - core_path = tmp_path / "src" / "esphome" / "core" - (core_path / "build_info_data.h").write_text("// old") - (core_path / "build_info_data.cpp").write_text("// old") - build_info_json_path.write_bytes(b'\xff{"config_hash": 1}') - _run_copy_src_tree() - written = [call.args[0] for call in mock_write.call_args_list] - assert build_info_json_path in written - - @patch("esphome.writer.CORE") @patch("esphome.writer.iter_components") @patch("esphome.writer.walk_files")