mirror of
https://github.com/esphome/esphome.git
synced 2026-08-25 07:36:19 +00:00
152 lines
5.3 KiB
Python
152 lines
5.3 KiB
Python
"""Tests for esphome.stacktrace."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
from esphome import stacktrace
|
|
from esphome.const import PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266
|
|
from esphome.core import EsphomeError
|
|
|
|
CONFIG = {"esphome": {"name": "test"}}
|
|
|
|
|
|
def _run(
|
|
handler,
|
|
platform: str = PLATFORM_ESP32,
|
|
lines: tuple[str, ...] = ("PC: 0x4010496e",),
|
|
) -> stacktrace.LogLineProcessor:
|
|
"""Processor with the resolver stubbed, fed the given lines."""
|
|
with patch.object(
|
|
stacktrace.platform_hooks, "get_stacktrace_handler", return_value=handler
|
|
):
|
|
processor = stacktrace.LogLineProcessor(CONFIG, platform)
|
|
for line in lines:
|
|
processor.process_line(line)
|
|
return processor
|
|
|
|
|
|
def _fed(handler) -> list[str]:
|
|
return [call.args[1] for call in handler.call_args_list]
|
|
|
|
|
|
def _warnings(caplog) -> list[str]:
|
|
return [r.message for r in caplog.records if r.levelname == "WARNING"]
|
|
|
|
|
|
def test_decoder_contains_failures_and_short_circuits() -> None:
|
|
"""One decode failure is contained and never retried.
|
|
|
|
aioesphomeapi isolates exceptions raised by log handlers, so an
|
|
escaping one logs a full traceback for every line it fires on; and
|
|
_decode_pc shells out to the toolchain, so retrying it per backtrace
|
|
line would stall streaming.
|
|
"""
|
|
handler = Mock(side_effect=EsphomeError("no idedata"))
|
|
processor = _run(
|
|
handler, lines=("PC: 0x4010496e", "BT0: 0x4010496e", "BT1: 0x401049aa")
|
|
)
|
|
|
|
assert handler.call_count == 1
|
|
assert processor.backtrace_state is False
|
|
|
|
|
|
def test_resolution_failure_is_contained(caplog) -> None:
|
|
"""A platform package broken in an unanticipated way must not kill
|
|
the session; decoding degrades with a warning like any other failure.
|
|
"""
|
|
with patch.object(
|
|
stacktrace.platform_hooks,
|
|
"get_stacktrace_handler",
|
|
side_effect=RuntimeError("boom"),
|
|
):
|
|
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_ESP32)
|
|
processor.process_line("PC: 0x4010496e")
|
|
|
|
assert processor.backtrace_state is False
|
|
assert any("could not be loaded" in m for m in _warnings(caplog))
|
|
|
|
|
|
def test_decoder_swallows_os_error_with_remediation_hint(caplog) -> None:
|
|
"""Decoding failures that aren't EsphomeError must be contained too.
|
|
|
|
A missing build directory surfaces as an OSError; that is the
|
|
user's environment, not a decoder bug, so it disables decoding
|
|
like an EsphomeError does and keeps the recompile hint.
|
|
"""
|
|
handler = Mock(
|
|
side_effect=FileNotFoundError(2, "No such file or directory", "/build")
|
|
)
|
|
processor = _run(handler, lines=("PC: 0x4010496e", "BT0: 0x4010496e"))
|
|
|
|
assert handler.call_count == 1
|
|
assert processor.backtrace_state is False
|
|
warnings = _warnings(caplog)
|
|
assert any("esphome compile" in m for m in warnings)
|
|
assert not any("this is a bug" in m for m in warnings)
|
|
|
|
|
|
def test_decoder_warning_uses_fallback_for_empty_error(caplog) -> None:
|
|
"""A message-less EsphomeError must show a useful explanation.
|
|
|
|
Defensive: the in-tree idedata raise sites all carry a message now,
|
|
but a bare EsphomeError from elsewhere must not render as parens.
|
|
"""
|
|
_run(Mock(side_effect=EsphomeError()))
|
|
|
|
warnings = _warnings(caplog)
|
|
assert any("build artifacts not found locally" in m for m in warnings)
|
|
assert not any("()" in m for m in warnings)
|
|
|
|
|
|
def test_decoder_bug_with_empty_message_names_the_type(caplog) -> None:
|
|
"""A zero-message decoder bug must not masquerade as missing artifacts.
|
|
|
|
The recompile hint is only right for EsphomeError from _run_idedata;
|
|
anything else is ESPHome's own bug and says so instead of sending
|
|
the user down a dead-end remediation path.
|
|
"""
|
|
_run(Mock(side_effect=IndexError()))
|
|
|
|
warnings = _warnings(caplog)
|
|
assert any("IndexError" in m and "this is a bug" in m for m in warnings)
|
|
assert not any("esphome compile" in m for m in warnings)
|
|
|
|
|
|
def test_decoder_bug_warning_keeps_the_type_with_a_message(caplog) -> None:
|
|
"""The type must survive a non-empty message; a bare KeyError message
|
|
like 'prog_path' reads as a raised string in a bug report paste.
|
|
"""
|
|
_run(Mock(side_effect=KeyError("prog_path")))
|
|
|
|
warnings = _warnings(caplog)
|
|
assert any("KeyError: 'prog_path'" in m for m in warnings)
|
|
|
|
|
|
def test_state_threads_between_lines() -> None:
|
|
"""backtrace_state carries from one decoded line to the next."""
|
|
handler = Mock(side_effect=[True, True])
|
|
processor = _run(
|
|
handler,
|
|
platform=PLATFORM_ESP8266,
|
|
lines=(">>>stack>>>", "3ffffe10: 40201234 3ffe8410 00000000 40201000"),
|
|
)
|
|
|
|
assert _fed(handler) == [
|
|
">>>stack>>>",
|
|
"3ffffe10: 40201234 3ffe8410 00000000 40201000",
|
|
]
|
|
assert handler.call_args_list[0].args[2] is False
|
|
assert handler.call_args_list[1].args[2] is True
|
|
assert processor.backtrace_state is True
|
|
|
|
|
|
def test_no_analyzer_disables_decoding(caplog) -> None:
|
|
"""Platforms without an analyzer report at session start and stay quiet."""
|
|
caplog.set_level("INFO", logger="esphome.platform_hooks")
|
|
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_BK72XX)
|
|
processor.process_line("PC: 0x40104960")
|
|
|
|
assert "Stacktrace analysis is unavailable" in caplog.text
|
|
assert processor.backtrace_state is False
|