gwoe-antragspruefer/tests/test_protokoll_parser_nrw.py

206 lines
8.1 KiB
Python
Raw Normal View History

"""Tests fuer app/protokoll_parser_nrw.py — NRW-Plenarprotokoll-Parser v5.
Backfill aus #134. Der Parser ist deterministisch und anchor-basiert;
jede Aenderung an den RESULT_ANCHORS oder den Vote-Block-Regexes muss
sofort durch diese Tests fallen.
Die echte 19/19-Garantie auf MMP18-119 laeuft separat als Integration-Test
(braucht das PDF). Hier: pure-string-Tests fuer alle Reverse-Engineering-
Findings, die bei der iterativen Entwicklung 1-15 dokumentiert wurden.
"""
from __future__ import annotations
import sys
import types
# fitz ist via tests/conftest.py gestubbed — Pure-String-Funktionen kommen ohne aus.
from app.protokoll_parser_nrw import (
normalize_fraktionen,
find_results,
resolve_drucksache_for_ueber,
normalize_text,
_is_empty_phrase,
_parse_vote_block,
ALLE_FRAKTIONEN_NRW,
)
class TestNormalizeFraktionen:
def test_simple_cdu(self):
assert normalize_fraktionen("Wer stimmt zu? CDU") == ["CDU"]
def test_multiple_fraktionen(self):
result = normalize_fraktionen("CDU, SPD und GRÜNE")
assert result == sorted(["CDU", "SPD", "GRÜNE"])
def test_buendnis_90_normalizes_to_gruene(self):
assert normalize_fraktionen("Bündnis 90/Die Grünen") == ["GRÜNE"]
def test_fdp_with_dots_normalizes(self):
"""F.D.P. (mit Punkten) muss als FDP erkannt werden."""
assert normalize_fraktionen("F.D.P.") == ["FDP"]
def test_no_double_match_for_overlapping_keys(self):
"""'GRÜNE' darf nicht zusaetzlich als 'Grünen' wieder gematcht werden."""
result = normalize_fraktionen("GRÜNE und Grünen")
# Beide Tokens sind dieselbe Fraktion → nur einmal in der Liste
assert result.count("GRÜNE") == 1
def test_landesregierung_recognized(self):
assert "Landesregierung" in normalize_fraktionen("Landesregierung")
def test_empty_text_returns_empty(self):
assert normalize_fraktionen("") == []
def test_no_known_partei(self):
assert normalize_fraktionen("Some random text") == []
class TestIsEmptyPhrase:
def test_niemand_is_empty(self):
assert _is_empty_phrase("Stimmt jemand dagegen? Niemand") is True
def test_keine_is_empty(self):
assert _is_empty_phrase("Enthaltungen? Keine") is True
def test_nicht_der_fall(self):
assert _is_empty_phrase("Das ist nicht der Fall.") is True
def test_actual_fraktion_is_not_empty(self):
assert _is_empty_phrase("CDU und SPD") is False
class TestParseVoteBlock:
def test_simple_ja_extraction(self):
block = "Wer stimmt zu? CDU und SPD."
votes = _parse_vote_block(block)
assert "CDU" in votes["ja"] and "SPD" in votes["ja"]
def test_ja_with_negation_returns_empty(self):
"""'Wer stimmt zu? Niemand.' → ja-Liste muss leer sein."""
block = "Wer stimmt zu? Niemand."
votes = _parse_vote_block(block)
assert votes["ja"] == []
def test_nein_extraction(self):
block = "Wer stimmt dagegen? AfD."
votes = _parse_vote_block(block)
assert "AfD" in votes["nein"]
def test_dagegen_negation(self):
block = "Wer stimmt dagegen? Das ist nicht der Fall."
votes = _parse_vote_block(block)
assert votes["nein"] == []
class TestFindResults:
def test_direct_angenommen(self):
text = (
"Damit ist der Antrag Drucksache 18/123 mit den Stimmen "
"der CDU und der SPD angenommen."
)
results = find_results(text)
assert len(results) == 1
r = results[0]
assert r["drucksache"] == "18/123"
assert r["ergebnis"] == "angenommen"
def test_direct_abgelehnt(self):
text = (
"Damit ist der Antrag Drucksache 18/9999 mit den Stimmen "
"der CDU gegen die Stimmen der SPD abgelehnt."
)
results = find_results(text)
assert any(r["drucksache"] == "18/9999" and r["ergebnis"] == "abgelehnt" for r in results)
def test_einstimmig_flag_only_for_ueber_kind(self):
"""v5-Verhalten dokumentiert: 'einstimmig' wird in direct-kind-Anchors
NICHT gesetzt, nur in ueber/petition/uebersicht. Dieser Test pinnt
das aktuelle Verhalten wenn v6 einstimmig auch fuer direct erkennt,
muss der Test angepasst werden."""
text = "Damit ist der Antrag Drucksache 18/100 einstimmig angenommen."
results = find_results(text)
assert results[0]["kind"] == "direct_broad"
# einstimmig wird hier (noch) nicht gesetzt — Reverse-Engineering-Befund
assert results[0]["einstimmig"] is False
def test_einstimmig_flag_for_ueberweisung(self):
"""Bei Ueberweisungs-Anchors mit 'einstimmig' im naechsten Token-Bereich
wird das Flag gesetzt."""
text = "Drucksache 18/100 ... Damit ist diese Überweisungsempfehlung einstimmig angenommen."
results = find_results(text)
ueber_results = [r for r in results if r["kind"] == "ueber"]
assert ueber_results, "kein ueber-Result im Test-Text gefunden"
assert ueber_results[0]["einstimmig"] is True
def test_ueberweisung_so_beschlossen_implies_einstimmig(self):
"""'Damit ist das so beschlossen' = implizit einstimmige Ueberweisung."""
text = "Drucksache 18/200 ... Damit ist das so beschlossen."
results = find_results(text)
assert any(r["kind"] == "ueber" and r["einstimmig"] for r in results)
def test_neu_suffix_in_drucksachenummer(self):
"""Drucksache-Nummern mit (neu)-Suffix muessen matchen."""
text = "Damit ist der Antrag Drucksache 18/4567(neu) angenommen."
results = find_results(text)
# Match irgendwo in den Results
assert any(r["drucksache"] == "18/4567(neu)" for r in results)
def test_results_sorted_by_position(self):
"""Mehrere Anchors muessen nach anchor_start aufsteigend sortiert sein."""
text = (
"Damit ist der Antrag Drucksache 18/100 angenommen. "
"Spaeter im Text. Damit ist der Antrag Drucksache 18/200 abgelehnt."
)
results = find_results(text)
positions = [r["anchor_start"] for r in results]
assert positions == sorted(positions)
def test_dedup_same_position(self):
"""Wenn zwei Patterns am selben anchor_start matchen, nur einer im Output."""
text = "Damit ist der Antrag Drucksache 18/300 angenommen."
results = find_results(text)
positions = [r["anchor_start"] for r in results]
assert len(positions) == len(set(positions))
class TestResolveDrucksacheForUeber:
def test_finds_nearest_ds_before_anchor(self):
text = "Drucksache 18/100 ... irgendein Text ... Damit ist das so beschlossen."
anchor_start = text.find("Damit")
ds = resolve_drucksache_for_ueber(text, anchor_start)
assert ds == "18/100"
def test_picks_closest_when_multiple(self):
"""Bei mehreren DS-Nrn vor dem Anchor wird die naechste gewaehlt."""
text = "Drucksache 18/100 ... Drucksache 18/200 ... Damit ist das so beschlossen."
anchor_start = text.find("Damit")
ds = resolve_drucksache_for_ueber(text, anchor_start)
assert ds == "18/200"
def test_returns_none_when_no_ds_before(self):
text = "Damit ist das so beschlossen. Drucksache 18/100 spaeter."
anchor_start = 0
ds = resolve_drucksache_for_ueber(text, anchor_start)
assert ds is None
class TestNormalizeText:
def test_collapses_whitespace(self):
"""Mehrfach-Whitespace wird zu einzelnem Leerzeichen kollabiert."""
result = normalize_text("Damit ist\nder\tAntrag")
assert " " not in result
def test_preserves_drucksache_format(self):
"""Drucksache-Schreibweise mit Slash muss erhalten bleiben."""
result = normalize_text("Drucksache 18/123")
assert "18/123" in result
class TestKnownFraktionsList:
def test_alle_fraktionen_nrw_complete(self):
"""ALLE_FRAKTIONEN_NRW deckt die WP18-Fraktionen ab (CDU, SPD, GRÜNE, FDP, AfD)."""
for f in ("CDU", "SPD", "GRÜNE", "FDP", "AfD"):
assert f in ALLE_FRAKTIONEN_NRW