Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-22 11:44:16 -05:00
37 changed files with 218 additions and 566 deletions
+13 -34
View File
@@ -1,14 +1,8 @@
"""Drift tests for the native ESP8266 Arduino build generator.
These pin the ESPHome side of the transliteration (the knob-define
precedence, the define/flag sets, and the linker-script generation) against
literals audited from the PlatformIO builder
(framework-arduinoespressif8266/tools/platformio-build.py and
platform-espressif8266/builder/main.py): the knob-define precedence, the
define/flag sets, the link line, and the core source exclusions. They catch
an accidental edit on this side; an upstream change in a new framework
release is caught by the A/B byte-identical build check on a version bump,
not by these tests.
Pin the transliterated flag/define/link sets against literals audited from
the PlatformIO builder. Upstream drift is caught by the A/B build check on
version bumps, not here.
"""
from __future__ import annotations
@@ -17,10 +11,12 @@ from collections.abc import Generator
import logging
import os
from pathlib import Path
import shutil
from unittest.mock import MagicMock, patch
import pytest
from esphome.arduino.library import ArduinoLibrary
from esphome.arduino8266.framework import InstalledPaths, toolchain_tool
from esphome.build_gen import arduino8266
from esphome.build_gen.arduino8266 import (
@@ -30,7 +26,7 @@ from esphome.build_gen.arduino8266 import (
_resolve_build_config,
get_flash_ld_path,
)
from esphome.components.esp8266.boards import ESP8266_BOARD_BUILD
from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD
from esphome.components.esp8266.build_surgery import RATETABLE_RULE
from esphome.components.esp8266.const import (
KEY_BOARD,
@@ -255,9 +251,7 @@ def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None:
assert "-T eagle.flash.4m.ld" in content
# scanf float disabled: the forced-link flag must not appear
assert "_scanf_float" not in content
# $in/$out must stay UNQUOTED: ninja shell-escapes its built-in path
# variables itself, so added quotes would wrap ninja's quoting and break
# space-containing paths.
# $in/$out must stay unquoted; ninja escapes its own path variables
assert "-c $in -o $out" in content
assert "--app $in --flash_mode" in content
assert '"$in"' not in content
@@ -492,7 +486,6 @@ def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None:
def test_write_project_libraries_and_variant(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
from esphome.arduino.library import ArduinoLibrary
paths = _make_framework(tmp_path)
variant_src = paths.framework / "variants" / "nodemcu" / "variant.cpp"
@@ -572,7 +565,6 @@ def test_write_project_testing_mode(tmp_path: Path) -> None:
def test_write_project_missing_framework_dir_raises(tmp_path: Path) -> None:
"""An incomplete framework install fails naming the missing path."""
import shutil
paths = _make_framework(tmp_path)
shutil.rmtree(paths.framework / "tools" / "sdk" / "lwip2")
@@ -781,10 +773,7 @@ def test_write_project_missing_src_dir_raises(tmp_path: Path) -> None:
def test_build_config_custom_mmu_without_knob_raises() -> None:
"""Custom MMU sizes without the CUSTOM knob would compile against a
layout the linker script does not implement; refuse instead of warning
(PlatformIO warns, but its defaults win the compile line; ours would
not)."""
"""Custom MMU sizes without the CUSTOM knob are refused."""
with pytest.raises(EsphomeError, match="PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM"):
_resolve("-DMMU_IRAM_SIZE=0xC000")
@@ -833,8 +822,7 @@ def test_flag_tables_match_platformio_builder() -> None:
"-free",
"-fipa-pta",
]
# The -u block is where the deliberate -u _scanf_float omission lives;
# pinned in full so "restoring" it fails here first
# Pins the deliberate -u _scanf_float omission
assert arduino8266._LINKFLAGS == [
"-Os",
"-nostdlib",
@@ -895,7 +883,6 @@ def test_write_project_asm_keeps_quoted_defines(tmp_path: Path) -> None:
def test_write_project_unarchived_library_links_objects(tmp_path: Path) -> None:
"""A libArchive:false library's objects reach the link directly."""
from esphome.arduino.library import ArduinoLibrary
paths = _make_framework(tmp_path)
lib_src = tmp_path / "gdb" / "src"
@@ -927,8 +914,7 @@ def test_write_project_unknown_board_fails_by_name(tmp_path: Path) -> None:
def test_unflag_tokens_join_spaced_entries() -> None:
"""A spaced build_unflags entry ("-D FOO") removes -DFOO, and no bare half leaks into the
unflag set to collaterally drop unrelated tokens."""
"""Spaced build_unflags entries ("-D FOO") match the joined token."""
CORE.build_unflags = {"-D FOO", "-l bar"}
tokens = arduino8266._unflag_tokens()
assert tokens == {"-DFOO", "-lbar"}
@@ -950,8 +936,7 @@ def test_flag_defines_respects_unflags() -> None:
def test_vtables_unknown_raises() -> None:
"""A typo'd knob would win the sorted pick and die in the SDK header's
#error; fail by name at generation instead."""
"""An unknown VTABLES_IN_* knob fails by name."""
with pytest.raises(EsphomeError, match="Unknown VTABLES_IN_.*BANANA"):
_resolve("-DVTABLES_IN_BANANA")
@@ -1044,9 +1029,7 @@ def test_lexed_build_flags_shared_between_consumers(
"tok", ["-Tcustom.ld", "-Xlinker", "-u", "-e", "-s", "-static", "-nostartfiles"]
)
def test_project_flags_rejects_plain_linker_forms(tok: str) -> None:
"""A plain-form linker flag would land on the -c compile line where it
is inert; refuse naming the -Wl, form instead of shipping firmware that
silently lacks the requested link behavior."""
"""Plain-form linker flags are refused, naming the -Wl, form."""
_set_flags(tok)
with pytest.raises(EsphomeError, match="use the -Wl, form"):
arduino8266._project_flags(set(), arduino8266._lexed_build_flags())
@@ -1227,11 +1210,7 @@ def test_generate_ld_scripts_unreadable_header_forces_regeneration(
def test_board_tables_are_equal() -> None:
"""write_project rejects a board missing from either table, so the two
must stay exactly in sync (the build-surgery test only checks the
subset direction, which is how d1_wroom_02 went missing)."""
from esphome.components.esp8266.boards import BOARDS
"""BOARDS and ESP8266_BOARD_BUILD must stay exactly in sync."""
assert set(BOARDS) == set(ESP8266_BOARD_BUILD)
@@ -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,
@@ -7268,7 +7269,7 @@ def test_command_idedata_incompatible_toolchain(tmp_path: Path) -> None:
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(
@@ -7276,14 +7277,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
@@ -7307,14 +7300,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",
@@ -7336,14 +7321,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",
@@ -7394,8 +7371,6 @@ def test_command_analyze_memory_unsupported_toolchain(
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)]
@@ -7437,8 +7412,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)])