gwoe-antragspruefer/tests/test_protokoll_parsers_sl.py
Dotty Dotter d0f7b9217c feat(#161): SL-Parser produktiv — Saarland HTML-Abstimmungsergebnisse
Saarland publiziert keine Wortprotokolle, sondern eigene HTML-Seiten
mit strukturierten Abstimmungsergebnissen pro Sitzung:

  <p>Drucksache 17/2076 ... in Erster Lesung mit Stimmenmehrheit
  angenommen ... [SPD: dafür; CDU und AfD: dagegen]</p>

Daher Input ist HTML, nicht PDF. Parser nutzt LI-Block-Iteration und
extrahiert pro Block:
- Drucksache aus "Drucksache N/M"
- Status aus "(einstimmig|mit Stimmenmehrheit)? (angenommen|abgelehnt)"
- Vote-Block aus "[SPD: dafür; CDU: dagegen; AfD: Enthaltung]"
- einstimmig=True falls Status enthaelt "einstimmig"

Vote-Bracket-Parser (eigenstaendig vs. Reden-Stil-Parser anderer BL):
- Splits per ; → "Phrase: Status"
- Phrase per Wortgrenzen-Regex auf {SPD,CDU,AfD} matchen
- Status-Map: dafür→ja, dagegen→nein, Enthaltung→enthaltung

URL-Pattern (nicht direkt vorhersagbar wegen Datums-Slug):
https://www.landtag-saar.de/aktuelles/mitteilungen/abstimmungsergebnisse-der-{n}-landtagssitzung-vom-{datum}/

Auto-Ingest via Index-Scrape (analog HH/HE/SH):
- /aktuelles/mitteilungen/ scrape
- WP16-URLs (mit "wahlperiode-vom") ueberspringen
- Pro neue Sitzung: HTML herunterladen, ingest_pdf-API auf .html-Datei

Tests: 18 SL-Tests (Verifikation Sitzung 46 → 18 Votes mit korrekten
JA/NEIN/ENTH-Listen). Stand: 9 produktive Parser
(NRW, BUND, BE, HH, TH, HE, SH, HB, SL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:53:51 +02:00

114 lines
3.6 KiB
Python

"""Tests fuer app/protokoll_parsers/sl.py — SL Abstimmungsergebnisse-Parser (#161).
SL ist HTML-basiert (nicht PDF) — eigene Abstimmungsergebnisse-Seite pro
Sitzung mit strukturiertem [SPD: dafür; CDU und AfD: dagegen]-Format.
"""
from __future__ import annotations
import pytest
from app.protokoll_parsers.sl import (
_normalize_fraktionen_sl,
_parse_vote_bracket,
_strip_html,
DS_RE_SL,
STATUS_RE,
VOTE_BRACKET_RE,
LI_BLOCK_RE,
ALLE_FRAKTIONEN_SL,
)
class TestNormalizeFraktionenSl:
def test_simple_spd(self):
assert _normalize_fraktionen_sl("SPD") == ["SPD"]
def test_spd_und_cdu(self):
assert _normalize_fraktionen_sl("SPD und CDU") == ["CDU", "SPD"]
def test_alle_drei(self):
assert _normalize_fraktionen_sl("SPD, CDU und AfD") == ["AfD", "CDU", "SPD"]
def test_empty(self):
assert _normalize_fraktionen_sl("") == []
def test_word_boundary(self):
# Kein Match auf 'SP' oder Substrings
assert _normalize_fraktionen_sl("SP-Partei") == []
class TestParseVoteBracket:
def test_klassisches_pattern(self):
votes = _parse_vote_bracket("SPD: dafür; CDU und AfD: dagegen")
assert set(votes["ja"]) == {"SPD"}
assert set(votes["nein"]) == {"CDU", "AfD"}
assert votes["enthaltung"] == []
def test_mit_enthaltung(self):
votes = _parse_vote_bracket("SPD: dafür; CDU: dagegen; AfD: Enthaltung")
assert set(votes["ja"]) == {"SPD"}
assert set(votes["nein"]) == {"CDU"}
assert set(votes["enthaltung"]) == {"AfD"}
def test_alle_dafuer(self):
votes = _parse_vote_bracket("SPD und CDU: dafür; AfD: Enthaltung")
assert set(votes["ja"]) == {"SPD", "CDU"}
assert votes["nein"] == []
assert set(votes["enthaltung"]) == {"AfD"}
def test_alle_dagegen(self):
votes = _parse_vote_bracket("AfD: dafür; SPD und CDU: dagegen")
assert set(votes["ja"]) == {"AfD"}
assert set(votes["nein"]) == {"SPD", "CDU"}
class TestStripHtml:
def test_removes_tags(self):
assert _strip_html("<p>Hello <em>world</em></p>") == "Hello world"
def test_decodes_entities(self):
assert _strip_html("a &amp; b") == "a & b"
class TestDrucksacheRegex:
def test_matches_drucksache(self):
m = DS_RE_SL.search("Drucksache 17/2076")
assert m and m.group(1) == "17/2076"
def test_matches_with_spaces(self):
m = DS_RE_SL.search("(Drucksache 17/2074)")
assert m and m.group(1) == "17/2074"
class TestStatusRegex:
def test_matches_einstimmig_angenommen(self):
m = STATUS_RE.search("in Erster Lesung einstimmig angenommen")
assert m and m.group("ergebnis").lower() == "angenommen"
def test_matches_mit_stimmenmehrheit(self):
m = STATUS_RE.search("mit Stimmenmehrheit angenommen")
assert m and m.group("ergebnis").lower() == "angenommen"
def test_matches_abgelehnt(self):
m = STATUS_RE.search("in Erster Lesung abgelehnt")
assert m and m.group("ergebnis").lower() == "abgelehnt"
class TestVoteBracketRegex:
def test_matches_full_bracket(self):
m = VOTE_BRACKET_RE.search("[SPD: dafür; CDU und AfD: dagegen]")
assert m and "SPD" in m.group("inner")
class TestLiBlockRegex:
def test_extracts_li_content(self):
html = "<ul><li>foo</li><li>bar</li></ul>"
matches = list(LI_BLOCK_RE.finditer(html))
assert len(matches) == 2
assert matches[0].group(1) == "foo"
class TestConstants:
def test_all_fraktionen_set(self):
assert ALLE_FRAKTIONEN_SL == ["SPD", "CDU", "AfD"]