[core] Add the arduino toolchain seam and validate --toolchain on every platform (#18556)

This commit is contained in:
J. Nick Koston
2026-08-26 11:16:02 +12:00
committed by GitHub
parent c272c4c1a6
commit 370e8fffed
19 changed files with 370 additions and 43 deletions
+9 -8
View File
@@ -2734,10 +2734,14 @@ def run_esphome(argv):
# Skipped when -s overrides are passed, since the cache was written
# against the previous substitution set.
config: ConfigType | None = None
cache_eligible = (
cache_write_eligible = (
args.command in ("upload", "logs") and not command_line_substitutions
)
if cache_eligible:
# An explicit --toolchain must re-run the per-platform validators, so
# gate only the cache read; the refresh below saves the result unless
# the sidecar records a different toolchain.
cache_read_eligible = cache_write_eligible and args.toolchain is None
if cache_read_eligible:
from esphome.compiled_config import load_compiled_config
config = load_compiled_config(conf_path)
@@ -2761,17 +2765,14 @@ def run_esphome(argv):
return 2
CORE.config = config
# Fallback for platforms whose validators didn't set the toolchain
# (only the esp32 component reads esp32.framework.toolchain). All
# other platforms only support PlatformIO today. Must run before the
# cache refresh below so its sidecar records the same toolchain a
# compile would.
# The cache fast path skips validation, and legacy sidecars lack the
# toolchain field. Must run before the cache refresh below.
if CORE.toolchain is None:
CORE.toolchain = Toolchain.PLATFORMIO
# Refresh the cache so the next upload/logs hits the fast path
# instead of re-running read_config.
if cache_eligible and cache_missed:
if cache_write_eligible and cache_missed:
from esphome.compiled_config import save_compiled_config_and_sidecar
save_compiled_config_and_sidecar(config)
+15
View File
@@ -100,6 +100,21 @@ def _refresh_sidecar() -> bool:
)
return False
if old is not None and old.can_apply_to_core():
if (
old.toolchain is not None
and CORE.toolchain is not None
and old.toolchain != CORE.toolchain.value
):
# Platforms normalize toolchain-sensitive keys differently;
# never cache a config validated under a different toolchain
# than the compile's
_LOGGER.debug(
"Not caching: config validated with toolchain %r but the "
"last compile used %r",
CORE.toolchain.value,
old.toolchain,
)
return False
# Compile-written; nothing to refresh.
return True
if CORE.build_path is not None and CORE.build_path.exists():
+5 -13
View File
@@ -1105,19 +1105,11 @@ def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
return config
def _validate_toolchain(value) -> Toolchain:
return Toolchain(
cv.one_of(Toolchain.PLATFORMIO, Toolchain.ESP_IDF, lower=True)(value)
)
def _resolve_toolchain(value: ConfigType) -> ConfigType:
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
# Runs before _detect_variant so downstream validators can rely on
# CORE.toolchain instead of re-resolving it from the config dict.
if CORE.toolchain is None:
CORE.toolchain = value.get(CONF_TOOLCHAIN, Toolchain.ESP_IDF)
return value
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.ESP_IDF)
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
# Runs before _detect_variant so downstream validators can rely on
# CORE.toolchain instead of re-resolving it from the config dict.
_resolve_toolchain = cv.resolve_toolchain("ESP32", _TOOLCHAINS, Toolchain.ESP_IDF)
def _check_versions(config: ConfigType) -> ConfigType:
+3
View File
@@ -247,6 +247,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
}
),
# Until the native toolchain lands, PlatformIO is the only backend;
# reject a --toolchain this platform cannot serve yet.
cv.require_platformio_toolchain("ESP8266"),
set_core_data,
)
+1
View File
@@ -37,6 +37,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address,
}
),
cv.require_platformio_toolchain("host"),
set_core_data,
)
+2 -1
View File
@@ -300,7 +300,7 @@ FRAMEWORK_SCHEMA = cv.All(
_check_debug_order,
)
CONFIG_SCHEMA = cv.All(_notify_old_style)
CONFIG_SCHEMA = cv.All(_notify_old_style, cv.require_platformio_toolchain("LibreTiny"))
BASE_SCHEMA = cv.Schema(
{
@@ -314,6 +314,7 @@ BASE_SCHEMA = cv.Schema(
)
BASE_SCHEMA.add_extra(_detect_variant)
BASE_SCHEMA.add_extra(cv.require_platformio_toolchain("LibreTiny"))
BASE_SCHEMA.add_extra(_update_core_data)
+3 -8
View File
@@ -125,10 +125,8 @@ def set_core_data(config: ConfigType) -> ConfigType:
return config
def _resolve_toolchain(config: ConfigType) -> ConfigType:
if CORE.toolchain is None:
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.SDK_NRF)
return config
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.SDK_NRF)
_resolve_toolchain = cv.resolve_toolchain("nRF52", _TOOLCHAINS, Toolchain.SDK_NRF)
def set_framework(config: ConfigType) -> ConfigType:
@@ -170,10 +168,7 @@ BOOTLOADERS = [
]
def _validate_toolchain(value) -> Toolchain:
return Toolchain(
cv.one_of(Toolchain.PLATFORMIO, Toolchain.SDK_NRF, lower=True)(value)
)
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
def _detect_bootloader(config: ConfigType) -> ConfigType:
+1
View File
@@ -312,6 +312,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
_detect_variant,
cv.require_platformio_toolchain("RP2"),
set_core_data,
)
+62
View File
@@ -53,6 +53,7 @@ from esphome.const import (
CONF_SETUP_PRIORITY,
CONF_STATE_TOPIC,
CONF_SUBSCRIBE_QOS,
CONF_TOOLCHAIN,
CONF_TOPIC,
CONF_TYPE,
CONF_TYPE_ID,
@@ -75,6 +76,7 @@ from esphome.const import (
TYPE_GIT,
TYPE_LOCAL,
Framework,
Toolchain,
__version__ as ESPHOME_VERSION,
)
from esphome.core import (
@@ -106,6 +108,9 @@ from esphome.util import parse_esphome_version # noqa: F401
from esphome.voluptuous_schema import _Schema
from esphome.yaml_util import SensitiveStr, make_data_base
if typing.TYPE_CHECKING:
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
# pylint: disable=invalid-name
@@ -2532,6 +2537,63 @@ def platformio_version_constraint(value):
return constraints
def _check_supported_toolchain(
platform_name: str, supported: tuple[Toolchain, ...]
) -> None:
"""Raise when the resolved ``CORE.toolchain`` is not in ``supported``
(one message shape for every platform)."""
toolchain = CORE.toolchain
if toolchain is None:
# A caller ran the check before resolving; an ordering bug, not a
# user error
raise Invalid(f"Toolchain was not resolved before {platform_name} validation")
if toolchain not in supported:
names = ", ".join(f"'{tc.value}'" for tc in supported)
raise Invalid(
f"Unsupported toolchain "
f"'{toolchain.value}' for "
f"{platform_name}. Supported: {names}."
)
def toolchain_enum(supported: tuple[Toolchain, ...]) -> Callable[[str], Toolchain]:
"""Schema validator for a platform's ``toolchain`` config key."""
def validator(value: str) -> Toolchain:
return Toolchain(one_of(*supported, lower=True)(value))
return validator
def resolve_toolchain(
platform_name: str, supported: tuple[Toolchain, ...], default: Toolchain
) -> Callable[[ConfigType], ConfigType]:
"""Resolve ``CORE.toolchain`` (CLI > YAML > default) and reject one the
platform cannot serve.
Add to the platform's validation chain before anything that reads
``CORE.toolchain``.
"""
def validator(config: ConfigType) -> ConfigType:
if CORE.toolchain is None:
CORE.toolchain = config.get(CONF_TOOLCHAIN, default)
_check_supported_toolchain(platform_name, supported)
return config
return validator
def require_platformio_toolchain(
platform_name: str,
) -> Callable[[ConfigType], ConfigType]:
"""Reject a CLI-selected toolchain other than PlatformIO, for platforms
with only the PlatformIO backend."""
return resolve_toolchain(
platform_name, (Toolchain.PLATFORMIO,), Toolchain.PLATFORMIO
)
def require_framework_version(
*,
max_version=False,
+8
View File
@@ -21,6 +21,14 @@ class Toolchain(StrEnum):
PLATFORMIO = "platformio"
ESP_IDF = "esp-idf"
SDK_NRF = "sdk-nrf"
# ESP8266: the Arduino core built directly (no PlatformIO)
ARDUINO = "arduino"
# Toolchains that drive their build natively and never read platformio.ini.
# SDK_NRF is absent on purpose: the zephyr backend keeps consuming
# platformio_options.
NATIVE_TOOLCHAINS = frozenset({Toolchain.ESP_IDF, Toolchain.ARDUINO})
class Platform(StrEnum):
+16
View File
@@ -21,6 +21,7 @@ from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
NATIVE_TOOLCHAINS,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
@@ -982,6 +983,19 @@ class EsphomeCore:
def using_toolchain_sdk_nrf(self):
return self.toolchain == Toolchain.SDK_NRF
@property
def using_toolchain_arduino(self):
"""The native ESP8266 Arduino build toolchain (unlike
``using_arduino``, which is the target framework)."""
return self.toolchain == Toolchain.ARDUINO
@property
def using_native_toolchain(self):
"""Whether the selected toolchain builds natively, without reading
``platformio.ini`` (see ``NATIVE_TOOLCHAINS`` in ``esphome.const``;
keep its membership in sync with ``write_cpp_file``'s dispatch)."""
return self.toolchain in NATIVE_TOOLCHAINS
@property
def using_zephyr(self):
return self.target_framework == "zephyr"
@@ -1095,6 +1109,8 @@ class EsphomeCore:
return build_flag
def add_build_unflag(self, build_unflag: str) -> None:
# No warning for using_toolchain_arduino: the native ESP8266 build
# honors build_unflags (token-level, matching PlatformIO).
if self.using_toolchain_esp_idf:
# The native ESP-IDF build generator does not consume build_unflags
_LOGGER.warning(
+40 -10
View File
@@ -555,12 +555,24 @@ def _add_library_str(lib: str) -> None:
cg.add_library(lib, None)
# platformio_options keys the native ESP8266 Arduino generator (a later PR
# in this chain) will honor; its ignored-option warning will consume the same
# list so the two cannot drift
NATIVE_ARDUINO_PIO_OPTIONS = frozenset({"board_build.f_cpu", "board_build.ldscript"})
# The full set that survives into CORE.platformio_options under the native
# arduino toolchain: lib_ignore is the only specially-translated key below
# that is stored rather than translated away. Consumed by the esp8266 native
# backend (later in this chain) for its ignored-option warning; defined here
# so it stays adjacent to the routing.
NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS = NATIVE_ARDUINO_PIO_OPTIONS | {"lib_ignore"}
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> None:
if CORE.using_toolchain_esp_idf:
# The native ESP-IDF build doesn't read platformio.ini; honor the
# options with a native equivalent and warn about the rest, which
# would otherwise be silently ignored.
if CORE.using_native_toolchain:
# The native builds don't read platformio.ini; honor the options
# with a native equivalent and warn about the rest, which would
# otherwise be silently ignored.
for key, val in pio_options.items():
vals = [val] if isinstance(val, str) else val
if key == CONF_BUILD_FLAGS:
@@ -573,23 +585,41 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No
)
for flag in vals:
cg.add_build_flag(flag)
elif key == "build_unflags":
# Native equivalent: add_build_unflag (honored token-level by
# the arduino generator; the IDF generator warns there)
for flag in vals:
CORE.add_build_unflag(flag)
elif key == "lib_deps":
# Routed through the regular library mechanism so the libraries
# are converted to IDF components like any other PIO library
# Routed through the regular library mechanism so the
# libraries reach the native backend's converter (IDF
# components, or the ESP8266 native library resolution)
for lib in vals:
_add_library_str(lib)
elif key == "lib_ignore":
# Read by the PIO-library-to-IDF-component conversion
# (generate_idf_components); filters both top-level libraries
# and dependencies discovered during conversion
# Read by the shared library conversion (lib_ignore_set in
# platformio/library.py); filters top-level libraries and
# discovered dependencies
cg.add_platformio_option(key, vals)
elif (
key in NATIVE_ARDUINO_PIO_OPTIONS
and CORE.using_toolchain_arduino
and vals
):
# The esp8266 native generator reads these as scalars; the
# schema also permits the list form, where the last value
# wins like a later platformio.ini line (an empty list falls
# through to the ignored-option warning). Other native
# toolchains have no equivalent and fall through too.
cg.add_platformio_option(key, vals[-1])
elif key != "upload_speed":
# upload_speed needs no handling: it is read from the raw
# config at upload time (upload_using_esptool)
_LOGGER.warning(
"esphome->platformio_options->%s is ignored when building with "
"the native ESP-IDF toolchain",
"the native '%s' toolchain",
key,
CORE.toolchain.value,
)
return
# Add includes at the very end, so that they override everything
+14
View File
@@ -132,6 +132,20 @@ def test_esp32_rejects_unsupported_toolchains(
CONFIG_SCHEMA({"variant": VARIANT_ESP32, "toolchain": config_toolchain})
def test_esp32_rejects_unsupported_cli_toolchain(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A --toolchain the platform cannot serve fails instead of silently
building with PlatformIO (the CLI path bypasses the YAML validator)."""
set_core_config(PlatformFramework.ESP32_IDF)
from esphome.components.esp32 import CONFIG_SCHEMA
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):
CONFIG_SCHEMA({"variant": VARIANT_ESP32})
@pytest.mark.parametrize(
("config", "error_match"),
[
+49
View File
@@ -1285,6 +1285,7 @@ async def test_add_platformio_options_native_idf(
await config._add_platformio_options(
{
"build_flags": "-DSINGLE_FLAG", # string and list forms both valid
"build_unflags": ["-Os"],
"lib_deps": ["bblanchon/ArduinoJson@7.4.2"],
"lib_ignore": "libsodium",
"upload_speed": "115200",
@@ -1294,6 +1295,7 @@ async def test_add_platformio_options_native_idf(
assert "-DSINGLE_FLAG" in CORE.build_flags
assert "ArduinoJson" in CORE.platformio_libraries
assert "-Os" in CORE.build_unflags
# lib_ignore is stored (listified) for generate_idf_components to read;
# nothing else lands in platformio_options on the native toolchain.
assert CORE.platformio_options == {"lib_ignore": ["libsodium"]}
@@ -1389,3 +1391,50 @@ def test_esphome_build_internals_are_yaml_only() -> None:
assert markers[field].visibility is cv.Visibility.ADVANCED, field
# A regular device-config field stays on the main form.
assert markers[CONF_NAME_ADD_MAC_SUFFIX].visibility is None
@pytest.mark.asyncio
async def test_add_platformio_options_native_arduino(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The native ESP8266 Arduino toolchain honors board_build.f_cpu (a
real-world overclock knob) and warns about the rest like native IDF."""
CORE.toolchain = Toolchain.ARDUINO
CORE.data[KEY_CORE] = {
KEY_TARGET_PLATFORM: "esp8266",
KEY_TARGET_FRAMEWORK: "arduino",
}
await config._add_platformio_options(
{
"board_build.f_cpu": "160000000L",
# The schema also permits the list form; the last value wins
# and reaches the generator as a scalar
"board_build.ldscript": ["eagle.flash.2m.ld", "eagle.flash.4m2m.ld"],
"board_build.filesystem": "littlefs",
"upload_speed": "115200",
}
)
assert CORE.platformio_options["board_build.f_cpu"] == "160000000L"
assert CORE.platformio_options["board_build.ldscript"] == "eagle.flash.4m2m.ld"
assert "board_build.f_cpu is ignored" not in caplog.text
assert "board_build.ldscript is ignored" not in caplog.text
assert (
"esphome->platformio_options->board_build.filesystem is ignored" in caplog.text
)
# An empty list for an honored key is not a scalar; it falls through
# to the ignored-option warning instead of an IndexError
await config._add_platformio_options({"board_build.ldscript": []})
assert "board_build.ldscript is ignored" in caplog.text
assert "'arduino' toolchain" in caplog.text
assert "upload_speed" not in caplog.text
def test_esp8266_rejects_unsupported_cli_toolchain() -> None:
"""Until the native backend lands, ESP8266 serves only PlatformIO."""
from esphome.components.esp8266 import CONFIG_SCHEMA
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):
CONFIG_SCHEMA({"board": "nodemcuv2"})
+31
View File
@@ -68,6 +68,7 @@ def _write_storage(
esp_platform: str | None = "ESP32",
core_platform: str | None = "esp32",
build_path: str | None = "/build/lite_test",
toolchain: str | None = None,
) -> None:
"""Write a vanilla StorageJSON sidecar for the cache tests."""
storage_path.parent.mkdir(parents=True, exist_ok=True)
@@ -88,6 +89,7 @@ def _write_storage(
"no_mdns": False,
"framework": "arduino",
"core_platform": core_platform,
"toolchain": toolchain,
}
storage_path.write_text(json.dumps(data), encoding="utf-8")
@@ -629,6 +631,35 @@ def test_save_compiled_config_and_sidecar_builds_real_sidecar(tmp_path: Path) ->
assert load_compiled_config(yaml_path) is not None
@pytest.mark.parametrize(
("sidecar_toolchain", "saved"),
[
("esp-idf", False),
("platformio", True),
(None, True), # legacy sidecar without the field: guard is inert
],
)
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."""
yaml_path = _bare_yaml(tmp_path)
_prime_core(tmp_path)
CORE.config = {CONF_ESPHOME: {CONF_NAME: "lite_test"}}
CORE.toolchain = Toolchain.PLATFORMIO
_write_storage(
tmp_path / ".esphome" / "storage" / "lite_test.yaml.json",
toolchain=sidecar_toolchain,
)
save_compiled_config_and_sidecar(CORE.config)
cache = tmp_path / ".esphome" / "storage" / "lite_test.yaml.validated.json"
assert cache.exists() is saved
assert (load_compiled_config(yaml_path) is not None) is saved
@pytest.mark.parametrize("command", ["upload", "logs"])
def test_run_esphome_upload_and_logs_refresh_cache_on_fallback(
tmp_path: Path, command: str
@@ -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,
@@ -3165,3 +3167,46 @@ def test_file__remapped_path_is_directory_raises(setup_core: Path) -> None:
with pytest.raises(Invalid, match="is not a file"):
cv.file_("/original/config/headers")
def test_require_platformio_toolchain() -> None:
"""Platforms with only the PlatformIO backend reject other toolchains."""
validator = cv.require_platformio_toolchain("RP2")
CORE.toolchain = None
config: dict = {}
assert validator(config) is config
assert CORE.toolchain == Toolchain.PLATFORMIO
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(Invalid, match="Unsupported toolchain 'arduino' for RP2"):
validator(config)
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."""
CORE.toolchain = None
with pytest.raises(Invalid, match="not resolved before RP2 validation"):
cv._check_supported_toolchain("RP2", (Toolchain.PLATFORMIO,))
@pytest.mark.parametrize(
("platform", "minimal_config"),
[
("host", {}),
("rp2", {"board": "rpipicow"}),
("bk72xx", {"board": "generic-bk7231n-qfn32-tuya"}),
("rtl87xx", {"board": "generic-rtl8710bn-2mb-788k"}),
("ln882x", {"board": "generic-ln882h"}),
# The legacy stub platform must reject too, not just the chip families
("libretiny", {}),
],
)
def test_every_platformio_only_platform_rejects_arduino_toolchain(
platform: str, minimal_config: dict
) -> None:
"""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'"):
module.CONFIG_SCHEMA(dict(minimal_config))
+18
View File
@@ -958,6 +958,24 @@ class TestEsphomeCore:
target.toolchain = const.Toolchain.ESP_IDF
assert target.using_toolchain_sdk_nrf is False
def test_using_toolchain_arduino(self, target):
"""A toolchain choice, distinct from the arduino target framework."""
target.toolchain = const.Toolchain.ARDUINO
assert target.using_toolchain_arduino is True
target.toolchain = const.Toolchain.PLATFORMIO
assert target.using_toolchain_arduino is False
def test_using_native_toolchain(self, target):
"""True exactly for the toolchains that never read platformio.ini."""
target.toolchain = const.Toolchain.ESP_IDF
assert target.using_native_toolchain is True
target.toolchain = const.Toolchain.ARDUINO
assert target.using_native_toolchain is True
target.toolchain = const.Toolchain.PLATFORMIO
assert target.using_native_toolchain is False
target.toolchain = const.Toolchain.SDK_NRF
assert target.using_native_toolchain is False
def test_add_library__extracts_short_name_from_path(self, target):
"""Test add_library extracts short name from library paths like owner/lib."""
target.data[const.KEY_CORE] = {
+36
View File
@@ -7195,6 +7195,42 @@ def test_compile_program_espidf_idedata_none_warns(
assert "No idedata was generated" in caplog.text
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."""
conf = tmp_path / "device.yaml"
conf.write_text("esphome:\n name: t\n")
argv = ["esphome", "--toolchain", "arduino", "logs", str(conf)]
with (
patch("esphome.compiled_config.load_compiled_config") as mock_cache,
patch("esphome.config.read_config", return_value=None) as mock_read,
):
assert run_esphome(argv) == 2
mock_cache.assert_not_called()
mock_read.assert_called_once()
def test_cli_toolchain_still_refreshes_the_validated_config_cache(
tmp_path: Path,
) -> None:
"""An explicit --toolchain gates only the cache read; with a matching
sidecar the freshly validated config is still saved."""
conf = tmp_path / "device.yaml"
conf.write_text("esphome:\n name: t\n")
argv = ["esphome", "--toolchain", "platformio", "logs", str(conf)]
with (
patch("esphome.compiled_config.load_compiled_config") as mock_load,
patch("esphome.config.read_config", return_value={CONF_ESPHOME: {}}),
patch("esphome.compiled_config.save_compiled_config_and_sidecar") as mock_save,
patch.dict(
"esphome.__main__.POST_CONFIG_ACTIONS", {"logs": Mock(return_value=0)}
),
):
assert run_esphome(argv) == 0
mock_load.assert_not_called()
mock_save.assert_called_once()
@pytest.mark.asyncio
async def test_wrap_to_code_comment_is_insertion_order_independent() -> None:
"""The config comment dumps with sorted keys: voluptuous fills schema
+12 -3
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
@@ -560,7 +563,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 = (
@@ -572,7 +574,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 = (
@@ -621,3 +622,11 @@ def test_needs_venv_rebuild_on_dangling_interpreter_symlink(tmp_path: Path) -> N
assert not python.exists()
assert _needs_venv_rebuild(python, sentinel, "abc123")
def test_resolve_toolchain_rejects_unsupported() -> None:
"""A --toolchain nRF52 cannot serve fails instead of degrading silently."""
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):
_resolve_toolchain({})