From f326c3f04d327ffc67d84ce3be94d3a8775c8c79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 12:43:34 -0500 Subject: [PATCH] Move the shared helpers into a neutral build_helpers package --- esphome/build_helpers/__init__.py | 1 + .../{espidf => build_helpers}/extra_script.py | 0 esphome/{espidf => build_helpers}/idedata.py | 0 esphome/build_helpers/size_summary.py | 15 ++ esphome/espidf/clang_tidy.py | 2 +- esphome/espidf/component.py | 2 +- esphome/espidf/size_summary.py | 14 +- esphome/espidf/toolchain.py | 2 +- script/determine-jobs.py | 2 +- .../analyze_memory/test_build_artifacts.py | 2 +- .../build_helpers/test_extra_script.py | 230 ++++++++++++++++++ .../test_idedata.py} | 4 +- tests/unit_tests/test_espidf_component.py | 212 ---------------- tests/unit_tests/test_espidf_toolchain.py | 14 +- tests/unit_tests/test_size_summary.py | 2 +- 15 files changed, 263 insertions(+), 239 deletions(-) create mode 100644 esphome/build_helpers/__init__.py rename esphome/{espidf => build_helpers}/extra_script.py (100%) rename esphome/{espidf => build_helpers}/idedata.py (100%) create mode 100644 esphome/build_helpers/size_summary.py create mode 100644 tests/unit_tests/build_helpers/test_extra_script.py rename tests/unit_tests/{test_espidf_idedata.py => build_helpers/test_idedata.py} (99%) diff --git a/esphome/build_helpers/__init__.py b/esphome/build_helpers/__init__.py new file mode 100644 index 0000000000..df956a2509 --- /dev/null +++ b/esphome/build_helpers/__init__.py @@ -0,0 +1 @@ +"""Build helpers shared by the native (non-PlatformIO) toolchains.""" diff --git a/esphome/espidf/extra_script.py b/esphome/build_helpers/extra_script.py similarity index 100% rename from esphome/espidf/extra_script.py rename to esphome/build_helpers/extra_script.py diff --git a/esphome/espidf/idedata.py b/esphome/build_helpers/idedata.py similarity index 100% rename from esphome/espidf/idedata.py rename to esphome/build_helpers/idedata.py diff --git a/esphome/build_helpers/size_summary.py b/esphome/build_helpers/size_summary.py new file mode 100644 index 0000000000..8014b9e527 --- /dev/null +++ b/esphome/build_helpers/size_summary.py @@ -0,0 +1,15 @@ +"""The PlatformIO-format size bar shared by the native toolchains.""" + +from __future__ import annotations + + +def format_bar(used: int, total: int) -> str: + """Match PlatformIO's ``_format_availale_bytes`` (pioupload.py) exactly.""" + pct_raw = used / total if total else 0 + blocks = 10 + filled = min(int(round(blocks * pct_raw)), blocks) + progress = "=" * filled + return ( + f"[{progress:<{blocks}}] {pct_raw: 6.1%} " + f"(used {used:d} bytes from {total:d} bytes)" + ) diff --git a/esphome/espidf/clang_tidy.py b/esphome/espidf/clang_tidy.py index c91db775a3..d60e544412 100644 --- a/esphome/espidf/clang_tidy.py +++ b/esphome/espidf/clang_tidy.py @@ -415,7 +415,7 @@ def _idedata_from_tidy_project(compile_commands: Path) -> dict: """ import json - from esphome.espidf.idedata import _get_toolchain_includes, _parse_entry + from esphome.build_helpers.idedata import _get_toolchain_includes, _parse_entry entries = json.loads(Path(compile_commands).read_text(encoding="utf-8")) entry = next((e for e in entries if e["file"].endswith("tidy.cpp")), None) diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py index 68c9398e19..4847c834f0 100644 --- a/esphome/espidf/component.py +++ b/esphome/espidf/component.py @@ -43,8 +43,8 @@ def _idf_framework() -> str: def _apply_extra_script(component: IDFComponent) -> None: + from esphome.build_helpers.extra_script import apply_extra_script from esphome.components.esp32 import get_esp32_variant - from esphome.espidf.extra_script import apply_extra_script apply_extra_script(component, lambda: variant_to_idf_target(get_esp32_variant())) diff --git a/esphome/espidf/size_summary.py b/esphome/espidf/size_summary.py index 9af2dcc9e4..64f8edb98e 100644 --- a/esphome/espidf/size_summary.py +++ b/esphome/espidf/size_summary.py @@ -28,6 +28,8 @@ import json import logging from pathlib import Path +from esphome.build_helpers.size_summary import format_bar + _LOGGER = logging.getLogger(__name__) _SIZE_SUFFIXES = {"K": 1024, "M": 1024 * 1024} @@ -67,18 +69,6 @@ def _find_app_partition_size(partitions_csv: Path) -> int: raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}") -def format_bar(used: int, total: int) -> str: - """Match PlatformIO's ``_format_availale_bytes`` (pioupload.py) exactly.""" - pct_raw = used / total if total else 0 - blocks = 10 - filled = min(int(round(blocks * pct_raw)), blocks) - progress = "=" * filled - return ( - f"[{progress:<{blocks}}] {pct_raw: 6.1%} " - f"(used {used:d} bytes from {total:d} bytes)" - ) - - def print_summary(size_json: Path, partitions_csv: Path | None) -> None: """Print PlatformIO-shaped RAM and Flash one-liners. diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index c21a2ad716..3c5c4803c2 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -526,7 +526,7 @@ def get_idedata() -> dict | None: idedata fields IDE integrations and clang-tidy expect, cached alongside the PlatformIO idedata path. Returns None if the compile DB doesn't exist yet. """ - from esphome.espidf.idedata import load_or_build_idedata + from esphome.build_helpers.idedata import load_or_build_idedata # No launcher: CMake excludes CMAKE__COMPILER_LAUNCHER (ccache) # from the exported compile database, unlike ninja's compdb dump. diff --git a/script/determine-jobs.py b/script/determine-jobs.py index e4d002975c..910db9525e 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -530,7 +530,7 @@ def _esp32_platformio_path_or_file_trigger(files: list[str]) -> bool: # components, so the component matrix wouldn't otherwise force any esp32 # compile. When they change we fold the `esp32` component into the matrix so # the default native-IDF build path is still compiled on an infra-only PR. -ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES = ("esphome/espidf/",) +ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES = ("esphome/espidf/", "esphome/build_helpers/") ESP_IDF_INFRA_TRIGGER_FILES = frozenset({"esphome/build_gen/espidf.py"}) diff --git a/tests/unit_tests/analyze_memory/test_build_artifacts.py b/tests/unit_tests/analyze_memory/test_build_artifacts.py index d97ee94e1c..ed37858b86 100644 --- a/tests/unit_tests/analyze_memory/test_build_artifacts.py +++ b/tests/unit_tests/analyze_memory/test_build_artifacts.py @@ -9,7 +9,7 @@ from esphome.analyze_memory.toolchain import ( find_idedata_path, idedata_candidates, ) -from esphome.espidf.idedata import _cc_path_from_cxx +from esphome.build_helpers.idedata import _cc_path_from_cxx from esphome.platformio.toolchain import IDEData diff --git a/tests/unit_tests/build_helpers/test_extra_script.py b/tests/unit_tests/build_helpers/test_extra_script.py new file mode 100644 index 0000000000..e99921df51 --- /dev/null +++ b/tests/unit_tests/build_helpers/test_extra_script.py @@ -0,0 +1,230 @@ +"""Tests for the shared extraScript machinery (build_helpers.extra_script).""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from esphome.platformio.library import ConvertedLibrary as IDFComponent, URLSource + + +def test_extra_script_captures_libpath_libs_and_defines(tmp_path): + from esphome.build_helpers.extra_script import ( + captured_as_build_flags, + run_extra_script, + ) + + (tmp_path / "src" / "esp32").mkdir(parents=True) + script = tmp_path / "extra_script.py" + script.write_text( + "Import('env')\n" + "mcu = env.get('BOARD_MCU')\n" + "env.Append(\n" + " LIBPATH=[join('src', mcu)],\n" + " LIBS=['algobsec'],\n" + " CPPDEFINES=['FOO', ('BAR', '1')],\n" + " LINKFLAGS=['-Wl,--gc-sections'],\n" + ")\n" + ) + # The script uses bare ``join`` (PIO's extra-scripts run inside SCons + # where this is in scope). Inject it via the script header so the + # shim's exec namespace can resolve it. + script.write_text("from os.path import join\n" + script.read_text()) + + result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32") + + assert result.libpath == [str(Path("src") / "esp32")] + assert result.libs == ["algobsec"] + assert ("BAR", "1") in result.cppdefines + assert "FOO" in result.cppdefines + assert result.linkflags == ["-Wl,--gc-sections"] + + flags = captured_as_build_flags(result, library_dir=tmp_path) + sep = os.sep + assert f"-Lsrc{sep}esp32" in flags + assert "-lalgobsec" in flags + assert "-DFOO" in flags + assert "-DBAR=1" in flags + assert "-Wl,--gc-sections" in flags + + +def test_extra_script_libpath_relative_resolves_against_library_dir( + tmp_path, monkeypatch +): + """Relative LIBPATH entries must resolve against ``library_dir``, not the + caller's CWD (the shim restores CWD before ``captured_as_build_flags`` + runs).""" + from esphome.build_helpers.extra_script import ( + ExtraScriptResult, + captured_as_build_flags, + ) + + (tmp_path / "lib" / "esp32").mkdir(parents=True) + elsewhere = tmp_path.parent / "not_the_library_dir" + elsewhere.mkdir(exist_ok=True) + monkeypatch.chdir(elsewhere) + + result = ExtraScriptResult(libpath=["lib/esp32"]) + flags = captured_as_build_flags(result, library_dir=tmp_path) + + sep = os.sep + assert flags == [f"-Llib{sep}esp32"] + + +def test_extra_script_libpath_absolute_outside_library_dir(tmp_path): + from esphome.build_helpers.extra_script import ( + ExtraScriptResult, + captured_as_build_flags, + ) + + outside = tmp_path.parent / "system_lib" + outside.mkdir(exist_ok=True) + result = ExtraScriptResult(libpath=[str(outside)]) + + flags = captured_as_build_flags(result, library_dir=tmp_path) + assert flags == [f"-L{outside.resolve()}"] + + +def test_extra_script_failure_returns_empty_result(tmp_path, caplog): + from esphome.build_helpers.extra_script import run_extra_script + + script = tmp_path / "broken.py" + script.write_text("raise RuntimeError('boom')\n") + + with caplog.at_level("WARNING"): + result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32") + + assert result.libpath == [] + assert result.libs == [] + assert "broken.py" in caplog.text + + +def test_apply_extra_script_path_traversal_is_rejected(tmp_path): + from esphome.espidf.component import _apply_extra_script + + library_dir = tmp_path / "lib" + library_dir.mkdir() + outside = tmp_path / "evil.py" + outside.write_text("env.Append(LIBS=['pwned'])\n") + + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = library_dir + c.data = {"build": {"extraScript": "../evil.py"}} + + _apply_extra_script(c) + + # Nothing was folded into flags: the traversal was rejected before + # the script could run. + assert "flags" not in c.data["build"] + + +def test_apply_extra_script_merges_into_existing_flags(tmp_path, monkeypatch): + from esphome.components import esp32 as esp32_module + + monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32") + + from esphome.espidf.component import _apply_extra_script + + (tmp_path / "src").mkdir() + script = tmp_path / "extra.py" + script.write_text("env.Append(LIBS=['algobsec'])\n") + + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}} + + _apply_extra_script(c) + + assert "-DEXISTING" in c.data["build"]["flags"] + assert "-lalgobsec" in c.data["build"]["flags"] + + +def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None: + """The shared helper resolves a callable idf_target lazily and normalizes + a string ``build.flags`` value into a list before extending it.""" + from esphome.build_helpers.extra_script import apply_extra_script + + (tmp_path / "src").mkdir() + script = tmp_path / "extra.py" + script.write_text("env.Append(LIBS=[env.get('BOARD_MCU')])\n") + + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "extra.py", "flags": "-DBASE=1"}} + + apply_extra_script(c, lambda: "esp8266") + + assert c.data["build"]["flags"] == ["-DBASE=1", "-lesp8266"] + + +def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None: + from esphome.build_helpers.extra_script import apply_extra_script + + # No extraScript declared: nothing happens, the target is never resolved + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {}} + apply_extra_script(c, lambda: pytest.fail("target resolved without a script")) + + # A script that captures nothing leaves the flags untouched + script = tmp_path / "noop.py" + script.write_text("pass\n") + c.data = {"build": {"extraScript": "noop.py"}} + apply_extra_script(c, "esp8266") + assert "flags" not in c.data["build"] + + +def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path) -> None: + """Un-captured env vars and unsupported env methods are silent no-ops.""" + from esphome.build_helpers.extra_script import apply_extra_script + + script = tmp_path / "extra.py" + script.write_text( + "env.Replace(CC='clang')\nenv.Append(UNCAPTURED=['x'], LIBS='single')\n" + ) + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "extra.py"}} + apply_extra_script(c, "esp8266") + assert c.data["build"]["flags"] == ["-lsingle"] + + +def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None: + """A raising extra-script is best-effort: logged and skipped.""" + from esphome.build_helpers.extra_script import apply_extra_script + + script = tmp_path / "extra.py" + script.write_text("raise RuntimeError('boom')\n") + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "extra.py"}} + apply_extra_script(c, "esp8266") + assert "flags" not in c.data["build"] + assert "skipping" in caplog.text + + +def test_apply_extra_script_pio_platform(tmp_path) -> None: + """The backend's platform token is exposed to the script as PIOPLATFORM.""" + from esphome.build_helpers.extra_script import apply_extra_script + + script = tmp_path / "extra.py" + script.write_text("env.Append(LIBS=[env.get('PIOPLATFORM')])\n") + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "extra.py"}} + apply_extra_script(c, "esp8266", pio_platform="espressif8266") + assert c.data["build"]["flags"] == ["-lespressif8266"] + + +def test_apply_extra_script_missing_script_logged(tmp_path, caplog) -> None: + """A declared but absent extraScript is skipped with a visible warning: + its captured link flags are lost.""" + from esphome.build_helpers.extra_script import apply_extra_script + + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "nope.py"}} + apply_extra_script(c, "esp8266") + assert "not found" in caplog.text diff --git a/tests/unit_tests/test_espidf_idedata.py b/tests/unit_tests/build_helpers/test_idedata.py similarity index 99% rename from tests/unit_tests/test_espidf_idedata.py rename to tests/unit_tests/build_helpers/test_idedata.py index 6d218f09b7..c82feb4790 100644 --- a/tests/unit_tests/test_espidf_idedata.py +++ b/tests/unit_tests/build_helpers/test_idedata.py @@ -1,4 +1,4 @@ -"""Tests for esphome.espidf.idedata (compile_commands.json -> idedata).""" +"""Tests for esphome.build_helpers.idedata (compile_commands.json -> idedata).""" # pylint: disable=protected-access @@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch import pytest -from esphome.espidf import idedata +from esphome.build_helpers import idedata # An absolute, forward-slash (shlex-safe) path prefix valid on the host OS, so # tests exercise the same is-absolute / normalize behavior as a real compile DB diff --git a/tests/unit_tests/test_espidf_component.py b/tests/unit_tests/test_espidf_component.py index 92b13495c8..0fd09d80dc 100644 --- a/tests/unit_tests/test_espidf_component.py +++ b/tests/unit_tests/test_espidf_component.py @@ -1,7 +1,6 @@ import glob import hashlib import json -import os from pathlib import Path from unittest.mock import MagicMock @@ -369,128 +368,6 @@ def test_generate_idf_component_yml_missing_path_raises(tmp_component): generate_idf_component_yml(tmp_component) -def test_extra_script_captures_libpath_libs_and_defines(tmp_path): - from esphome.espidf.extra_script import captured_as_build_flags, run_extra_script - - (tmp_path / "src" / "esp32").mkdir(parents=True) - script = tmp_path / "extra_script.py" - script.write_text( - "Import('env')\n" - "mcu = env.get('BOARD_MCU')\n" - "env.Append(\n" - " LIBPATH=[join('src', mcu)],\n" - " LIBS=['algobsec'],\n" - " CPPDEFINES=['FOO', ('BAR', '1')],\n" - " LINKFLAGS=['-Wl,--gc-sections'],\n" - ")\n" - ) - # The script uses bare ``join`` (PIO's extra-scripts run inside SCons - # where this is in scope). Inject it via the script header so the - # shim's exec namespace can resolve it. - script.write_text("from os.path import join\n" + script.read_text()) - - result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32") - - assert result.libpath == [str(Path("src") / "esp32")] - assert result.libs == ["algobsec"] - assert ("BAR", "1") in result.cppdefines - assert "FOO" in result.cppdefines - assert result.linkflags == ["-Wl,--gc-sections"] - - flags = captured_as_build_flags(result, library_dir=tmp_path) - sep = os.sep - assert f"-Lsrc{sep}esp32" in flags - assert "-lalgobsec" in flags - assert "-DFOO" in flags - assert "-DBAR=1" in flags - assert "-Wl,--gc-sections" in flags - - -def test_extra_script_libpath_relative_resolves_against_library_dir( - tmp_path, monkeypatch -): - """Relative LIBPATH entries must resolve against ``library_dir``, not the - caller's CWD (the shim restores CWD before ``captured_as_build_flags`` - runs).""" - from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags - - (tmp_path / "lib" / "esp32").mkdir(parents=True) - elsewhere = tmp_path.parent / "not_the_library_dir" - elsewhere.mkdir(exist_ok=True) - monkeypatch.chdir(elsewhere) - - result = ExtraScriptResult(libpath=["lib/esp32"]) - flags = captured_as_build_flags(result, library_dir=tmp_path) - - sep = os.sep - assert flags == [f"-Llib{sep}esp32"] - - -def test_extra_script_libpath_absolute_outside_library_dir(tmp_path): - from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags - - outside = tmp_path.parent / "system_lib" - outside.mkdir(exist_ok=True) - result = ExtraScriptResult(libpath=[str(outside)]) - - flags = captured_as_build_flags(result, library_dir=tmp_path) - assert flags == [f"-L{outside.resolve()}"] - - -def test_extra_script_failure_returns_empty_result(tmp_path, caplog): - from esphome.espidf.extra_script import run_extra_script - - script = tmp_path / "broken.py" - script.write_text("raise RuntimeError('boom')\n") - - with caplog.at_level("WARNING"): - result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32") - - assert result.libpath == [] - assert result.libs == [] - assert "broken.py" in caplog.text - - -def test_apply_extra_script_path_traversal_is_rejected(tmp_path): - from esphome.espidf.component import _apply_extra_script - - library_dir = tmp_path / "lib" - library_dir.mkdir() - outside = tmp_path / "evil.py" - outside.write_text("env.Append(LIBS=['pwned'])\n") - - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = library_dir - c.data = {"build": {"extraScript": "../evil.py"}} - - _apply_extra_script(c) - - # Nothing was folded into flags: the traversal was rejected before - # the script could run. - assert "flags" not in c.data["build"] - - -def test_apply_extra_script_merges_into_existing_flags(tmp_path, monkeypatch): - from esphome.components import esp32 as esp32_module - - monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32") - - from esphome.espidf.component import _apply_extra_script - - (tmp_path / "src").mkdir() - script = tmp_path / "extra.py" - script.write_text("env.Append(LIBS=['algobsec'])\n") - - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = tmp_path - c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}} - - _apply_extra_script(c) - - assert "-DEXISTING" in c.data["build"]["flags"] - assert "-lalgobsec" in c.data["build"]["flags"] - - def test_parse_library_json(tmp_path): f = tmp_path / "library.json" f.write_text(json.dumps({"name": "test"})) @@ -1190,92 +1067,3 @@ def test_idf_component_download_passes_salt() -> None: "owner/name", force=True, salt="abcd1234", namespace="idf" ) assert c.path == Path("/converted/owner/name") - - -def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None: - """The shared helper resolves a callable idf_target lazily and normalizes - a string ``build.flags`` value into a list before extending it.""" - from esphome.espidf.extra_script import apply_extra_script - - (tmp_path / "src").mkdir() - script = tmp_path / "extra.py" - script.write_text("env.Append(LIBS=[env.get('BOARD_MCU')])\n") - - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = tmp_path - c.data = {"build": {"extraScript": "extra.py", "flags": "-DBASE=1"}} - - apply_extra_script(c, lambda: "esp8266") - - assert c.data["build"]["flags"] == ["-DBASE=1", "-lesp8266"] - - -def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None: - from esphome.espidf.extra_script import apply_extra_script - - # No extraScript declared: nothing happens, the target is never resolved - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = tmp_path - c.data = {"build": {}} - apply_extra_script(c, lambda: pytest.fail("target resolved without a script")) - - # A script that captures nothing leaves the flags untouched - script = tmp_path / "noop.py" - script.write_text("pass\n") - c.data = {"build": {"extraScript": "noop.py"}} - apply_extra_script(c, "esp8266") - assert "flags" not in c.data["build"] - - -def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path) -> None: - """Un-captured env vars and unsupported env methods are silent no-ops.""" - from esphome.espidf.extra_script import apply_extra_script - - script = tmp_path / "extra.py" - script.write_text( - "env.Replace(CC='clang')\nenv.Append(UNCAPTURED=['x'], LIBS='single')\n" - ) - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = tmp_path - c.data = {"build": {"extraScript": "extra.py"}} - apply_extra_script(c, "esp8266") - assert c.data["build"]["flags"] == ["-lsingle"] - - -def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None: - """A raising extra-script is best-effort: logged and skipped.""" - from esphome.espidf.extra_script import apply_extra_script - - script = tmp_path / "extra.py" - script.write_text("raise RuntimeError('boom')\n") - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = tmp_path - c.data = {"build": {"extraScript": "extra.py"}} - apply_extra_script(c, "esp8266") - assert "flags" not in c.data["build"] - assert "skipping" in caplog.text - - -def test_apply_extra_script_pio_platform(tmp_path) -> None: - """The backend's platform token is exposed to the script as PIOPLATFORM.""" - from esphome.espidf.extra_script import apply_extra_script - - script = tmp_path / "extra.py" - script.write_text("env.Append(LIBS=[env.get('PIOPLATFORM')])\n") - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = tmp_path - c.data = {"build": {"extraScript": "extra.py"}} - apply_extra_script(c, "esp8266", pio_platform="espressif8266") - assert c.data["build"]["flags"] == ["-lespressif8266"] - - -def test_apply_extra_script_missing_script_logged(tmp_path, caplog) -> None: - """A declared but absent extraScript is skipped with a visible warning: - its captured link flags are lost.""" - from esphome.espidf.extra_script import apply_extra_script - - c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) - c.path = tmp_path - c.data = {"build": {"extraScript": "nope.py"}} - apply_extra_script(c, "esp8266") - assert "not found" in caplog.text diff --git a/tests/unit_tests/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 2556397aef..a65bd0a21b 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -140,7 +140,7 @@ def test_get_idedata_generates_and_caches(setup_core: Path) -> None: compile_commands.write_text("[]") with patch( - "esphome.espidf.idedata.idedata_from_build", + "esphome.build_helpers.idedata.idedata_from_build", return_value={"cxx_path": "g++"}, ) as mock_transform: result = toolchain.get_idedata() @@ -161,7 +161,7 @@ def test_get_idedata_uses_cache_when_valid(setup_core: Path) -> None: cc_mtime = compile_commands.stat().st_mtime os.utime(cache, (cc_mtime + 1, cc_mtime + 1)) - with patch("esphome.espidf.idedata.idedata_from_build") as mock_transform: + with patch("esphome.build_helpers.idedata.idedata_from_build") as mock_transform: result = toolchain.get_idedata() mock_transform.assert_not_called() @@ -183,7 +183,7 @@ def test_get_idedata_regenerates_cache_without_cc_path(setup_core: Path) -> None os.utime(cache, (cc_mtime + 1, cc_mtime + 1)) with patch( - "esphome.espidf.idedata.idedata_from_build", + "esphome.build_helpers.idedata.idedata_from_build", return_value={"cc_path": "gcc", "cxx_path": "g++"}, ) as mock_transform: result = toolchain.get_idedata() @@ -203,7 +203,7 @@ def test_get_idedata_regenerates_when_compile_commands_newer(setup_core: Path) - os.utime(compile_commands, (cache_mtime + 1, cache_mtime + 1)) with patch( - "esphome.espidf.idedata.idedata_from_build", + "esphome.build_helpers.idedata.idedata_from_build", return_value={"cxx_path": "fresh"}, ) as mock_transform: result = toolchain.get_idedata() @@ -230,7 +230,7 @@ def test_get_idedata_regenerates_on_non_dict_cache( os.utime(cache, (cc_mtime + 1, cc_mtime + 1)) with patch( - "esphome.espidf.idedata.idedata_from_build", + "esphome.build_helpers.idedata.idedata_from_build", return_value={"cc_path": "gcc", "cxx_path": "g++"}, ) as mock_transform: result = toolchain.get_idedata() @@ -250,7 +250,7 @@ def test_get_idedata_regenerates_on_corrupted_cache(setup_core: Path) -> None: os.utime(cache, (cc_mtime + 1, cc_mtime + 1)) with patch( - "esphome.espidf.idedata.idedata_from_build", + "esphome.build_helpers.idedata.idedata_from_build", return_value={"cxx_path": "regen"}, ) as mock_transform: result = toolchain.get_idedata() @@ -267,7 +267,7 @@ def test_get_idedata_prog_path_points_at_firmware_elf(setup_core: Path) -> None: compile_commands.write_text("[]") with patch( - "esphome.espidf.idedata.idedata_from_build", + "esphome.build_helpers.idedata.idedata_from_build", return_value={"cxx_path": "g++"}, ): result = toolchain.get_idedata() diff --git a/tests/unit_tests/test_size_summary.py b/tests/unit_tests/test_size_summary.py index e65195aab5..37ec17f8fa 100644 --- a/tests/unit_tests/test_size_summary.py +++ b/tests/unit_tests/test_size_summary.py @@ -130,7 +130,7 @@ def test_print_summary_handles_no_memory_types( def test_format_bar_zero_total() -> None: """A zero total must not divide by zero.""" - from esphome.espidf.size_summary import format_bar + from esphome.build_helpers.size_summary import format_bar assert format_bar(0, 0) == "[ ] 0.0% (used 0 bytes from 0 bytes)"