36 tests covering build_families (linear chains, branching, disjoint, Shedinja case), resolve_base_form, to_roman (parametrized), and strip_roman_suffix including round-trip verification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
5.8 KiB
Python
175 lines
5.8 KiB
Python
"""Unit tests for the services layer (families, naming utilities)."""
|
|
|
|
import pytest
|
|
|
|
from app.services.families import build_families, resolve_base_form
|
|
from app.services.naming import strip_roman_suffix, to_roman
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Minimal Evolution stand-in — only the two fields the services touch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Evo:
|
|
"""Lightweight stand-in for app.models.evolution.Evolution."""
|
|
|
|
def __init__(self, from_id: int, to_id: int) -> None:
|
|
self.from_pokemon_id = from_id
|
|
self.to_pokemon_id = to_id
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build_families
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBuildFamilies:
|
|
def test_empty_evolutions_returns_empty_dict(self):
|
|
assert build_families([]) == {}
|
|
|
|
def test_linear_chain(self):
|
|
# A(1) → B(2) → C(3)
|
|
evos = [Evo(1, 2), Evo(2, 3)]
|
|
families = build_families(evos)
|
|
assert set(families[1]) == {1, 2, 3}
|
|
assert set(families[2]) == {1, 2, 3}
|
|
assert set(families[3]) == {1, 2, 3}
|
|
|
|
def test_branching_evolutions(self):
|
|
# Eevee-like: 1 → 2, 1 → 3, 1 → 4
|
|
evos = [Evo(1, 2), Evo(1, 3), Evo(1, 4)]
|
|
families = build_families(evos)
|
|
assert set(families[1]) == {1, 2, 3, 4}
|
|
assert set(families[2]) == {1, 2, 3, 4}
|
|
assert set(families[4]) == {1, 2, 3, 4}
|
|
|
|
def test_disjoint_chains_are_separate_families(self):
|
|
# Chain 1→2 and independent chain 3→4
|
|
evos = [Evo(1, 2), Evo(3, 4)]
|
|
families = build_families(evos)
|
|
assert set(families[1]) == {1, 2}
|
|
assert set(families[3]) == {3, 4}
|
|
assert 3 not in set(families[1])
|
|
assert 1 not in set(families[3])
|
|
|
|
def test_shedinja_case(self):
|
|
# Nincada(1) → Ninjask(2) and Nincada(1) → Shedinja(3)
|
|
evos = [Evo(1, 2), Evo(1, 3)]
|
|
families = build_families(evos)
|
|
assert set(families[1]) == {1, 2, 3}
|
|
assert set(families[3]) == {1, 2, 3}
|
|
|
|
def test_pokemon_not_in_any_evolution_not_in_result(self):
|
|
evos = [Evo(1, 2)]
|
|
families = build_families(evos)
|
|
assert 99 not in families
|
|
|
|
def test_all_family_members_have_identical_family_list(self):
|
|
evos = [Evo(10, 11), Evo(11, 12)]
|
|
families = build_families(evos)
|
|
assert set(families[10]) == set(families[11]) == set(families[12])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolve_base_form
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestResolveBaseForm:
|
|
def test_pokemon_not_in_any_evolution_returns_itself(self):
|
|
assert resolve_base_form(99, []) == 99
|
|
|
|
def test_base_form_returns_itself(self):
|
|
# A(1) → B(2): base of 1 is still 1
|
|
evos = [Evo(1, 2)]
|
|
assert resolve_base_form(1, evos) == 1
|
|
|
|
def test_final_form_returns_base(self):
|
|
# A(1) → B(2) → C(3): base of 3 is 1
|
|
evos = [Evo(1, 2), Evo(2, 3)]
|
|
assert resolve_base_form(3, evos) == 1
|
|
|
|
def test_middle_form_returns_base(self):
|
|
# A(1) → B(2) → C(3): base of 2 is 1
|
|
evos = [Evo(1, 2), Evo(2, 3)]
|
|
assert resolve_base_form(2, evos) == 1
|
|
|
|
def test_branching_evolution_base(self):
|
|
# 1 → 2, 1 → 3: base of both 2 and 3 is 1
|
|
evos = [Evo(1, 2), Evo(1, 3)]
|
|
assert resolve_base_form(2, evos) == 1
|
|
assert resolve_base_form(3, evos) == 1
|
|
|
|
def test_shedinja_resolves_to_nincada(self):
|
|
# Nincada(1) → Ninjask(2), Nincada(1) → Shedinja(3)
|
|
evos = [Evo(1, 2), Evo(1, 3)]
|
|
assert resolve_base_form(3, evos) == 1
|
|
|
|
def test_empty_evolutions_returns_self(self):
|
|
assert resolve_base_form(42, []) == 42
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# to_roman
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestToRoman:
|
|
@pytest.mark.parametrize(
|
|
"n, expected",
|
|
[
|
|
(1, "I"),
|
|
(2, "II"),
|
|
(3, "III"),
|
|
(4, "IV"),
|
|
(5, "V"),
|
|
(6, "VI"),
|
|
(9, "IX"),
|
|
(10, "X"),
|
|
(11, "XI"),
|
|
(14, "XIV"),
|
|
(40, "XL"),
|
|
(50, "L"),
|
|
(90, "XC"),
|
|
(100, "C"),
|
|
],
|
|
)
|
|
def test_converts_integer_to_roman(self, n: int, expected: str):
|
|
assert to_roman(n) == expected
|
|
|
|
def test_typical_genlocke_sequence(self):
|
|
# Lineage names: Heracles I, II, III, IV, V
|
|
assert [to_roman(i) for i in range(1, 6)] == ["I", "II", "III", "IV", "V"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# strip_roman_suffix
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestStripRomanSuffix:
|
|
def test_strips_roman_numeral_ii(self):
|
|
assert strip_roman_suffix("Heracles II") == "Heracles"
|
|
|
|
def test_strips_roman_numeral_iii(self):
|
|
assert strip_roman_suffix("Athena III") == "Athena"
|
|
|
|
def test_strips_roman_numeral_iv(self):
|
|
assert strip_roman_suffix("Nova IV") == "Nova"
|
|
|
|
def test_strips_roman_numeral_x(self):
|
|
assert strip_roman_suffix("Zeus X") == "Zeus"
|
|
|
|
def test_no_suffix_returns_unchanged(self):
|
|
assert strip_roman_suffix("Apollo") == "Apollo"
|
|
|
|
def test_name_with_i_suffix(self):
|
|
# Single "I" at end is a valid roman numeral suffix
|
|
assert strip_roman_suffix("Heracles I") == "Heracles"
|
|
|
|
def test_round_trip_with_to_roman(self):
|
|
base = "Heracles"
|
|
for n in range(1, 6):
|
|
suffixed = f"{base} {to_roman(n)}"
|
|
assert strip_roman_suffix(suffixed) == base
|