282 lines
11 KiB
Python
282 lines
11 KiB
Python
#!/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 "<pre><code>stemctl test demo</code></pre>" not in html:
|
|
raise AssertionError("HTML nie zawiera wyrenderowanego bloku verbatim")
|
|
for marker in (
|
|
'class="sheet-content"',
|
|
"--viewer-canvas-scale",
|
|
"--viewer-content-scale",
|
|
"event.altKey",
|
|
"event.shiftKey",
|
|
'id="viewer-zoom-reset"',
|
|
"viewerCanvasMaximum",
|
|
"viewerAutoFit",
|
|
):
|
|
if marker not in html:
|
|
raise AssertionError(f"HTML nie zawiera obsługi zoomu viewera: {marker!r}")
|
|
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 "<code>M02</code>" 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())
|