what_to_wear/tests/test_layering.py
Nora eff3fa28a9 feat(1.1): Skeleton, pure-core Datenmodell, Options-Schema & Layering-Guard
Story 1.1 (TDD, Suite gruen 19/19):
- logic/model.py: frozen Dataclasses (NormField, RawForecast, NormForecast,
  Requirement, Item, ChosenItem, Gap, Outfit, Recommendation) + Enums
  RequirementKey/Category/Priority (AD-14/21/22).
- const.py: normatives flaches Options-Schema + default_options()-Deepcopy,
  TO_REDACT, Timeouts, LLM-URLs (AD-5/6/23).
- manifest.json/hacs.json (min HA 2025.3), Layering-Guard per AST (AD-1).
- luna-pro-Story-Review: 4/6 Findings uebernommen, 2 verworfen (Evidenz im Ledger).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:20:42 +00:00

62 lines
2.5 KiB
Python

"""Story 1.1 — architectural layering guard (AD-1).
The pure core under ``logic/`` must never import Home Assistant (nor ``dt_util``).
This is a static AST scan of import statements, so it holds even though the test
environment has Home Assistant installed, and it does not false-positive on the
strings "import homeassistant" appearing inside docstrings or data.
"""
from __future__ import annotations
import ast
import pathlib
LOGIC_DIR = (
pathlib.Path(__file__).parent.parent
/ "custom_components"
/ "what_to_wear"
/ "logic"
)
def _forbidden_imports(source: str) -> list[str]:
"""Return the offending imported module names in ``source`` (AST-based)."""
offenders: list[str] = []
tree = ast.parse(source)
for node in ast.walk(tree):
names: list[str] = []
if isinstance(node, ast.Import):
names = [alias.name for alias in node.names]
elif isinstance(node, ast.ImportFrom):
# module is None for "from . import x"; treat as empty
names = [node.module or ""]
names += [alias.name for alias in node.names]
for name in names:
root = name.split(".")[0]
if root == "homeassistant" or name == "dt_util" or "dt_util" in name:
offenders.append(name)
return offenders
def test_logic_has_no_homeassistant_imports() -> None:
py_files = list(LOGIC_DIR.rglob("*.py"))
assert py_files, "logic/ must contain Python modules"
offenders: dict[str, list[str]] = {}
for path in py_files:
found = _forbidden_imports(path.read_text(encoding="utf-8"))
if found:
offenders[str(path.relative_to(LOGIC_DIR.parent))] = found
assert not offenders, f"logic/ must not import homeassistant/dt_util: {offenders}"
def test_guard_catches_violations_and_ignores_strings() -> None:
# Real imports are caught.
assert _forbidden_imports("import homeassistant\n")
assert _forbidden_imports("from homeassistant.core import HomeAssistant\n")
assert _forbidden_imports("from homeassistant.util import dt as dt_util\n")
assert _forbidden_imports("import homeassistant.util.dt as dt_util\n")
# Stdlib imports are fine.
assert not _forbidden_imports("import datetime\n")
assert not _forbidden_imports("from zoneinfo import ZoneInfo\n")
# A docstring merely *mentioning* the words is NOT a violation (no false positive).
assert not _forbidden_imports('"""We must never import homeassistant here."""\n')
assert not _forbidden_imports('X = "from homeassistant.core import HomeAssistant"\n')