Files
bs-roster-parser/tests/test_parser.py
root 948d98accb Initial commit: bs-roster-parser v1.0.0
Python library for parsing BattleScribe/NewRecruit roster JSON.
Extracted from Nachmund Tracker's processJSON, ported JS→Python.

- parse_roster(json_string) / parse_roster_file(path) → RosterSummary
- Extracts: unit name, pts, CP, model count, weapon breakdown per model variant
- Handles: variable-model-count units, nested costs, compound upgrades
- Unicode apostrophe-safe unit lookup (find_unit)
- to_dict() for JSON serialization
- 8/8 tests passing on real roster data (27 units, 2815pts)
2026-06-18 03:04:16 +00:00

137 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Tests for bs-roster-parser using the Nachmund Tracker example roster."""
import json
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from bs_roster_parser import parse_roster, parse_roster_file, RosterSummary, Unit
TEST_FILE = os.path.join(os.path.dirname(__file__), "example_roster.json")
def test_parse_file():
"""Parse the example roster file and verify basic structure."""
summary = parse_roster_file(TEST_FILE)
assert isinstance(summary, RosterSummary)
assert summary.roster_name == "Cadian 67th Legion (1)"
assert summary.game_system == "Warhammer 40,000 10th Edition"
assert summary.points_limit == 2000
print(f"✓ Parsed: {summary.roster_name}")
print(f" Game: {summary.game_system}")
print(f" Points limit: {summary.points_limit}")
print(f" Faction: {summary.faction}")
print(f" Units: {summary.unit_count}")
print(f" Total pts: {summary.total_pts}")
print(f" Total CP: {summary.total_cp}")
print(f" Total models: {summary.total_models}")
def test_units_extracted():
"""Verify units are extracted with correct names and costs."""
summary = parse_roster_file(TEST_FILE)
assert summary.units, "No units extracted"
# Should have at least some units with pts
units_with_pts = [u for u in summary.units if u.pts > 0]
assert units_with_pts, "No units with pts > 0 found"
# Gaunt's Ghosts should be 100 pts (from the example JSON)
# Note: the roster uses a unicode curly apostrophe (') not a straight one (')
ghosts = summary.find_unit("Gaunt's Ghosts")
assert ghosts is not None, "Gaunt's Ghosts not found"
assert ghosts.pts == 100, f"Gaunt's Ghosts pts = {ghosts.pts}, expected 100"
assert ghosts.type == "unit"
print(f"✓ Gaunt's Ghosts: {ghosts.pts}pts, {ghosts.model_count} models")
def test_model_count():
"""Verify model counting works for unit nodes."""
summary = parse_roster_file(TEST_FILE)
for u in summary.units:
if u.model_count > 0:
print(f"{u.name}: {u.model_count} models, {u.pts}pts")
def test_crusade_points():
"""Verify crusade points extraction."""
summary = parse_roster_file(TEST_FILE)
if summary.total_cp > 0:
cp_units = [u for u in summary.units if u.cp > 0]
print(f"✓ Crusade points: {summary.total_cp} CP across {len(cp_units)} units")
for u in cp_units:
print(f" {u.name}: {u.cp} CP")
def test_model_breakdown():
"""Verify model variant breakdown (weapons, distinct model types)."""
summary = parse_roster_file(TEST_FILE)
for u in summary.units:
if u.breakdown:
print(f"{u.name} breakdown:")
for m in u.breakdown:
weapon_str = f", weapons: {m.weapons}" if m.weapons else ""
print(f" {m.name} ×{m.count}{weapon_str}")
def test_to_dict():
"""Verify serialization to dict."""
summary = parse_roster_file(TEST_FILE)
d = summary.to_dict()
assert "units" in d
assert "total_pts" in d
assert isinstance(d["units"], list)
# Round-trip through JSON
json_str = json.dumps(d)
restored = json.loads(json_str)
assert restored["total_pts"] == summary.total_pts
print(f"✓ Serialization: {len(restored['units'])} units, {restored['total_pts']}pts")
def test_find_unit_case_insensitive():
"""Verify case-insensitive unit lookup."""
summary = parse_roster_file(TEST_FILE)
# Try different cases
for name in ["gaunt's ghosts", "GAUNT'S GHOSTS", "Gaunt's Ghosts"]:
u = summary.find_unit(name)
if u:
print(f"✓ Found '{name}'{u.pts}pts")
return
# If Gaunt's Ghosts isn't in this roster, just verify find_unit returns None for nonsense
assert summary.find_unit("Nonexistent Unit") is None
print("✓ find_unit returns None for nonexistent units")
def test_string_input():
"""Verify parse_roster accepts a JSON string."""
with open(TEST_FILE) as f:
json_str = f.read()
summary = parse_roster(json_str)
assert summary.units
print(f"✓ String input: {summary.unit_count} units parsed")
if __name__ == "__main__":
tests = [
test_parse_file,
test_units_extracted,
test_model_count,
test_crusade_points,
test_model_breakdown,
test_to_dict,
test_find_unit_case_insensitive,
test_string_input,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except Exception as e:
print(f"{test.__name__}: {e}")
failed += 1
print(f"\n{'='*40}")
print(f"Results: {passed} passed, {failed} failed")
sys.exit(1 if failed else 0)