Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-22 21:17:55 -05:00
14 changed files with 228 additions and 211 deletions
@@ -151,6 +151,40 @@ def test_is_esphome_src_handles_backslash_paths() -> None:
assert not idedata._is_esphome_src(r"C:\b\src\esphome\core\app.h")
def test_idedata_from_build_dedupes_identical_command_shapes(
tmp_path: Path,
) -> None:
"""Translation units sharing one ninja rule (same command modulo
file/output) carry
identical includes, so only one per shape is tokenized; a differing
shape still contributes its includes."""
def _tu(name: str, inc: str) -> dict:
# ninja's compdb embeds the file and output strings verbatim
file = f"{ABS}build/src/esphome/core/{name}.cpp"
return _entry(
f"{ABS}build", file, f"/tools/g++ -I{ABS}inc/{inc} -c {file} -o {name}.o"
) | {"output": f"{name}.o"}
entries = [_tu(name, "shared") for name in ("application", "component", "helpers")]
entries.append(_tu("extra", "extra"))
compile_commands = tmp_path / "compile_commands.json"
compile_commands.write_text(json.dumps(entries))
with (
patch.object(idedata, "get_toolchain_includes", return_value=[]),
patch.object(idedata, "parse_entry", wraps=idedata.parse_entry) as spy,
):
data = idedata.idedata_from_build(compile_commands)
includes = set(data["includes"]["build"])
assert f"{ABS}inc/shared".replace("\\", "/") in {
i.replace("\\", "/") for i in includes
}
assert any("inc/extra" in i for i in includes)
# Representative + one distinct shape; the two same-shape duplicates
# are never tokenized
assert spy.call_count == 2
def test_idedata_from_build(tmp_path: Path) -> None:
"""Full transform: representative entry + include union + toolchain dirs."""
compile_commands = tmp_path / "compile_commands.json"
+9 -31
View File
@@ -112,33 +112,15 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
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)
env = framework.get_build_env(tmp_path, None)
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:
"""Delegates on every call; the env/PATH decision must not freeze for
the process lifetime."""
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()
assert framework.ccache_env(None) == {}
with patch.dict(os.environ, {"CCACHE_NOHASHDIR": "false"}, clear=True):
env = framework.ccache_env("/usr/bin/ccache")
# User-set values are respected; the rest get defaults
assert "CCACHE_NOHASHDIR" not in env
assert env["CCACHE_DEPEND"] == "1"
@@ -159,7 +141,7 @@ def test_get_build_env_without_path_has_no_empty_entry(tmp_path: Path) -> None:
patch.dict(os.environ, {}, clear=True),
patch.object(framework, "ccache_env", return_value={}),
):
env = framework.get_build_env(tmp_path)
env = framework.get_build_env(tmp_path, None)
assert env["PATH"] == str(tmp_path / "bin")
with (
patch.dict(
@@ -167,20 +149,16 @@ def test_get_build_env_without_path_has_no_empty_entry(tmp_path: Path) -> None:
),
patch.object(framework, "ccache_env", return_value={}),
):
env = framework.get_build_env(tmp_path)
env = framework.get_build_env(tmp_path, None)
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.dict(os.environ, {}, clear=True),
patch.object(framework, "ccache_path") as mock_resolve,
):
"""The caller resolves ccache once and threads it through; None means
resolved-and-disabled."""
with patch.dict(os.environ, {}, clear=True):
assert framework.ccache_env(None) == {}
env = framework.ccache_env("/usr/bin/ccache")
mock_resolve.assert_not_called()
assert env["CCACHE_DIR"].endswith("ccache")
+3 -3
View File
@@ -989,7 +989,7 @@ def test_prefetch_downloads_archives_concurrently(tmp_path: Path) -> None:
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch(
"esphome.espidf.framework.ThreadPoolExecutor", wraps=ThreadPoolExecutor
"esphome.framework_helpers.ThreadPoolExecutor", wraps=ThreadPoolExecutor
) as pool,
):
_prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None)
@@ -1008,7 +1008,7 @@ def test_prefetch_single_archive_uses_one_worker(tmp_path: Path) -> None:
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch(
"esphome.espidf.framework.ThreadPoolExecutor", wraps=ThreadPoolExecutor
"esphome.framework_helpers.ThreadPoolExecutor", wraps=ThreadPoolExecutor
) as pool,
):
_prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None)
@@ -1108,7 +1108,7 @@ def test_prefetch_finishes_progress_bar_and_cancels_queue(tmp_path: Path) -> Non
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch("esphome.espidf.framework.BatchDownloadProgress") as progress_cls,
patch(
"esphome.espidf.framework.ThreadPoolExecutor", wraps=ThreadPoolExecutor
"esphome.framework_helpers.ThreadPoolExecutor", wraps=ThreadPoolExecutor
) as pool_cls,
):
pool = MagicMock(wraps=ThreadPoolExecutor(max_workers=2))