Replaces the async _do_restore() (called after thread launch) with a synchronous _try_restore() called before the thread starts. This eliminates the race condition where restore mutates z-machine state while the interpreter thread is running. The restore prefix message is now part of start()'s return value instead of being sent separately in play.py.
193 lines
5.9 KiB
Python
193 lines
5.9 KiB
Python
"""Tests for the play command."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_writer():
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
return writer
|
|
|
|
|
|
@pytest.fixture
|
|
def player(mock_writer):
|
|
from mudlib.player import Player
|
|
|
|
return Player(name="tester", x=5, y=5, writer=mock_writer)
|
|
|
|
|
|
def test_play_command_registered():
|
|
"""Verify play command is registered."""
|
|
import mudlib.commands.play # noqa: F401
|
|
from mudlib import commands
|
|
|
|
assert "play" in commands._registry
|
|
cmd = commands._registry["play"]
|
|
assert cmd.name == "play"
|
|
assert cmd.mode == "normal"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_no_args(player):
|
|
"""Playing with no args sends usage message."""
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
await cmd_play(player, "")
|
|
player.writer.write.assert_called()
|
|
output = player.writer.write.call_args[0][0]
|
|
assert "play what?" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_unknown_story(player):
|
|
"""Playing unknown story sends error message."""
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
await cmd_play(player, "nosuchgame")
|
|
player.writer.write.assert_called()
|
|
output = player.writer.write.call_args[0][0]
|
|
assert "no story" in output.lower()
|
|
assert "nosuchgame" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_enters_if_mode(player):
|
|
"""Playing a valid story enters IF mode and creates session."""
|
|
from pathlib import Path
|
|
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock IFSession
|
|
mock_session = Mock()
|
|
mock_session.start = AsyncMock(return_value="Welcome to Zork!")
|
|
mock_session.save_path = Mock(spec=Path)
|
|
mock_session.save_path.exists = Mock(return_value=False)
|
|
|
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
|
MockIFSession.return_value = mock_session
|
|
|
|
# Use .z5 to test dfrotz path
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/zork1.z5"
|
|
|
|
await cmd_play(player, "zork1")
|
|
|
|
# Verify session was created and started
|
|
mock_session.start.assert_called_once()
|
|
|
|
# Verify mode was pushed
|
|
assert "if" in player.mode_stack
|
|
|
|
# Verify session was attached to player
|
|
assert player.if_session is mock_session
|
|
|
|
# Verify intro was sent
|
|
player.writer.write.assert_called()
|
|
output = player.writer.write.call_args[0][0]
|
|
assert "Welcome to Zork!" in output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_handles_dfrotz_missing(player):
|
|
"""Playing when dfrotz is missing sends error."""
|
|
from pathlib import Path
|
|
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock IFSession to raise FileNotFoundError on start
|
|
mock_session = Mock()
|
|
mock_session.start = AsyncMock(side_effect=FileNotFoundError())
|
|
mock_session.stop = AsyncMock()
|
|
mock_session.save_path = Mock(spec=Path)
|
|
mock_session.save_path.exists = Mock(return_value=False)
|
|
|
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
|
MockIFSession.return_value = mock_session
|
|
|
|
# Use .z5 to test dfrotz path
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/zork1.z5"
|
|
|
|
await cmd_play(player, "zork1")
|
|
|
|
# Verify error message was sent
|
|
player.writer.write.assert_called()
|
|
output = player.writer.write.call_args[0][0]
|
|
assert "dfrotz not found" in output.lower()
|
|
|
|
# Verify mode was NOT pushed
|
|
assert "if" not in player.mode_stack
|
|
|
|
# Verify session was NOT attached
|
|
assert player.if_session is None
|
|
|
|
# Verify session.stop() was called
|
|
mock_session.stop.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_restores_save_if_exists(player):
|
|
"""Playing restores saved game if save file exists (via start())."""
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock IFSession - restore now happens in start() before thread launches
|
|
mock_session = Mock()
|
|
restored_output = (
|
|
"restoring saved game...\r\nrestored.\r\n\r\n"
|
|
"West of House\nYou are standing in an open field."
|
|
)
|
|
mock_session.start = AsyncMock(return_value=restored_output)
|
|
|
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
|
MockIFSession.return_value = mock_session
|
|
|
|
# Use .z5 to test dfrotz path
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/zork1.z5"
|
|
|
|
await cmd_play(player, "zork1")
|
|
|
|
# Verify session was created and started
|
|
mock_session.start.assert_called_once()
|
|
|
|
# Verify mode was pushed
|
|
assert "if" in player.mode_stack
|
|
|
|
# Verify restored text was sent (start() returns full output with restore)
|
|
calls = [call[0][0] for call in player.writer.write.call_args_list]
|
|
full_output = "".join(calls)
|
|
assert "West of House" in full_output
|
|
assert "open field" in full_output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_no_restore_if_no_save(player):
|
|
"""Playing does not restore if no save file exists."""
|
|
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock IFSession
|
|
mock_session = Mock()
|
|
mock_session.start = AsyncMock(return_value="Welcome to Zork!")
|
|
|
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
|
MockIFSession.return_value = mock_session
|
|
|
|
# Use .z5 to test dfrotz path
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/zork1.z5"
|
|
|
|
await cmd_play(player, "zork1")
|
|
|
|
# Verify session was created and started
|
|
mock_session.start.assert_called_once()
|
|
|
|
# Verify intro was sent but not restore message
|
|
calls = [call[0][0] for call in player.writer.write.call_args_list]
|
|
full_output = "".join(calls)
|
|
assert "Welcome to Zork!" in full_output
|
|
assert "restoring" not in full_output.lower()
|