mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 08:20:21 +00:00
[core] Support platformio_options on the native ESP-IDF toolchain (#16917)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -515,7 +516,7 @@ def test_generate_idf_components_dedupes_shared_dependency(
|
||||
"esphome/C": {"name": "C"},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False):
|
||||
def fake_download(self, force=False, salt=""):
|
||||
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;")
|
||||
@@ -557,6 +558,62 @@ def test_generate_idf_components_dedupes_shared_dependency(
|
||||
assert "idf_component_register" in generated
|
||||
|
||||
|
||||
def test_generate_idf_components_lib_ignore_filters_top_level_and_dependencies(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
esp32_idf_core: None,
|
||||
) -> None:
|
||||
# lib_ignore must drop B at the top level and C when it is discovered as a
|
||||
# dependency of A during the graph walk -- neither may be resolved,
|
||||
# downloaded, or wired into a manifest. Matching is by lowercase short name.
|
||||
manifests = {
|
||||
"esphome/A": {
|
||||
"name": "A",
|
||||
"dependencies": [
|
||||
{"owner": "esphome", "name": "C", "version": "==1.10021.0"}
|
||||
],
|
||||
},
|
||||
"esphome/B": {"name": "B"},
|
||||
}
|
||||
|
||||
download_salts: list[str] = []
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
download_salts.append(salt)
|
||||
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)
|
||||
|
||||
resolve_calls: list[str] = []
|
||||
|
||||
def fake_resolve(owner, pkgname, requirements):
|
||||
resolve_calls.append(pkgname)
|
||||
return owner, pkgname, "1.0.0", f"http://x/{pkgname}.tar.gz"
|
||||
|
||||
monkeypatch.setattr(
|
||||
esphome.espidf.component, "_resolve_registry_version", fake_resolve
|
||||
)
|
||||
# lib_ignore is read from CORE.platformio_options (stored there by
|
||||
# _add_platformio_options); matched by lowercase short name.
|
||||
monkeypatch.setattr(CORE, "platformio_options", {"lib_ignore": ["B", "esphome/C"]})
|
||||
|
||||
top = generate_idf_components(
|
||||
[Library("esphome/A", "1.0.0", None), Library("esphome/B", "1.0.0", None)]
|
||||
)
|
||||
|
||||
assert [c.name for c in top] == ["esphome/A"]
|
||||
# Ignored libraries were never resolved (and therefore never downloaded).
|
||||
assert resolve_calls == ["A"]
|
||||
# The ignored dependency is not wired into A's manifest.
|
||||
assert top[0].dependencies == []
|
||||
# lib_ignore changes the generated wiring, so the cache path is salted to
|
||||
# keep this conversion separate from ones with a different lib_ignore.
|
||||
assert download_salts == [hashlib.sha256(b"b,c").hexdigest()[:8]]
|
||||
|
||||
|
||||
def test_generate_idf_components_handles_dependency_cycle(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -575,7 +632,7 @@ def test_generate_idf_components_handles_dependency_cycle(
|
||||
},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False):
|
||||
def fake_download(self, force=False, salt=""):
|
||||
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;")
|
||||
@@ -632,7 +689,7 @@ def test_generate_idf_components_git_overrides_registry_warns(
|
||||
"esphome/shared": {"name": "shared"},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False):
|
||||
def fake_download(self, force=False, salt=""):
|
||||
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;")
|
||||
@@ -669,7 +726,7 @@ def test_generate_idf_components_missing_manifest_raises(
|
||||
) -> 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):
|
||||
def fake_download(self, force=False, salt=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
# no library.json / library.properties written
|
||||
@@ -711,7 +768,7 @@ def test_generate_idf_components_warns_on_noncanonical_duplicate(
|
||||
"owner/shared": {"name": "shared"},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False):
|
||||
def fake_download(self, force=False, salt=""):
|
||||
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;")
|
||||
@@ -744,7 +801,7 @@ def test_generate_idf_components_incompatible_top_level_raises(
|
||||
) -> 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):
|
||||
def fake_download(self, force=False, salt=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "library.json").write_text(
|
||||
@@ -782,7 +839,7 @@ def test_generate_idf_components_incompatible_dependency_skipped(
|
||||
"esphome/B": {"name": "B", "platforms": ["espressif8266"]},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False):
|
||||
def fake_download(self, force=False, salt=""):
|
||||
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]))
|
||||
@@ -804,3 +861,54 @@ def test_generate_idf_components_incompatible_dependency_skipped(
|
||||
assert [c.name for c in top] == ["esphome/A"]
|
||||
# The incompatible dependency was dropped, not wired in.
|
||||
assert top[0].dependencies == []
|
||||
|
||||
|
||||
def test_url_source_salt_changes_cache_path(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The salt is mixed into the URL hash so salted conversions get their own
|
||||
cache tree. Pre-created extraction markers keep this network-free."""
|
||||
monkeypatch.setattr(CORE, "config_path", tmp_path / "test.yaml")
|
||||
url = "http://example.com/lib.tar.gz"
|
||||
base = tmp_path / ".esphome" / "pio_components"
|
||||
expected = {}
|
||||
for salt in ("", "abcd1234"):
|
||||
digest = hashlib.sha256((url + salt).encode()).hexdigest()[:8]
|
||||
expected[salt] = base / digest / "lib"
|
||||
expected[salt].mkdir(parents=True)
|
||||
(expected[salt] / ".esphome_extracted").touch()
|
||||
|
||||
source = URLSource(url)
|
||||
assert source.download("lib") == expected[""]
|
||||
assert source.download("lib", salt="abcd1234") == expected["abcd1234"]
|
||||
|
||||
|
||||
def test_git_source_salt_scopes_domain(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""The salt becomes a subdirectory of the git clone domain."""
|
||||
domains: list[str] = []
|
||||
|
||||
def fake_clone_or_update(**kwargs):
|
||||
domains.append(kwargs["domain"])
|
||||
return Path("/cloned"), None
|
||||
|
||||
monkeypatch.setattr(
|
||||
esphome.espidf.component.git, "clone_or_update", fake_clone_or_update
|
||||
)
|
||||
|
||||
source = GitSource("https://github.com/esphome/noise-c.git", "v1.0")
|
||||
source.download("noise-c")
|
||||
source.download("noise-c", salt="abcd1234")
|
||||
assert domains == ["pio_components", "pio_components/abcd1234"]
|
||||
|
||||
|
||||
def test_idf_component_download_passes_salt() -> None:
|
||||
"""IDFComponent.download forwards the sanitized name and salt to the
|
||||
source and records the returned path."""
|
||||
source = MagicMock()
|
||||
source.download.return_value = Path("/converted/owner/name")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=source)
|
||||
c.download(force=True, salt="abcd1234")
|
||||
|
||||
source.download.assert_called_once_with("owner/name", force=True, salt="abcd1234")
|
||||
assert c.path == Path("/converted/owner/name")
|
||||
|
||||
Reference in New Issue
Block a user