Add tests for embedded z-machine MUD integration
Unit tests for MUD UI components (screen, input stream, filesystem) and integration tests with real zork1.z3 (session lifecycle, escape commands, save/restore round-trip, state inspection).
This commit is contained in:
parent
7c1d1efcdb
commit
b6d933acc5
2 changed files with 250 additions and 5 deletions
242
tests/test_embedded_if.py
Normal file
242
tests/test_embedded_if.py
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
"""Tests for embedded z-machine MUD integration.
|
||||||
|
|
||||||
|
Tests the MUD UI components and EmbeddedIFSession integration with real zork1.z3.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mudlib.zmachine.mud_ui import MudFilesystem, MudInputStream, MudScreen
|
||||||
|
|
||||||
|
ZORK_PATH = Path(__file__).parent.parent / "content" / "stories" / "zork1.z3"
|
||||||
|
requires_zork = pytest.mark.skipif(not ZORK_PATH.exists(), reason="zork1.z3 not found")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MockWriter:
|
||||||
|
def write(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def drain(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Unit tests for MUD UI components
|
||||||
|
|
||||||
|
|
||||||
|
def test_mud_screen_captures_output():
|
||||||
|
"""MudScreen captures written text and flush returns it."""
|
||||||
|
screen = MudScreen()
|
||||||
|
screen.write("Hello ")
|
||||||
|
screen.write("world!")
|
||||||
|
output = screen.flush()
|
||||||
|
assert output == "Hello world!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mud_screen_flush_clears_buffer():
|
||||||
|
"""MudScreen flush clears buffer, second flush returns empty."""
|
||||||
|
screen = MudScreen()
|
||||||
|
screen.write("test")
|
||||||
|
first = screen.flush()
|
||||||
|
assert first == "test"
|
||||||
|
|
||||||
|
second = screen.flush()
|
||||||
|
assert second == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_mud_input_stream_feed_and_read():
|
||||||
|
"""MudInputStream feed and read_line work with threading."""
|
||||||
|
stream = MudInputStream()
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def reader():
|
||||||
|
result.append(stream.read_line())
|
||||||
|
|
||||||
|
t = threading.Thread(target=reader)
|
||||||
|
t.start()
|
||||||
|
# Wait for stream to signal it's waiting
|
||||||
|
stream._waiting.wait(timeout=2)
|
||||||
|
stream.feed("hello")
|
||||||
|
t.join(timeout=2)
|
||||||
|
assert result == ["hello"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mud_filesystem_save_restore(tmp_path):
|
||||||
|
"""MudFilesystem save and restore bytes correctly."""
|
||||||
|
save_path = tmp_path / "test.qzl"
|
||||||
|
filesystem = MudFilesystem(save_path)
|
||||||
|
|
||||||
|
test_data = b"\x01\x02\x03\x04\x05"
|
||||||
|
success = filesystem.save_game(test_data)
|
||||||
|
assert success
|
||||||
|
assert save_path.exists()
|
||||||
|
|
||||||
|
restored = filesystem.restore_game()
|
||||||
|
assert restored == test_data
|
||||||
|
|
||||||
|
|
||||||
|
# Integration tests with real zork1.z3
|
||||||
|
|
||||||
|
|
||||||
|
@requires_zork
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_embedded_session_start():
|
||||||
|
"""EmbeddedIFSession starts and returns intro containing game info."""
|
||||||
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
mock_writer = MockWriter()
|
||||||
|
player = Player(name="tester", x=5, y=5, writer=mock_writer)
|
||||||
|
|
||||||
|
session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
intro = await session.start()
|
||||||
|
|
||||||
|
assert intro is not None
|
||||||
|
assert len(intro) > 0
|
||||||
|
# Intro should contain game title or location
|
||||||
|
assert "ZORK" in intro or "West of House" in intro
|
||||||
|
|
||||||
|
|
||||||
|
@requires_zork
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_embedded_session_handle_input():
|
||||||
|
"""EmbeddedIFSession handles input and returns response."""
|
||||||
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
mock_writer = MockWriter()
|
||||||
|
player = Player(name="tester", x=5, y=5, writer=mock_writer)
|
||||||
|
|
||||||
|
session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
await session.start()
|
||||||
|
|
||||||
|
response = await session.handle_input("look")
|
||||||
|
|
||||||
|
assert response is not None
|
||||||
|
assert response.done is False
|
||||||
|
assert len(response.output) > 0
|
||||||
|
# Looking should describe the starting location
|
||||||
|
assert "West of House" in response.output or "house" in response.output
|
||||||
|
|
||||||
|
|
||||||
|
@requires_zork
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_embedded_session_escape_help():
|
||||||
|
"""EmbeddedIFSession ::help returns help text."""
|
||||||
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
mock_writer = MockWriter()
|
||||||
|
player = Player(name="tester", x=5, y=5, writer=mock_writer)
|
||||||
|
|
||||||
|
session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
await session.start()
|
||||||
|
|
||||||
|
response = await session.handle_input("::help")
|
||||||
|
|
||||||
|
assert response.done is False
|
||||||
|
assert "::quit" in response.output
|
||||||
|
assert "::save" in response.output
|
||||||
|
assert "::help" in response.output
|
||||||
|
|
||||||
|
|
||||||
|
@requires_zork
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_embedded_session_escape_quit():
|
||||||
|
"""EmbeddedIFSession ::quit returns done=True."""
|
||||||
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
mock_writer = MockWriter()
|
||||||
|
player = Player(name="tester", x=5, y=5, writer=mock_writer)
|
||||||
|
|
||||||
|
session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
await session.start()
|
||||||
|
|
||||||
|
response = await session.handle_input("::quit")
|
||||||
|
|
||||||
|
assert response.done is True
|
||||||
|
assert "saved" in response.output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@requires_zork
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_embedded_session_location_name():
|
||||||
|
"""EmbeddedIFSession get_location_name returns location after input."""
|
||||||
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
mock_writer = MockWriter()
|
||||||
|
player = Player(name="tester", x=5, y=5, writer=mock_writer)
|
||||||
|
|
||||||
|
session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
await session.start()
|
||||||
|
|
||||||
|
# Send a command to advance game state
|
||||||
|
await session.handle_input("look")
|
||||||
|
|
||||||
|
location = session.get_location_name()
|
||||||
|
|
||||||
|
# Location may be None or a string depending on game state
|
||||||
|
assert location is None or isinstance(location, str)
|
||||||
|
|
||||||
|
|
||||||
|
@requires_zork
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_embedded_session_room_objects():
|
||||||
|
"""EmbeddedIFSession get_room_objects returns a list after start."""
|
||||||
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
mock_writer = MockWriter()
|
||||||
|
player = Player(name="tester", x=5, y=5, writer=mock_writer)
|
||||||
|
|
||||||
|
session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
await session.start()
|
||||||
|
|
||||||
|
objects = session.get_room_objects()
|
||||||
|
|
||||||
|
assert isinstance(objects, list)
|
||||||
|
# Zork1 starting location usually has some objects
|
||||||
|
assert len(objects) >= 0 # May or may not have visible objects initially
|
||||||
|
|
||||||
|
|
||||||
|
@requires_zork
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_embedded_session_save_and_restore():
|
||||||
|
"""Save a game, create new session, restore it."""
|
||||||
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
mock_writer = MockWriter()
|
||||||
|
# Start first session
|
||||||
|
player = Player(name="testplayer", writer=mock_writer, x=0, y=0)
|
||||||
|
session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
await session.start()
|
||||||
|
|
||||||
|
# Do something to change state
|
||||||
|
await session.handle_input("open mailbox")
|
||||||
|
|
||||||
|
# Save
|
||||||
|
save_result = await session.handle_input("::save")
|
||||||
|
assert "saved" in save_result.output.lower()
|
||||||
|
await session.stop()
|
||||||
|
|
||||||
|
# Start new session - should auto-restore via play.py pattern
|
||||||
|
session2 = EmbeddedIFSession(player, str(ZORK_PATH), "zork1")
|
||||||
|
await session2.start()
|
||||||
|
result = await session2._do_restore()
|
||||||
|
assert result # Should have restored
|
||||||
|
|
||||||
|
# The game state should reflect the restored state
|
||||||
|
# (location may differ after restore, just verify it works)
|
||||||
|
response = await session2.handle_input("look")
|
||||||
|
assert response.output # Should get some output
|
||||||
|
await session2.stop()
|
||||||
|
|
||||||
|
# Clean up save file
|
||||||
|
if session2.save_path.exists():
|
||||||
|
session2.save_path.unlink()
|
||||||
|
|
@ -70,9 +70,9 @@ async def test_play_enters_if_mode(player):
|
||||||
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
||||||
MockIFSession.return_value = mock_session
|
MockIFSession.return_value = mock_session
|
||||||
|
|
||||||
# Ensure story file exists check passes
|
# Use .z5 to test dfrotz path
|
||||||
with patch("mudlib.commands.play._find_story") as mock_find:
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
||||||
mock_find.return_value = "/fake/path/zork1.z3"
|
mock_find.return_value = "/fake/path/zork1.z5"
|
||||||
|
|
||||||
await cmd_play(player, "zork1")
|
await cmd_play(player, "zork1")
|
||||||
|
|
||||||
|
|
@ -108,8 +108,9 @@ async def test_play_handles_dfrotz_missing(player):
|
||||||
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
||||||
MockIFSession.return_value = mock_session
|
MockIFSession.return_value = mock_session
|
||||||
|
|
||||||
|
# Use .z5 to test dfrotz path
|
||||||
with patch("mudlib.commands.play._find_story") as mock_find:
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
||||||
mock_find.return_value = "/fake/path/zork1.z3"
|
mock_find.return_value = "/fake/path/zork1.z5"
|
||||||
|
|
||||||
await cmd_play(player, "zork1")
|
await cmd_play(player, "zork1")
|
||||||
|
|
||||||
|
|
@ -147,8 +148,9 @@ async def test_play_restores_save_if_exists(player):
|
||||||
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
||||||
MockIFSession.return_value = mock_session
|
MockIFSession.return_value = mock_session
|
||||||
|
|
||||||
|
# Use .z5 to test dfrotz path
|
||||||
with patch("mudlib.commands.play._find_story") as mock_find:
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
||||||
mock_find.return_value = "/fake/path/zork1.z3"
|
mock_find.return_value = "/fake/path/zork1.z5"
|
||||||
|
|
||||||
await cmd_play(player, "zork1")
|
await cmd_play(player, "zork1")
|
||||||
|
|
||||||
|
|
@ -186,8 +188,9 @@ async def test_play_no_restore_if_no_save(player):
|
||||||
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
||||||
MockIFSession.return_value = mock_session
|
MockIFSession.return_value = mock_session
|
||||||
|
|
||||||
|
# Use .z5 to test dfrotz path
|
||||||
with patch("mudlib.commands.play._find_story") as mock_find:
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
||||||
mock_find.return_value = "/fake/path/zork1.z3"
|
mock_find.return_value = "/fake/path/zork1.z5"
|
||||||
|
|
||||||
await cmd_play(player, "zork1")
|
await cmd_play(player, "zork1")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue