Files
esphome/tests/unit_tests/analyze_memory/test_build_artifacts.py
T

158 lines
5.4 KiB
Python

"""Tests for locating build artifacts across the supported toolchain layouts."""
from pathlib import Path
import pytest
from esphome.analyze_memory.toolchain import (
find_elf_path,
find_idedata_path,
idedata_candidates,
)
from esphome.espidf.idedata import _cc_path_from_cxx
from esphome.platformio.toolchain import IDEData
def _make_build_dir(tmp_path: Path, name: str = "mydevice") -> Path:
"""Create <tmp_path>/.esphome/build/<name>, mirroring a real data dir."""
build_path = tmp_path / ".esphome" / "build" / name
build_path.mkdir(parents=True)
return build_path
def _touch(path: Path) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("", encoding="utf-8")
return path
def test_find_elf_path_native_esp_idf(tmp_path: Path) -> None:
"""The native ESP-IDF toolchain writes the ELF under build/."""
build_path = _make_build_dir(tmp_path)
elf = _touch(build_path / "build" / "firmware.elf")
assert find_elf_path(build_path) == elf
def test_find_elf_path_platformio(tmp_path: Path) -> None:
"""The PlatformIO toolchain writes the ELF under .pioenvs/<name>/."""
build_path = _make_build_dir(tmp_path)
elf = _touch(build_path / ".pioenvs" / build_path.name / "firmware.elf")
assert find_elf_path(build_path) == elf
def test_find_elf_path_libretiny(tmp_path: Path) -> None:
"""The LibreTiny toolchain names the unwrapped ELF raw_firmware.elf."""
build_path = _make_build_dir(tmp_path)
elf = _touch(build_path / ".pioenvs" / build_path.name / "raw_firmware.elf")
assert find_elf_path(build_path) == elf
@pytest.mark.parametrize(
"relative_elf",
[
# SDK < 2.9.2
"zephyr/zephyr.elf",
# SDK >= 2.9.2 nests the artifacts one level deeper
"zephyr/zephyr/zephyr.elf",
],
)
def test_find_elf_path_zephyr(tmp_path: Path, relative_elf: str) -> None:
"""Zephyr (nRF52) keeps the ELF under .pioenvs/<name>/zephyr/."""
build_path = _make_build_dir(tmp_path)
elf = _touch(build_path / ".pioenvs" / build_path.name / relative_elf)
assert find_elf_path(build_path) == elf
def test_find_elf_path_missing(tmp_path: Path) -> None:
"""An unknown layout resolves to None rather than a bogus path."""
assert find_elf_path(_make_build_dir(tmp_path)) is None
def test_find_idedata_path_in_data_dir(tmp_path: Path) -> None:
"""The idedata cache sits in the data dir that holds the build dir."""
build_path = _make_build_dir(tmp_path)
idedata = _touch(tmp_path / ".esphome" / "idedata" / f"{build_path.name}.json")
assert find_idedata_path(build_path) == idedata
def test_find_idedata_path_in_pioenvs(tmp_path: Path) -> None:
"""Test builds may keep idedata alongside the PlatformIO env."""
build_path = _make_build_dir(tmp_path)
idedata = _touch(build_path / ".pioenvs" / build_path.name / "idedata.json")
assert find_idedata_path(build_path) == idedata
def test_find_idedata_path_missing(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A missing idedata resolves to None."""
# Keep the cwd/home fallbacks from finding an unrelated file on this machine
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path))
assert find_idedata_path(_make_build_dir(tmp_path)) is None
def test_idedata_candidates_are_what_find_probes(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Every advertised candidate is one find_idedata_path actually accepts.
The candidates are reported to the user when idedata is missing, so a list
that drifts from the lookup would send someone hunting in the wrong place.
"""
# Two candidates are relative to the cwd and to home; keep the test from
# writing into the real ones.
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path))
build_path = _make_build_dir(tmp_path)
candidates = idedata_candidates(build_path)
assert candidates, "no candidates advertised"
for candidate in candidates:
_touch(candidate)
assert find_idedata_path(build_path) == candidate
candidate.unlink()
@pytest.mark.parametrize(
("cxx_path", "expected"),
[
("/tools/bin/xtensa-esp32-elf-g++", "/tools/bin/xtensa-esp32-elf-gcc"),
("/tools/bin/riscv32-esp-elf-g++", "/tools/bin/riscv32-esp-elf-gcc"),
(
r"C:\tools\bin\xtensa-esp32-elf-g++.exe",
r"C:\tools\bin\xtensa-esp32-elf-gcc.exe",
),
# Nothing to rewrite; leave the path alone
("/tools/bin/clang++", "/tools/bin/clang++"),
],
)
def test_cc_path_from_cxx(cxx_path: str, expected: str) -> None:
"""cc_path is derived from the C++ compiler that compile_commands.json names."""
assert _cc_path_from_cxx(cxx_path) == expected
def test_native_idedata_resolves_toolchain_tools() -> None:
"""The binutils paths are derived from the native ESP-IDF cc_path.
Without cc_path, IDEData.objdump_path raises KeyError and the memory
analysis silently degrades to no component or symbol detail.
"""
idedata = IDEData(
{
"cc_path": _cc_path_from_cxx("/tools/bin/xtensa-esp32-elf-g++"),
"cxx_path": "/tools/bin/xtensa-esp32-elf-g++",
}
)
assert idedata.objdump_path == "/tools/bin/xtensa-esp32-elf-objdump"
assert idedata.readelf_path == "/tools/bin/xtensa-esp32-elf-readelf"