#!/usr/bin/env python3 from __future__ import annotations import json import base64 import os import shutil import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SCHEMAS = ROOT / "schemas" TEMPLATES = ROOT / "templates" EXAMPLE_CARD = ROOT / "examples" / "card" EXAMPLE_SERIES = ROOT / "examples" / "series" / "series.json" def load_json(path: Path) -> dict: with path.open(encoding="utf-8") as stream: value = json.load(stream) if not isinstance(value, dict): raise AssertionError(f"{path}: korzeń JSON musi być obiektem") return value def check_json_files() -> None: for path in sorted(SCHEMAS.glob("*.json")): schema = load_json(path) assert schema.get("$schema", "").endswith("2020-12/schema"), path for path in sorted(TEMPLATES.glob("*.json")): template = load_json(path) assert template.get("schema") == "esc-card-template.v1", path assert template.get("emitters", {}).get("latex"), path assert template.get("emitters", {}).get("html"), path card = load_json(EXAMPLE_CARD / "json" / "card_source.json") assert card.get("schema") == "esc-card-source.v1" assert card.get("sections") assert card.get("educational_requirements") series = load_json(EXAMPLE_SERIES) assert series.get("version") == 1 assert series.get("cards") def check_optional_json_schema() -> None: try: import jsonschema except ModuleNotFoundError: if os.environ.get("CARD_LAYOUTS_REQUIRE_JSONSCHEMA") == "1": raise AssertionError("brak pakietu jsonschema wymaganego przez pełną kontrolę") print("INFO: jsonschema nie jest zainstalowany; wykonuję walidację generatora.") return pairs = [ (SCHEMAS / "card-source.schema.json", EXAMPLE_CARD / "json" / "card_source.json"), (SCHEMAS / "series.schema.json", EXAMPLE_SERIES), ] pairs.extend( (SCHEMAS / "card-template.schema.json", path) for path in sorted(TEMPLATES.glob("*.json")) ) for schema_path, document_path in pairs: jsonschema.Draft202012Validator(load_json(schema_path)).validate(load_json(document_path)) def check_render() -> None: subprocess.run( [sys.executable, str(ROOT / "tools" / "render_card.py"), str(EXAMPLE_CARD)], cwd=ROOT, check=True, ) tex_path = EXAMPLE_CARD / "build" / "tex" / "main.tex" html_path = EXAMPLE_CARD / "build" / "html" / "index.html" css_path = EXAMPLE_CARD / "build" / "html" / "style.css" for path in (tex_path, html_path, css_path): if not path.is_file() or path.stat().st_size == 0: raise AssertionError(f"brak wygenerowanego pliku: {path}") tex = tex_path.read_text(encoding="utf-8") html = html_path.read_text(encoding="utf-8") for marker in ("DEMO-01", "IP WO", "INF04", "System kart pracy"): if marker not in tex: raise AssertionError(f"TeX nie zawiera znacznika {marker!r}") if marker not in html: raise AssertionError(f"HTML nie zawiera znacznika {marker!r}") if "\x00" in html: raise AssertionError("HTML zawiera bajt NUL po rozwinięciu placeholderów") if not any(r"\qrcode[" in line and "height=" in line for line in tex.splitlines()): raise AssertionError("TeX nie zawiera kodu QR z tabliczki") for expected_url in ( "https://demo.example.edu/cards/demo-01", "https://gitea.example.edu/edu/cards/demo-01", ): if expected_url not in tex or expected_url not in html: raise AssertionError(f"brak adresu nagłówka {expected_url!r}") if tex.count(r"\qrcode[level=L,height=22mm]") < 2: raise AssertionError("TeX nie zawiera obu kodów QR nagłówka") for marker in ("resource-qr-repository", "resource-qr-page"): if marker not in html: raise AssertionError(f"HTML nie zawiera komórki {marker!r}") if "PROJECT" not in html: raise AssertionError("HTML nie zawiera nowej tabliczki projektu") if "resource-qr-label" in html: raise AssertionError("HTML nadal zawiera podpis pod kodem QR") for marker in ('class="title-qr"', 'class="code-subblock"'): if marker not in html: raise AssertionError(f"HTML nie zawiera znacznika {marker!r}") def check_section_assets() -> None: with tempfile.TemporaryDirectory(prefix="card-layouts-assets-") as temp_dir: card_root = Path(temp_dir) / "card" shutil.copytree(EXAMPLE_CARD, card_root, ignore=shutil.ignore_patterns("build")) source_path = card_root / "json" / "card_source.json" source = load_json(source_path) source["template"] = str(ROOT / "templates" / "karta-klasyczna.json") source["generated"] = { "tex": "build/tex/main.tex", "html": "build/html/index.html", "html_css": "build/html/style.css", } source["sections"][0]["content_tex"] += ( r" \textbf{Widok \texttt{GDB}.}" "\\begin{verbatim}stemctl test demo\\end{verbatim}" ) source["sections"][0]["assets"] = [ { "path": "assets/debug-screen.png", "caption": "Kontrolny zrzut debuggera.", "label": "fig:debug-screen", "alt": "Zrzut debuggera", "kind": "screenshot", "width": 0.96, } ] source["sections"][0]["steps"][0]["figure_refs"] = ["fig:debug-screen"] source_path.write_text(json.dumps(source, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") asset_path = card_root / "doc" / "assets" / "debug-screen.png" asset_path.parent.mkdir(parents=True, exist_ok=True) asset_path.write_bytes( base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" ) ) subprocess.run( [sys.executable, str(ROOT / "tools" / "render_card.py"), str(card_root)], cwd=ROOT, check=True, stdout=subprocess.DEVNULL, ) tex = (card_root / "build" / "tex" / "main.tex").read_text(encoding="utf-8") html = (card_root / "build" / "html" / "index.html").read_text(encoding="utf-8") copied_asset = card_root / "build" / "html" / "figures" / "debug-screen.png" for marker in ("fig:debug-screen", "debug-screen.png", "Kontrolny zrzut debuggera"): if marker not in tex: raise AssertionError(f"TeX assetu nie zawiera znacznika {marker!r}") if marker not in html: raise AssertionError(f"HTML assetu nie zawiera znacznika {marker!r}") if "\x00" in html: raise AssertionError("HTML assetu zawiera bajt NUL") if r"\begin{verbatim}" in html or r"\end{verbatim}" in html: raise AssertionError("HTML zawiera surowe środowisko verbatim") if "
stemctl test demo
" not in html: raise AssertionError("HTML nie zawiera wyrenderowanego bloku verbatim") if not copied_asset.is_file(): raise AssertionError(f"asset nie został skopiowany: {copied_asset}") def check_external_references() -> None: with tempfile.TemporaryDirectory(prefix="card-layouts-references-") as temp_dir: root = Path(temp_dir) card_root = root / "card" registry_root = root / "registry" shutil.copytree(EXAMPLE_CARD, card_root, ignore=shutil.ignore_patterns("build")) (registry_root / "json").mkdir(parents=True) (registry_root / "docs").mkdir() registry_uuid = "03403a8a-e20d-5b17-9004-26d9fe6c3003" strategy_uuid = "1ecfc474-4fbd-5a56-8ef1-80ae557761d0" card_uuid = "4abec2d9-79e9-5dca-9040-c60669f51ff8" registry = { "schema": "esc-reference-registry.v1", "uuid": registry_uuid, "name": "test-registry", "title": "Rejestr testowy", "root_path": "..", "base_url": "https://example.test/materials/", "entries": [ { "uuid": strategy_uuid, "kind": "strategy", "code": "M02", "title": "Breakpointy", "path": "docs/M02.md", }, { "uuid": card_uuid, "kind": "card", "title": "Wskaźniki i tablice", "path": "docs/card.md", }, ], } registry_path = registry_root / "json" / "reference_registry.json" registry_path.write_text( json.dumps(registry, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) (registry_root / "docs" / "M02.md").write_text("# M02\n", encoding="utf-8") (registry_root / "docs" / "card.md").write_text("# Karta\n", encoding="utf-8") source_path = card_root / "json" / "card_source.json" source = load_json(source_path) source["template"] = str(ROOT / "templates" / "karta-klasyczna.json") source["reference_registries"] = [ {"uuid": registry_uuid, "source": str(registry_path)} ] source["sections"][0]["steps"][0]["references"] = [ {"kind": "strategy", "registry_uuid": registry_uuid, "uuid": strategy_uuid}, {"kind": "card", "registry_uuid": registry_uuid, "uuid": card_uuid}, ] source_path.write_text( json.dumps(source, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) subprocess.run( [sys.executable, str(ROOT / "tools" / "render_card.py"), str(card_root)], cwd=ROOT, check=True, stdout=subprocess.DEVNULL, ) tex = (card_root / "build" / "tex" / "main.tex").read_text(encoding="utf-8") html = (card_root / "build" / "html" / "index.html").read_text(encoding="utf-8") expected = ( "https://example.test/materials/docs/M02.md", "https://example.test/materials/docs/card.md", "M02", "Wskaźniki i tablice", ) for marker in expected: if marker not in tex: raise AssertionError(f"TeX referencji nie zawiera {marker!r}") if marker not in html: raise AssertionError(f"HTML referencji nie zawiera {marker!r}") if f'data-reference-uuid="{strategy_uuid}"' not in html: raise AssertionError("HTML nie zachowuje UUID rozwiązanej strategii") if "M02" not in html: raise AssertionError("HTML nie pokazuje kodu strategii jako etykiety") def main() -> int: check_json_files() check_optional_json_schema() check_render() check_section_assets() check_external_references() print("PASS: schematy, layouty, przykład serii oraz render TeX/HTML są spójne.") return 0 if __name__ == "__main__": raise SystemExit(main())