mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
tests: cover the roster and the command line
The roster tests pin down the .old backup rotation and the five minute periodic save, with a fake clock in place of datetime. The command line tests call main() in-process with the save and data paths in a temporary directory, so coverage counts them. They cover listing, loading, importing and exporting saves, dumping and loading word lists, and the first-run character creation. The curses UI is swapped for a stub there so a stray code path fails instead of taking over the terminal. --load-save 0 falls through to the menu and --load-save -1 plays the last character instead of being rejected; both are marked xfail. A shared fixture restores pytest's SIGINT handler after the UIs install their own.
This commit is contained in:
4 files changed
+561
No files matched your search
@@ -1,4 +1,16 @@
|
|||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Iterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _restore_sigint() -> Iterator[None]:
|
||||||
|
"""The UIs install their own SIGINT handler; keep pytest's."""
|
||||||
|
handler = signal.getsignal(signal.SIGINT)
|
||||||
|
yield
|
||||||
|
signal.signal(signal.SIGINT, handler)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""Characters, rosters and .pqw files for the UI and CLI tests."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from test_pqw import played
|
||||||
|
|
||||||
|
from pqcli import pqw
|
||||||
|
from pqcli.game_data import load_game_data
|
||||||
|
from pqcli.mechanic import Player
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
|
||||||
|
DATA = load_game_data()
|
||||||
|
|
||||||
|
|
||||||
|
def hero(name: str = "Exportée", ticks: int = 300) -> Player:
|
||||||
|
"""A character that has played for a while."""
|
||||||
|
player = played(ticks).player
|
||||||
|
player.name = name
|
||||||
|
return player
|
||||||
|
|
||||||
|
|
||||||
|
def save_path(config: Path) -> Path:
|
||||||
|
"""Where pqcli keeps its roster with XDG_CONFIG_HOME set to config."""
|
||||||
|
return config / "pqcli" / "save.dat"
|
||||||
|
|
||||||
|
|
||||||
|
def write_roster(config: Path, *players: Player) -> Path:
|
||||||
|
path = save_path(config)
|
||||||
|
Roster(path, list(players)).save()
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def read_names(config: Path) -> list[str]:
|
||||||
|
return [player.name for player in Roster.load(save_path(config)).players]
|
||||||
|
|
||||||
|
|
||||||
|
def write_pqw(path: Path, player: Player, *, online: bool = False) -> Path:
|
||||||
|
"""Write player as a .pqw; online adds fields the import warns about."""
|
||||||
|
game = pqw.export_game(player, DATA)
|
||||||
|
if online:
|
||||||
|
game["online"] = {"realm": "Alpaquil", "passkey": 1}
|
||||||
|
path.write_text(pqw.encode(game), encoding="ascii")
|
||||||
|
return path
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
"""pqcli's command line, run in-process so coverage sees it."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from saves import DATA, hero, read_names, save_path, write_pqw, write_roster
|
||||||
|
|
||||||
|
import pqcli.__main__ as cli
|
||||||
|
from pqcli import pqw
|
||||||
|
from pqcli.game_data import DATA_FILES
|
||||||
|
from pqcli.mechanic import Player
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
from pqcli.ui.basic import BasicUserInterface
|
||||||
|
from pqcli.ui.curses import CursesUserInterface
|
||||||
|
|
||||||
|
Run = Callable[..., int]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||||
|
"""Point the save and data paths at tmp_path/config."""
|
||||||
|
config = tmp_path / "config"
|
||||||
|
monkeypatch.setattr(cli, "SAVE_PATH", save_path(config))
|
||||||
|
monkeypatch.setattr(cli, "DATA_PATH", config / "pqcli" / "data")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class NoCurses:
|
||||||
|
"""Fails the test instead of taking over the terminal with curses."""
|
||||||
|
|
||||||
|
def __init__(self, *args: object) -> None:
|
||||||
|
raise AssertionError("the curses UI was started")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _no_curses(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(cli, "CursesUserInterface", NoCurses)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def run(config: Path, monkeypatch: pytest.MonkeyPatch) -> Run:
|
||||||
|
"""Call main() with the given arguments and return its exit code."""
|
||||||
|
|
||||||
|
def run(*args: str) -> int:
|
||||||
|
monkeypatch.setattr("sys.argv", ["pqcli", *args])
|
||||||
|
try:
|
||||||
|
cli.main()
|
||||||
|
except SystemExit as ex:
|
||||||
|
assert isinstance(ex.code, int | None)
|
||||||
|
return ex.code or 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
def feed(monkeypatch: pytest.MonkeyPatch, answers: Iterable[str]) -> None:
|
||||||
|
"""Answer input() prompts in order; running out is a test failure."""
|
||||||
|
it = iter(answers)
|
||||||
|
|
||||||
|
def fake_input(prompt: str = "") -> str:
|
||||||
|
print(prompt)
|
||||||
|
try:
|
||||||
|
return next(it)
|
||||||
|
except StopIteration:
|
||||||
|
raise AssertionError(f"unexpected prompt {prompt!r}") from None
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.input", fake_input)
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults_to_the_curses_ui(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.undo()
|
||||||
|
monkeypatch.setattr("sys.argv", ["pqcli"])
|
||||||
|
args = cli.parse_args()
|
||||||
|
assert args.ui is CursesUserInterface
|
||||||
|
assert args.use_saves
|
||||||
|
assert args.colors
|
||||||
|
assert not args.cheats
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_saves(
|
||||||
|
run: Run, config: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"), hero("Bob"))
|
||||||
|
assert run("--list-saves") == 0
|
||||||
|
assert capsys.readouterr().out == "1. Ann\n2. Bob\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_saves_when_there_are_none(
|
||||||
|
run: Run, config: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
assert run("--list-saves") == 0
|
||||||
|
assert capsys.readouterr().out == ""
|
||||||
|
# no first-run character creation, and nothing written
|
||||||
|
assert not config.exists()
|
||||||
|
|
||||||
|
|
||||||
|
BAD_LOAD_SAVE = "--load-save 0 and negative numbers are not rejected"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"num",
|
||||||
|
[
|
||||||
|
"3",
|
||||||
|
pytest.param(
|
||||||
|
"0", marks=pytest.mark.xfail(strict=True, reason=BAD_LOAD_SAVE)
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"-1", marks=pytest.mark.xfail(strict=True, reason=BAD_LOAD_SAVE)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_load_save_rejects_unknown_character(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
num: str,
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"), hero("Bob"))
|
||||||
|
# 0 falls through to the menu and -1 plays Bob; neither should
|
||||||
|
feed(monkeypatch, ["7"])
|
||||||
|
monkeypatch.setattr(BasicUserInterface, "play", lambda self, p: None)
|
||||||
|
assert run("--basic", "--load-save", num) == 1
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert err == "Invalid player. Available players:\n1. Ann\n2. Bob\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_save_plays_the_character_and_saves(
|
||||||
|
run: Run, config: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"), hero("Bob"))
|
||||||
|
played: list[Player] = []
|
||||||
|
|
||||||
|
def play(self: BasicUserInterface, player: Player) -> None:
|
||||||
|
played.append(player)
|
||||||
|
player.name = "Bobby"
|
||||||
|
|
||||||
|
monkeypatch.setattr(BasicUserInterface, "play", play)
|
||||||
|
assert run("--basic", "--load-save", "2") == 0
|
||||||
|
assert [p.name for p in played] == ["Bobby"]
|
||||||
|
assert read_names(config) == ["Ann", "Bobby"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_save_leaves_the_roster_alone(
|
||||||
|
run: Run, config: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"))
|
||||||
|
|
||||||
|
def play(self: BasicUserInterface, player: Player) -> None:
|
||||||
|
player.name = "Changed"
|
||||||
|
|
||||||
|
monkeypatch.setattr(BasicUserInterface, "play", play)
|
||||||
|
assert run("--basic", "--no-save", "--load-save", "1") == 0
|
||||||
|
assert read_names(config) == ["Ann"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_saves_even_when_the_ui_crashes(
|
||||||
|
run: Run, config: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"))
|
||||||
|
|
||||||
|
def play(self: BasicUserInterface, player: Player) -> None:
|
||||||
|
player.name = "Crashed"
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
monkeypatch.setattr(BasicUserInterface, "play", play)
|
||||||
|
with pytest.raises(RuntimeError, match="boom"):
|
||||||
|
run("--basic", "--load-save", "1")
|
||||||
|
assert read_names(config) == ["Crashed"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dump_data(
|
||||||
|
run: Run, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
target = tmp_path / "words"
|
||||||
|
assert run("--dump-data", str(target)) == 0
|
||||||
|
assert capsys.readouterr().out == (
|
||||||
|
f"Wrote {len(DATA_FILES)} files to {target}\n"
|
||||||
|
)
|
||||||
|
assert sorted(p.stem for p in target.iterdir()) == sorted(DATA_FILES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dump_data_refuses_to_overwrite(
|
||||||
|
run: Run, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
target = tmp_path / "words"
|
||||||
|
target.mkdir()
|
||||||
|
existing = target / f"{next(iter(DATA_FILES))}.json"
|
||||||
|
existing.write_text("mine")
|
||||||
|
assert run("--dump-data", str(target)) == 1
|
||||||
|
assert capsys.readouterr().err.startswith("pqcli: refusing to overwrite")
|
||||||
|
assert existing.read_text() == "mine"
|
||||||
|
|
||||||
|
|
||||||
|
def test_data_dir_must_be_a_directory(
|
||||||
|
run: Run, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
not_dir = tmp_path / "file"
|
||||||
|
not_dir.write_text("")
|
||||||
|
assert run("--data-dir", str(not_dir), "--list-saves") == 1
|
||||||
|
assert capsys.readouterr().err == f"pqcli: {not_dir}: not a directory\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_data_dir_with_broken_list(
|
||||||
|
run: Run, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
(tmp_path / "races.json").write_text("{not json")
|
||||||
|
assert run("--data-dir", str(tmp_path), "--list-saves") == 1
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert err.startswith("pqcli: ")
|
||||||
|
assert "races.json" in err
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_data_dir_is_used(
|
||||||
|
run: Run, config: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
data_dir = config / "pqcli" / "data"
|
||||||
|
data_dir.mkdir(parents=True)
|
||||||
|
(data_dir / "races.json").write_text("[]")
|
||||||
|
assert run("--list-saves") == 1
|
||||||
|
assert "races.json" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_saves(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
tmp_path: Path,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"))
|
||||||
|
first = write_pqw(tmp_path / "a.pqw", hero("Bob"))
|
||||||
|
second = write_pqw(tmp_path / "b.pqw", hero("Cid"), online=True)
|
||||||
|
|
||||||
|
assert run("--import-save", str(first), "--import-save", str(second)) == 0
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert out == "Imported Bob\nImported Cid\n"
|
||||||
|
assert (
|
||||||
|
err == f"pqcli: {second}: dropped 'online': pqcli plays offline only\n"
|
||||||
|
)
|
||||||
|
assert read_names(config) == ["Ann", "Bob", "Cid"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_saves_without_saving(
|
||||||
|
run: Run, config: Path, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
source = write_pqw(tmp_path / "a.pqw", hero("Bob"))
|
||||||
|
assert run("--no-save", "--import-save", str(source)) == 0
|
||||||
|
assert not save_path(config).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_stops_at_the_first_bad_file(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
tmp_path: Path,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
good = write_pqw(tmp_path / "a.pqw", hero("Bob"))
|
||||||
|
missing = tmp_path / "missing.pqw"
|
||||||
|
assert run("--import-save", str(good), "--import-save", str(missing)) == 1
|
||||||
|
assert capsys.readouterr().err.startswith(f"pqcli: cannot read {missing}")
|
||||||
|
assert not save_path(config).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_save(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
tmp_path: Path,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"), hero("Bob"))
|
||||||
|
target = tmp_path / "bob.pqw"
|
||||||
|
assert run("--export-save", "2", str(target)) == 0
|
||||||
|
assert capsys.readouterr().out == f"Exported Bob to {target}\n"
|
||||||
|
assert pqw.read_file(target, DATA)[0].name == "Bob"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("num", ["0", "3", "x", "-1"])
|
||||||
|
def test_export_save_rejects_unknown_character(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
tmp_path: Path,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
num: str,
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"), hero("Bob"))
|
||||||
|
target = tmp_path / "x.pqw"
|
||||||
|
assert run("--export-save", num, str(target)) == 1
|
||||||
|
assert capsys.readouterr().err == (
|
||||||
|
"Invalid player. Available players:\n1. Ann\n2. Bob\n"
|
||||||
|
)
|
||||||
|
assert not target.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_save_never_overwrites(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
tmp_path: Path,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"))
|
||||||
|
target = tmp_path / "x.pqw"
|
||||||
|
target.write_text("mine")
|
||||||
|
assert run("--export-save", "1", str(target)) == 1
|
||||||
|
assert capsys.readouterr().err == f"pqcli: {target} already exists\n"
|
||||||
|
assert target.read_text() == "mine"
|
||||||
|
|
||||||
|
|
||||||
|
# first run ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_run_creates_a_character_then_shows_the_menu(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
race, class_ = DATA.races[2], DATA.classes[3]
|
||||||
|
# name, race, class, accept the stats, then quit from the main menu
|
||||||
|
feed(monkeypatch, ["Newbie", "3", "4", "y", "7"])
|
||||||
|
assert run("--basic") == 0
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert out.startswith(
|
||||||
|
"No saved characters found. Starting first-run character creation."
|
||||||
|
)
|
||||||
|
# no "play now?" question on the first run
|
||||||
|
assert "Do you want to play" not in out
|
||||||
|
assert "1) Create a new character" in out
|
||||||
|
|
||||||
|
[player] = Roster.load(save_path(config)).players
|
||||||
|
assert player.name == "Newbie"
|
||||||
|
assert player.race == race
|
||||||
|
assert player.class_ == class_
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_run_can_be_retried(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
feed(monkeypatch, ["", "maybe", "y", "Second", "1", "1", "y", "7"])
|
||||||
|
assert run("--basic") == 0
|
||||||
|
assert read_names(config) == ["Second"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_run_gives_up_without_a_character(
|
||||||
|
run: Run,
|
||||||
|
config: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
feed(monkeypatch, ["", "n"])
|
||||||
|
assert run("--basic") == 1
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert "Cancelled." in out
|
||||||
|
assert err == "A character is required for first run.\n"
|
||||||
|
assert not save_path(config).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_run_is_skipped_with_characters(
|
||||||
|
run: Run, config: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
write_roster(config, hero("Ann"))
|
||||||
|
feed(monkeypatch, ["7"])
|
||||||
|
assert run("--basic") == 0
|
||||||
|
assert read_names(config) == ["Ann"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dump_data_writes_valid_json(run: Run, tmp_path: Path) -> None:
|
||||||
|
target = tmp_path / "words"
|
||||||
|
run("--dump-data", str(target))
|
||||||
|
for path in target.iterdir():
|
||||||
|
json.loads(path.read_text(encoding="utf-8"))
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import pickle
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from saves import hero
|
||||||
|
|
||||||
|
from pqcli import roster as roster_module
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
|
||||||
|
START = datetime(2024, 1, 1, 12, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class Clock:
|
||||||
|
"""Stands in for datetime in pqcli.roster; only now() is used."""
|
||||||
|
|
||||||
|
current = START
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls) -> datetime:
|
||||||
|
return cls.current
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def advance(cls, seconds: float) -> None:
|
||||||
|
cls.current += timedelta(seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clock(monkeypatch: pytest.MonkeyPatch) -> type[Clock]:
|
||||||
|
monkeypatch.setattr(Clock, "current", START)
|
||||||
|
monkeypatch.setattr(roster_module, "datetime", Clock)
|
||||||
|
return Clock
|
||||||
|
|
||||||
|
|
||||||
|
def names(path: Path) -> list[str]:
|
||||||
|
return [player.name for player in pickle.loads(path.read_bytes())]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_missing_file_gives_empty_roster(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "nowhere" / "save.dat"
|
||||||
|
roster = Roster.load(path)
|
||||||
|
assert roster.players == []
|
||||||
|
assert roster.path == path
|
||||||
|
assert not path.parent.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_accepts_a_string_path(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "save.dat"
|
||||||
|
Roster(path, [hero("Ann")]).save()
|
||||||
|
assert Roster.load(str(path)).path == path
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_then_load_round_trip(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "deep" / "dir" / "save.dat"
|
||||||
|
player = hero("Ann", ticks=1000)
|
||||||
|
Roster(path, [player, hero("Bob")]).save()
|
||||||
|
|
||||||
|
loaded = Roster.load(path).players
|
||||||
|
assert [p.name for p in loaded] == ["Ann", "Bob"]
|
||||||
|
assert loaded[0].level == player.level
|
||||||
|
assert loaded[0].inventory.gold == player.inventory.gold
|
||||||
|
assert loaded[0].quest_book.quests == player.quest_book.quests
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_keeps_the_previous_save_as_old(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "save.dat"
|
||||||
|
old = tmp_path / "save.dat.old"
|
||||||
|
roster = Roster(path, [hero("Ann")])
|
||||||
|
|
||||||
|
roster.save()
|
||||||
|
assert names(path) == ["Ann"]
|
||||||
|
assert not old.exists()
|
||||||
|
|
||||||
|
roster.players.append(hero("Bob"))
|
||||||
|
roster.save()
|
||||||
|
assert names(path) == ["Ann", "Bob"]
|
||||||
|
assert names(old) == ["Ann"]
|
||||||
|
|
||||||
|
roster.players.pop(0)
|
||||||
|
roster.save()
|
||||||
|
assert names(path) == ["Bob"]
|
||||||
|
assert names(old) == ["Ann", "Bob"]
|
||||||
|
|
||||||
|
assert sorted(p.name for p in tmp_path.iterdir()) == [
|
||||||
|
"save.dat",
|
||||||
|
"save.dat.old",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_periodically_waits_five_minutes(
|
||||||
|
tmp_path: Path, clock: type[Clock]
|
||||||
|
) -> None:
|
||||||
|
path = tmp_path / "save.dat"
|
||||||
|
roster = Roster(path, [hero("Ann")])
|
||||||
|
|
||||||
|
clock.advance(299)
|
||||||
|
roster.save_periodically()
|
||||||
|
assert not path.exists()
|
||||||
|
|
||||||
|
clock.advance(1)
|
||||||
|
roster.save_periodically()
|
||||||
|
assert names(path) == ["Ann"]
|
||||||
|
|
||||||
|
# the timer starts over after a save
|
||||||
|
roster.players.append(hero("Bob"))
|
||||||
|
clock.advance(299)
|
||||||
|
roster.save_periodically()
|
||||||
|
assert names(path) == ["Ann"]
|
||||||
|
|
||||||
|
clock.advance(1)
|
||||||
|
roster.save_periodically()
|
||||||
|
assert names(path) == ["Ann", "Bob"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_save_restarts_the_periodic_timer(
|
||||||
|
tmp_path: Path, clock: type[Clock]
|
||||||
|
) -> None:
|
||||||
|
path = tmp_path / "save.dat"
|
||||||
|
roster = Roster(path, [hero("Ann")])
|
||||||
|
|
||||||
|
clock.advance(200)
|
||||||
|
roster.save()
|
||||||
|
roster.players.append(hero("Bob"))
|
||||||
|
|
||||||
|
clock.advance(200)
|
||||||
|
roster.save_periodically()
|
||||||
|
assert names(path) == ["Ann"]
|
||||||
|
|
||||||
|
clock.advance(100)
|
||||||
|
roster.save_periodically()
|
||||||
|
assert names(path) == ["Ann", "Bob"]
|
||||||
Reference in new issue
Block a user