gwoe-antragspruefer/tests/test_redline_parser.py

226 lines
10 KiB
Python
Raw Permalink Normal View History

"""Tests für _parse_redline_segments aus app.main.
Prüft alle Marker-Formate (§INS§/§DEL§ und **/**+~~), Edge-Cases und
gemischte Eingaben. Keine DB- oder HTTP-Abhängigkeiten.
"""
from __future__ import annotations
import pytest
from app.redline_utils import parse_redline_segments as _parse_redline_segments
# ─────────────────────────────────────────────────────────────────────────────
# Hilfsfunktionen
# ─────────────────────────────────────────────────────────────────────────────
def types(segments):
return [s["type"] for s in segments]
def texts(segments):
return [s["text"] for s in segments]
# ─────────────────────────────────────────────────────────────────────────────
# Basis-Parsing
# ─────────────────────────────────────────────────────────────────────────────
class TestBasicParsing:
def test_empty_string_returns_empty(self):
assert _parse_redline_segments("") == []
def test_none_returns_empty(self):
assert _parse_redline_segments(None) == [] # type: ignore[arg-type]
def test_plain_text_is_ctx(self):
segs = _parse_redline_segments("kein Marker hier")
assert types(segs) == ["ctx"]
assert texts(segs) == ["kein Marker hier"]
def test_ins_marker_tag_format(self):
segs = _parse_redline_segments("§INS§neuer Text§INS§")
assert types(segs) == ["ins"]
assert texts(segs) == ["neuer Text"]
def test_del_marker_tag_format(self):
segs = _parse_redline_segments("§DEL§alter Text§DEL§")
assert types(segs) == ["del"]
assert texts(segs) == ["alter Text"]
def test_markdown_bold_becomes_ins(self):
segs = _parse_redline_segments("**eingefügt**")
assert types(segs) == ["ins"]
assert texts(segs) == ["eingefügt"]
def test_markdown_strikethrough_becomes_del(self):
segs = _parse_redline_segments("~~gestrichen~~")
assert types(segs) == ["del"]
assert texts(segs) == ["gestrichen"]
# ─────────────────────────────────────────────────────────────────────────────
# Kontext + Marker gemischt
# ─────────────────────────────────────────────────────────────────────────────
class TestMixedContent:
def test_ctx_ins_ctx(self):
segs = _parse_redline_segments("§ 3 Abs. 2 §INS§verpflichtend§INS§ ab 2026")
assert types(segs) == ["ctx", "ins", "ctx"]
assert texts(segs)[1] == "verpflichtend"
def test_ctx_del_ins(self):
segs = _parse_redline_segments("Text §DEL§alt§DEL§§INS§neu§INS§ Ende")
assert types(segs) == ["ctx", "del", "ins", "ctx"]
def test_markdown_mixed(self):
segs = _parse_redline_segments("Vor ~~weg~~ und **rein** nach")
assert types(segs) == ["ctx", "del", "ctx", "ins", "ctx"]
def test_both_formats_in_one_string(self):
segs = _parse_redline_segments("§DEL§raus§DEL§ und **rein**")
assert "del" in types(segs)
assert "ins" in types(segs)
# ─────────────────────────────────────────────────────────────────────────────
# Edge-Cases
# ─────────────────────────────────────────────────────────────────────────────
class TestEdgeCases:
def test_empty_ins_marker(self):
segs = _parse_redline_segments("§INS§§INS§")
# Leerer ins-Marker bleibt ein ins-Segment mit leerem Text
assert any(s["type"] == "ins" for s in segs)
def test_empty_del_marker(self):
segs = _parse_redline_segments("§DEL§§DEL§")
assert any(s["type"] == "del" for s in segs)
def test_unbalanced_marker_treated_as_ctx(self):
# Nur ein §INS§ ohne schließenden Partner → kein ins-Segment
segs = _parse_redline_segments("§INS§unvollständig")
assert all(s["type"] == "ctx" for s in segs)
def test_marker_with_whitespace_only(self):
segs = _parse_redline_segments("§INS§ §INS§")
ins_segs = [s for s in segs if s["type"] == "ins"]
assert len(ins_segs) == 1
assert ins_segs[0]["text"].strip() == ""
def test_multiple_ins_markers(self):
segs = _parse_redline_segments("§INS§A§INS§ und §INS§B§INS§")
ins_texts = [s["text"] for s in segs if s["type"] == "ins"]
assert ins_texts == ["A", "B"]
def test_multiple_del_markers(self):
segs = _parse_redline_segments("§DEL§X§DEL§ und §DEL§Y§DEL§")
del_texts = [s["text"] for s in segs if s["type"] == "del"]
assert del_texts == ["X", "Y"]
def test_no_empty_ctx_segments(self):
# Leere ctx-Segmente sollen nicht in der Ergebnisliste auftauchen
segs = _parse_redline_segments("§INS§nur Marker§INS§")
ctx_segs = [s for s in segs if s["type"] == "ctx"]
# Leere ctx-Strings dürfen nicht enthalten sein
for seg in ctx_segs:
assert seg["text"] != ""
def test_newline_inside_marker(self):
segs = _parse_redline_segments("§INS§Zeile 1\nZeile 2§INS§")
ins_segs = [s for s in segs if s["type"] == "ins"]
assert len(ins_segs) == 1
assert "Zeile 1" in ins_segs[0]["text"]
def test_long_realistic_redline(self):
text = (
"Die Gemeinde §DEL§soll§DEL§ §INS§muss§INS§ bis zum "
"§DEL§31.12.2026§DEL§ §INS§30.06.2025§INS§ einen Plan vorlegen."
)
segs = _parse_redline_segments(text)
del_texts = [s["text"] for s in segs if s["type"] == "del"]
ins_texts = [s["text"] for s in segs if s["type"] == "ins"]
assert "soll" in del_texts
assert "muss" in ins_texts
assert "31.12.2026" in del_texts
assert "30.06.2025" in ins_texts
# ─── build_pdf_href Tests (#134 Coverage-Backfill) ───────────────────────────
class TestBuildPdfHref:
"""Tests fuer build_pdf_href: rekonstruiert PDF-URLs aus Zitat-Metadaten,
bevorzugt die explizite url, faellt auf WAHLPROGRAMME-Lookup zurueck."""
def test_explicit_url_passed_through(self):
from app.redline_utils import build_pdf_href
zitat = {"url": "/api/wahlprogramm-cite?pid=cdu-nrw-2022&seite=15"}
assert build_pdf_href(zitat) == "/api/wahlprogramm-cite?pid=cdu-nrw-2022&seite=15"
def test_empty_url_falls_back_to_quelle_lookup(self):
"""Ohne url muss die quelle reconstruiert werden via WAHLPROGRAMME."""
from app.redline_utils import build_pdf_href
# Ein in WAHLPROGRAMME hinterlegter Titel
from app.wahlprogramme import WAHLPROGRAMME
# Pick the first programme from the registry
bl, parteien = next(iter(WAHLPROGRAMME.items()))
partei, info = next(iter(parteien.items()))
titel = info.get("titel", "")
if not titel:
pytest.skip("Kein WAHLPROGRAMME-Eintrag mit titel verfuegbar")
zitat = {
"quelle": f"{titel} · S. 42",
"text": "Wir wollen die Energiewende",
"url": "",
}
href = build_pdf_href(zitat)
assert "/api/wahlprogramm-cite" in href
assert "seite=42" in href
assert "#page=42" in href # URL-Hash fuer Browser-PDF-Viewer
def test_no_seitenzahl_returns_empty(self):
from app.redline_utils import build_pdf_href
zitat = {"quelle": "Irgendein Programm ohne Seite", "text": "x", "url": ""}
assert build_pdf_href(zitat) == ""
def test_unmatched_quelle_returns_empty(self):
from app.redline_utils import build_pdf_href
zitat = {
"quelle": "Erfundenes Programm 1995, S. 1",
"text": "x",
"url": "",
}
assert build_pdf_href(zitat) == ""
def test_query_uses_first_5_words_of_text(self):
from app.redline_utils import build_pdf_href
from app.wahlprogramme import WAHLPROGRAMME
bl, parteien = next(iter(WAHLPROGRAMME.items()))
partei, info = next(iter(parteien.items()))
titel = info.get("titel", "")
if not titel:
pytest.skip("Kein WAHLPROGRAMME-Eintrag mit titel verfuegbar")
zitat = {
"quelle": f"{titel} · S. 5",
"text": "Eins zwei drei vier fünf sechs sieben",
"url": "",
}
href = build_pdf_href(zitat)
# max. 5 Worte → "sechs sieben" muessen im Query fehlen
assert "sechs" not in href
assert "sieben" not in href
# erste fuenf Wortteile sollten kodiert in q= auftauchen
assert "Eins" in href or "Eins" in href.replace("+", " ")
def test_handles_seite_with_comma_separator(self):
"""Quelle 'Titel, S. 42' (Komma) muss genauso parsen wie '· S. 42'."""
from app.redline_utils import build_pdf_href
from app.wahlprogramme import WAHLPROGRAMME
bl, parteien = next(iter(WAHLPROGRAMME.items()))
partei, info = next(iter(parteien.items()))
titel = info.get("titel", "")
if not titel:
pytest.skip("Kein WAHLPROGRAMME-Eintrag mit titel verfuegbar")
zitat = {"quelle": f"{titel}, S. 17", "text": "x", "url": ""}
href = build_pdf_href(zitat)
assert "seite=17" in href