mirror of
https://github.com/esphome/esphome.git
synced 2025-10-29 22:24:26 +00:00
125
tests/unit_tests/test_substitutions.py
Normal file
125
tests/unit_tests/test_substitutions.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
|
||||
from esphome import yaml_util
|
||||
from esphome.components import substitutions
|
||||
from esphome.const import CONF_PACKAGES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Set to True for dev mode behavior
|
||||
# This will generate the expected version of the test files.
|
||||
|
||||
DEV_MODE = False
|
||||
|
||||
|
||||
def sort_dicts(obj):
|
||||
"""Recursively sort dictionaries for order-insensitive comparison."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: sort_dicts(obj[k]) for k in sorted(obj)}
|
||||
elif isinstance(obj, list):
|
||||
# Lists are not sorted; we preserve order
|
||||
return [sort_dicts(i) for i in obj]
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
def dict_diff(a, b, path=""):
|
||||
"""Recursively find differences between two dict/list structures."""
|
||||
diffs = []
|
||||
if isinstance(a, dict) and isinstance(b, dict):
|
||||
a_keys = set(a)
|
||||
b_keys = set(b)
|
||||
for key in a_keys - b_keys:
|
||||
diffs.append(f"{path}/{key} only in actual")
|
||||
for key in b_keys - a_keys:
|
||||
diffs.append(f"{path}/{key} only in expected")
|
||||
for key in a_keys & b_keys:
|
||||
diffs.extend(dict_diff(a[key], b[key], f"{path}/{key}"))
|
||||
elif isinstance(a, list) and isinstance(b, list):
|
||||
min_len = min(len(a), len(b))
|
||||
for i in range(min_len):
|
||||
diffs.extend(dict_diff(a[i], b[i], f"{path}[{i}]"))
|
||||
if len(a) > len(b):
|
||||
for i in range(min_len, len(a)):
|
||||
diffs.append(f"{path}[{i}] only in actual: {a[i]!r}")
|
||||
elif len(b) > len(a):
|
||||
for i in range(min_len, len(b)):
|
||||
diffs.append(f"{path}[{i}] only in expected: {b[i]!r}")
|
||||
else:
|
||||
if a != b:
|
||||
diffs.append(f"\t{path}: actual={a!r} expected={b!r}")
|
||||
return diffs
|
||||
|
||||
|
||||
def write_yaml(path, data):
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(yaml_util.dump(data))
|
||||
|
||||
|
||||
def test_substitutions_fixtures(fixture_path):
|
||||
base_dir = fixture_path / "substitutions"
|
||||
sources = sorted(glob.glob(str(base_dir / "*.input.yaml")))
|
||||
assert sources, f"No input YAML files found in {base_dir}"
|
||||
|
||||
failures = []
|
||||
for source_path in sources:
|
||||
try:
|
||||
expected_path = source_path.replace(".input.yaml", ".approved.yaml")
|
||||
test_case = os.path.splitext(os.path.basename(source_path))[0].replace(
|
||||
".input", ""
|
||||
)
|
||||
|
||||
# Load using ESPHome's YAML loader
|
||||
config = yaml_util.load_yaml(source_path)
|
||||
|
||||
if CONF_PACKAGES in config:
|
||||
from esphome.components.packages import do_packages_pass
|
||||
|
||||
config = do_packages_pass(config)
|
||||
|
||||
substitutions.do_substitution_pass(config, None)
|
||||
|
||||
# Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE
|
||||
if os.path.isfile(expected_path):
|
||||
expected = yaml_util.load_yaml(expected_path)
|
||||
elif DEV_MODE:
|
||||
expected = {}
|
||||
else:
|
||||
assert os.path.isfile(expected_path), (
|
||||
f"Expected file missing: {expected_path}"
|
||||
)
|
||||
|
||||
# Sort dicts only (not lists) for comparison
|
||||
got_sorted = sort_dicts(config)
|
||||
expected_sorted = sort_dicts(expected)
|
||||
|
||||
if got_sorted != expected_sorted:
|
||||
diff = "\n".join(dict_diff(got_sorted, expected_sorted))
|
||||
msg = (
|
||||
f"Substitution result mismatch for {os.path.basename(source_path)}\n"
|
||||
f"Diff:\n{diff}\n\n"
|
||||
f"Got: {got_sorted}\n"
|
||||
f"Expected: {expected_sorted}"
|
||||
)
|
||||
# Write out the received file when test fails
|
||||
if DEV_MODE:
|
||||
received_path = os.path.join(
|
||||
os.path.dirname(source_path), f"{test_case}.received.yaml"
|
||||
)
|
||||
write_yaml(received_path, config)
|
||||
print(msg)
|
||||
failures.append(msg)
|
||||
else:
|
||||
raise AssertionError(msg)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error in test file %s", source_path)
|
||||
raise err
|
||||
|
||||
if DEV_MODE and failures:
|
||||
print(f"\n{len(failures)} substitution test case(s) failed.")
|
||||
|
||||
if DEV_MODE:
|
||||
_LOGGER.error("Tests passed, but Dev mode is enabled.")
|
||||
assert not DEV_MODE # make sure DEV_MODE is disabled after you are finished.
|
||||
Reference in New Issue
Block a user