Implements Phase 4 of the z-machine compatibility plan. Creates automated regression tests that smoke-test all supported games (V3, V5, V8) by loading each story, executing basic commands, and verifying the interpreter doesn't crash. Key features: - Parametrized test covering 7 games (zork1, curses, photopia, Tangle, shade, LostPig, anchor) - QuietScreen class that disables [MORE] prompts for unattended testing - AutoInputStream that auto-feeds commands then exits cleanly - Tests verify: no crashes, unimplemented opcodes, and minimum instruction count - All tests pass in ~2 seconds Tests skip gracefully if story files aren't present, making this safe to run in CI or on systems without all game files.
118 lines
3.5 KiB
Python
118 lines
3.5 KiB
Python
"""Regression tests for z-machine game compatibility.
|
|
|
|
Smoke tests a suite of games to ensure the interpreter can load them,
|
|
execute basic commands, and reach the input prompt without crashing.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from mudlib.zmachine import ZMachine, zscreen, zstream, zui
|
|
from mudlib.zmachine.trivialzui import (
|
|
TrivialAudio,
|
|
TrivialFilesystem,
|
|
TrivialScreen,
|
|
)
|
|
from mudlib.zmachine.zcpu import ZCpuQuit, ZCpuRestart
|
|
|
|
STORIES_DIR = Path(__file__).parent.parent / "content" / "stories"
|
|
|
|
# Game test suite: (filename, commands to feed)
|
|
GAMES = [
|
|
("zork1.z3", ["look", "open mailbox", "read leaflet"]),
|
|
("curses.z5", ["look", "inventory", "north"]),
|
|
("photopia.z5", ["look", "yes", "look"]),
|
|
("Tangle.z5", ["look", "inventory", "north"]),
|
|
("shade.z5", ["look", "inventory", "look"]),
|
|
("LostPig.z8", ["look", "inventory", "north"]),
|
|
("anchor.z8", ["look", "inventory", "north"]),
|
|
]
|
|
|
|
|
|
class QuietScreen(TrivialScreen):
|
|
"""Screen for testing that never shows [MORE] prompts."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
# Set infinite rows to prevent [MORE] prompts
|
|
self._rows = zscreen.INFINITE_ROWS
|
|
|
|
|
|
class AutoInputStream(zstream.ZInputStream):
|
|
"""Input stream that auto-feeds commands."""
|
|
|
|
def __init__(self, commands=None):
|
|
super().__init__()
|
|
self._commands = commands or []
|
|
self._input_count = 0
|
|
|
|
def read_line(self, *args, **kwargs):
|
|
if self._input_count >= len(self._commands):
|
|
raise ZCpuQuit
|
|
cmd = self._commands[self._input_count]
|
|
self._input_count += 1
|
|
return cmd
|
|
|
|
def read_char(self, *args, **kwargs):
|
|
if self._input_count >= len(self._commands):
|
|
raise ZCpuQuit
|
|
cmd = self._commands[self._input_count]
|
|
self._input_count += 1
|
|
# Return first character as ord
|
|
return ord(cmd[0]) if cmd else ord(" ")
|
|
|
|
|
|
@pytest.mark.parametrize("game,commands", GAMES, ids=[g[0] for g in GAMES])
|
|
def test_game_smoke(game, commands):
|
|
"""Run game through interpreter, feed commands, verify no crash."""
|
|
story_path = STORIES_DIR / game
|
|
if not story_path.exists():
|
|
pytest.skip(f"{game} not found in {STORIES_DIR}")
|
|
|
|
# Load story
|
|
story_bytes = story_path.read_bytes()
|
|
|
|
# Create test UI components
|
|
audio = TrivialAudio()
|
|
screen = QuietScreen()
|
|
keyboard = AutoInputStream(commands)
|
|
filesystem = TrivialFilesystem()
|
|
ui = zui.ZUI(audio, screen, keyboard, filesystem)
|
|
|
|
# Create interpreter
|
|
zm = ZMachine(story_bytes, ui)
|
|
|
|
# Step through instructions
|
|
step_count = 0
|
|
max_steps = 500_000 # Enough to get through several commands
|
|
|
|
try:
|
|
while step_count < max_steps:
|
|
(opcode_class, opcode_number, operands) = (
|
|
zm._cpu._opdecoder.get_next_instruction()
|
|
)
|
|
|
|
implemented, func = zm._cpu._get_handler(opcode_class, opcode_number)
|
|
|
|
# If unimplemented, that's a test failure
|
|
assert implemented, (
|
|
f"Unimplemented opcode {opcode_class}:{opcode_number:02x}"
|
|
)
|
|
|
|
# Execute instruction
|
|
func(zm._cpu, *operands)
|
|
|
|
step_count += 1
|
|
|
|
except ZCpuQuit:
|
|
# Normal exit (ran out of commands)
|
|
pass
|
|
except ZCpuRestart:
|
|
# Some games restart - this is fine for smoke test
|
|
pass
|
|
|
|
# Sanity check: at least some instructions executed
|
|
assert step_count >= 100, (
|
|
f"Only {step_count} instructions executed, expected at least 100"
|
|
)
|