god extended conflict-decisions.json with an optional dateOfDeath (DD.MM.YYYY). apply_conflict_decisions now sets the animal's death date (normalized) as authoritative when present — clearing D5 death-date conflicts the same way genotype/farbschlag decisions are applied. No C# change (death already flows to Gerbil.DateOfDeath). test_extract covers a Flint dateOfDeath resolution. python green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
5.1 KiB
Python
117 lines
5.1 KiB
Python
"""Zero-dep tests for extract.py band-aware Farbschlag + name-bleed guard.
|
|
|
|
Run: python test_extract.py (exit 0 = all pass)
|
|
Covers (PEDIGREE-LINK / Julian-confirmed): deep pedigree bands (gen >= 2, cols K/N/Q...)
|
|
are Name/DOB/Genotype ONLY — no Farbschlag cell — so a stray health note or the next
|
|
block's name must NOT be captured as Farbschlag; early bands (gen 0-1) keep their real
|
|
Farbschlag. Plus the looks_like_animal_name guard (a parent name must not be a Farbschlag).
|
|
"""
|
|
import os
|
|
import sys
|
|
import zipfile
|
|
import tempfile
|
|
import extract as e
|
|
|
|
failed = 0
|
|
|
|
|
|
def check(name, cond):
|
|
global failed
|
|
print(("ok: " if cond else "FAIL: ") + name)
|
|
if not cond:
|
|
failed += 1
|
|
|
|
|
|
def _cell(ref, text):
|
|
return f'<c r="{ref}" t="inlineStr"><is><t>{text}</t></is></c>'
|
|
|
|
|
|
def _make_xlsx(path, cells):
|
|
"""cells: {(colLetter+row): text}. Build a minimal single-sheet xlsx (no styles)."""
|
|
rows = {}
|
|
for ref, text in cells.items():
|
|
r = int("".join(ch for ch in ref if ch.isdigit()))
|
|
rows.setdefault(r, []).append(_cell(ref, text))
|
|
body = "".join(f'<row r="{r}">{"".join(cs)}</row>' for r, cs in sorted(rows.items()))
|
|
sheet = ('<?xml version="1.0"?><worksheet xmlns="http://x"><sheetData>'
|
|
+ body + "</sheetData></worksheet>")
|
|
with zipfile.ZipFile(path, "w") as z:
|
|
z.writestr("xl/worksheets/sheet1.xml", sheet)
|
|
|
|
|
|
# --- band-aware Farbschlag ---
|
|
# col E = gen 0 (early, HAS Farbschlag); col K = col 11 = gen 2 (deep, NO Farbschlag).
|
|
tmp = os.path.join(tempfile.gettempdir(), "bandtest.xlsx")
|
|
_make_xlsx(tmp, {
|
|
# early band (E): Name / *DOB / Farbschlag / Genotype
|
|
"E10": "Chesnut",
|
|
"E11": "*13.11.2019",
|
|
"E12": "Kohlfuchsschimmel",
|
|
"E13": "aa CC DD ee GG PP spsp rere",
|
|
# deep band (K): Name / *DOB / Genotype / stray NOTE (must NOT become Farbschlag)
|
|
"K10": "DeepAnimal",
|
|
"K11": "*01.01.2020",
|
|
"K12": "aa CC DD EE GG PP spsp rere",
|
|
"K13": "DD-Tumor",
|
|
})
|
|
try:
|
|
animals = e.extract_stammbaum(tmp)
|
|
by_name = {a["name"]: a for a in animals}
|
|
check("early band keeps real Farbschlag",
|
|
by_name.get("Chesnut", {}).get("farbschlag") == "Kohlfuchsschimmel")
|
|
check("deep band has NO Farbschlag (note not grabbed)",
|
|
by_name.get("DeepAnimal", {}).get("farbschlag") == "")
|
|
check("deep-band animal still parsed (Name/DOB/Genotype)",
|
|
"DeepAnimal" in by_name and by_name["DeepAnimal"]["dob"].startswith("01.01"))
|
|
finally:
|
|
try: os.remove(tmp)
|
|
except OSError: pass
|
|
|
|
gen = e.gen_of
|
|
check("gen_of: early bands < 2 (E,H)", gen(5) < 2 and gen(8) < 2)
|
|
check("gen_of: deep bands >= 2 (K,N,Q)", gen(11) >= 2 and gen(14) >= 2)
|
|
|
|
# --- conflict-decisions consumption (HUMANQUESTION D / C6) ---
|
|
dec_path = os.path.join(tempfile.gettempdir(), "conflict-decisions-test.json")
|
|
import json as _json
|
|
_json.dump({"resolutions": [
|
|
{"name": "Firefly von den Kleinen Chaoten", "dob": "18.12.2019",
|
|
"decision": "D-locus = D-", "genotype": "Aa c[chm]c[chm] D- Ee Gg PP Spsp",
|
|
"source": "test"},
|
|
{"name": "Flint von den Kleinen Chaoten", "dob": "23.12.2017",
|
|
"decision": "Todesdatum 10.05.2021 (2022 war Tippfehler)", "dateOfDeath": "10.05.2021",
|
|
"source": "test"},
|
|
]}, open(dec_path, "w", encoding="utf-8"))
|
|
merged = [
|
|
{"id": "x1", "name": "Firefly von den Kleinen Chaoten", "dob": "18.12.2019",
|
|
"conflict": True, "farbschlag": "", "death": "",
|
|
"genotype": {"mapped8locus": {"D": ["D", "D"]}, "rawGenotype": "DD", "unmappedTokens": []}},
|
|
{"id": "x2", "name": "Flint von den Kleinen Chaoten", "dob": "23.12.2017",
|
|
"conflict": True, "farbschlag": "", "death": "10.05.2022",
|
|
"genotype": {"mapped8locus": {}, "rawGenotype": "", "unmappedTokens": []}},
|
|
]
|
|
conflicts = [{"id": "x1", "name": "Firefly von den Kleinen Chaoten", "dob": "18.12.2019"},
|
|
{"id": "x2", "name": "Flint von den Kleinen Chaoten", "dob": "23.12.2017"}]
|
|
n = e.apply_conflict_decisions(merged, conflicts, dec_path)
|
|
check("decision un-quarantines (conflict cleared)", merged[0]["conflict"] is False)
|
|
check("decision marks resolvedByDecision", merged[0].get("resolvedByDecision") is True)
|
|
check("decision genotype is authoritative (D- not DD)", merged[0]["genotype"]["mapped8locus"]["D"] == ["D", "?"])
|
|
check("decision dateOfDeath is authoritative (D5)", merged[1]["death"] == "10.05.2021")
|
|
check("decision removes both entries from conflicts list", conflicts == [])
|
|
check("apply_conflict_decisions returns resolved count", n == 2)
|
|
check("missing decisions file tolerated (returns 0)",
|
|
e.apply_conflict_decisions([], [], os.path.join(tempfile.gettempdir(), "does-not-exist.json")) == 0)
|
|
try: os.remove(dec_path)
|
|
except OSError: pass
|
|
|
|
# --- name-bleed guard (a parent name is not a Farbschlag) ---
|
|
check("v.d. name rejected", e.looks_like_animal_name("Tennessee von den Kleinen Chaoten"))
|
|
check("gen.+v.d. name rejected", e.looks_like_animal_name("Victoria Welby gen. Welby v.d. Kleinen Chaoten"))
|
|
check("real Farbschlag accepted", not e.looks_like_animal_name("Kohlfuchsschimmel"))
|
|
check("real Farbschlag accepted 2", not e.looks_like_animal_name("Orangeschimmel, hell"))
|
|
|
|
if failed:
|
|
print(f"\n{failed} test(s) FAILED")
|
|
sys.exit(1)
|
|
print("\nALL PASS")
|