Files
esphome/tests/unit_tests/test_vscode.py
2026-10-11 13:33:18 +11:00

267 lines
7.4 KiB
Python

import json
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):
"""Reusable test function for different input scenarios."""
input_data.append(_exit())
with (
patch("builtins.input", side_effect=input_data),
patch("sys.stdout") as mock_stdout,
):
args = Mock([])
args.ace = False
args.substitution = None
vscode.read_config(args)
# Capture printed output
full_output = "".join(
call[0][0] for call in mock_stdout.write.call_args_list
).strip()
splitted_output = full_output.split("\n")
return splitted_output[1:] # remove first entry with version info
def _validate(file_path: str):
return json.dumps({"type": "validate", "file": file_path})
def _file_response(data: str):
return json.dumps({"type": "file_response", "content": data})
def _read_file(file_path: str):
return json.dumps({"type": "read_file", "path": file_path})
def _exit():
return json.dumps({"type": "exit"})
RESULT_NO_ERROR = '{"type": "result", "yaml_errors": [], "validation_errors": []}'
def test_multi_file():
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
esp8266:
board: !secret my_secret_board
"""),
# read_file secrets.yaml
_file_response("""my_secret_board: d1_mini"""),
]
)
expected_lines = [
_read_file(source_path),
_read_file(str(Path("dir_path", "secrets.yaml"))),
RESULT_NO_ERROR,
]
assert output_lines == expected_lines
def test_shows_correct_range_error():
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
esp8266:
broad: !secret my_secret_board # typo here
"""),
# read_file secrets.yaml
_file_response("""my_secret_board: d1_mini"""),
]
)
assert len(output_lines) == 3
error = json.loads(output_lines[2])
validation_error = error["validation_errors"][0]
assert validation_error["message"].startswith("[broad] is an invalid option for")
range = validation_error["range"]
assert range["document"] == source_path
assert range["start_line"] == 3
assert range["start_col"] == 2
assert range["end_line"] == 3
assert range["end_col"] == 7
def test_shows_correct_loaded_file_error():
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
packages:
board: !include .pkg.esp8266.yaml
"""),
# read_file .pkg.esp8266.yaml
_file_response("""esp8266:
broad: esp1f # typo here
"""),
]
)
assert len(output_lines) == 3
error = json.loads(output_lines[2])
validation_error = error["validation_errors"][0]
assert validation_error["message"].startswith("[broad] is an invalid option for")
range = validation_error["range"]
assert range["document"] == str(Path("dir_path", ".pkg.esp8266.yaml"))
assert range["start_line"] == 1
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"
def test_reports_error_with_exception_message():
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
host:
sensor:
- platform: template
name: foo
update_interval: ms # no number, so the validator raises Invalid(ValueError)
"""),
]
)
error = json.loads(output_lines[-1])
validation_error = error["validation_errors"][0]
assert validation_error["message"] == "could not convert string to float: ''."
assert validation_error["range"]["start_line"] == 6
def test_unknown_action_range_is_the_key():
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
host:
button:
- platform: template
on_press:
- loggr.log: "msg"
"""),
]
)
error = json.loads(output_lines[-1])
validation_error = error["validation_errors"][0]
assert (
validation_error["message"]
== "Unable to find action with the name 'loggr.log'."
)
assert validation_error["range"]["start_line"] == 6
assert validation_error["range"]["start_col"] == 8
def test_error_kept_when_range_lookup_fails():
source_path = str(Path("dir_path", "x.yaml"))
with patch.object(vscode, "get_invalid_range", side_effect=RuntimeError("boom")):
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
host:
logger:
levl: DEBUG
"""),
]
)
error = json.loads(output_lines[-1])
(validation_error,) = error["validation_errors"]
assert validation_error["range"] is None
assert "extra keys not allowed" in validation_error["message"]