#!/usr/bin/env python3 """Adversarial tests for lint-skin.py's accessible-SVG contract.""" from __future__ import annotations import importlib.util import shutil import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent LINTER = ROOT / "scripts/lint-skin.py" BUILD_ICONS = ROOT / "scripts/build-icons.py" VALID_SVG = """\ Fixture diagram Diagram showing a fixture connected to a result. """ def run_linter(path: Path) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, str(LINTER), str(path)], capture_output=True, text=True, check=False, ) def require_failure(name: str, html: str, expected: str, directory: Path) -> None: path = directory / f"{name}.html" path.write_text(html, encoding="utf-8") result = run_linter(path) if result.returncode != 1: raise AssertionError( f"{name}: expected exit 1, got {result.returncode}\n{result.stdout}{result.stderr}" ) if f"a11y: {expected}" not in result.stdout: raise AssertionError( f"{name}: missing expected a11y finding {expected!r}\n{result.stdout}" ) print(f"OK: {name} rejected — {expected}") def require_pass(name: str, html: str, directory: Path) -> None: path = directory / f"{name}.html" path.write_text(html, encoding="utf-8") result = run_linter(path) if result.returncode != 0: raise AssertionError( f"{name}: expected exit 0, got {result.returncode}\n{result.stdout}{result.stderr}" ) if "a11y:" in result.stdout: raise AssertionError(f"{name}: unexpected a11y finding\n{result.stdout}") print(f"OK: {name} accepted") def main() -> int: with tempfile.TemporaryDirectory(prefix="lint-a11y-") as temp_dir: directory = Path(temp_dir) require_failure( "missing-role", VALID_SVG.replace(' role="img"', ""), 'diagram must carry role="img"', directory, ) require_failure( "unresolved-labelled-by", VALID_SVG.replace( 'aria-labelledby="fixture-title fixture-desc"', 'aria-labelledby="absent-title absent-desc"', ), "aria-labelledby references missing id(s): absent-title, absent-desc", directory, ) require_failure( "labels-unrelated-elements", VALID_SVG.replace(' id="fixture-title"', "") .replace(' id="fixture-desc"', "") .replace( ' ', ' \n' ' \n' ' ', ), "aria-labelledby must name the and IDs", directory, ) require_failure( "late-title", VALID_SVG.replace( ' Fixture diagram\n \n Fixture diagram\n must be the first child element of ", directory, ) require_failure( "empty-desc", VALID_SVG.replace( "Diagram showing a fixture connected to a result.", " " ), " must not be empty", directory, ) require_failure( "bare-ids", VALID_SVG.replace("fixture-title", "title").replace( "fixture-desc", "desc" ), 'bare id="title" and id="desc" are not allowed', directory, ) require_failure( "wrong-file-slug", VALID_SVG, 'accessible-name IDs must match file slug "wrong-file-slug": ' 'expected "wrong-file-slug-title" / "wrong-file-slug-desc"', directory, ) require_failure( "placeholder-ids", VALID_SVG.replace("fixture-title", "[diagram-slug]-title").replace( "fixture-desc", "[diagram-slug]-desc" ), "accessible-name IDs contain unresolved placeholder(s): " "[diagram-slug]-title, [diagram-slug]-desc", directory, ) require_failure( "duplicate-naming-ids", VALID_SVG + VALID_SVG, 'duplicate accessible-name id="fixture-desc" is not allowed', directory, ) require_pass( "decorative", '\n', directory, ) spec = importlib.util.spec_from_file_location("build_icons", BUILD_ICONS) if spec is None or spec.loader is None: raise AssertionError("generated-icons: could not load build-icons.py") build_icons = importlib.util.module_from_spec(spec) previous_bytecode_setting = sys.dont_write_bytecode sys.dont_write_bytecode = True try: spec.loader.exec_module(build_icons) finally: sys.dont_write_bytecode = previous_bytecode_setting generated_icons = { "tabler": build_icons.normalize_tabler(""), "simple": build_icons.normalize_simple(""), "url": build_icons.normalize_url( '' ), "devicon": build_icons.normalize_devicon(""), "logz": build_icons.normalize_logz(""), } for source, icon in generated_icons.items(): slug = f"generated-{source}-icon" require_pass( slug, VALID_SVG.replace("fixture-title", f"{slug}-title") .replace("fixture-desc", f"{slug}-desc") .replace("", f" {icon}\n"), directory, ) project = directory / "baseline-project" (project / "scripts").mkdir(parents=True) (project / "skills/diagram-design/assets").mkdir(parents=True) (project / "skills/diagram-design/references").mkdir(parents=True) shutil.copy2(LINTER, project / "scripts/lint-skin.py") shutil.copy2( ROOT / "skills/diagram-design/references/style-guide.md", project / "skills/diagram-design/references/style-guide.md", ) (project / "scripts/lint-skin-baseline.txt").write_text( "example-baseline.html\n", encoding="utf-8" ) (project / "skills/diagram-design/assets/example-baseline.html").write_text( '\n', encoding="utf-8" ) baseline_result = subprocess.run( [ sys.executable, str(project / "scripts/lint-skin.py"), "--all", "--baseline", ], capture_output=True, text=True, check=False, ) if baseline_result.returncode != 1: raise AssertionError( "baseline-a11y: expected exit 1, got " f"{baseline_result.returncode}\n" f"{baseline_result.stdout}{baseline_result.stderr}" ) if "a11y: diagram must carry role=\"img\"" not in baseline_result.stdout: raise AssertionError( "baseline-a11y: missing expected accessibility finding\n" f"{baseline_result.stdout}" ) if "color:" in baseline_result.stdout: raise AssertionError( "baseline-a11y: legacy visual finding was not exempted\n" f"{baseline_result.stdout}" ) print("OK: baseline file receives a11y checks but keeps visual exemptions") print("All accessible-SVG lint cases passed.") return 0 if __name__ == "__main__": sys.exit(main())