| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- """Frozen-fixture suite for analyze.py classifier and aggregator.
- Each test parses a real-ish Clerk/Senate XML fixture via parse.py, then asserts
- classify_vote / aggregate behavior. Fixtures live in tests/fixtures/ and are
- trimmed/edited copies of real cached XMLs (see header comment in each).
- """
- import os
- import pytest
- import analyze
- import parse
- FIX = os.path.join(os.path.dirname(__file__), "fixtures")
- # ---------- Unit tests on pure helpers ----------
- @pytest.mark.parametrize("raw,expected", [
- ("Yea", "Yea"), ("Aye", "Yea"),
- ("Nay", "Nay"), ("No", "Nay"),
- ("Present", "Present"),
- ("Not Voting", "Not Voting"),
- (None, None), ("", ""),
- ])
- def test_norm_vote(raw, expected):
- assert analyze._norm_vote(raw) == expected
- @pytest.mark.parametrize("totals,expected", [
- ({"yea": 100, "nay": 50}, "Yea"),
- ({"yea": 50, "nay": 100}, "Nay"),
- ({"yea": 100, "nay": 100}, "Split"),
- ({"yea": 0, "nay": 0}, "Split"),
- ])
- def test_majority_position(totals, expected):
- assert analyze._majority_position(totals) == expected
- # ---------- Fixture loaders ----------
- def _house(name, year=2025, roll=900):
- rec, _ = parse.parse_house_vote(os.path.join(FIX, name), year, roll)
- assert rec is not None, f"parse_house_vote returned None for {name}"
- return rec
- def _senate(name, session=1, vnum=900):
- rec, _ = parse.parse_senate_vote(os.path.join(FIX, name), session, vnum)
- assert rec is not None, f"parse_senate_vote returned None for {name}"
- return rec
- # ---------- Classification tests ----------
- def test_partisan_house_helps_republicans():
- rec = _house("partisan_house.xml")
- # Jordan (R) Yea on R-Yea/D-Nay partisan vote
- cls = analyze.classify_vote(rec, "J000289")
- assert cls["member_vote"] == "Yea"
- assert cls["member_vote_norm"] == "Yea"
- assert cls["r_pos"] == "Yea"
- assert cls["d_pos"] == "Nay"
- assert cls["alignment"] == "Helped Republicans"
- assert cls["blocked"] is None
- def test_partisan_house_helps_democrats():
- rec = _house("partisan_house.xml")
- # Khanna (D) Nay on R-Yea/D-Nay partisan vote
- cls = analyze.classify_vote(rec, "K000389")
- assert cls["alignment"] == "Helped Democrats"
- def test_bipartisan_aye_no_normalization():
- rec = _house("bipartisan_house.xml")
- # Both parties Yea; Jordan voted "Aye" (procedural)
- cls = analyze.classify_vote(rec, "J000289")
- assert cls["member_vote"] == "Aye" # raw preserved
- assert cls["member_vote_norm"] == "Yea" # normalized
- assert cls["alignment"] == "Helped Both"
- # Khanna also Aye → Helped Both
- assert analyze.classify_vote(rec, "K000389")["alignment"] == "Helped Both"
- # Donalds "No" → normalized Nay, helped neither majority
- donalds = analyze.classify_vote(rec, "D000032")
- assert donalds["member_vote"] == "No"
- assert donalds["member_vote_norm"] == "Nay"
- assert donalds["alignment"] == "Helped Neither"
- def test_member_absent():
- rec = _house("partisan_house.xml")
- cls = analyze.classify_vote(rec, "Z999999") # not in record
- assert cls["alignment"].startswith("N/A: absent")
- assert cls["member_vote"] is None
- assert cls["member_vote_norm"] is None
- assert cls["blocked"] is None
- def test_split_party_no_false_helped():
- rec = _house("split_party_house.xml")
- # D is tied (100/100) → Split. Khanna (D) voted Yea; R majority is Yea.
- # Should classify as Helped Republicans only — NOT Helped Democrats.
- cls = analyze.classify_vote(rec, "K000389")
- assert cls["d_pos"] == "Split"
- assert cls["r_pos"] == "Yea"
- assert cls["alignment"] == "Helped Republicans"
- # And aggregate must not credit Khanna a "with-dem" or "against-dem"
- # because d_pos is Split (no party position to compare against).
- kpi = analyze.aggregate([rec], "K000389", "D", "house")
- assert kpi["voted_with_dem"] == 0
- assert kpi["voted_against_dem"] == 0
- assert kpi["voted_with_gop"] == 1
- # ---------- Failed-blocking tests ----------
- def test_failed_blocking_democrat_senate():
- rec = _senate("failed_blocking_senate.xml", session=1, vnum=910)
- # D-Yea / R-Nay / Failed. Manchin (D) voted Nay → blocked="Democrat".
- cls = analyze.classify_vote(rec, "S307")
- assert cls["d_pos"] == "Yea"
- assert cls["r_pos"] == "Nay"
- assert cls["blocked"] == "Democrat"
- # Cruz (R) Nay also gets blocked="Democrat" (he helped block Dem-backed bill)
- assert analyze.classify_vote(rec, "S288")["blocked"] == "Democrat"
- # Schumer (D) Yea → no block (he supported it)
- assert analyze.classify_vote(rec, "S168")["blocked"] is None
- def test_failed_blocking_republican_senate():
- rec = _senate("failed_blocking_rep_senate.xml", session=1, vnum=911)
- # R-Yea / D-Nay / Rejected. Collins (R) Nay → blocked="Republican".
- assert analyze.classify_vote(rec, "S289")["blocked"] == "Republican"
- # Warren (D) Nay → blocked="Republican" (Dem helped block GOP-backed bill)
- assert analyze.classify_vote(rec, "S366")["blocked"] == "Republican"
- # Cruz (R) Yea → no block
- assert analyze.classify_vote(rec, "S288")["blocked"] is None
- def test_failed_blocking_house_analog():
- rec = _house("failed_blocking_house.xml", year=2025, roll=920)
- # House R-Yea / D-Nay / Failed. Confirms classifier is chamber-agnostic.
- assert analyze.classify_vote(rec, "D000032")["blocked"] == "Republican"
- assert analyze.classify_vote(rec, "K000389")["blocked"] == "Republican"
- # ---------- Aggregate end-to-end ----------
- def test_aggregate_end_to_end_house():
- recs = [
- _house("partisan_house.xml", year=2025, roll=900),
- _house("bipartisan_house.xml", year=2025, roll=901),
- _house("failed_blocking_house.xml", year=2025, roll=920),
- _house("split_party_house.xml", year=2025, roll=930),
- ]
- # Khanna (D) — votes: Nay (partisan), Aye(=Yea) (bipartisan),
- # Nay (failed-blocking-house), Yea (split). All 4 are voting (no Present/NV).
- kpi = analyze.aggregate(recs, "K000389", "D", "house")
- assert kpi["chamber"] == "house"
- assert kpi["total"] == 4
- assert kpi["voting"] == 4
- assert kpi["yeas"] == 2 # bipartisan Aye→Yea, split Yea
- assert kpi["nays"] == 2 # partisan Nay, failed-blocking-house Nay
- assert kpi["present"] == 0
- assert kpi["nv"] == 0
- # voted_with_gop: vote matched R majority position (excluding R-Split votes).
- # partisan: R=Yea, Khanna=Nay → against. bipartisan: R=Yea, Khanna=Yea → with.
- # failed-blocking-house: R=Yea, Khanna=Nay → against. split: R=Yea, Khanna=Yea → with.
- assert kpi["voted_with_gop"] == 2
- assert kpi["voted_against_gop"] == 2
- # blocked counter: failed_blocking_house gives blocked="Republican" for Khanna
- assert kpi["blocked_rep_count"] == 1
- assert kpi["blocked_dem_count"] == 0
- assert kpi["lone_wolf_threshold"] == 5
- # alignment counts sum to total
- assert sum(kpi["alignment"].values()) == 4
- def test_lone_wolf_threshold_chamber_dependent():
- # partisan_house: R yea=210, R nay=4. Donalds (R) voted Nay → defector.
- # defectors == 4. House threshold 5 → lone wolf. Senate threshold 3 → NOT.
- rec = _house("partisan_house.xml")
- house_kpi = analyze.aggregate([rec], "D000032", "R", "house")
- assert house_kpi["lone_wolf"] == 1
- assert house_kpi["lone_wolf_threshold"] == 5
- senate_kpi = analyze.aggregate([rec], "D000032", "R", "senate")
- assert senate_kpi["lone_wolf"] == 0
- assert senate_kpi["lone_wolf_threshold"] == 3
|