From 57c8648210d38ca4c34e4862be6b03bbba56733e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 13:38:54 -0500 Subject: [PATCH 1/6] Drop the test package init that collides with script/build_helpers.py on the CI path --- tests/unit_tests/build_helpers/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/unit_tests/build_helpers/__init__.py diff --git a/tests/unit_tests/build_helpers/__init__.py b/tests/unit_tests/build_helpers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 3eec5085da792f568b526802b231269646f1f007 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 13:39:46 -0500 Subject: [PATCH 2/6] Give the format_bar tests a unique basename --- .../build_helpers/{test_size_summary.py => test_format_bar.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit_tests/build_helpers/{test_size_summary.py => test_format_bar.py} (100%) diff --git a/tests/unit_tests/build_helpers/test_size_summary.py b/tests/unit_tests/build_helpers/test_format_bar.py similarity index 100% rename from tests/unit_tests/build_helpers/test_size_summary.py rename to tests/unit_tests/build_helpers/test_format_bar.py From e82453e80e6700c9e0fa1ae19007ea4bd584746f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 13:42:15 -0500 Subject: [PATCH 3/6] Complete the unit-test package chain so test modules import fully qualified --- tests/unit_tests/__init__.py | 0 tests/unit_tests/build_gen/__init__.py | 0 tests/unit_tests/build_helpers/__init__.py | 0 .../build_helpers/{test_format_bar.py => test_size_summary.py} | 0 tests/unit_tests/test_core.py | 2 +- 5 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/unit_tests/__init__.py create mode 100644 tests/unit_tests/build_gen/__init__.py create mode 100644 tests/unit_tests/build_helpers/__init__.py rename tests/unit_tests/build_helpers/{test_format_bar.py => test_size_summary.py} (100%) diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/build_gen/__init__.py b/tests/unit_tests/build_gen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/build_helpers/__init__.py b/tests/unit_tests/build_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/build_helpers/test_format_bar.py b/tests/unit_tests/build_helpers/test_size_summary.py similarity index 100% rename from tests/unit_tests/build_helpers/test_format_bar.py rename to tests/unit_tests/build_helpers/test_size_summary.py diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index c373116106..7adf955217 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -6,9 +6,9 @@ from unittest.mock import patch from hypothesis import given import pytest -from strategies import mac_addr_strings from esphome import const, core +from tests.unit_tests.strategies import mac_addr_strings class TestHexInt: From e3936f500fb1fa3d4e14faab8db7bbbd40153001 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 13:44:12 -0500 Subject: [PATCH 4/6] Patch exactly the named segments and share the segment-line pattern --- esphome/components/esp8266/build_surgery.py | 56 +++++++++---------- .../components/esp8266/test_build_surgery.py | 48 +++++++--------- 2 files changed, 45 insertions(+), 59 deletions(-) diff --git a/esphome/components/esp8266/build_surgery.py b/esphome/components/esp8266/build_surgery.py index 039ecaf0c6..9aeee79216 100644 --- a/esphome/components/esp8266/build_surgery.py +++ b/esphome/components/esp8266/build_surgery.py @@ -5,6 +5,7 @@ These mirror the PlatformIO extra scripts in this directory inside SCons and must stay self-contained. The native build generator applies the same patches to the linker scripts it generates, so the logic lives here as plain functions. Keep both in sync when changing either. +``segment_length`` is native-toolchain-only and has no script twin. """ from __future__ import annotations @@ -45,48 +46,41 @@ def relocate_ratetable(content: str) -> str: ) -_TESTING_SEGMENT_SIZES = ( - ("iram1_0_seg", TESTING_IRAM_SIZE), - ("dram0_0_seg", TESTING_DRAM_SIZE), - ("irom0_0_seg", TESTING_FLASH_SIZE), -) +_TESTING_SEGMENT_SIZES = { + "iram1_0_seg": TESTING_IRAM_SIZE, + "dram0_0_seg": TESTING_DRAM_SIZE, + "irom0_0_seg": TESTING_FLASH_SIZE, +} -def _patch_segment_size(content: str, segment_name: str, new_size: str) -> str: - pattern = ( +def _segment_line_re(segment_name: str) -> re.Pattern[str]: + """The MEMORY line for one segment: `` : org = 0x..., len = 0x...``.""" + return re.compile( rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)" - r"0x[0-9a-fA-F]+" + r"(0x[0-9a-fA-F]+)" ) - return re.sub(pattern, rf"\g<1>{new_size}", content) -def apply_testing_memory_patches(content: str, require: Collection[str]) -> str: - """Enlarge IRAM/DRAM/flash segments so grouped CI test builds can link. +def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str: + """Enlarge the named memory segments so grouped CI test builds can link. - ``require`` names the segments this file must define; a silently - unpatched segment would keep the real memory limits and fail grouped - builds far from the cause. The segments are split across the two linker - scripts (iram1_0_seg in the generated common one, dram0_0_seg and - irom0_0_seg in the flash one), so each caller requires only its own. + Each caller passes the segments its linker script defines; a segment + that fails to match raises, since a silently kept real memory limit + would fail grouped builds far from the cause. """ - missing = set(require) - for segment, size in _TESTING_SEGMENT_SIZES: - patched = _patch_segment_size(content, segment, size) - if patched != content: - missing.discard(segment) - content = patched - if missing: - raise RuntimeError( - f"Testing-mode memory patch failed: segment(s) {', '.join(sorted(missing))} " - "not found (has the Arduino core linker script changed?)" + for segment in segments: + content, count = _segment_line_re(segment).subn( + rf"\g<1>{_TESTING_SEGMENT_SIZES[segment]}", content ) + if count == 0: + raise RuntimeError( + f"Testing-mode memory patch failed: segment {segment} " + "not found (has the Arduino core linker script changed?)" + ) return content def segment_length(content: str, segment_name: str) -> int | None: """Read a memory segment's length from linker script content.""" - match = re.search( - rf"{segment_name}\s*:.+len\s*=\s*(0x[\da-fA-F]+)", - content, - ) - return int(match.group(1), 16) if match else None + match = _segment_line_re(segment_name).search(content) + return int(match.group(2), 16) if match else None diff --git a/tests/unit_tests/components/esp8266/test_build_surgery.py b/tests/unit_tests/components/esp8266/test_build_surgery.py index 200954494b..0e5a26294a 100644 --- a/tests/unit_tests/components/esp8266/test_build_surgery.py +++ b/tests/unit_tests/components/esp8266/test_build_surgery.py @@ -4,10 +4,12 @@ from __future__ import annotations import pytest +from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD from esphome.components.esp8266.build_surgery import ( RATETABLE_RULE, apply_testing_memory_patches, relocate_ratetable, + segment_length, ) _COMMON_LD_SNIPPET = """\ @@ -49,39 +51,29 @@ def test_relocate_ratetable_requires_anchor() -> None: def test_testing_memory_patches_enlarge_segments() -> None: - patched = apply_testing_memory_patches(_FLASH_LD_SNIPPET, require=()) - assert ( - "iram1_0_seg : org = 0x40100000, len = 0x200000" - in patched - ) - assert ( - "dram0_0_seg : org = 0x3FFE8000, len = 0x200000" - in patched - ) - assert ( - "irom0_0_seg : org = 0x40201010, len = 0x2000000" - in patched + patched = apply_testing_memory_patches( + _FLASH_LD_SNIPPET, ("iram1_0_seg", "dram0_0_seg", "irom0_0_seg") ) + assert segment_length(patched, "iram1_0_seg") == 0x200000 + assert segment_length(patched, "dram0_0_seg") == 0x200000 + assert segment_length(patched, "irom0_0_seg") == 0x2000000 + # Untouched segments keep their sizes + assert segment_length(patched, "dport0_0_seg") == 0x10 def test_segment_length() -> None: - from esphome.components.esp8266.build_surgery import segment_length - assert segment_length(_FLASH_LD_SNIPPET, "irom0_0_seg") == 0xFEFF0 assert segment_length(_FLASH_LD_SNIPPET, "missing_seg") is None -def test_testing_memory_patches_require() -> None: - """A required segment the patch could not find raises instead of - silently keeping the real memory limits.""" - patched = apply_testing_memory_patches( - _FLASH_LD_SNIPPET, require=("dram0_0_seg", "irom0_0_seg") - ) - assert "0x2000000" in patched - with pytest.raises(RuntimeError, match="dram0_0_seg, irom0_0_seg"): - apply_testing_memory_patches( - "MEMORY { }", require=("dram0_0_seg", "irom0_0_seg") - ) - # Segments a file does not require are patched opportunistically only - # With nothing required, unmatched content passes through unchanged - assert apply_testing_memory_patches("MEMORY { }", require=()) == "MEMORY { }" +def test_testing_memory_patches_missing_segment_raises() -> None: + """A named segment the patch could not find raises instead of silently + keeping the real memory limits.""" + with pytest.raises(RuntimeError, match="dram0_0_seg"): + apply_testing_memory_patches("MEMORY { }", ("dram0_0_seg",)) + + +def test_board_build_covers_every_board() -> None: + """Every supported board has native build metadata (the table may carry + extras that BOARDS does not expose).""" + assert set(BOARDS) <= set(ESP8266_BOARD_BUILD) From 0ed4c47b66ce5c52ac7e0d6d7cbf3565d52dd4d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 13:48:16 -0500 Subject: [PATCH 5/6] Adopt the exact-segments surgery API --- esphome/build_gen/arduino8266.py | 4 ++-- tests/unit_tests/build_gen/test_arduino8266.py | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index fb46cfd7f1..433c0b5d7d 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -388,7 +388,7 @@ def generate_ld_scripts( content = build_surgery.relocate_ratetable(result.stdout) if CORE.testing_mode: content = build_surgery.apply_testing_memory_patches( - content, require=("iram1_0_seg",) + content, ("iram1_0_seg",) ) write_file_if_changed(output, content) stamp.write_text(stamp_content, encoding="utf-8") @@ -401,7 +401,7 @@ def generate_ld_scripts( ld_dir / f"testing_{flash_ld_name}", build_surgery.apply_testing_memory_patches( flash_ld.read_text(encoding="utf-8"), - require=("dram0_0_seg", "irom0_0_seg"), + ("dram0_0_seg", "irom0_0_seg"), ), ) diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index b214e45e49..9bba720da6 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -24,7 +24,7 @@ from esphome.build_gen.arduino8266 import ( _resolve_build_config, get_flash_ld_path, ) -from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD +from esphome.components.esp8266.boards import ESP8266_BOARD_BUILD from esphome.components.esp8266.build_surgery import RATETABLE_RULE from esphome.components.esp8266.const import ( KEY_BOARD, @@ -58,11 +58,6 @@ def _set_flags(*flags: str) -> None: CORE.build_flags = set(flags) -def test_board_build_covers_every_board() -> None: - """Every supported board must have variant/define metadata.""" - assert set(BOARDS) <= set(ESP8266_BOARD_BUILD) - - def test_build_config_defaults() -> None: _set_flags() From 03d5b131ca14b077ed0bd561a12d26f1a03eaa8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 13:48:56 -0500 Subject: [PATCH 6/6] Adopt the relocated extraScript helper and its neutral parameter --- esphome/arduino8266/component.py | 6 ++++-- tests/unit_tests/test_arduino8266_component.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/arduino8266/component.py b/esphome/arduino8266/component.py index 17fd5471b4..3d70f142ca 100644 --- a/esphome/arduino8266/component.py +++ b/esphome/arduino8266/component.py @@ -17,8 +17,8 @@ from dataclasses import dataclass, field import logging from pathlib import Path -from esphome.build_helpers.extra_script import apply_extra_script from esphome.core import CORE, EsphomeError, Library +from esphome.platformio.extra_script import apply_extra_script from esphome.platformio.library import ( DEFAULT_BUILD_INCLUDE_DIR, DEFAULT_BUILD_SRC_FILTER, @@ -210,7 +210,9 @@ def resolve_libraries(framework_path: Path) -> list[ArduinoLibrary]: bundled.append(_bundled_library(framework_path, name)) def _emit(component: ConvertedLibrary) -> None: - apply_extra_script(component, "esp8266", pio_platform=ESP8266_PLATFORM) + apply_extra_script( + component, board_mcu="esp8266", pio_platform=ESP8266_PLATFORM + ) converted.append( _library_info( component.get_require_name(), component.source_dir, component.data diff --git a/tests/unit_tests/test_arduino8266_component.py b/tests/unit_tests/test_arduino8266_component.py index efe38a68dc..d26b1439c7 100644 --- a/tests/unit_tests/test_arduino8266_component.py +++ b/tests/unit_tests/test_arduino8266_component.py @@ -190,7 +190,7 @@ def test_resolve_libraries_external_and_bundled_deps(tmp_path: Path) -> None: libs = component.resolve_libraries(framework) mock_extra.assert_called_once_with( - converted, "esp8266", pio_platform="espressif8266" + converted, board_mcu="esp8266", pio_platform="espressif8266" ) assert [lib.name for lib in libs] == [ "Wire",