[espidf] Read the ESP-IDF version in process instead of starting idf_tools (#19998)

This commit is contained in:
J. Nick Koston
2026-10-01 19:48:00 -05:00
committed by GitHub
parent 6479e079d2
commit 8617368294
4 changed files with 133 additions and 17 deletions
+25 -2
View File
@@ -16,7 +16,7 @@ import check_idf_py_equivalence as guard # noqa: E402
from esphome.build_gen import espidf as build_gen # noqa: E402
from esphome.core import CORE # noqa: E402
from esphome.espidf import toolchain # noqa: E402
from esphome.espidf import framework, toolchain # noqa: E402
@pytest.fixture(autouse=True)
@@ -74,10 +74,12 @@ def _run_check(
esphome_rcs: tuple[int, int] = (0, 0),
macro_matches: bool = True,
envs: list[dict[str, str]] | None = None,
versions: tuple[str | None, str | None, str] = ("5.5", "5.5", "5.5"),
) -> tuple[list[str], list[list[str]]]:
"""Run check() with idf.py replaced by ``side_effect``; return problems, calls.
``envs`` collects the env each idf.py call receives.
``envs`` collects the env each idf.py call receives. ``versions`` is what
version.txt, the version header and idf_tools report for the framework.
"""
calls: list[list[str]] = []
@@ -98,6 +100,9 @@ def _run_check(
patch.object(toolchain, "run_reconfigure", return_value=esphome_rcs[0]),
patch.object(toolchain, "_run_ninja", return_value=esphome_rcs[1]),
patch.object(build_gen, "idf_macro_matches", return_value=macro_matches),
patch.object(framework, "read_idf_version_txt", return_value=versions[0]),
patch.object(framework, "read_idf_version_header", return_value=versions[1]),
patch.object(framework, "idf_tools_version", return_value=versions[2]),
patch.object(guard.subprocess, "run", side_effect=run),
patch.dict(os.environ),
):
@@ -285,6 +290,24 @@ def test_check_fails_loudly_when_the_idf_macro_changed(tmp_path: Path) -> None:
assert calls == []
@pytest.mark.parametrize(
("versions", "source"),
[(("5.4", "5.5", "5.5"), "txt"), (("5.5", None, "5.5"), "header")],
)
def test_check_fails_loudly_when_the_version_read_drifts(
tmp_path: Path, versions: tuple[str | None, str | None, str], source: str
) -> None:
"""An IDF bump that changes how idf_tools reads its version must fail CI;
both sources are checked since a managed tree never reaches the header."""
tree = _make_tree(tmp_path)
problems, calls = _run_check(tree, versions=versions)
ours = versions[0] if source == "txt" else versions[1]
assert problems == [
guard.VERSION_DRIFT.format(ours=ours, source=source, theirs="5.5")
]
assert calls == []
def test_main_without_build_trees(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
+47 -6
View File
@@ -44,6 +44,7 @@ from esphome.espidf.framework import (
check_esp_idf_install,
get_framework_env,
get_idf_tools_path,
idf_tools_version,
)
from esphome.framework_helpers import _tar_extract_all, get_python_env_executable_path
@@ -1468,19 +1469,59 @@ def test_demote_unused_tools_already_patched_is_noop(tmp_path: Path) -> None:
# ---------------------------------------------------------------------------
def test_get_idf_version_parses_stdout(tmp_path: Path) -> None:
def _write_idf_version_header(root: Path, major: int, minor: int) -> None:
include = root / "components" / "esp_common" / "include"
include.mkdir(parents=True)
(include / "esp_idf_version.h").write_text(
f"#define ESP_IDF_VERSION_MAJOR {major}\n"
"/** Minor version number (x.X.x) */\n"
f"#define ESP_IDF_VERSION_MINOR {minor}\n"
"#define ESP_IDF_VERSION_PATCH 0\n",
encoding="utf-8",
)
def test_get_idf_version_reads_version_txt(tmp_path: Path) -> None:
"""version.txt wins and gives major.minor, as idf_tools returns it."""
(tmp_path / "version.txt").write_text("v5.5.5\n", encoding="utf-8")
_write_idf_version_header(tmp_path, 6, 1)
assert _get_idf_version(tmp_path) == "5.5"
def test_get_idf_version_falls_back_to_the_header(tmp_path: Path) -> None:
"""A version.txt that does not match (a git ref) defers to the header."""
(tmp_path / "version.txt").write_text("vrelease/v6.1\n", encoding="utf-8")
_write_idf_version_header(tmp_path, 6, 1)
assert _get_idf_version(tmp_path) == "6.1"
def test_get_idf_version_raises_without_a_source(tmp_path: Path) -> None:
with pytest.raises(RuntimeError, match="Can't get ESP-IDF version"):
_get_idf_version(tmp_path)
def test_get_idf_version_wraps_an_unreadable_source(tmp_path: Path) -> None:
"""A source that cannot be decoded keeps the RuntimeError contract."""
(tmp_path / "version.txt").write_bytes(b"\xff\xfev")
with pytest.raises(RuntimeError, match="Can't get ESP-IDF version") as info:
_get_idf_version(tmp_path)
assert isinstance(info.value.__cause__, UnicodeError)
def test_idf_tools_version_runs_the_framework_script(tmp_path: Path) -> None:
with patch(
"esphome.espidf.framework.run_command", return_value=(True, "5.1.2\n", "")
):
assert _get_idf_version(tmp_path) == "5.1.2"
"esphome.espidf.framework.run_command", return_value=(True, "5.5\n", "")
) as run:
assert idf_tools_version(tmp_path) == "5.5"
assert run.call_args.args[0][1].endswith("get_idf_version.py")
def test_get_idf_version_raises_on_failure(tmp_path: Path) -> None:
def test_idf_tools_version_raises_on_failure(tmp_path: Path) -> None:
with (
patch("esphome.espidf.framework.run_command", return_value=(False, "", "boom")),
pytest.raises(RuntimeError, match="Can't get ESP-IDF version"),
):
_get_idf_version(tmp_path)
idf_tools_version(tmp_path)
def test_get_idf_tool_paths_parses_json(tmp_path: Path) -> None: