Apply the 18559 simplify pass: surgery wrapper, shared clean hint, resilient write helper, unified knob lookups, test scaffolding

This commit is contained in:
J. Nick Koston
2026-08-22 22:09:36 -05:00
parent d2ed17b688
commit 2521f29782
3 changed files with 135 additions and 140 deletions
+65 -72
View File
@@ -28,7 +28,7 @@ from esphome.build_helpers.ninja import shell_token as _shell_token
from esphome.components.esp8266 import build_surgery
from esphome.core import CORE, EsphomeError
from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.platformio.library import BARE_ARG_FLAGS, lex_build_flags
from esphome.platformio.library import lex_build_flags, raise_on_empty_arg_flags
if TYPE_CHECKING:
from esphome.arduino8266.framework import InstalledPaths
@@ -49,6 +49,26 @@ _DEFAULT_F_CPU = "80000000L"
# against; the cache stamp and stderr sidecars derive from the output name
_COMMON_LD_HEADER = "eagle.app.v6.common.ld.h"
_COMMON_LD_NAME = "local.eagle.app.v6.common.ld"
# Testing mode shadows the SDK flash ld with a patched copy under this name
_TESTING_LD_PREFIX = "testing_"
# The recovery hint for a half-extracted or damaged framework cache
_CLEAN_HINT = "run 'esphome clean-all' and retry"
def _sdk_ld_dir(framework: Path) -> Path:
return framework / "tools" / "sdk" / "ld"
def _apply_surgery(fn, *args: object) -> str:
"""Run one build_surgery edit, naming a failed anchor instead of a
traceback (the surgery module raises bare RuntimeError so its
``.py.script`` twins stay importable without esphome)."""
try:
return fn(*args)
except RuntimeError as err:
raise EsphomeError(str(err)) from err
# From platformio-build.py. Knob suffix -> SDK define; the first entry is
# the default (dicts preserve insertion order). With multiple SDK knobs set
@@ -213,13 +233,8 @@ def _lexed_build_flags() -> list[str]:
Lex once per build; consumers share the tokens.
"""
tokens = lex_build_flags(sorted(CORE.build_flags), "esphome")
# The lexer glues '-D ""' to a bare "-D"; gcc would eat the next flag
# as its argument (or add the CWD for -L). Always a typo, so raise for
# every consumer of the shared token list.
if empty := sorted({tok for tok in tokens if tok in BARE_ARG_FLAGS}):
raise EsphomeError(
f"build_flags contain empty-argument flag(s): {', '.join(empty)}"
)
# Raises for every consumer of the shared token list
raise_on_empty_arg_flags(tokens, "build_flags")
return tokens
@@ -242,11 +257,14 @@ def _flag_defines(unflags: set[str], tokens: list[str]) -> dict[str, str]:
def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
nonosdk = next(iter(_NONOSDK_VERSIONS.values()))
for name, define in _NONOSDK_VERSIONS.items():
if f"PIO_FRAMEWORK_ARDUINO_ESPRESSIF_{name}" in defines:
nonosdk = define
break
nonosdk = next(
(
define
for name, define in _NONOSDK_VERSIONS.items()
if f"PIO_FRAMEWORK_ARDUINO_ESPRESSIF_{name}" in defines
),
next(iter(_NONOSDK_VERSIONS.values())),
)
# Same compile-line/linked-artifact split as the lwIP knobs below: a
# raw NONOSDK* would define a second SDK macro while the link still
# resolves against the knob's libraries
@@ -257,11 +275,10 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
"build flags"
)
tcp_mss, features, ipv6, lwip_lib = _LWIP_DEFAULT
for knob, variant in _LWIP_VARIANTS.items():
if knob in defines:
tcp_mss, features, ipv6, lwip_lib = variant
break
tcp_mss, features, ipv6, lwip_lib = next(
(variant for knob, variant in _LWIP_VARIANTS.items() if knob in defines),
_LWIP_DEFAULT,
)
# The lwIP triple selects a prebuilt library; a raw override would win
# the compile line (user tokens come last here) while the link still
@@ -496,21 +513,6 @@ def _write_note(path: Path, text: str, *, warn: bool = False) -> None:
log("Could not write %s: %s", path, err)
def _write_generated(path: Path, content: str) -> None:
"""write_file_if_changed, replacing an unreadable existing copy.
The recovery is scoped to the comparison read: a damaged cached file is
logged and overwritten, while a genuine write failure still raises.
"""
try:
if path.is_file():
path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as err:
_LOGGER.warning("Replacing damaged generated file %s: %s", path, err)
path.unlink(missing_ok=True)
write_file_if_changed(path, content)
def generate_ld_scripts(
paths: InstalledPaths, config: _BuildConfig, flash_ld_name: str
) -> None:
@@ -532,7 +534,7 @@ def generate_ld_scripts(
cmd += [f"-D{d}" for d in config.mmu_defines]
if config.fp_in_irom:
cmd.append("-DFP_IN_IROM")
header = framework / "tools" / "sdk" / "ld" / _COMMON_LD_HEADER
header = _sdk_ld_dir(framework) / _COMMON_LD_HEADER
cmd += [str(header), "-o", "-"]
# The inputs are the command line (defines + framework version, which is
@@ -584,9 +586,7 @@ def generate_ld_scripts(
)
except OSError as err:
# A half-extracted or half-deleted toolchain cache reaches here
raise EsphomeError(
f"Could not run {gcc}: {err}; run 'esphome clean-all' and retry"
) from err
raise EsphomeError(f"Could not run {gcc}: {err}; {_CLEAN_HINT}") from err
if result.returncode != 0:
raise EsphomeError(f"Generating the linker script failed:\n{result.stderr}")
if result.stderr.strip():
@@ -599,24 +599,14 @@ def generate_ld_scripts(
if "SECTIONS" not in result.stdout:
# A degenerate zero-exit run must not be stamped as a good cache
raise EsphomeError(
"Generated linker script is missing its SECTIONS block; "
"run 'esphome clean-all' and retry"
f"Generated linker script is missing its SECTIONS block; {_CLEAN_HINT}"
)
try:
content = build_surgery.relocate_ratetable(result.stdout)
except RuntimeError as err:
# The anchor moved in a new core release: a named error, not a
# traceback, and never a silently unrelocated rate table
raise EsphomeError(str(err)) from err
content = _apply_surgery(build_surgery.relocate_ratetable, result.stdout)
if CORE.testing_mode:
try:
content = build_surgery.apply_testing_memory_patches(
content, ("iram1_0_seg",)
)
except RuntimeError as err:
# Same changed-linker-script failure class as the ratetable
raise EsphomeError(str(err)) from err
_write_generated(output, content)
content = _apply_surgery(
build_surgery.apply_testing_memory_patches, content, ("iram1_0_seg",)
)
write_file_if_changed(output, content)
_write_note(
stamp,
f"{stamp_content} content={hashlib.sha256(content.encode('utf-8')).hexdigest()}",
@@ -636,22 +626,25 @@ def generate_ld_scripts(
)
if CORE.testing_mode:
# A patched copy of the flash ld in the build dir; resolved through
# the same -L path as the SDK original it shadows.
flash_ld = framework / "tools" / "sdk" / "ld" / flash_ld_name
try:
flash_ld_text = flash_ld.read_text(encoding="utf-8")
except OSError as err:
# Same half-extracted-cache hazard as the gcc spawn above
raise EsphomeError(
f"Could not read {flash_ld}: {err}; run 'esphome clean-all' and retry"
) from err
try:
patched_flash_ld = build_surgery.apply_testing_memory_patches(
flash_ld_text,
("dram0_0_seg", "irom0_0_seg"),
)
except RuntimeError as err:
# Same changed-linker-script failure class as the ratetable
raise EsphomeError(str(err)) from err
_write_generated(ld_dir / f"testing_{flash_ld_name}", patched_flash_ld)
_generate_testing_flash_ld(framework, ld_dir, flash_ld_name)
def _generate_testing_flash_ld(
framework: Path, ld_dir: Path, flash_ld_name: str
) -> None:
"""A patched copy of the flash ld in the build dir; resolved through the
same -L path as the SDK original it shadows."""
flash_ld = _sdk_ld_dir(framework) / flash_ld_name
try:
flash_ld_text = flash_ld.read_text(encoding="utf-8")
except OSError as err:
# Same half-extracted-cache hazard as the preprocessor spawn
raise EsphomeError(f"Could not read {flash_ld}: {err}; {_CLEAN_HINT}") from err
patched_flash_ld = _apply_surgery(
build_surgery.apply_testing_memory_patches,
flash_ld_text,
("dram0_0_seg", "irom0_0_seg"),
)
write_file_if_changed(
ld_dir / f"{_TESTING_LD_PREFIX}{flash_ld_name}", patched_flash_ld
)
+9 -1
View File
@@ -556,9 +556,17 @@ def write_file_if_changed(path: Path, text: str) -> bool:
Returns true if the file was changed.
"""
from esphome.core import EsphomeError
src_content = None
if path.is_file():
src_content = read_file(path)
try:
src_content = read_file(path)
except (EsphomeError, UnicodeDecodeError) as err:
# A damaged existing file (unreadable, non-UTF-8) must be
# replaced, not abort the regeneration that would fix it
_LOGGER.warning("Replacing damaged file %s: %s", path, err)
path.unlink(missing_ok=True)
if src_content == text:
return False
write_file(path, text)
+61 -67
View File
@@ -62,7 +62,33 @@ def _set_flags(*flags: str) -> None:
def _resolve(*flags: str):
"""Set the build flags and resolve the knob config in one step."""
_set_flags(*flags)
return _resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
return _resolve_current()
def _defines():
"""The -D map for the current build flags."""
return _flag_defines(set(), arduino8266._lexed_build_flags())
def _resolve_current():
"""Resolve whatever flags are already set (must not clear them)."""
return _resolve_build_config(_defines())
def _split_flags():
"""Classify the current build flags the way write_project does."""
return arduino8266._project_flags(
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
)
def _ok_result(stdout=None, stderr=""):
"""A successful preprocessor spawn (defaults to the common ld output)."""
return MagicMock(
returncode=0,
stdout=_COMMON_LD_H_OUTPUT if stdout is None else stdout,
stderr=stderr,
)
def test_build_config_defaults() -> None:
@@ -100,9 +126,7 @@ def test_build_config_knobs() -> None:
"-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48",
"-DVTABLES_IN_DRAM",
)
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve_current()
assert config.nonosdk == "NONOSDK305"
assert config.exceptions
assert config.vtables == "VTABLES_IN_DRAM"
@@ -119,9 +143,7 @@ def test_build_config_mmu_custom_requires_sizes() -> None:
"-DMMU_IRAM_SIZE=0xC000",
"-DMMU_ICACHE_SIZE=0x4000",
)
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve_current()
# Emitted pre-sorted so build.ninja stays byte-stable across runs
assert config.mmu_defines == [
"MMU_ICACHE_SIZE=0x4000",
@@ -134,7 +156,7 @@ def test_defines_match_platformio_builder() -> None:
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
assert _defines_flags(
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags())),
_resolve_current(),
"dout",
"nodemcuv2",
ESP8266_BOARD_BUILD["nodemcuv2"]["defines"],
@@ -240,12 +262,7 @@ def test_build_config_lwip_variants(
def test_build_config_mmu_variants(knob: str, expected: list[str]) -> None:
_set_flags(f"-D{knob}")
assert (
_resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
).mmu_defines
== expected
)
assert _resolve_build_config(_defines()).mmu_defines == expected
def test_build_config_waveform_locked_phase() -> None:
@@ -272,9 +289,7 @@ SECTIONS
def _run_generate_ld_scripts(paths: InstalledPaths) -> Path:
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve_current()
arduino8266.generate_ld_scripts(paths, config, "eagle.flash.4m.ld")
return CORE.relative_pioenvs_path(CORE.name, "ld")
@@ -283,7 +298,7 @@ def test_generate_ld_scripts(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
_set_flags("-DFP_IN_IROM")
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with (
patch.object(arduino8266.subprocess, "run", return_value=result) as mock_run,
patch.object(arduino8266._LOGGER, "warning") as mock_warn,
@@ -318,7 +333,7 @@ def test_generate_ld_scripts(tmp_path: Path) -> None:
def test_generate_ld_scripts_corrupt_cache_regenerates(tmp_path: Path) -> None:
"""A truncated cached linker script regenerates even with a fresh stamp."""
paths = _make_framework(tmp_path)
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=result):
ld_dir = _run_generate_ld_scripts(paths)
output = ld_dir / "local.eagle.app.v6.common.ld"
@@ -352,7 +367,7 @@ def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None:
"}\n"
)
CORE.testing_mode = True
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=result):
ld_dir = _run_generate_ld_scripts(paths)
patched = (ld_dir / "testing_eagle.flash.4m.ld").read_text()
@@ -365,7 +380,7 @@ def test_generate_ld_scripts_testing_mode_missing_flash_ld_raises(
"""A missing flash ld in testing mode names the file and the fix."""
paths = _make_framework(tmp_path)
CORE.testing_mode = True
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with (
patch.object(arduino8266.subprocess, "run", return_value=result),
pytest.raises(EsphomeError, match="Could not read .*clean-all"),
@@ -380,21 +395,14 @@ def test_build_config_nonosdk_precedence() -> None:
"-DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305",
"-DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK221",
)
assert (
_resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
).nonosdk
== "NONOSDK221"
)
assert _resolve_build_config(_defines()).nonosdk == "NONOSDK221"
def test_project_flags_trailing_bare_linker_flag_warns(
caplog: pytest.LogCaptureFixture,
) -> None:
_set_flags("-l")
compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags(
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
)
compile_flags, link_flags, lib_dirs, libs = _split_flags()
assert "Ignoring trailing '-l'" in caplog.text
assert not libs
assert not lib_dirs
@@ -404,9 +412,7 @@ def test_project_flags_trailing_bare_linker_flag_warns(
def test_project_flags_lexed_entry_scatters_non_linker_tokens() -> None:
_set_flags("-L /d -Wl,-Map=m stray")
compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags(
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
)
compile_flags, link_flags, lib_dirs, libs = _split_flags()
assert lib_dirs == [Path("/d")]
assert link_flags == ["-Wl,-Map=m"]
assert "stray" in compile_flags
@@ -416,7 +422,7 @@ def test_project_flags_lexed_entry_scatters_non_linker_tokens() -> None:
def test_flag_defines_lexes_multi_token_entries() -> None:
"""A knob inside a multi-token entry is detected like PlatformIO does."""
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH -DFOO=1 -Os")
defines = _flag_defines(set(), arduino8266._lexed_build_flags())
defines = _defines()
assert "PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH" in defines
assert defines["FOO"] == "FOO=1"
config = _resolve_build_config(defines)
@@ -426,9 +432,7 @@ def test_flag_defines_lexes_multi_token_entries() -> None:
def test_project_flags_lexes_every_entry() -> None:
"""A linker flag anywhere in an entry reaches the link line (PIO parity)."""
_set_flags("-DFOO=1 -lbar")
compile_flags, _link, _dirs, libs = arduino8266._project_flags(
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
)
compile_flags, _link, _dirs, libs = _split_flags()
assert libs == ["bar"]
assert "-DFOO=1" in compile_flags
@@ -437,9 +441,7 @@ def test_project_flags_unflags_match_tokens() -> None:
"""build_unflags removes a token embedded in a multi-token entry."""
_set_flags("-Os -g3")
CORE.build_unflags = {"-Os"}
compile_flags, _link, _dirs, _libs = arduino8266._project_flags(
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
)
compile_flags, _link, _dirs, _libs = _split_flags()
assert "-g3" in compile_flags
assert "-Os" not in compile_flags
@@ -447,9 +449,7 @@ def test_project_flags_unflags_match_tokens() -> None:
def test_project_flags_requotes_lexed_defines() -> None:
"""A quoted spaced value stays one compiler argument after lex/emit."""
_set_flags('-DGREETING="hello world"')
compile_flags, _link, _dirs, _libs = arduino8266._project_flags(
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
)
compile_flags, _link, _dirs, _libs = _split_flags()
# shlex folds the quotes (as PIO's ParseFlags does); _shell_token
# re-quotes the spaced token so the shell passes one argv element
expected = (
@@ -461,7 +461,7 @@ def test_project_flags_requotes_lexed_defines() -> None:
def test_flag_defines_joins_spaced_define() -> None:
"""A spaced "-D KNOB" entry is detected exactly as PlatformIO detects it."""
_set_flags("-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
defines = _flag_defines(set(), arduino8266._lexed_build_flags())
defines = _defines()
assert "PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH" in defines
assert "" not in defines
@@ -475,20 +475,14 @@ def test_build_config_custom_mmu_without_knob_raises() -> None:
def test_flag_defines_lexes_quoted_single_tokens() -> None:
"""A quoted single-token define reads the same as on the compile line."""
_set_flags('-DMMU_SEC_HEAP="0x40108000"')
assert (
_flag_defines(set(), arduino8266._lexed_build_flags())["MMU_SEC_HEAP"]
== "MMU_SEC_HEAP=0x40108000"
)
assert _defines()["MMU_SEC_HEAP"] == "MMU_SEC_HEAP=0x40108000"
def test_flag_defines_duplicate_defines_resolve_deterministically() -> None:
"""Duplicate conflicting defines pick the same winner every run (sorted
iteration, last writer wins), independent of the set's hash seed."""
_set_flags("-DMMU_IRAM_SIZE=0x8000", "-DMMU_IRAM_SIZE=0xC000")
assert (
_flag_defines(set(), arduino8266._lexed_build_flags())["MMU_IRAM_SIZE"]
== "MMU_IRAM_SIZE=0xC000"
)
assert _defines()["MMU_IRAM_SIZE"] == "MMU_IRAM_SIZE=0xC000"
def test_flag_tables_match_platformio_builder() -> None:
@@ -647,9 +641,7 @@ def test_build_config_mmu_defines_do_not_alias_the_table() -> None:
module table for later builds in the same process."""
config = _resolve("-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48")
config.mmu_defines.append("MMU_BOGUS")
again = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
again = _resolve_current()
assert "MMU_BOGUS" not in again.mmu_defines
assert all(isinstance(v, tuple) for v in arduino8266._MMU_VARIANTS.values())
@@ -695,7 +687,7 @@ def test_generate_ld_scripts_header_change_invalidates_stamp(
paths = _make_framework(tmp_path)
header = paths.framework / "tools" / "sdk" / "ld" / "eagle.app.v6.common.ld.h"
header.write_text("v1")
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=result):
_run_generate_ld_scripts(paths)
header.write_text("v2 (longer)")
@@ -707,7 +699,7 @@ def test_generate_ld_scripts_header_change_invalidates_stamp(
def test_generate_ld_scripts_unreadable_stamp_regenerates(tmp_path: Path) -> None:
"""A non-UTF-8 stamp is a damaged cache: regenerate, never abort."""
paths = _make_framework(tmp_path)
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=result):
ld_dir = _run_generate_ld_scripts(paths)
(ld_dir / ".local.eagle.app.v6.common.ld.stamp").write_bytes(b"\xff\xfe")
@@ -743,19 +735,21 @@ def test_write_generated_replaces_damaged_file(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A non-UTF-8 existing copy is logged and overwritten; the write path
still raises for real failures."""
still raises for real failures (now the shared helper's contract)."""
from esphome.helpers import write_file_if_changed
target = tmp_path / "gen.ld"
target.write_bytes(b"\xff\xfe")
arduino8266._write_generated(target, "SECTIONS { }")
write_file_if_changed(target, "SECTIONS { }")
assert target.read_text(encoding="utf-8") == "SECTIONS { }"
assert "Replacing damaged generated file" in caplog.text
assert "Replacing damaged file" in caplog.text
def test_generate_ld_scripts_edited_output_regenerates(tmp_path: Path) -> None:
"""The stamp records the content hash, so an externally edited cached
script regenerates instead of linking untrusted content."""
paths = _make_framework(tmp_path)
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=result):
ld_dir = _run_generate_ld_scripts(paths)
output = ld_dir / "local.eagle.app.v6.common.ld"
@@ -770,7 +764,7 @@ def test_generate_ld_scripts_corrupt_output_is_overwritten(tmp_path: Path) -> No
"""A non-UTF-8 cached script must be overwritten by the regeneration,
not abort it (write_file_if_changed reads the old content)."""
paths = _make_framework(tmp_path)
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=result):
ld_dir = _run_generate_ld_scripts(paths)
output = ld_dir / "local.eagle.app.v6.common.ld"
@@ -785,7 +779,7 @@ def test_generate_ld_scripts_unreadable_note_still_warns(
) -> None:
"""A cached diagnostic that cannot be read must not vanish silently."""
paths = _make_framework(tmp_path)
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="warn!")
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")
@@ -928,7 +922,7 @@ def test_generate_ld_scripts_testing_surgery_failure_is_named(
named error, like the ratetable surgery."""
paths = _make_framework(tmp_path)
CORE.testing_mode = True
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with (
patch.object(arduino8266.subprocess, "run", return_value=result),
patch.object(
@@ -950,7 +944,7 @@ def test_generate_ld_scripts_testing_flash_ld_surgery_failure_is_named(
"MEMORY { }"
)
CORE.testing_mode = True
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with (
patch.object(arduino8266.subprocess, "run", return_value=result),
patch.object(
@@ -996,7 +990,7 @@ def test_generate_ld_scripts_unreadable_header_forces_regeneration(
raise PermissionError(13, "denied")
return real_stat(self, **kwargs)
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(Path, "stat", fake_stat):
with patch.object(
arduino8266.subprocess, "run", return_value=result
@@ -1024,7 +1018,7 @@ def test_generate_ld_scripts_gcc_change_invalidates_stamp(tmp_path: Path) -> Non
paths = _make_framework(tmp_path)
gcc = toolchain_tool(paths.toolchain, "gcc")
gcc.write_text("v1")
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
result = _ok_result()
with patch.object(arduino8266.subprocess, "run", return_value=result):
_run_generate_ld_scripts(paths)
gcc.write_text("v2 (longer)")