Move the shared helpers into a neutral build_helpers package

This commit is contained in:
J. Nick Koston
2026-08-20 12:43:34 -05:00
parent 3b51ec2ae7
commit f326c3f04d
15 changed files with 263 additions and 239 deletions
+1
View File
@@ -0,0 +1 @@
"""Build helpers shared by the native (non-PlatformIO) toolchains."""
+15
View File
@@ -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)"
)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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()))
+2 -12
View File
@@ -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.
+1 -1
View File
@@ -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_<LANG>_COMPILER_LAUNCHER (ccache)
# from the exported compile database, unlike ninja's compdb dump.
+1 -1
View File
@@ -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"})
@@ -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
@@ -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
@@ -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
-212
View File
@@ -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
+7 -7
View File
@@ -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()
+1 -1
View File
@@ -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)"