159 lines
5.6 KiB
Python
159 lines
5.6 KiB
Python
|
|
"""Tests fuer app/protokoll_parsers/sh.py — SH Plenarprotokoll-Parser (#160).
|
|||
|
|
|
|||
|
|
Stichprobe-getestet gegen WP20 Sitzungen 115 + 116 (Schleswig-Holstein).
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from app.protokoll_parsers.sh import (
|
|||
|
|
_normalize_fraktionen_sh,
|
|||
|
|
_normalize_text,
|
|||
|
|
_parse_vote_block_sh,
|
|||
|
|
_resolve_drucksache_sh,
|
|||
|
|
RESULT_ANCHOR_RE,
|
|||
|
|
ALLE_FRAKTIONEN_SH,
|
|||
|
|
FRAKTIONEN_MAP_SH,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestNormalizeFraktionenSh:
|
|||
|
|
def test_simple_cdu(self):
|
|||
|
|
assert _normalize_fraktionen_sh("die CDU") == ["CDU"]
|
|||
|
|
|
|||
|
|
def test_buendnis_normalizes_to_gruene(self):
|
|||
|
|
assert _normalize_fraktionen_sh("BÜNDNIS 90/DIE GRÜNEN") == ["GRÜNE"]
|
|||
|
|
|
|||
|
|
def test_die_gruenen_normalizes(self):
|
|||
|
|
assert _normalize_fraktionen_sh("DIE GRÜNEN") == ["GRÜNE"]
|
|||
|
|
|
|||
|
|
def test_ssw(self):
|
|||
|
|
assert _normalize_fraktionen_sh("die SSW-Fraktion") == ["SSW"]
|
|||
|
|
|
|||
|
|
def test_combined_fraktionen(self):
|
|||
|
|
result = _normalize_fraktionen_sh(
|
|||
|
|
"die Fraktionen von SPD, FDP und SSW"
|
|||
|
|
)
|
|||
|
|
assert set(result) == {"SPD", "FDP", "SSW"}
|
|||
|
|
|
|||
|
|
def test_koalition_phrase(self):
|
|||
|
|
result = _normalize_fraktionen_sh(
|
|||
|
|
"die Fraktionen von CDU und BÜNDNIS 90/DIE GRÜNEN"
|
|||
|
|
)
|
|||
|
|
assert set(result) == {"CDU", "GRÜNE"}
|
|||
|
|
|
|||
|
|
def test_empty(self):
|
|||
|
|
assert _normalize_fraktionen_sh("") == []
|
|||
|
|
|
|||
|
|
def test_no_double_count_gruene(self):
|
|||
|
|
# 'BÜNDNIS 90/DIE GRÜNEN' und 'GRÜNE' beide getroffen → nur 1× GRÜNE
|
|||
|
|
result = _normalize_fraktionen_sh("BÜNDNIS 90/DIE GRÜNEN und GRÜNE")
|
|||
|
|
assert result.count("GRÜNE") == 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestNormalizeText:
|
|||
|
|
def test_collapses_whitespace(self):
|
|||
|
|
assert _normalize_text("a b\n\tc") == "a b c"
|
|||
|
|
|
|||
|
|
def test_repairs_soft_hyphenation(self):
|
|||
|
|
# Deutsche Silbentrennung am Zeilenumbruch
|
|||
|
|
assert _normalize_text("zustim- men") == "zustimmen"
|
|||
|
|
|
|||
|
|
def test_preserves_legitimate_hyphens(self):
|
|||
|
|
# Schulgeld-Kosten ist ein Kompositum, kein Trennstrich
|
|||
|
|
# → "Schulgeld-Kosten" mit Großbuchstaben nach Bindestrich → bleibt
|
|||
|
|
assert _normalize_text("Schulgeld-Kosten") == "Schulgeld-Kosten"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestParseVoteBlockSh:
|
|||
|
|
def test_complete_qa_block(self):
|
|||
|
|
block = (
|
|||
|
|
"Wer dem zustimmen will, den bitte ich um das Handzeichen. "
|
|||
|
|
"– Das sind die Fraktionen von SPD, FDP und SSW. "
|
|||
|
|
"Wer stimmt dagegen? – Das sind die Fraktionen von CDU "
|
|||
|
|
"und BÜNDNIS 90/DIE GRÜNEN. Damit ist der Antrag abgelehnt."
|
|||
|
|
)
|
|||
|
|
votes = _parse_vote_block_sh(block)
|
|||
|
|
assert set(votes["ja"]) == {"SPD", "FDP", "SSW"}
|
|||
|
|
assert set(votes["nein"]) == {"CDU", "GRÜNE"}
|
|||
|
|
assert votes["enthaltung"] == []
|
|||
|
|
|
|||
|
|
def test_block_with_enthaltung(self):
|
|||
|
|
block = (
|
|||
|
|
"Wer dem zustimmen will, den bitte ich um das Handzeichen. "
|
|||
|
|
"– Das sind die Fraktionen von FDP und SSW. "
|
|||
|
|
"Wer stimmt dagegen? – Das ist die CDU-Fraktion. "
|
|||
|
|
"Wer enthält sich? – Das sind die Fraktionen von "
|
|||
|
|
"BÜNDNIS 90/DIE GRÜNEN und SPD. Damit ist der Antrag abgelehnt."
|
|||
|
|
)
|
|||
|
|
votes = _parse_vote_block_sh(block)
|
|||
|
|
assert set(votes["ja"]) == {"FDP", "SSW"}
|
|||
|
|
assert set(votes["nein"]) == {"CDU"}
|
|||
|
|
assert set(votes["enthaltung"]) == {"GRÜNE", "SPD"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestResolveDrucksacheSh:
|
|||
|
|
def test_finds_drucksache_before_anchor(self):
|
|||
|
|
text = (
|
|||
|
|
"Drucksache 20/1234 ... Wer dem zustimmen will. – Das ist die SPD. "
|
|||
|
|
"Damit ist der Antrag abgelehnt."
|
|||
|
|
)
|
|||
|
|
anchor = text.index("Damit")
|
|||
|
|
assert _resolve_drucksache_sh(text, anchor) == "20/1234"
|
|||
|
|
|
|||
|
|
def test_picks_most_recent_drucksache(self):
|
|||
|
|
text = (
|
|||
|
|
"Drucksache 20/1000 ... Drucksache 20/2000 wird abgestimmt. "
|
|||
|
|
"Damit ist der Antrag abgelehnt."
|
|||
|
|
)
|
|||
|
|
anchor = text.index("Damit")
|
|||
|
|
assert _resolve_drucksache_sh(text, anchor) == "20/2000"
|
|||
|
|
|
|||
|
|
def test_returns_none_when_no_ds(self):
|
|||
|
|
assert _resolve_drucksache_sh("Damit ist abgelehnt.", 0) is None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestResultAnchorRegex:
|
|||
|
|
def test_matches_antrag_abgelehnt(self):
|
|||
|
|
m = RESULT_ANCHOR_RE.search("Damit ist der Antrag abgelehnt.")
|
|||
|
|
assert m and m.group("ergebnis") == "abgelehnt"
|
|||
|
|
|
|||
|
|
def test_matches_mehrheitlich_angenommen(self):
|
|||
|
|
m = RESULT_ANCHOR_RE.search(
|
|||
|
|
"Damit ist der Antrag mehrheitlich angenommen."
|
|||
|
|
)
|
|||
|
|
assert m and m.group("modus") == "mehrheitlich"
|
|||
|
|
assert m.group("ergebnis") == "angenommen"
|
|||
|
|
|
|||
|
|
def test_matches_trotzdem_mit_mehrheit(self):
|
|||
|
|
m = RESULT_ANCHOR_RE.search(
|
|||
|
|
"Damit ist der Antrag trotzdem mit Mehrheit angenommen."
|
|||
|
|
)
|
|||
|
|
assert m and m.group("ergebnis") == "angenommen"
|
|||
|
|
|
|||
|
|
def test_matches_ausschussueberweisung_einstimmig(self):
|
|||
|
|
m = RESULT_ANCHOR_RE.search(
|
|||
|
|
"Damit ist die Ausschussüberweisung einstimmig so beschlossen."
|
|||
|
|
)
|
|||
|
|
assert m
|
|||
|
|
assert m.group("modus") == "einstimmig"
|
|||
|
|
assert "beschlossen" in m.group("ergebnis").lower()
|
|||
|
|
|
|||
|
|
def test_no_match_random_text(self):
|
|||
|
|
m = RESULT_ANCHOR_RE.search("Der Antrag wurde abgelehnt.")
|
|||
|
|
# 'Damit ist' fehlt
|
|||
|
|
assert m is None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestConstants:
|
|||
|
|
def test_all_fraktionen_complete(self):
|
|||
|
|
# WP20-SH: CDU+GRÜNE Koalition, SPD+FDP+SSW Opposition
|
|||
|
|
assert set(ALLE_FRAKTIONEN_SH) == {"CDU", "GRÜNE", "SPD", "FDP", "SSW"}
|
|||
|
|
|
|||
|
|
def test_mapping_covers_all_fraktionen(self):
|
|||
|
|
all_codes = set()
|
|||
|
|
for _phrase, codes in FRAKTIONEN_MAP_SH:
|
|||
|
|
all_codes.update(codes)
|
|||
|
|
for f in ALLE_FRAKTIONEN_SH:
|
|||
|
|
assert f in all_codes, f"Fraktion {f} fehlt im FRAKTIONEN_MAP_SH"
|