feat: evolution track seed data and tests (WP-03) (#68)
Closes #68 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a66ef9bd7c
commit
25f04892c2
0
app/seed/__init__.py
Normal file
0
app/seed/__init__.py
Normal file
5
app/seed/evolution_tracks.json
Normal file
5
app/seed/evolution_tracks.json
Normal file
@ -0,0 +1,5 @@
|
||||
[
|
||||
{"name": "Batter", "card_type": "batter", "formula": "pa+tb*2", "t1": 37, "t2": 149, "t3": 448, "t4": 896},
|
||||
{"name": "Starting Pitcher", "card_type": "sp", "formula": "ip+k", "t1": 10, "t2": 40, "t3": 120, "t4": 240},
|
||||
{"name": "Relief Pitcher", "card_type": "rp", "formula": "ip+k", "t1": 3, "t2": 12, "t3": 35, "t4": 70}
|
||||
]
|
||||
41
app/seed/evolution_tracks.py
Normal file
41
app/seed/evolution_tracks.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Seed data fixture for EvolutionTrack.
|
||||
|
||||
Inserts the three universal evolution tracks (Batter, Starting Pitcher,
|
||||
Relief Pitcher) if they do not already exist. Safe to call multiple times
|
||||
thanks to get_or_create — depends on WP-01 (EvolutionTrack model) to run.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
_JSON_PATH = os.path.join(os.path.dirname(__file__), "evolution_tracks.json")
|
||||
|
||||
|
||||
def load_tracks():
|
||||
"""Return the locked list of evolution track dicts from the JSON fixture."""
|
||||
with open(_JSON_PATH) as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def seed(model_class=None):
|
||||
"""Insert evolution tracks that are not yet in the database.
|
||||
|
||||
Args:
|
||||
model_class: Peewee model with get_or_create support. Defaults to
|
||||
``app.db_engine.EvolutionTrack`` (imported lazily so this module
|
||||
can be imported before WP-01 lands).
|
||||
|
||||
Returns:
|
||||
List of (instance, created) tuples from get_or_create.
|
||||
"""
|
||||
if model_class is None:
|
||||
from app.db_engine import EvolutionTrack as model_class # noqa: PLC0415
|
||||
|
||||
results = []
|
||||
for track in load_tracks():
|
||||
instance, created = model_class.get_or_create(
|
||||
card_type=track["card_type"],
|
||||
defaults=track,
|
||||
)
|
||||
results.append((instance, created))
|
||||
return results
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
119
tests/test_evolution_seed.py
Normal file
119
tests/test_evolution_seed.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Tests for the evolution track seed data fixture (WP-03).
|
||||
|
||||
Unit tests verify the JSON fixture is correctly formed without touching any
|
||||
database. The integration test binds a minimal in-memory EvolutionTrack
|
||||
model (mirroring the schema WP-01 will add to db_engine) to an in-memory
|
||||
SQLite database, calls seed(), and verifies idempotency.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from peewee import CharField, IntegerField, Model, SqliteDatabase
|
||||
|
||||
from app.seed.evolution_tracks import load_tracks, seed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_test_db = SqliteDatabase(":memory:")
|
||||
|
||||
|
||||
class EvolutionTrackStub(Model):
|
||||
"""Minimal EvolutionTrack model for integration tests.
|
||||
|
||||
Mirrors the schema that WP-01 will add to db_engine so the integration
|
||||
test can run without WP-01 being merged.
|
||||
"""
|
||||
|
||||
name = CharField()
|
||||
card_type = CharField(unique=True)
|
||||
formula = CharField()
|
||||
t1 = IntegerField()
|
||||
t2 = IntegerField()
|
||||
t3 = IntegerField()
|
||||
t4 = IntegerField()
|
||||
|
||||
class Meta:
|
||||
database = _test_db
|
||||
table_name = "evolution_track"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _db():
|
||||
"""Bind and create the stub table; drop it after each test."""
|
||||
_test_db.connect(reuse_if_open=True)
|
||||
_test_db.create_tables([EvolutionTrackStub])
|
||||
yield
|
||||
_test_db.drop_tables([EvolutionTrackStub])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unit tests — JSON fixture only, no database
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_three_tracks_in_seed_data():
|
||||
"""load_tracks() must return exactly 3 evolution tracks."""
|
||||
assert len(load_tracks()) == 3
|
||||
|
||||
|
||||
def test_card_types_are_exactly_batter_sp_rp():
|
||||
"""The set of card_type values must be exactly {'batter', 'sp', 'rp'}."""
|
||||
types = {t["card_type"] for t in load_tracks()}
|
||||
assert types == {"batter", "sp", "rp"}
|
||||
|
||||
|
||||
def test_all_thresholds_positive_and_ascending():
|
||||
"""Each track must have t1 < t2 < t3 < t4, all positive."""
|
||||
for track in load_tracks():
|
||||
assert track["t1"] > 0
|
||||
assert track["t1"] < track["t2"] < track["t3"] < track["t4"]
|
||||
|
||||
|
||||
def test_all_tracks_have_non_empty_formula():
|
||||
"""Every track must have a non-empty formula string."""
|
||||
for track in load_tracks():
|
||||
assert isinstance(track["formula"], str) and track["formula"].strip()
|
||||
|
||||
|
||||
def test_tier_thresholds_match_locked_values():
|
||||
"""Threshold values must exactly match the locked design spec."""
|
||||
tracks = {t["card_type"]: t for t in load_tracks()}
|
||||
|
||||
assert tracks["batter"]["t1"] == 37
|
||||
assert tracks["batter"]["t2"] == 149
|
||||
assert tracks["batter"]["t3"] == 448
|
||||
assert tracks["batter"]["t4"] == 896
|
||||
|
||||
assert tracks["sp"]["t1"] == 10
|
||||
assert tracks["sp"]["t2"] == 40
|
||||
assert tracks["sp"]["t3"] == 120
|
||||
assert tracks["sp"]["t4"] == 240
|
||||
|
||||
assert tracks["rp"]["t1"] == 3
|
||||
assert tracks["rp"]["t2"] == 12
|
||||
assert tracks["rp"]["t3"] == 35
|
||||
assert tracks["rp"]["t4"] == 70
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration test — uses the stub model + in-memory SQLite
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_seed_is_idempotent():
|
||||
"""Calling seed() twice must not create duplicate rows (get_or_create).
|
||||
|
||||
First call: all three tracks created (created=True for each).
|
||||
Second call: all three already exist (created=False for each).
|
||||
Both calls succeed without error.
|
||||
"""
|
||||
results_first = seed(model_class=EvolutionTrackStub)
|
||||
assert len(results_first) == 3
|
||||
assert all(created for _, created in results_first)
|
||||
|
||||
results_second = seed(model_class=EvolutionTrackStub)
|
||||
assert len(results_second) == 3
|
||||
assert not any(created for _, created in results_second)
|
||||
|
||||
assert EvolutionTrackStub.select().count() == 3
|
||||
Loading…
Reference in New Issue
Block a user