Files
snake-python/tests/test_LegacyMigration.py
T
daniel156161 a2414f0b3d
Build and Push Docker Container / build-and-push (push) Successful in 2m18s
feat: add Zenith strategy and move latency tracing
- Register Zenith 1.1 with balanced duel verdicts and forced-outcome overrides.
- Prioritize permanent-trail space in Constrictor and health-aware hazard routes.
- Deduct executor queue delays and return fallback directions on move timeouts.
- Correlate response headers, replay timings, and sampled structured trace logs.
- Move shared metrics publication off the response path without losing local counts.
- Allow configurable tournament opponents and add a psql helper recipe.
- Preserve empty latency values through compact replay encoding and migration.
- Update Arena game links and use default assets for missing customizations.
- Cover strategy, HTTP lifecycle, tracing, replay codecs, and dashboard behavior.
- Validate with 260 passing Python tests, 4 skips, and 2 passing JavaScript tests.
2026-11-05 01:11:31 +01:00

177 lines
8.5 KiB
Python

import asyncio
import json
import sqlite3
import subprocess
import sys
import tempfile
import unittest
from contextlib import closing
from pathlib import Path
from server.database.backend.SqliteGameplayBackend import SqliteGameplayBackend
from server.database.legacy.convert import LegacyDataError, build_legacy_bundle
from server.database.legacy.migrate import migrate_legacy
from server.database.legacy.reader_sqlite import LegacySqliteReader
REPO_ROOT = Path(__file__).resolve().parents[1]
LEGACY_SCHEMA = """
CREATE TABLE games (
game_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, ended_at TEXT,
width INTEGER, height INTEGER, source TEXT, map_name TEXT,
ruleset_name TEXT, ruleset_version TEXT, your_snake_id TEXT,
your_snake_name TEXT, your_snake_type TEXT, your_snake_version TEXT,
game_type TEXT, winner_name TEXT, winner_you INTEGER NOT NULL DEFAULT 0,
final_turn INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'running'
);
CREATE TABLE game_snakes (
game_id TEXT NOT NULL, snake_id TEXT NOT NULL, snake_name TEXT,
is_you INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (game_id, snake_id)
);
CREATE TABLE turns (
id INTEGER PRIMARY KEY AUTOINCREMENT, game_id TEXT NOT NULL, turn INTEGER NOT NULL,
observed_at TEXT NOT NULL, my_move TEXT, my_thinking_json TEXT,
board_state_json TEXT NOT NULL, snakes_json TEXT NOT NULL, you_json TEXT NOT NULL,
food_json TEXT NOT NULL, hazards_json TEXT NOT NULL, UNIQUE (game_id, turn)
);
CREATE TABLE snake_turns (
id INTEGER PRIMARY KEY AUTOINCREMENT, game_id TEXT NOT NULL, turn INTEGER NOT NULL,
snake_id TEXT NOT NULL, snake_name TEXT, health INTEGER, length INTEGER,
head_x INTEGER, head_y INTEGER, body_json TEXT NOT NULL, is_you INTEGER NOT NULL DEFAULT 0,
inferred_move TEXT, latency TEXT, UNIQUE (game_id, turn, snake_id)
);
"""
def _body(*points):
return [{"x": x, "y": y} for x, y in points]
THINKING = {"turn": 1, "health": 99, "length": 3, "food_count": 1, "board_size": "11x11", "scores": {"up": 2.5}}
def create_legacy_database(path:Path, game_snakes:bool=True) -> None:
with closing(sqlite3.connect(path)) as connection, connection:
connection.executescript(LEGACY_SCHEMA)
connection.execute("""
INSERT INTO games (game_id, started_at, ended_at, width, height, source, map_name,
ruleset_name, ruleset_version, your_snake_id, your_snake_name, game_type,
winner_name, winner_you, final_turn, status)
VALUES ('g1', '2026-08-01T10:00:00+00:00', '2026-08-01T10:01:00+00:00', 11, 11,
'league', 'standard', 'standard', 'v1', 'me', 'Me', 'duel', 'Me', 1, 2, 'finished')
""")
if game_snakes:
connection.executemany(
"INSERT INTO game_snakes VALUES (?, ?, ?, ?)",
[("g1", "me", "Me", 1), ("g1", "enemy", "Enemy", 0)],
)
food = json.dumps(_body((5, 5)))
for turn, move, thinking in ((0, None, None), (1, "up", THINKING), (2, "left", {"scores": {"left": 1}})):
connection.execute("""
INSERT INTO turns (game_id, turn, observed_at, my_move, my_thinking_json,
board_state_json, snakes_json, you_json, food_json, hazards_json)
VALUES ('g1', ?, ?, ?, ?, '{}', '[]', '{}', ?, '[]')
""", (turn, f"2026-08-01T10:00:0{turn}.123456+00:00", move, json.dumps(thinking) if thinking else None, food))
me = [_body((1, 1), (1, 1), (1, 1)), _body((1, 2), (1, 1), (1, 1)), _body((0, 2), (1, 2), (1, 1))]
enemy = [_body((9, 9), (9, 9), (9, 9)), _body((9, 8), (9, 9), (9, 9)), _body((9, 7), (9, 8), (9, 9))]
for turn in range(3):
for snake_id, bodies, latency in (("me", me, "12"), ("enemy", enemy, "")):
body = bodies[turn]
connection.execute("""
INSERT INTO snake_turns (game_id, turn, snake_id, snake_name, health, length,
head_x, head_y, body_json, is_you, latency)
VALUES ('g1', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
turn, snake_id, None if game_snakes else snake_id.title(), 100 - turn, len(body),
body[0]["x"], body[0]["y"], json.dumps(body), int(snake_id == "me" and not game_snakes), latency,
))
class TestLegacyConvert(unittest.TestCase):
def test_rejects_inconsistent_rows_instead_of_losing_data(self):
with self.assertRaises(LegacyDataError):
build_legacy_bundle(
{"game_id": "g", "started_at": "2026-01-01"}, [], [],
[{"snake_id": "s", "turn": 0, "length": 5, "body": json.dumps(_body((1, 1)))}],
)
def test_invalid_json_aborts(self):
with self.assertRaises(LegacyDataError):
build_legacy_bundle(
{"game_id": "g", "started_at": "2026-01-01"}, [],
[{"turn": 0, "observed_at": "2026-01-01", "my_thinking": "{invalid"}], [],
)
class TestLegacyMigration(unittest.TestCase):
def _migrate(self, source:Path, destination:Path, **options):
async def run():
backend = SqliteGameplayBackend(str(destination))
with LegacySqliteReader(source) as reader:
stats = await migrate_legacy(reader, backend, **options)
return stats, await backend.get_game_replay("g1")
return asyncio.run(run())
def test_migrates_finished_game_into_identical_replay(self):
with tempfile.TemporaryDirectory() as temp_dir:
source, destination = Path(temp_dir) / "legacy.sqlite3", Path(temp_dir) / "compact.sqlite3"
create_legacy_database(source)
stats, replay = self._migrate(source, destination)
self.assertEqual((stats.games, stats.turns, stats.states, stats.failed), (1, 3, 6, 0))
turns = replay["turns"]
self.assertEqual([turn["my_move"] for turn in turns], [None, "up", "left"])
self.assertEqual(turns[1]["my_thinking"], THINKING)
self.assertEqual(turns[2]["food"], _body((5, 5)))
self.assertEqual(turns[1]["observed_at"], "2026-08-01T10:00:01.123456+00:00")
me = turns[2]["snakes"][0]
self.assertEqual((me["snake_id"], me["snake_name"], me["is_you"]), ("me", "Me", True))
self.assertEqual((me["inferred_move"], me["latency"], me["health"], me["length"]), ("left", "12", 98, 3))
enemy = turns[2]["snakes"][1]
self.assertEqual((enemy["inferred_move"], enemy["latency"]), ("down", ""))
with closing(sqlite3.connect(destination)) as connection:
self.assertEqual(connection.execute("SELECT turn_count FROM game_replays").fetchone()[0], 3)
def test_synthesizes_missing_game_snakes_from_snake_rows(self):
with tempfile.TemporaryDirectory() as temp_dir:
source, destination = Path(temp_dir) / "legacy.sqlite3", Path(temp_dir) / "compact.sqlite3"
create_legacy_database(source, game_snakes=False)
_, replay = self._migrate(source, destination)
snakes = replay["turns"][0]["snakes"]
self.assertEqual([(s["snake_id"], s["snake_name"], s["is_you"]) for s in snakes], [
("me", "Me", True), ("enemy", "Enemy", False),
])
self.assertEqual(snakes[0]["customizations"], {})
def test_quality_filter_keeps_result_but_drops_replay(self):
with tempfile.TemporaryDirectory() as temp_dir:
source, destination = Path(temp_dir) / "legacy.sqlite3", Path(temp_dir) / "compact.sqlite3"
create_legacy_database(source)
stats, replay = self._migrate(source, destination, minimum_quality="high")
self.assertEqual(stats.dropped_replays, 1)
self.assertIsNone(replay)
with closing(sqlite3.connect(destination)) as connection:
row = connection.execute("SELECT has_replay, quality_status, winner_you FROM games").fetchone()
self.assertEqual(row, (0, "low_quality", 1))
self.assertEqual(connection.execute("SELECT COUNT(*) FROM game_replays").fetchone()[0], 0)
def test_script_migrates_in_place_and_drops_legacy_tables(self):
with tempfile.TemporaryDirectory() as temp_dir:
database = Path(temp_dir) / "gameplay.sqlite3"
create_legacy_database(database)
script = [sys.executable, "scripts/migrate_gameplay_to_compact.py", "--source", str(database)]
migrated = subprocess.run(script, cwd=REPO_ROOT, text=True, capture_output=True)
dropped = subprocess.run(script + ["--drop-legacy"], cwd=REPO_ROOT, text=True, capture_output=True)
self.assertEqual(migrated.returncode, 0, migrated.stderr)
self.assertEqual(dropped.returncode, 0, dropped.stderr)
with closing(sqlite3.connect(database)) as connection:
tables = {row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type = 'table'")}
self.assertFalse({"turns", "snake_turns"} & tables)
replay = asyncio.run(SqliteGameplayBackend(str(database)).get_game_replay("g1"))
self.assertEqual(len(replay["turns"]), 3)
if __name__ == "__main__":
unittest.main()