/
/
/
1"""Tests for the ConfigEntry definition check."""
2
3import ast
4
5from scripts.check_config_entries import (
6 _entry_key,
7 _is_required_without_value,
8 _options_entry_calls,
9 main,
10)
11
12
13def _first_entry(source: str) -> ast.Call:
14 """Return the first ``ConfigEntry(...)`` call node in a source snippet."""
15 return next(
16 node
17 for node in ast.walk(ast.parse(source))
18 if isinstance(node, ast.Call)
19 and isinstance(node.func, ast.Name)
20 and node.func.id == "ConfigEntry"
21 )
22
23
24def _entry(*args: str) -> ast.Call:
25 """Return the ConfigEntry call node for a snippet built from the given keyword arguments."""
26 return _first_entry(f"ConfigEntry({', '.join(args)})")
27
28
29def test_required_without_default_is_flagged() -> None:
30 """An explicitly required entry without a default can never resolve to a value."""
31 assert _is_required_without_value(_entry("key=CONF_TOKEN", "required=True")) is True
32
33
34def test_implicitly_required_without_default_is_flagged() -> None:
35 """``required`` defaults to True in the model, so omitting it is still required."""
36 assert _is_required_without_value(_entry("key=CONF_TOKEN")) is True
37
38
39def test_default_value_satisfies_required() -> None:
40 """A required entry with a default resolves to that default."""
41 assert (
42 _is_required_without_value(_entry("key=CONF_LOCALE", "required=True", 'default_value="us"'))
43 is False
44 )
45 assert _is_required_without_value(_entry("key=CONF_PLAYERS", "default_value=[]")) is False
46
47
48def test_preset_value_satisfies_required() -> None:
49 """An entry carrying a preset value resolves to that value."""
50 assert _is_required_without_value(_entry("key=CONF_URL", 'value="http://localhost"')) is False
51
52
53def test_explicit_none_default_is_flagged() -> None:
54 """A literal ``None`` default leaves a required entry just as unresolvable."""
55 assert _is_required_without_value(_entry("key=CONF_TOKEN", "default_value=None")) is True
56
57
58def test_optional_entry_is_not_flagged() -> None:
59 """An entry that is not required may resolve to nothing."""
60 assert _is_required_without_value(_entry("key=CONF_PORT", "required=False")) is False
61
62
63def test_computed_flags_are_not_flagged() -> None:
64 """A computed ``required`` or ``default_value`` cannot be judged statically."""
65 assert _is_required_without_value(_entry("key=CONF_SECRET", "required=not stored")) is False
66 assert _is_required_without_value(_entry("key=CONF_TTS", "default_value=entities[0]")) is False
67
68
69def test_ui_only_types_are_not_flagged() -> None:
70 """UI-only entries hold no value: the model clears ``required`` for them."""
71 assert (
72 _is_required_without_value(_entry("key=CONF_ACTION", "type=ConfigEntryType.ACTION"))
73 is False
74 )
75 assert (
76 _is_required_without_value(_entry("key=CONF_NOTE", "type=ConfigEntryType.LABEL")) is False
77 )
78 # an explicit required=True on a UI-only type is honoured by the check, not silently ignored
79 assert (
80 _is_required_without_value(
81 _entry("key=CONF_ACTION", "type=ConfigEntryType.ACTION", "required=True")
82 )
83 is True
84 )
85
86
87def test_only_get_config_entries_calls_are_collected() -> None:
88 """Setup-flow form entries are required by design, so only options entries are collected."""
89 source = (
90 "_FORM = (ConfigEntry(key='token', type=ConfigEntryType.SECURE_STRING),)\n"
91 "class P:\n"
92 " async def get_config_entries(self):\n"
93 " return (ConfigEntry(key='quality', default_value='high'),)\n"
94 " async def run_setup(self, session):\n"
95 " return ConfigEntry(key='password')\n"
96 )
97 tree = ast.parse(source)
98 collected = _options_entry_calls(tree)
99 assert len(collected) == 1
100 keys = [
101 _entry_key(node)
102 for node in ast.walk(tree)
103 if isinstance(node, ast.Call) and id(node) in collected
104 ]
105 assert keys == ["'quality'"]
106
107
108def test_entry_key_rendering() -> None:
109 """A key is rendered from a literal, a constant reference or an attribute access."""
110 assert _entry_key(_entry("key='locale'")) == "'locale'"
111 assert _entry_key(_entry("key=CONF_LOCALE")) == "CONF_LOCALE"
112 assert _entry_key(_entry("key=constants.CONF_LOCALE")) == "CONF_LOCALE"
113 assert _entry_key(_entry("type=ConfigEntryType.STRING")) == "<unknown>"
114
115
116def test_real_tree_is_clean() -> None:
117 """The shipped tree defines no invalid config entries."""
118 assert main() == 0
119