Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-23 19:38:34 -05:00
3 changed files with 96 additions and 58 deletions
+29 -23
View File
@@ -88,11 +88,15 @@ def _split_flags():
def _ok_result(stdout=None, stderr=""):
"""A successful preprocessor spawn (defaults to the common ld output)."""
"""A successful preprocessor spawn (defaults to the common ld output).
Streams are bytes, as the un-decoded subprocess.run delivers them.
"""
stdout = _COMMON_LD_H_OUTPUT if stdout is None else stdout
return MagicMock(
returncode=0,
stdout=_COMMON_LD_H_OUTPUT if stdout is None else stdout,
stderr=stderr,
stdout=stdout.encode() if isinstance(stdout, str) else stdout,
stderr=stderr.encode() if isinstance(stderr, str) else stderr,
)
@@ -549,7 +553,7 @@ def test_generate_ld_scripts_corrupt_cache_regenerates(tmp_path: Path) -> None:
def test_generate_ld_scripts_failure(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
result = MagicMock(returncode=1, stderr="nope")
result = MagicMock(returncode=1, stderr=b"nope")
with (
patch.object(arduino8266.subprocess, "run", return_value=result),
pytest.raises(EsphomeError, match="linker script failed"),
@@ -1073,16 +1077,14 @@ def test_generate_ld_scripts_surfaces_preprocessor_warnings(
"""Preprocessor stderr on a zero exit reaches the user; degenerate output is refused."""
paths = _make_framework(tmp_path)
_set_flags()
result = MagicMock(
returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="warning: something"
)
result = _ok_result(stderr="warning: something")
with patch.object(arduino8266.subprocess, "run", return_value=result):
_run_generate_ld_scripts(paths)
assert "Linker-script preprocessor: warning: something" in caplog.text
# New flags invalidate the stamp so the degenerate run regenerates
_set_flags("-DVTABLES_IN_DRAM")
result = MagicMock(returncode=0, stdout="", stderr="")
result = _ok_result(stdout="")
with (
patch.object(arduino8266.subprocess, "run", return_value=result),
pytest.raises(EsphomeError, match="SECTIONS"),
@@ -1097,9 +1099,7 @@ def test_generate_ld_scripts_lost_warn_note_vetoes_the_stamp(
re-runs -E and re-derives the diagnostic instead of losing it."""
paths = _make_framework(tmp_path)
_set_flags()
result = MagicMock(
returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="warning: something"
)
result = _ok_result(stderr="warning: something")
real_write_text = Path.write_text
def fail_note_writes(self: Path, text: str, encoding: str = "utf-8") -> int:
@@ -1129,8 +1129,8 @@ def test_generate_ld_scripts_unremovable_stale_note_vetoes_the_stamp(
obsolete diagnostic is not re-emitted on cache hits forever."""
paths = _make_framework(tmp_path)
_set_flags()
warn = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="warning: old")
clean = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
warn = _ok_result(stderr="warning: old")
clean = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=warn):
_run_generate_ld_scripts(paths)
@@ -1310,18 +1310,26 @@ def test_generate_ld_scripts_corrupt_output_is_overwritten(tmp_path: Path) -> No
assert "SECTIONS" in output.read_text(encoding="utf-8")
def test_generate_ld_scripts_unreadable_note_still_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
@pytest.mark.parametrize("damage", ["corrupt", "remove"])
def test_generate_ld_scripts_damaged_note_invalidates_cache(
tmp_path: Path, caplog: pytest.LogCaptureFixture, damage: str
) -> None:
"""A cached diagnostic that cannot be read must not vanish silently."""
"""A corrupted or externally removed diagnostic note is a cache miss:
-E re-runs and re-derives the warning instead of dropping it silently."""
paths = _make_framework(tmp_path)
result = _ok_result(stderr="warn!")
with patch.object(arduino8266.subprocess, "run", return_value=result):
ld_dir = _run_generate_ld_scripts(paths)
(ld_dir / ".local.eagle.app.v6.common.ld.stderr").write_bytes(b"\xff\xfe")
with patch.object(arduino8266.subprocess, "run", return_value=result):
note = ld_dir / ".local.eagle.app.v6.common.ld.stderr"
if damage == "corrupt":
note.write_bytes(b"\xff\xfe")
else:
note.unlink()
caplog.clear()
with patch.object(arduino8266.subprocess, "run", return_value=result) as mock_run:
_run_generate_ld_scripts(paths)
assert "could not be read" in caplog.text
assert mock_run.called
assert "Linker-script preprocessor: warn!" in caplog.text
@pytest.mark.parametrize("value", ["0x8000", "0xC000ul", "0x10UL"])
@@ -1446,7 +1454,7 @@ def test_generate_ld_scripts_surgery_failure_is_named(tmp_path: Path) -> None:
"""A moved rate-table anchor surfaces as a build error, not a traceback
or a silently unrelocated table."""
paths = _make_framework(tmp_path)
result = MagicMock(returncode=0, stdout="SECTIONS { no anchor here }", stderr="")
result = _ok_result(stdout="SECTIONS { no anchor here }")
with (
patch.object(arduino8266.subprocess, "run", return_value=result),
pytest.raises(EsphomeError, match="anchor not found"),
@@ -1539,9 +1547,7 @@ def test_generate_ld_scripts_reemits_cached_preprocessor_warning(
"""A preprocessor diagnostic survives cache hits instead of appearing
once and vanishing for the life of the build dir."""
paths = _make_framework(tmp_path)
result = MagicMock(
returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="warning: something odd"
)
result = _ok_result(stderr="warning: something odd")
with patch.object(arduino8266.subprocess, "run", return_value=result):
_run_generate_ld_scripts(paths)
assert caplog.text.count("warning: something odd") == 1
+26 -18
View File
@@ -2468,29 +2468,37 @@ def test_copy_src_tree_ignores_removed_generated_file(
assert new_json["config_hash"] == 0xDEADBEEF
def test_build_info_stale_branches(tmp_path: Path) -> None:
@pytest.mark.parametrize(
("case", "content", "expected"),
[
("files missing", None, True),
("json missing", "ABSENT", True),
("json unreadable", "not json", True),
# Valid JSON that is not an object is stale, not an AttributeError
("json not an object", "[]", True),
("hash mismatch", {"config_hash": 2, "esphome_version": "CURRENT"}, True),
("version mismatch", {"config_hash": 1, "esphome_version": "0.0.0"}, True),
("matching record", {"config_hash": 1, "esphome_version": "CURRENT"}, False),
],
)
def test_build_info_stale_branches(
tmp_path: Path, case: str, content, expected: bool
) -> None:
"""Missing files, an unreadable JSON, a hash or version mismatch each
regenerate; a matching record does not."""
import json as json_mod
from esphome.const import __version__
from esphome.writer import _build_info_stale
h = tmp_path / "build_info_data.h"
cpp = tmp_path / "build_info_data.cpp"
info = tmp_path / "build_info.json"
assert _build_info_stale(h, cpp, info, 1) is True # files missing
h.write_text("")
cpp.write_text("")
assert _build_info_stale(h, cpp, info, 1) is True # JSON unreadable
info.write_text("not json")
assert _build_info_stale(h, cpp, info, 1) is True
# Valid JSON that is not an object is stale, not an AttributeError
info.write_text("[]")
assert _build_info_stale(h, cpp, info, 1) is True
info.write_text(json_mod.dumps({"config_hash": 2, "esphome_version": __version__}))
assert _build_info_stale(h, cpp, info, 1) is True # hash mismatch
info.write_text(json_mod.dumps({"config_hash": 1, "esphome_version": "0.0.0"}))
assert _build_info_stale(h, cpp, info, 1) is True # version mismatch
info.write_text(json_mod.dumps({"config_hash": 1, "esphome_version": __version__}))
assert _build_info_stale(h, cpp, info, 1) is False
if content is not None:
h.write_text("")
cpp.write_text("")
if isinstance(content, dict):
if content.get("esphome_version") == "CURRENT":
content["esphome_version"] = __version__
info.write_text(json.dumps(content))
elif isinstance(content, str) and content != "ABSENT":
info.write_text(content)
assert _build_info_stale(h, cpp, info, 1) is expected, case