mirror of
https://github.com/esphome/esphome.git
synced 2026-11-03 11:47:38 +00:00
Put a recorder in front of the AmebaZ2 ROM fault handlers (vectors 3 to 7 of the RAM vector table). It saves PC, LR, SP, CFSR, the fault address and a stack scan into .ram.bss.noinit, re-arms the watchdog for a 1 s reset (the ROM handler spins after its dump) and continues into the ROM handler. The next boot logs the record and the CLI decodes it, as on BK72xx.
187 lines
6.8 KiB
Python
187 lines
6.8 KiB
Python
"""Guard the platform CLI-hook registry in ``esphome.platform_hooks``.
|
|
|
|
The registry lets the logs/upload fast path skip importing platform
|
|
packages that don't provide a hook; these tests fail when a platform
|
|
gains or loses a hook without the registry being updated, and pin down
|
|
that the fast path really avoids the import.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import logging
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from esphome import platform_hooks
|
|
from esphome.const import PLATFORM_ESP32, PLATFORM_LN882X, Platform
|
|
|
|
|
|
def test_no_unregistered_platform_exposes_a_hook() -> None:
|
|
"""Every platform hook the packages expose must be registered.
|
|
|
|
Behavioural on purpose: a hook added as a re-export, an assignment,
|
|
or an ``async def`` is invisible to source scanning but very visible
|
|
to ``hasattr``, and an unregistered hook is silently never called.
|
|
The registered direction is covered by
|
|
test_every_registered_pair_resolves below.
|
|
"""
|
|
for platform in frozenset(Platform):
|
|
module = importlib.import_module(f"esphome.components.{platform}")
|
|
for hook, registered in platform_hooks.PLATFORM_HOOKS.items():
|
|
if hasattr(module, hook):
|
|
assert platform in registered, (
|
|
f"{platform} exposes {hook} but is not registered for it. "
|
|
"Update esphome/platform_hooks.py."
|
|
)
|
|
|
|
|
|
def test_registered_platform_resolves_hook() -> None:
|
|
hook = platform_hooks.get_platform_hook(PLATFORM_ESP32, "process_stacktrace")
|
|
from esphome.components import esp32
|
|
|
|
assert hook is esp32.process_stacktrace
|
|
|
|
|
|
def test_every_registered_pair_resolves() -> None:
|
|
"""Each registered platform must actually expose the hook at runtime.
|
|
|
|
Text scanning can miss re-exports or decorated definitions; this is
|
|
the behavioural check for the direction that matters when the CLI
|
|
runs.
|
|
"""
|
|
for hook, platforms in platform_hooks.PLATFORM_HOOKS.items():
|
|
for platform in platforms:
|
|
assert callable(platform_hooks.get_platform_hook(platform, hook)), (
|
|
f"{platform} is registered for {hook} but does not expose it"
|
|
)
|
|
|
|
|
|
def test_external_platform_falls_back_to_probe(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Out-of-tree target platforms keep working via the dynamic probe."""
|
|
module = type("FakePlatform", (), {"show_logs": staticmethod(lambda *a: True)})
|
|
imported: list[str] = []
|
|
|
|
def fake_import(name: str):
|
|
imported.append(name)
|
|
return module
|
|
|
|
monkeypatch.setattr(platform_hooks, "import_module", fake_import)
|
|
hook = platform_hooks.get_platform_hook("my_external_chip", "show_logs")
|
|
assert hook is module.show_logs
|
|
assert imported == ["esphome.components.my_external_chip"]
|
|
|
|
|
|
def test_external_platform_missing_module_degrades(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""A warm-cache run may not have the external package importable.
|
|
|
|
Skipping a behavior-changing hook is visible at warning; losing
|
|
stacktrace decoding is cosmetic and stays at debug.
|
|
"""
|
|
monkeypatch.setattr(
|
|
platform_hooks,
|
|
"import_module",
|
|
Mock(
|
|
side_effect=ModuleNotFoundError(
|
|
"not found", name="esphome.components.my_external_chip"
|
|
)
|
|
),
|
|
)
|
|
assert platform_hooks.get_platform_hook("my_external_chip", "show_logs") is None
|
|
assert "not importable" in caplog.text
|
|
assert any(r.levelname == "WARNING" for r in caplog.records)
|
|
|
|
caplog.clear()
|
|
assert (
|
|
platform_hooks.get_platform_hook("my_external_chip", "process_stacktrace")
|
|
is None
|
|
)
|
|
assert not any(r.levelname == "WARNING" for r in caplog.records)
|
|
|
|
|
|
def test_external_platform_without_hook_logs_debug(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""The common no-hook case stays quiet but diagnosable."""
|
|
caplog.set_level("DEBUG", logger="esphome.platform_hooks")
|
|
module = type("ExternalPlatform", (), {}) # imports fine, no hook
|
|
monkeypatch.setattr(platform_hooks, "import_module", Mock(return_value=module))
|
|
assert platform_hooks.get_platform_hook("my_external_chip", "show_logs") is None
|
|
assert "does not expose" in caplog.text
|
|
assert not any(r.levelname == "WARNING" for r in caplog.records)
|
|
|
|
|
|
def test_stale_registry_entry_warns(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""A vendored tree where a registered hook vanished must say so."""
|
|
module = type("StalePlatform", (), {}) # registered but no hook
|
|
monkeypatch.setattr(platform_hooks, "import_module", Mock(return_value=module))
|
|
assert platform_hooks.get_platform_hook("nrf52", "show_logs") is None
|
|
assert "no longer exposes it" in caplog.text
|
|
|
|
|
|
def test_external_platform_broken_dependency_raises(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A missing dependency inside the external package must surface."""
|
|
monkeypatch.setattr(
|
|
platform_hooks,
|
|
"import_module",
|
|
Mock(side_effect=ModuleNotFoundError("not found", name="some_missing_dep")),
|
|
)
|
|
with pytest.raises(ModuleNotFoundError, match="not found"):
|
|
platform_hooks.get_platform_hook("my_external_chip", "show_logs")
|
|
|
|
|
|
def test_lookup_miss_does_not_import_platform_package(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The whole point: probing a platform without hooks must not import it."""
|
|
monkeypatch.setattr(
|
|
platform_hooks,
|
|
"import_module",
|
|
Mock(side_effect=AssertionError("platform package imported on registry miss")),
|
|
)
|
|
assert platform_hooks.get_platform_hook(PLATFORM_ESP32, "show_logs") is None
|
|
|
|
|
|
def test_get_stacktrace_handler_resolves_registered_platform() -> None:
|
|
hook = platform_hooks.get_stacktrace_handler(PLATFORM_ESP32)
|
|
from esphome.components import esp32
|
|
|
|
assert hook is esp32.process_stacktrace
|
|
|
|
|
|
def test_get_stacktrace_handler_reports_missing_analyzer(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
caplog.set_level("INFO", logger="esphome.platform_hooks")
|
|
assert platform_hooks.get_stacktrace_handler(PLATFORM_LN882X) is None
|
|
assert "no compatible analyzer" in caplog.text
|
|
# A capability gap is ordinary; it must not warn.
|
|
assert not any(r.levelno >= logging.WARNING for r in caplog.records)
|
|
|
|
|
|
def test_get_stacktrace_handler_reports_import_failure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
platform_hooks,
|
|
"import_module",
|
|
Mock(side_effect=ImportError("broken install")),
|
|
)
|
|
assert platform_hooks.get_stacktrace_handler(PLATFORM_ESP32) is None
|
|
assert "failed to import: broken install" in caplog.text
|
|
# A broken install is a real breakage; it must warn, not inform.
|
|
assert any(r.levelno == logging.WARNING for r in caplog.records)
|