mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
Merge branch 'esp8266-native-shared-helpers' into esp8266-native-library-backend
This commit is contained in:
@@ -21,7 +21,7 @@ def _entry(directory: str, file: str, command: str) -> dict:
|
||||
return {"directory": directory, "file": file, "command": command}
|
||||
|
||||
|
||||
def test_parse_entry_extracts_fields() -> None:
|
||||
def testparse_entry_extracts_fields() -> None:
|
||||
"""cxx_path, defines, includes and remaining flags are split apart."""
|
||||
entry = _entry(
|
||||
f"{ABS}build",
|
||||
@@ -30,7 +30,7 @@ def test_parse_entry_extracts_fields() -> None:
|
||||
f"-I{ABS}inc/a -isystem {ABS}sys/b -std=gnu++20 -c app.cpp -o app.cpp.o",
|
||||
)
|
||||
|
||||
cxx_path, defines, includes, cxx_flags = idedata._parse_entry(entry)
|
||||
cxx_path, defines, includes, cxx_flags = idedata.parse_entry(entry)
|
||||
|
||||
assert cxx_path == "/tools/xtensa-esp32-elf-g++"
|
||||
assert "USE_ESP32" in defines
|
||||
@@ -45,7 +45,7 @@ def test_parse_entry_extracts_fields() -> None:
|
||||
assert "app.cpp.o" not in cxx_flags
|
||||
|
||||
|
||||
def test_parse_entry_space_separated_args() -> None:
|
||||
def testparse_entry_space_separated_args() -> None:
|
||||
"""``-D X`` / ``-I path`` (separate arg) and ``-isystem<path>`` (joined)."""
|
||||
entry = _entry(
|
||||
f"{ABS}build",
|
||||
@@ -53,14 +53,14 @@ def test_parse_entry_space_separated_args() -> None:
|
||||
f"g++ -D FOO=1 -I {ABS}inc/sep -isystem{ABS}sys/joined -c x.cpp",
|
||||
)
|
||||
|
||||
_, defines, includes, _ = idedata._parse_entry(entry)
|
||||
_, defines, includes, _ = idedata.parse_entry(entry)
|
||||
|
||||
assert "FOO=1" in defines
|
||||
assert f"{ABS}inc/sep" in includes
|
||||
assert f"{ABS}sys/joined" in includes
|
||||
|
||||
|
||||
def test_parse_entry_resolves_relative_includes() -> None:
|
||||
def testparse_entry_resolves_relative_includes() -> None:
|
||||
"""Relative includes are resolved against the entry's ``directory``."""
|
||||
directory = f"{ABS}build/proj"
|
||||
entry = _entry(
|
||||
@@ -69,10 +69,10 @@ def test_parse_entry_resolves_relative_includes() -> None:
|
||||
"g++ -Iconfig -I../shared -isystem rel/sys -c x.cpp",
|
||||
)
|
||||
|
||||
_, _, includes, _ = idedata._parse_entry(entry)
|
||||
_, _, includes, _ = idedata.parse_entry(entry)
|
||||
|
||||
def resolved(rel: str) -> str:
|
||||
# _parse_entry emits forward slashes for consistency (normpath would
|
||||
# parse_entry emits forward slashes for consistency (normpath would
|
||||
# yield backslashes on Windows).
|
||||
return os.path.normpath(Path(directory) / rel).replace("\\", "/")
|
||||
|
||||
@@ -83,7 +83,7 @@ def test_parse_entry_resolves_relative_includes() -> None:
|
||||
assert all(Path(inc).is_absolute() for inc in includes)
|
||||
|
||||
|
||||
def test_parse_entry_skips_dependency_flags() -> None:
|
||||
def testparse_entry_skips_dependency_flags() -> None:
|
||||
"""Dependency-generation flags (and their args) are dropped."""
|
||||
entry = _entry(
|
||||
"/build",
|
||||
@@ -91,7 +91,7 @@ def test_parse_entry_skips_dependency_flags() -> None:
|
||||
"g++ -MD -MT x.cpp.o -MF x.cpp.o.d -c x.cpp -o x.cpp.o",
|
||||
)
|
||||
|
||||
_, _, _, cxx_flags = idedata._parse_entry(entry)
|
||||
_, _, _, cxx_flags = idedata.parse_entry(entry)
|
||||
|
||||
for tok in ("-MD", "-MT", "x.cpp.o", "-MF", "x.cpp.o.d", "-c", "-o", "x.cpp"):
|
||||
assert tok not in cxx_flags
|
||||
@@ -198,17 +198,17 @@ def test_idedata_from_build(tmp_path: Path) -> None:
|
||||
assert data["includes"]["toolchain"] == ["/tc/inc/c++", "/tc/inc"]
|
||||
|
||||
|
||||
def test_get_toolchain_includes_raises_on_probe_failure() -> None:
|
||||
def testget_toolchain_includes_raises_on_probe_failure() -> None:
|
||||
"""A failed compiler probe is a hard error, not a silent empty list."""
|
||||
fake_proc = MagicMock(returncode=1, stderr="xtensa-esp32-elf-g++: not found")
|
||||
with (
|
||||
patch.object(idedata.subprocess, "run", return_value=fake_proc),
|
||||
pytest.raises(RuntimeError, match="builtin include dirs"),
|
||||
):
|
||||
idedata._get_toolchain_includes("/bad/compiler")
|
||||
idedata.get_toolchain_includes("/bad/compiler")
|
||||
|
||||
|
||||
def test_get_toolchain_includes_raises_when_no_dirs_found() -> None:
|
||||
def testget_toolchain_includes_raises_when_no_dirs_found() -> None:
|
||||
"""Markers present but no dirs (anomalous output) also raises."""
|
||||
fake_proc = MagicMock(
|
||||
returncode=0,
|
||||
@@ -218,7 +218,7 @@ def test_get_toolchain_includes_raises_when_no_dirs_found() -> None:
|
||||
patch.object(idedata.subprocess, "run", return_value=fake_proc),
|
||||
pytest.raises(RuntimeError, match="builtin include dirs"),
|
||||
):
|
||||
idedata._get_toolchain_includes("/some/compiler")
|
||||
idedata.get_toolchain_includes("/some/compiler")
|
||||
|
||||
|
||||
# ESP-IDF's compile_commands.json on Windows mixes literal backslash path
|
||||
@@ -248,7 +248,7 @@ def test_split_command_empty_returns_empty() -> None:
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "nt", reason="Windows argv tokenization")
|
||||
def test_parse_entry_normalizes_windows_cxx_path() -> None:
|
||||
def testparse_entry_normalizes_windows_cxx_path() -> None:
|
||||
"""A backslash compiler path is emitted forward-slashed; define unescaped."""
|
||||
entry = _entry(
|
||||
r"C:\b",
|
||||
@@ -256,7 +256,7 @@ def test_parse_entry_normalizes_windows_cxx_path() -> None:
|
||||
r"C:\esp\bin\g++.exe -DVER=\"1.2.3\" -IC:/inc/a -c x.cpp",
|
||||
)
|
||||
|
||||
cxx_path, defines, includes, _ = idedata._parse_entry(entry)
|
||||
cxx_path, defines, includes, _ = idedata.parse_entry(entry)
|
||||
|
||||
assert cxx_path == "C:/esp/bin/g++.exe"
|
||||
assert "\\" not in cxx_path
|
||||
@@ -264,7 +264,7 @@ def test_parse_entry_normalizes_windows_cxx_path() -> None:
|
||||
assert "C:/inc/a" in includes
|
||||
|
||||
|
||||
def test_parse_entry_strips_launcher_prefix() -> None:
|
||||
def testparse_entry_strips_launcher_prefix() -> None:
|
||||
"""A launcher-wrapped compile names the compiler second; the exact
|
||||
configured launcher is stripped, not anything ccache-shaped."""
|
||||
entry = _entry(
|
||||
@@ -273,18 +273,18 @@ def test_parse_entry_strips_launcher_prefix() -> None:
|
||||
"/opt/homebrew/bin/ccache /tools/xtensa-lx106-elf-g++ -DUSE_ESP8266 "
|
||||
"-c app.cpp -o app.cpp.o",
|
||||
)
|
||||
cxx_path, defines, _, _ = idedata._parse_entry(
|
||||
cxx_path, defines, _, _ = idedata.parse_entry(
|
||||
entry, launcher="/opt/homebrew/bin/ccache"
|
||||
)
|
||||
assert cxx_path == "/tools/xtensa-lx106-elf-g++"
|
||||
assert defines == ["USE_ESP8266"]
|
||||
# Without a configured launcher nothing is stripped, even a token that
|
||||
# happens to be named ccache -- but the surprise is warned about
|
||||
cxx_path, _, _, _ = idedata._parse_entry(entry)
|
||||
cxx_path, _, _, _ = idedata.parse_entry(entry)
|
||||
assert cxx_path == "/opt/homebrew/bin/ccache"
|
||||
|
||||
|
||||
def test_parse_entry_warns_when_first_token_is_not_a_compiler(
|
||||
def testparse_entry_warns_when_first_token_is_not_a_compiler(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
entry = _entry(
|
||||
@@ -292,10 +292,10 @@ def test_parse_entry_warns_when_first_token_is_not_a_compiler(
|
||||
f"{ABS}build/src/esphome/core/application.cpp",
|
||||
"/opt/homebrew/bin/ccache /tools/xtensa-lx106-elf-g++ -c a.cpp -o a.o",
|
||||
)
|
||||
idedata._parse_entry(entry)
|
||||
idedata.parse_entry(entry)
|
||||
assert "does not start with a compiler" in caplog.text
|
||||
caplog.clear()
|
||||
idedata._parse_entry(entry, launcher="/opt/homebrew/bin/ccache")
|
||||
idedata.parse_entry(entry, launcher="/opt/homebrew/bin/ccache")
|
||||
assert "does not start with a compiler" not in caplog.text
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ def test_load_or_build_idedata_builds_and_caches(tmp_path: Path) -> None:
|
||||
compile_commands = _write_compile_commands(tmp_path)
|
||||
cache = tmp_path / "cache" / "test.json"
|
||||
with patch.object(
|
||||
idedata, "_get_toolchain_includes", return_value=["/toolchain/include"]
|
||||
idedata, "get_toolchain_includes", return_value=["/toolchain/include"]
|
||||
):
|
||||
data = idedata.load_or_build_idedata(
|
||||
compile_commands, tmp_path / "firmware.elf", cache
|
||||
@@ -355,8 +355,39 @@ def test_load_or_build_idedata_rebuilds_bad_cache(tmp_path: Path) -> None:
|
||||
for bad in ("not json", json.dumps({"no_cc_path": True})):
|
||||
cache.write_text(bad)
|
||||
os.utime(cache, (compile_commands.stat().st_mtime + 10,) * 2)
|
||||
with patch.object(idedata, "_get_toolchain_includes", return_value=[]):
|
||||
with patch.object(idedata, "get_toolchain_includes", return_value=[]):
|
||||
data = idedata.load_or_build_idedata(
|
||||
compile_commands, tmp_path / "f.elf", cache
|
||||
)
|
||||
assert "cc_path" in data
|
||||
|
||||
|
||||
def test_load_or_build_idedata_rebuilds_when_compile_db_newer(tmp_path: Path) -> None:
|
||||
"""A compile DB newer than the cache forces regeneration."""
|
||||
compile_commands = _write_compile_commands(tmp_path)
|
||||
cache = tmp_path / "cache.json"
|
||||
cache.write_text(json.dumps({"cc_path": "stale"}))
|
||||
os.utime(compile_commands, (cache.stat().st_mtime + 10,) * 2)
|
||||
with patch.object(idedata, "get_toolchain_includes", return_value=[]):
|
||||
data = idedata.load_or_build_idedata(
|
||||
compile_commands, tmp_path / "f.elf", cache
|
||||
)
|
||||
assert data["cc_path"] != "stale"
|
||||
|
||||
|
||||
def test_load_or_build_idedata_rebuilds_non_dict_cache(tmp_path: Path) -> None:
|
||||
"""Valid JSON that is not an object is regenerated, never handed out.
|
||||
|
||||
A bare string would otherwise pass the cc_path check by substring.
|
||||
"""
|
||||
compile_commands = _write_compile_commands(tmp_path)
|
||||
cache = tmp_path / "cache.json"
|
||||
for bad in ('"cc_path is a string"', "[]", "42"):
|
||||
cache.write_text(bad)
|
||||
os.utime(cache, (compile_commands.stat().st_mtime + 10,) * 2)
|
||||
with patch.object(idedata, "get_toolchain_includes", return_value=[]):
|
||||
data = idedata.load_or_build_idedata(
|
||||
compile_commands, tmp_path / "f.elf", cache
|
||||
)
|
||||
assert isinstance(data, dict)
|
||||
assert "cc_path" in data
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Tests for the shared PlatformIO-format size bar."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from esphome.build_helpers.size_summary import format_bar
|
||||
|
||||
|
||||
def test_format_bar_zero_total() -> None:
|
||||
"""A zero total must not divide by zero."""
|
||||
assert format_bar(0, 0) == "[ ] 0.0% (used 0 bytes from 0 bytes)"
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Tests for esphome.espidf.clang_tidy tidy-project generation."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.espidf import clang_tidy
|
||||
from esphome.espidf.clang_tidy import _Settings, _setup_core, _write_tidy_project
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -68,11 +71,6 @@ def test_setup_core_sets_arduino_env(
|
||||
|
||||
def test_idedata_from_tidy_project(tmp_path) -> None:
|
||||
"""The tidy TU's compile entry is assembled into consumer-shaped idedata."""
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from esphome.espidf import clang_tidy
|
||||
|
||||
compile_commands = tmp_path / "compile_commands.json"
|
||||
compile_commands.write_text(
|
||||
json.dumps(
|
||||
@@ -87,7 +85,7 @@ def test_idedata_from_tidy_project(tmp_path) -> None:
|
||||
)
|
||||
)
|
||||
with patch(
|
||||
"esphome.espidf.clang_tidy._get_toolchain_includes", return_value=["/tc/inc"]
|
||||
"esphome.espidf.clang_tidy.get_toolchain_includes", return_value=["/tc/inc"]
|
||||
):
|
||||
data = clang_tidy._idedata_from_tidy_project(compile_commands)
|
||||
assert data["cxx_path"] == "/tc/xtensa-esp32-elf-g++"
|
||||
@@ -97,10 +95,6 @@ def test_idedata_from_tidy_project(tmp_path) -> None:
|
||||
|
||||
|
||||
def test_idedata_from_tidy_project_missing_tu_raises(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
from esphome.espidf import clang_tidy
|
||||
|
||||
compile_commands = tmp_path / "compile_commands.json"
|
||||
compile_commands.write_text(json.dumps([]))
|
||||
with pytest.raises(RuntimeError, match="tidy.cpp not found"):
|
||||
|
||||
@@ -1067,3 +1067,19 @@ 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_wrapper_wires_esp32_target(tmp_path, monkeypatch):
|
||||
"""The espidf wrapper resolves the esp32 variant into the shared helper."""
|
||||
from esphome.components import esp32 as esp32_module
|
||||
from esphome.espidf.component import _apply_extra_script
|
||||
|
||||
monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32")
|
||||
(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"}}
|
||||
_apply_extra_script(c)
|
||||
assert c.data["build"]["flags"] == ["-lesp32"]
|
||||
|
||||
@@ -151,114 +151,6 @@ def test_get_idedata_generates_and_caches(setup_core: Path) -> None:
|
||||
assert json.loads(cache.read_text()) == {"cxx_path": "g++", "prog_path": prog_path}
|
||||
|
||||
|
||||
def test_get_idedata_uses_cache_when_valid(setup_core: Path) -> None:
|
||||
"""A cache at least as new as the compile DB is reused without regenerating."""
|
||||
compile_commands, cache = _setup_build(setup_core)
|
||||
compile_commands.parent.mkdir(parents=True, exist_ok=True)
|
||||
compile_commands.write_text("[]")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_text('{"cc_path": "cached-gcc", "cxx_path": "cached"}')
|
||||
cc_mtime = compile_commands.stat().st_mtime
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch("esphome.build_helpers.idedata.idedata_from_build") as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
|
||||
mock_transform.assert_not_called()
|
||||
assert result == {"cc_path": "cached-gcc", "cxx_path": "cached"}
|
||||
|
||||
|
||||
def test_get_idedata_regenerates_cache_without_cc_path(setup_core: Path) -> None:
|
||||
"""A cache predating cc_path is rebuilt even though it is newer.
|
||||
|
||||
Such a cache stays newer than the compile DB forever, so consumers that
|
||||
derive the binutils paths from cc_path would keep failing on it.
|
||||
"""
|
||||
compile_commands, cache = _setup_build(setup_core)
|
||||
compile_commands.parent.mkdir(parents=True, exist_ok=True)
|
||||
compile_commands.write_text("[]")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_text('{"cxx_path": "cached"}')
|
||||
cc_mtime = compile_commands.stat().st_mtime
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cc_path": "gcc", "cxx_path": "g++"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
|
||||
mock_transform.assert_called_once()
|
||||
assert result["cc_path"] == "gcc"
|
||||
|
||||
|
||||
def test_get_idedata_regenerates_when_compile_commands_newer(setup_core: Path) -> None:
|
||||
"""A compile DB newer than the cache forces regeneration."""
|
||||
compile_commands, cache = _setup_build(setup_core)
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_text('{"cxx_path": "stale"}')
|
||||
compile_commands.parent.mkdir(parents=True, exist_ok=True)
|
||||
compile_commands.write_text("[]")
|
||||
cache_mtime = cache.stat().st_mtime
|
||||
os.utime(compile_commands, (cache_mtime + 1, cache_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cxx_path": "fresh"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
|
||||
mock_transform.assert_called_once()
|
||||
assert result == {"cxx_path": "fresh", "prog_path": str(toolchain.get_elf_path())}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cached", ['"cc_path is a string"', "[]", "42"])
|
||||
def test_get_idedata_regenerates_on_non_dict_cache(
|
||||
setup_core: Path, cached: str
|
||||
) -> None:
|
||||
"""A newer cache holding valid JSON that is not an object is regenerated.
|
||||
|
||||
A bare string would otherwise pass the cc_path check by substring and be
|
||||
handed to consumers expecting a dict.
|
||||
"""
|
||||
compile_commands, cache = _setup_build(setup_core)
|
||||
compile_commands.parent.mkdir(parents=True, exist_ok=True)
|
||||
compile_commands.write_text("[]")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_text(cached)
|
||||
cc_mtime = compile_commands.stat().st_mtime
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cc_path": "gcc", "cxx_path": "g++"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
|
||||
mock_transform.assert_called_once()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
def test_get_idedata_regenerates_on_corrupted_cache(setup_core: Path) -> None:
|
||||
"""An unparseable (but newer) cache falls back to regeneration."""
|
||||
compile_commands, cache = _setup_build(setup_core)
|
||||
compile_commands.parent.mkdir(parents=True, exist_ok=True)
|
||||
compile_commands.write_text("[]")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_text("{not json")
|
||||
cc_mtime = compile_commands.stat().st_mtime
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cxx_path": "regen"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
|
||||
mock_transform.assert_called_once()
|
||||
assert result == {"cxx_path": "regen", "prog_path": str(toolchain.get_elf_path())}
|
||||
|
||||
|
||||
def test_get_idedata_prog_path_points_at_firmware_elf(setup_core: Path) -> None:
|
||||
"""The idedata exposes prog_path (the ELF) so consumers like build-action
|
||||
can locate firmware.factory.bin / firmware.ota.bin as its siblings."""
|
||||
|
||||
+34
-30
@@ -1,4 +1,4 @@
|
||||
"""Tests for the shared extraScript machinery (build_helpers.extra_script)."""
|
||||
"""Tests for the shared extraScript machinery (platformio.extra_script)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,7 +11,7 @@ from esphome.platformio.library import ConvertedLibrary as IDFComponent, URLSour
|
||||
|
||||
|
||||
def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
|
||||
from esphome.build_helpers.extra_script import (
|
||||
from esphome.platformio.extra_script import (
|
||||
captured_as_build_flags,
|
||||
run_extra_script,
|
||||
)
|
||||
@@ -33,7 +33,9 @@ def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
|
||||
# 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")
|
||||
result = run_extra_script(
|
||||
script, library_dir=tmp_path, board_mcu="esp32", pio_platform="espressif32"
|
||||
)
|
||||
|
||||
assert result.libpath == [str(Path("src") / "esp32")]
|
||||
assert result.libs == ["algobsec"]
|
||||
@@ -56,7 +58,7 @@ def test_extra_script_libpath_relative_resolves_against_library_dir(
|
||||
"""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 (
|
||||
from esphome.platformio.extra_script import (
|
||||
ExtraScriptResult,
|
||||
captured_as_build_flags,
|
||||
)
|
||||
@@ -74,7 +76,7 @@ def test_extra_script_libpath_relative_resolves_against_library_dir(
|
||||
|
||||
|
||||
def test_extra_script_libpath_absolute_outside_library_dir(tmp_path):
|
||||
from esphome.build_helpers.extra_script import (
|
||||
from esphome.platformio.extra_script import (
|
||||
ExtraScriptResult,
|
||||
captured_as_build_flags,
|
||||
)
|
||||
@@ -88,13 +90,15 @@ def test_extra_script_libpath_absolute_outside_library_dir(tmp_path):
|
||||
|
||||
|
||||
def test_extra_script_failure_returns_empty_result(tmp_path, caplog):
|
||||
from esphome.build_helpers.extra_script import run_extra_script
|
||||
from esphome.platformio.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")
|
||||
result = run_extra_script(
|
||||
script, library_dir=tmp_path, board_mcu="esp32", pio_platform="espressif32"
|
||||
)
|
||||
|
||||
assert result.libpath == []
|
||||
assert result.libs == []
|
||||
@@ -102,7 +106,7 @@ def test_extra_script_failure_returns_empty_result(tmp_path, caplog):
|
||||
|
||||
|
||||
def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
|
||||
from esphome.espidf.component import _apply_extra_script
|
||||
from esphome.platformio.extra_script import apply_extra_script
|
||||
|
||||
library_dir = tmp_path / "lib"
|
||||
library_dir.mkdir()
|
||||
@@ -113,19 +117,15 @@ def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
|
||||
c.path = library_dir
|
||||
c.data = {"build": {"extraScript": "../evil.py"}}
|
||||
|
||||
_apply_extra_script(c)
|
||||
apply_extra_script(c, board_mcu="esp32", pio_platform="espressif32")
|
||||
|
||||
# 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
|
||||
def test_apply_extra_script_merges_into_existing_flags(tmp_path):
|
||||
from esphome.platformio.extra_script import apply_extra_script
|
||||
|
||||
(tmp_path / "src").mkdir()
|
||||
script = tmp_path / "extra.py"
|
||||
@@ -135,16 +135,16 @@ def test_apply_extra_script_merges_into_existing_flags(tmp_path, monkeypatch):
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}}
|
||||
|
||||
_apply_extra_script(c)
|
||||
apply_extra_script(c, board_mcu="esp32", pio_platform="espressif32")
|
||||
|
||||
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
|
||||
"""The shared helper resolves a callable board_mcu lazily and normalizes
|
||||
a string ``build.flags`` value into a list before extending it."""
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
from esphome.platformio.extra_script import apply_extra_script
|
||||
|
||||
(tmp_path / "src").mkdir()
|
||||
script = tmp_path / "extra.py"
|
||||
@@ -154,31 +154,35 @@ def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None:
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py", "flags": "-DBASE=1"}}
|
||||
|
||||
apply_extra_script(c, lambda: "esp8266")
|
||||
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
|
||||
|
||||
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
|
||||
from esphome.platformio.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"))
|
||||
apply_extra_script(
|
||||
c,
|
||||
board_mcu=lambda: pytest.fail("target resolved without a script"),
|
||||
pio_platform="espressif8266",
|
||||
)
|
||||
|
||||
# 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")
|
||||
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
|
||||
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
|
||||
from esphome.platformio.extra_script import apply_extra_script
|
||||
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text(
|
||||
@@ -187,44 +191,44 @@ def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path) -> None:
|
||||
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")
|
||||
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
|
||||
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
|
||||
from esphome.platformio.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")
|
||||
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
|
||||
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
|
||||
from esphome.platformio.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")
|
||||
apply_extra_script(c, board_mcu="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
|
||||
from esphome.platformio.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")
|
||||
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
|
||||
assert "not found" in caplog.text
|
||||
@@ -128,13 +128,6 @@ def test_print_summary_handles_no_memory_types(
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_format_bar_zero_total() -> None:
|
||||
"""A zero total must not divide by zero."""
|
||||
from esphome.build_helpers.size_summary import format_bar
|
||||
|
||||
assert format_bar(0, 0) == "[ ] 0.0% (used 0 bytes from 0 bytes)"
|
||||
|
||||
|
||||
def test_print_summary_flash_line(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user