mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 14:37:04 +00:00
623 lines
19 KiB
Python
623 lines
19 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,
|
|
_convert_library_to_component,
|
|
_parse_library_json,
|
|
_parse_library_properties,
|
|
_process_dependencies,
|
|
_split_list_by_condition,
|
|
generate_cmakelists_txt,
|
|
generate_idf_component_yml,
|
|
)
|
|
|
|
|
|
@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_overwrites_bundled_files(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
esp32_idf_core: None,
|
|
) -> None:
|
|
# A library that ships its own CMakeLists.txt + idf_component.yml must
|
|
# have both replaced by ESPHome's generated content. Library authors'
|
|
# bundled IDF metadata is frequently broken (bogus REQUIRES, hard-coded
|
|
# frameworks), so we always regenerate from library.json.
|
|
from esphome.espidf.component import _generate_idf_component
|
|
|
|
(tmp_path / "src").mkdir()
|
|
(tmp_path / "src" / "main.cpp").write_text("// dummy\n")
|
|
(tmp_path / "library.json").write_text(json.dumps({"name": "tripwire-lib"}))
|
|
(tmp_path / "CMakeLists.txt").write_text("# TRIPWIRE_BUNDLED_CMAKELISTS\n")
|
|
(tmp_path / "idf_component.yml").write_text("# TRIPWIRE_BUNDLED_MANIFEST\n")
|
|
|
|
fake_component = IDFComponent(
|
|
"owner/tripwire-lib", "1.0.0", source=URLSource("http://dummy")
|
|
)
|
|
fake_component.path = tmp_path
|
|
monkeypatch.setattr(
|
|
esphome.espidf.component,
|
|
"_convert_library_to_component",
|
|
lambda _lib: fake_component,
|
|
)
|
|
monkeypatch.setattr(fake_component, "download", lambda force=False: None)
|
|
|
|
_generate_idf_component(Library("owner/tripwire-lib", "1.0.0", None))
|
|
|
|
cml = (tmp_path / "CMakeLists.txt").read_text()
|
|
manifest = (tmp_path / "idf_component.yml").read_text()
|
|
assert "TRIPWIRE_BUNDLED_CMAKELISTS" not in cml
|
|
assert "TRIPWIRE_BUNDLED_MANIFEST" not in manifest
|
|
assert "idf_component_register" in cml
|
|
|
|
|
|
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_convert_library_with_repository():
|
|
lib = Library("name", None, "https://github.com/foo/bar.git#v1.2.3")
|
|
|
|
result = _convert_library_to_component(lib)
|
|
|
|
assert result.name == "foo/bar"
|
|
assert result.version == "*"
|
|
assert isinstance(result.source, GitSource)
|
|
assert result.source.ref == "v1.2.3"
|
|
|
|
|
|
def test_convert_library_with_branch_ref():
|
|
lib = Library("name", None, "https://github.com/foo/bar.git#some-branch")
|
|
|
|
result = _convert_library_to_component(lib)
|
|
|
|
assert result.name == "foo/bar"
|
|
assert result.version == "*"
|
|
assert isinstance(result.source, GitSource)
|
|
assert result.source.ref == "some-branch"
|
|
|
|
|
|
def test_convert_library_missing_ref_uses_default_branch():
|
|
"""A bare URL with no #ref clones the remote's default branch.
|
|
|
|
Matches PIO's lib_deps behavior and external_components handling --
|
|
git.clone_or_update with ref=None leaves the depth-1 clone on
|
|
whatever branch the remote HEAD points at.
|
|
"""
|
|
lib = Library("name", None, "https://github.com/foo/bar.git")
|
|
|
|
result = _convert_library_to_component(lib)
|
|
|
|
assert result.name == "foo/bar"
|
|
assert result.version == "*"
|
|
assert isinstance(result.source, GitSource)
|
|
assert result.source.ref is None
|
|
|
|
|
|
def test_convert_library_registry(monkeypatch):
|
|
lib = Library("foo/bar", "^1.0.0", None)
|
|
|
|
monkeypatch.setattr(
|
|
esphome.espidf.component,
|
|
"_get_package_from_pio_registry",
|
|
lambda o, n, r: ("foo", "bar", "1.2.3", "http://example.com/pkg.zip"),
|
|
)
|
|
|
|
result = _convert_library_to_component(lib)
|
|
|
|
assert result.name == "foo/bar"
|
|
assert result.version == "1.2.3"
|
|
assert isinstance(result.source, URLSource)
|
|
|
|
|
|
def test_process_dependencies_adds_valid_dependency(tmp_component, monkeypatch):
|
|
tmp_component.data = {
|
|
"dependencies": [
|
|
{
|
|
"name": "foo",
|
|
"version": "1.0",
|
|
}
|
|
]
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
esphome.espidf.component,
|
|
"_generate_idf_component",
|
|
lambda lib: esphome.espidf.component.IDFComponent(
|
|
lib.name, lib.version, source=URLSource("http://dummy.com")
|
|
),
|
|
)
|
|
|
|
monkeypatch.setattr(esphome.espidf.component, "_check_library_data", lambda x: None)
|
|
|
|
_process_dependencies(tmp_component)
|
|
|
|
assert len(tmp_component.dependencies) == 1
|
|
|
|
|
|
def test_process_dependencies_skips_invalid(tmp_component):
|
|
tmp_component.data = {
|
|
"dependencies": [
|
|
{"name": "foo", "version": "1.0", "platforms": ["arduino"]},
|
|
{"invalid": "entry"},
|
|
]
|
|
}
|
|
|
|
_process_dependencies(tmp_component)
|
|
|
|
assert tmp_component.dependencies == []
|
|
|
|
|
|
def test_process_dependencies_dict_form(tmp_component, monkeypatch):
|
|
"""PIO library.json shorthand ``{"owner/Name": "version"}`` is honored.
|
|
|
|
Iterating a dict gives string keys, which would silently fail the
|
|
``"name" in dependency`` substring check. Normalize to list-of-dicts
|
|
first so the dict form (used by e.g. tesla-ble for its nanopb dep)
|
|
is treated the same as the verbose list form.
|
|
"""
|
|
captured: list[Library] = []
|
|
|
|
def fake_generate(library):
|
|
captured.append(library)
|
|
return IDFComponent(
|
|
library.name, library.version, source=URLSource("http://dummy.com")
|
|
)
|
|
|
|
tmp_component.data = {
|
|
"dependencies": {
|
|
"nanopb/Nanopb": "^0.4.91",
|
|
"BareName": "1.2.3",
|
|
}
|
|
}
|
|
monkeypatch.setattr(
|
|
esphome.espidf.component, "_generate_idf_component", fake_generate
|
|
)
|
|
monkeypatch.setattr(esphome.espidf.component, "_check_library_data", lambda x: None)
|
|
|
|
_process_dependencies(tmp_component)
|
|
|
|
assert len(tmp_component.dependencies) == 2
|
|
names = sorted(lib.name for lib in captured)
|
|
versions = sorted(lib.version for lib in captured)
|
|
assert names == ["BareName", "nanopb/Nanopb"]
|
|
assert versions == ["1.2.3", "^0.4.91"]
|
|
|
|
|
|
def test_process_dependencies_dict_form_with_url_value(tmp_component, monkeypatch):
|
|
"""A dict-value that's a URL gets routed to ``repository`` like the list form."""
|
|
captured: list[Library] = []
|
|
|
|
def fake_generate(library):
|
|
captured.append(library)
|
|
return IDFComponent(library.name, "*", source=URLSource("http://dummy.com"))
|
|
|
|
tmp_component.data = {
|
|
"dependencies": {
|
|
"foo/Bar": "https://github.com/foo/bar.git#main",
|
|
}
|
|
}
|
|
monkeypatch.setattr(
|
|
esphome.espidf.component, "_generate_idf_component", fake_generate
|
|
)
|
|
monkeypatch.setattr(esphome.espidf.component, "_check_library_data", lambda x: None)
|
|
|
|
_process_dependencies(tmp_component)
|
|
|
|
assert len(captured) == 1
|
|
assert captured[0].name == "foo/Bar"
|
|
assert captured[0].version is None
|
|
assert captured[0].repository == "https://github.com/foo/bar.git#main"
|
|
|
|
|
|
def test_process_dependencies_dict_form_with_nested_spec(tmp_component, monkeypatch):
|
|
"""A dict-value that's itself a dict is merged into the entry.
|
|
|
|
PIO's library.json allows ``{"owner/Name": {"version": "...", ...}}``
|
|
for entries that need fields beyond just a version (platforms,
|
|
frameworks, etc.). The extra fields flow into _check_library_data
|
|
via the entry merge.
|
|
"""
|
|
captured: list[Library] = []
|
|
checked: list[dict] = []
|
|
|
|
def fake_generate(library):
|
|
captured.append(library)
|
|
return IDFComponent(
|
|
library.name, library.version, source=URLSource("http://dummy.com")
|
|
)
|
|
|
|
tmp_component.data = {
|
|
"dependencies": {
|
|
"nanopb/Nanopb": {"version": "^0.4.91", "platforms": "espidf"},
|
|
}
|
|
}
|
|
monkeypatch.setattr(
|
|
esphome.espidf.component, "_generate_idf_component", fake_generate
|
|
)
|
|
monkeypatch.setattr(
|
|
esphome.espidf.component,
|
|
"_check_library_data",
|
|
checked.append,
|
|
)
|
|
|
|
_process_dependencies(tmp_component)
|
|
|
|
assert len(captured) == 1
|
|
assert captured[0].name == "nanopb/Nanopb"
|
|
assert captured[0].version == "^0.4.91"
|
|
# Extra spec fields reach _check_library_data so platform/framework
|
|
# gating still applies.
|
|
assert checked == [
|
|
{
|
|
"name": "Nanopb",
|
|
"owner": "nanopb",
|
|
"version": "^0.4.91",
|
|
"platforms": "espidf",
|
|
}
|
|
]
|