Files
esphome/tests/unit_tests/test_espidf_component.py

807 lines
26 KiB
Python

import json
import os
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
Framework,
Platform,
)
from esphome.core import CORE, Library
import esphome.espidf.component
from esphome.espidf.component import (
GitSource,
IDFComponent,
InvalidIDFComponent,
URLSource,
_check_library_data,
_collect_filtered_files,
_node_key,
_normalize_dependencies,
_parse_library_json,
_parse_library_properties,
_resolve_registry_version,
_split_list_by_condition,
generate_cmakelists_txt,
generate_idf_component_yml,
generate_idf_components,
)
@pytest.fixture(name="tmp_component")
def fixture_tmp_component(tmp_path):
c = IDFComponent("owner/name", "1.0.0", source=MagicMock())
c.path = tmp_path
return c
@pytest.fixture(name="esp32_idf_core")
def fixture_esp32_idf_core():
CORE.data[KEY_CORE] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = str(Platform.ESP32)
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = str(Framework.ESP_IDF)
def test_idf_component_str():
c = IDFComponent("foo/bar", "1.0", source=URLSource("http://dummy.com"))
assert str(c) == "foo/bar@1.0=http://dummy.com"
def test_idf_component_sanitized_name():
c = IDFComponent("foo/bar bar-bar", "1.0", source=URLSource("http://dummy.com"))
assert c.get_sanitized_name() == "foo/bar_bar-bar"
def test_idf_component_require_name():
c = IDFComponent("foo/bar", "1.0", source=URLSource("http://dummy.com"))
assert c.get_require_name() == "foo__bar"
def test_collect_filtered_files_basic(tmp_path):
f1 = tmp_path / "a.c"
f2 = tmp_path / "b" / "b.cpp"
f1.write_text("int a;")
f2.parent.mkdir(parents=True)
f2.write_text("int b;")
result = _collect_filtered_files(tmp_path, ["+<*>"])
assert str(f1) in result
assert str(f2) in result
def test_collect_filtered_files_exclude(tmp_path):
f1 = tmp_path / "a.c"
f2 = tmp_path / "b.cpp"
f1.write_text("int a;")
f2.write_text("int b;")
result = _collect_filtered_files(tmp_path, ["+<*> -<*.cpp>"])
assert str(f1) in result
assert str(f2) not in result
def test_split_list_by_condition():
items = ["-Iinclude", "-Llib", "-Wall"]
matched, rest = _split_list_by_condition(
items, lambda x: x[2:] if x.startswith("-I") else None
)
assert matched == ["include"]
assert "-Llib" in rest
assert "-Wall" in rest
def test_generate_cmakelists_txt_basic(tmp_component):
src_dir = tmp_component.path / "src"
src_dir.mkdir()
f = src_dir / "main.c"
f.write_text("int main() {}")
tmp_component.data = {}
content = generate_cmakelists_txt(tmp_component)
assert "idf_component_register" in content
assert "main.c" in content
def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path):
src_dir = tmp_component.path / "src"
src_dir.mkdir()
(src_dir / "main.c").write_text("int main() {}")
dep = IDFComponent("dep", "1.0", source=URLSource("http://dummy.com"))
dep.path = tmp_path / "dep"
tmp_component.dependencies = [dep]
tmp_component.data = {
"build": {"flags": ["-Iinclude", "-Llib", "-lmylib", "-Wall", "-DTEST"]}
}
content = generate_cmakelists_txt(tmp_component)
sep = "\\\\" if os.name == "nt" else "/"
assert (
content
== f"""idf_component_register(
SRCS "src{sep}main.c"
INCLUDE_DIRS "src"
REQUIRES dep ${{ESPHOME_PROJECT_MANAGED_COMPONENTS}} ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
)
target_compile_options(${{COMPONENT_LIB}} PUBLIC
"-DTEST"
)
target_compile_options(${{COMPONENT_LIB}} PRIVATE
"-Wall"
)
target_link_directories(${{COMPONENT_LIB}} INTERFACE
"lib"
)
target_link_libraries(${{COMPONENT_LIB}} INTERFACE
"mylib"
)
"""
)
def test_generate_cmakelists_txt_references_project_managed_components_variable(
tmp_component: IDFComponent,
) -> None:
# The CMakeLists is cached under pio_components/<hash>/ and shared
# across projects, so the project-managed REQUIRES list is exposed via
# a CMake variable expanded at configure time rather than baked here.
src_dir = tmp_component.path / "src"
src_dir.mkdir()
(src_dir / "main.c").write_text("int main() {}")
tmp_component.data = {}
content = generate_cmakelists_txt(tmp_component)
assert "${ESPHOME_PROJECT_MANAGED_COMPONENTS}" in content
def test_generate_idf_component_yml_basic(tmp_component):
tmp_component.data = {"description": "test", "repository": {"url": "http://aaa"}}
result = generate_idf_component_yml(tmp_component)
assert result == "description: test\nrepository: http://aaa\n"
def test_generate_idf_component_yml_with_dependencies(tmp_component, tmp_path):
dep = IDFComponent("dep", "1.0", source=URLSource("http://dummy.com"))
dep.path = tmp_path / "dep"
tmp_component.dependencies = [dep]
tmp_component.data = {}
result = generate_idf_component_yml(tmp_component)
assert (
result
== f"""dependencies:
dep:
override_path: {dep.path}
"""
)
def test_generate_idf_component_yml_missing_path_raises(tmp_component):
# A dep without a path is a contract violation — every dep is expected
# to have been downloaded before YAML generation. Raise loudly.
dep = IDFComponent("foo/bar", "1.0", source=None)
tmp_component.dependencies = [dep]
tmp_component.data = {}
with pytest.raises(RuntimeError):
generate_idf_component_yml(tmp_component)
def test_check_library_data_valid(esp32_idf_core):
_check_library_data({"platforms": "*", "frameworks": "*"})
def test_check_library_data_valid2(esp32_idf_core):
_check_library_data({"platforms": "*"})
def test_check_library_data_valid3(esp32_idf_core):
_check_library_data({})
def test_check_library_data_valid4(esp32_idf_core):
_check_library_data({"platforms": "espressif32", "frameworks": "*"})
def test_check_library_data_valid5(esp32_idf_core):
_check_library_data({"platforms": "*", "frameworks": "espidf"})
def test_check_library_data_invalid_platform(esp32_idf_core):
with pytest.raises(InvalidIDFComponent):
_check_library_data({"platforms": ["other"], "frameworks": "*"})
def test_check_library_data_invalid_framework(
esp32_idf_core: None, caplog: pytest.LogCaptureFixture
) -> None:
# Framework mismatch is a warning, not a hard skip: the library is still
# included so that PIO manifests that only list "arduino" (but actually
# compile under IDF) can be used without forking them.
_check_library_data({"name": "lib", "platforms": "*", "frameworks": ["other"]})
assert "do not include 'espidf'" in caplog.text
def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
from esphome.espidf.extra_script import captured_as_build_flags, run_extra_script
(tmp_path / "src" / "esp32").mkdir(parents=True)
script = tmp_path / "extra_script.py"
script.write_text(
"Import('env')\n"
"mcu = env.get('BOARD_MCU')\n"
"env.Append(\n"
" LIBPATH=[join('src', mcu)],\n"
" LIBS=['algobsec'],\n"
" CPPDEFINES=['FOO', ('BAR', '1')],\n"
" LINKFLAGS=['-Wl,--gc-sections'],\n"
")\n"
)
# The script uses bare ``join`` (PIO's extra-scripts run inside SCons
# where this is in scope). Inject it via the script header so the
# shim's exec namespace can resolve it.
script.write_text("from os.path import join\n" + script.read_text())
result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32")
assert result.libpath == [str(Path("src") / "esp32")]
assert result.libs == ["algobsec"]
assert ("BAR", "1") in result.cppdefines
assert "FOO" in result.cppdefines
assert result.linkflags == ["-Wl,--gc-sections"]
flags = captured_as_build_flags(result, library_dir=tmp_path)
sep = os.sep
assert f"-Lsrc{sep}esp32" in flags
assert "-lalgobsec" in flags
assert "-DFOO" in flags
assert "-DBAR=1" in flags
assert "-Wl,--gc-sections" in flags
def test_extra_script_libpath_relative_resolves_against_library_dir(
tmp_path, monkeypatch
):
"""Relative LIBPATH entries must resolve against ``library_dir``, not the
caller's CWD (the shim restores CWD before ``captured_as_build_flags``
runs)."""
from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags
(tmp_path / "lib" / "esp32").mkdir(parents=True)
elsewhere = tmp_path.parent / "not_the_library_dir"
elsewhere.mkdir(exist_ok=True)
monkeypatch.chdir(elsewhere)
result = ExtraScriptResult(libpath=["lib/esp32"])
flags = captured_as_build_flags(result, library_dir=tmp_path)
sep = os.sep
assert flags == [f"-Llib{sep}esp32"]
def test_extra_script_libpath_absolute_outside_library_dir(tmp_path):
from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags
outside = tmp_path.parent / "system_lib"
outside.mkdir(exist_ok=True)
result = ExtraScriptResult(libpath=[str(outside)])
flags = captured_as_build_flags(result, library_dir=tmp_path)
assert flags == [f"-L{outside.resolve()}"]
def test_extra_script_failure_returns_empty_result(tmp_path, caplog):
from esphome.espidf.extra_script import run_extra_script
script = tmp_path / "broken.py"
script.write_text("raise RuntimeError('boom')\n")
with caplog.at_level("WARNING"):
result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32")
assert result.libpath == []
assert result.libs == []
assert "broken.py" in caplog.text
def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
from esphome.espidf.component import _apply_extra_script
library_dir = tmp_path / "lib"
library_dir.mkdir()
outside = tmp_path / "evil.py"
outside.write_text("env.Append(LIBS=['pwned'])\n")
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = library_dir
c.data = {"build": {"extraScript": "../evil.py"}}
_apply_extra_script(c)
# Nothing was folded into flags: the traversal was rejected before
# the script could run.
assert "flags" not in c.data["build"]
def test_apply_extra_script_merges_into_existing_flags(tmp_path, monkeypatch):
from esphome.components import esp32 as esp32_module
monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32")
from esphome.espidf.component import _apply_extra_script
(tmp_path / "src").mkdir()
script = tmp_path / "extra.py"
script.write_text("env.Append(LIBS=['algobsec'])\n")
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = tmp_path
c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}}
_apply_extra_script(c)
assert "-DEXISTING" in c.data["build"]["flags"]
assert "-lalgobsec" in c.data["build"]["flags"]
def test_parse_library_json(tmp_path):
f = tmp_path / "library.json"
f.write_text(json.dumps({"name": "test"}))
result = _parse_library_json(f)
assert result["name"] == "test"
def test_parse_library_properties(tmp_path):
f = tmp_path / "library.properties"
f.write_text(
"""
name=Test
version=1.0
# description=ABCD
empty=
"""
)
result = _parse_library_properties(f)
assert result["name"] == "Test"
assert result["version"] == "1.0"
assert "empty" not in result
def test_node_key_git_with_ref():
key, is_git, locator = _node_key(
"name", None, "https://github.com/foo/bar.git#v1.2.3"
)
assert key == "foo/bar"
assert is_git is True
assert locator == ("https://github.com/foo/bar.git", "v1.2.3")
def test_node_key_git_branch_ref():
key, is_git, locator = _node_key(
"name", None, "https://github.com/foo/bar.git#some-branch"
)
assert (key, is_git, locator[1]) == ("foo/bar", True, "some-branch")
def test_node_key_git_no_ref():
_key, is_git, locator = _node_key("name", None, "https://github.com/foo/bar.git")
assert is_git is True
assert locator == ("https://github.com/foo/bar.git", None)
def test_node_key_registry_owner_name():
key, is_git, locator = _node_key("foo/bar", "^1.0.0", None)
assert (key, is_git, locator) == ("foo/bar", False, ("foo", "bar"))
def test_node_key_registry_bare_name():
key, is_git, locator = _node_key("bar", "1.0", None)
assert (key, is_git, locator) == ("bar", False, (None, "bar"))
def test_normalize_dependencies_none():
assert _normalize_dependencies(None) == []
def test_normalize_dependencies_list_form():
deps = [{"name": "foo", "version": "1.0"}]
assert _normalize_dependencies(deps) == [{"name": "foo", "version": "1.0"}]
def test_normalize_dependencies_dict_form():
out = _normalize_dependencies({"nanopb/Nanopb": "^0.4.91", "BareName": "1.2.3"})
assert {"name": "Nanopb", "owner": "nanopb", "version": "^0.4.91"} in out
assert {"name": "BareName", "owner": None, "version": "1.2.3"} in out
def test_normalize_dependencies_dict_form_nested_spec():
out = _normalize_dependencies(
{"nanopb/Nanopb": {"version": "^0.4.91", "platforms": "espidf"}}
)
assert out == [
{
"name": "Nanopb",
"owner": "nanopb",
"version": "^0.4.91",
"platforms": "espidf",
}
]
def _patch_registry(monkeypatch, versions):
"""Patch the registry client to serve a canned version list (no network).
Only ``fetch_registry_package`` is faked; the real
``get_compatible_registry_versions`` / ``pick_best_registry_version`` run on
the canned data so the intersection logic is exercised for real.
"""
registry = esphome.espidf.component._make_registry_client()
monkeypatch.setattr(
registry,
"fetch_registry_package",
lambda spec: {
"owner": {"username": spec.owner or "owner"},
"name": spec.name,
"versions": [
{"name": v, "files": [{"download_url": f"http://x/{v}.tar.gz"}]}
for v in versions
],
},
)
monkeypatch.setattr(
esphome.espidf.component, "_make_registry_client", lambda: registry
)
def test_resolve_registry_version_intersects_constraints(monkeypatch):
_patch_registry(monkeypatch, ["1.10018.1", "1.10021.0", "1.10021.1"])
owner, name, version, url = _resolve_registry_version(
"esphome", "libsodium", {"==1.10021.0", "^1.10018.1"}
)
assert (owner, name, version) == ("esphome", "libsodium", "1.10021.0")
assert url == "http://x/1.10021.0.tar.gz"
def test_resolve_registry_version_picks_highest_satisfying(monkeypatch):
_patch_registry(monkeypatch, ["1.0.0", "1.5.0", "2.0.0"])
_owner, _name, version, _url = _resolve_registry_version("o", "p", {"^1.0.0"})
assert version == "1.5.0"
def test_resolve_registry_version_conflict_raises(monkeypatch):
_patch_registry(monkeypatch, ["1.0.0", "2.0.0"])
with pytest.raises(RuntimeError, match="satisfies all requirements"):
_resolve_registry_version("o", "p", {"==1.0.0", "==2.0.0"})
def test_generate_idf_components_dedupes_shared_dependency(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
esp32_idf_core: None,
) -> None:
# A and B both depend on shared C under different version specs. The batch
# must resolve C once with BOTH requirements collected, wire a single C
# instance into both, and regenerate (overwrite) each library's build files.
manifests = {
"esphome/A": {
"name": "A",
"dependencies": [
{"owner": "esphome", "name": "C", "version": "==1.10021.0"}
],
},
"esphome/B": {
"name": "B",
"dependencies": [
{"owner": "esphome", "name": "C", "version": "^1.10018.1"}
],
},
"esphome/C": {"name": "C"},
}
def fake_download(self, force=False):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "src" / "x.c").write_text("int x;")
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
(self.path / "CMakeLists.txt").write_text("# TRIPWIRE\n")
monkeypatch.setattr(IDFComponent, "download", fake_download)
captured: dict[str, set[str]] = {}
resolve_calls: list[str] = []
def fake_resolve(owner, pkgname, requirements):
resolve_calls.append(pkgname)
captured[f"{owner}/{pkgname}"] = set(requirements)
version = "1.10021.0" if pkgname == "C" else "1.0.0"
return owner, pkgname, version, f"http://x/{pkgname}.tar.gz"
monkeypatch.setattr(
esphome.espidf.component, "_resolve_registry_version", fake_resolve
)
top = generate_idf_components(
[Library("esphome/A", "1.0.0", None), Library("esphome/B", "1.0.0", None)]
)
# C resolved once (not once per consumer) with BOTH requirements gathered.
assert captured["esphome/C"] == {"==1.10021.0", "^1.10018.1"}
assert resolve_calls.count("C") == 1
# Top-level components returned in request order.
assert [c.name for c in top] == ["esphome/A", "esphome/B"]
# A and B reference the SAME single C instance (deduped).
a_dep = top[0].dependencies[0]
b_dep = top[1].dependencies[0]
assert a_dep.name == "esphome/C"
assert a_dep is b_dep
# The bundled CMakeLists was overwritten with generated content.
generated = (a_dep.path / "CMakeLists.txt").read_text()
assert "TRIPWIRE" not in generated
assert "idf_component_register" in generated
def test_generate_idf_components_handles_dependency_cycle(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
esp32_idf_core: None,
) -> None:
# A -> B -> A. Must terminate (not recurse forever) and wire the cycle with
# a single instance per component.
manifests = {
"esphome/A": {
"name": "A",
"dependencies": [{"owner": "esphome", "name": "B", "version": "1.0.0"}],
},
"esphome/B": {
"name": "B",
"dependencies": [{"owner": "esphome", "name": "A", "version": "1.0.0"}],
},
}
def fake_download(self, force=False):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "src" / "x.c").write_text("int x;")
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
monkeypatch.setattr(IDFComponent, "download", fake_download)
monkeypatch.setattr(
esphome.espidf.component,
"_resolve_registry_version",
lambda owner, pkgname, requirements: (
owner,
pkgname,
"1.0.0",
f"http://x/{pkgname}.tar.gz",
),
)
top = generate_idf_components([Library("esphome/A", "1.0.0", None)])
assert [c.name for c in top] == ["esphome/A"]
component_a = top[0]
component_b = component_a.dependencies[0]
assert component_b.name == "esphome/B"
# The cycle is wired back to the same A instance, not a duplicate.
assert component_b.dependencies[0] is component_a
def test_generate_idf_components_git_overrides_registry_warns(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
esp32_idf_core: None,
caplog: pytest.LogCaptureFixture,
) -> None:
# A pulls shared as a registry pin; B pulls the same component from a git
# source. The git source wins, but the dropped registry pin must be warned
# about (not silently discarded).
manifests = {
"esphome/A": {
"name": "A",
"dependencies": [
{"owner": "esphome", "name": "shared", "version": "==1.0.0"}
],
},
"esphome/B": {
"name": "B",
"dependencies": [
{
"owner": "esphome",
"name": "shared",
"version": "https://github.com/esphome/shared.git#main",
}
],
},
"esphome/shared": {"name": "shared"},
}
def fake_download(self, force=False):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "src" / "x.c").write_text("int x;")
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
monkeypatch.setattr(IDFComponent, "download", fake_download)
monkeypatch.setattr(
esphome.espidf.component,
"_resolve_registry_version",
lambda owner, pkgname, requirements: (
owner,
pkgname,
"1.0.0",
f"http://x/{pkgname}.tar.gz",
),
)
top = generate_idf_components(
[Library("esphome/A", "1.0.0", None), Library("esphome/B", "1.0.0", None)]
)
# shared resolved from the git source (version "*"), not the registry pin.
shared = top[0].dependencies[0]
assert shared.name == "esphome/shared"
assert isinstance(shared.source, GitSource)
assert "using the git source" in caplog.text
assert "==1.0.0" in caplog.text
def test_generate_idf_components_missing_manifest_raises(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
esp32_idf_core: None,
) -> None:
# A library with neither library.json nor library.properties is invalid;
# fail loudly rather than silently generating build files for it.
def fake_download(self, force=False):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
# no library.json / library.properties written
monkeypatch.setattr(IDFComponent, "download", fake_download)
monkeypatch.setattr(
esphome.espidf.component,
"_resolve_registry_version",
lambda owner, pkgname, requirements: (
owner,
pkgname,
"1.0.0",
f"http://x/{pkgname}.tar.gz",
),
)
with pytest.raises(RuntimeError, match="missing library.json"):
generate_idf_components([Library("esphome/A", "1.0.0", None)])
def test_generate_idf_components_warns_on_noncanonical_duplicate(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
esp32_idf_core: None,
caplog: pytest.LogCaptureFixture,
) -> None:
# A references "shared" (bare) and B references "owner/shared"; both resolve
# to the same canonical name but as distinct graph nodes, so they aren't
# deduplicated -- warn about it.
manifests = {
"esphome/A": {
"name": "A",
"dependencies": [{"name": "shared", "version": "1.0.0"}],
},
"esphome/B": {
"name": "B",
"dependencies": [{"owner": "owner", "name": "shared", "version": "1.0.0"}],
},
"owner/shared": {"name": "shared"},
}
def fake_download(self, force=False):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "src" / "x.c").write_text("int x;")
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
monkeypatch.setattr(IDFComponent, "download", fake_download)
# Bare "shared" and "owner/shared" both resolve to canonical owner/shared.
monkeypatch.setattr(
esphome.espidf.component,
"_resolve_registry_version",
lambda owner, pkgname, requirements: (
owner or "owner",
pkgname,
"1.0.0",
f"http://x/{pkgname}.tar.gz",
),
)
generate_idf_components(
[Library("esphome/A", "1.0.0", None), Library("esphome/B", "1.0.0", None)]
)
assert "referenced under multiple names" in caplog.text
def test_generate_idf_components_incompatible_top_level_raises(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
esp32_idf_core: None,
) -> None:
# A top-level library that isn't ESP-IDF/esp32 compatible must fail fast,
# not be silently dropped.
def fake_download(self, force=False):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "library.json").write_text(
json.dumps({"name": "A", "platforms": ["espressif8266"]})
)
monkeypatch.setattr(IDFComponent, "download", fake_download)
monkeypatch.setattr(
esphome.espidf.component,
"_resolve_registry_version",
lambda owner, pkgname, requirements: (
owner,
pkgname,
"1.0.0",
f"http://x/{pkgname}.tar.gz",
),
)
with pytest.raises(RuntimeError, match="not compatible with ESP-IDF"):
generate_idf_components([Library("esphome/A", "1.0.0", None)])
def test_generate_idf_components_incompatible_dependency_skipped(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
esp32_idf_core: None,
) -> None:
# An incompatible *transitive* dependency is skipped (not fatal): A is fine,
# its esp8266-only dep B is dropped and not wired.
manifests = {
"esphome/A": {
"name": "A",
"dependencies": [{"owner": "esphome", "name": "B", "version": "1.0.0"}],
},
"esphome/B": {"name": "B", "platforms": ["espressif8266"]},
}
def fake_download(self, force=False):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
monkeypatch.setattr(IDFComponent, "download", fake_download)
monkeypatch.setattr(
esphome.espidf.component,
"_resolve_registry_version",
lambda owner, pkgname, requirements: (
owner,
pkgname,
"1.0.0",
f"http://x/{pkgname}.tar.gz",
),
)
top = generate_idf_components([Library("esphome/A", "1.0.0", None)])
assert [c.name for c in top] == ["esphome/A"]
# The incompatible dependency was dropped, not wired in.
assert top[0].dependencies == []