mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[api] Don't spam tracebacks when decoding a crash without a local build (#17597)
This commit is contained in:
@@ -12,11 +12,9 @@ from esphome.core import EsphomeError
|
||||
def test_decoder_swallows_esphome_error() -> None:
|
||||
"""A failing stack-trace decode must not propagate.
|
||||
|
||||
on_log runs inside an asyncio protocol callback; if EsphomeError
|
||||
escapes, the loop reports "Fatal error: protocol.data_received()
|
||||
call failed.", tears the connection down, and ReconnectLogic loops
|
||||
forever as the device replays the same crash trace on every
|
||||
reconnect.
|
||||
aioesphomeapi isolates exceptions raised by log handlers, so an
|
||||
escaping one logs a full traceback for every line it fires on rather
|
||||
than being reported once as an unavailable decoder.
|
||||
"""
|
||||
config = {"esphome": {"name": "test"}}
|
||||
|
||||
@@ -43,6 +41,32 @@ def test_decoder_swallows_platform_handler_error() -> None:
|
||||
assert processor.backtrace_state is False
|
||||
|
||||
|
||||
def test_decoder_swallows_non_esphome_error() -> None:
|
||||
"""Decoding failures that aren't EsphomeError must be contained too.
|
||||
|
||||
A missing build directory surfaces as FileNotFoundError from the toolchain
|
||||
subprocess. aioesphomeapi isolates it, so the session survives, but it logs
|
||||
a traceback for every PC/BT line and decoding is never disabled, which
|
||||
buries the crash dump the user is trying to read.
|
||||
"""
|
||||
config = {"esphome": {"name": "test"}}
|
||||
|
||||
with patch.object(
|
||||
esp32,
|
||||
"process_stacktrace",
|
||||
side_effect=FileNotFoundError(
|
||||
2, "No such file or directory", "/build/ol/build"
|
||||
),
|
||||
) as mock_process:
|
||||
processor = api_client._LogLineProcessor(config, esp32.process_stacktrace)
|
||||
processor.process_line("PC: 0x4010496e")
|
||||
processor.process_line("BT0: 0x4010496e")
|
||||
|
||||
# Disabled after the first failure rather than retried per backtrace line.
|
||||
assert mock_process.call_count == 1
|
||||
assert processor.backtrace_state is False
|
||||
|
||||
|
||||
def test_decoder_warning_uses_fallback_for_empty_error(caplog) -> None:
|
||||
"""_run_idedata raises EsphomeError with no message; the warning
|
||||
must show a useful explanation rather than empty parens.
|
||||
@@ -61,7 +85,7 @@ def test_decoder_warning_uses_fallback_for_empty_error(caplog) -> None:
|
||||
def test_decoder_short_circuits_after_failure() -> None:
|
||||
"""After one failure, subsequent lines must not retry the decoder.
|
||||
|
||||
_decode_pc shells out to PlatformIO; a crash dump can contain many
|
||||
_decode_pc shells out to the toolchain; a crash dump can contain many
|
||||
PC/BT lines and retrying the failing subprocess for each one would
|
||||
stall log streaming.
|
||||
"""
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.const import CONF_FRAMEWORK, CONF_SOURCE
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
|
||||
@@ -237,6 +238,77 @@ def test_get_idf_env_sets_git_ceiling_directories(setup_core: Path) -> None:
|
||||
assert str(CORE.config_dir) in env["GIT_CEILING_DIRECTORIES"].split(os.pathsep)
|
||||
|
||||
|
||||
def test_get_cmake_output_without_build_dir(setup_core: Path) -> None:
|
||||
"""A build dir that was never created raises EsphomeError.
|
||||
|
||||
Without this, subprocess.run(cwd=build_dir) raises FileNotFoundError, which
|
||||
the log stack-trace decoder doesn't recognise as a decode failure.
|
||||
"""
|
||||
_setup_build(setup_core)
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
assert not build_dir.exists()
|
||||
|
||||
with pytest.raises(EsphomeError, match="No ESP-IDF build found"):
|
||||
toolchain._get_cmake_output(build_dir)
|
||||
|
||||
|
||||
def test_get_cmake_output_without_cmake_cache(setup_core: Path) -> None:
|
||||
"""A build dir that exists but was never configured raises EsphomeError."""
|
||||
_setup_build(setup_core)
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
build_dir.mkdir(parents=True)
|
||||
|
||||
with pytest.raises(EsphomeError, match="No ESP-IDF build found"):
|
||||
toolchain._get_cmake_output(build_dir)
|
||||
|
||||
|
||||
def test_get_cmake_output_with_configured_build(setup_core: Path) -> None:
|
||||
"""A configured build still runs cmake and caches the output.
|
||||
|
||||
The missing-build guard must not get in the way of a real build.
|
||||
"""
|
||||
_setup_build(setup_core)
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
build_dir.mkdir(parents=True)
|
||||
(build_dir / "CMakeCache.txt").write_text("")
|
||||
|
||||
completed = subprocess.CompletedProcess(
|
||||
args=[], returncode=0, stdout="CMAKE_ADDR2LINE:FILEPATH=/tool/addr2line\n"
|
||||
)
|
||||
with (
|
||||
patch.object(toolchain, "_get_idf_env", return_value={}),
|
||||
patch.object(toolchain.subprocess, "run", return_value=completed) as mock_run,
|
||||
):
|
||||
assert toolchain._get_cmake_output(build_dir) == completed.stdout
|
||||
# Second call is served from the cache rather than re-running cmake.
|
||||
assert toolchain._get_cmake_output(build_dir) == completed.stdout
|
||||
|
||||
mock_run.assert_called_once()
|
||||
assert toolchain._get_cmake_tool_path("CMAKE_ADDR2LINE") == Path("/tool/addr2line")
|
||||
|
||||
|
||||
def test_get_cmake_output_missing_build_does_not_resolve_idf_env(
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
"""The build check runs before the env is resolved.
|
||||
|
||||
Resolving the env calls check_esp_idf_install(), which can download and
|
||||
extract the whole framework. A doomed call must never start that.
|
||||
"""
|
||||
_setup_build(setup_core)
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
|
||||
with (
|
||||
patch.object(toolchain, "_get_idf_env") as mock_env,
|
||||
patch.object(toolchain.subprocess, "run") as mock_run,
|
||||
pytest.raises(EsphomeError),
|
||||
):
|
||||
toolchain._get_cmake_output(build_dir)
|
||||
|
||||
mock_env.assert_not_called()
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
def test_get_core_framework_version_from_core_data():
|
||||
"""The version is read from CORE.data when validation populated it."""
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION
|
||||
|
||||
Reference in New Issue
Block a user