mirror of
https://github.com/esphome/esphome.git
synced 2026-10-03 01:35:59 +00:00
Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts: # esphome/components/esp32/__init__.py
This commit is contained in:
@@ -574,11 +574,101 @@ def test_load_or_build_idedata_never_caches_a_launcher(tmp_path: Path) -> None:
|
||||
assert not cache.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cached",
|
||||
(
|
||||
{"cc_path": "/x/gcc", "cxx_path": "/opt/homebrew/bin/ccache"},
|
||||
{"cc_path": "/x/gcc", "cxx_path": "/tools/g++"},
|
||||
{"cc_path": "/x/gcc", "cxx_path": "/tools/g++", "includes": {}},
|
||||
),
|
||||
ids=("launcher-cxx", "no-includes", "no-build-list"),
|
||||
)
|
||||
def test_load_or_build_idedata_regenerates_invalid_cache(
|
||||
tmp_path: Path, cached: dict
|
||||
) -> None:
|
||||
"""A cache written by an older version fails validation and regenerates."""
|
||||
compile_commands = _write_compile_commands(tmp_path)
|
||||
cache = tmp_path / "c.json"
|
||||
cache.write_text(json.dumps(cached))
|
||||
os.utime(cache, (compile_commands.stat().st_mtime + 5,) * 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["cxx_path"] == "/tools/g++"
|
||||
assert "includes" in data
|
||||
|
||||
|
||||
def test_load_or_build_idedata_cache_hit_restamps_prog_path(tmp_path: Path) -> None:
|
||||
"""A served cache carries the current ELF path, not the one it was written with."""
|
||||
compile_commands = _write_compile_commands(tmp_path)
|
||||
cache = tmp_path / "c.json"
|
||||
cache.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"cc_path": "/tools/gcc",
|
||||
"cxx_path": "/tools/g++",
|
||||
"includes": {"build": [], "toolchain": []},
|
||||
"prog_path": "/old/location/firmware.elf",
|
||||
}
|
||||
)
|
||||
)
|
||||
os.utime(cache, (compile_commands.stat().st_mtime + 5,) * 2)
|
||||
data = idedata.load_or_build_idedata(
|
||||
compile_commands, tmp_path / "firmware.elf", cache
|
||||
)
|
||||
assert data["prog_path"] == str(tmp_path / "firmware.elf")
|
||||
|
||||
|
||||
def test_idedata_from_build_non_list_compile_db_raises(tmp_path: Path) -> None:
|
||||
"""Valid JSON that is not a list raises by name, inside the best-effort tuple."""
|
||||
compile_commands = tmp_path / "compile_commands.json"
|
||||
for bad in ("{}", "null", '"text"'):
|
||||
compile_commands.write_text(bad)
|
||||
with pytest.raises(EsphomeError, match="not a compile-command list"):
|
||||
idedata.idedata_from_build(compile_commands)
|
||||
|
||||
|
||||
def test_idedata_from_build_same_file_rsp_commands_never_dedupe(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Two objects built from one source with different .rsp files keep both
|
||||
include sets; the rsp sentinel keys on the output, not the source."""
|
||||
file = f"{ABS}build/src/esphome/core/shared.cpp"
|
||||
entries = []
|
||||
for name in ("a", "b"):
|
||||
rsp = tmp_path / f"{name}.o.rsp"
|
||||
rsp.write_text(f"-I{ABS}inc/{name}")
|
||||
entries.append(
|
||||
{
|
||||
"directory": str(tmp_path),
|
||||
"file": file,
|
||||
"command": f"/tools/g++ @{rsp.name} -c {file} -o {name}.o",
|
||||
"output": f"{name}.o",
|
||||
}
|
||||
)
|
||||
compile_commands = tmp_path / "compile_commands.json"
|
||||
compile_commands.write_text(json.dumps(entries))
|
||||
with patch.object(idedata, "get_toolchain_includes", return_value=[]):
|
||||
data = idedata.idedata_from_build(compile_commands)
|
||||
joined = " ".join(data["includes"]["build"])
|
||||
assert "inc/a" in joined and "inc/b" in joined
|
||||
|
||||
|
||||
def test_load_or_build_idedata_cache_hit_skips_rebuild(tmp_path: Path) -> None:
|
||||
"""A valid cache newer than the compile DB is served without re-parsing."""
|
||||
compile_commands = _write_compile_commands(tmp_path)
|
||||
cache = tmp_path / "c.json"
|
||||
cache.write_text(json.dumps({"cc_path": "/tools/gcc", "cached": True}))
|
||||
cache.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"cc_path": "/tools/gcc",
|
||||
"cxx_path": "/tools/g++",
|
||||
"includes": {"build": ["/inc"], "toolchain": []},
|
||||
"cached": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
os.utime(cache, (compile_commands.stat().st_mtime + 5,) * 2)
|
||||
with patch.object(idedata, "idedata_from_build") as mock_build:
|
||||
data = idedata.load_or_build_idedata(
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from esphome.config_helpers import (
|
||||
filter_source_files_from_defines,
|
||||
filter_source_files_from_platform,
|
||||
frameworks_for_platforms,
|
||||
get_logger_level,
|
||||
@@ -18,6 +19,7 @@ from esphome.const import (
|
||||
KEY_TARGET_PLATFORM,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import Define
|
||||
|
||||
|
||||
def test_filter_source_files_from_platform_esp32() -> None:
|
||||
@@ -148,3 +150,25 @@ def test_frameworks_for_platforms_derives_and_rejects_unknown() -> None:
|
||||
}
|
||||
with pytest.raises(ValueError, match="unknown platform"):
|
||||
frameworks_for_platforms(["esp32", "not_a_platform"])
|
||||
|
||||
|
||||
def test_filter_source_files_from_defines() -> None:
|
||||
"""Files are excluded unless one of their defines is set."""
|
||||
files_map: dict[str, str | tuple[str, ...]] = {
|
||||
"filter.cpp": "USE_SENSOR_FILTER",
|
||||
"automation.cpp": ("USE_CLICK", "USE_MULTI_CLICK"),
|
||||
}
|
||||
filter_func: Callable[[], list[str]] = filter_source_files_from_defines(files_map)
|
||||
|
||||
with patch("esphome.config_helpers.CORE") as mock_core:
|
||||
mock_core.defines = {Define("USE_SENSOR_FILTER")}
|
||||
assert filter_func() == ["automation.cpp"]
|
||||
|
||||
mock_core.defines = {Define("USE_MULTI_CLICK")}
|
||||
assert filter_func() == ["filter.cpp"]
|
||||
|
||||
mock_core.defines = {Define("USE_SENSOR_FILTER"), Define("USE_CLICK")}
|
||||
assert filter_func() == []
|
||||
|
||||
mock_core.defines = set()
|
||||
assert sorted(filter_func()) == ["automation.cpp", "filter.cpp"]
|
||||
|
||||
@@ -256,8 +256,7 @@ class Test_write_file_if_changed:
|
||||
def test_damaged_existing_file_is_replaced(
|
||||
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""A non-UTF-8 existing file is logged and overwritten; aborting
|
||||
would block the very regeneration that fixes it."""
|
||||
"""A non-UTF-8 existing file is logged and overwritten."""
|
||||
dst = tmp_path / "generated.txt"
|
||||
dst.write_bytes(b"\xff\xfe")
|
||||
|
||||
@@ -267,8 +266,7 @@ class Test_write_file_if_changed:
|
||||
assert "Replacing damaged file" in caplog.text
|
||||
|
||||
def test_unreadable_existing_file_still_raises(self, tmp_path: Path):
|
||||
"""An OSError on the comparison read may hide an intact file, so it
|
||||
propagates as EsphomeError instead of unlinking."""
|
||||
"""An OSError on the comparison read still raises EsphomeError."""
|
||||
dst = tmp_path / "generated.txt"
|
||||
dst.write_text("intact")
|
||||
|
||||
|
||||
@@ -302,6 +302,33 @@ def test_apply_extra_script_missing_script_raises(tmp_path) -> None:
|
||||
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", (["a.py"], {"esp32": "a.py"}), ids=("list", "dict"))
|
||||
def test_apply_extra_script_non_string_raises(tmp_path, bad) -> None:
|
||||
"""A non-string extraScript fails naming the library, not with a TypeError."""
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": bad}}
|
||||
with pytest.raises(EsphomeError, match="of library owner/name must be a string"):
|
||||
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
|
||||
|
||||
|
||||
def test_extra_script_cpppath_captured_as_include_flags(tmp_path, monkeypatch):
|
||||
"""CPPPATH entries translate to -I flags anchored like LIBPATH."""
|
||||
|
||||
(tmp_path / "include").mkdir()
|
||||
outside = tmp_path.parent / "system_inc"
|
||||
outside.mkdir(exist_ok=True)
|
||||
elsewhere = tmp_path.parent / "not_the_library_dir"
|
||||
elsewhere.mkdir(exist_ok=True)
|
||||
monkeypatch.chdir(elsewhere)
|
||||
|
||||
result = ExtraScriptResult(cpppath=["include", str(outside), 7])
|
||||
flags = captured_as_build_flags(result, library_dir=tmp_path)
|
||||
|
||||
assert flags == ["-Iinclude", f"-I{outside.resolve()}"]
|
||||
|
||||
|
||||
def test_run_extra_script_failure_discards_partial_capture(tmp_path, caplog) -> None:
|
||||
"""A crashed script yields an empty result: half-applied flags could
|
||||
build wrong-output firmware that links cleanly."""
|
||||
@@ -396,6 +423,6 @@ def test_uncaptured_append_key_warns_once(caplog) -> None:
|
||||
env = _FakeSConsEnv(
|
||||
board_mcu="esp8266", pio_env="esphome_esp8266", pio_platform="espressif8266"
|
||||
)
|
||||
env.Append(CPPPATH=["a"])
|
||||
env.Append(CPPPATH=["b"])
|
||||
assert caplog.text.count("env.Append(CPPPATH=...) is not captured") == 1
|
||||
env.Append(RANLIBFLAGS=["a"])
|
||||
env.Append(RANLIBFLAGS=["b"])
|
||||
assert caplog.text.count("env.Append(RANLIBFLAGS=...) is not captured") == 1
|
||||
|
||||
@@ -205,8 +205,19 @@ def test_print_summary_non_dict_json_warns(tmp_path, caplog) -> None:
|
||||
|
||||
|
||||
def test_print_summary_zero_app_partition_warns(tmp_path, caplog) -> None:
|
||||
"""A malformed partition row parsing to 0 must not render a 0% bar for
|
||||
CI's memory-impact extraction to ingest."""
|
||||
"""A partition row with size 0 drops the Flash bar instead of rendering 0%."""
|
||||
size_json = tmp_path / "size.json"
|
||||
size_json.write_text(
|
||||
'{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": 100}'
|
||||
)
|
||||
partitions = tmp_path / "partitions.csv"
|
||||
partitions.write_text("app0, app, ota_0, 0x10000, 0,\n")
|
||||
print_summary(size_json, partitions)
|
||||
assert "app partition size is" in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_blank_partition_size_warns(tmp_path, caplog) -> None:
|
||||
"""A blank size cell raises ValueError by name instead of parsing to 0."""
|
||||
size_json = tmp_path / "size.json"
|
||||
size_json.write_text(
|
||||
'{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": 100}'
|
||||
@@ -214,4 +225,21 @@ def test_print_summary_zero_app_partition_warns(tmp_path, caplog) -> None:
|
||||
partitions = tmp_path / "partitions.csv"
|
||||
partitions.write_text("app0, app, ota_0, 0x10000, ,\n")
|
||||
print_summary(size_json, partitions)
|
||||
assert "app partition size is" in caplog.text
|
||||
assert "blank partition size cell" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
(
|
||||
'{"memory_types": []}',
|
||||
'{"memory_types": {"DRAM": 5}}',
|
||||
'{"memory_types": {"DRAM": {"used": "x", "size": "y"}}}',
|
||||
),
|
||||
ids=("non-dict-memory-types", "scalar-region", "non-numeric-sizes"),
|
||||
)
|
||||
def test_print_summary_nested_shapes_never_raise(tmp_path, caplog, payload) -> None:
|
||||
"""The blanket guard keeps unexpected nested shapes from raising."""
|
||||
size_json = tmp_path / "size.json"
|
||||
size_json.write_text(payload)
|
||||
print_summary(size_json, tmp_path / "partitions.csv")
|
||||
assert "Skipping size summary" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user