- Implemented a local web server using http.server to serve the save editor. - Created a user interface with HTML, CSS, and JavaScript for managing game slots and editing save files. - Added API endpoints for retrieving slot information, overview, and raw data for editing. - Implemented functionality for saving changes back to the game files. - Added tests for ARR and DTA formats to ensure data integrity during round-trip serialization. Co-authored-by: Codex <noreply@openai.com>
139 lines
4.2 KiB
Python
139 lines
4.2 KiB
Python
"""Testy formatów. Najważniejszy to round-trip po prawdziwych plikach gry:
|
|
jeśli parser + writer nie odtwarzają bajtów, edytor po cichu psuje zapis.
|
|
|
|
Ustaw RIC_GAME żeby uruchomić testy korpusowe:
|
|
RIC_GAME="$HOME/Reksio/Reksio i Czarodzieje" pytest
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from ricsave import arr, catalog, dta
|
|
from ricsave.slot import Game
|
|
|
|
GAME_ROOT = os.environ.get("RIC_GAME")
|
|
needs_game = pytest.mark.skipif(not GAME_ROOT, reason="brak RIC_GAME")
|
|
|
|
|
|
# ---- format ARR ----------------------------------------------------------
|
|
|
|
def test_arr_empty():
|
|
assert arr.dumps([]) == b"\x00\x00\x00\x00"
|
|
assert arr.loads(b"\x00\x00\x00\x00") == []
|
|
|
|
|
|
def test_arr_mixed_roundtrip():
|
|
values = [0, -1, 2**31 - 1, "GULDRYK", "", "11A"]
|
|
assert arr.loads(arr.dumps(values)) == values
|
|
|
|
|
|
def test_arr_rejects_trailing_garbage():
|
|
with pytest.raises(arr.ArrError, match="śmieci"):
|
|
arr.loads(b"\x00\x00\x00\x00\xff")
|
|
|
|
|
|
def test_arr_rejects_unknown_type():
|
|
with pytest.raises(arr.ArrError, match="nieznany typ"):
|
|
arr.loads(b"\x01\x00\x00\x00" + b"\x09\x00\x00\x00")
|
|
|
|
|
|
def test_arr_rejects_bool():
|
|
# bool jest podklasą int — bez jawnej kontroli zapisałby się jako 0/1 po cichu
|
|
with pytest.raises(arr.ArrError, match="bool"):
|
|
arr.dumps([True])
|
|
|
|
|
|
# ---- format DTA ----------------------------------------------------------
|
|
|
|
def test_dta_roundtrip_with_trailing_newline():
|
|
raw = b"0|NULL|0|NULL|0\r\n1|BUTY|3|CAT|1\r\n"
|
|
table = dta.loads(raw)
|
|
assert len(table) == 2
|
|
assert table[1] == ["1", "BUTY", "3", "CAT", "1"]
|
|
assert dta.dumps(table) == raw
|
|
|
|
|
|
def test_dta_roundtrip_without_trailing_newline():
|
|
"""Pliki *_def.dta nie mają końcowego CRLF — writer musi to uszanować."""
|
|
raw = b"A|B\r\nC|D"
|
|
table = dta.loads(raw)
|
|
assert table.trailing_newline is False
|
|
assert dta.dumps(table) == raw
|
|
|
|
|
|
def test_dta_tolerates_ragged_rows_but_check_width_catches_them():
|
|
"""fighters.dta ma wiersze 4- i 5-kolumnowe, więc parser nie może być surowy.
|
|
Kontrola szerokości należy do warstwy schematu."""
|
|
table = dta.loads(b"A|B|C\r\nD|E\r\n")
|
|
assert table.widths == {3, 2}
|
|
with pytest.raises(dta.DtaError, match="oczekiwano 3"):
|
|
table.check_width(3, "test")
|
|
|
|
|
|
def test_dta_rejects_unix_newlines():
|
|
with pytest.raises(dta.DtaError, match="LF"):
|
|
dta.loads(b"A|B\nC|D\n")
|
|
|
|
|
|
# ---- korpus: prawdziwe pliki gry ----------------------------------------
|
|
|
|
@needs_game
|
|
def test_all_arr_files_roundtrip_byte_for_byte():
|
|
common = Game(GAME_ROOT).common
|
|
checked = 0
|
|
for path in common.iterdir():
|
|
if path.suffix.lower() not in (".arr", ".sav"):
|
|
continue
|
|
raw = path.read_bytes()
|
|
assert arr.dumps(arr.loads(raw)) == raw, f"round-trip nie wyszedł: {path.name}"
|
|
checked += 1
|
|
assert checked > 50, f"sprawdzono tylko {checked} plików .ARR — coś nie tak ze ścieżką"
|
|
|
|
|
|
@needs_game
|
|
def test_all_dta_files_roundtrip_byte_for_byte():
|
|
common = Game(GAME_ROOT).common
|
|
checked = 0
|
|
for path in common.iterdir():
|
|
if path.suffix.lower() != ".dta":
|
|
continue
|
|
raw = path.read_bytes()
|
|
assert dta.dumps(dta.loads(raw)) == raw, f"round-trip nie wyszedł: {path.name}"
|
|
checked += 1
|
|
assert checked > 100, f"sprawdzono tylko {checked} plików .DTA"
|
|
|
|
|
|
@needs_game
|
|
def test_manifest_matches_files_on_disk():
|
|
game = Game(GAME_ROOT)
|
|
missing = [
|
|
e.filename(0) for e in catalog.MANIFEST
|
|
if not game.path(e, 0).exists()
|
|
]
|
|
assert not missing, f"manifest wymienia pliki, których nie ma: {missing}"
|
|
|
|
|
|
@needs_game
|
|
def test_scene_tables_have_sobject_width():
|
|
slot = Game(GAME_ROOT).slot(0)
|
|
for name in catalog.SCENES:
|
|
table = slot.raw(f"scene:{name}")
|
|
assert table.width == 9, f"{name}: {table.width} kolumn zamiast 9 (SOBJECT)"
|
|
|
|
|
|
@needs_game
|
|
def test_items_table_shape():
|
|
items = Game(GAME_ROOT).slot(0).items
|
|
assert len(items.table) == catalog.ITEM_SLOTS
|
|
assert items.table.width == 5
|
|
|
|
|
|
@needs_game
|
|
def test_live_slot_validates():
|
|
problems = Game(GAME_ROOT).slot(0).validate()
|
|
assert problems == [], problems
|