mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
[espidf] Regenerate bundled CMakeLists; auto-REQUIRE via IDF build properties (#16406)
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""Tests for esphome.build_gen.espidf module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
KEY_REPO,
|
||||
)
|
||||
from esphome.const import KEY_CORE
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_core(tmp_path: Path) -> None:
|
||||
"""Give each test its own CORE.build_path and a clean esp32 data slot."""
|
||||
CORE.build_path = str(tmp_path)
|
||||
CORE.data.setdefault(KEY_CORE, {})
|
||||
CORE.data[KEY_ESP32] = {KEY_COMPONENTS: {}}
|
||||
|
||||
|
||||
def _write_project_description(tmp_path: Path, components: dict[str, str]) -> None:
|
||||
"""Stub a project_description.json with the given component_name -> dir map."""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir(exist_ok=True)
|
||||
(build_dir / "project_description.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"build_component_info": {
|
||||
name: {"dir": dir_} for name, dir_ in components.items()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_get_available_components_returns_none_without_build_path() -> None:
|
||||
"""No build_path set yet: must not raise on Path(None)."""
|
||||
CORE.build_path = None
|
||||
from esphome.build_gen.espidf import get_available_components
|
||||
|
||||
assert get_available_components() is None
|
||||
|
||||
|
||||
def test_get_available_components_returns_none_without_project_description(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from esphome.build_gen.espidf import get_available_components
|
||||
|
||||
assert get_available_components() is None
|
||||
|
||||
|
||||
def test_get_available_components_filters_src_managed_and_pio(tmp_path: Path) -> None:
|
||||
"""Built-ins are returned; src/, managed_components/, pio_components/ skipped."""
|
||||
_write_project_description(
|
||||
tmp_path,
|
||||
{
|
||||
"src": f"{tmp_path}/src",
|
||||
"esp_lcd": "/idf/components/esp_lcd",
|
||||
"espressif__arduino-esp32": f"{tmp_path}/managed_components/arduino",
|
||||
"JPEGDEC": f"{tmp_path}/pio_components/arduino/abc/bitbank2/JPEGDEC",
|
||||
"freertos": "/idf/components/freertos",
|
||||
},
|
||||
)
|
||||
from esphome.build_gen.espidf import get_available_components
|
||||
|
||||
assert sorted(get_available_components()) == ["esp_lcd", "freertos"]
|
||||
|
||||
|
||||
def test_get_project_cmakelists_minimal_omits_builtin_components_property(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Minimal write must not emit ESPHOME_PROJECT_BUILTIN_COMPONENTS even
|
||||
when project_description.json exists (the data may be stale on the
|
||||
first write before the discovery pass refreshes it)."""
|
||||
_write_project_description(tmp_path, {"esp_lcd": "/idf/components/esp_lcd"})
|
||||
|
||||
with (
|
||||
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||
patch.object(CORE, "name", "test"),
|
||||
):
|
||||
from esphome.build_gen.espidf import get_project_cmakelists
|
||||
|
||||
content = get_project_cmakelists(minimal=True)
|
||||
|
||||
assert "ESPHOME_PROJECT_BUILTIN_COMPONENTS" not in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_full_emits_builtin_components_property(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Non-minimal write emits one idf_build_set_property line per built-in,
|
||||
sorted, and excludes src/managed/pio components."""
|
||||
_write_project_description(
|
||||
tmp_path,
|
||||
{
|
||||
"src": f"{tmp_path}/src",
|
||||
"esp_lcd": "/idf/components/esp_lcd",
|
||||
"freertos": "/idf/components/freertos",
|
||||
"espressif__esp-dsp": f"{tmp_path}/managed_components/esp-dsp",
|
||||
"JPEGDEC": f"{tmp_path}/pio_components/arduino/abc/bitbank2/JPEGDEC",
|
||||
},
|
||||
)
|
||||
|
||||
with (
|
||||
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||
patch.object(CORE, "name", "test"),
|
||||
):
|
||||
from esphome.build_gen.espidf import get_project_cmakelists
|
||||
|
||||
content = get_project_cmakelists(minimal=False)
|
||||
|
||||
assert (
|
||||
"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS esp_lcd APPEND)"
|
||||
in content
|
||||
)
|
||||
assert (
|
||||
"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS freertos APPEND)"
|
||||
in content
|
||||
)
|
||||
# Excluded by get_available_components filtering.
|
||||
assert "espressif__esp-dsp APPEND" not in content
|
||||
assert "JPEGDEC APPEND" not in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_emits_managed_components_property(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""ESPHOME_PROJECT_MANAGED_COMPONENTS is always emitted (both modes)
|
||||
from the esp32 add_idf_component registry."""
|
||||
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {
|
||||
"espressif/esp-dsp": {KEY_REPO: None, KEY_REF: "1.7.1", KEY_PATH: None},
|
||||
"espressif/arduino-esp32": {KEY_REPO: None, KEY_REF: "3.3.8", KEY_PATH: None},
|
||||
}
|
||||
|
||||
with (
|
||||
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||
patch.object(CORE, "name", "test"),
|
||||
):
|
||||
from esphome.build_gen.espidf import get_project_cmakelists
|
||||
|
||||
for minimal in (True, False):
|
||||
content = get_project_cmakelists(minimal=minimal)
|
||||
assert (
|
||||
"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS"
|
||||
" espressif__arduino-esp32 APPEND)"
|
||||
) in content
|
||||
assert (
|
||||
"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS"
|
||||
" espressif__esp-dsp APPEND)"
|
||||
) in content
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -21,7 +22,6 @@ from esphome.espidf.component import (
|
||||
_check_library_data,
|
||||
_collect_filtered_files,
|
||||
_convert_library_to_component,
|
||||
_detect_requires,
|
||||
_parse_library_json,
|
||||
_parse_library_properties,
|
||||
_process_dependencies,
|
||||
@@ -83,19 +83,6 @@ def test_collect_filtered_files_exclude(tmp_path):
|
||||
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"]
|
||||
|
||||
@@ -142,7 +129,7 @@ def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path):
|
||||
== f"""idf_component_register(
|
||||
SRCS "src{sep}main.c"
|
||||
INCLUDE_DIRS "src"
|
||||
REQUIRES dep
|
||||
REQUIRES dep ${{ESPHOME_PROJECT_MANAGED_COMPONENTS}} ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
||||
)
|
||||
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
||||
"-DTEST"
|
||||
@@ -160,6 +147,58 @@ target_link_libraries(${{COMPONENT_LIB}} INTERFACE
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
@@ -187,27 +226,6 @@ dependencies:
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user