193 lines
7.5 KiB
Python
193 lines
7.5 KiB
Python
|
|
"""Unit-Tests für app.marker — Heuchelei + Konsistenz."""
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from app.marker import (
|
|||
|
|
heuchelei_score,
|
|||
|
|
decisive_outcome,
|
|||
|
|
consistency_state,
|
|||
|
|
DECISIVE_OUTCOMES,
|
|||
|
|
)
|
|||
|
|
_HAS_MARKER = True
|
|||
|
|
except ImportError:
|
|||
|
|
_HAS_MARKER = False
|
|||
|
|
|
|||
|
|
pytestmark = pytest.mark.skipif(not _HAS_MARKER, reason="app.marker nicht importierbar")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── heuchelei_score ────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class TestHeucheleiScore:
|
|||
|
|
"""heuchelei_score liefert WP-Score wenn Fraktion NEIN-stimmt aber WP≥7."""
|
|||
|
|
|
|||
|
|
def test_score_high_returns_score(self):
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 7.0}}]
|
|||
|
|
assert heuchelei_score("CDU", fs) == 7.0
|
|||
|
|
|
|||
|
|
def test_score_at_threshold_returns_score(self):
|
|||
|
|
# 7.0 ist genau Schwelle und sollte greifen
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 7.0}}]
|
|||
|
|
assert heuchelei_score("CDU", fs, threshold=7.0) == 7.0
|
|||
|
|
|
|||
|
|
def test_score_below_returns_none(self):
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 6.9}}]
|
|||
|
|
assert heuchelei_score("CDU", fs) is None
|
|||
|
|
|
|||
|
|
def test_score_above_default_returns_score(self):
|
|||
|
|
fs = [{"fraktion": "FDP", "wahlprogramm": {"score": 8.0}}]
|
|||
|
|
assert heuchelei_score("FDP", fs) == 8.0
|
|||
|
|
|
|||
|
|
def test_unknown_fraktion_returns_none(self):
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 9.0}}]
|
|||
|
|
assert heuchelei_score("AfD", fs) is None
|
|||
|
|
|
|||
|
|
def test_empty_list_returns_none(self):
|
|||
|
|
assert heuchelei_score("CDU", []) is None
|
|||
|
|
|
|||
|
|
def test_none_input_returns_none(self):
|
|||
|
|
assert heuchelei_score("CDU", None) is None
|
|||
|
|
|
|||
|
|
def test_missing_score_field_returns_none(self):
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {}}]
|
|||
|
|
assert heuchelei_score("CDU", fs) is None
|
|||
|
|
|
|||
|
|
def test_score_as_int_handled(self):
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 7}}]
|
|||
|
|
assert heuchelei_score("CDU", fs) == 7.0
|
|||
|
|
|
|||
|
|
def test_invalid_score_returns_none(self):
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": "not-a-number"}}]
|
|||
|
|
assert heuchelei_score("CDU", fs) is None
|
|||
|
|
|
|||
|
|
def test_custom_threshold(self):
|
|||
|
|
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 5.0}}]
|
|||
|
|
# bei threshold=4 greift's
|
|||
|
|
assert heuchelei_score("CDU", fs, threshold=4.0) == 5.0
|
|||
|
|
# bei threshold=6 nicht
|
|||
|
|
assert heuchelei_score("CDU", fs, threshold=6.0) is None
|
|||
|
|
|
|||
|
|
def test_finds_first_match(self):
|
|||
|
|
"""Wenn dieselbe Fraktion mehrfach in der Liste ist, wird der erste Treffer genommen."""
|
|||
|
|
fs = [
|
|||
|
|
{"fraktion": "CDU", "wahlprogramm": {"score": 8.0}},
|
|||
|
|
{"fraktion": "CDU", "wahlprogramm": {"score": 3.0}},
|
|||
|
|
]
|
|||
|
|
assert heuchelei_score("CDU", fs) == 8.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── decisive_outcome ───────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class TestDecisiveOutcome:
|
|||
|
|
"""decisive_outcome wählt erstes Vote mit angenommen|abgelehnt|bestätigt."""
|
|||
|
|
|
|||
|
|
def test_single_decisive(self):
|
|||
|
|
votes = [{"ergebnis": "angenommen"}]
|
|||
|
|
assert decisive_outcome(votes) == "angenommen"
|
|||
|
|
|
|||
|
|
def test_single_abgelehnt(self):
|
|||
|
|
votes = [{"ergebnis": "abgelehnt"}]
|
|||
|
|
assert decisive_outcome(votes) == "abgelehnt"
|
|||
|
|
|
|||
|
|
def test_single_bestaetigt(self):
|
|||
|
|
votes = [{"ergebnis": "bestätigt"}]
|
|||
|
|
assert decisive_outcome(votes) == "bestätigt"
|
|||
|
|
|
|||
|
|
def test_skip_ueberwiesen_to_decisive(self):
|
|||
|
|
"""Bei Überweisung → angenommen sollte angenommen gewählt werden."""
|
|||
|
|
votes = [
|
|||
|
|
{"ergebnis": "überwiesen"},
|
|||
|
|
{"ergebnis": "angenommen"},
|
|||
|
|
]
|
|||
|
|
assert decisive_outcome(votes) == "angenommen"
|
|||
|
|
|
|||
|
|
def test_only_ueberwiesen_returns_none(self):
|
|||
|
|
votes = [{"ergebnis": "überwiesen"}]
|
|||
|
|
assert decisive_outcome(votes) is None
|
|||
|
|
|
|||
|
|
def test_only_zurueckgezogen_returns_none(self):
|
|||
|
|
votes = [{"ergebnis": "zurückgezogen"}]
|
|||
|
|
assert decisive_outcome(votes) is None
|
|||
|
|
|
|||
|
|
def test_empty_list_returns_none(self):
|
|||
|
|
assert decisive_outcome([]) is None
|
|||
|
|
|
|||
|
|
def test_none_input_returns_none(self):
|
|||
|
|
assert decisive_outcome(None) is None
|
|||
|
|
|
|||
|
|
def test_first_decisive_wins(self):
|
|||
|
|
"""Wenn beide Votes definitiv sind, wird das erste gewählt."""
|
|||
|
|
votes = [
|
|||
|
|
{"ergebnis": "angenommen"},
|
|||
|
|
{"ergebnis": "abgelehnt"},
|
|||
|
|
]
|
|||
|
|
assert decisive_outcome(votes) == "angenommen"
|
|||
|
|
|
|||
|
|
def test_case_insensitive(self):
|
|||
|
|
votes = [{"ergebnis": "Angenommen"}]
|
|||
|
|
assert decisive_outcome(votes) == "angenommen"
|
|||
|
|
|
|||
|
|
def test_decisive_outcomes_constant(self):
|
|||
|
|
"""Konstante DECISIVE_OUTCOMES enthält genau die drei Werte."""
|
|||
|
|
assert DECISIVE_OUTCOMES == frozenset({"angenommen", "abgelehnt", "bestätigt"})
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── consistency_state ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class TestConsistencyState:
|
|||
|
|
"""consistency_state vergleicht GWÖ-Empfehlung × Beschluss."""
|
|||
|
|
|
|||
|
|
def test_supports_passed_aligned(self):
|
|||
|
|
votes = [{"ergebnis": "angenommen"}]
|
|||
|
|
assert consistency_state("Uneingeschränkt unterstützen", votes) == "aligned"
|
|||
|
|
|
|||
|
|
def test_supports_failed_conflict(self):
|
|||
|
|
votes = [{"ergebnis": "abgelehnt"}]
|
|||
|
|
assert consistency_state("Uneingeschränkt unterstützen", votes) == "conflict"
|
|||
|
|
|
|||
|
|
def test_rejects_failed_aligned(self):
|
|||
|
|
votes = [{"ergebnis": "abgelehnt"}]
|
|||
|
|
assert consistency_state("Ablehnen", votes) == "aligned"
|
|||
|
|
|
|||
|
|
def test_rejects_passed_conflict(self):
|
|||
|
|
votes = [{"ergebnis": "angenommen"}]
|
|||
|
|
assert consistency_state("Ablehnen", votes) == "conflict"
|
|||
|
|
|
|||
|
|
def test_supports_with_changes_passed_aligned(self):
|
|||
|
|
"""„Unterstützen mit Änderungen" enthält 'unterstützen' → rec_supports."""
|
|||
|
|
votes = [{"ergebnis": "angenommen"}]
|
|||
|
|
assert consistency_state("Unterstützen mit Änderungen", votes) == "aligned"
|
|||
|
|
|
|||
|
|
def test_befuerworten_synonym(self):
|
|||
|
|
votes = [{"ergebnis": "angenommen"}]
|
|||
|
|
assert consistency_state("Befürworten", votes) == "aligned"
|
|||
|
|
|
|||
|
|
def test_ambivalent_returns_none(self):
|
|||
|
|
"""„Mit Vorbehalt" enthält weder 'unterstützen' noch 'ablehnen'."""
|
|||
|
|
votes = [{"ergebnis": "angenommen"}]
|
|||
|
|
assert consistency_state("Mit Vorbehalt", votes) is None
|
|||
|
|
|
|||
|
|
def test_ueberwiesen_returns_none(self):
|
|||
|
|
votes = [{"ergebnis": "überwiesen"}]
|
|||
|
|
assert consistency_state("Uneingeschränkt unterstützen", votes) is None
|
|||
|
|
|
|||
|
|
def test_no_votes_returns_none(self):
|
|||
|
|
assert consistency_state("Uneingeschränkt unterstützen", []) is None
|
|||
|
|
|
|||
|
|
def test_no_verdict_returns_none(self):
|
|||
|
|
votes = [{"ergebnis": "angenommen"}]
|
|||
|
|
assert consistency_state("", votes) is None
|
|||
|
|
assert consistency_state(None, votes) is None
|
|||
|
|
|
|||
|
|
def test_multivote_picks_decisive(self):
|
|||
|
|
"""Bei Überweisung → Endabstimmung wird die Endabstimmung gewertet."""
|
|||
|
|
votes = [
|
|||
|
|
{"ergebnis": "überwiesen"},
|
|||
|
|
{"ergebnis": "abgelehnt"},
|
|||
|
|
]
|
|||
|
|
assert consistency_state("Uneingeschränkt unterstützen", votes) == "conflict"
|
|||
|
|
|
|||
|
|
def test_bestaetigt_treated_as_passed(self):
|
|||
|
|
votes = [{"ergebnis": "bestätigt"}]
|
|||
|
|
assert consistency_state("Uneingeschränkt unterstützen", votes) == "aligned"
|
|||
|
|
assert consistency_state("Ablehnen", votes) == "conflict"
|