[core] Share the stacktrace analyzer resolution in platform_hooks (#18075)

This commit is contained in:
J. Nick Koston
2026-08-04 14:38:28 -05:00
committed by GitHub
parent a904426431
commit 342f3d6994
4 changed files with 70 additions and 27 deletions
+3 -21
View File
@@ -630,27 +630,9 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
return 1
_LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
# Stacktrace analysis is optional; a broken platform import must not
# stop serial log streaming, but it is a real breakage and must not
# masquerade as an ordinary capability gap.
try:
process_stacktrace = platform_hooks.get_platform_hook(
CORE.target_platform, "process_stacktrace"
)
except ImportError as err:
_LOGGER.debug("Stacktrace analyzer import failed", exc_info=True)
_LOGGER.warning(
'Stacktrace analysis is unavailable: analyzer for target platform "%s" failed to import: %s',
CORE.target_platform,
err,
)
process_stacktrace = None
else:
if process_stacktrace is None:
_LOGGER.info(
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
CORE.target_platform,
)
# Stacktrace analysis is optional; platform_hooks owns resolution
# and the user-facing messages.
process_stacktrace = platform_hooks.get_stacktrace_handler(CORE.target_platform)
backtrace_state = False
ser = serial.Serial()
+31 -3
View File
@@ -10,9 +10,10 @@ imports each platform package and fails when they drift.
The compile-path ``run_compile`` hook is deliberately not registered:
compiling imports the platform package regardless, so its probe in
``__main__.py`` stays eager. The network log client's
``process_stacktrace`` probe in ``esphome/api_client.py`` still uses the
old importlib pattern; converting it is a separate change.
``__main__.py`` stays eager. The serial log path resolves
``process_stacktrace`` through get_stacktrace_handler below; the network
log client's probe in ``esphome/api_client.py`` still uses the old
importlib pattern and is converted separately.
"""
from __future__ import annotations
@@ -111,3 +112,30 @@ def get_platform_hook(platform: str, hook: str) -> Callable[..., Any] | None:
hook,
)
return handler
def get_stacktrace_handler(platform: str) -> Callable[..., Any] | None:
"""Resolve ``process_stacktrace`` for *platform*, degrading with a log.
Stacktrace decoding is a diagnostic nicety. This only distinguishes
an import failure from an ordinary capability gap so the message is
accurate; it returns None for both, and callers own any further
containment. Shared so the user-facing message lives in one place.
"""
try:
handler = get_platform_hook(platform, "process_stacktrace")
except ImportError as err:
# A real breakage, not an ordinary capability gap; say so louder.
_LOGGER.debug("Stacktrace analyzer import failed", exc_info=True)
_LOGGER.warning(
'Stacktrace analysis is unavailable: analyzer for target platform "%s" failed to import: %s',
platform,
err,
)
return None
if handler is None:
_LOGGER.info(
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
platform,
)
return handler
+2 -2
View File
@@ -5732,7 +5732,7 @@ def test_run_miniterm_analyzer_import_failure_keeps_streaming(
args = MockArgs()
with (
caplog.at_level("INFO", logger="esphome.__main__"),
caplog.at_level("INFO", logger="esphome.platform_hooks"),
patch("serial.Serial", return_value=mock_serial),
patch(
"esphome.platform_hooks.get_platform_hook",
@@ -5762,7 +5762,7 @@ def test_run_miniterm_no_stacktrace_analyzer(
args = MockArgs()
with (
caplog.at_level("INFO", logger="esphome.__main__"),
caplog.at_level("INFO", logger="esphome.platform_hooks"),
patch("serial.Serial", return_value=mock_serial),
):
result = run_miniterm(config, "/dev/ttyUSB0", args)
+34 -1
View File
@@ -9,12 +9,13 @@ 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
from esphome.const import PLATFORM_BK72XX, PLATFORM_ESP32, Platform
def test_no_unregistered_platform_exposes_a_hook() -> None:
@@ -151,3 +152,35 @@ def test_lookup_miss_does_not_import_platform_package(
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_BK72XX) 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)