You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Home-AssistantConfig/codex_skills/homeassistant-yaml-dry-veri.../tests/test_verify_ha_yaml_dry.py

303 lines
8.3 KiB

import contextlib
import importlib.util
import io
import re
import sys
import tempfile
import unittest
from pathlib import Path
MODULE_PATH = (
Path(__file__).resolve().parents[1] / "scripts" / "verify_ha_yaml_dry.py"
)
MODULE_NAME = "verify_ha_yaml_dry"
SPEC = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError(f"Unable to load module spec from {MODULE_PATH}")
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[MODULE_NAME] = MODULE
SPEC.loader.exec_module(MODULE)
class VerifyHaYamlDryTests(unittest.TestCase):
def _run_verifier(
self,
files: dict[str, str],
*extra_args: str,
scan_subpath: str = ".",
) -> tuple[int, str, str]:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
for rel_path, content in files.items():
file_path = root / rel_path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding="utf-8")
scan_path = root / scan_subpath
stdout = io.StringIO()
stderr = io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
rc = MODULE.main([str(scan_path), *extra_args])
return rc, stdout.getvalue(), stderr.getvalue()
@staticmethod
def _full_block_group_order(output: str) -> list[str]:
marker = "\nFULL_BLOCK findings:\n"
if marker not in output:
return []
section = output.split(marker, 1)[1]
for stop in ("\nENTRY findings:\n", "\nINTRA findings:\n", "\nCENTRAL_SCRIPT findings:\n"):
if stop in section:
section = section.split(stop, 1)[0]
break
groups: list[str] = []
for line in section.splitlines():
text = line.strip()
match = re.match(r"^\d+\.\s+([a-z]+\.[a-z_]+)\s+repeated\s+\d+\s+times$", text)
if match:
groups.append(match.group(1))
return groups
def test_full_block_detection_with_fixture(self) -> None:
files = {
"automations.yaml": """
- alias: A1
trigger:
- platform: state
entity_id: binary_sensor.one
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
- alias: A2
trigger:
- platform: state
entity_id: binary_sensor.two
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
"""
}
rc, out, err = self._run_verifier(files)
self.assertEqual(rc, 0, out + err)
self.assertIn("Duplicate full-block groups: 1", out)
self.assertIn("FULL_BLOCK findings:", out)
self.assertIn("automation.action repeated 2 times", out)
def test_entry_detection_with_fixture(self) -> None:
files = {
"automations.yaml": """
- alias: A1
trigger:
- platform: state
entity_id: binary_sensor.one
to: "on"
action:
- service: script.shared_handler
- alias: A2
trigger:
- platform: state
entity_id: binary_sensor.two
to: "on"
action:
- service: script.shared_handler
- delay: "00:00:01"
"""
}
rc, out, err = self._run_verifier(files)
self.assertEqual(rc, 0, out + err)
self.assertIn("Duplicate entry groups: 1", out)
self.assertIn("ENTRY findings:", out)
self.assertIn("automation.action entry repeated 2 times", out)
def test_intra_detection_with_fixture(self) -> None:
files = {
"automations.yaml": """
- alias: Repeat Inside Block
trigger:
- platform: state
entity_id: binary_sensor.one
to: "on"
action:
- service: light.turn_off
target:
entity_id: light.den
- service: light.turn_off
target:
entity_id: light.den
"""
}
rc, out, err = self._run_verifier(files)
self.assertEqual(rc, 0, out + err)
self.assertIn("INTRA findings:", out)
self.assertIn("INTRA automation.action: Repeat Inside Block has 2 duplicated entries", out)
def test_central_script_detection_with_package_definition_and_multi_caller(self) -> None:
files = {
"config/packages/shared_scripts.yaml": """
script:
my_shared_script:
alias: Shared Script
sequence:
- service: logbook.log
data:
name: Shared
message: Hello
""",
"config/automations/caller_one.yaml": """
automation:
- alias: Caller One
trigger:
- platform: state
entity_id: binary_sensor.one
to: "on"
action:
- service: script.my_shared_script
""",
"config/automations/caller_two.yaml": """
automation:
- alias: Caller Two
trigger:
- platform: state
entity_id: binary_sensor.two
to: "on"
action:
- service: script.turn_on
target:
entity_id: script.my_shared_script
""",
}
rc, out, err = self._run_verifier(files)
self.assertEqual(rc, 0, out + err)
self.assertIn("Central-script findings: 1", out)
self.assertIn("script.my_shared_script is package-defined and called from 2 files", out)
self.assertIn("suggestion: Move definition to config/script/my_shared_script.yaml", out)
def test_subsumed_entry_groups_are_collapsed(self) -> None:
files = {
"automations.yaml": """
- alias: A1
trigger:
- platform: state
entity_id: binary_sensor.one
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
- alias: A2
trigger:
- platform: state
entity_id: binary_sensor.two
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
"""
}
rc, out, err = self._run_verifier(files)
self.assertEqual(rc, 0, out + err)
self.assertIn("Duplicate full-block groups: 1", out)
self.assertIn("Duplicate entry groups: 0", out)
def test_full_block_findings_order_is_deterministic(self) -> None:
files = {
"automations.yaml": """
- alias: A1
trigger:
- platform: state
entity_id: binary_sensor.same
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
- alias: A2
trigger:
- platform: state
entity_id: binary_sensor.same
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
""",
"scripts.yaml": """
script:
script_one:
sequence:
- service: light.turn_off
target:
entity_id: light.kitchen
script_two:
sequence:
- service: light.turn_off
target:
entity_id: light.kitchen
""",
}
rc, out, err = self._run_verifier(files)
self.assertEqual(rc, 0, out + err)
order = self._full_block_group_order(out)
self.assertGreaterEqual(len(order), 3, out)
self.assertEqual(order[:3], ["automation.action", "automation.trigger", "script.sequence"])
def test_exit_codes_for_strict_and_non_strict_modes(self) -> None:
files = {
"automations.yaml": """
- alias: A1
trigger:
- platform: state
entity_id: binary_sensor.one
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
- alias: A2
trigger:
- platform: state
entity_id: binary_sensor.two
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
"""
}
rc_non_strict, _, _ = self._run_verifier(files)
rc_strict, _, _ = self._run_verifier(files, "--strict")
self.assertEqual(rc_non_strict, 0)
self.assertEqual(rc_strict, 1)
def test_parse_error_path_returns_exit_code_two(self) -> None:
files = {
"good.yaml": """
automation:
- alias: Good
trigger:
- platform: state
entity_id: binary_sensor.one
to: "on"
action:
- service: light.turn_on
target:
entity_id: light.kitchen
""",
"bad.yaml": "automation: [\n",
}
rc, out, err = self._run_verifier(files)
self.assertEqual(rc, 2, out + err)
self.assertIn("Parse errors:", out)
self.assertIn("bad.yaml", out)
if __name__ == "__main__":
unittest.main()

Powered by TurnKey Linux.