"""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