"""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"