#!/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()