204 lines
6.4 KiB
Python
204 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Regression test runner for hand-drawn-diagram skill.
|
||
|
||
Usage:
|
||
cd skills/hand-drawn-diagram
|
||
python3 tests/run_tests.py
|
||
|
||
Zero dependencies — uses only Python stdlib.
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
import glob
|
||
import tempfile
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# ANSI colors
|
||
# ---------------------------------------------------------------------------
|
||
GREEN = "\033[92m"
|
||
RED = "\033[91m"
|
||
YELLOW= "\033[93m"
|
||
BLUE = "\033[94m"
|
||
RESET = "\033[0m"
|
||
BOLD = "\033[1m"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
def ok(msg): print(f"{GREEN} ✓{RESET} {msg}")
|
||
def fail(msg): print(f"{RED} ✗{RESET} {msg}")
|
||
def info(msg): print(f"{BLUE} ℹ{RESET} {msg}")
|
||
|
||
def count_tag(content: str, tag: str) -> int:
|
||
"""Count occurrences of a tag in SVG/HTML content."""
|
||
return content.count(tag)
|
||
|
||
def run_assertion(content: str, assertion: dict, fixture_name: str) -> tuple[bool, str]:
|
||
"""Run a single assertion. Returns (passed, message)."""
|
||
check = assertion["check"]
|
||
|
||
if check == "not_empty":
|
||
if len(content.strip()) > 0:
|
||
return True, "output is not empty"
|
||
return False, "output is empty"
|
||
|
||
if check == "contains":
|
||
text = assertion.get("text", "")
|
||
if text in content:
|
||
return True, f"contains '{text[:40]}'"
|
||
return False, f"missing '{text[:40]}'"
|
||
|
||
if check == "contains_tag":
|
||
tag = assertion["tag"]
|
||
cnt = count_tag(content, tag)
|
||
min_count = assertion.get("min_count", 1)
|
||
if cnt >= min_count:
|
||
return True, f"contains {cnt}× '{tag}' (need ≥{min_count})"
|
||
return False, f"contains {cnt}× '{tag}' (need ≥{min_count})"
|
||
|
||
return False, f"unknown check: {check}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Main test runner
|
||
# ---------------------------------------------------------------------------
|
||
def main():
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
skill_dir = os.path.dirname(script_dir)
|
||
sys.path.insert(0, skill_dir)
|
||
|
||
from generator import (
|
||
quick_architecture,
|
||
DiagramGenerator,
|
||
Component, Connection, Container
|
||
)
|
||
|
||
fixture_dir = os.path.join(script_dir, "fixtures")
|
||
tmp_dir = tempfile.mkdtemp(prefix="hdg_test_")
|
||
|
||
fixture_files = sorted(glob.glob(os.path.join(fixture_dir, "*.json")))
|
||
if not fixture_files:
|
||
print(f"{YELLOW}No fixtures found in {fixture_dir}{RESET}")
|
||
sys.exit(1)
|
||
|
||
passed = 0
|
||
failed = 0
|
||
total_assertions = 0
|
||
failed_fixtures = []
|
||
|
||
print(f"\n{BOLD}Running {len(fixture_files)} fixture(s){RESET}")
|
||
print(f"Temp dir: {tmp_dir}\n")
|
||
|
||
for fp in fixture_files:
|
||
with open(fp, "r", encoding="utf-8") as f:
|
||
fixture = json.load(f)
|
||
|
||
name = fixture["name"]
|
||
desc = fixture.get("description", "")
|
||
ftype = fixture["type"]
|
||
params = fixture["params"]
|
||
assertions = fixture.get("assertions", [])
|
||
|
||
# Resolve output path
|
||
output = params["output"].replace("__TMP__", tmp_dir)
|
||
os.makedirs(os.path.dirname(output), exist_ok=True)
|
||
|
||
print(f"{BOLD}{name}{RESET} — {desc}")
|
||
|
||
# ---- generate ----
|
||
try:
|
||
if ftype == "quick_architecture":
|
||
quick_architecture(
|
||
params["title"],
|
||
params["layers"],
|
||
output,
|
||
style=params.get("style", "sketch"),
|
||
format=params.get("format", "svg")
|
||
)
|
||
elif ftype == "create_architecture":
|
||
gen = DiagramGenerator(style=params.get("style", "sketch"))
|
||
comps = [Component(**c) for c in params.get("components", [])]
|
||
conns = []
|
||
for c in params.get("connections", []):
|
||
# map 'from'/'to' to Connection field names
|
||
mapped = dict(c)
|
||
if "from" in mapped:
|
||
mapped["from_comp"] = mapped.pop("from")
|
||
if "to" in mapped:
|
||
mapped["to_comp"] = mapped.pop("to")
|
||
conns.append(Connection(**mapped))
|
||
conts = [Container(**c) for c in params.get("containers", [])]
|
||
gen.create_architecture(
|
||
params["title"],
|
||
comps,
|
||
conns,
|
||
output,
|
||
cards=params.get("cards", []),
|
||
auto_layout=params.get("auto_layout", False),
|
||
format=params.get("format", "svg"),
|
||
containers=conts
|
||
)
|
||
else:
|
||
fail(f"unknown fixture type: {ftype}")
|
||
failed += 1
|
||
failed_fixtures.append(name)
|
||
continue
|
||
except Exception as e:
|
||
fail(f"generation error: {e}")
|
||
failed += 1
|
||
failed_fixtures.append(name)
|
||
continue
|
||
|
||
# ---- read output ----
|
||
try:
|
||
with open(output, "r", encoding="utf-8") as f:
|
||
content = f.read()
|
||
except Exception as e:
|
||
fail(f"read error: {e}")
|
||
failed += 1
|
||
failed_fixtures.append(name)
|
||
continue
|
||
|
||
# ---- assertions ----
|
||
fixture_passed = True
|
||
for assertion in assertions:
|
||
total_assertions += 1
|
||
a_ok, msg = run_assertion(content, assertion, name)
|
||
if a_ok:
|
||
ok(msg)
|
||
else:
|
||
fail(msg)
|
||
fixture_passed = False
|
||
|
||
if fixture_passed:
|
||
passed += 1
|
||
else:
|
||
failed += 1
|
||
failed_fixtures.append(name)
|
||
|
||
print()
|
||
|
||
# ---- summary ----
|
||
print(f"{BOLD}{'='*50}{RESET}")
|
||
print(f"Fixtures: {len(fixture_files)}")
|
||
print(f"Passed: {GREEN}{passed}{RESET}")
|
||
print(f"Failed: {RED}{failed}{RESET}")
|
||
print(f"Assertions:{total_assertions}")
|
||
print(f"{BOLD}{'='*50}{RESET}")
|
||
|
||
if failed_fixtures:
|
||
print(f"\n{BOLD}Failed fixtures:{RESET}")
|
||
for fn in failed_fixtures:
|
||
print(f" {RED}• {fn}{RESET}")
|
||
|
||
# cleanup
|
||
import shutil
|
||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||
|
||
sys.exit(0 if failed == 0 else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|