"""Tests for extract_contracts.py — run: python test_extract_contracts.py
Zero third-party deps (mirrors test_extract_docx.py). Builds tiny in-memory
.docx files (a zip with word/document.xml) so the tests run without the
network share. Covers: filename parsing, run-splitting de-mangling, both body
layouts (side-by-side / stacked), price/date normalisation, and skip rules.
"""
import io
import os
import sys
import zipfile
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import extract_contracts as ec
failed = 0
def check(name, cond):
global failed
print(("ok: " if cond else "FAIL: ") + name)
if not cond:
failed += 1
def make_docx(paragraphs):
"""paragraphs: list of lists of run-strings → bytes of a .docx zip."""
body = []
for runs in paragraphs:
rs = "".join(f"{r}" for r in runs)
body.append(f"{rs}")
xml = (''
+ "".join(body) + "")
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
z.writestr("word/document.xml", xml)
return buf.getvalue()
def write_tmp(name, data):
d = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_test_tmp")
os.makedirs(d, exist_ok=True)
p = os.path.join(d, name)
with open(p, "wb") as f:
f.write(data)
return p
# --- _norm_date -----------------------------------------------------------
check("date 4-digit year", ec._norm_date("18.02.2014") == "2014-02-18")
check("date 2-digit year", ec._norm_date("09.12.14") == "2014-12-09")
check("date embedded", ec._norm_date("Bonn, 20.09.2014") == "2014-09-20")
check("date invalid", ec._norm_date("99.99.9999") == "")
check("date empty", ec._norm_date("") == "")
# --- _clean_money ---------------------------------------------------------
check("money comma", ec._clean_money("27,50 Euro (Überweisung)") == "27,50")
check("money dot→comma", ec._clean_money("25.00 €") == "25,00")
check("money plain", ec._clean_money("28 Euro") == "28")
check("money none", ec._clean_money("kostenlos") == "")
# --- parse_filename -------------------------------------------------------
fn = ec.parse_filename(
"Zucht der kleinen Chaoten _ Agouti (Kathlin.Baxter) - Stephan Füchsle_.docx")
check("fn color", fn["color"] == "Agouti")
check("fn animals", fn["animals"] == ["Kathlin", "Baxter"])
check("fn buyer", fn["buyer"] == "Stephan Füchsle")
fn2 = ec.parse_filename(
"Zucht der kleinen Chaoten _ dd Polar Sp (Velvet.Vance Jr.)- Alexandra Wendler_.docx")
check("fn multiword color", fn2["color"] == "dd Polar Sp")
check("fn multiword animal", fn2["animals"] == ["Velvet", "Vance Jr"])
check("fn buyer 2", fn2["buyer"] == "Alexandra Wendler")
fn3 = ec.parse_filename("Zucht der kleinen Chaoten _ Ethan - Stefanie Stoica_.docx")
check("fn no-paren animal", fn3["animals"] == ["Ethan"])
check("fn no-paren buyer", fn3["buyer"] == "Stefanie Stoica")
fn4 = ec.parse_filename("Zucht der kleinen Chaoten _Azrael_.docx")
check("fn name-only animal", fn4["animals"] == ["Azrael"])
check("fn name-only no buyer", fn4["buyer"] == "")
# --- run-splitting de-mangling (the core text-extraction fix) -------------
data = make_docx([["Name:", "F", "r", "au Josephin Kiefer"]])
p = write_tmp("split.docx", data)
check("run-split joins to 'Frau Josephin Kiefer'",
ec._full_text(p) == "Name:Frau Josephin Kiefer")
money = make_docx([["Schutzgebühr: ", "3", "0,00 €"]])
p = write_tmp("money.docx", money)
check("run-split price intact",
ec._clean_money(ec._label_value(ec._full_text(p), r"Schutzgeb\w+")) == "30,00")
# --- full contract: stacked layout (B) ------------------------------------
stacked = make_docx([
["Vermittlungsvertrag"],
["Abgebender/Züchter:"],
["Name:", "Frau Drazena Rimac"],
["Straße:", "New-York-Str. 30"],
["Empfänger/Abnehmer:"],
["Name:", "Familie Tanja und Thorsten Kurz"],
["Straße:", "Ebelstraße 2"],
["Tierdaten:"],
["Name:", "Einstein"],
["Geburtsdatum:", "10.08.2019"],
["Geschlecht:", "männlich"],
["Farbschlag: ", "Schwarz"],
["Abgabedatum:", "31.12.2019"],
["Schutzgebühr: ", "25,00 €"],
["Butzbach, den 31.12.2019"],
])
p = write_tmp("stacked.docx",
make_docx_b := stacked)
rec = ec.parse_contract(p)
check("stacked buyer", rec["buyer"] == "Familie Tanja und Thorsten Kurz")
check("stacked not seller", rec["buyer"] != "Frau Drazena Rimac")
check("stacked animal name", rec["animalNameBody"] == "Einstein")
check("stacked dob", rec["dob"] == "2019-08-10")
check("stacked gender", rec["gender"] == "Male")
check("stacked color", rec["color"] == "Schwarz")
check("stacked handover", rec["handoverDate"] == "2019-12-31")
check("stacked price", rec["price"] == "25,00")
check("stacked contract date", rec["contractDate"] == "2019-12-31")
# --- full contract: side-by-side columns (A) ------------------------------
# Two columns render as separate paragraphs (table cells).
sidebyside = make_docx([
["Vermittlungsvertrag"],
["Züchter:"], ["Abnehmer:"],
["Name:", "Drazena Rimac"], ["Name:", "Andrea Thesing"],
["Straße:", "Alten-Busecker-Str. 57"], ["Straße:", "Veilchenweg 32"],
["Tierdaten:"],
["Name:", "Sally"],
["Geburtsdatum:", "03.02.2014"],
["Geschlecht:", "weiblich"],
["Farbschlag:", "Kohlfuchs-hell"],
["Abgabedatum:", "20.09.2014"],
["Gesamtpreis: ", "27,00 Euro (Überweisung)"],
["Bonn, 20.09.2014"],
])
p = write_tmp("sidebyside.docx", sidebyside)
rec = ec.parse_contract(p)
check("sbs buyer is Andrea Thesing", rec["buyer"] == "Andrea Thesing")
check("sbs buyer not seller", rec["buyer"] != "Drazena Rimac")
check("sbs animal", rec["animalNameBody"] == "Sally")
check("sbs gender female", rec["gender"] == "Female")
check("sbs price", rec["price"] == "27,00")
# --- skip rules -----------------------------------------------------------
abstammung = make_docx([
["Abstammungsnachweis"],
["Name:"], ["Grace"],
["Geburtsdatum:", "04.10.2015"],
])
p = write_tmp("abst.docx", abstammung)
check("Abstammungsnachweis skipped", ec.parse_contract(p) is None)
# bad zip
badp = write_tmp("bad.docx", b"not a zip")
check("bad zip → None", ec.parse_contract(badp) is None)
# --- filename color fallback when body has no Farbschlag ------------------
nocolor = make_docx([
["Vermittlungsvertrag"], ["Abnehmer:"], ["Name:", "Max Mustermann"],
["Tierdaten:"], ["Name:", "Bello"],
["Geburtsdatum:", "01.01.2020"], ["Geschlecht:", "männlich"],
["Gesamtpreis: ", "20,00 €"],
])
p = write_tmp("Zucht der kleinen Chaoten _ Agouti (Bello)- Max Mustermann_.docx",
nocolor)
rec = ec.parse_contract(p)
check("color falls back to filename", rec["color"] == "Agouti")
check("animals from filename", rec["animals"] == ["Bello"])
# cleanup tmp
import shutil
shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), "_test_tmp"),
ignore_errors=True)
print()
if failed:
print(f"{failed} test(s) FAILED")
sys.exit(1)
print("All extract_contracts tests passed.")