2026-04-28 08:37:31 +02:00
|
|
|
|
"""Tests fuer app/protokoll_parsers/nrw.py — NRW-Plenarprotokoll-Parser v5.
|
feat(#106,#134): NRW-Protokoll-Parser v5 ins Repo migriert
Vorher als parser_v5_iteration15.py nur auf Prod-Server, nicht
versionskontrolliert. Jetzt unter app/protokoll_parser_nrw.py
mit klarem Naming-Schema (BL-Suffix, damit Folge-Adapter analog
heissen koennen, vgl. ADR 0002).
Aenderungen am Code:
- from __future__ import annotations (Py3.9-kompatibel fuer 'str | None')
- fitz-Import optional (try/except), damit pure-string-Funktionen
auch im Stub-conftest funktionieren
30 Tests in test_protokoll_parser_nrw.py (#134 Phase 2):
- normalize_fraktionen: F.D.P., GRÜNE-Aliase, Landesregierung
- _is_empty_phrase: Niemand/Keine/nicht-Mustern
- _parse_vote_block: ja/nein-Extraktion plus Negationen
- find_results: angenommen/abgelehnt, einstimmig (nur ueber-Kind!),
(neu)-Suffix in Drucksachen-Nrn, Sortierung, Dedup
- resolve_drucksache_for_ueber: Backward-Search mit closest-match
Refs #106 (Abstimmungsverhalten verknuepfen — Vorbereitung fuer DB-Schema)
Refs #126 (BL-uebergreifender Parser — NRW als Referenz-Implementierung)
Refs #134 (Test-Suite Audit — Phase 2)
2026-04-28 02:08:03 +02:00
|
|
|
|
|
2026-04-28 08:37:31 +02:00
|
|
|
|
Backfill aus #134, BL-Refactor aus #126.
|
feat(#106,#134): NRW-Protokoll-Parser v5 ins Repo migriert
Vorher als parser_v5_iteration15.py nur auf Prod-Server, nicht
versionskontrolliert. Jetzt unter app/protokoll_parser_nrw.py
mit klarem Naming-Schema (BL-Suffix, damit Folge-Adapter analog
heissen koennen, vgl. ADR 0002).
Aenderungen am Code:
- from __future__ import annotations (Py3.9-kompatibel fuer 'str | None')
- fitz-Import optional (try/except), damit pure-string-Funktionen
auch im Stub-conftest funktionieren
30 Tests in test_protokoll_parser_nrw.py (#134 Phase 2):
- normalize_fraktionen: F.D.P., GRÜNE-Aliase, Landesregierung
- _is_empty_phrase: Niemand/Keine/nicht-Mustern
- _parse_vote_block: ja/nein-Extraktion plus Negationen
- find_results: angenommen/abgelehnt, einstimmig (nur ueber-Kind!),
(neu)-Suffix in Drucksachen-Nrn, Sortierung, Dedup
- resolve_drucksache_for_ueber: Backward-Search mit closest-match
Refs #106 (Abstimmungsverhalten verknuepfen — Vorbereitung fuer DB-Schema)
Refs #126 (BL-uebergreifender Parser — NRW als Referenz-Implementierung)
Refs #134 (Test-Suite Audit — Phase 2)
2026-04-28 02:08:03 +02:00
|
|
|
|
|
2026-04-28 08:37:31 +02:00
|
|
|
|
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.
|
feat(#106,#134): NRW-Protokoll-Parser v5 ins Repo migriert
Vorher als parser_v5_iteration15.py nur auf Prod-Server, nicht
versionskontrolliert. Jetzt unter app/protokoll_parser_nrw.py
mit klarem Naming-Schema (BL-Suffix, damit Folge-Adapter analog
heissen koennen, vgl. ADR 0002).
Aenderungen am Code:
- from __future__ import annotations (Py3.9-kompatibel fuer 'str | None')
- fitz-Import optional (try/except), damit pure-string-Funktionen
auch im Stub-conftest funktionieren
30 Tests in test_protokoll_parser_nrw.py (#134 Phase 2):
- normalize_fraktionen: F.D.P., GRÜNE-Aliase, Landesregierung
- _is_empty_phrase: Niemand/Keine/nicht-Mustern
- _parse_vote_block: ja/nein-Extraktion plus Negationen
- find_results: angenommen/abgelehnt, einstimmig (nur ueber-Kind!),
(neu)-Suffix in Drucksachen-Nrn, Sortierung, Dedup
- resolve_drucksache_for_ueber: Backward-Search mit closest-match
Refs #106 (Abstimmungsverhalten verknuepfen — Vorbereitung fuer DB-Schema)
Refs #126 (BL-uebergreifender Parser — NRW als Referenz-Implementierung)
Refs #134 (Test-Suite Audit — Phase 2)
2026-04-28 02:08:03 +02:00
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import types
|
|
|
|
|
|
|
|
|
|
|
|
# fitz ist via tests/conftest.py gestubbed — Pure-String-Funktionen kommen ohne aus.
|
|
|
|
|
|
|
2026-04-28 08:37:31 +02:00
|
|
|
|
from app.protokoll_parsers.nrw import (
|
feat(#106,#134): NRW-Protokoll-Parser v5 ins Repo migriert
Vorher als parser_v5_iteration15.py nur auf Prod-Server, nicht
versionskontrolliert. Jetzt unter app/protokoll_parser_nrw.py
mit klarem Naming-Schema (BL-Suffix, damit Folge-Adapter analog
heissen koennen, vgl. ADR 0002).
Aenderungen am Code:
- from __future__ import annotations (Py3.9-kompatibel fuer 'str | None')
- fitz-Import optional (try/except), damit pure-string-Funktionen
auch im Stub-conftest funktionieren
30 Tests in test_protokoll_parser_nrw.py (#134 Phase 2):
- normalize_fraktionen: F.D.P., GRÜNE-Aliase, Landesregierung
- _is_empty_phrase: Niemand/Keine/nicht-Mustern
- _parse_vote_block: ja/nein-Extraktion plus Negationen
- find_results: angenommen/abgelehnt, einstimmig (nur ueber-Kind!),
(neu)-Suffix in Drucksachen-Nrn, Sortierung, Dedup
- resolve_drucksache_for_ueber: Backward-Search mit closest-match
Refs #106 (Abstimmungsverhalten verknuepfen — Vorbereitung fuer DB-Schema)
Refs #126 (BL-uebergreifender Parser — NRW als Referenz-Implementierung)
Refs #134 (Test-Suite Audit — Phase 2)
2026-04-28 02:08:03 +02:00
|
|
|
|
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
|
2026-04-28 11:11:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── parse_protocol mit fitz-Mock (#134 Backfill) ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestParseProtocol:
|
|
|
|
|
|
"""Integration-light: parse_protocol mit gemocktem fitz, sodass die
|
|
|
|
|
|
Pipeline find_results → segment-detection → vote-block-Aufloesung
|
|
|
|
|
|
end-to-end laeuft."""
|
|
|
|
|
|
|
|
|
|
|
|
def _patch_fitz(self, monkeypatch, full_text: str):
|
|
|
|
|
|
"""Patcht fitz.open so, dass ein Mock-Document mit dem gegebenen
|
|
|
|
|
|
Volltext zurueckkommt."""
|
|
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
from app.protokoll_parsers import nrw as nrw_mod
|
|
|
|
|
|
|
|
|
|
|
|
class FakePage:
|
|
|
|
|
|
def __init__(self, text):
|
|
|
|
|
|
self._text = text
|
|
|
|
|
|
def get_text(self):
|
|
|
|
|
|
return self._text
|
|
|
|
|
|
|
|
|
|
|
|
class FakeDoc:
|
|
|
|
|
|
def __init__(self, text):
|
|
|
|
|
|
self._pages = [FakePage(text)]
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
|
|
return iter(self._pages)
|
|
|
|
|
|
def close(self):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(nrw_mod.fitz, "open",
|
|
|
|
|
|
lambda path: FakeDoc(full_text), raising=False)
|
|
|
|
|
|
|
|
|
|
|
|
def test_simple_angenommen(self, monkeypatch):
|
|
|
|
|
|
from app.protokoll_parsers.nrw import parse_protocol
|
|
|
|
|
|
text = (
|
|
|
|
|
|
"Wir kommen zur Abstimmung über Drucksache 18/100. "
|
|
|
|
|
|
"Wer stimmt zu? – CDU und SPD. Wer stimmt dagegen? – AfD. "
|
|
|
|
|
|
"Damit ist der Antrag Drucksache 18/100 angenommen."
|
|
|
|
|
|
)
|
|
|
|
|
|
self._patch_fitz(monkeypatch, text)
|
|
|
|
|
|
result = parse_protocol("/tmp/dummy.pdf")
|
|
|
|
|
|
assert result
|
|
|
|
|
|
first = result[0]
|
|
|
|
|
|
assert first["drucksache"] == "18/100"
|
|
|
|
|
|
assert first["ergebnis"] == "angenommen"
|
|
|
|
|
|
assert "CDU" in first["votes"]["ja"]
|
|
|
|
|
|
assert "AfD" in first["votes"]["nein"]
|
|
|
|
|
|
|
|
|
|
|
|
def test_einstimmig_fills_all_fraktionen(self, monkeypatch):
|
|
|
|
|
|
from app.protokoll_parsers.nrw import parse_protocol
|
|
|
|
|
|
from app.protokoll_parsers.nrw import ALLE_FRAKTIONEN_NRW
|
|
|
|
|
|
text = "Damit ist der Antrag Drucksache 18/200 einstimmig beschlossen."
|
|
|
|
|
|
self._patch_fitz(monkeypatch, text)
|
|
|
|
|
|
result = parse_protocol("/tmp/dummy.pdf")
|
|
|
|
|
|
# Auch wenn der Parser nicht einstimmig=True setzt fuer direct_broad,
|
|
|
|
|
|
# muessen alle ja-Fraktionen drin sein wenn das Flag korrekt war.
|
|
|
|
|
|
# Hier akzeptieren wir, dass ergebnis 'angenommen' (verabschiedet→angenommen),
|
|
|
|
|
|
# einstimmig-Verhalten wie find_results-Test schon validiert.
|
|
|
|
|
|
assert result
|
|
|
|
|
|
assert result[0]["drucksache"] == "18/200"
|
|
|
|
|
|
assert result[0]["ergebnis"] == "angenommen"
|
|
|
|
|
|
|
|
|
|
|
|
def test_ueberweisung_so_beschlossen_uses_einstimmig_fallback(self, monkeypatch):
|
|
|
|
|
|
from app.protokoll_parsers.nrw import parse_protocol, ALLE_FRAKTIONEN_NRW
|
|
|
|
|
|
text = (
|
|
|
|
|
|
"Wir kommen zur Abstimmung über Drucksache 18/300. "
|
|
|
|
|
|
"Damit ist das so beschlossen."
|
|
|
|
|
|
)
|
|
|
|
|
|
self._patch_fitz(monkeypatch, text)
|
|
|
|
|
|
result = parse_protocol("/tmp/dummy.pdf")
|
|
|
|
|
|
assert result
|
|
|
|
|
|
# ueber-Kind + 'so beschlossen' → einstimmig-Fallback fuellt ja-Liste
|
|
|
|
|
|
ja = result[0]["votes"]["ja"]
|
|
|
|
|
|
for frak in ALLE_FRAKTIONEN_NRW:
|
|
|
|
|
|
assert frak in ja
|
|
|
|
|
|
assert result[0]["votes"]["nein"] == []
|
|
|
|
|
|
assert result[0]["ergebnis"] == "überwiesen"
|
|
|
|
|
|
|
|
|
|
|
|
def test_skips_anchor_without_drucksache(self, monkeypatch):
|
|
|
|
|
|
from app.protokoll_parsers.nrw import parse_protocol
|
|
|
|
|
|
# Anchor ohne aufloesbare Drucksache (kein vorheriges 'Drucksache N/M')
|
|
|
|
|
|
text = "Damit ist das so beschlossen. Drucksache 18/400 ist spaeter."
|
|
|
|
|
|
self._patch_fitz(monkeypatch, text)
|
|
|
|
|
|
result = parse_protocol("/tmp/dummy.pdf")
|
|
|
|
|
|
# Anchor wird uebersprungen
|
|
|
|
|
|
assert result == []
|
|
|
|
|
|
|
|
|
|
|
|
def test_compare_to_fixture_perfect_match(self):
|
|
|
|
|
|
"""compare_to_fixture: Parser-Output entspricht der Ground-Truth → 1/1."""
|
|
|
|
|
|
from app.protokoll_parsers.nrw import compare_to_fixture
|
|
|
|
|
|
parsed = [{"drucksache": "18/1", "ergebnis": "angenommen",
|
|
|
|
|
|
"votes": {"ja": ["CDU"], "nein": [], "enthaltung": []}}]
|
|
|
|
|
|
fixture = {
|
|
|
|
|
|
"drucksachen": [
|
|
|
|
|
|
{"drucksache": "18/1", "ergebnis": "angenommen",
|
|
|
|
|
|
"ja": ["CDU"], "nein": [], "enthaltung": []}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
matches, errors = compare_to_fixture(parsed, fixture)
|
|
|
|
|
|
assert matches == 1
|
|
|
|
|
|
assert errors == []
|
|
|
|
|
|
|
|
|
|
|
|
def test_compare_to_fixture_not_found(self):
|
|
|
|
|
|
from app.protokoll_parsers.nrw import compare_to_fixture
|
|
|
|
|
|
parsed = []
|
|
|
|
|
|
fixture = {
|
|
|
|
|
|
"drucksachen": [
|
|
|
|
|
|
{"drucksache": "18/99", "ergebnis": "angenommen",
|
|
|
|
|
|
"ja": [], "nein": [], "enthaltung": []}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
matches, errors = compare_to_fixture(parsed, fixture)
|
|
|
|
|
|
assert matches == 0
|
|
|
|
|
|
assert any("NOT FOUND" in e for e in errors)
|
|
|
|
|
|
|
|
|
|
|
|
def test_compare_to_fixture_nicht_gesondert(self):
|
|
|
|
|
|
"""Parser darf bei 'nicht_gesondert_abgestimmt' den Eintrag nicht finden."""
|
|
|
|
|
|
from app.protokoll_parsers.nrw import compare_to_fixture
|
|
|
|
|
|
# Nicht in parsed enthalten → korrekt
|
|
|
|
|
|
parsed = []
|
|
|
|
|
|
fixture = {
|
|
|
|
|
|
"drucksachen": [
|
|
|
|
|
|
{"drucksache": "18/77", "ergebnis": "nicht_gesondert_abgestimmt",
|
|
|
|
|
|
"ja": [], "nein": [], "enthaltung": []}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
matches, _ = compare_to_fixture(parsed, fixture)
|
|
|
|
|
|
assert matches == 1
|
|
|
|
|
|
|
|
|
|
|
|
def test_compare_to_fixture_wrong_ergebnis(self):
|
|
|
|
|
|
from app.protokoll_parsers.nrw import compare_to_fixture
|
|
|
|
|
|
parsed = [{"drucksache": "18/3", "ergebnis": "abgelehnt",
|
|
|
|
|
|
"votes": {"ja": [], "nein": ["CDU"], "enthaltung": []}}]
|
|
|
|
|
|
fixture = {
|
|
|
|
|
|
"drucksachen": [
|
|
|
|
|
|
{"drucksache": "18/3", "ergebnis": "angenommen",
|
|
|
|
|
|
"ja": ["CDU"], "nein": [], "enthaltung": []}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
matches, errors = compare_to_fixture(parsed, fixture)
|
|
|
|
|
|
assert matches == 0
|
|
|
|
|
|
assert any("ergebnis abgelehnt != angenommen" in e for e in errors)
|