mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
toolchain_tool carries the bin/xtensa-lx106-elf-<name> pattern and the Windows suffix that four call sites previously spelled out (only one of which handled the suffix). ccache_env and get_build_env accept the already-resolved ccache path so run_compile can resolve once instead of paying the PATH scan and runnability probe three times per build.
173 lines
7.4 KiB
Python
173 lines
7.4 KiB
Python
"""Tests for esphome.arduino8266.framework (downloads and environment)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from esphome.arduino8266 import framework
|
|
import esphome.config_validation as cv
|
|
from esphome.core import CORE, EsphomeError
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _build_path(tmp_path: Path) -> None:
|
|
CORE.build_path = tmp_path
|
|
|
|
|
|
def test_framework_package_version() -> None:
|
|
assert framework.framework_package_version(cv.Version(3, 1, 2)) == "3.30102.0"
|
|
assert framework.framework_package_version(cv.Version(3, 2, 0)) == "3.30200.0"
|
|
# 2.6.3+ cores use the same package-major-3 encoding (PlatformIO path)
|
|
assert framework.framework_package_version(cv.Version(2, 7, 4)) == "3.20704.0"
|
|
# A future major bump needs its own encoding, not a doomed registry lookup
|
|
with pytest.raises(EsphomeError, match="not supported yet"):
|
|
framework.framework_package_version(cv.Version(4, 0, 0))
|
|
# Cores up to 2.6.2 use other encodings; the helper is total, not wrong,
|
|
# and its boundary matches the PlatformIO era guard: a 2.6.2 pre-release
|
|
# sorts above 2.6.2 and keeps the package-major-3 encoding
|
|
with pytest.raises(EsphomeError, match="older package encoding"):
|
|
framework.framework_package_version(cv.Version(2, 6, 2))
|
|
assert framework.framework_package_version(cv.Version(2, 6, 2, "b1")) == "3.20602.0"
|
|
assert framework.framework_package_version(cv.Version(2, 6, 3)) == "3.20603.0"
|
|
|
|
|
|
def test_format_framework_arduino_version_pins_all_series() -> None:
|
|
"""The esp8266 component's PIO source formatter across every encoding
|
|
era, including the 4.x rejection it now shares with the installer."""
|
|
from esphome.components.esp8266 import _format_framework_arduino_version as fmt
|
|
|
|
assert fmt(cv.Version(2, 4, 1)) == "~1.20401.0"
|
|
assert fmt(cv.Version(2, 6, 2)) == "~2.20602.0"
|
|
assert fmt(cv.Version(2, 7, 4)) == "~3.20704.0"
|
|
assert fmt(cv.Version(3, 1, 2)) == "~3.30102.0"
|
|
# Anchored to the framework version line, not a bare EsphomeError
|
|
with pytest.raises(cv.Invalid, match="not supported yet") as excinfo:
|
|
fmt(cv.Version(4, 0, 0))
|
|
assert excinfo.value.path == ["version"]
|
|
|
|
|
|
def test_tools_path_default_and_prefix(tmp_path: Path) -> None:
|
|
with patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}):
|
|
assert framework.get_arduino8266_tools_path() == tmp_path.resolve()
|
|
# A blank prefix must be treated as unset, not as the CWD
|
|
with patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": " "}):
|
|
path = framework.get_arduino8266_tools_path()
|
|
assert path.name == "arduino8266"
|
|
assert path != Path.cwd()
|
|
|
|
|
|
def test_check_and_install_returns_paths(tmp_path: Path) -> None:
|
|
with (
|
|
patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}),
|
|
patch.object(framework, "install_package") as mock_install,
|
|
patch.object(framework, "find_ninja", return_value=tmp_path / "ninja"),
|
|
):
|
|
paths = framework.check_and_install(cv.Version(3, 1, 2))
|
|
assert paths.framework == tmp_path / "frameworks" / "3.30102.0"
|
|
assert paths.toolchain == tmp_path / "toolchains" / framework.TOOLCHAIN_VERSION
|
|
assert paths.ninja == tmp_path / "ninja"
|
|
assert mock_install.call_count == 2
|
|
# Full argument pinning: a copy-paste swap between the two near-identical
|
|
# calls (mirrors, destination) must not stay green
|
|
fw_call, tc_call = mock_install.call_args_list
|
|
assert fw_call.args == (
|
|
framework.FRAMEWORK_PACKAGE,
|
|
"3.30102.0",
|
|
tmp_path / "frameworks" / "3.30102.0",
|
|
framework.ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
|
|
tmp_path / "downloads",
|
|
)
|
|
assert fw_call.kwargs["expect"] == ("cores/esp8266", "tools/sdk", "libraries")
|
|
assert tc_call.args == (
|
|
framework.TOOLCHAIN_PACKAGE,
|
|
framework.TOOLCHAIN_VERSION,
|
|
tmp_path / "toolchains" / framework.TOOLCHAIN_VERSION,
|
|
framework.ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS,
|
|
tmp_path / "downloads",
|
|
)
|
|
assert tc_call.kwargs["expect"] == ("bin", "xtensa-lx106-elf")
|
|
|
|
|
|
def test_get_build_env_prepends_toolchain_bin(tmp_path: Path) -> None:
|
|
with patch.object(framework, "ccache_env", return_value={"CCACHE_DIR": "x"}):
|
|
env = framework.get_build_env(tmp_path)
|
|
assert env["PATH"].startswith(str(tmp_path / "bin") + os.pathsep)
|
|
assert env["CCACHE_DIR"] == "x"
|
|
|
|
|
|
def test_ccache_path_delegates_uncached(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The wrapper delegates to the shared policy (covered in
|
|
build_helpers/test_ccache.py) on every call: the env/PATH decision
|
|
must not freeze for the process lifetime in a long-lived host."""
|
|
monkeypatch.delenv("ESPHOME_CCACHE_ENABLE", raising=False)
|
|
with patch.object(
|
|
framework, "resolve_ccache_path", return_value="/usr/bin/ccache"
|
|
) as mock_resolve:
|
|
assert framework.ccache_path() == "/usr/bin/ccache"
|
|
assert framework.ccache_path() == "/usr/bin/ccache"
|
|
assert mock_resolve.call_count == 2
|
|
|
|
|
|
def test_ccache_env(tmp_path: Path) -> None:
|
|
with patch.object(framework, "ccache_path", return_value=None):
|
|
assert framework.ccache_env() == {}
|
|
with (
|
|
patch.object(framework, "ccache_path", return_value="/usr/bin/ccache"),
|
|
patch.dict(os.environ, {"CCACHE_NOHASHDIR": "false"}, clear=True),
|
|
):
|
|
env = framework.ccache_env()
|
|
# User-set values are respected; the rest get defaults
|
|
assert "CCACHE_NOHASHDIR" not in env
|
|
assert env["CCACHE_DEPEND"] == "1"
|
|
assert env["CCACHE_BASEDIR"] == str(Path(CORE.build_path).resolve())
|
|
assert env["CCACHE_DIR"].endswith("ccache")
|
|
|
|
|
|
def test_check_and_install_rejects_old_core(tmp_path: Path) -> None:
|
|
"""Calling the installer below the floor fails before any download."""
|
|
with pytest.raises(EsphomeError, match=">= 3.1.1"):
|
|
framework.check_and_install(cv.Version(3, 0, 2))
|
|
|
|
|
|
def test_get_build_env_without_path_has_no_empty_entry(tmp_path: Path) -> None:
|
|
"""An absent PATH must not leave a trailing separator (an empty entry
|
|
means the current directory to the shell)."""
|
|
with (
|
|
patch.dict(os.environ, {}, clear=True),
|
|
patch.object(framework, "ccache_env", return_value={}),
|
|
):
|
|
env = framework.get_build_env(tmp_path)
|
|
assert env["PATH"] == str(tmp_path / "bin")
|
|
with (
|
|
patch.dict(
|
|
os.environ, {"PATH": f"/usr/bin{os.pathsep}{os.pathsep}/bin"}, clear=True
|
|
),
|
|
patch.object(framework, "ccache_env", return_value={}),
|
|
):
|
|
env = framework.get_build_env(tmp_path)
|
|
assert env["PATH"].split(os.pathsep) == [str(tmp_path / "bin"), "/usr/bin", "/bin"]
|
|
|
|
|
|
def test_ccache_env_accepts_a_preresolved_path() -> None:
|
|
"""A caller that already resolved ccache threads it through; the probe
|
|
must not run again (None means resolved-and-disabled)."""
|
|
with patch.object(framework, "ccache_path") as mock_resolve:
|
|
assert framework.ccache_env(None) == {}
|
|
env = framework.ccache_env("/usr/bin/ccache")
|
|
mock_resolve.assert_not_called()
|
|
assert env["CCACHE_DIR"].endswith("ccache")
|
|
|
|
|
|
def test_toolchain_tool_layout(tmp_path: Path) -> None:
|
|
"""One owner for the bin/xtensa-lx106-elf-<name> layout."""
|
|
tool = framework.toolchain_tool(tmp_path, "addr2line")
|
|
assert tool.parent == tmp_path / "bin"
|
|
assert tool.name.startswith("xtensa-lx106-elf-addr2line")
|
|
assert (tool.suffix == ".exe") is (os.name == "nt")
|