Files
esphome/tests/unit_tests/test_espidf_component.py
Diorcet Yann e9cc10fedc [core] Native idf full support (#14678)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-05-11 02:12:07 +00:00

358 lines
9.3 KiB
Python

import json
import os
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,
_detect_requires,
_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_detect_requires(tmp_path):
f = tmp_path / "main.c"
f.write_text('#include "mbedtls/foo.h"')
result = _detect_requires([str(f)])
assert "mbedtls" in result
def test_detect_requires_ignores_invalid_file(tmp_path):
result = _detect_requires([str(tmp_path / "missing.c")])
assert result == set()
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
)
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_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\nversion: 1.0.0\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"""version: 1.0.0
dependencies:
dep:
version: '1.0'
override_path: {dep.path}
"""
)
def test_generate_idf_component_yml_arduino_registry_dep(tmp_component):
# Synthetic arduino-esp32 dep with no source / no path: should emit a
# version-only entry so the IDF component manager resolves it from the
# registry instead of via git.
dep = IDFComponent("espressif/arduino-esp32", "3.3.8", source=None)
tmp_component.dependencies = [dep]
tmp_component.data = {}
result = generate_idf_component_yml(tmp_component)
assert (
result
== """version: 1.0.0
dependencies:
espressif/arduino-esp32:
version: 3.3.8
"""
)
def test_generate_idf_component_yml_missing_path_reraises(tmp_component):
# A dep without a path and without a recognised source should re-raise
# the underlying RuntimeError instead of silently producing a bad manifest.
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):
with pytest.raises(InvalidIDFComponent):
_check_library_data({"platforms": "*", "frameworks": ["other"]})
def test_extra_script_logs_warning(caplog, esp32_idf_core):
extra_script = "myscript.sh"
with caplog.at_level("WARNING"):
_check_library_data({"build": {"extraScript": extra_script}})
assert "not supported" in caplog.text
assert "myscript.sh" in caplog.text
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 == "1.2.3"
assert isinstance(result.source, GitSource)
def test_convert_library_missing_ref():
lib = Library("name", None, "https://github.com/foo/bar.git")
with pytest.raises(ValueError):
_convert_library_to_component(lib)
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 == []