mirror of
https://github.com/esphome/esphome.git
synced 2026-09-25 14:00:25 +00:00
[rp2] Rename rp2040 platform to rp2 (#17145)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
co-authored by
Jonathan Swoboda
parent
9caf431740
commit
cdd334284e
@@ -0,0 +1,95 @@
|
||||
"""Tests for the ``rp2`` target-platform component.
|
||||
|
||||
``rp2`` is the canonical name for the Raspberry Pi RP-series target
|
||||
platform. ``rp2040`` is a deprecated alias declared via
|
||||
``ALIASES = ["rp2040"]`` on the rp2 component — the framework
|
||||
(see ``esphome/loader.py`` and ``esphome/config.py``) handles both
|
||||
Python-import aliasing (via a ``sys.meta_path`` finder) and YAML-key
|
||||
aliasing (via a pre-pass in ``validate_config``), so there is no
|
||||
hand-rolled shim in ``esphome/components/rp2040/``.
|
||||
|
||||
These tests pin down the canonical board helpers; the alias contract
|
||||
itself (Python imports, YAML key rename, deprecation warning) is covered
|
||||
by the framework tests under ``tests/unit_tests/``.
|
||||
"""
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_known_wifi_board() -> None:
|
||||
"""``rpipicow`` is the canonical Pico W → True."""
|
||||
from esphome.components import rp2
|
||||
|
||||
assert rp2.board_id_has_wifi("rpipicow") is True
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_known_non_wifi_board() -> None:
|
||||
"""Plain ``rpipico`` has no CYW43 → False."""
|
||||
from esphome.components import rp2
|
||||
|
||||
assert rp2.board_id_has_wifi("rpipico") is False
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_rp2350_w_variant() -> None:
|
||||
"""``rpipico2w`` is the RP2350 Pico 2 W → True."""
|
||||
from esphome.components import rp2
|
||||
|
||||
assert rp2.board_id_has_wifi("rpipico2w") is True
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_unknown_board_returns_true() -> None:
|
||||
"""Unknown ids fail open so a custom board is not rejected.
|
||||
|
||||
The validator falls back to ESPHome's compile-time check; the
|
||||
helper returning True here means the wizard emits a ``wifi:``
|
||||
block and any genuinely-unsupported config trips the existing
|
||||
"no CYW43" guard at compile time.
|
||||
"""
|
||||
from esphome.components import rp2
|
||||
|
||||
assert rp2.board_id_has_wifi("not-a-real-board-id") is True
|
||||
|
||||
|
||||
def test_rp2_declares_rp2040_as_alias() -> None:
|
||||
"""The framework-level deprecation hook is on the ``rp2`` component.
|
||||
|
||||
The legacy ``rp2040:`` YAML key works because the rp2 component
|
||||
opts in via ``ALIASES``; without this declaration the rename
|
||||
framework wouldn't route legacy configs.
|
||||
"""
|
||||
from esphome.components import rp2
|
||||
|
||||
assert "rp2040" in rp2.ALIASES
|
||||
assert rp2.ALIAS_REMOVAL_VERSION == "2027.7.0"
|
||||
|
||||
|
||||
def test_rp2040_python_import_resolves_to_rp2() -> None:
|
||||
"""``from esphome.components import rp2040`` must work for external
|
||||
custom components and external tooling (device-builder, the dashboard
|
||||
wizard, etc.) that still import from the legacy module path.
|
||||
|
||||
The ``_AliasFinder`` on ``sys.meta_path`` rewrites the lookup to
|
||||
the canonical module — both should be the same object.
|
||||
"""
|
||||
from esphome.components import (
|
||||
rp2,
|
||||
rp2040, # routed via _AliasFinder
|
||||
)
|
||||
|
||||
assert rp2040 is rp2
|
||||
|
||||
|
||||
def test_rp2040_submodule_imports_resolve_to_rp2_submodules() -> None:
|
||||
"""Submodule imports (e.g. ``esphome.components.rp2040.boards``) must
|
||||
also route to the canonical equivalents — the board-generator script
|
||||
and the dashboard wizard both rely on this path.
|
||||
"""
|
||||
from esphome.components.rp2 import (
|
||||
boards as rp2_boards,
|
||||
generate_boards as rp2_generate,
|
||||
)
|
||||
from esphome.components.rp2040 import (
|
||||
boards as rp2040_boards,
|
||||
generate_boards as rp2040_generate,
|
||||
)
|
||||
|
||||
assert rp2040_boards is rp2_boards
|
||||
assert rp2040_generate is rp2_generate
|
||||
@@ -1,92 +0,0 @@
|
||||
"""Tests for RP2040 component public helpers and variant detection."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.rp2040 import _detect_variant, board_id_has_wifi
|
||||
from esphome.components.rp2040.const import VARIANT_RP2040, VARIANT_RP2350
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BOARD, CONF_VARIANT
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_known_wifi_board() -> None:
|
||||
"""``rpipicow`` is the canonical Pico W → True."""
|
||||
assert board_id_has_wifi("rpipicow") is True
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_known_non_wifi_board() -> None:
|
||||
"""Plain ``rpipico`` has no CYW43 → False."""
|
||||
assert board_id_has_wifi("rpipico") is False
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_rp2350_w_variant() -> None:
|
||||
"""``rpipico2w`` is the RP2350 Pico 2 W → True."""
|
||||
assert board_id_has_wifi("rpipico2w") is True
|
||||
|
||||
|
||||
def test_board_id_has_wifi_for_unknown_board_returns_true() -> None:
|
||||
"""Unknown ids fail open so a custom board is not rejected.
|
||||
|
||||
The validator falls back to ESPHome's compile-time check; the
|
||||
helper returning True here means the wizard emits a ``wifi:``
|
||||
block and any genuinely-unsupported config trips the existing
|
||||
"no CYW43" guard at compile time.
|
||||
"""
|
||||
assert board_id_has_wifi("not-a-real-board-id") is True
|
||||
|
||||
|
||||
def test_detect_variant_derives_variant_from_board() -> None:
|
||||
"""Board alone resolves to the matching variant."""
|
||||
result = _detect_variant({CONF_BOARD: "rpipicow"})
|
||||
assert result[CONF_BOARD] == "rpipicow"
|
||||
assert result[CONF_VARIANT] == VARIANT_RP2040
|
||||
|
||||
|
||||
def test_detect_variant_derives_variant_from_rp2350_board() -> None:
|
||||
"""An RP2350 board resolves to ``RP2350``."""
|
||||
result = _detect_variant({CONF_BOARD: "rpipico2"})
|
||||
assert result[CONF_BOARD] == "rpipico2"
|
||||
assert result[CONF_VARIANT] == VARIANT_RP2350
|
||||
|
||||
|
||||
def test_detect_variant_only_picks_default_board_rp2040() -> None:
|
||||
"""Variant alone picks Pico W as the canonical RP2040 board."""
|
||||
result = _detect_variant({CONF_VARIANT: VARIANT_RP2040})
|
||||
assert result[CONF_BOARD] == "rpipicow"
|
||||
assert result[CONF_VARIANT] == VARIANT_RP2040
|
||||
|
||||
|
||||
def test_detect_variant_only_picks_default_board_rp2350() -> None:
|
||||
"""Variant alone picks Pico 2 W as the canonical RP2350 board."""
|
||||
result = _detect_variant({CONF_VARIANT: VARIANT_RP2350})
|
||||
assert result[CONF_BOARD] == "rpipico2w"
|
||||
assert result[CONF_VARIANT] == VARIANT_RP2350
|
||||
|
||||
|
||||
def test_detect_variant_matching_explicit_variant_passes() -> None:
|
||||
"""Specifying both a board and the matching variant is allowed."""
|
||||
result = _detect_variant({CONF_BOARD: "rpipico2", CONF_VARIANT: VARIANT_RP2350})
|
||||
assert result[CONF_BOARD] == "rpipico2"
|
||||
assert result[CONF_VARIANT] == VARIANT_RP2350
|
||||
|
||||
|
||||
def test_detect_variant_mismatched_variant_raises() -> None:
|
||||
"""Board/variant mismatch must be rejected and name the offending board."""
|
||||
with pytest.raises(
|
||||
cv.Invalid, match=r"does not match the selected board 'rpipicow'"
|
||||
):
|
||||
_detect_variant({CONF_BOARD: "rpipicow", CONF_VARIANT: VARIANT_RP2350})
|
||||
|
||||
|
||||
def test_detect_variant_unknown_board_without_variant_raises() -> None:
|
||||
"""Unknown board with no variant tells the user how to recover."""
|
||||
with pytest.raises(cv.Invalid, match="please specify the chip variant"):
|
||||
_detect_variant({CONF_BOARD: "not-a-real-board"})
|
||||
|
||||
|
||||
def test_detect_variant_unknown_board_with_variant_passes() -> None:
|
||||
"""Unknown board + explicit variant is accepted (with a warning)."""
|
||||
result = _detect_variant(
|
||||
{CONF_BOARD: "not-a-real-board", CONF_VARIANT: VARIANT_RP2040}
|
||||
)
|
||||
assert result[CONF_BOARD] == "not-a-real-board"
|
||||
assert result[CONF_VARIANT] == VARIANT_RP2040
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Tests for rp2040 generate_boards.py."""
|
||||
"""Tests for rp2 generate_boards.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -8,7 +8,7 @@ import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.rp2040.generate_boards import load_boards, parse_variant_pins
|
||||
from esphome.components.rp2.generate_boards import load_boards, parse_variant_pins
|
||||
|
||||
PICO_PINS_HEADER = textwrap.dedent("""\
|
||||
#pragma once
|
||||
@@ -87,8 +87,8 @@ def test_has_native_wifi_esp32_variant_case_insensitive() -> None:
|
||||
|
||||
def test_has_native_wifi_dispatches_rp2040_to_board_check() -> None:
|
||||
"""RP2040 platform routes through ``rp2040.board_id_has_wifi``."""
|
||||
assert has_native_wifi(platform=Platform.RP2040, board="rpipicow") is True
|
||||
assert has_native_wifi(platform=Platform.RP2040, board="rpipico") is False
|
||||
assert has_native_wifi(platform=Platform.RP2, board="rpipicow") is True
|
||||
assert has_native_wifi(platform=Platform.RP2, board="rpipico") is False
|
||||
|
||||
|
||||
def test_has_native_wifi_returns_false_for_nrf52() -> None:
|
||||
@@ -134,7 +134,7 @@ def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None:
|
||||
|
||||
def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None:
|
||||
"""RP2040 without a board id falls open to True (custom-board default)."""
|
||||
assert has_native_wifi(platform=Platform.RP2040) is True
|
||||
assert has_native_wifi(platform=Platform.RP2) is True
|
||||
|
||||
|
||||
def _wifi_config(
|
||||
|
||||
@@ -39,7 +39,7 @@ from esphome.const import (
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_HOST,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RP2,
|
||||
PLATFORM_RTL87XX,
|
||||
SCHEDULER_DONT_RUN,
|
||||
TYPE_GIT,
|
||||
@@ -438,7 +438,7 @@ def hex_int__valid(value):
|
||||
("esp-idf", PLATFORM_ESP32, VARIANT_ESP32C6, "16", "16", "14", "14"),
|
||||
("arduino", PLATFORM_ESP32, VARIANT_ESP32H2, "18", "17", "18", "17"),
|
||||
("esp-idf", PLATFORM_ESP32, VARIANT_ESP32H2, "19", "19", "17", "17"),
|
||||
("arduino", PLATFORM_RP2040, None, "20", "20", "20", "20"),
|
||||
("arduino", PLATFORM_RP2, None, "20", "20", "20", "20"),
|
||||
("arduino", PLATFORM_BK72XX, None, "21", "21", "21", "21"),
|
||||
("arduino", PLATFORM_RTL87XX, None, "22", "22", "22", "22"),
|
||||
("arduino", PLATFORM_LN882X, None, "23", "23", "23", "23"),
|
||||
@@ -469,7 +469,7 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple)
|
||||
"esp32_c3": "11",
|
||||
"esp32_c6": "14",
|
||||
"esp32_h2": "17",
|
||||
"rp2040": "20",
|
||||
"rp2": "20",
|
||||
"bk72xx": "21",
|
||||
"rtl87xx": "22",
|
||||
"ln882x": "23",
|
||||
@@ -517,7 +517,7 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple)
|
||||
("arduino", PLATFORM_ESP32, "ESP32 using arduino framework"),
|
||||
("esp-idf", PLATFORM_ESP32, "ESP32 using esp-idf framework"),
|
||||
("arduino", PLATFORM_ESP8266, "ESP8266 using arduino framework"),
|
||||
("arduino", PLATFORM_RP2040, "RP2040 using arduino framework"),
|
||||
("arduino", PLATFORM_RP2, "RP2 using arduino framework"),
|
||||
("arduino", PLATFORM_BK72XX, "BK72XX using arduino framework"),
|
||||
("host", PLATFORM_HOST, "HOST using host framework"),
|
||||
],
|
||||
@@ -540,7 +540,7 @@ def test_require_framework_version(framework, platform, message):
|
||||
esp_idf=cv.Version(0, 5, 0),
|
||||
esp32_arduino=cv.Version(0, 5, 0),
|
||||
esp8266_arduino=cv.Version(0, 5, 0),
|
||||
rp2040_arduino=cv.Version(0, 5, 0),
|
||||
rp2_arduino=cv.Version(0, 5, 0),
|
||||
bk72xx_arduino=cv.Version(0, 5, 0),
|
||||
host=cv.Version(0, 5, 0),
|
||||
extra_message="test 1",
|
||||
@@ -556,7 +556,7 @@ def test_require_framework_version(framework, platform, message):
|
||||
esp_idf=cv.Version(2, 0, 0),
|
||||
esp32_arduino=cv.Version(2, 0, 0),
|
||||
esp8266_arduino=cv.Version(2, 0, 0),
|
||||
rp2040_arduino=cv.Version(2, 0, 0),
|
||||
rp2_arduino=cv.Version(2, 0, 0),
|
||||
bk72xx_arduino=cv.Version(2, 0, 0),
|
||||
host=cv.Version(2, 0, 0),
|
||||
extra_message="test 2",
|
||||
@@ -567,7 +567,7 @@ def test_require_framework_version(framework, platform, message):
|
||||
esp_idf=cv.Version(1, 5, 0),
|
||||
esp32_arduino=cv.Version(1, 5, 0),
|
||||
esp8266_arduino=cv.Version(1, 5, 0),
|
||||
rp2040_arduino=cv.Version(1, 5, 0),
|
||||
rp2_arduino=cv.Version(1, 5, 0),
|
||||
bk72xx_arduino=cv.Version(1, 5, 0),
|
||||
host=cv.Version(1, 5, 0),
|
||||
max_version=True,
|
||||
@@ -584,7 +584,7 @@ def test_require_framework_version(framework, platform, message):
|
||||
esp_idf=cv.Version(0, 5, 0),
|
||||
esp32_arduino=cv.Version(0, 5, 0),
|
||||
esp8266_arduino=cv.Version(0, 5, 0),
|
||||
rp2040_arduino=cv.Version(0, 5, 0),
|
||||
rp2_arduino=cv.Version(0, 5, 0),
|
||||
bk72xx_arduino=cv.Version(0, 5, 0),
|
||||
host=cv.Version(0, 5, 0),
|
||||
max_version=True,
|
||||
@@ -599,6 +599,194 @@ def test_require_framework_version(framework, platform, message):
|
||||
)("test")
|
||||
|
||||
|
||||
def _setup_core_for_framework(platform: str, framework: str) -> None:
|
||||
"""Wire CORE.data with the minimum keys for require_framework_version /
|
||||
SplitDefault to evaluate without raising KeyError."""
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
)
|
||||
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: platform,
|
||||
KEY_TARGET_FRAMEWORK: framework,
|
||||
KEY_FRAMEWORK_VERSION: cv.Version(1, 0, 0),
|
||||
}
|
||||
|
||||
|
||||
def test_only_on_rp2_passes_on_rp2_platform() -> None:
|
||||
"""``cv.only_on_rp2`` is the canonical family gate. It accepts any value
|
||||
untouched when the configured platform is rp2."""
|
||||
_setup_core_for_framework(PLATFORM_RP2, "arduino")
|
||||
assert cv.only_on_rp2("anything") == "anything"
|
||||
|
||||
|
||||
def test_only_on_rp2_rejects_other_platforms() -> None:
|
||||
"""The same gate raises ``Invalid`` outside the rp2 platform."""
|
||||
_setup_core_for_framework(PLATFORM_ESP32, "arduino")
|
||||
with pytest.raises(Invalid, match="rp2"):
|
||||
cv.only_on_rp2("anything")
|
||||
|
||||
|
||||
def test_only_on_rp2040_delegates_and_warns_once(caplog) -> None:
|
||||
"""``cv.only_on_rp2040`` is a deprecation shim — it logs a one-shot
|
||||
warning, dedupes via CORE.data, and delegates to ``only_on_rp2``.
|
||||
Repeated calls in the same run must not log again."""
|
||||
import logging
|
||||
|
||||
_setup_core_for_framework(PLATFORM_RP2, "arduino")
|
||||
# Reset the dedupe flag so this test is independent of order.
|
||||
CORE.data.pop(cv._ONLY_ON_RP2040_DEPRECATED_KEY, None)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.config_validation"):
|
||||
assert cv.only_on_rp2040("ok") == "ok"
|
||||
first_warnings = [r for r in caplog.records if "only_on_rp2040" in r.message]
|
||||
assert len(first_warnings) == 1
|
||||
assert "2027.7.0" in first_warnings[0].message
|
||||
|
||||
# Second call dedupes — no additional warning is emitted.
|
||||
assert cv.only_on_rp2040("ok") == "ok"
|
||||
warnings_after_second = [
|
||||
r for r in caplog.records if "only_on_rp2040" in r.message
|
||||
]
|
||||
assert len(warnings_after_second) == 1
|
||||
|
||||
|
||||
def test_only_on_rp2040_still_gates_on_non_rp2(caplog) -> None:
|
||||
"""The deprecation shim must still raise on non-rp2 platforms — it
|
||||
delegates to ``only_on_rp2``, so the gating behavior is preserved."""
|
||||
import logging
|
||||
|
||||
_setup_core_for_framework(PLATFORM_ESP32, "arduino")
|
||||
CORE.data.pop(cv._ONLY_ON_RP2040_DEPRECATED_KEY, None)
|
||||
|
||||
with (
|
||||
caplog.at_level(logging.WARNING, logger="esphome.config_validation"),
|
||||
pytest.raises(Invalid, match="rp2"),
|
||||
):
|
||||
cv.only_on_rp2040("anything")
|
||||
|
||||
|
||||
def test_require_framework_version_esp32_variant_specific_key() -> None:
|
||||
"""ESP32 variant-specific kwargs (``esp32_c3_arduino``) must win over
|
||||
the base ``esp32_arduino`` key when the configured variant matches."""
|
||||
from esphome.components.esp32 import KEY_ESP32
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
KEY_VARIANT,
|
||||
)
|
||||
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: PLATFORM_ESP32,
|
||||
KEY_TARGET_FRAMEWORK: "arduino",
|
||||
KEY_FRAMEWORK_VERSION: cv.Version(1, 2, 0),
|
||||
}
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32C3}
|
||||
|
||||
# Variant-specific entry permits this version; base key would reject it.
|
||||
assert (
|
||||
cv.require_framework_version(
|
||||
esp32_arduino=cv.Version(5, 0, 0), # would reject
|
||||
esp32_c3_arduino=cv.Version(1, 0, 0), # wins, ok
|
||||
)("test")
|
||||
== "test"
|
||||
)
|
||||
|
||||
|
||||
def test_require_framework_version_rp2_variant_specific_key() -> None:
|
||||
"""RP2 variant kwargs (``rp2_2040_arduino``) must win over the base
|
||||
``rp2_arduino`` key when ``CORE.data['rp2']['variant']`` is wired."""
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
)
|
||||
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: PLATFORM_RP2,
|
||||
KEY_TARGET_FRAMEWORK: "arduino",
|
||||
KEY_FRAMEWORK_VERSION: cv.Version(1, 2, 0),
|
||||
}
|
||||
CORE.data["rp2"] = {"variant": "RP2040"}
|
||||
|
||||
# Variant key wins — base ``rp2_arduino`` (which would reject) is ignored.
|
||||
assert (
|
||||
cv.require_framework_version(
|
||||
rp2_arduino=cv.Version(5, 0, 0), # would reject
|
||||
rp2_2040_arduino=cv.Version(1, 0, 0), # wins, ok
|
||||
)("test")
|
||||
== "test"
|
||||
)
|
||||
|
||||
# Without a variant kwarg the base ``rp2_arduino`` is used (fallback).
|
||||
CORE.data["rp2"] = {"variant": "RP2350"}
|
||||
assert (
|
||||
cv.require_framework_version(
|
||||
rp2_arduino=cv.Version(1, 0, 0),
|
||||
)("test")
|
||||
== "test"
|
||||
)
|
||||
|
||||
|
||||
def test_split_default_rp2_variant_keys() -> None:
|
||||
"""``SplitDefault`` resolves ``rp2_<chip>_<framework>`` first, falling
|
||||
back to ``rp2_<chip>`` and ``rp2_<framework>`` before the base key."""
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM
|
||||
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: PLATFORM_RP2,
|
||||
KEY_TARGET_FRAMEWORK: "arduino",
|
||||
}
|
||||
CORE.data["rp2"] = {"variant": "RP2040"}
|
||||
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
"full",
|
||||
rp2="base",
|
||||
rp2_arduino="base-framework",
|
||||
rp2_2040="variant-only",
|
||||
rp2_2040_arduino="variant-framework",
|
||||
): str,
|
||||
}
|
||||
)
|
||||
# Most specific (variant + framework) wins.
|
||||
assert schema({}).get("full") == "variant-framework"
|
||||
|
||||
# Drop the most-specific kwarg → variant-only wins.
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
"full",
|
||||
rp2="base",
|
||||
rp2_arduino="base-framework",
|
||||
rp2_2040="variant-only",
|
||||
): str,
|
||||
}
|
||||
)
|
||||
assert schema({}).get("full") == "variant-only"
|
||||
|
||||
# RP2350 variant — no rp2_2350_* kwargs → fall through to base framework.
|
||||
CORE.data["rp2"] = {"variant": "RP2350"}
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
"full",
|
||||
rp2="base",
|
||||
rp2_arduino="base-framework",
|
||||
rp2_2040="not-this",
|
||||
): str,
|
||||
}
|
||||
)
|
||||
assert schema({}).get("full") == "base-framework"
|
||||
|
||||
|
||||
def test_only_with_single_component_loaded() -> None:
|
||||
"""Test OnlyWith with single component when component is loaded."""
|
||||
CORE.loaded_integrations = {"mqtt"}
|
||||
|
||||
@@ -591,6 +591,36 @@ class TestEsphomeCore:
|
||||
assert target.is_esp32 is False
|
||||
assert target.is_esp8266 is True
|
||||
|
||||
def test_is_rp2(self, target):
|
||||
"""The canonical RP2 family gate flips on for the rp2 platform."""
|
||||
target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "rp2"}
|
||||
|
||||
assert target.is_rp2 is True
|
||||
assert target.is_esp32 is False
|
||||
assert target.is_esp8266 is False
|
||||
|
||||
def test_is_rp2040_deprecated_alias_matches_is_rp2(self, target, caplog):
|
||||
"""``is_rp2040`` is kept as a deprecation shim that returns whatever
|
||||
``is_rp2`` returns; both must agree across platform values. A
|
||||
one-shot deprecation warning is emitted on first access and
|
||||
deduped via ``CORE.data`` for the rest of the run."""
|
||||
import logging
|
||||
|
||||
target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "rp2"}
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.core"):
|
||||
assert target.is_rp2040 is True
|
||||
assert target.is_rp2040 == target.is_rp2
|
||||
|
||||
warnings = [r for r in caplog.records if "is_rp2040" in r.message]
|
||||
assert len(warnings) == 1
|
||||
assert "2027.7.0" in warnings[0].message
|
||||
|
||||
# Reset the dedupe so the False-platform branch also runs the shim.
|
||||
target.data.pop("_core_is_rp2040_deprecated_warned", None)
|
||||
target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "esp32"}
|
||||
assert target.is_rp2040 is False
|
||||
assert target.is_rp2040 == target.is_rp2
|
||||
|
||||
def test_firmware_bin__default(self, target):
|
||||
"""Default platforms produce <pioenvs>/<name>/firmware.bin."""
|
||||
target.name = "test-device"
|
||||
|
||||
+110
-434
@@ -1,19 +1,13 @@
|
||||
"""Unit tests for esphome.loader module."""
|
||||
|
||||
import ast
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import textwrap
|
||||
from types import ModuleType
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome import config as esphome_config, config_validation as cv
|
||||
from esphome.core import CORE
|
||||
import esphome.loader as loader_mod
|
||||
from esphome.loader import (
|
||||
AliasMeta,
|
||||
ComponentManifest,
|
||||
@@ -21,6 +15,7 @@ from esphome.loader import (
|
||||
_build_alias_map,
|
||||
_read_aliases,
|
||||
_replace_component_manifest,
|
||||
get_alias_metadata,
|
||||
get_component,
|
||||
)
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
@@ -348,17 +343,12 @@ def test_component_manifest_resources_recursive_filter_source_files_supports_sub
|
||||
# Component aliases (renamed-platform back-compat)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# These tests pin down the substrate behind `ALIASES = [...]` on component
|
||||
# `__init__.py` files: the AST scanner, the resulting global alias map, the
|
||||
# Python-import `sys.meta_path` finder, the `get_component` integration, and
|
||||
# the YAML pre-pass that rewrites legacy top-level keys.
|
||||
#
|
||||
# The framework is component-agnostic, so the integration tests inject a
|
||||
# synthetic alias map (pointing a fake legacy name at the real `esp32`
|
||||
# component) rather than depending on any specific renamed component.
|
||||
|
||||
# A legacy name that is NOT a real component, used as a synthetic alias.
|
||||
_FAKE_ALIAS = "esp32_legacy_alias"
|
||||
# The framework here is the substrate behind `ALIASES = [...]` on component
|
||||
# `__init__.py` files. These tests pin down the AST scanner, the resulting
|
||||
# global alias map, the Python-import `sys.meta_path` finder, and the
|
||||
# integration with `get_component`. The rp2 → rp2040 actual mapping in this
|
||||
# repo is used as a real-world fixture; other cases use temp dirs / mocks so
|
||||
# the framework's behavior is testable in isolation.
|
||||
|
||||
|
||||
def _write_component(root: Path, name: str, body: str) -> None:
|
||||
@@ -383,12 +373,12 @@ def test_read_aliases_extracts_removal_version(tmp_path: Path) -> None:
|
||||
init.write_text(
|
||||
textwrap.dedent("""\
|
||||
ALIASES = ['old']
|
||||
ALIAS_REMOVAL_VERSION = "2027.6.0"
|
||||
ALIAS_REMOVAL_VERSION = "2027.7.0"
|
||||
""")
|
||||
)
|
||||
aliases, removal = _read_aliases(init, ast)
|
||||
assert aliases == ["old"]
|
||||
assert removal == "2027.6.0"
|
||||
assert removal == "2027.7.0"
|
||||
|
||||
|
||||
def test_read_aliases_skips_dynamic_forms(tmp_path: Path) -> None:
|
||||
@@ -409,28 +399,19 @@ def test_read_aliases_returns_empty_for_missing_declaration(tmp_path: Path) -> N
|
||||
assert removal is None
|
||||
|
||||
|
||||
def test_read_aliases_handles_syntax_error(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
def test_read_aliases_handles_syntax_error(tmp_path: Path) -> None:
|
||||
"""A broken __init__.py shouldn't crash the alias scanner — it'll
|
||||
surface as an ImportError elsewhere, but the scanner logs a warning and
|
||||
yields nothing so other components keep working. The substring pre-filter
|
||||
only skips files with no ``ALIASES`` token, so this file (which has one)
|
||||
still reaches the parse."""
|
||||
surface as an ImportError elsewhere, but the scanner just yields
|
||||
nothing so other components keep working.
|
||||
|
||||
The source must contain the substring ``ALIASES`` so the scanner
|
||||
actually attempts to parse the file; otherwise the early-return
|
||||
optimization would short-circuit before reaching the parser and
|
||||
this test would not exercise the syntax-error branch.
|
||||
"""
|
||||
init = tmp_path / "__init__.py"
|
||||
init.write_text("ALIASES = ['x']\ndef broken( :\n")
|
||||
init.write_text("ALIASES = ['oops'\ndef broken( :\n")
|
||||
assert _read_aliases(init, ast) == ([], None)
|
||||
assert "Could not parse" in caplog.text
|
||||
|
||||
|
||||
def test_read_aliases_handles_read_error(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""An unreadable __init__.py logs a warning and yields nothing rather
|
||||
than aborting the whole component scan."""
|
||||
missing = tmp_path / "nope" / "__init__.py"
|
||||
assert _read_aliases(missing, ast) == ([], None)
|
||||
assert "Could not read" in caplog.text
|
||||
|
||||
|
||||
def test_build_alias_map_aggregates_components(tmp_path: Path) -> None:
|
||||
@@ -480,96 +461,64 @@ def test_build_alias_map_handles_missing_dir(tmp_path: Path) -> None:
|
||||
but possible in some test contexts), we want an empty map rather than
|
||||
a crash — the rest of the loader can still function."""
|
||||
fake = tmp_path / "does-not-exist"
|
||||
assert not fake.exists()
|
||||
with patch("esphome.loader.CORE_COMPONENTS_PATH", fake):
|
||||
alias_map, meta_map = _build_alias_map()
|
||||
assert alias_map == {}
|
||||
assert meta_map == {}
|
||||
|
||||
|
||||
def test_build_alias_map_rejects_alias_shadowing_component(tmp_path: Path) -> None:
|
||||
"""An alias that names an existing component package is refused: it would
|
||||
hijack a live domain, and a self-alias (alias == canonical) would send
|
||||
``_lookup_module`` into infinite recursion."""
|
||||
# `newcomp` declares itself as an alias — its own package already exists.
|
||||
_write_component(tmp_path, "newcomp", "ALIASES = ['newcomp']\n")
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
with (
|
||||
patch("esphome.loader.CORE_COMPONENTS_PATH", tmp_path),
|
||||
pytest.raises(EsphomeError, match="shadows an existing component"),
|
||||
):
|
||||
_build_alias_map()
|
||||
# ---- Live integration against the real rp2/rp2040 mapping in this repo ----
|
||||
|
||||
|
||||
# ---- Integration against a synthetic alias map (fake legacy -> esp32) ----
|
||||
def test_real_alias_map_includes_rp2040() -> None:
|
||||
"""The rp2 component declares ``ALIASES = ['rp2040']`` in this repo;
|
||||
the live alias map should surface it. This guards against future
|
||||
refactors silently dropping the declaration."""
|
||||
meta = get_alias_metadata()
|
||||
assert "rp2040" in meta
|
||||
assert meta["rp2040"].canonical == "rp2"
|
||||
assert meta["rp2040"].removal_version == "2027.7.0"
|
||||
|
||||
|
||||
def _patch_alias_map(monkeypatch: pytest.MonkeyPatch, mapping: dict[str, str]) -> None:
|
||||
"""Force the loader's alias map (used by the finder and get_component).
|
||||
|
||||
Patches the lazily-built caches so both ``_get_alias_map`` and the
|
||||
installed meta-path finder resolve against ``mapping`` regardless of
|
||||
what the real on-disk scan would produce.
|
||||
"""
|
||||
monkeypatch.setattr("esphome.loader._get_alias_map", lambda: mapping)
|
||||
|
||||
|
||||
def test_get_component_resolves_alias(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""``get_component(<alias>)`` should return the canonical manifest — every
|
||||
def test_get_component_resolves_alias() -> None:
|
||||
"""``get_component('rp2040')`` should return the rp2 manifest — every
|
||||
caller of the loader (dep checker, schema validator, codegen) hits
|
||||
the canonical component without knowing about the alias."""
|
||||
import esphome.loader as loader_mod
|
||||
|
||||
_patch_alias_map(monkeypatch, {_FAKE_ALIAS: "esp32"})
|
||||
loader_mod._COMPONENT_CACHE.pop(_FAKE_ALIAS, None)
|
||||
|
||||
canonical = get_component("esp32")
|
||||
aliased = get_component(_FAKE_ALIAS)
|
||||
assert canonical is not None
|
||||
assert aliased is canonical
|
||||
rp2 = get_component("rp2")
|
||||
rp2040 = get_component("rp2040")
|
||||
assert rp2 is not None
|
||||
assert rp2040 is rp2
|
||||
|
||||
|
||||
def test_alias_finder_resolves_top_level_import(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""``import esphome.components.<alias>`` resolves to the canonical
|
||||
module via the meta-path finder. ``_FAKE_ALIAS`` == ``esp32_legacy_alias``."""
|
||||
_patch_alias_map(monkeypatch, {_FAKE_ALIAS: "esp32"})
|
||||
sys.modules.pop(f"esphome.components.{_FAKE_ALIAS}", None)
|
||||
|
||||
def test_alias_finder_resolves_top_level_import() -> None:
|
||||
"""``import esphome.components.rp2040`` resolves to the canonical
|
||||
module via the meta-path finder."""
|
||||
# Remove any cached entry so we exercise the finder, not sys.modules cache.
|
||||
sys.modules.pop("esphome.components.rp2040", None)
|
||||
finder = _AliasFinder()
|
||||
spec = finder.find_spec(f"esphome.components.{_FAKE_ALIAS}", None)
|
||||
spec = finder.find_spec("esphome.components.rp2040", None)
|
||||
assert spec is not None
|
||||
|
||||
import esphome.components.esp32
|
||||
import esphome.components.esp32_legacy_alias
|
||||
import esphome.components.rp2
|
||||
import esphome.components.rp2040
|
||||
|
||||
assert esphome.components.esp32_legacy_alias is esphome.components.esp32
|
||||
assert esphome.components.rp2040 is esphome.components.rp2
|
||||
|
||||
|
||||
def test_alias_finder_resolves_submodule_import(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""``from esphome.components.<alias> import boards`` routes through to
|
||||
``esphome.components.esp32.boards`` — same submodule object on both paths.
|
||||
|
||||
The canonical submodule is imported first so its parent module carries
|
||||
the ``boards`` attribute; ``from <alias> import boards`` then resolves
|
||||
the aliased parent (via the finder) and reads that same attribute,
|
||||
rather than triggering a fresh file load under the alias name.
|
||||
``_FAKE_ALIAS`` == ``esp32_legacy_alias``."""
|
||||
_patch_alias_map(monkeypatch, {_FAKE_ALIAS: "esp32"})
|
||||
sys.modules.pop(f"esphome.components.{_FAKE_ALIAS}", None)
|
||||
|
||||
def test_alias_finder_resolves_submodule_import() -> None:
|
||||
"""``from esphome.components.rp2040 import boards`` routes through to
|
||||
``esphome.components.rp2.boards`` — same submodule object on both
|
||||
paths."""
|
||||
sys.modules.pop("esphome.components.rp2040.boards", None)
|
||||
finder = _AliasFinder()
|
||||
spec = finder.find_spec(f"esphome.components.{_FAKE_ALIAS}.boards", None)
|
||||
spec = finder.find_spec("esphome.components.rp2040.boards", None)
|
||||
assert spec is not None
|
||||
|
||||
from esphome.components.esp32 import boards as canonical_boards
|
||||
from esphome.components.esp32_legacy_alias import boards as aliased_boards
|
||||
from esphome.components.rp2 import boards as rp2_boards
|
||||
from esphome.components.rp2040 import boards as rp2040_boards
|
||||
|
||||
assert aliased_boards is canonical_boards
|
||||
assert rp2040_boards is rp2_boards
|
||||
|
||||
|
||||
def test_alias_finder_ignores_non_components_path() -> None:
|
||||
@@ -581,9 +530,6 @@ def test_alias_finder_ignores_non_components_path() -> None:
|
||||
assert finder.find_spec("os.path", None) is None
|
||||
# `esphome.components` itself (no domain segment) is not a candidate.
|
||||
assert finder.find_spec("esphome.components", None) is None
|
||||
# A real, non-aliased component domain defers to normal import machinery
|
||||
# (no component declares an alias in this repo, so the live map is empty).
|
||||
assert finder.find_spec("esphome.components.logger", None) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -593,391 +539,121 @@ def test_alias_finder_ignores_non_components_path() -> None:
|
||||
# The companion to the loader-side alias map: ``esphome.config`` runs a
|
||||
# pre-pass over the user's parsed YAML that rewrites legacy top-level keys
|
||||
# to their canonical names, surfacing a one-shot deprecation warning. These
|
||||
# tests inject a synthetic alias-metadata map so the rewrite behavior, the
|
||||
# warning text, and the both-keys-present conflict can be tested in isolation.
|
||||
|
||||
|
||||
def _patch_alias_metadata(
|
||||
monkeypatch: pytest.MonkeyPatch, mapping: dict[str, AliasMeta]
|
||||
) -> None:
|
||||
monkeypatch.setattr("esphome.loader.get_alias_metadata", lambda: mapping)
|
||||
# tests pin down the rewrite behavior, the warning text, and the
|
||||
# both-keys-present conflict.
|
||||
|
||||
|
||||
def test_resolve_component_aliases_renames_legacy_key(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A legacy alias key should be renamed to the canonical key and a
|
||||
deprecation warning citing the removal version logged."""
|
||||
"""A legacy alias key ``rp2040:`` should be renamed to the canonical
|
||||
``rp2:`` and a deprecation warning citing the removal version logged."""
|
||||
import logging
|
||||
|
||||
from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases
|
||||
from esphome.core import CORE
|
||||
|
||||
_patch_alias_metadata(
|
||||
monkeypatch,
|
||||
{"oldcomp": AliasMeta(canonical="newcomp", removal_version="2027.6.0")},
|
||||
)
|
||||
CORE.data.pop(_ALIAS_WARNED_KEY, None) # ensure the warning fires
|
||||
config = {"esphome": {"name": "test"}, "oldcomp": {"board": "x"}}
|
||||
config = {"esphome": {"name": "test"}, "rp2040": {"board": "rpipicow"}}
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.config"):
|
||||
_resolve_component_aliases(config)
|
||||
|
||||
assert "oldcomp" not in config
|
||||
assert config["newcomp"] == {"board": "x"}
|
||||
assert "rp2040" not in config
|
||||
assert config["rp2"] == {"board": "rpipicow"}
|
||||
assert any(
|
||||
"'oldcomp:' top-level key is deprecated" in record.message
|
||||
and "rename it to 'newcomp:'" in record.message
|
||||
and "2027.6.0" in record.message
|
||||
"'rp2040:' top-level key is deprecated" in record.message
|
||||
and "rename it to 'rp2:'" in record.message
|
||||
and "2027.7.0" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_component_aliases_dedupes_warning_within_a_run(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Schema validators can run twice (auto-load discovery + final pass)
|
||||
so the rename pass must emit the warning only once per alias per run.
|
||||
Deduped via ``CORE.data``; cleared between runs."""
|
||||
import logging
|
||||
|
||||
from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases
|
||||
from esphome.core import CORE
|
||||
|
||||
_patch_alias_metadata(
|
||||
monkeypatch,
|
||||
{"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)},
|
||||
)
|
||||
CORE.data.pop(_ALIAS_WARNED_KEY, None)
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.config"):
|
||||
_resolve_component_aliases({"oldcomp": {"board": "a"}})
|
||||
_resolve_component_aliases({"oldcomp": {"board": "b"}})
|
||||
_resolve_component_aliases({"rp2040": {"board": "rpipicow"}})
|
||||
_resolve_component_aliases({"rp2040": {"board": "rpipico2w"}})
|
||||
|
||||
matches = [
|
||||
r
|
||||
for r in caplog.records
|
||||
if "'oldcomp:' top-level key is deprecated" in r.message
|
||||
if "'rp2040:' top-level key is deprecated" in r.message
|
||||
]
|
||||
assert len(matches) == 1
|
||||
|
||||
|
||||
def test_resolve_component_aliases_rejects_both_keys_present(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
def test_resolve_component_aliases_rejects_both_keys_present() -> None:
|
||||
"""If the user has BOTH legacy and canonical keys, silently dropping
|
||||
one would hide a real misconfiguration. Raise instead."""
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases
|
||||
from esphome.core import CORE
|
||||
|
||||
_patch_alias_metadata(
|
||||
monkeypatch,
|
||||
{"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)},
|
||||
)
|
||||
CORE.data.pop(_ALIAS_WARNED_KEY, None)
|
||||
config = {"newcomp": {"board": "x"}, "oldcomp": {"board": "x"}}
|
||||
with pytest.raises(vol.Invalid, match="Both 'oldcomp:'"):
|
||||
config = {
|
||||
"rp2": {"board": "rpipicow"},
|
||||
"rp2040": {"board": "rpipicow"},
|
||||
}
|
||||
with pytest.raises(vol.Invalid, match="Both 'rp2040:'"):
|
||||
_resolve_component_aliases(config)
|
||||
|
||||
|
||||
def test_resolve_component_aliases_rejects_canonical_key_after_legacy(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The both-keys conflict must be detected even when the canonical key
|
||||
appears *after* the legacy key in the config (the up-front conflict
|
||||
scan, not a position-dependent check)."""
|
||||
from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases
|
||||
from esphome.core import CORE
|
||||
|
||||
_patch_alias_metadata(
|
||||
monkeypatch,
|
||||
{"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)},
|
||||
)
|
||||
CORE.data.pop(_ALIAS_WARNED_KEY, None)
|
||||
config = {"oldcomp": {"board": "x"}, "newcomp": {"board": "x"}}
|
||||
with pytest.raises(vol.Invalid, match="Both 'oldcomp:'"):
|
||||
_resolve_component_aliases(config)
|
||||
|
||||
|
||||
def test_resolve_component_aliases_rejects_multiple_aliases_of_one_component(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Two different deprecated aliases of the same canonical component is
|
||||
ambiguous — silently keeping one would hide a misconfiguration."""
|
||||
from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases
|
||||
from esphome.core import CORE
|
||||
|
||||
_patch_alias_metadata(
|
||||
monkeypatch,
|
||||
{
|
||||
"oldcomp": AliasMeta(canonical="newcomp", removal_version=None),
|
||||
"legacycomp": AliasMeta(canonical="newcomp", removal_version=None),
|
||||
},
|
||||
)
|
||||
CORE.data.pop(_ALIAS_WARNED_KEY, None)
|
||||
config = {"oldcomp": {"board": "x"}, "legacycomp": {"board": "y"}}
|
||||
with pytest.raises(vol.Invalid, match=r"Multiple deprecated aliases of 'newcomp:'"):
|
||||
_resolve_component_aliases(config)
|
||||
|
||||
|
||||
def test_resolve_component_aliases_preserves_key_position(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The renamed canonical key keeps the legacy key's original position
|
||||
rather than being moved to the end of the config."""
|
||||
from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases
|
||||
from esphome.core import CORE
|
||||
|
||||
_patch_alias_metadata(
|
||||
monkeypatch,
|
||||
{"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)},
|
||||
)
|
||||
CORE.data.pop(_ALIAS_WARNED_KEY, None)
|
||||
config = {"esphome": {"name": "t"}, "oldcomp": {"board": "x"}, "logger": {}}
|
||||
|
||||
_resolve_component_aliases(config)
|
||||
|
||||
assert list(config) == ["esphome", "newcomp", "logger"]
|
||||
|
||||
|
||||
def test_resolve_component_aliases_no_op_when_no_legacy_keys(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
def test_resolve_component_aliases_no_op_when_no_legacy_keys() -> None:
|
||||
"""The pre-pass must be a no-op (no warning, no mutation) for configs
|
||||
that already use canonical keys."""
|
||||
import logging
|
||||
|
||||
from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases
|
||||
from esphome.core import CORE
|
||||
|
||||
_patch_alias_metadata(
|
||||
monkeypatch,
|
||||
{"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)},
|
||||
)
|
||||
CORE.data.pop(_ALIAS_WARNED_KEY, None)
|
||||
config = {"esphome": {"name": "test"}, "newcomp": {"board": "x"}}
|
||||
config = {"esphome": {"name": "test"}, "rp2": {"board": "rpipicow"}}
|
||||
original = dict(config)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.config"):
|
||||
with caplog_at_warning() as records:
|
||||
_resolve_component_aliases(config)
|
||||
|
||||
assert config == original
|
||||
assert not any("deprecated" in r.message for r in caplog.records)
|
||||
assert not any("deprecated" in r.message for r in records)
|
||||
_ = logging # silence unused-import in branches that don't read records
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ComponentManifest alias properties
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper context manager — small enough to inline rather than pull in
|
||||
# caplog for the simple "did anything warn?" case above.
|
||||
import contextlib # noqa: E402
|
||||
|
||||
|
||||
def test_component_manifest_alias_properties_default_empty() -> None:
|
||||
"""``aliases`` / ``alias_removal_version`` fall back to ``[]`` / ``None``
|
||||
when the component module declares neither.
|
||||
@contextlib.contextmanager
|
||||
def caplog_at_warning():
|
||||
"""Minimal in-test caplog substitute: collect WARNING records on a
|
||||
dedicated handler attached to ``esphome.config``."""
|
||||
import logging
|
||||
|
||||
Uses a real ``ModuleType`` rather than a ``MagicMock`` so that the
|
||||
``getattr(..., default)`` fallback is actually exercised — a bare mock
|
||||
auto-creates any attribute on access and would never hit the default."""
|
||||
mod = ModuleType("fake_component")
|
||||
manifest = ComponentManifest(mod)
|
||||
assert manifest.aliases == []
|
||||
assert manifest.alias_removal_version is None
|
||||
logger = logging.getLogger("esphome.config")
|
||||
records: list[logging.LogRecord] = []
|
||||
|
||||
class _Handler(logging.Handler):
|
||||
def emit(self, record): # noqa: D401
|
||||
records.append(record)
|
||||
|
||||
def test_component_manifest_alias_properties_read_module_values() -> None:
|
||||
"""The properties surface the module's declared values verbatim."""
|
||||
mod = MagicMock()
|
||||
mod.ALIASES = ["legacy"]
|
||||
mod.ALIAS_REMOVAL_VERSION = "2027.6.0"
|
||||
manifest = ComponentManifest(mod)
|
||||
assert manifest.aliases == ["legacy"]
|
||||
assert manifest.alias_removal_version == "2027.6.0"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Real (unpatched) lazy build + cache and remaining scanner branches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_alias_map_real_build_and_caches(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Exercise the real lazy build over the actual components dir (no patch):
|
||||
the first call scans and caches, the second returns the cached object."""
|
||||
monkeypatch.setattr(loader_mod, "_ALIAS_MAP_CACHE", None)
|
||||
first = loader_mod._get_alias_map()
|
||||
second = loader_mod._get_alias_map()
|
||||
assert isinstance(first, dict)
|
||||
assert first is second # cached, not rebuilt on the second call
|
||||
|
||||
|
||||
def test_get_alias_metadata_real_build_and_caches(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(loader_mod, "_ALIAS_META_CACHE", None)
|
||||
first = loader_mod.get_alias_metadata()
|
||||
second = loader_mod.get_alias_metadata()
|
||||
assert isinstance(first, dict)
|
||||
assert first is second
|
||||
|
||||
|
||||
def test_build_alias_map_skips_files_and_initless_dirs(tmp_path: Path) -> None:
|
||||
"""Loose files and directories without an ``__init__.py`` are ignored;
|
||||
only real component packages contribute to the map."""
|
||||
(tmp_path / "loose_file.py").write_text("ALIASES = ['ignored']\n")
|
||||
(tmp_path / "initless").mkdir() # a dir, but no __init__.py
|
||||
_write_component(tmp_path, "realcomp", "ALIASES = ['legacy']\n")
|
||||
|
||||
with patch("esphome.loader.CORE_COMPONENTS_PATH", tmp_path):
|
||||
alias_map, _ = _build_alias_map()
|
||||
|
||||
assert alias_map == {"legacy": "realcomp"}
|
||||
|
||||
|
||||
def test_read_aliases_ignores_non_assignment_and_complex_targets(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Non-assignment statements and assignments to non-Name targets are
|
||||
skipped; only simple ``NAME = ...`` assignments are read."""
|
||||
init = tmp_path / "__init__.py"
|
||||
init.write_text(
|
||||
"import os\n" # non-Assign (Import) node -> skipped
|
||||
"obj.attr = 'v'\n" # Assign with an Attribute target -> skipped
|
||||
"ALIASES = ['legacy']\n"
|
||||
)
|
||||
aliases, _ = _read_aliases(init, ast)
|
||||
assert aliases == ["legacy"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Finder / loader edge branches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_alias_finder_returns_none_when_canonical_missing(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""If an alias points at a canonical *target* that doesn't exist, the
|
||||
finder declines (returns None) and lets normal import machinery report
|
||||
the missing module."""
|
||||
_patch_alias_map(monkeypatch, {"broken_alias": "definitely_not_a_real_component"})
|
||||
finder = _AliasFinder()
|
||||
assert finder.find_spec("esphome.components.broken_alias", None) is None
|
||||
|
||||
|
||||
def test_alias_finder_reraises_when_canonical_dependency_missing(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""If the canonical module exists but fails to import one of its own
|
||||
dependencies, the finder surfaces that real error instead of masking it
|
||||
as an unresolved alias (which would silently fall through to a confusing
|
||||
'no module named <alias>')."""
|
||||
_patch_alias_map(monkeypatch, {"some_alias": "real_canonical"})
|
||||
|
||||
def boom(name: str) -> None:
|
||||
raise ModuleNotFoundError("No module named 'missing_dep'", name="missing_dep")
|
||||
|
||||
monkeypatch.setattr("esphome.loader.importlib.import_module", boom)
|
||||
finder = _AliasFinder()
|
||||
with pytest.raises(ModuleNotFoundError, match="missing_dep"):
|
||||
finder.find_spec("esphome.components.some_alias", None)
|
||||
|
||||
|
||||
def test_install_alias_finder_is_idempotent() -> None:
|
||||
"""The finder is installed once at import; calling the installer again is
|
||||
a no-op (no duplicate ``_AliasFinder`` on ``sys.meta_path``)."""
|
||||
before = [e for e in sys.meta_path if isinstance(e, _AliasFinder)]
|
||||
assert len(before) == 1 # installed at module import time
|
||||
loader_mod._install_alias_finder()
|
||||
after = [e for e in sys.meta_path if isinstance(e, _AliasFinder)]
|
||||
assert len(after) == 1
|
||||
|
||||
|
||||
def test_get_component_alias_to_missing_canonical_returns_none(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""If an alias resolves to a canonical component that can't be loaded,
|
||||
``get_component`` returns None and caches no bogus manifest."""
|
||||
_patch_alias_map(monkeypatch, {"ghost_alias": "definitely_not_a_real_component"})
|
||||
loader_mod._COMPONENT_CACHE.pop("ghost_alias", None)
|
||||
|
||||
assert get_component("ghost_alias") is None
|
||||
assert "ghost_alias" not in loader_mod._COMPONENT_CACHE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# YAML pre-pass: empty-map fast path + validate_config integration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_resolve_component_aliases_noop_when_no_aliases_declared(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When no component declares an alias, the pre-pass returns immediately
|
||||
without inspecting or mutating the config."""
|
||||
from esphome.config import _resolve_component_aliases
|
||||
|
||||
monkeypatch.setattr("esphome.loader.get_alias_metadata", dict) # empty map
|
||||
config = {"esphome": {"name": "t"}, "rp2040": {"board": "x"}}
|
||||
original = dict(config)
|
||||
_resolve_component_aliases(config)
|
||||
assert config == original
|
||||
|
||||
|
||||
def _default_component_mock() -> Mock:
|
||||
"""A permissive component mock that validates any config (ALLOW_EXTRA)."""
|
||||
return Mock(
|
||||
auto_load=[],
|
||||
is_platform_component=False,
|
||||
is_platform=False,
|
||||
multi_conf=False,
|
||||
multi_conf_no_default=False,
|
||||
dependencies=[],
|
||||
conflicts_with=[],
|
||||
config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_core")
|
||||
def test_validate_config_renames_alias_key(
|
||||
mock_get_component: Mock, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""End-to-end: a legacy top-level key is renamed to its canonical name
|
||||
before the rest of ``validate_config`` runs, and validation succeeds.
|
||||
|
||||
A real ``esp32`` target platform is included so ``preload_core_config``
|
||||
is satisfied and validation runs to completion (the renamed canonical
|
||||
key is loaded via the mocked, permissive component)."""
|
||||
mock_get_component.side_effect = lambda name: _default_component_mock()
|
||||
monkeypatch.setattr(
|
||||
"esphome.loader.get_alias_metadata",
|
||||
lambda: {
|
||||
"legacyfoo": AliasMeta(canonical="newcomp", removal_version="2027.6.0")
|
||||
},
|
||||
)
|
||||
CORE.data.pop("_component_aliases_warned", None)
|
||||
|
||||
raw_config = {
|
||||
"esphome": {"name": "test"},
|
||||
"esp32": {"board": "esp32dev"},
|
||||
"legacyfoo": {"opt": 1},
|
||||
}
|
||||
result = esphome_config.validate_config(raw_config, {})
|
||||
|
||||
assert not result.errors, f"unexpected errors: {result.errors}"
|
||||
assert "newcomp" in result
|
||||
assert "legacyfoo" not in result
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_core")
|
||||
def test_validate_config_reports_alias_conflict_as_error(
|
||||
mock_get_component: Mock, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""If both the legacy and canonical keys are present, ``validate_config``
|
||||
surfaces the conflict as a config error (the ``vol.Invalid`` path)."""
|
||||
mock_get_component.return_value = _default_component_mock()
|
||||
monkeypatch.setattr(
|
||||
"esphome.loader.get_alias_metadata",
|
||||
lambda: {"legacyfoo": AliasMeta(canonical="newcomp", removal_version=None)},
|
||||
)
|
||||
CORE.data.pop("_component_aliases_warned", None)
|
||||
|
||||
raw_config = {
|
||||
"esphome": {"name": "test"},
|
||||
"newcomp": {"opt": 1},
|
||||
"legacyfoo": {"opt": 2},
|
||||
}
|
||||
result = esphome_config.validate_config(raw_config, {})
|
||||
|
||||
assert result.errors
|
||||
assert "Both 'legacyfoo:'" in str(result.errors)
|
||||
handler = _Handler(level=logging.WARNING)
|
||||
logger.addHandler(handler)
|
||||
prev_level = logger.level
|
||||
logger.setLevel(logging.WARNING)
|
||||
try:
|
||||
yield records
|
||||
finally:
|
||||
logger.removeHandler(handler)
|
||||
logger.setLevel(prev_level)
|
||||
|
||||
@@ -94,7 +94,7 @@ from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RP2,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
@@ -1226,7 +1226,7 @@ def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel(
|
||||
mock_choose_prompt: Mock,
|
||||
) -> None:
|
||||
"""Test interactive mode shows RP2040 BOOTSEL option via picotool."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -1249,7 +1249,7 @@ def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel(
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_rp2040_no_device_shows_bootsel_help() -> None:
|
||||
"""Test BOOTSEL instructions shown when no RP2040 device found."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -1271,7 +1271,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_ota(
|
||||
) -> None:
|
||||
"""Test BOOTSEL tip shown when only OTA options exist for RP2040."""
|
||||
setup_core(
|
||||
platform=PLATFORM_RP2040,
|
||||
platform=PLATFORM_RP2,
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
@@ -1300,7 +1300,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports(
|
||||
mock_choose_prompt: Mock,
|
||||
) -> None:
|
||||
"""Test BOOTSEL tip shown when serial ports exist but no BOOTSEL device."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
|
||||
mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")]
|
||||
with (
|
||||
@@ -1325,7 +1325,7 @@ def test_choose_upload_log_host_rp2040_permission_error_no_options(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test permission warning shown when BOOTSEL device found but not accessible."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -1355,7 +1355,7 @@ def test_choose_upload_log_host_rp2040_permission_error_with_ota(
|
||||
) -> None:
|
||||
"""Test permission warning shown with OTA fallback available."""
|
||||
setup_core(
|
||||
platform=PLATFORM_RP2040,
|
||||
platform=PLATFORM_RP2,
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
@@ -1412,7 +1412,7 @@ def test_choose_upload_log_host_rp2040_serial_and_bootsel(
|
||||
mock_choose_prompt: Mock,
|
||||
) -> None:
|
||||
"""Test both serial ports and BOOTSEL option shown for RP2040."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
|
||||
mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")]
|
||||
with (
|
||||
@@ -1665,7 +1665,7 @@ def test_upload_using_esptool_with_file_path(
|
||||
@pytest.mark.parametrize(
|
||||
"platform,device",
|
||||
[
|
||||
(PLATFORM_RP2040, "/dev/ttyACM0"),
|
||||
(PLATFORM_RP2, "/dev/ttyACM0"),
|
||||
(PLATFORM_BK72XX, "/dev/ttyUSB0"), # LibreTiny platform
|
||||
],
|
||||
)
|
||||
@@ -1720,7 +1720,7 @@ def test_upload_using_platformio_creates_signed_bin_for_rp2040(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that upload_using_platformio creates firmware.bin.signed for RP2040."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
@@ -1756,6 +1756,53 @@ def test_upload_using_platformio_skips_signed_bin_for_non_rp2040(
|
||||
assert result == 0
|
||||
|
||||
|
||||
def test_upload_using_platformio_skips_signed_bin_when_already_present(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""The signed-bin copy is idempotent: if ``firmware.bin.signed`` already
|
||||
exists on the RP2 build path, the upload step must not overwrite it
|
||||
(and must not fail when the unsigned ``firmware.bin`` is absent)."""
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
# Pre-existing signed bin with distinct content — must be preserved.
|
||||
signed_bin = build_dir / "firmware.bin.signed"
|
||||
signed_bin.write_bytes(b"already signed")
|
||||
# No unsigned firmware.bin on disk — the `is_file()` guard must hold.
|
||||
firmware_elf = build_dir / "firmware.elf"
|
||||
firmware_elf.write_bytes(b"elf")
|
||||
|
||||
mock_idedata = MagicMock()
|
||||
mock_idedata.firmware_elf_path = str(firmware_elf)
|
||||
|
||||
with (
|
||||
patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata),
|
||||
patch("esphome.platformio.toolchain.run_platformio_cli_run", return_value=0),
|
||||
):
|
||||
result = upload_using_platformio({}, "/dev/ttyACM0")
|
||||
|
||||
assert result == 0
|
||||
# Pre-existing signed bin is untouched.
|
||||
assert signed_bin.read_bytes() == b"already signed"
|
||||
|
||||
|
||||
def test_upload_using_platformio_handles_port_none(tmp_path: Path) -> None:
|
||||
"""The upload step must work without a serial port (PlatformIO picks the
|
||||
target itself); the ``--upload-port`` flag is only appended when a port
|
||||
is provided."""
|
||||
setup_core(platform=PLATFORM_ESP32)
|
||||
|
||||
with patch(
|
||||
"esphome.platformio.toolchain.run_platformio_cli_run", return_value=0
|
||||
) as mock_run:
|
||||
result = upload_using_platformio({}, None)
|
||||
|
||||
assert result == 0
|
||||
args = mock_run.call_args.args
|
||||
assert "--upload-port" not in args
|
||||
|
||||
|
||||
def test_upload_program_serial_upload_failed(
|
||||
mock_upload_using_esptool: Mock,
|
||||
mock_get_port_type: Mock,
|
||||
@@ -1783,7 +1830,7 @@ def test_upload_program_bootsel(
|
||||
mock_get_port_type: Mock,
|
||||
) -> None:
|
||||
"""Test upload_program with BOOTSEL for RP2040."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
mock_get_port_type.return_value = "BOOTSEL"
|
||||
mock_upload_using_picotool.return_value = 0
|
||||
|
||||
@@ -1804,7 +1851,7 @@ def test_upload_program_bootsel_failed(
|
||||
mock_get_port_type: Mock,
|
||||
) -> None:
|
||||
"""Test upload_program when BOOTSEL upload fails."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
setup_core(platform=PLATFORM_RP2)
|
||||
mock_get_port_type.return_value = "BOOTSEL"
|
||||
mock_upload_using_picotool.return_value = 1
|
||||
|
||||
@@ -1821,7 +1868,7 @@ def test_upload_program_bootsel_failed(
|
||||
|
||||
def test_upload_using_picotool_success(tmp_path: Path) -> None:
|
||||
"""Test upload_using_picotool succeeds."""
|
||||
setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path)
|
||||
setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path)
|
||||
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
@@ -1858,7 +1905,7 @@ def test_upload_using_picotool_success(tmp_path: Path) -> None:
|
||||
|
||||
def test_upload_using_picotool_no_elf(tmp_path: Path) -> None:
|
||||
"""Test upload_using_picotool when ELF file is missing."""
|
||||
setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path)
|
||||
setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path)
|
||||
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
@@ -1876,7 +1923,7 @@ def test_upload_using_picotool_no_elf(tmp_path: Path) -> None:
|
||||
|
||||
def test_upload_using_picotool_not_found(tmp_path: Path) -> None:
|
||||
"""Test upload_using_picotool when picotool binary not found."""
|
||||
setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path)
|
||||
setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path)
|
||||
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
@@ -1896,7 +1943,7 @@ def test_upload_using_picotool_not_found(tmp_path: Path) -> None:
|
||||
|
||||
def test_upload_using_picotool_permission_error(tmp_path: Path) -> None:
|
||||
"""Test upload_using_picotool shows helpful message on permission error."""
|
||||
setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path)
|
||||
setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path)
|
||||
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
@@ -6411,7 +6458,7 @@ def test_command_run_rp2040_bootsel_redetects_serial_port() -> None:
|
||||
picks up the newly enumerated serial port before showing logs."""
|
||||
setup_core(
|
||||
config={"logger": {}, CONF_API: {}, CONF_MDNS: {CONF_DISABLED: False}},
|
||||
platform=PLATFORM_RP2040,
|
||||
platform=PLATFORM_RP2,
|
||||
)
|
||||
|
||||
args = MockArgs()
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
|
||||
from esphome.components.esp32.boards import ESP32_BOARD_PINS
|
||||
from esphome.components.esp8266.boards import ESP8266_BOARD_PINS
|
||||
from esphome.components.ln882x.boards import LN882X_BOARD_PINS
|
||||
from esphome.components.rp2.boards import RP2_BOARD_PINS
|
||||
from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS
|
||||
from esphome.core import CORE
|
||||
import esphome.wizard as wz
|
||||
@@ -300,6 +301,31 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx(
|
||||
assert "rtl87xx:" in generated_config
|
||||
|
||||
|
||||
def test_wizard_write_defaults_platform_from_board_rp2(
|
||||
default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch
|
||||
):
|
||||
"""
|
||||
If the platform is not explicitly set, use "RP2" when the board is in
|
||||
the RP2 boards list. The generated config must use the canonical
|
||||
``rp2:`` top-level key (not the deprecated ``rp2040:`` alias).
|
||||
"""
|
||||
# Given
|
||||
del default_config["platform"]
|
||||
default_config["board"] = [*RP2_BOARD_PINS][0]
|
||||
|
||||
monkeypatch.setattr(wz, "write_file", MagicMock())
|
||||
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
|
||||
|
||||
# When
|
||||
wz.wizard_write(tmp_path, **default_config)
|
||||
|
||||
# Then
|
||||
generated_config = wz.write_file.call_args.args[1]
|
||||
assert "rp2:" in generated_config
|
||||
# Guard against regressing to the legacy alias key.
|
||||
assert "rp2040:" not in generated_config
|
||||
|
||||
|
||||
def test_safe_print_step_prints_step_number_and_description(monkeypatch: MonkeyPatch):
|
||||
"""
|
||||
The safe_print_step function prints the step number and the passed description
|
||||
@@ -450,6 +476,34 @@ def test_wizard_accepts_default_answers_esp32(
|
||||
assert retval == 0
|
||||
|
||||
|
||||
def test_wizard_accepts_default_answers_bk72xx(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
|
||||
):
|
||||
"""
|
||||
The wizard should accept the given default answers for bk72xx. The
|
||||
libretiny branch also exercises the False side of the
|
||||
``elif platform == "RP2":`` checks in the platform / board-link
|
||||
elif chain (without this, those branches show as partial coverage
|
||||
because only the rpipico interactive test reaches them with platform
|
||||
== "RP2").
|
||||
"""
|
||||
# Given
|
||||
wizard_answers[1] = "BK72XX"
|
||||
wizard_answers[2] = next(iter(BK72XX_BOARD_PINS))
|
||||
config_file = tmp_path / "test.yaml"
|
||||
input_mock = MagicMock(side_effect=wizard_answers)
|
||||
monkeypatch.setattr("builtins.input", input_mock)
|
||||
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
|
||||
monkeypatch.setattr(wz, "sleep", lambda _: 0)
|
||||
monkeypatch.setattr(wz, "wizard_write", MagicMock())
|
||||
|
||||
# When
|
||||
retval = wz.wizard(config_file)
|
||||
|
||||
# Then
|
||||
assert retval == 0
|
||||
|
||||
|
||||
def test_wizard_offers_better_node_name(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
|
||||
):
|
||||
@@ -612,7 +666,7 @@ def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch):
|
||||
# Given
|
||||
wizard_answers_rp2040 = [
|
||||
"test-node", # Name of the node
|
||||
"RP2040", # platform
|
||||
"RP2", # platform (canonical name; ``RP2040`` was the legacy alias)
|
||||
"rpipico", # board (no WiFi support)
|
||||
]
|
||||
config_file = tmp_path / "test.yaml"
|
||||
|
||||
@@ -18,7 +18,7 @@ from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RP2,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import EsphomeError
|
||||
@@ -338,7 +338,7 @@ def test_storage_should_not_update_cmake_cache_when_nothing_changes(
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"core_platform",
|
||||
[PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX],
|
||||
[PLATFORM_ESP8266, PLATFORM_RP2, PLATFORM_BK72XX, PLATFORM_RTL87XX],
|
||||
)
|
||||
def test_storage_should_not_update_cmake_cache_for_non_esp32(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
|
||||
Reference in New Issue
Block a user