From 17f718e8c9448e3d12cfdf12dd0e86136af2cda7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2026 20:55:27 -0500 Subject: [PATCH] Simplify the native dispatch and CI cache wiring A cache-arduino8266 composite action mirrors cache-esp-idf (one key resolver, dev-writes/PR-restores) and both jobs pin ESPHOME_ARDUINO8266_PREFIX so YAML and Python agree on the path by construction. esp32 provides the native_toolchain_module hook, so the shared dispatcher never names a backend and the unclaimed-native raise loses its esp-idf carve-out; the esp8266 decode and run_compile hooks resolve through the same seam. run_compile resolves ccache once and threads it to the generator, env, and idedata; the compdb rule names follow the shared kinds; the pio-options warner inlines into the driver and derives its consumed set from the core routing constant; the per-toolchain CI narrowing shares one body; create_components_graph is memoized per run; the decode rate-limit drops its math sentinel. --- .github/actions/cache-arduino8266/action.yml | 36 ++++++++++ .github/workflows/ci.yml | 44 +++---------- esphome/__main__.py | 19 +++--- esphome/arduino8266/toolchain.py | 66 +++++++++++-------- esphome/components/esp32/__init__.py | 13 ++++ esphome/components/esp8266/__init__.py | 13 ++-- script/determine-jobs.py | 27 +++++--- script/helpers.py | 2 + .../unit_tests/test_arduino8266_toolchain.py | 37 +++++------ tests/unit_tests/test_main.py | 4 +- 10 files changed, 147 insertions(+), 114 deletions(-) create mode 100644 .github/actions/cache-arduino8266/action.yml diff --git a/.github/actions/cache-arduino8266/action.yml b/.github/actions/cache-arduino8266/action.yml new file mode 100644 index 0000000000..917b7aa077 --- /dev/null +++ b/.github/actions/cache-arduino8266/action.yml @@ -0,0 +1,36 @@ +name: Cache Arduino ESP8266 +description: > + Resolve the pinned Arduino core and xtensa toolchain versions and cache the + native ESP8266 install (~110 MB framework + toolchain; no ccache store, the + seed job saves before any compile runs). Callers must set env + ESPHOME_ARDUINO8266_PREFIX: ~/.esphome-arduino8266 and have the Python venv + already restored. Mirrors cache-esp-idf: only dev-branch pushes write the + shared cache, everything else restores. +runs: + using: composite + steps: + - name: Resolve the native toolchain cache key + # The versions are pinned in code, not in a hashable file, so resolve + # them for the key (actions/cache never overwrites a key, so a bump + # must change it). Assignment form so errexit catches a resolver + # failure; a nested $(...) inside echo would silently yield a + # degenerate key. + id: version + shell: bash + run: | + . venv/bin/activate + key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")') + [ -n "$key" ] || exit 1 + echo "key=$key" >> "$GITHUB_OUTPUT" + - name: Cache the native toolchain (write on dev) + if: github.ref == 'refs/heads/dev' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.esphome-arduino8266 + key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }} + - name: Restore the native toolchain (off dev) + if: github.ref != 'refs/heads/dev' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.esphome-arduino8266 + key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36c074003f..76bace8ecf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,6 +187,10 @@ jobs: # seed-apt-cache / cache-esp-idf). if: github.event_name == 'push' timeout-minutes: 15 + env: + # The composite action and the install below agree on this path by + # construction, not by matching the Python-side default + ESPHOME_ARDUINO8266_PREFIX: ~/.esphome-arduino8266 steps: - name: Check out code from GitHub uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -195,23 +199,9 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - name: Resolve the native toolchain cache key - id: esp8266-native-cache-key - run: | - . venv/bin/activate - # Assignment form so errexit catches a resolver failure; a nested - # $(...) inside echo would silently yield a degenerate key - key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")') - [ -n "$key" ] || exit 1 - echo "key=esp8266-native-$key" >> $GITHUB_OUTPUT - name: Cache the native toolchain - id: esp8266-native-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/esphome/arduino8266 - key: ${{ steps.esp8266-native-cache-key.outputs.key }} + uses: ./.github/actions/cache-arduino8266 - name: Install the native toolchain - if: steps.esp8266-native-cache.outputs.cache-hit != 'true' run: | . venv/bin/activate python -c "from esphome.arduino8266.framework import check_and_install; from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION; check_and_install(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION)" @@ -1181,6 +1171,7 @@ jobs: # Single source of truth -- the full list lives in # script/determine-jobs.py::ESP8266_NATIVE_TEST_COMPONENTS. TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }} + ESPHOME_ARDUINO8266_PREFIX: ~/.esphome-arduino8266 steps: - name: Check out code from GitHub uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -1191,27 +1182,8 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - # ~110 MB of framework + toolchain (no ccache: the seed job saves - # before any compile runs, so the store would always be empty). The versions - # are pinned in code, not in a hashable file, so resolve them for the - # key (actions/cache never overwrites a key, so a bump must change it). - # PRs are restore-only; the shared entry is seeded on pushes to dev by - # seed-esp8266-native-cache, mirroring the seed-apt-cache scoping. - - name: Resolve the native toolchain cache key - id: esp8266-native-cache-key - run: | - . venv/bin/activate - # Assignment form so errexit catches a resolver failure; a nested - # $(...) inside echo would silently yield a degenerate key - key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")') - [ -n "$key" ] || exit 1 - echo "key=esp8266-native-$key" >> $GITHUB_OUTPUT - - - name: Restore the native toolchain - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/esphome/arduino8266 - key: ${{ steps.esp8266-native-cache-key.outputs.key }} + - name: Cache the native toolchain + uses: ./.github/actions/cache-arduino8266 - name: Run native toolchain compile test run: | diff --git a/esphome/__main__.py b/esphome/__main__.py index 2ea942d735..b242cc6baf 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -849,13 +849,6 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: platform_run_compile = getattr(module, "run_compile", None) if platform_run_compile is not None and platform_run_compile(args, config): pass - elif CORE.using_native_toolchain and not CORE.using_toolchain_esp_idf: - # A resolved native toolchain must be claimed by its platform hook; - # falling through would build a mis-configured PlatformIO project - raise EsphomeError( - f"Toolchain '{CORE.toolchain.value}' resolved but no platform " - "backend claimed the build" - ) elif CORE.using_toolchain_esp_idf: from esphome.espidf import toolchain @@ -883,6 +876,14 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: # (ValueError/LookupError) must not fail a successful build # either. _LOGGER.warning("Could not generate idedata: %s", err) + elif CORE.using_native_toolchain: + # A resolved native toolchain must be claimed by its platform hook + # or a branch above; falling through would build a mis-configured + # PlatformIO project + raise EsphomeError( + f"Toolchain '{CORE.toolchain.value}' resolved but no platform " + "backend claimed the build" + ) else: from esphome.platformio import toolchain @@ -1951,10 +1952,6 @@ def _native_toolchain_module(): ``native_toolchain_module`` hook (the same per-platform module seam ``compile_program`` uses), so shared dispatch never names a backend. """ - if CORE.using_toolchain_esp_idf: - from esphome.espidf import toolchain - - return toolchain module = importlib.import_module("esphome.components." + CORE.target_platform) get_native = getattr(module, "native_toolchain_module", None) native = get_native() if get_native is not None else None diff --git a/esphome/arduino8266/toolchain.py b/esphome/arduino8266/toolchain.py index 6b3977d68b..74e913fd23 100644 --- a/esphome/arduino8266/toolchain.py +++ b/esphome/arduino8266/toolchain.py @@ -7,7 +7,6 @@ from pathlib import Path import subprocess from esphome.arduino8266 import framework -from esphome.build_helpers.pio_options import warn_ignored_platformio_options from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, @@ -15,7 +14,7 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, ) from esphome.core import CORE, EsphomeError -from esphome.helpers import IS_WINDOWS, write_file_if_changed +from esphome.helpers import write_file_if_changed from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -23,13 +22,26 @@ _LOGGER = logging.getLogger(__name__) # ESP8266 user RAM (matches upload.maximum_ram_size in every board manifest) _MAX_RAM_SIZE = 81920 -# platformio_options keys the native build consumes. YAML-set upload_speed -# never reaches CORE.platformio_options under the native toolchain (it is -# read from the raw config at upload time), so anything here came from a -# component and genuinely is dropped; warn for it. -_CONSUMED_PIO_OPTIONS = frozenset( - {"lib_ignore", "board_build.f_cpu", "board_build.ldscript"} -) + +def _warn_ignored_platformio_options() -> None: + """Warn for component-added platformio options the native build drops. + + The consumed set derives from the routing constant in core/config.py so + the two lists cannot drift. YAML-set upload_speed never reaches + CORE.platformio_options under the native toolchain (it is read from the + raw config at upload time), so anything unconsumed came from a + component and genuinely is dropped. + """ + from esphome.core.config import NATIVE_ARDUINO_PIO_OPTIONS + + consumed = NATIVE_ARDUINO_PIO_OPTIONS | {"lib_ignore"} + for key in sorted(CORE.platformio_options or {}): + if key not in consumed: + _LOGGER.warning( + "platformio_options->%s is ignored when building with the " + "native 'arduino' toolchain", + key, + ) _RAM_SECTIONS = (".data", ".rodata", ".bss") @@ -44,14 +56,8 @@ def get_elf_path() -> Path: return get_build_dir() / "firmware.elf" -# Windows binutils carry the executable suffix; is_file() checks need it -_EXE_SUFFIX = ".exe" if IS_WINDOWS else "" - - def _toolchain_tool(name: str) -> Path: - return ( - framework.get_toolchain_path() / "bin" / f"xtensa-lx106-elf-{name}{_EXE_SUFFIX}" - ) + return framework.toolchain_tool(framework.get_toolchain_path(), name) def get_addr2line_path() -> Path: @@ -69,12 +75,15 @@ def get_readelf_path() -> Path: def run_compile(config: ConfigType, verbose: bool) -> int: from esphome.build_gen import arduino8266 as build_gen - warn_ignored_platformio_options(_CONSUMED_PIO_OPTIONS, "arduino") + _warn_ignored_platformio_options() paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]) - ninja_changed = build_gen.write_project(paths) + # Resolved once per build: the resolution probes PATH and spawns the + # runnability check, and three consumers need the same answer + ccache = framework.ccache_path() + ninja_changed = build_gen.write_project(paths, ccache) build_dir = get_build_dir() - env = framework.get_build_env(paths.toolchain) + env = framework.get_build_env(paths.toolchain, ccache) # The compile database is a pure function of build.ninja (no compilation # involved), so regenerate it before the build: a failed build can then @@ -94,9 +103,9 @@ def run_compile(config: ConfigType, verbose: bool) -> int: if rc != 0: return rc - _print_size_summary(build_dir) + _print_size_summary(build_dir, paths) try: - idedata = get_idedata() + idedata = get_idedata(ccache) except (EsphomeError, LookupError, OSError, RuntimeError, ValueError) as err: # The firmware already built; idedata is a bonus artifact here. # Broad on purpose: a vanished compiler (OSError), a failed include @@ -116,7 +125,7 @@ def _write_compile_commands( ninja_path: Path, build_dir: Path, env: dict[str, str] ) -> None: result = subprocess.run( - [str(ninja_path), "-C", str(build_dir), "-t", "compdb", "cc", "cxx", "asm"], + [str(ninja_path), "-C", str(build_dir), "-t", "compdb", "c", "cxx", "asm"], env=env, capture_output=True, text=True, @@ -133,14 +142,14 @@ def _write_compile_commands( write_file_if_changed(build_dir / "compile_commands.json", result.stdout) -def _parse_app_size(build_dir: Path) -> int | None: +def _parse_app_size(build_dir: Path, paths: framework.InstalledPaths) -> int | None: """Read the app flash budget (irom0_0_seg length) from the linker script.""" from esphome.build_gen.arduino8266 import get_flash_ld_path from esphome.components.esp8266.build_surgery import segment_length # Warnings, not debug: without the app size the Flash summary line is # dropped and CI's memory-impact extraction loses its flash metric. - ld_path = get_flash_ld_path(build_dir) + ld_path = get_flash_ld_path(build_dir, paths) try: ld_text = ld_path.read_text(encoding="utf-8") except OSError as err: @@ -157,7 +166,7 @@ def _parse_app_size(build_dir: Path) -> int | None: return app_size -def _print_size_summary(build_dir: Path) -> None: +def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> None: """Print the PlatformIO-shaped RAM/Flash lines. The exact shape (including the bar) is parsed by @@ -196,7 +205,7 @@ def _print_size_summary(build_dir: Path) -> None: # Resolve the flash budget before printing anything: a RAM line without # its Flash line would let CI's memory-impact extraction sum the two # metrics over different build counts (_parse_app_size already warned). - app_size = _parse_app_size(build_dir) + app_size = _parse_app_size(build_dir, paths) if not app_size: return ram = sum(sections[s] for s in _RAM_SECTIONS) @@ -205,7 +214,7 @@ def _print_size_summary(build_dir: Path) -> None: print_size_line("Flash", flash, app_size) -def get_idedata() -> dict | None: +def get_idedata(ccache: str | None = framework.CCACHE_UNRESOLVED) -> dict | None: """Derive idedata from the build's compile_commands.json. Same contract as ``espidf.toolchain.get_idedata``: the fields IDE @@ -213,7 +222,8 @@ def get_idedata() -> dict | None: """ from esphome.build_helpers.idedata import load_or_build_idedata - ccache = framework.ccache_path() + if ccache is framework.CCACHE_UNRESOLVED: + ccache = framework.ccache_path() return load_or_build_idedata( get_build_dir() / "compile_commands.json", get_elf_path(), diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 120e4d8ccd..70b24e0372 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -3442,3 +3442,16 @@ def process_stacktrace(config, line, backtrace_state): _decode_pc(config, addr.group()) return backtrace_state + + +def native_toolchain_module(): + """The native build backend for the resolved toolchain, if any. + + Hook for ``__main__``'s shared dispatch (idedata, analyze_memory, + decode); same seam the esp8266 component provides. + """ + if not CORE.using_toolchain_esp_idf: + return None + from esphome.espidf import toolchain + + return toolchain diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 1bcc8c0ad3..234550c48e 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,5 +1,4 @@ import logging -import math from pathlib import Path import platform import re @@ -542,10 +541,9 @@ async def finalize_serial_config() -> None: # PlatformIO toolchain. def run_compile(args, config: ConfigType) -> bool: # Positive check: the native backend only runs when explicitly resolved - if not CORE.using_toolchain_arduino: + toolchain = native_toolchain_module() + if toolchain is None: return False - from esphome.arduino8266 import toolchain - if toolchain.run_compile(config, CORE.verbose) != 0: raise EsphomeError("ESP8266 native build failed") return True @@ -625,16 +623,15 @@ def _warn_decode_problem(key: str, message: str, *args) -> None: the suppression expires instead of living for the process lifetime. """ now = time.monotonic() - if now - _DECODE_WARNED_AT.get(key, -math.inf) < 30: + last = _DECODE_WARNED_AT.get(key) + if last is not None and now - last < 30: return _DECODE_WARNED_AT[key] = now _LOGGER.warning(message, *args) def _decode_pc(config, addr): - if CORE.using_toolchain_arduino: - from esphome.arduino8266 import toolchain as native_toolchain - + if (native_toolchain := native_toolchain_module()) is not None: addr2line = native_toolchain.get_addr2line_path() elf = native_toolchain.get_elf_path() for path in (addr2line, elf): diff --git a/script/determine-jobs.py b/script/determine-jobs.py index e032b1ca37..43150895ae 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -50,6 +50,7 @@ from __future__ import annotations import argparse from collections import Counter +from collections.abc import Callable from enum import StrEnum from functools import cache import json @@ -595,12 +596,23 @@ def esp32_platformio_components_to_test(branch: str | None = None) -> list[str]: Returns: Sorted list of component names to compile. """ + return _native_components_to_test( + branch, ESP32_PLATFORMIO_TEST_COMPONENTS, _esp32_platformio_path_or_file_trigger + ) + + +def _native_components_to_test( + branch: str | None, + test_set: frozenset[str] | set[str], + infra_trigger: Callable[[list[str]], bool], +) -> list[str]: + """The shared narrowing rule for the per-toolchain smoke-test jobs.""" files = changed_files(branch) - if core_changed(files) or _esp32_platformio_path_or_file_trigger(files): - return sorted(ESP32_PLATFORMIO_TEST_COMPONENTS) + if core_changed(files) or infra_trigger(files): + return sorted(test_set) - return sorted(ESP32_PLATFORMIO_TEST_COMPONENTS & _changed_components_closure(files)) + return sorted(test_set & _changed_components_closure(files)) def should_run_esp32_platformio(branch: str | None = None) -> bool: @@ -682,12 +694,9 @@ def esp8266_native_components_to_test(branch: str | None = None) -> list[str]: list on core or infrastructure changes, otherwise the intersection with the changed-component dependency closure (empty list skips the job). """ - files = changed_files(branch) - - if core_changed(files) or _esp8266_native_path_or_file_trigger(files): - return sorted(ESP8266_NATIVE_TEST_COMPONENTS) - - return sorted(ESP8266_NATIVE_TEST_COMPONENTS & _changed_components_closure(files)) + return _native_components_to_test( + branch, ESP8266_NATIVE_TEST_COMPONENTS, _esp8266_native_path_or_file_trigger + ) def determine_cpp_unit_tests( diff --git a/script/helpers.py b/script/helpers.py index 8132ee49e5..e811f8b520 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -3,6 +3,7 @@ from __future__ import annotations import ast from collections.abc import Callable from dataclasses import dataclass, field +import functools from functools import cache import hashlib import json @@ -1248,6 +1249,7 @@ def get_components_graph_cache_key() -> str: return hasher.hexdigest() +@functools.cache def create_components_graph() -> dict[str, list[str]]: """Create a graph of component dependencies (cached). diff --git a/tests/unit_tests/test_arduino8266_toolchain.py b/tests/unit_tests/test_arduino8266_toolchain.py index 7dbb2183e0..c24fda3b14 100644 --- a/tests/unit_tests/test_arduino8266_toolchain.py +++ b/tests/unit_tests/test_arduino8266_toolchain.py @@ -2,13 +2,13 @@ from __future__ import annotations +import os from pathlib import Path from unittest.mock import MagicMock, patch import pytest from esphome.arduino8266 import framework, toolchain -from esphome.build_helpers.pio_options import warn_ignored_platformio_options import esphome.config_validation as cv from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, @@ -51,14 +51,11 @@ def _paths(tmp_path: Path) -> framework.InstalledPaths: def test_path_getters(tmp_path: Path) -> None: assert toolchain.get_build_dir() == CORE.relative_pioenvs_path("test8266") assert toolchain.get_elf_path().name == "firmware.elf" - # Pin both suffix variants so the test passes on every host platform - with patch.object(toolchain, "_EXE_SUFFIX", ""): - assert toolchain.get_addr2line_path().name == "xtensa-lx106-elf-addr2line" - assert toolchain.get_objdump_path().name == "xtensa-lx106-elf-objdump" - assert toolchain.get_readelf_path().name == "xtensa-lx106-elf-readelf" - # Windows binutils carry the executable suffix - with patch.object(toolchain, "_EXE_SUFFIX", ".exe"): - assert toolchain.get_addr2line_path().name == "xtensa-lx106-elf-addr2line.exe" + # The framework accessor owns the layout and the Windows suffix + suffix = ".exe" if os.name == "nt" else "" + assert toolchain.get_addr2line_path().name == f"xtensa-lx106-elf-addr2line{suffix}" + assert toolchain.get_objdump_path().name == f"xtensa-lx106-elf-objdump{suffix}" + assert toolchain.get_readelf_path().name == f"xtensa-lx106-elf-readelf{suffix}" def test_run_compile_build_failure(tmp_path: Path) -> None: @@ -152,22 +149,22 @@ def test_parse_app_size(tmp_path: Path) -> None: ld = tmp_path / "eagle.flash.4m.ld" ld.write_text("MEMORY\n{\n irom0_0_seg : org = 0x40201010, len = 0xfeff0\n}\n") with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld): - assert toolchain._parse_app_size(tmp_path) == 0xFEFF0 + assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) == 0xFEFF0 ld.write_text("MEMORY { }\n") with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld): - assert toolchain._parse_app_size(tmp_path) is None + assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None # A zero-length segment is bad data, not a budget; warn and drop it ld.write_text("MEMORY\n{\n irom0_0_seg : org = 0x40201010, len = 0x0\n}\n") with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld): - assert toolchain._parse_app_size(tmp_path) is None + assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None with patch( "esphome.build_gen.arduino8266.get_flash_ld_path", return_value=tmp_path / "missing.ld", ): - assert toolchain._parse_app_size(tmp_path) is None + assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None def test_print_size_summary(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: @@ -179,7 +176,7 @@ def test_print_size_summary(tmp_path: Path, capsys: pytest.CaptureFixture[str]) ), patch.object(toolchain, "_parse_app_size", return_value=1044464), ): - toolchain._print_size_summary(tmp_path) + toolchain._print_size_summary(tmp_path, _paths(tmp_path)) out = capsys.readouterr().out # Exact PlatformIO shape so script/ci_memory_impact_extract.py can parse it assert "RAM: [==== ] 37.9% (used 31016 bytes from 81920 bytes)" in out @@ -197,7 +194,7 @@ def test_print_size_summary_no_app_size( ), patch.object(toolchain, "_parse_app_size", return_value=None), ): - toolchain._print_size_summary(tmp_path) + toolchain._print_size_summary(tmp_path, _paths(tmp_path)) out = capsys.readouterr().out # Both lines are skipped together: a RAM line without Flash would skew # CI's memory-impact sums across builds @@ -214,7 +211,7 @@ def test_print_size_summary_size_tool_failure( "run", return_value=MagicMock(returncode=1, stdout="", stderr="bad elf"), ): - toolchain._print_size_summary(tmp_path) + toolchain._print_size_summary(tmp_path, _paths(tmp_path)) assert capsys.readouterr().out == "" assert "Could not summarize firmware size" in caplog.text @@ -287,7 +284,7 @@ def test_print_size_summary_unparsable_section( "run", return_value=MagicMock(returncode=0, stdout=bad), ): - toolchain._print_size_summary(tmp_path) + toolchain._print_size_summary(tmp_path, _paths(tmp_path)) assert capsys.readouterr().out == "" assert "Unparsable size output" in caplog.text @@ -301,7 +298,7 @@ def test_print_size_summary_unparsable_section( ), patch.object(toolchain, "_parse_app_size", return_value=1044464), ): - toolchain._print_size_summary(tmp_path) + toolchain._print_size_summary(tmp_path, _paths(tmp_path)) assert "RAM:" in capsys.readouterr().out assert "Unparsable size output" in caplog.text @@ -320,7 +317,7 @@ def test_print_size_summary_missing_section_skips_summary( "run", return_value=MagicMock(returncode=0, stdout=without_bss), ): - toolchain._print_size_summary(tmp_path) + toolchain._print_size_summary(tmp_path, _paths(tmp_path)) assert capsys.readouterr().out == "" assert "missing section(s) .bss" in caplog.text @@ -335,7 +332,7 @@ def test_warn_ignored_platformio_options(caplog: pytest.LogCaptureFixture) -> No "lib_ignore": ["Updater"], "upload_speed": "460800", } - warn_ignored_platformio_options(toolchain._CONSUMED_PIO_OPTIONS, "arduino") + toolchain._warn_ignored_platformio_options() assert "platformio_options->board_build.filesystem is ignored" in caplog.text assert "native 'arduino' toolchain" in caplog.text assert "board_build.ldscript is ignored" not in caplog.text diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3265da04e5..27b965f534 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -6944,7 +6944,7 @@ def test_command_run_rp2040_bootsel_redetects_serial_port() -> None: def test_command_idedata_esp_idf_prints_json(capsys: CaptureFixture) -> None: """Under the native ESP-IDF toolchain, idedata is emitted as JSON.""" - setup_core() + setup_core(platform=PLATFORM_ESP32) CORE.toolchain = Toolchain.ESP_IDF data = {"cxx_path": "g++", "prog_path": "/build/firmware.elf"} @@ -6958,7 +6958,7 @@ def test_command_idedata_esp_idf_prints_json(capsys: CaptureFixture) -> None: def test_command_idedata_esp_idf_no_build_errors() -> None: """Under ESP-IDF, a missing build (no idedata) returns an error, not a crash.""" - setup_core() + setup_core(platform=PLATFORM_ESP32) CORE.toolchain = Toolchain.ESP_IDF with patch("esphome.espidf.toolchain.get_idedata", return_value=None):