[vscode] Report the origin of an unexpected exception during validation (#18494)

This commit is contained in:
J. Nick Koston
2026-08-18 19:37:10 -05:00
committed by GitHub
parent 4f866c563b
commit 8b637b339b
2 changed files with 84 additions and 2 deletions
+18 -2
View File
@@ -3,12 +3,14 @@ from __future__ import annotations
from io import StringIO
import json
from pathlib import Path
import sys
import traceback
from typing import Any
from esphome.config import Config, _format_vol_invalid, validate_config
import esphome.config_validation as cv
from esphome.const import __version__ as ESPHOME_VERSION
from esphome.core import CORE, DocumentRange
from esphome.core import CORE, DocumentRange, EsphomeError
from esphome.yaml_util import parse_yaml
@@ -97,6 +99,16 @@ def _ace_loader(fname: Path) -> dict[str, Any]:
return parse_yaml(fname, raw_yaml_stream)
def _format_unexpected_error(err: Exception) -> str:
"""Describe a crash inside validation with the frame it came from."""
message = f"Unexpected error while validating: {type(err).__name__}: {err}"
frames = traceback.extract_tb(err.__traceback__)
if not frames:
return message
frame = frames[-1]
return f"{message} ({frame.filename}:{frame.lineno} in {frame.name})"
def _print_version():
"""Print ESPHome version."""
print(
@@ -134,8 +146,12 @@ def read_config(args):
try:
config = loader(file_name)
res = validate_config(config, command_line_substitutions)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
except (EsphomeError, cv.Invalid) as err:
vs.add_yaml_error(str(err))
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
# stdout carries the JSON protocol; the full chain goes to stderr.
traceback.print_exc(file=sys.stderr)
vs.add_yaml_error(_format_unexpected_error(err))
else:
for err in res.errors:
try:
+66
View File
@@ -3,6 +3,8 @@ from pathlib import Path
from unittest.mock import Mock, patch
from esphome import vscode
import esphome.config_validation as cv
from esphome.core import EsphomeError
def _run_repl_test(input_data):
@@ -126,3 +128,67 @@ packages:
assert range["start_col"] == 2
assert range["end_line"] == 1
assert range["end_col"] == 7
def _explode(*_args: object, **_kwargs: object) -> None:
raise AttributeError("'NoneType' object has no attribute 'get'")
def test_unexpected_error_reports_origin() -> None:
source_path = str(Path("dir_path", "x.yaml"))
with patch("esphome.vscode.validate_config", _explode):
output_lines = _run_repl_test(
[
_validate(source_path),
_file_response("""esphome:
name: test1
"""),
]
)
result = json.loads(output_lines[-1])
assert result["validation_errors"] == []
(error,) = result["yaml_errors"]
assert error["message"].startswith(
"Unexpected error while validating: AttributeError: "
"'NoneType' object has no attribute 'get' ("
)
assert "test_vscode.py" in error["message"]
assert error["message"].endswith(" in _explode)")
def test_esphome_error_stays_plain() -> None:
source_path = str(Path("dir_path", "x.yaml"))
with patch("esphome.vscode.validate_config", side_effect=EsphomeError("boom")):
output_lines = _run_repl_test(
[
_validate(source_path),
_file_response("""esphome:
name: test1
"""),
]
)
result = json.loads(output_lines[-1])
assert result["yaml_errors"] == [{"message": "boom"}]
def test_invalid_stays_plain() -> None:
source_path = str(Path("dir_path", "x.yaml"))
with patch("esphome.vscode.validate_config", side_effect=cv.Invalid("bad value")):
output_lines = _run_repl_test(
[
_validate(source_path),
_file_response("""esphome:
name: test1
"""),
]
)
result = json.loads(output_lines[-1])
assert result["yaml_errors"] == [{"message": "bad value"}]
def test_format_unexpected_error_without_traceback() -> None:
message = vscode._format_unexpected_error(ValueError("boom"))
assert message == "Unexpected error while validating: ValueError: boom"