mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
tests: cover the basic UI
Drive the line-based UI in-process: input() answers come from a list and the output is captured. The tests walk the main menu through creating, playing, querying, deleting, importing and exporting characters, plus bad menu input and the Ctrl+C handler. The play loop runs on a fake clock that advances a minute per call and stops when time.sleep has been called a set number of times, so it levels up and saves on schedule without waiting. The test helpers move to tests/helpers.py, which now also holds the input() feeder.
This commit is contained in:
4 files changed
+501
-18
No files matched your search
@@ -1,7 +1,9 @@
|
|||||||
"""Characters, rosters and .pqw files for the UI and CLI tests."""
|
"""Characters, saves and prompts shared by the UI and CLI tests."""
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
from test_pqw import played
|
from test_pqw import played
|
||||||
|
|
||||||
from pqcli import pqw
|
from pqcli import pqw
|
||||||
@@ -41,3 +43,21 @@ def write_pqw(path: Path, player: Player, *, online: bool = False) -> Path:
|
|||||||
game["online"] = {"realm": "Alpaquil", "passkey": 1}
|
game["online"] = {"realm": "Alpaquil", "passkey": 1}
|
||||||
path.write_text(pqw.encode(game), encoding="ascii")
|
path.write_text(pqw.encode(game), encoding="ascii")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def feed(monkeypatch: pytest.MonkeyPatch, answers: Iterable[str]) -> None:
|
||||||
|
"""Answer input() prompts in order; running out is a test failure.
|
||||||
|
|
||||||
|
Prompts are echoed so captured output reads like a session.
|
||||||
|
"""
|
||||||
|
it = iter(answers)
|
||||||
|
|
||||||
|
def fake_input(prompt: str = "") -> str:
|
||||||
|
try:
|
||||||
|
answer = next(it)
|
||||||
|
except StopIteration:
|
||||||
|
raise AssertionError(f"unexpected prompt {prompt!r}") from None
|
||||||
|
print(f"{prompt}{answer}")
|
||||||
|
return answer
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.input", fake_input)
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
"""The basic line-based UI, driven in-process through input()."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import signal
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from helpers import DATA, feed, hero, read_names, save_path, write_pqw
|
||||||
|
from test_pqw import played
|
||||||
|
|
||||||
|
from pqcli import pqw
|
||||||
|
from pqcli import roster as roster_module
|
||||||
|
from pqcli.config import PRIME_STATS
|
||||||
|
from pqcli.lingo import to_roman
|
||||||
|
from pqcli.mechanic import Player
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
from pqcli.ui import basic
|
||||||
|
from pqcli.ui.basic import BasicUserInterface
|
||||||
|
|
||||||
|
|
||||||
|
def make_ui(
|
||||||
|
config: Path, *players: Player, use_saves: bool = True
|
||||||
|
) -> BasicUserInterface:
|
||||||
|
args = argparse.Namespace(use_saves=use_saves, cheats=False, colors=True)
|
||||||
|
roster = Roster(save_path(config), list(players))
|
||||||
|
return BasicUserInterface(roster, None, args, DATA)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def no_play(monkeypatch: pytest.MonkeyPatch) -> list[Player]:
|
||||||
|
"""Make play() record the character instead of looping forever."""
|
||||||
|
played: list[Player] = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
BasicUserInterface, "play", lambda self, player: played.append(player)
|
||||||
|
)
|
||||||
|
return played
|
||||||
|
|
||||||
|
|
||||||
|
# main menu ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_quit_saves_the_roster(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path, hero("Ann"))
|
||||||
|
feed(monkeypatch, ["7"])
|
||||||
|
ui.run()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "| __/| | | (_) | (_| |" in out # the logo
|
||||||
|
for line in [
|
||||||
|
"1) Create a new character",
|
||||||
|
"2) Play as character",
|
||||||
|
"3) Query character info",
|
||||||
|
"4) Delete a character",
|
||||||
|
"5) Import a character (.pqw)",
|
||||||
|
"6) Export a character (.pqw)",
|
||||||
|
"7) Quit",
|
||||||
|
]:
|
||||||
|
assert line in out
|
||||||
|
assert read_names(tmp_path) == ["Ann"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_quit_without_saving(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path, hero("Ann"), use_saves=False)
|
||||||
|
feed(monkeypatch, ["7"])
|
||||||
|
ui.run()
|
||||||
|
assert not save_path(tmp_path).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_with_a_character_goes_straight_to_play(
|
||||||
|
tmp_path: Path, no_play: list[Player]
|
||||||
|
) -> None:
|
||||||
|
player = hero("Ann")
|
||||||
|
ui = make_ui(tmp_path, player)
|
||||||
|
ui.player = player
|
||||||
|
ui.run() # no prompts: feed() is not set up, input() would fail
|
||||||
|
assert no_play == [player]
|
||||||
|
|
||||||
|
|
||||||
|
def test_menu_rejects_bad_input(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path)
|
||||||
|
feed(monkeypatch, ["", "two", "0", "8", "-1", "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert out.count("Not a number") == 2
|
||||||
|
assert out.count("Expected a number between 1..7") == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_accepts_several_spellings(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path)
|
||||||
|
answers = ["Y", "yes", "1", "N", "No", "0"]
|
||||||
|
feed(monkeypatch, ["?", *answers])
|
||||||
|
assert [ui.confirm("Sure?") for _ in answers] == [True] * 3 + [False] * 3
|
||||||
|
assert capsys.readouterr().out.count("Sure? [y/n] ") == 7
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("choice", ["2", "3", "4", "6"])
|
||||||
|
def test_choosing_with_no_characters(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
choice: str,
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path)
|
||||||
|
feed(monkeypatch, [choice, "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
assert "No characters to choose from!" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
# creating -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_rerolls_until_accepted(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
no_play: list[Player],
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path)
|
||||||
|
# create, name, race, class, reroll twice, accept, don't play, quit
|
||||||
|
feed(monkeypatch, ["1", "Rolf", "2", "3", "n", "n", "y", "n", "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert out.count("Total: ") == 3
|
||||||
|
for race in DATA.races:
|
||||||
|
assert f") {race.name}\n" in out
|
||||||
|
assert "Do you want to play as your new character? [y/n] n" in out
|
||||||
|
assert no_play == []
|
||||||
|
|
||||||
|
[player] = ui.roster.players
|
||||||
|
assert player.name == "Rolf"
|
||||||
|
assert player.race == DATA.races[1]
|
||||||
|
assert player.class_ == DATA.classes[2]
|
||||||
|
|
||||||
|
# the accepted stats are the last ones shown
|
||||||
|
last_shown = out.split("Is this okay?")[-2].splitlines()[-7:-1]
|
||||||
|
assert last_shown == [
|
||||||
|
f"{stat.value}: {player.stats[stat]}" for stat in PRIME_STATS
|
||||||
|
]
|
||||||
|
assert read_names(tmp_path) == ["Rolf"] # saved on quit
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_then_play(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
no_play: list[Player],
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path)
|
||||||
|
feed(monkeypatch, ["Rolf", "1", "1", "y", "y"])
|
||||||
|
player = ui.create_player()
|
||||||
|
assert no_play == [player]
|
||||||
|
assert ui.roster.players == [player]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_cancelled_by_empty_name(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
ui = make_ui(tmp_path)
|
||||||
|
feed(monkeypatch, [""])
|
||||||
|
assert ui.create_player() is None
|
||||||
|
assert "Cancelled." in capsys.readouterr().out
|
||||||
|
assert ui.roster.players == []
|
||||||
|
|
||||||
|
|
||||||
|
# playing --------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_from_the_menu(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
no_play: list[Player],
|
||||||
|
) -> None:
|
||||||
|
ann, bob = hero("Ann"), hero("Bob")
|
||||||
|
ui = make_ui(tmp_path, ann, bob)
|
||||||
|
feed(monkeypatch, ["2", "x", "3", "2", "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
assert no_play == [bob]
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Choose your character:\n1) Ann\n2) Bob\n" in out
|
||||||
|
assert "Expected a number between 1..2" in out
|
||||||
|
|
||||||
|
|
||||||
|
class StopPlaying(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FastClock:
|
||||||
|
"""Stands in for datetime: every now() is a minute after the last."""
|
||||||
|
|
||||||
|
current = datetime(2024, 1, 1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls) -> datetime:
|
||||||
|
cls.current += timedelta(minutes=1)
|
||||||
|
return cls.current
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def game_loop(monkeypatch: pytest.MonkeyPatch) -> list[float]:
|
||||||
|
"""Run play() on a fast clock, stopping it after 800 rounds.
|
||||||
|
|
||||||
|
Returns the requested sleeps.
|
||||||
|
"""
|
||||||
|
sleeps: list[float] = []
|
||||||
|
|
||||||
|
def sleep(seconds: float) -> None:
|
||||||
|
sleeps.append(seconds)
|
||||||
|
if len(sleeps) == 800:
|
||||||
|
raise StopPlaying
|
||||||
|
|
||||||
|
monkeypatch.setattr(FastClock, "current", datetime(2024, 1, 1))
|
||||||
|
monkeypatch.setattr(basic, "datetime", FastClock)
|
||||||
|
monkeypatch.setattr(roster_module, "datetime", FastClock)
|
||||||
|
monkeypatch.setattr(basic.time, "sleep", sleep)
|
||||||
|
return sleeps
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_prints_the_sheet_on_level_up(
|
||||||
|
tmp_path: Path,
|
||||||
|
game_loop: list[float],
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
# played(0) reaches level 2 within 800 minute-long ticks
|
||||||
|
player = played(0).player
|
||||||
|
ui = make_ui(tmp_path, player, use_saves=False)
|
||||||
|
with pytest.raises(StopPlaying):
|
||||||
|
ui.play(player)
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert out.startswith("Playing as Exportée\n")
|
||||||
|
sheets = out.split("--- Character Sheet ---")[1:]
|
||||||
|
assert len(sheets) == 2
|
||||||
|
assert "Level: 1\n" in sheets[0]
|
||||||
|
assert "Level: 2\n" in sheets[1]
|
||||||
|
assert player.level == 2
|
||||||
|
# ticks are a minute late, so play() never waits
|
||||||
|
assert set(game_loop) == {0}
|
||||||
|
assert not save_path(tmp_path).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_saves_every_five_minutes(
|
||||||
|
tmp_path: Path, game_loop: list[float], monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
player = played(0).player
|
||||||
|
ui = make_ui(tmp_path, player)
|
||||||
|
saves: list[datetime] = []
|
||||||
|
real_save = ui.roster.save
|
||||||
|
|
||||||
|
def save() -> None:
|
||||||
|
saves.append(FastClock.current)
|
||||||
|
real_save()
|
||||||
|
|
||||||
|
monkeypatch.setattr(ui.roster, "save", save)
|
||||||
|
with pytest.raises(StopPlaying):
|
||||||
|
ui.play(player)
|
||||||
|
|
||||||
|
assert len(saves) > 100
|
||||||
|
gaps = {b - a for a, b in zip(saves, saves[1:], strict=False)}
|
||||||
|
assert min(gaps) >= timedelta(minutes=5)
|
||||||
|
assert read_names(tmp_path) == ["Exportée"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_waits_out_the_rest_of_a_tick(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""With no time passing, each round sleeps a full 100 ms."""
|
||||||
|
sleeps: list[float] = []
|
||||||
|
|
||||||
|
class FrozenClock:
|
||||||
|
@staticmethod
|
||||||
|
def now() -> datetime:
|
||||||
|
return datetime(2024, 1, 1)
|
||||||
|
|
||||||
|
def sleep(seconds: float) -> None:
|
||||||
|
sleeps.append(seconds)
|
||||||
|
if len(sleeps) == 3:
|
||||||
|
raise StopPlaying
|
||||||
|
|
||||||
|
monkeypatch.setattr(basic, "datetime", FrozenClock)
|
||||||
|
monkeypatch.setattr(basic.time, "sleep", sleep)
|
||||||
|
player = played(0).player
|
||||||
|
with pytest.raises(StopPlaying):
|
||||||
|
make_ui(tmp_path, use_saves=False).play(player)
|
||||||
|
assert sleeps == [0.1, 0.1, 0.1]
|
||||||
|
|
||||||
|
|
||||||
|
# info -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_info_for_a_seasoned_character(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
player = hero("Ann", ticks=20000)
|
||||||
|
assert player.spell_book.best is not None
|
||||||
|
ui = make_ui(tmp_path, player)
|
||||||
|
feed(monkeypatch, ["3", "1", "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Name: Ann\n" in out
|
||||||
|
assert f"Race: {player.race.name}\n" in out
|
||||||
|
assert f"Class: {player.class_.name}\n" in out
|
||||||
|
assert f"Level: {player.level}\n" in out
|
||||||
|
assert "No spells memorized yet." not in out
|
||||||
|
for spell in player.spell_book:
|
||||||
|
assert f"\n{spell.name} " in out
|
||||||
|
assert f"Gold: {player.inventory.gold}\n" in out
|
||||||
|
for item in player.inventory:
|
||||||
|
assert f"{item.name}: {item.quantity}\n" in out
|
||||||
|
assert f"Current quest: {player.quest_book.current_quest}\n" in out
|
||||||
|
assert player.task is not None
|
||||||
|
assert f"Current task: {player.task.description}\n" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_info_for_a_new_character(
|
||||||
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
player = played(0, seed="new").player
|
||||||
|
player.task = None
|
||||||
|
make_ui(tmp_path).print_player_info(player)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "No spells memorized yet." in out
|
||||||
|
assert f"Current act: {to_roman(player.quest_book.act)}\n" in out
|
||||||
|
assert "Current task: ?\n" in out
|
||||||
|
|
||||||
|
|
||||||
|
# deleting -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_after_confirming(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
ann, bob = hero("Ann"), hero("Bob")
|
||||||
|
ui = make_ui(tmp_path, ann, bob)
|
||||||
|
feed(monkeypatch, ["4", "1", "n", "4", "1", "y", "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
assert ui.roster.players == [bob]
|
||||||
|
assert capsys.readouterr().out.count("Ann") >= 2
|
||||||
|
assert read_names(tmp_path) == ["Bob"]
|
||||||
|
|
||||||
|
|
||||||
|
# importing and exporting ----------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_import(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
|
write_pqw(tmp_path / "cid.pqw", hero("Cid"), online=True)
|
||||||
|
ui = make_ui(tmp_path / "config", hero("Ann"))
|
||||||
|
feed(monkeypatch, ["5", " ~/cid.pqw ", "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Warning: dropped 'online': pqcli plays offline only\n" in out
|
||||||
|
assert "Imported Cid.\n" in out
|
||||||
|
assert [p.name for p in ui.roster.players] == ["Ann", "Cid"]
|
||||||
|
assert read_names(tmp_path / "config") == ["Ann", "Cid"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_without_saving(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
source = write_pqw(tmp_path / "cid.pqw", hero("Cid"))
|
||||||
|
ui = make_ui(tmp_path, use_saves=False)
|
||||||
|
feed(monkeypatch, [str(source)])
|
||||||
|
ui.import_player()
|
||||||
|
assert [p.name for p in ui.roster.players] == ["Cid"]
|
||||||
|
assert not save_path(tmp_path).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("answer", "message"),
|
||||||
|
[("", "Cancelled."), ("missing.pqw", "Cannot import: cannot read")],
|
||||||
|
)
|
||||||
|
def test_import_failures(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
answer: str,
|
||||||
|
message: str,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
ui = make_ui(tmp_path)
|
||||||
|
feed(monkeypatch, [answer])
|
||||||
|
ui.import_player()
|
||||||
|
assert message in capsys.readouterr().out
|
||||||
|
assert ui.roster.players == []
|
||||||
|
assert not save_path(tmp_path).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_to_the_default_file(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
ui = make_ui(tmp_path, hero("Ann"), hero("Bob"))
|
||||||
|
feed(monkeypatch, ["6", "2", "", "7"])
|
||||||
|
ui.main_menu()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Export to .pqw file [Bob.pqw]: " in out
|
||||||
|
assert "Exported Bob to Bob.pqw.\n" in out
|
||||||
|
assert pqw.read_file(tmp_path / "Bob.pqw", DATA)[0].name == "Bob"
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_to_a_chosen_file(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
|
ui = make_ui(tmp_path, hero("Ann"))
|
||||||
|
feed(monkeypatch, [" ~/out.pqw "])
|
||||||
|
ui.export_player(ui.roster.players[0])
|
||||||
|
assert "Exported Ann to ~/out.pqw.\n" in capsys.readouterr().out
|
||||||
|
assert pqw.read_file(tmp_path / "out.pqw", DATA)[0].name == "Ann"
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_never_overwrites(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
target = tmp_path / "out.pqw"
|
||||||
|
target.write_text("mine")
|
||||||
|
ui = make_ui(tmp_path, hero("Ann"))
|
||||||
|
feed(monkeypatch, [str(target)])
|
||||||
|
ui.export_player(ui.roster.players[0])
|
||||||
|
assert f"Cannot export: {target} already exists" in capsys.readouterr().out
|
||||||
|
assert target.read_text() == "mine"
|
||||||
|
|
||||||
|
|
||||||
|
# Ctrl+C ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("use_saves", [True, False])
|
||||||
|
def test_ctrl_c_saves_and_exits(tmp_path: Path, use_saves: bool) -> None:
|
||||||
|
ui = make_ui(tmp_path, hero("Ann"), use_saves=use_saves)
|
||||||
|
handler = signal.getsignal(signal.SIGINT)
|
||||||
|
assert callable(handler)
|
||||||
|
with pytest.raises(SystemExit) as ex:
|
||||||
|
handler(signal.SIGINT, None)
|
||||||
|
assert ex.value.code == 0
|
||||||
|
assert save_path(tmp_path).exists() == use_saves
|
||||||
|
assert ui.roster.players # still there
|
||||||
+10
-16
@@ -1,11 +1,19 @@
|
|||||||
"""pqcli's command line, run in-process so coverage sees it."""
|
"""pqcli's command line, run in-process so coverage sees it."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from saves import DATA, hero, read_names, save_path, write_pqw, write_roster
|
from helpers import (
|
||||||
|
DATA,
|
||||||
|
feed,
|
||||||
|
hero,
|
||||||
|
read_names,
|
||||||
|
save_path,
|
||||||
|
write_pqw,
|
||||||
|
write_roster,
|
||||||
|
)
|
||||||
|
|
||||||
import pqcli.__main__ as cli
|
import pqcli.__main__ as cli
|
||||||
from pqcli import pqw
|
from pqcli import pqw
|
||||||
@@ -55,20 +63,6 @@ def run(config: Path, monkeypatch: pytest.MonkeyPatch) -> Run:
|
|||||||
return run
|
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:
|
def test_defaults_to_the_curses_ui(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.undo()
|
monkeypatch.undo()
|
||||||
monkeypatch.setattr("sys.argv", ["pqcli"])
|
monkeypatch.setattr("sys.argv", ["pqcli"])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from saves import hero
|
from helpers import hero
|
||||||
|
|
||||||
from pqcli import roster as roster_module
|
from pqcli import roster as roster_module
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
|
|||||||
Reference in new issue
Block a user