[espidf] Skip the lwip sources that compile to empty objects (#20000)

This commit is contained in:
J. Nick Koston
2026-10-02 07:59:52 -05:00
committed by GitHub
parent a58f3d34aa
commit 797682cccc
5 changed files with 211 additions and 3 deletions
@@ -103,6 +103,7 @@ def _run_check(
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, "_lwip_empty_source_problems", return_value=[]),
patch.object(guard.subprocess, "run", side_effect=run),
patch.dict(os.environ),
):
@@ -319,6 +320,99 @@ def test_main_without_build_trees(
assert "No native ESP-IDF build tree found" in capsys.readouterr().out
def _make_lwip_tree(tmp_path: Path, objects: list[str], config: dict) -> Path:
"""A tree with lwip objects, their sdkconfig.json and an nm in the cache."""
tree = _make_tree(tmp_path)
objdir = tree / "build" / "esp-idf" / "lwip" / "CMakeFiles" / "__idf_lwip.dir"
for name in objects:
(objdir / name).parent.mkdir(parents=True, exist_ok=True)
(objdir / name).write_bytes(b"x")
(tree / "build" / "config").mkdir(parents=True, exist_ok=True)
(tree / "build" / "config" / "sdkconfig.json").write_text(json.dumps(config))
with (tree / "build" / "CMakeCache.txt").open("a") as cache:
cache.write("CMAKE_NM:FILEPATH=/tools/nm\n")
return tree
def _run_lwip_check(
tree: Path,
non_empty: set[str] = frozenset(),
failing: set[str] = frozenset(),
calls: list[list[str]] | None = None,
) -> list[str]:
"""Run the lwip check with nm faked; ``calls`` collects the nm commands."""
def run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess:
if calls is not None:
calls.append(cmd)
name = Path(cmd[-1]).name
if name in failing:
return subprocess.CompletedProcess(cmd, 1, "", "bad object")
return subprocess.CompletedProcess(
cmd, 0, "symbol\n" if name in non_empty else "", ""
)
with (
patch.object(toolchain, "run_reconfigure", return_value=0) as reconfigure,
patch.object(toolchain, "_run_ninja", return_value=0),
patch.object(guard.subprocess, "run", side_effect=run),
):
problems = guard._lwip_empty_source_problems(tree)
reconfigure.assert_called_once_with(
extra_env={build_gen.LWIP_FULL_SOURCES_ENV: "1"}
)
return problems
def test_lwip_check_inspects_only_the_dropped_sources(tmp_path: Path) -> None:
"""An option that is on, or absent (invisible), keeps its sources unchecked."""
tree = _make_lwip_tree(
tmp_path,
[
"lwip/src/netif/ppp/auth.c.obj",
"lwip/src/core/ipv6/ip6.c.obj",
"lwip/src/core/ipv4/autoip.c.obj",
],
{"LWIP_PPP_SUPPORT": False, "LWIP_IPV6": True},
)
calls: list[list[str]] = []
assert _run_lwip_check(tree, calls=calls) == []
assert [Path(c[-1]).name for c in calls] == ["auth.c.obj"]
def test_lwip_check_flags_a_dropped_source_with_symbols(tmp_path: Path) -> None:
tree = _make_lwip_tree(
tmp_path, ["lwip/src/netif/ppp/auth.c.obj"], {"LWIP_PPP_SUPPORT": False}
)
assert _run_lwip_check(tree, non_empty={"auth.c.obj"}) == [
guard.LWIP_NOT_EMPTY.format(source="auth.c", option="CONFIG_LWIP_PPP_SUPPORT")
]
def test_lwip_check_flags_a_failed_nm(tmp_path: Path) -> None:
"""A broken nm must not pass as an empty object."""
tree = _make_lwip_tree(
tmp_path, ["lwip/src/netif/ppp/auth.c.obj"], {"LWIP_PPP_SUPPORT": False}
)
assert _run_lwip_check(tree, failing={"auth.c.obj"}) == [
guard.LWIP_NM_FAILED.format(source="auth.c", error="bad object")
]
def test_lwip_check_fails_per_pattern_that_matched_nothing(tmp_path: Path) -> None:
"""A stale pattern is reported even while the others still match."""
tree = _make_lwip_tree(
tmp_path,
["lwip/src/netif/ppp/auth.c.obj"],
{"LWIP_PPP_SUPPORT": False, "LWIP_STATS": False},
)
assert _run_lwip_check(tree) == [
guard.LWIP_NOTHING_MATCHED.format(
regex="/core/stats[.]c$", option="CONFIG_LWIP_STATS"
)
]
@pytest.mark.parametrize(("problems", "rc"), [([], 0), (["idf.py changed x"], 1)])
def test_main_reports_each_tree(
tmp_path: Path,
+18
View File
@@ -222,6 +222,24 @@ def test_get_project_cmakelists_size_command_uses_json2() -> None:
assert "--format=json2" in content
def test_get_project_cmakelists_drops_empty_lwip_sources() -> None:
"""The filter comes after project(), where the lwip target exists."""
from esphome.build_gen.espidf import (
LWIP_EMPTY_SOURCES,
LWIP_FULL_SOURCES_ENV,
lwip_empty_source_gate,
)
content = _render()
filter_at = content.index(
"set_property(TARGET ${esphome_lwip_lib} PROPERTY SOURCES"
)
assert filter_at > content.index("project(")
assert f"NOT DEFINED ENV{{{LWIP_FULL_SOURCES_ENV}}}" in content
for entry in LWIP_EMPTY_SOURCES:
assert lwip_empty_source_gate(*entry) in content
def test_get_project_cmakelists_declares_map_as_link_byproduct() -> None:
"""The link declares the map so size can build in the same ninja run."""
content = _render()