402 lines
19 KiB
Python
402 lines
19 KiB
Python
|
|
"""Tests fuer app.auswertungen.aggregate_stimm_index/heuchelei/pro_wert/cross_bl.
|
||
|
|
|
||
|
|
Verifiziert die JOIN-Aggregations-Logik gegen eine in-memory SQLite-DB
|
||
|
|
mit kontrollierten Sample-Assessments und Sample-Vote-Results.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sqlite3
|
||
|
|
from datetime import datetime
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.auswertungen import (
|
||
|
|
aggregate_heuchelei,
|
||
|
|
aggregate_stimm_index,
|
||
|
|
aggregate_stimm_index_cross_bl,
|
||
|
|
aggregate_stimm_index_pro_wert,
|
||
|
|
_wert_score_for_assessment,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
# Fixture: assessments + plenum_vote_results
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
def _matrix(*ratings: tuple[str, int]) -> str:
|
||
|
|
"""Build a gwoe_matrix JSON-Array. Args: (field, rating) tuples."""
|
||
|
|
return json.dumps([
|
||
|
|
{"field": f, "label": f, "aspect": f[-1], "rating": r,
|
||
|
|
"symbol": "++" if r >= 4 else ("+" if r >= 1 else "○")}
|
||
|
|
for f, r in ratings
|
||
|
|
])
|
||
|
|
|
||
|
|
|
||
|
|
def _wp_scores(*items: tuple[str, int, bool]) -> str:
|
||
|
|
"""Build a wahlprogramm_scores JSON-Array. Args: (fraktion, score, is_antrag)."""
|
||
|
|
return json.dumps([
|
||
|
|
{
|
||
|
|
"fraktion": fr,
|
||
|
|
"ist_antragsteller": ist,
|
||
|
|
"wahlprogramm": {"score": sc, "begründung": "stub"},
|
||
|
|
}
|
||
|
|
for fr, sc, ist in items
|
||
|
|
])
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def sample_db(tmp_path: Path) -> Path:
|
||
|
|
"""Lege eine Mini-DB mit assessments + plenum_vote_results an, die typische
|
||
|
|
Faelle abdeckt:
|
||
|
|
|
||
|
|
- NRW Antrag mit hohem GWOe-Score, GRÜNE/SPD ja, CDU/AfD nein
|
||
|
|
- NRW Antrag mit niedrigem GWOe-Score, AfD ja, GRÜNE nein (Anti-Pattern)
|
||
|
|
- NRW Antrag mit hohem Score, CDU Antragsteller (sollte bei Default
|
||
|
|
excluded werden)
|
||
|
|
- MV Anträge fuer Cross-BL-Test
|
||
|
|
- Heuchelei-Sample: GRÜNE-Antrag mit GRÜNE-wahlprogramm_score=9 aber
|
||
|
|
GRÜNE stimmt NEIN (synthetisch konstruiert)
|
||
|
|
"""
|
||
|
|
db = tmp_path / "test_stimmverhalten.db"
|
||
|
|
conn = sqlite3.connect(str(db))
|
||
|
|
conn.execute("""
|
||
|
|
CREATE TABLE assessments (
|
||
|
|
drucksache TEXT PRIMARY KEY,
|
||
|
|
title TEXT,
|
||
|
|
fraktionen TEXT,
|
||
|
|
datum TEXT,
|
||
|
|
bundesland TEXT,
|
||
|
|
gwoe_score REAL,
|
||
|
|
link TEXT,
|
||
|
|
gwoe_begruendung TEXT,
|
||
|
|
gwoe_matrix TEXT,
|
||
|
|
gwoe_schwerpunkt TEXT,
|
||
|
|
wahlprogramm_scores TEXT,
|
||
|
|
verbesserungen TEXT,
|
||
|
|
staerken TEXT,
|
||
|
|
schwaechen TEXT,
|
||
|
|
empfehlung TEXT,
|
||
|
|
empfehlung_symbol TEXT,
|
||
|
|
verbesserungspotenzial TEXT,
|
||
|
|
themen TEXT,
|
||
|
|
antrag_zusammenfassung TEXT,
|
||
|
|
antrag_kernpunkte TEXT,
|
||
|
|
source TEXT,
|
||
|
|
model TEXT,
|
||
|
|
created_at TEXT,
|
||
|
|
updated_at TEXT
|
||
|
|
)
|
||
|
|
""")
|
||
|
|
conn.execute("""
|
||
|
|
CREATE TABLE plenum_vote_results (
|
||
|
|
bundesland TEXT NOT NULL,
|
||
|
|
drucksache TEXT NOT NULL,
|
||
|
|
ergebnis TEXT NOT NULL,
|
||
|
|
einstimmig INTEGER NOT NULL DEFAULT 0,
|
||
|
|
fraktionen_ja TEXT NOT NULL DEFAULT '[]',
|
||
|
|
fraktionen_nein TEXT NOT NULL DEFAULT '[]',
|
||
|
|
fraktionen_enthaltung TEXT NOT NULL DEFAULT '[]',
|
||
|
|
quelle_protokoll TEXT NOT NULL,
|
||
|
|
quelle_url TEXT,
|
||
|
|
parsed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
|
|
PRIMARY KEY (bundesland, drucksache, quelle_protokoll)
|
||
|
|
)
|
||
|
|
""")
|
||
|
|
|
||
|
|
# ─── Assessments ───
|
||
|
|
now = datetime.utcnow().isoformat()
|
||
|
|
assessments = [
|
||
|
|
# NRW WP18
|
||
|
|
("18/A", "NRW", "2024-01-15", '["GRÜNE"]', 8.5,
|
||
|
|
_matrix(("A1", 4), ("B2", 3), ("C3", 5), ("D4", 4), ("E5", 5)),
|
||
|
|
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
|
||
|
|
("SPD", 7, False), ("AfD", 1, False))),
|
||
|
|
("18/B", "NRW", "2024-02-15", '["GRÜNE"]', 7.5,
|
||
|
|
_matrix(("A2", 3), ("C3", 4), ("D4", 3), ("E5", 4)),
|
||
|
|
_wp_scores(("GRÜNE", 8, True), ("CDU", 2, False),
|
||
|
|
("SPD", 6, False), ("AfD", 1, False))),
|
||
|
|
("18/C", "NRW", "2024-03-15", '["GRÜNE"]', 9.0,
|
||
|
|
_matrix(("A3", 5), ("D4", 4), ("E5", 5)),
|
||
|
|
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
|
||
|
|
("SPD", 7, False), ("AfD", 1, False))),
|
||
|
|
("18/D", "NRW", "2024-04-15", '["GRÜNE"]', 7.0,
|
||
|
|
_matrix(("B2", 3), ("C2", 4), ("D2", 3)),
|
||
|
|
_wp_scores(("GRÜNE", 8, True), ("CDU", 4, False),
|
||
|
|
("SPD", 7, False), ("AfD", 2, False))),
|
||
|
|
("18/E", "NRW", "2024-05-15", '["GRÜNE"]', 8.0,
|
||
|
|
_matrix(("A1", 4), ("B1", 3), ("E5", 4)),
|
||
|
|
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
|
||
|
|
("SPD", 7, False), ("AfD", 1, False))),
|
||
|
|
# AfD-Antrag mit niedrigem Score (Anti-Pattern)
|
||
|
|
("18/F", "NRW", "2024-06-15", '["AfD"]', 2.0,
|
||
|
|
_matrix(("A1", -3), ("B2", -2), ("E5", -4)),
|
||
|
|
_wp_scores(("AfD", 8, True), ("GRÜNE", 1, False),
|
||
|
|
("CDU", 4, False), ("SPD", 2, False))),
|
||
|
|
("18/G", "NRW", "2024-07-15", '["AfD"]', 1.5,
|
||
|
|
_matrix(("A1", -4), ("E5", -5)),
|
||
|
|
_wp_scores(("AfD", 9, True), ("GRÜNE", 1, False),
|
||
|
|
("CDU", 3, False), ("SPD", 1, False))),
|
||
|
|
("18/H", "NRW", "2024-08-15", '["CDU"]', 5.0,
|
||
|
|
_matrix(("D4", 1), ("D3", 0)),
|
||
|
|
_wp_scores(("CDU", 7, True), ("GRÜNE", 4, False),
|
||
|
|
("SPD", 5, False), ("AfD", 3, False))),
|
||
|
|
("18/I", "NRW", "2024-09-15", '["SPD"]', 6.5,
|
||
|
|
_matrix(("B2", 3), ("D2", 2)),
|
||
|
|
_wp_scores(("SPD", 8, True), ("GRÜNE", 6, False),
|
||
|
|
("CDU", 4, False), ("AfD", 1, False))),
|
||
|
|
("18/J", "NRW", "2024-10-15", '["SPD"]', 4.0,
|
||
|
|
_matrix(("D4", 1), ("E5", 0)),
|
||
|
|
_wp_scores(("SPD", 5, True), ("GRÜNE", 3, False),
|
||
|
|
("CDU", 5, False), ("AfD", 2, False))),
|
||
|
|
# MV WP8 fuer Cross-BL
|
||
|
|
("8/A", "MV", "2024-04-01", '["GRÜNE"]', 7.0,
|
||
|
|
_matrix(("A1", 3), ("D4", 4)),
|
||
|
|
_wp_scores(("GRÜNE", 8, True), ("CDU", 4, False),
|
||
|
|
("SPD", 6, False), ("AfD", 2, False))),
|
||
|
|
("8/B", "MV", "2024-05-01", '["GRÜNE"]', 8.0,
|
||
|
|
_matrix(("B2", 4), ("D4", 5)),
|
||
|
|
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
|
||
|
|
("SPD", 7, False), ("AfD", 1, False))),
|
||
|
|
("8/C", "MV", "2024-06-01", '["AfD"]', 2.0,
|
||
|
|
_matrix(("A1", -3), ("E5", -4)),
|
||
|
|
_wp_scores(("AfD", 9, True), ("GRÜNE", 1, False),
|
||
|
|
("CDU", 3, False), ("SPD", 2, False))),
|
||
|
|
]
|
||
|
|
for ds, bl, dat, fr, sc, mat, wps in assessments:
|
||
|
|
conn.execute(
|
||
|
|
"INSERT INTO assessments (drucksache, title, fraktionen, datum, "
|
||
|
|
"bundesland, gwoe_score, gwoe_matrix, wahlprogramm_scores, "
|
||
|
|
"source, model, created_at, updated_at) VALUES "
|
||
|
|
"(?, ?, ?, ?, ?, ?, ?, ?, 'test', 'test', ?, ?)",
|
||
|
|
(ds, f"Test {ds}", fr, dat, bl, sc, mat, wps, now, now),
|
||
|
|
)
|
||
|
|
|
||
|
|
# ─── Vote-Results ───
|
||
|
|
# Jeder NRW-Hoch-Antrag (A-E,I): GRÜNE+SPD ja, CDU+AfD nein
|
||
|
|
# AfD-Antrag F,G: AfD ja, GRÜNE+SPD+CDU nein
|
||
|
|
# CDU-Antrag H: CDU ja, GRÜNE+SPD+AfD nein
|
||
|
|
# SPD-Antrag J: SPD ja, GRÜNE+CDU+AfD nein
|
||
|
|
# MV-Hoch-Anträge: GRÜNE+SPD ja, CDU+AfD nein
|
||
|
|
# MV-AfD-Antrag: AfD ja, andere nein
|
||
|
|
votes = [
|
||
|
|
("NRW", "18/A", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-A"),
|
||
|
|
("NRW", "18/B", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-B"),
|
||
|
|
("NRW", "18/C", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-C"),
|
||
|
|
("NRW", "18/D", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-D"),
|
||
|
|
("NRW", "18/E", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-E"),
|
||
|
|
("NRW", "18/F", "abgelehnt", '["AfD"]', '["GRÜNE","SPD","CDU"]', "MMP18-F"),
|
||
|
|
("NRW", "18/G", "abgelehnt", '["AfD"]', '["GRÜNE","SPD","CDU"]', "MMP18-G"),
|
||
|
|
("NRW", "18/H", "angenommen", '["CDU","AfD"]', '["GRÜNE","SPD"]', "MMP18-H"),
|
||
|
|
("NRW", "18/I", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-I"),
|
||
|
|
("NRW", "18/J", "abgelehnt", '["SPD"]', '["GRÜNE","CDU","AfD"]', "MMP18-J"),
|
||
|
|
("MV", "8/A", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "PlPr8-A"),
|
||
|
|
("MV", "8/B", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "PlPr8-B"),
|
||
|
|
("MV", "8/C", "abgelehnt", '["AfD"]', '["GRÜNE","SPD","CDU"]', "PlPr8-C"),
|
||
|
|
]
|
||
|
|
for bl, ds, erg, ja, nein, prot in votes:
|
||
|
|
conn.execute(
|
||
|
|
"INSERT INTO plenum_vote_results "
|
||
|
|
"(bundesland, drucksache, ergebnis, fraktionen_ja, fraktionen_nein, "
|
||
|
|
" quelle_protokoll) VALUES (?, ?, ?, ?, ?, ?)",
|
||
|
|
(bl, ds, erg, ja, nein, prot),
|
||
|
|
)
|
||
|
|
|
||
|
|
conn.commit()
|
||
|
|
conn.close()
|
||
|
|
return db
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
# aggregate_stimm_index
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestAggregateStimmIndex:
|
||
|
|
def test_grüne_high_index(self, sample_db):
|
||
|
|
"""GRÜNE: stimmt JA bei hohen GWÖ-Anträgen, NEIN bei niedrigen → hoher Index."""
|
||
|
|
out = aggregate_stimm_index(db_path=sample_db, min_n=2)
|
||
|
|
gruene = next(f for f in out["fraktionen"] if f["partei"] == "GRÜNE")
|
||
|
|
# JA: 18/A,B,D,E,I + 8/A,B (NRW+MV high score). Antragsteller-exclude
|
||
|
|
# entfernt 18/A-E + 8/A,B fuer GRÜNE → bleibt 18/I als JA. NEIN: 18/H + 18/J + 18/F + 18/G + 8/C.
|
||
|
|
assert gruene["n_ja"] >= 1
|
||
|
|
assert gruene["n_nein"] >= 4
|
||
|
|
# Wenn beide N>0: stimm_index sollte positiv sein
|
||
|
|
if gruene["stimm_index"] is not None:
|
||
|
|
assert gruene["stimm_index"] > 0
|
||
|
|
|
||
|
|
def test_afd_low_index(self, sample_db):
|
||
|
|
"""AfD: stimmt NEIN bei hohen GWÖ-Anträgen, JA bei niedrigen → negativer Index."""
|
||
|
|
out = aggregate_stimm_index(db_path=sample_db, min_n=2)
|
||
|
|
afd = next(f for f in out["fraktionen"] if f["partei"] == "AfD")
|
||
|
|
# AfD ist Antragsteller bei 18/F,G,8/C → diese werden ausgeschlossen.
|
||
|
|
# Bleibt: NEIN bei 18/A-E, 18/I, 18/J, 8/A, 8/B. AfD JA bei 18/H (CDU-Antrag).
|
||
|
|
assert afd["n_nein"] >= 4
|
||
|
|
# Wenn JA und NEIN beide gefuellt: Index negativ
|
||
|
|
if afd["stimm_index"] is not None:
|
||
|
|
assert afd["stimm_index"] < 0
|
||
|
|
|
||
|
|
def test_exclude_antragsteller_default(self, sample_db):
|
||
|
|
"""Default schliesst eigene Antraege aus."""
|
||
|
|
with_excl = aggregate_stimm_index(
|
||
|
|
db_path=sample_db, exclude_antragsteller=True, min_n=2,
|
||
|
|
)
|
||
|
|
without_excl = aggregate_stimm_index(
|
||
|
|
db_path=sample_db, exclude_antragsteller=False, min_n=2,
|
||
|
|
)
|
||
|
|
# GRÜNE hat 7 eigene Anträge → ohne Exclude mehr n_ja
|
||
|
|
gr_with = next(f for f in with_excl["fraktionen"] if f["partei"] == "GRÜNE")
|
||
|
|
gr_without = next(f for f in without_excl["fraktionen"] if f["partei"] == "GRÜNE")
|
||
|
|
assert gr_without["n_ja"] > gr_with["n_ja"]
|
||
|
|
|
||
|
|
def test_filter_by_bundesland(self, sample_db):
|
||
|
|
nrw = aggregate_stimm_index(db_path=sample_db, filter_bl="NRW", min_n=2)
|
||
|
|
assert nrw["n_assessments_matched"] == 10
|
||
|
|
|
||
|
|
mv = aggregate_stimm_index(db_path=sample_db, filter_bl="MV", min_n=2)
|
||
|
|
assert mv["n_assessments_matched"] == 3
|
||
|
|
|
||
|
|
def test_min_n_threshold(self, sample_db):
|
||
|
|
"""Fraktion mit n_ja=2, min_n=5 → ausreichend=False."""
|
||
|
|
out = aggregate_stimm_index(db_path=sample_db, min_n=5)
|
||
|
|
for f in out["fraktionen"]:
|
||
|
|
if f["n_ja"] < 5 or f["n_nein"] < 5:
|
||
|
|
assert f["ausreichend"] is False
|
||
|
|
|
||
|
|
def test_empty_db(self, tmp_path):
|
||
|
|
out = aggregate_stimm_index(db_path=tmp_path / "missing.db")
|
||
|
|
assert out["n_assessments_matched"] == 0
|
||
|
|
assert out["fraktionen"] == []
|
||
|
|
|
||
|
|
def test_sorted_by_index_desc(self, sample_db):
|
||
|
|
"""Output sorted by stimm_index descending — None at end."""
|
||
|
|
out = aggregate_stimm_index(db_path=sample_db, min_n=1)
|
||
|
|
indices = [f["stimm_index"] for f in out["fraktionen"]]
|
||
|
|
# All not-None values should be in descending order
|
||
|
|
not_none = [v for v in indices if v is not None]
|
||
|
|
assert not_none == sorted(not_none, reverse=True)
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
# aggregate_heuchelei
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestAggregateHeuchelei:
|
||
|
|
def test_returns_structure(self, sample_db):
|
||
|
|
out = aggregate_heuchelei(db_path=sample_db, min_n=1)
|
||
|
|
assert "fraktionen" in out
|
||
|
|
assert "n_assessments_matched" in out
|
||
|
|
|
||
|
|
def test_heuchelei_quote_calculation(self, sample_db):
|
||
|
|
out = aggregate_heuchelei(
|
||
|
|
db_path=sample_db, score_threshold=7.0, min_n=1,
|
||
|
|
)
|
||
|
|
# SPD: wahlprogramm_score>=7 in 18/A-E (5x). SPD stimmt JA in allen
|
||
|
|
# → heuchelei_quote = 0/5 = 0.
|
||
|
|
spd = next(f for f in out["fraktionen"] if f["partei"] == "SPD")
|
||
|
|
assert spd["n_im_programm"] >= 5
|
||
|
|
assert spd["heuchelei_quote"] == 0 or spd["heuchelei_quote"] is None
|
||
|
|
|
||
|
|
def test_threshold_filter(self, sample_db):
|
||
|
|
"""Hoher Threshold reduziert n_im_programm."""
|
||
|
|
low = aggregate_heuchelei(db_path=sample_db, score_threshold=1.0, min_n=1)
|
||
|
|
high = aggregate_heuchelei(db_path=sample_db, score_threshold=9.5, min_n=1)
|
||
|
|
# Bei threshold=1 sind quasi alle Fraktionen drin, bei 9.5 sehr wenige
|
||
|
|
low_total = sum(f["n_im_programm"] for f in low["fraktionen"])
|
||
|
|
high_total = sum(f["n_im_programm"] for f in high["fraktionen"])
|
||
|
|
assert low_total > high_total
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
# _wert_score_for_assessment helper
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestWertScoreHelper:
|
||
|
|
def test_extracts_per_column(self):
|
||
|
|
matrix = [
|
||
|
|
{"field": "A1", "rating": 3},
|
||
|
|
{"field": "B1", "rating": 5},
|
||
|
|
{"field": "C2", "rating": 4},
|
||
|
|
]
|
||
|
|
result = _wert_score_for_assessment(matrix)
|
||
|
|
# Spalte 1 (Würde): A1=3 + B1=5 → Ø=4
|
||
|
|
assert result["1"] == 4.0
|
||
|
|
# Spalte 2 (Solidarität): C2=4 → Ø=4
|
||
|
|
assert result["2"] == 4.0
|
||
|
|
|
||
|
|
def test_empty_matrix(self):
|
||
|
|
assert _wert_score_for_assessment([]) == {}
|
||
|
|
|
||
|
|
def test_invalid_entries_skipped(self):
|
||
|
|
matrix = [
|
||
|
|
{"field": "A1", "rating": 3},
|
||
|
|
{"field": "", "rating": 5}, # empty field skipped
|
||
|
|
{"field": "X9", "rating": 2}, # invalid column suffix
|
||
|
|
"not a dict", # type error
|
||
|
|
]
|
||
|
|
result = _wert_score_for_assessment(matrix)
|
||
|
|
assert result == {"1": 3.0}
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
# aggregate_stimm_index_pro_wert
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestAggregateProWert:
|
||
|
|
def test_structure(self, sample_db):
|
||
|
|
out = aggregate_stimm_index_pro_wert(db_path=sample_db, min_n=1)
|
||
|
|
assert out["werte"] == [
|
||
|
|
"Menschenwürde", "Solidarität", "Ökologische Nachhaltigkeit",
|
||
|
|
"Soziale Gerechtigkeit", "Transparenz & Demokratie",
|
||
|
|
]
|
||
|
|
assert "cells" in out
|
||
|
|
for partei in out["fraktionen"]:
|
||
|
|
assert set(out["cells"][partei].keys()) == set(out["werte"])
|
||
|
|
|
||
|
|
def test_cell_format(self, sample_db):
|
||
|
|
out = aggregate_stimm_index_pro_wert(db_path=sample_db, min_n=1)
|
||
|
|
if not out["fraktionen"]:
|
||
|
|
pytest.skip("no parteien — DB empty")
|
||
|
|
first_partei = out["fraktionen"][0]
|
||
|
|
cell = out["cells"][first_partei]["Menschenwürde"]
|
||
|
|
assert "stimm_index" in cell
|
||
|
|
assert "n_ja" in cell
|
||
|
|
assert "n_nein" in cell
|
||
|
|
assert "ausreichend" in cell
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
# aggregate_stimm_index_cross_bl
|
||
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestAggregateCrossBl:
|
||
|
|
def test_structure(self, sample_db):
|
||
|
|
out = aggregate_stimm_index_cross_bl(db_path=sample_db, min_n=1)
|
||
|
|
assert "NRW" in out["bundeslaender"]
|
||
|
|
assert "MV" in out["bundeslaender"]
|
||
|
|
assert "fraktionen" in out
|
||
|
|
assert "fraktionen_alle" in out
|
||
|
|
|
||
|
|
def test_only_multi_bl_in_fraktionen(self, sample_db):
|
||
|
|
"""Fraktionen-Liste enthält nur Parteien mit ≥2 BL ausreichend-erfüllt."""
|
||
|
|
out = aggregate_stimm_index_cross_bl(db_path=sample_db, min_n=2)
|
||
|
|
# Bei min_n=2 sind die meisten Kombinationen nicht ausreichend.
|
||
|
|
# Test prueft nur Struktur: fraktionen ⊆ fraktionen_alle.
|
||
|
|
assert set(out["fraktionen"]).issubset(set(out["fraktionen_alle"]))
|
||
|
|
|
||
|
|
def test_cell_format(self, sample_db):
|
||
|
|
out = aggregate_stimm_index_cross_bl(db_path=sample_db, min_n=1)
|
||
|
|
if out["fraktionen_alle"]:
|
||
|
|
partei = out["fraktionen_alle"][0]
|
||
|
|
for bl in out["bundeslaender"]:
|
||
|
|
cell = out["cells"][partei][bl]
|
||
|
|
assert "stimm_index" in cell
|
||
|
|
assert "n_ja" in cell
|
||
|
|
assert "n_nein" in cell
|