mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
555 lines
21 KiB
Python
555 lines
21 KiB
Python
"""Tests for esphome/platformio/pch.py.script against a fake SCons env."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from esphome.build_helpers import pch
|
|
from esphome.platformio import toolchain
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.name == "nt", reason="the fake compiler is a POSIX shell script"
|
|
)
|
|
|
|
_SCRIPT = Path(toolchain.__file__).parent / "pch.py.script"
|
|
|
|
|
|
class _FakePlatform:
|
|
name = "fake"
|
|
packages = {"framework-x": {}, "toolchain-y": {}}
|
|
|
|
def get_package_version(self, name: str) -> str | None:
|
|
# None for an optional package that is not installed
|
|
return None if name == "toolchain-y" else "1.2.3"
|
|
|
|
|
|
class _FakeSConsEnv(dict):
|
|
"""Just enough of a SCons construction environment for pch.py."""
|
|
|
|
def __init__(
|
|
self,
|
|
proj_dir: Path,
|
|
src_dir: Path,
|
|
cxx: str,
|
|
flags: list[str],
|
|
platform_cls: type[_FakePlatform] = _FakePlatform,
|
|
):
|
|
super().__init__(ENV={})
|
|
self._subst = {
|
|
"$PROJECT_DIR": str(proj_dir),
|
|
"$PROJECT_SRC_DIR": str(src_dir),
|
|
"$CXX": cxx,
|
|
}
|
|
self._flags = flags
|
|
self._platform_cls = platform_cls
|
|
self.prepended: list[str] = []
|
|
|
|
def subst(self, expr: str) -> str: # noqa: N802
|
|
return self._subst[expr]
|
|
|
|
def subst_list(self, expr: str) -> list[list[str]]: # noqa: N802
|
|
return [self._flags]
|
|
|
|
def PioPlatform(self) -> _FakePlatform: # noqa: N802
|
|
return self._platform_cls()
|
|
|
|
def Prepend(self, CXXFLAGS: list[str]) -> None: # noqa: N802, N803
|
|
self.prepended = CXXFLAGS
|
|
|
|
def Flatten(self, nodes: list) -> list: # noqa: N802
|
|
return nodes
|
|
|
|
def GetProjectOption(self, name: str, default: list) -> list: # noqa: N802
|
|
return self.get(f"option:{name}", default)
|
|
|
|
|
|
def _fake_cxx(tmp_path: Path, fail: bool = False) -> Path:
|
|
"""A compiler stand-in that records its argv and writes the -o target."""
|
|
cxx = tmp_path / "fake-gxx"
|
|
# As GCC: the program's path when it exists next to the driver, else its
|
|
# name; the version from FAKE_GCC_VERSION
|
|
body = (
|
|
'case "$1" in -print-prog-name=*) n=${1#*=};'
|
|
' p="$(dirname "$0")/../libexec/gcc/arm-none-eabi/10.3.1/$n";'
|
|
' [ -x "$p" ] && echo "$p" || echo "$n"; exit 0;;'
|
|
' -dumpversion) echo "${FAKE_GCC_VERSION:-10.3.1}"; exit 0;;'
|
|
' --version) echo "${FAKE_GCC_BANNER:-fake-g++ (test) ${FAKE_GCC_VERSION:-10.3.1}}";'
|
|
" exit 0;; esac\n"
|
|
'printf -- ---call---\\\\n >> "$0.argv"; printf \'%s\\n\' "$@" >> "$0.argv"\n'
|
|
)
|
|
if fail:
|
|
body += "echo boom >&2\nexit 1\n"
|
|
else:
|
|
body += 'out=""; prev=""\nfor a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n'
|
|
body += '[ -n "$out" ] && echo gch > "$out"\n'
|
|
cxx.write_text("#!/bin/sh\n" + body)
|
|
cxx.chmod(cxx.stat().st_mode | stat.S_IEXEC)
|
|
return cxx
|
|
|
|
|
|
def _run_script(
|
|
tmp_path: Path,
|
|
flags: list[str] | None = None,
|
|
fail: bool = False,
|
|
env_vars: dict[str, str] | None = None,
|
|
name: str = "dev",
|
|
platform_cls: type[_FakePlatform] = _FakePlatform,
|
|
build_files: Callable[[tuple], list] | None = None,
|
|
cxx: Path | None = None,
|
|
) -> _FakeSConsEnv:
|
|
proj = tmp_path / name
|
|
src = proj / "src"
|
|
(src / "esphome" / "core").mkdir(parents=True, exist_ok=True)
|
|
(src / "esphome" / "core" / "defines.h").write_text("#define USE_X\n")
|
|
(src / "esphome" / "core" / "pch_prefix.h").write_text(
|
|
'#include "esphome/core/defines.h"\n'
|
|
)
|
|
if cxx is None:
|
|
cxx = _fake_cxx(tmp_path, fail=fail)
|
|
args = (proj, src, str(cxx), flags or ["-DX=1"], platform_cls)
|
|
# Distinct objects: the -include flags must land on projenv only
|
|
global_env = _FakeSConsEnv(*args)
|
|
projenv = _FakeSConsEnv(*args)
|
|
projenv.global_env = global_env
|
|
if build_files is not None:
|
|
global_env["PIOBUILDFILES"] = build_files(args)
|
|
source = _SCRIPT.read_text()
|
|
with patch.dict(os.environ, env_vars or {}, clear=True):
|
|
exec( # noqa: S102
|
|
compile(source, "pch.py", "exec"),
|
|
{
|
|
"Import": lambda *_names: None,
|
|
"env": global_env,
|
|
"projenv": projenv,
|
|
"COMMAND_LINE_TARGETS": [],
|
|
},
|
|
)
|
|
return projenv
|
|
|
|
|
|
def test_pch_script_builds_and_prepends_relative_include(tmp_path: Path) -> None:
|
|
scons_env = _run_script(tmp_path)
|
|
proj = tmp_path / "dev"
|
|
assert (proj / "esphome_pch_src.h").read_text() == pch.pch_header_text(
|
|
pch.PCH_DEFAULT_HEADERS
|
|
)
|
|
assert (proj / "esphome_pch.h").read_text() == pch.PCH_GUARD_TEXT
|
|
assert (proj / "esphome_pch.h.gch").is_file()
|
|
assert len((proj / "esphome_pch.h.gch.sum").read_text().strip()) == 64
|
|
assert scons_env.prepended == pch.pch_consumer_flags()
|
|
# The -include flags are scoped to projenv (src compiles)
|
|
assert scons_env.global_env.prepended == []
|
|
|
|
|
|
class _LibreTinyPlatform(_FakePlatform):
|
|
name = "libretiny"
|
|
|
|
|
|
def _run_on_host(
|
|
tmp_path: Path,
|
|
host: str,
|
|
machine: str,
|
|
platform_cls: type[_FakePlatform],
|
|
cxx: Path | None = None,
|
|
gcc_version: str = "10.3.1",
|
|
) -> _FakeSConsEnv:
|
|
with (
|
|
patch.object(sys, "platform", host),
|
|
patch("platform.machine", return_value=machine),
|
|
):
|
|
return _run_script(
|
|
tmp_path,
|
|
platform_cls=platform_cls,
|
|
cxx=cxx,
|
|
env_vars={"FAKE_GCC_VERSION": gcc_version},
|
|
)
|
|
|
|
|
|
def _fake_toolchain(tmp_path: Path) -> tuple[Path, Path]:
|
|
"""A driver in bin/ and a cc1plus that records its argv in libexec/."""
|
|
toolchain = tmp_path / "toolchain"
|
|
(toolchain / "bin").mkdir(parents=True)
|
|
cxx = _fake_cxx(toolchain / "bin")
|
|
real = toolchain / "libexec" / "gcc" / "arm-none-eabi" / "10.3.1" / "cc1plus"
|
|
real.parent.mkdir(parents=True)
|
|
real.write_text('#!/bin/sh\nprintf \'%s\\n\' "$@" > "$0.argv"\n')
|
|
real.chmod(0o755)
|
|
return cxx, real
|
|
|
|
|
|
def test_pch_script_gcc10_wrapper_on_apple_silicon(tmp_path: Path) -> None:
|
|
"""The LibreTiny .gch compile and the consumers get a -B directory
|
|
holding a cc1plus that starts the real one."""
|
|
cxx, real = _fake_toolchain(tmp_path)
|
|
scons_env = _run_on_host(tmp_path, "darwin", "arm64", _LibreTinyPlatform, cxx)
|
|
wrapper = tmp_path / "dev" / pch.PCH_CC1_DIR / "cc1plus"
|
|
assert wrapper.stat().st_mode & stat.S_IXUSR
|
|
assert repr(str(real)) in wrapper.read_text(encoding="utf-8")
|
|
argv = Path(f"{cxx}.argv").read_text(encoding="utf-8").split("\n")
|
|
assert f"-B{pch.PCH_CC1_DIR}/" in argv
|
|
assert scons_env.prepended == [f"-B{pch.PCH_CC1_DIR}/", *pch.pch_consumer_flags()]
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "darwin", reason="the wrapper is macOS only")
|
|
def test_pch_script_gcc10_wrapper_starts_the_real_cc1plus(tmp_path: Path) -> None:
|
|
cxx, real = _fake_toolchain(tmp_path)
|
|
_run_on_host(tmp_path, "darwin", "arm64", _LibreTinyPlatform, cxx)
|
|
wrapper = tmp_path / "dev" / pch.PCH_CC1_DIR / "cc1plus"
|
|
result = subprocess.run(
|
|
[str(wrapper), "-quiet", "x.cpp"], capture_output=True, text=True, check=False
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
assert Path(f"{real}.argv").read_text(encoding="utf-8") == "-quiet\nx.cpp\n"
|
|
|
|
|
|
def test_pch_script_gcc10_without_cc1plus_builds_plainly(
|
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""A driver without a cc1plus of its own gets no wrapper and no header."""
|
|
scons_env = _run_on_host(tmp_path, "darwin", "arm64", _LibreTinyPlatform)
|
|
assert not (tmp_path / "dev" / "esphome_pch.h.gch").exists()
|
|
assert scons_env.prepended == []
|
|
assert "compiling without it" in capsys.readouterr().out
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("host", "machine"), [("darwin", "x86_64"), ("win32", "AMD64")]
|
|
)
|
|
def test_pch_script_gcc10_skipped_elsewhere(
|
|
tmp_path: Path, host: str, machine: str, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""Where the GCC 10 .gch cannot load, the build runs without it."""
|
|
scons_env = _run_on_host(tmp_path, host, machine, _LibreTinyPlatform)
|
|
assert not (tmp_path / "dev" / "esphome_pch.h.gch").exists()
|
|
assert scons_env.prepended == []
|
|
assert "compiling without it" in capsys.readouterr().out
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("host", "machine", "platform_cls", "gcc_version"),
|
|
[
|
|
("darwin", "arm64", _FakePlatform, "10.3.1"),
|
|
("linux", "aarch64", _LibreTinyPlatform, "10.3.1"),
|
|
# From GCC 12 the .gch loads at any address, so the rule retires itself
|
|
("darwin", "arm64", _LibreTinyPlatform, "12.2.0"),
|
|
# Windows needs the fix for GCC bug 14940 as well
|
|
("win32", "AMD64", _LibreTinyPlatform, "14.4.0"),
|
|
],
|
|
)
|
|
def test_pch_script_no_wrapper_where_the_gch_loads(
|
|
tmp_path: Path,
|
|
host: str,
|
|
machine: str,
|
|
platform_cls: type[_FakePlatform],
|
|
gcc_version: str,
|
|
) -> None:
|
|
scons_env = _run_on_host(
|
|
tmp_path, host, machine, platform_cls, gcc_version=gcc_version
|
|
)
|
|
assert not (tmp_path / "dev" / pch.PCH_CC1_DIR).exists()
|
|
assert scons_env.prepended == pch.pch_consumer_flags()
|
|
|
|
|
|
def test_pch_script_names_match_the_python_side(tmp_path: Path) -> None:
|
|
"""The script cannot import esphome, so its copies are pinned."""
|
|
namespace: dict[str, object] = {
|
|
"Import": lambda *_names: None,
|
|
"env": _FakeSConsEnv(tmp_path, tmp_path, "g++", []),
|
|
"projenv": None,
|
|
"COMMAND_LINE_TARGETS": ["nobuild"],
|
|
}
|
|
exec(compile(_SCRIPT.read_text(), "pch.py", "exec"), namespace) # noqa: S102
|
|
assert namespace["_HEADER_NAME"] == pch.PCH_HEADER_NAME
|
|
assert namespace["_SOURCE_NAME"] == pch.PCH_SOURCE_NAME
|
|
assert namespace["_DEFAULT_HEADERS"] == pch.PCH_DEFAULT_HEADERS
|
|
assert namespace["_CONSUMER_FLAGS"] == pch.pch_consumer_flags()
|
|
assert namespace["_GUARD_TEXT"] == pch.PCH_GUARD_TEXT
|
|
assert namespace["_INCLUDE_RE"].pattern == pch._INCLUDE_RE.pattern
|
|
assert namespace["_CC1_DIR"] == pch.PCH_CC1_DIR
|
|
assert namespace["_WINDOWS_GCC_FIXED"] == pch.PCH_WINDOWS_GCC_FIXED
|
|
assert namespace["_WINDOWS_GCC_FIXED_DEFAULT"] == pch.PCH_WINDOWS_GCC_FIXED_DEFAULT
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("version", "env_vars", "on"),
|
|
[
|
|
("14.2.0", {}, False),
|
|
("14.4.0", {}, True),
|
|
("14.2.0", {"ESPHOME_PCH_ENABLE": "1"}, True),
|
|
],
|
|
)
|
|
def test_pch_script_asks_the_compiler_on_windows(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capsys: pytest.CaptureFixture[str],
|
|
version: str,
|
|
env_vars: dict[str, str],
|
|
on: bool,
|
|
) -> None:
|
|
"""The Python rule again, plus the normalized knob."""
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
scons_env = _run_script(
|
|
tmp_path, env_vars={"FAKE_GCC_VERSION": version, **env_vars}
|
|
)
|
|
assert (scons_env.prepended == pch.pch_consumer_flags()) is on
|
|
assert (tmp_path / "dev" / "esphome_pch.h.gch").is_file() is on
|
|
out = capsys.readouterr().out
|
|
assert ("cannot load a precompiled header on Windows" in out) is not on
|
|
|
|
|
|
def test_pch_script_spares_another_compiler_the_gcc_rule(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
scons_env = _run_script(
|
|
tmp_path, env_vars={"FAKE_GCC_BANNER": "clang version 15.0.0"}
|
|
)
|
|
assert scons_env.prepended == pch.pch_consumer_flags()
|
|
|
|
|
|
def test_pch_script_leaves_the_header_off_when_the_compiler_cannot_run(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
scons_env = _run_script(tmp_path, cxx=tmp_path / "missing-g++")
|
|
assert scons_env.prepended == []
|
|
assert "GCC of unknown version cannot load" in capsys.readouterr().out
|
|
|
|
|
|
def test_pch_script_compile_failure_stops_the_build(tmp_path: Path) -> None:
|
|
"""The pch holds only ESPHome's own headers: a failure is a defect."""
|
|
with pytest.raises(RuntimeError, match="could not compile") as raised:
|
|
_run_script(tmp_path, fail=True)
|
|
assert "boom" in str(raised.value)
|
|
assert not (tmp_path / "dev" / "esphome_pch.h.gch.sum").exists()
|
|
|
|
|
|
def test_pch_script_preserves_spaced_flag_elements(tmp_path: Path) -> None:
|
|
"""One SCons element stays one compiler argv; -include pairs are
|
|
stripped from the .gch compile."""
|
|
spaced = tmp_path / "My Configs"
|
|
spaced.mkdir()
|
|
(tmp_path / "dev" / "src").mkdir(parents=True, exist_ok=True)
|
|
(tmp_path / "dev" / "src" / "other.h").write_text("")
|
|
flags = ['-DUSB_PRODUCT=\\"Pico 2W\\"', "-I", str(spaced), "-include", "other.h"]
|
|
_run_script(tmp_path, flags=flags)
|
|
calls = (tmp_path / "fake-gxx.argv").read_text().split("---call---\n")
|
|
gch_call = next(c for c in calls if "c++-header" in c).splitlines()
|
|
assert '-DUSB_PRODUCT="Pico 2W"' in gch_call
|
|
assert str(spaced) in gch_call
|
|
assert "-include" not in gch_call
|
|
# The stripped src-resolvable -include is folded into the prefix header
|
|
pch = (tmp_path / "dev" / "esphome_pch_src.h").read_text()
|
|
assert pch.splitlines()[0] == '#include "other.h"'
|
|
|
|
|
|
def test_pch_script_folds_joined_force_include_spelling(tmp_path: Path) -> None:
|
|
"""-includefoo.h folds like the separated form, matching the native path."""
|
|
(tmp_path / "dev" / "src").mkdir(parents=True, exist_ok=True)
|
|
(tmp_path / "dev" / "src" / "other.h").write_text("")
|
|
_run_script(tmp_path, flags=["-DX=1", "-includeother.h"])
|
|
pch = (tmp_path / "dev" / "esphome_pch_src.h").read_text()
|
|
assert pch.splitlines()[0] == '#include "other.h"'
|
|
|
|
|
|
def test_pch_script_sum_is_device_independent(tmp_path: Path) -> None:
|
|
"""Regression: identical configs in different dirs share cache keys."""
|
|
sums = []
|
|
for name in ("dev_a", "dev_b"):
|
|
proj = tmp_path / name
|
|
_run_script(
|
|
tmp_path,
|
|
flags=["-DX=1", "-I", str(proj / "include")],
|
|
env_vars={"CCACHE_BASEDIR": str(proj)},
|
|
name=name,
|
|
)
|
|
sums.append((proj / "esphome_pch.h.gch.sum").read_text())
|
|
(tmp_path / "fake-gxx").unlink()
|
|
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
|
|
assert sums[0] == sums[1]
|
|
|
|
|
|
def test_pch_script_uses_the_envs_of_existing_src_objects(tmp_path: Path) -> None:
|
|
"""The ESP-IDF builder creates the src objects from its own environments
|
|
before this script runs; flags on projenv would never reach them."""
|
|
made: list[_FakeSConsEnv] = []
|
|
|
|
def build_files(args: tuple) -> list:
|
|
src = args[1]
|
|
made.extend(_FakeSConsEnv(*args) for _ in range(3))
|
|
return [
|
|
SimpleNamespace(env=made[0], sources=[src / "main.cpp"]),
|
|
SimpleNamespace(env=made[0], sources=[src / "esphome" / "a.cpp"]),
|
|
# C and framework objects keep their environments untouched
|
|
SimpleNamespace(env=made[1], sources=[src / "esphome" / "b.c"]),
|
|
SimpleNamespace(env=made[2], sources=[src.parent / "lib" / "c.cpp"]),
|
|
]
|
|
|
|
projenv = _run_script(tmp_path, build_files=build_files)
|
|
assert made[0].prepended == pch.pch_consumer_flags()
|
|
assert made[1].prepended == made[2].prepended == projenv.prepended == []
|
|
|
|
|
|
def test_copy_pch_script(tmp_path: Path) -> None:
|
|
from esphome.core import CORE
|
|
|
|
CORE.build_path = tmp_path
|
|
toolchain.copy_pch_script()
|
|
assert (tmp_path / "pch.py").read_text() == _SCRIPT.read_text()
|
|
|
|
|
|
def _run_without_projenv(tmp_path: Path, targets: list[str]) -> Path:
|
|
proj = tmp_path / "dev"
|
|
(proj / "src").mkdir(parents=True)
|
|
|
|
def strict_import(*names: str) -> None:
|
|
if "projenv" in names:
|
|
raise RuntimeError("Import of non-existent variable 'projenv'")
|
|
|
|
env = _FakeSConsEnv(proj, proj / "src", "g++", ["-DX=1"])
|
|
exec( # noqa: S102
|
|
compile(_SCRIPT.read_text(), "pch.py", "exec"),
|
|
{"Import": strict_import, "env": env, "COMMAND_LINE_TARGETS": targets},
|
|
)
|
|
return proj
|
|
|
|
|
|
def test_pch_script_nobuild_without_projenv_is_noop(tmp_path: Path) -> None:
|
|
"""-t nobuild never exports projenv; the script must not abort."""
|
|
proj = _run_without_projenv(tmp_path, ["nobuild"])
|
|
assert not (proj / "esphome_pch.h").exists()
|
|
|
|
|
|
def test_pch_script_build_without_projenv_stops(tmp_path: Path) -> None:
|
|
with pytest.raises(RuntimeError, match="no project environment"):
|
|
_run_without_projenv(tmp_path, [])
|
|
|
|
|
|
def test_pch_script_stops_when_no_source_object_matches(tmp_path: Path) -> None:
|
|
"""Flags on projenv would reach no compile while the log says the
|
|
precompiled header is in use."""
|
|
|
|
def build_files(args: tuple) -> list:
|
|
lib = args[1].parent / "lib" / "c.cpp"
|
|
return [SimpleNamespace(env=_FakeSConsEnv(*args), sources=[lib])]
|
|
|
|
with pytest.raises(RuntimeError, match="no C\\+\\+ source takes"):
|
|
_run_script(tmp_path, build_files=build_files)
|
|
|
|
|
|
def test_pch_script_ignores_library_trees_and_non_headers(tmp_path: Path) -> None:
|
|
""".piolibdeps and non-header files must not enter the digest (or be
|
|
read at all); package versions already cover library identity."""
|
|
proj = tmp_path / "dev"
|
|
libdeps = proj / ".piolibdeps" / "lib" / "src"
|
|
libdeps.mkdir(parents=True)
|
|
(libdeps / "lib.h").write_text("#define A 1\n")
|
|
override = proj / "lwip_override"
|
|
override.mkdir(parents=True)
|
|
(override / "lwipopts.h").write_text("#define TCP_MSS 1460\n")
|
|
(override / "notes.txt").write_text("v1\n")
|
|
flags = ["-DX=1", "-I", str(libdeps), "-I", str(override)]
|
|
_run_script(tmp_path, flags=flags)
|
|
first = (proj / "esphome_pch.h.gch.sum").read_text()
|
|
(libdeps / "lib.h").write_text("#define A 2\n")
|
|
(override / "notes.txt").write_text("v2\n")
|
|
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
|
|
_run_script(tmp_path, flags=flags)
|
|
assert (proj / "esphome_pch.h.gch.sum").read_text() == first
|
|
|
|
|
|
def test_pch_script_hashes_project_local_include_dirs(tmp_path: Path) -> None:
|
|
"""Generated headers in project-local -I dirs (e.g. rp2's lwip_override)
|
|
must invalidate the checksum when they change."""
|
|
proj = tmp_path / "dev"
|
|
override = proj / "lwip_override"
|
|
override.mkdir(parents=True)
|
|
(override / "lwipopts.h").write_text("#define TCP_MSS 1460\n")
|
|
flags = ["-DX=1", "-I", str(override)]
|
|
_run_script(tmp_path, flags=flags)
|
|
first = (proj / "esphome_pch.h.gch.sum").read_text()
|
|
(override / "lwipopts.h").write_text("#define TCP_MSS 536\n")
|
|
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
|
|
_run_script(tmp_path, flags=flags)
|
|
assert (proj / "esphome_pch.h.gch.sum").read_text() != first
|
|
|
|
|
|
def test_pch_script_folds_force_includes_found_on_the_include_path(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A framework force-include (Arduino.h) is read before the core
|
|
headers, as it was without the precompiled header."""
|
|
_run_script(tmp_path, flags=["-DX=1", "-include", "Arduino.h"])
|
|
source = (tmp_path / "dev" / "esphome_pch_src.h").read_text()
|
|
assert source.splitlines() == [
|
|
'#include "Arduino.h"',
|
|
'#include "esphome/core/pch_prefix.h"',
|
|
]
|
|
|
|
|
|
def test_pch_script_skipped_for_an_absolute_force_include(
|
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
outside = tmp_path / "outside.h"
|
|
outside.write_text("")
|
|
scons_env = _run_script(tmp_path, flags=["-DX=1", "-include", str(outside)])
|
|
assert scons_env.prepended == []
|
|
assert not (tmp_path / "dev" / "esphome_pch.h.gch").exists()
|
|
assert "prevents the precompiled header" in capsys.readouterr().out
|
|
|
|
|
|
def _sum_after(tmp_path: Path, flags: list[str]) -> str:
|
|
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
|
|
_run_script(tmp_path, flags=flags)
|
|
return (tmp_path / "dev" / "esphome_pch.h.gch.sum").read_text()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"option", [["-Iinclude"], ["-isystem", "include"], ["-iquoteinclude"]]
|
|
)
|
|
def test_pch_script_hashes_relative_include_dirs(
|
|
tmp_path: Path, option: list[str]
|
|
) -> None:
|
|
"""Compiles run in the project root, so -Iinclude is a project dir."""
|
|
include = tmp_path / "dev" / "include"
|
|
include.mkdir(parents=True)
|
|
(include / "user.h").write_text("#define A 1\n")
|
|
flags = ["-DX=1", *option, "-I."]
|
|
first = _sum_after(tmp_path, flags)
|
|
(include / "user.h").write_text("#define A 2\n")
|
|
assert _sum_after(tmp_path, flags) != first
|
|
|
|
|
|
def test_pch_script_hashes_the_sdkconfig(tmp_path: Path) -> None:
|
|
"""sdkconfig.h is build output; the configuration it comes from is not."""
|
|
proj = tmp_path / "dev"
|
|
proj.mkdir()
|
|
(proj / "sdkconfig.dev").write_text("CONFIG_X=y\n")
|
|
first = _sum_after(tmp_path, ["-DX=1"])
|
|
(proj / "sdkconfig.dev").write_text("CONFIG_X=n\n")
|
|
assert _sum_after(tmp_path, ["-DX=1"]) != first
|
|
|
|
|
|
def test_pch_script_hashes_the_library_versions(tmp_path: Path) -> None:
|
|
sums = []
|
|
for version in ("7.4.1", "7.4.2"):
|
|
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
|
|
|
|
with patch.object(
|
|
_FakeSConsEnv,
|
|
"GetProjectOption",
|
|
lambda self, name, default, version=version: [f"ArduinoJson@{version}"],
|
|
):
|
|
_run_script(tmp_path)
|
|
sums.append((tmp_path / "dev" / "esphome_pch.h.gch.sum").read_text())
|
|
assert sums[0] != sums[1]
|