"""Tests fuer app/protokoll_parsers/bund.py — Bundestags-Plenarprotokoll-Parser (#148). Stichprobe-getestet gegen WP20 Sitzung 30 + 100 (XML aus dserver.bundestag.de). Pure-string-Tests fuer Vote-Block-Parsing, Anchor-Detection, Fraktions-Mapping. """ from __future__ import annotations import pytest from app.protokoll_parsers.bund import ( _normalize_fraktionen_bt, _parse_vote_block_bt, _resolve_drucksache_bt, RESULT_ANCHOR_RE, parse_protocol, WP20_KOALITIONSFRAKTIONEN, WP20_OPPOSITIONSFRAKTIONEN, ALL_BT_FRAKTIONEN, ) class TestNormalizeFraktionenBt: def test_simple_spd(self): assert _normalize_fraktionen_bt("SPD-Fraktion") == ["SPD"] def test_cdu_csu(self): assert _normalize_fraktionen_bt("CDU/CSU-Fraktion") == ["CDU/CSU"] def test_buendnis_90_normalizes_to_gruene(self): result = _normalize_fraktionen_bt("Fraktion Bündnis 90/Die Grünen") assert result == ["GRÜNE"] def test_koalitionsfraktionen_expands_wp20(self): """In WP20: Koalition = SPD + GRÜNE + FDP.""" result = _normalize_fraktionen_bt("der Koalitionsfraktionen") assert set(result) == set(WP20_KOALITIONSFRAKTIONEN) def test_oppositionsfraktionen_expands_wp20(self): result = _normalize_fraktionen_bt("der Oppositionsfraktionen") assert set(result) == set(WP20_OPPOSITIONSFRAKTIONEN) def test_combined_phrase(self): """'Koalitionsfraktionen und der Fraktion Die Linke' → SPD+GRÜNE+FDP+LINKE.""" result = _normalize_fraktionen_bt( "der Koalitionsfraktionen und der Fraktion Die Linke" ) assert set(result) == {"SPD", "GRÜNE", "FDP", "LINKE"} def test_empty_returns_empty(self): assert _normalize_fraktionen_bt("") == [] def test_no_double_count(self): """SPD darf in 'SPD-Fraktion' nicht zweimal gezaehlt werden.""" result = _normalize_fraktionen_bt("der SPD-Fraktion und der SPD") assert result.count("SPD") == 1 class TestParseVoteBlockBt: def test_full_block_with_all_three_kinds(self): block = ( " der Koalitionsfraktionen und der Fraktion Die Linke " "gegen die Stimmen der CDU/CSU-Fraktion " "bei Enthaltung der AfD-Fraktion" ) votes = _parse_vote_block_bt(block) assert set(votes["ja"]) == {"SPD", "GRÜNE", "FDP", "LINKE"} assert votes["nein"] == ["CDU/CSU"] assert votes["enthaltung"] == ["AfD"] def test_only_ja_and_nein(self): block = ( " der SPD-Fraktion, der Fraktion Bündnis 90/Die Grünen, der FDP-Fraktion, " "der CDU/CSU-Fraktion und der Fraktion Die Linke " "gegen die Stimmen der AfD-Fraktion" ) votes = _parse_vote_block_bt(block) assert "AfD" not in votes["ja"] assert votes["nein"] == ["AfD"] assert votes["enthaltung"] == [] assert set(votes["ja"]) == {"SPD", "GRÜNE", "FDP", "CDU/CSU", "LINKE"} def test_only_ja(self): block = " der Koalitionsfraktionen" votes = _parse_vote_block_bt(block) assert set(votes["ja"]) == set(WP20_KOALITIONSFRAKTIONEN) assert votes["nein"] == [] assert votes["enthaltung"] == [] class TestResolveDrucksacheBt: def test_finds_nearest_ds_before(self): text = ( "Drucksache 20/100 ... irgendwas ... " "Die Beschlussempfehlung ist mit den Stimmen ..." ) anchor = text.index("Die Beschlussempfehlung") assert _resolve_drucksache_bt(text, anchor) == "20/100" def test_picks_closest_when_multiple(self): """Bei mehreren DS-Nrn vor dem Anchor wird die naechste gewaehlt.""" text = ( "Drucksache 20/100 ... Drucksache 20/200 ... " "Die Beschlussempfehlung ..." ) anchor = text.index("Die Beschlussempfehlung") assert _resolve_drucksache_bt(text, anchor) == "20/200" def test_returns_none_when_no_ds(self): text = "Die Beschlussempfehlung ist mit den Stimmen ..." anchor = 0 assert _resolve_drucksache_bt(text, anchor) is None def test_neu_suffix_supported(self): text = "auf Drucksache 20/4567(neu) ... Die Beschlussempfehlung ..." anchor = text.index("Die Beschlussempfehlung") assert _resolve_drucksache_bt(text, anchor) == "20/4567(neu)" class TestResultAnchorRegex: def test_matches_beschlussempfehlung_angenommen(self): text = ( "Die Beschlussempfehlung ist mit den Stimmen der Koalitionsfraktionen " "gegen die Stimmen der CDU/CSU-Fraktion angenommen." ) m = RESULT_ANCHOR_RE.search(text) assert m assert m.group("subject") == "Die Beschlussempfehlung" assert m.group("ergebnis") == "angenommen" def test_matches_ueberweisungsvorschlag_abgelehnt(self): text = ( "Der Überweisungsvorschlag ist mit den Stimmen der Koalitionsfraktionen " "gegen die Stimmen der AfD-Fraktion abgelehnt." ) m = RESULT_ANCHOR_RE.search(text) assert m assert m.group("ergebnis") == "abgelehnt" def test_no_match_in_speech(self): """'angenommen' in einer Rede (ohne mit-den-Stimmen-Form) darf nicht matchen.""" text = "Wir haben das Angebot angenommen, weil das Geld gut angelegt ist." assert RESULT_ANCHOR_RE.search(text) is None class TestParseProtocolEndToEnd: """Integration-light: parsen ein Mock-XML mit BT-typischen Beschluessen.""" def _write_xml(self, tmp_path, body_text): xml_path = tmp_path / "test.xml" # Minimal-XML, alles in einem
xml_path.write_text(
f''
f' {body_text}