"""Tests for the Zephyr backend of the shared PlatformIO library converter.""" from pathlib import Path import pytest import esphome.components.zephyr.library as zlib from esphome.components.zephyr.library import ( generate_cmakelists_txt, generate_module_yml, generate_zephyr_modules, ) from esphome.core import EsphomeError, Library from esphome.platformio.library import ConvertedLibrary, URLSource def _make_component(path: Path, name: str = "mylib") -> ConvertedLibrary: c = ConvertedLibrary(name, "1.0", source=URLSource("http://dummy")) c.path = path return c def test_generate_module_yml_uses_sanitized_name(): c = ConvertedLibrary("owner/My Lib", "1.0", source=URLSource("http://dummy")) out = generate_module_yml(c) # "/" -> "__" and " " -> "_" so it's a valid Zephyr module name. assert "name: owner__My_Lib" in out assert "cmake: zephyr" in out def test_generate_cmakelists_txt_basic(tmp_path): c = _make_component(tmp_path) src = tmp_path / "src" src.mkdir() (src / "main.c").write_text("int main() {}") c.data = {} out = generate_cmakelists_txt(c) assert "zephyr_library_named(mylib)" in out assert "zephyr_library_sources(" in out # Sources are emitted as absolute paths (CMakeLists lives in zephyr/ subdir), # backslash-escaped for CMake (matching the output on Windows). assert str((src / "main.c").resolve()).replace("\\", "\\\\") in out def test_generate_cmakelists_txt_flags_and_includes(tmp_path): c = _make_component(tmp_path) (tmp_path / "src").mkdir() (tmp_path / "src" / "a.c").write_text("") (tmp_path / "include").mkdir() c.data = {"build": {"flags": ["-Iinclude", "-DFOO", "-Wall", "-Llibdir", "-lm"]}} out = generate_cmakelists_txt(c) assert "zephyr_include_directories(" in out assert str((tmp_path / "include").resolve()).replace("\\", "\\\\") in out assert "zephyr_library_compile_options(" in out assert "-DFOO" in out assert "-Wall" in out assert "zephyr_link_libraries(" in out # -L paths are absolutised against the library dir (the CMakeLists lives in a # zephyr/ subdir, so a relative path would resolve from the wrong place). abs_libdir = str((tmp_path / "libdir").resolve()).replace("\\", "\\\\") assert f"-L{abs_libdir}" in out assert "-lm" in out def test_generate_cmakelists_txt_escapes_embedded_quotes(tmp_path): """A define value carrying a literal quote (reachable via the lex round-trip) survives as an escaped quote, not a broken CMake string.""" c = _make_component(tmp_path) (tmp_path / "src").mkdir() (tmp_path / "src" / "a.c").write_text("") c.data = {"build": {"flags": ['-DMSG=\\"hi\\"']}} out = generate_cmakelists_txt(c) assert '"-DMSG=\\"hi\\""' in out def test_generate_cmakelists_txt_lexes_spaced_flags(tmp_path): """A spaced -I entry routes to include dirs instead of landing verbatim in compile options; same shared lexer as the espidf emitter.""" c = _make_component(tmp_path) (tmp_path / "src").mkdir() (tmp_path / "src" / "a.c").write_text("") (tmp_path / "include").mkdir() c.data = {"build": {"flags": "-I include -DBAR=1"}} out = generate_cmakelists_txt(c) assert str((tmp_path / "include").resolve()).replace("\\", "\\\\") in out assert "-DBAR=1" in out assert "-I include" not in out def test_generate_zephyr_modules_collects_all_dirs_and_writes(tmp_path, monkeypatch): # Two converted libraries: one top-level, one transitive dependency. The # converter calls backend.emit for both; generate_zephyr_modules must return # *all* module dirs (not just top-level) so every module is discoverable. top = _make_component(tmp_path / "top", "top") (top.path / "src").mkdir(parents=True) (top.path / "src" / "t.c").write_text("") dep = _make_component(tmp_path / "dep", "dep") (dep.path / "src").mkdir(parents=True) (dep.path / "src" / "d.c").write_text("") captured = {} def fake_convert(libraries, backend): captured["platform"] = backend.platform captured["framework"] = backend.framework backend.emit(top) backend.emit(dep) return [top] monkeypatch.setattr(zlib, "convert_libraries", fake_convert) dirs = generate_zephyr_modules([Library("top", "1.0", None)]) assert dirs == [top.path, dep.path] # Platform check disabled for Zephyr; framework declared as zephyr. assert captured["platform"] is None assert captured["framework"] == "zephyr" for comp in (top, dep): assert (comp.path / "zephyr" / "module.yml").is_file() assert (comp.path / "zephyr" / "CMakeLists.txt").is_file() def test_generate_zephyr_modules_errors_on_duplicate_module_name(tmp_path, monkeypatch): # The same library referenced under inconsistent specs (e.g. bare vs # owner-qualified, or git vs registry) resolves to two components with the # same Zephyr module name, which would collide in zephyr_library_named(). a = _make_component(tmp_path / "a", "esphome/noise-c") a.path.mkdir(parents=True) b = _make_component(tmp_path / "b", "esphome/noise-c") b.path.mkdir(parents=True) assert a.get_require_name() == b.get_require_name() def fake_convert(libraries, backend): backend.emit(a) backend.emit(b) return [a] monkeypatch.setattr(zlib, "convert_libraries", fake_convert) with pytest.raises(EsphomeError, match="same Zephyr module"): generate_zephyr_modules([Library("esphome/noise-c", "1.0", None)])