|
| 1 | +# tests/test_examples_against_fixtures.py |
| 2 | +import json |
| 3 | +import subprocess |
| 4 | +import os |
| 5 | +from pathlib import Path |
| 6 | +import difflib |
| 7 | +import pytest |
| 8 | +from lxml import etree, html as lxml_html |
| 9 | + |
| 10 | +# adjust import path to your tei2json module location |
| 11 | +import sys |
| 12 | +repo_root = Path(__file__).resolve().parents[1] |
| 13 | +sys.path.insert(0, str(repo_root / )) |
| 14 | + |
| 15 | +import tei2json # must expose process_file(Path) -> dict |
| 16 | + |
| 17 | +# where to find examples and expected outputs (adjust if your layout differs) |
| 18 | +EXAMPLES_DIR = Path("exampleData/xml") |
| 19 | +EXPECTED_JSON_DIR = Path("exampleData/json") |
| 20 | +EXPECTED_HTML_DIR = Path("exampleData/ms") |
| 21 | + |
| 22 | +# optional Saxon jar; tests that need Saxon will skip if not present |
| 23 | +SAXON_JAR = os.environ.get("SAXON_JAR") or "saxon.jar" |
| 24 | + |
| 25 | +def normalize_json(obj): |
| 26 | + """Return a canonical JSON string for comparison: sorted keys, no extra whitespace.""" |
| 27 | + return json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":")) |
| 28 | + |
| 29 | +def normalize_html_string(s: str) -> str: |
| 30 | + """Parse HTML-ish string and return a normalized serialization for stable comparison.""" |
| 31 | + # parse as HTML fragment (lxml.html) to tolerate non-well-formedness |
| 32 | + try: |
| 33 | + doc = lxml_html.fromstring(s) |
| 34 | + # tostring with method='html' to preserve typical HTML serialization |
| 35 | + return lxml_html.tostring(doc, encoding="unicode", method="html").strip() |
| 36 | + except Exception: |
| 37 | + # fallback: collapse whitespace and strip |
| 38 | + return " ".join(s.split()) |
| 39 | + |
| 40 | +def run_saxon_transform(input_xml: Path, xsl_path: Path, out_path: Path): |
| 41 | + """Run Saxon jar to transform input_xml with xsl_path -> out_path. |
| 42 | + Raises subprocess.CalledProcessError on failure. |
| 43 | + """ |
| 44 | + jar = Path(SAXON_JAR) |
| 45 | + if not jar.exists(): |
| 46 | + raise FileNotFoundError(f"Saxon JAR not found at {jar}; set SAXON_JAR env or put saxon.jar at repo root.") |
| 47 | + cmd = [ |
| 48 | + "java", "-jar", str(jar), |
| 49 | + "-s:" + str(input_xml), |
| 50 | + "-xsl:" + str(xsl_path), |
| 51 | + "-o:" + str(out_path) |
| 52 | + ] |
| 53 | + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 54 | + |
| 55 | +def all_example_xmls(): |
| 56 | + """Yield Path objects for all XML example files under EXAMPLES_DIR (recursive).""" |
| 57 | + if not EXAMPLES_DIR.exists(): |
| 58 | + return [] |
| 59 | + return sorted(EXAMPLES_DIR.rglob("*.xml")) |
| 60 | + |
| 61 | +@pytest.mark.parametrize("xml_path", all_example_xmls()) |
| 62 | +def test_example_json_matches_fixture(xml_path, tmp_path): |
| 63 | + """ |
| 64 | + For each example XML: |
| 65 | + - process with tei2json.process_file |
| 66 | + - compare produced JSON to exampleData/json/<stem>.json |
| 67 | + """ |
| 68 | + # process xml into dict |
| 69 | + data = tei2json.process_file(xml_path) # should return a dict |
| 70 | + produced = normalize_json(data) |
| 71 | + |
| 72 | + expected_file = EXPECTED_JSON_DIR / (xml_path.stem + ".json") |
| 73 | + if not expected_file.exists(): |
| 74 | + pytest.skip(f"Expected JSON fixture not found for {xml_path.stem}: {expected_file}") |
| 75 | + |
| 76 | + expected_obj = json.loads(expected_file.read_text(encoding="utf8")) |
| 77 | + expected = normalize_json(expected_obj) |
| 78 | + |
| 79 | + if produced != expected: |
| 80 | + # produce a readable diff (pretty-printed) |
| 81 | + pretty_prod = json.dumps(json.loads(produced), ensure_ascii=False, indent=2, sort_keys=True) |
| 82 | + pretty_exp = json.dumps(json.loads(expected), ensure_ascii=False, indent=2, sort_keys=True) |
| 83 | + diff = "\n".join(difflib.unified_diff(pretty_exp.splitlines(), pretty_prod.splitlines(), |
| 84 | + fromfile="expected", tofile="produced", lineterm="")) |
| 85 | + pytest.fail(f"JSON mismatch for {xml_path}:\n{diff}") |
| 86 | + |
| 87 | +@pytest.mark.parametrize("xml_path", all_example_xmls()) |
| 88 | +def test_example_html_matches_fixture(xml_path, tmp_path): |
| 89 | + """ |
| 90 | + For each example XML: |
| 91 | + - if expected HTML fixture exists, attempt to run XSLT (siteGenerator/xsl/tei2html.xsl) |
| 92 | + using Saxon (SAXON_JAR env or saxon.jar at repo root). |
| 93 | + - compare normalized HTML serialization to exampleData/ms/<stem>.html |
| 94 | + """ |
| 95 | + expected_html_file = EXPECTED_HTML_DIR / (xml_path.stem + ".html") |
| 96 | + if not expected_html_file.exists(): |
| 97 | + pytest.skip(f"Expected HTML fixture not found for {xml_path.stem}: {expected_html_file}") |
| 98 | + |
| 99 | + # find the XSL file (adjust this relative path if needed) |
| 100 | + xsl_path = Path("siteGenerator/xsl/tei2html.xsl") |
| 101 | + if not xsl_path.exists(): |
| 102 | + pytest.skip(f"XSLT stylesheet not found at {xsl_path}; cannot produce HTML for comparison.") |
| 103 | + |
| 104 | + # require saxon jar available |
| 105 | + jar = Path(SAXON_JAR) |
| 106 | + if not jar.exists(): |
| 107 | + pytest.skip(f"Saxon JAR not found at {jar}; set SAXON_JAR env or place saxon.jar at repo root to run HTML tests.") |
| 108 | + |
| 109 | + out_html = tmp_path / (xml_path.stem + ".html") |
| 110 | + try: |
| 111 | + run_saxon_transform(xml_path, xsl_path, out_html) |
| 112 | + except subprocess.CalledProcessError as e: |
| 113 | + # capture stderr for clearer test output |
| 114 | + stderr = e.stderr.decode() if hasattr(e, "stderr") else str(e) |
| 115 | + pytest.fail(f"Saxon transform failed for {xml_path} with error:\n{stderr}") |
| 116 | + |
| 117 | + # compare normalized HTML |
| 118 | + produced_raw = out_html.read_text(encoding="utf8") |
| 119 | + expected_raw = expected_html_file.read_text(encoding="utf8") |
| 120 | + produced_norm = normalize_html_string(produced_raw) |
| 121 | + expected_norm = normalize_html_string(expected_raw) |
| 122 | + |
| 123 | + if produced_norm != expected_norm: |
| 124 | + # produce diff of the textual serializations |
| 125 | + diff = "\n".join(difflib.unified_diff(expected_norm.splitlines(), produced_norm.splitlines(), |
| 126 | + fromfile="expected_html", tofile="produced_html", lineterm="")) |
| 127 | + pytest.fail(f"HTML mismatch for {xml_path}:\n{diff}") |
0 commit comments