Merge branch 'esp8266-native-library-backend' into esp8266-native-build-spec

This commit is contained in:
J. Nick Koston
2026-08-22 11:42:02 -05:00
35 changed files with 181 additions and 471 deletions
@@ -3,6 +3,8 @@
from __future__ import annotations
from pathlib import Path
import subprocess
import sys
from unittest.mock import MagicMock, patch
import pytest
@@ -50,8 +52,6 @@ def test_unknown_mode(capsys: pytest.CaptureFixture[str]) -> None:
def test_runs_as_script(tmp_path: Path) -> None:
"""The ninja rules invoke the file as a plain script."""
import subprocess
import sys
src = tmp_path / "a.bin"
src.write_text("x")
@@ -429,8 +429,7 @@ def test_load_or_build_idedata_corrupted_cache_is_logged(
def test_load_or_build_idedata_never_caches_a_launcher(tmp_path: Path) -> None:
"""A compile DB naming a launcher as the compiler is rejected by name,
before the toolchain probe could fail opaquely, and never cached."""
"""A compile DB naming a launcher as the compiler is rejected, never cached."""
compile_commands = tmp_path / "compile_commands.json"
compile_commands.write_text(
json.dumps(
+1 -1
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import os
from pathlib import Path
import subprocess
import sys
from unittest.mock import MagicMock, patch
@@ -84,7 +85,6 @@ def test_shell_token_quotes_shell_metacharacters() -> None:
def test_shell_token_posix_roundtrips_through_sh() -> None:
"""Backslash runs, $, backticks, and quotes must reach the compiler
exactly as lexed once ninja un-doubles $$ and /bin/sh strips quotes."""
import subprocess
if sys.platform == "win32":
pytest.skip("POSIX sh quoting")
@@ -2,8 +2,13 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import sys
import pytest
from esphome.components.esp8266 import build_surgery
from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD
from esphome.components.esp8266.build_surgery import (
RATETABLE_RULE,
@@ -110,11 +115,6 @@ def test_board_build_covers_every_board() -> None:
def test_surgery_fingerprint_is_stable_and_sensitive(tmp_path) -> None:
"""The properties the linker-script cache depends on: the fingerprint is
stable across calls and changes when the module's source changes."""
import importlib.util
from pathlib import Path as _Path
import sys
from esphome.components.esp8266 import build_surgery
first = build_surgery.surgery_fingerprint()
assert first == build_surgery.surgery_fingerprint()
@@ -124,7 +124,7 @@ def test_surgery_fingerprint_is_stable_and_sensitive(tmp_path) -> None:
# A modified copy of the module must fingerprint differently
copy = tmp_path / "build_surgery_variant.py"
copy.write_text(
_Path(build_surgery.__file__).read_text(encoding="utf-8")
Path(build_surgery.__file__).read_text(encoding="utf-8")
+ "\nEXTRA_BEHAVIORAL_INPUT = 1\n",
encoding="utf-8",
)
@@ -26,9 +26,8 @@ def test_framework_package_version() -> None:
# A future major bump needs its own encoding, not a doomed registry lookup
with pytest.raises(EsphomeError, match="not supported yet"):
framework.framework_package_version(cv.Version(4, 0, 0))
# Cores up to 2.6.2 use other encodings; the helper is total, not wrong,
# and its boundary matches the PlatformIO era guard: a 2.6.2 pre-release
# sorts above 2.6.2 and keeps the package-major-3 encoding
# The boundary matches the PlatformIO era guard; a 2.6.2 pre-release
# keeps this encoding
with pytest.raises(EsphomeError, match="older package encoding"):
framework.framework_package_version(cv.Version(2, 6, 2))
assert framework.framework_package_version(cv.Version(2, 6, 2, "b1")) == "3.20602.0"
@@ -102,9 +101,8 @@ def test_get_build_env_prepends_toolchain_bin(tmp_path: Path) -> None:
def test_ccache_path_delegates_uncached(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The wrapper delegates to the shared policy (covered in
build_helpers/test_ccache.py) on every call: the env/PATH decision
must not freeze for the process lifetime in a long-lived host."""
"""Delegates on every call; the env/PATH decision must not freeze for
the process lifetime."""
monkeypatch.delenv("ESPHOME_CCACHE_ENABLE", raising=False)
with patch.object(
framework, "resolve_ccache_path", return_value="/usr/bin/ccache"
+14 -30
View File
@@ -12,7 +12,13 @@ import pytest
from esphome.arduino import library as component
from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM, PLATFORM_ESP8266
from esphome.core import CORE, EsphomeError, Library
from esphome.platformio.library import ConvertedLibrary, LibraryBackend
import esphome.platformio.library as pio_library
from esphome.platformio.library import (
ConvertedLibrary,
IncompatiblePlatform,
InvalidLibrary,
LibraryBackend,
)
@pytest.fixture(autouse=True)
@@ -212,9 +218,7 @@ def test_resolve_libraries_bundled(tmp_path: Path) -> None:
def test_resolve_libraries_registry_name_is_external(
tmp_path: Path, version: str | None
) -> None:
"""A name that is not bundled reaches the converter: bare resolves from
the registry at the latest version (matching PlatformIO and the
documented libraries: key) and a version pin is a registry package."""
"""A name that is not bundled reaches the converter, bare or pinned."""
framework = _make_framework(tmp_path)
_add_library("pngle", version)
with patch.object(component, "convert_libraries", return_value=[]) as mock_convert:
@@ -410,12 +414,8 @@ def test_bundled_dependency_nonplatform_rejection_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""An InvalidLibrary whose cause is not the platform filter is visible."""
from esphome.platformio.library import InvalidLibrary
framework = _make_framework(tmp_path)
converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]})
import esphome.platformio.library as pio_library
with (
_emitting_converter(converted),
patch.object(
@@ -466,9 +466,7 @@ def test_library_info_lib_archive_parse(
def test_bundled_dependency_dict_shorthand_prefers_bundled(tmp_path: Path) -> None:
"""The {"Wire": "*"} dict shorthand (version="*", no owner) must resolve
to the bundled library, matching PIO's process_dependencies, instead of
being routed to the registry."""
"""The {"Wire": "*"} dict shorthand resolves to the bundled library."""
framework = _make_framework(tmp_path)
converted = _webserver(tmp_path, {"build": {}, "dependencies": {"Wire": "*"}})
with _emitting_converter(converted):
@@ -481,12 +479,8 @@ def test_bundled_dependency_platform_rejection_is_debug(
) -> None:
"""The typed IncompatiblePlatform (the routine cross-platform skip)
stays at debug regardless of message wording."""
from esphome.platformio.library import IncompatiblePlatform
framework = _make_framework(tmp_path)
converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]})
import esphome.platformio.library as pio_library
with (
_emitting_converter(converted),
patch.object(
@@ -626,11 +620,8 @@ def test_bundled_library_non_dict_manifest_skips_probes_and_raises(
def test_dict_shorthand_dependency_skips_registry_through_real_converter(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""{"Wire": "*"} in a real manifest must never reach the registry: the
graph walk skips backend-provided names and the bundled copy is added
after emit (no converter mock; a registry touch fails the test)."""
import esphome.platformio.library as pio_library
"""{"Wire": "*"} resolves to the bundled copy without touching the
registry (real converter)."""
framework = _make_framework(tmp_path)
_local_lib(tmp_path, {"Wire": "*"})
# Pin the component cache to tmp_path (data_dir honors an ambient
@@ -702,8 +693,6 @@ def test_pinned_bundled_dependency_substitution_warns(
) -> None:
"""A non-* version pin on a backend-provided dependency is discarded
for the bundled copy; the substitution must be visible."""
import esphome.platformio.library as pio_library
framework = _make_framework(tmp_path)
_local_lib(tmp_path, {"Wire": "^2.0.0"})
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
@@ -720,9 +709,7 @@ def test_pinned_bundled_dependency_substitution_warns(
def test_transitively_resolved_dependency_does_not_warn(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A version-less dependency the walk resolved as another library's
registry dependency is present in the build; the skipping warning must
stay quiet for it."""
"""A dependency the walk already resolved does not warn."""
framework = _make_framework(tmp_path)
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
ws, tcp = _ws_tcp_pair(tmp_path)
@@ -754,9 +741,8 @@ def test_external_short_name(spec: str, expected: str) -> None:
def test_converted_manifest_name_suppresses_bundled_dependency(
tmp_path: Path,
) -> None:
"""A dependency name a converted library's manifest provides is not
also added from the framework tree (a duplicate archive would surface
as duplicate-symbol link errors), even when the provider emits later."""
"""A name a converted library's manifest provides is not also added
from the framework tree, even when the provider emits later."""
framework = _make_framework(tmp_path)
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
# Requested under a different short name; only the manifest says "Wire"
@@ -814,8 +800,6 @@ def test_versionless_dependency_with_provider_stays_quiet(
) -> None:
"""With a provides backend the version-less skip is routine (debug) and
the bundled copy is picked up after emit."""
import esphome.platformio.library as pio_library
framework = _make_framework(tmp_path)
_local_lib(tmp_path, [{"name": "Wire"}])
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
+1 -2
View File
@@ -643,8 +643,7 @@ def test_save_compiled_config_and_sidecar_toolchain_mismatch(
tmp_path: Path, sidecar_toolchain: str | None, saved: bool
) -> None:
"""A config validated under a different toolchain than the compile's
must not overwrite the cache: platforms normalize toolchain-sensitive
keys differently and the sidecar keeps the compile's toolchain."""
must not overwrite the cache."""
yaml_path = _bare_yaml(tmp_path)
_prime_core(tmp_path)
CORE.config = {CONF_ESPHOME: {CONF_NAME: "lite_test"}}
+3 -14
View File
@@ -1,3 +1,4 @@
import importlib
import json
import logging
from pathlib import Path
@@ -48,6 +49,7 @@ from esphome.const import (
TYPE_GIT,
TYPE_LOCAL,
Framework,
Toolchain,
)
from esphome.core import (
CORE,
@@ -3169,9 +3171,6 @@ def test_file__remapped_path_is_directory_raises(setup_core: Path) -> None:
def test_require_platformio_toolchain() -> None:
"""Platforms with only the PlatformIO backend reject other toolchains."""
from esphome.const import Toolchain
from esphome.core import CORE
validator = cv.require_platformio_toolchain("RP2")
CORE.toolchain = None
config: dict = {}
@@ -3186,9 +3185,6 @@ def test_require_platformio_toolchain() -> None:
def test_check_supported_toolchain_unresolved_is_an_ordering_bug() -> None:
"""Calling the check before resolution fails naming the ordering bug,
not a user-facing unsupported-toolchain error."""
from esphome.const import Toolchain
from esphome.core import CORE
CORE.toolchain = None
with pytest.raises(Invalid, match="not resolved before RP2 validation"):
cv._check_supported_toolchain("RP2", (Toolchain.PLATFORMIO,))
@@ -3209,14 +3205,7 @@ def test_check_supported_toolchain_unresolved_is_an_ordering_bug() -> None:
def test_every_platformio_only_platform_rejects_arduino_toolchain(
platform: str, minimal_config: dict
) -> None:
"""The invariant every native-toolchain gate relies on: a platform that
cannot serve a CLI toolchain rejects it at validation (esp32, esp8266,
and nrf52 pin this in their own suites)."""
import importlib
from esphome.const import Toolchain
from esphome.core import CORE
"""A platform that cannot serve a CLI toolchain rejects it at validation."""
module = importlib.import_module(f"esphome.components.{platform}")
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(Invalid, match="Unsupported toolchain 'arduino'"):
+2 -2
View File
@@ -6,6 +6,7 @@ from unittest.mock import MagicMock
import pytest
from esphome.components import esp32 as esp32_module
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
@@ -15,6 +16,7 @@ from esphome.const import (
)
from esphome.core import CORE, Library
from esphome.espidf.component import (
_emit_idf_component,
generate_cmakelists_txt,
generate_idf_component_yml,
generate_idf_components,
@@ -1072,8 +1074,6 @@ def test_idf_component_download_passes_salt() -> None:
def test_emit_idf_component_wires_esp32_target(tmp_path, monkeypatch):
"""Emitting a component resolves the esp32 variant into the shared
extraScript helper."""
from esphome.components import esp32 as esp32_module
from esphome.espidf.component import _emit_idf_component
monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32")
(tmp_path / "src").mkdir()
@@ -1395,8 +1395,6 @@ def test_get_framework_env_without_python_env_uses_os_path(tmp_path: Path) -> No
def _ccache_patches(tmp_path: Path, which: str | None, build_path: Path | None):
return (
# The gate defers to the shared resolver (which carries the PATH
# lookup, ESPHOME_CCACHE_ENABLE parse, and runnability probe)
patch("esphome.espidf.framework.resolve_ccache_path", return_value=which),
patch(
"esphome.espidf.framework.get_idf_tools_path",
+2 -29
View File
@@ -101,6 +101,7 @@ from esphome.const import (
CONF_WEB_SERVER,
CONF_WIFI,
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_BK72XX,
PLATFORM_ESP32,
@@ -7139,7 +7140,7 @@ def test_warn_source_tree_mismatch_falls_back_when_stat_fails(
RuntimeError("Could not query builtin include dirs"),
ValueError("no C++ translation unit found"),
KeyError("command"),
None, # replaced with EsphomeError inside (import is function-local)
None, # replaced with EsphomeError inside
],
)
def test_compile_program_espidf_idedata_failure_does_not_fail_build(
@@ -7147,14 +7148,6 @@ def test_compile_program_espidf_idedata_failure_does_not_fail_build(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A post-compile idedata error is a warning: the firmware already built."""
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
Toolchain,
)
from esphome.core import CORE, EsphomeError
if error is None:
error = EsphomeError("compile database is unusable")
CORE.toolchain = Toolchain.ESP_IDF
@@ -7178,14 +7171,6 @@ def test_compile_program_espidf_idedata_success_is_silent(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The healthy path: idedata generated, nothing to warn about."""
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
Toolchain,
)
from esphome.core import CORE
CORE.toolchain = Toolchain.ESP_IDF
CORE.data[KEY_CORE] = {
KEY_TARGET_PLATFORM: "esp32",
@@ -7207,14 +7192,6 @@ def test_compile_program_espidf_idedata_none_warns(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A silent None from the post-compile idedata refresh is made visible."""
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
Toolchain,
)
from esphome.core import CORE
CORE.toolchain = Toolchain.ESP_IDF
CORE.data[KEY_CORE] = {
KEY_TARGET_PLATFORM: "esp32",
@@ -7235,8 +7212,6 @@ def test_compile_program_espidf_idedata_none_warns(
def test_cli_toolchain_skips_the_validated_config_cache(tmp_path: Path) -> None:
"""An explicit --toolchain must run the per-platform validators, so the
upload/logs fast path becomes a cache miss."""
from esphome.__main__ import run_esphome
conf = tmp_path / "device.yaml"
conf.write_text("esphome:\n name: t\n")
argv = ["esphome", "--toolchain", "arduino", "logs", str(conf)]
@@ -7255,8 +7230,6 @@ def test_cli_toolchain_still_refreshes_the_validated_config_cache(
"""An explicit --toolchain gates only the cache read; the freshly
validated config is still saved so a later plain run keeps the fast
path (an existing compile-written sidecar keeps its toolchain)."""
from esphome.__main__ import run_esphome
conf = tmp_path / "device.yaml"
conf.write_text("esphome:\n name: t\n")
argv = ["esphome", "--toolchain", "platformio", "logs", str(conf)]
+4 -6
View File
@@ -7,8 +7,10 @@ import sys
from types import SimpleNamespace
from unittest.mock import patch
import platformdirs
import pytest
from esphome.components.nrf52 import _resolve_toolchain
from esphome.components.nrf52.framework import (
_PLATFORMIO_PENV_REQUIREMENTS,
_REQUIREMENTS,
@@ -22,8 +24,9 @@ from esphome.components.nrf52.framework import (
get_sdk_nrf_tools_path,
setup_platformio_python_env,
)
import esphome.config_validation as cv
from esphome.config_validation import Version
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain
from esphome.core import CORE, EsphomeError
from esphome.framework_helpers import get_python_env_executable_path
@@ -558,7 +561,6 @@ def testget_tools_path_blank_env_falls_back_to_default(
Path("") would resolve to the working directory, which clean-all could
then delete by accident.
"""
import platformdirs
monkeypatch.setenv("ESPHOME_SDK_NRF_PREFIX", value)
expected = (
@@ -570,7 +572,6 @@ def testget_tools_path_blank_env_falls_back_to_default(
def testget_tools_path_default_is_global_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import platformdirs
monkeypatch.delenv("ESPHOME_SDK_NRF_PREFIX", raising=False)
expected = (
@@ -623,9 +624,6 @@ def test_needs_venv_rebuild_on_dangling_interpreter_symlink(tmp_path: Path) -> N
def test_resolve_toolchain_rejects_unsupported() -> None:
"""A --toolchain nRF52 cannot serve fails instead of degrading silently."""
from esphome.components.nrf52 import _resolve_toolchain
import esphome.config_validation as cv
from esphome.const import Toolchain
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):
@@ -2,19 +2,25 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
from unittest.mock import patch
import pytest
from esphome.core import EsphomeError
from esphome.platformio.extra_script import (
ExtraScriptResult,
_FakeSConsEnv,
apply_extra_script,
captured_as_build_flags,
run_extra_script,
)
from esphome.platformio.library import ConvertedLibrary as IDFComponent, URLSource
def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
from esphome.platformio.extra_script import (
captured_as_build_flags,
run_extra_script,
)
(tmp_path / "src" / "esp32").mkdir(parents=True)
script = tmp_path / "extra_script.py"
@@ -58,10 +64,6 @@ def test_extra_script_libpath_relative_resolves_against_library_dir(
"""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.platformio.extra_script import (
ExtraScriptResult,
captured_as_build_flags,
)
(tmp_path / "lib" / "esp32").mkdir(parents=True)
elsewhere = tmp_path.parent / "not_the_library_dir"
@@ -76,10 +78,6 @@ def test_extra_script_libpath_relative_resolves_against_library_dir(
def test_extra_script_libpath_absolute_outside_library_dir(tmp_path):
from esphome.platformio.extra_script import (
ExtraScriptResult,
captured_as_build_flags,
)
outside = tmp_path.parent / "system_lib"
outside.mkdir(exist_ok=True)
@@ -90,7 +88,6 @@ def test_extra_script_libpath_absolute_outside_library_dir(tmp_path):
def test_extra_script_failure_returns_empty_result(tmp_path, caplog):
from esphome.platformio.extra_script import run_extra_script
script = tmp_path / "broken.py"
script.write_text("raise RuntimeError('boom')\n")
@@ -106,7 +103,6 @@ def test_extra_script_failure_returns_empty_result(tmp_path, caplog):
def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
from esphome.platformio.extra_script import apply_extra_script
library_dir = tmp_path / "lib"
library_dir.mkdir()
@@ -117,8 +113,6 @@ def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
c.path = library_dir
c.data = {"build": {"extraScript": "../evil.py"}}
from esphome.core import EsphomeError
with pytest.raises(EsphomeError, match="escapes the library directory"):
apply_extra_script(c, board_mcu=lambda: "esp32", pio_platform="espressif32")
# Nothing was folded into flags: the traversal was rejected before
@@ -127,7 +121,6 @@ def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
def test_apply_extra_script_merges_into_existing_flags(tmp_path):
from esphome.platformio.extra_script import apply_extra_script
(tmp_path / "src").mkdir()
script = tmp_path / "extra.py"
@@ -146,8 +139,6 @@ def test_apply_extra_script_merges_into_existing_flags(tmp_path):
def test_apply_extra_script_malformed_flags_raises(tmp_path) -> None:
"""A null/dict build.flags fails naming the library instead of injecting
a non-string into the compiler command line."""
from esphome.core import EsphomeError
from esphome.platformio.extra_script import apply_extra_script
(tmp_path / "src").mkdir()
script = tmp_path / "extra.py"
@@ -164,7 +155,6 @@ def test_apply_extra_script_malformed_flags_raises(tmp_path) -> None:
def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None:
"""The shared helper resolves the board_mcu callable lazily and normalizes
a string ``build.flags`` value into a list before extending it."""
from esphome.platformio.extra_script import apply_extra_script
(tmp_path / "src").mkdir()
script = tmp_path / "extra.py"
@@ -180,7 +170,6 @@ def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None:
def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None:
from esphome.platformio.extra_script import apply_extra_script
# No extraScript declared: nothing happens, the target is never resolved
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
@@ -203,9 +192,6 @@ def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None:
def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path, caplog) -> None:
"""Un-captured env vars and unsupported env methods are skipped but
diagnosable from the build log."""
import logging
from esphome.platformio.extra_script import apply_extra_script
caplog.set_level(logging.DEBUG)
script = tmp_path / "extra.py"
@@ -223,7 +209,6 @@ def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path, caplog) -> No
def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None:
"""A raising extra-script is best-effort: logged and skipped."""
from esphome.platformio.extra_script import apply_extra_script
script = tmp_path / "extra.py"
script.write_text("raise RuntimeError('boom')\n")
@@ -237,7 +222,6 @@ def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None:
def test_apply_extra_script_pio_platform(tmp_path) -> None:
"""The backend's platform token is exposed to the script as PIOPLATFORM."""
from esphome.platformio.extra_script import apply_extra_script
script = tmp_path / "extra.py"
script.write_text("env.Append(LIBS=[env.get('PIOPLATFORM')])\n")
@@ -251,8 +235,6 @@ def test_apply_extra_script_pio_platform(tmp_path) -> None:
def test_apply_extra_script_missing_script_raises(tmp_path) -> None:
"""A declared but absent extraScript is a broken package and fails by
name, as it would under PlatformIO."""
from esphome.core import EsphomeError
from esphome.platformio.extra_script import apply_extra_script
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = tmp_path
@@ -264,7 +246,6 @@ def test_apply_extra_script_missing_script_raises(tmp_path) -> None:
def test_run_extra_script_failure_discards_partial_capture(tmp_path, caplog) -> None:
"""A crashed script yields an empty result: half-applied flags could
build wrong-output firmware that links cleanly."""
from esphome.platformio.extra_script import run_extra_script
script = tmp_path / "extra.py"
script.write_text("env.Append(LIBS=['algobsec'])\nraise RuntimeError('boom')\n")
@@ -278,7 +259,6 @@ def test_run_extra_script_failure_discards_partial_capture(tmp_path, caplog) ->
def test_run_extra_script_syntax_error_is_best_effort(tmp_path, caplog) -> None:
"""A vendored script that does not even compile warns and skips instead
of aborting the build."""
from esphome.platformio.extra_script import run_extra_script
script = tmp_path / "extra.py"
script.write_text("def broken(:\n")
@@ -291,7 +271,6 @@ def test_run_extra_script_syntax_error_is_best_effort(tmp_path, caplog) -> None:
def test_unsupported_env_method_warns_once(caplog) -> None:
"""Repeated calls to the same unsupported method warn only once."""
from esphome.platformio.extra_script import _FakeSConsEnv
env = _FakeSConsEnv(
board_mcu="esp8266", pio_env="esphome_esp8266", pio_platform="espressif8266"
@@ -304,7 +283,6 @@ def test_unsupported_env_method_warns_once(caplog) -> None:
def test_run_extra_script_sys_exit_is_best_effort(tmp_path, caplog) -> None:
"""A nonzero sys.exit() in a vendored script must not kill the esphome
run, and its output is discarded."""
from esphome.platformio.extra_script import run_extra_script
script = tmp_path / "extra.py"
script.write_text("import sys\nenv.Append(LIBS=['x'])\nsys.exit(3)\n")
@@ -317,7 +295,6 @@ def test_run_extra_script_sys_exit_is_best_effort(tmp_path, caplog) -> None:
def test_run_extra_script_sys_exit_zero_is_success(tmp_path, caplog) -> None:
"""sys.exit(0) is a normal PlatformIO script ending: the capture is kept."""
from esphome.platformio.extra_script import run_extra_script
script = tmp_path / "extra.py"
script.write_text("import sys\nenv.Append(LIBS=['algobsec'])\nsys.exit(0)\n")
@@ -330,10 +307,6 @@ def test_run_extra_script_sys_exit_zero_is_success(tmp_path, caplog) -> None:
def test_run_extra_script_unreadable_raises(tmp_path) -> None:
"""An unreadable declared script is a broken package, like a missing one."""
from unittest.mock import patch
from esphome.core import EsphomeError
from esphome.platformio.extra_script import run_extra_script
script = tmp_path / "extra.py"
script.write_text("")
@@ -348,7 +321,6 @@ def test_run_extra_script_unreadable_raises(tmp_path) -> None:
def test_run_extra_script_bad_encoding_is_best_effort(tmp_path, caplog) -> None:
"""Undecodable content warns and skips, like a SyntaxError."""
from esphome.platformio.extra_script import run_extra_script
script = tmp_path / "extra.py"
script.write_bytes(b"\xff\xfe\x00bad")
@@ -361,7 +333,6 @@ def test_run_extra_script_bad_encoding_is_best_effort(tmp_path, caplog) -> None:
def test_uncaptured_append_key_warns_once(caplog) -> None:
"""A loop of Appends to the same uncaptured key warns once."""
from esphome.platformio.extra_script import _FakeSConsEnv
env = _FakeSConsEnv(
board_mcu="esp8266", pio_env="esphome_esp8266", pio_platform="espressif8266"
+7 -11
View File
@@ -13,6 +13,7 @@ import pytest
from esphome.core import EsphomeError, Library
import esphome.platformio.library as lib
from esphome.platformio.library import (
SOURCE_KIND_FOR_SUFFIX,
ConvertedLibrary,
GitSource,
InvalidLibrary,
@@ -23,6 +24,8 @@ from esphome.platformio.library import (
_resolve_registry_version,
check_library_data,
convert_libraries,
join_flag_args,
split_flag_entry,
)
@@ -539,7 +542,6 @@ def test_convert_libraries_skips_incompatible_dependency(tmp_path, monkeypatch):
def test_split_flag_entry_unbalanced_quote_is_clean() -> None:
"""A malformed flags entry raises EsphomeError, not a raw ValueError."""
from esphome.platformio.library import split_flag_entry
assert split_flag_entry('-DX="a b"', "library x") == ["-DX=a b"]
with pytest.raises(EsphomeError, match=r"Malformed build flag.*library x"):
@@ -548,7 +550,6 @@ def test_split_flag_entry_unbalanced_quote_is_clean() -> None:
def test_join_flag_args_reglues_spaced_define() -> None:
"""A spaced -D re-glues to its argument, as ParseFlags does."""
from esphome.platformio.library import join_flag_args
assert join_flag_args(["-D", "FOO=1", "-Os"], "x") == ["-DFOO=1", "-Os"]
@@ -556,7 +557,6 @@ def test_join_flag_args_reglues_spaced_define() -> None:
def test_join_flag_args_trailing_bare_flag_warns(
caplog: pytest.LogCaptureFixture,
) -> None:
from esphome.platformio.library import join_flag_args
assert join_flag_args(["-Os", "-l"], "library x") == ["-Os"]
assert "Ignoring trailing '-l'" in caplog.text
@@ -576,7 +576,6 @@ def test_lex_build_flags_dangling_flag_does_not_cross_entries(
def test_split_flag_entry_non_string_is_clean() -> None:
"""A dict or number from a third-party manifest fails naming the entry,
not with an opaque shlex traceback."""
from esphome.platformio.library import split_flag_entry
with pytest.raises(EsphomeError, match="Malformed build flag"):
split_flag_entry({"esp32": ["-DX"]}, "lib x")
@@ -587,7 +586,6 @@ def test_split_flag_entry_non_string_is_clean() -> None:
def test_source_kind_map_shape() -> None:
"""The kind values the native compile rules key on, and the deliberate
AS/ASPP merge (.s and .S both map to asm)."""
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX
assert set(SOURCE_KIND_FOR_SUFFIX.values()) == {"c", "cxx", "asm"}
assert SOURCE_KIND_FOR_SUFFIX[".s"] == "asm"
@@ -681,9 +679,8 @@ def test_walk_warns_for_nonplatform_invalid_library(
def test_versionless_owner_qualified_dependency_warns_despite_provides(
tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture
) -> None:
"""The backend's provides() only covers owner-less names (the walk's
backend-provided skip has the same guard), so an owner-qualified
version-less dependency that nobody adds must still warn."""
"""An owner-qualified version-less dependency is not satisfied by
provides(); it must still warn."""
_patch_download_with_manifests(
monkeypatch,
tmp_path,
@@ -758,9 +755,8 @@ def test_versionless_url_ish_dependency_name_warns_cleanly(
def test_versionless_dependency_matching_resolved_manifest_name_stays_quiet(
tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture
) -> None:
"""A bare dependency name satisfied by a component requested under an
owner-qualified spec (manifest names match) is not a drop; a nameless
entry is skipped without a reconciliation warning."""
"""A bare name satisfied by an owner-qualified component's manifest
name is not a drop."""
_patch_download_with_manifests(
monkeypatch,
tmp_path,
-2
View File
@@ -1051,8 +1051,6 @@ def test_clean_all_removes_global_arduino8266_install(
config_dir = tmp_path / "config"
config_dir.mkdir()
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(config_dir)])