#!/usr/bin/env python3 """Regression test for extract_fields.py against the erp#39 golden set. The email skill's extraction was a heredoc inside a shell script: impossible to run in isolation, therefore never measured, therefore silently wrong. This pins it to the 16 hand-verified supplier invoices in fleet/golden/invoice-extract/. Scored per field, and a WRONG value counts against us far more than a missing one: emitting Arcodange's VAT number as an invoice reference is worse than emitting nothing, because a human copies it without looking. python3 test_extract.py # summary python3 test_extract.py --verbose # per-document diff """ from __future__ import annotations import json import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from extract_fields import extract, parse_amount REPO = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "..")) GOLDEN = os.path.join(REPO, "fleet", "golden", "invoice-extract") VERBOSE = "--verbose" in sys.argv def norm_date(s): """dd/mm/yyyy or yyyy-mm-dd → yyyy-mm-dd; anything else stays as-is.""" if not s: return None s = s.strip() if len(s) == 10 and s[4] == "-": return s for sep in ("/", ".", "-"): p = s.split(sep) if len(p) == 3 and all(x.strip().isdigit() for x in p): d, m, y = (x.strip() for x in p) if len(y) == 2: y = "20" + y if len(y) == 4: return f"{y}-{int(m):02d}-{int(d):02d}" return s def main() -> int: inputs = os.path.join(GOLDEN, "inputs") if not os.path.isdir(inputs): print(f"golden set not found at {GOLDEN} — skipping", file=sys.stderr) return 0 stats = {f: {"ok": 0, "wrong": 0, "missing": 0} for f in ("invoice_ref", "invoice_date_raw", "total_ht", "total_ttc")} docs = 0 for name in sorted(os.listdir(inputs)): if not name.endswith(".json"): continue doc = json.load(open(os.path.join(inputs, name))) exp_path = os.path.join(GOLDEN, "expected", name) if not os.path.exists(exp_path): continue exp = json.load(open(exp_path)) got = extract(doc["text"]) docs += 1 checks = { "invoice_ref": (exp.get("ref_supplier"), got.get("invoice_ref")), "invoice_date_raw": (exp.get("date_issue"), norm_date(got.get("invoice_date_raw"))), "total_ht": (exp.get("totals", {}).get("ht"), parse_amount(got.get("total_ht") or "")), "total_ttc": (exp.get("totals", {}).get("ttc"), parse_amount(got.get("total_ttc") or "")), } for field, (want, have) in checks.items(): if have is None: stats[field]["missing"] += 1 verdict = "MANQUE" elif isinstance(want, (int, float)) and isinstance(have, (int, float)): ok = abs(float(want) - float(have)) <= 0.01 stats[field]["ok" if ok else "wrong"] += 1 verdict = "ok" if ok else "FAUX" else: ok = str(want) == str(have) stats[field]["ok" if ok else "wrong"] += 1 verdict = "ok" if ok else "FAUX" if VERBOSE and verdict != "ok": print(f" {name[:34]:<36}{field:<18}{verdict:<8}attendu={want!r} obtenu={have!r}") print(f"\n{docs} documents du golden set\n") print(f"{'champ':<20}{'exact':>7}{'FAUX':>7}{'absent':>9} {'exactitude'}") print("-" * 62) total_wrong = 0 for f, s in stats.items(): total = s["ok"] + s["wrong"] + s["missing"] total_wrong += s["wrong"] print(f"{f:<20}{s['ok']:>7}{s['wrong']:>7}{s['missing']:>9} {100*s['ok']/total:.1f} %") print("-" * 62) print(f"\nvaleurs FAUSSES (le pire cas — un humain les recopie) : {total_wrong}") return 1 if total_wrong else 0 if __name__ == "__main__": sys.exit(main())