Add round-trip save/restore integration test
Verifies complete save/restore pipeline by generating save data with QuetzalWriter and restoring it with QuetzalParser. Tests cover: - Basic round-trip with memory, stack, and PC restoration - Multiple nested call frames - Preservation of unchanged memory bytes (run-length encoding) - Empty stack (no routine frames) Each test confirms that state modified after save is correctly restored to original values from the save data.
This commit is contained in:
parent
b0fb9b5e2c
commit
1ffc4e14c2
1 changed files with 301 additions and 0 deletions
301
tests/test_quetzal_roundtrip.py
Normal file
301
tests/test_quetzal_roundtrip.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
"""Integration tests for Quetzal save/restore round-trip.
|
||||
|
||||
Tests that verify the complete save/restore pipeline works end-to-end by
|
||||
generating save data with QuetzalWriter and restoring it with QuetzalParser.
|
||||
"""
|
||||
|
||||
|
||||
class TestQuetzalRoundTrip:
|
||||
"""Test complete save/restore cycle with real zmachine state."""
|
||||
|
||||
def test_basic_round_trip(self):
|
||||
"""Test saving and restoring basic zmachine state."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
|
||||
from mudlib.zmachine.zmemory import ZMemory
|
||||
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
|
||||
|
||||
# Create a minimal z-machine story file (V3)
|
||||
# Based on the Z-machine spec, this is the minimum viable header
|
||||
story_data = bytearray(8192)
|
||||
story_data[0] = 3 # Version 3
|
||||
story_data[0x02:0x04] = [0x12, 0x34] # Release number
|
||||
story_data[0x04:0x06] = [0x10, 0x00] # High memory start
|
||||
story_data[0x06:0x08] = [0x08, 0x00] # Initial PC
|
||||
story_data[0x0E:0x10] = [0x04, 0x00] # Static memory start
|
||||
story_data[0x0C:0x0E] = [0x02, 0x00] # Global variables start
|
||||
story_data[0x12:0x18] = b"860101" # Serial number
|
||||
story_data[0x1C:0x1E] = [0xAB, 0xCD] # Checksum
|
||||
|
||||
# Create pristine and current memory
|
||||
pristine_mem = ZMemory(bytes(story_data))
|
||||
current_mem = ZMemory(bytes(story_data))
|
||||
|
||||
# Modify some bytes in dynamic memory (before static start at 0x400)
|
||||
current_mem[0x100] = 0x42
|
||||
current_mem[0x101] = 0x43
|
||||
current_mem[0x200] = 0xFF
|
||||
|
||||
# Create a stack with one frame
|
||||
stack_manager = ZStackManager(current_mem)
|
||||
routine = ZRoutine(
|
||||
start_addr=0x5000,
|
||||
return_addr=0x1234,
|
||||
zmem=current_mem,
|
||||
args=[],
|
||||
local_vars=[0x0001, 0x0002, 0x0003],
|
||||
stack=[0x1111, 0x2222],
|
||||
)
|
||||
stack_manager.push_routine(routine)
|
||||
|
||||
# Set up mock zmachine
|
||||
zmachine = Mock()
|
||||
zmachine._pristine_mem = pristine_mem
|
||||
zmachine._mem = current_mem
|
||||
zmachine._stackmanager = stack_manager
|
||||
zmachine._cpu = Mock()
|
||||
zmachine._cpu._program_counter = 0x0850 # Current PC
|
||||
zmachine._opdecoder = Mock()
|
||||
|
||||
# SAVE: Generate save data
|
||||
writer = QuetzalWriter(zmachine)
|
||||
save_data = writer.generate_save_data()
|
||||
|
||||
# Verify we got valid IFF data
|
||||
assert save_data[:4] == b"FORM"
|
||||
assert save_data[8:12] == b"IFZS"
|
||||
|
||||
# MODIFY: Change the zmachine state to different values
|
||||
current_mem[0x100] = 0x99
|
||||
current_mem[0x101] = 0x99
|
||||
current_mem[0x200] = 0x00
|
||||
stack_manager._call_stack.clear()
|
||||
from mudlib.zmachine.zstackmanager import ZStackBottom
|
||||
|
||||
stack_manager._call_stack.append(ZStackBottom())
|
||||
zmachine._cpu._program_counter = 0x9999
|
||||
|
||||
# Verify state was actually modified
|
||||
assert current_mem[0x100] == 0x99
|
||||
assert len(stack_manager._call_stack) == 1 # Only bottom sentinel
|
||||
assert zmachine._cpu._program_counter == 0x9999
|
||||
|
||||
# RESTORE: Load save data
|
||||
parser = QuetzalParser(zmachine)
|
||||
parser.load_from_bytes(save_data)
|
||||
|
||||
# VERIFY: Memory was restored
|
||||
assert current_mem[0x100] == 0x42
|
||||
assert current_mem[0x101] == 0x43
|
||||
assert current_mem[0x200] == 0xFF
|
||||
|
||||
# VERIFY: Stack was restored (get fresh reference since parser replaces it)
|
||||
restored_stack = zmachine._stackmanager
|
||||
assert len(restored_stack._call_stack) == 2 # Bottom + one frame
|
||||
restored_frame = restored_stack._call_stack[1]
|
||||
assert restored_frame.return_addr == 0x1234
|
||||
assert restored_frame.local_vars[:3] == [0x0001, 0x0002, 0x0003]
|
||||
assert restored_frame.stack == [0x1111, 0x2222]
|
||||
|
||||
# VERIFY: Program counter was restored
|
||||
assert zmachine._opdecoder.program_counter == 0x0850
|
||||
|
||||
def test_round_trip_with_multiple_frames(self):
|
||||
"""Test save/restore with nested call frames."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
|
||||
from mudlib.zmachine.zmemory import ZMemory
|
||||
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
|
||||
|
||||
# Create minimal story
|
||||
story_data = bytearray(8192)
|
||||
story_data[0] = 3 # Version 3
|
||||
story_data[0x02:0x04] = [0x10, 0x00] # Release
|
||||
story_data[0x04:0x06] = [0x10, 0x00] # High memory
|
||||
story_data[0x06:0x08] = [0x08, 0x00] # Initial PC
|
||||
story_data[0x0E:0x10] = [0x04, 0x00] # Static memory
|
||||
story_data[0x0C:0x0E] = [0x02, 0x00] # Globals
|
||||
story_data[0x12:0x18] = b"860101" # Serial
|
||||
story_data[0x1C:0x1E] = [0x00, 0x00] # Checksum
|
||||
|
||||
pristine_mem = ZMemory(bytes(story_data))
|
||||
current_mem = ZMemory(bytes(story_data))
|
||||
|
||||
# Create nested call frames
|
||||
stack_manager = ZStackManager(current_mem)
|
||||
routine1 = ZRoutine(
|
||||
start_addr=0x5000,
|
||||
return_addr=0x1000,
|
||||
zmem=current_mem,
|
||||
args=[],
|
||||
local_vars=[0xAAAA, 0xBBBB],
|
||||
stack=[0x1111],
|
||||
)
|
||||
stack_manager.push_routine(routine1)
|
||||
|
||||
routine2 = ZRoutine(
|
||||
start_addr=0x6000,
|
||||
return_addr=0x2000,
|
||||
zmem=current_mem,
|
||||
args=[],
|
||||
local_vars=[0xCCCC],
|
||||
stack=[0x2222, 0x3333],
|
||||
)
|
||||
stack_manager.push_routine(routine2)
|
||||
|
||||
# Set up zmachine
|
||||
zmachine = Mock()
|
||||
zmachine._pristine_mem = pristine_mem
|
||||
zmachine._mem = current_mem
|
||||
zmachine._stackmanager = stack_manager
|
||||
zmachine._cpu = Mock()
|
||||
zmachine._cpu._program_counter = 0x0900
|
||||
zmachine._opdecoder = Mock()
|
||||
|
||||
# Save
|
||||
writer = QuetzalWriter(zmachine)
|
||||
save_data = writer.generate_save_data()
|
||||
|
||||
# Clear stack
|
||||
stack_manager._call_stack.clear()
|
||||
from mudlib.zmachine.zstackmanager import ZStackBottom
|
||||
|
||||
stack_manager._call_stack.append(ZStackBottom())
|
||||
|
||||
# Restore
|
||||
parser = QuetzalParser(zmachine)
|
||||
parser.load_from_bytes(save_data)
|
||||
|
||||
# Verify both frames restored (get fresh reference)
|
||||
restored_stack = zmachine._stackmanager
|
||||
assert len(restored_stack._call_stack) == 3 # Bottom + two frames
|
||||
|
||||
frame1 = restored_stack._call_stack[1]
|
||||
assert frame1.return_addr == 0x1000
|
||||
assert frame1.local_vars[:2] == [0xAAAA, 0xBBBB]
|
||||
assert frame1.stack == [0x1111]
|
||||
|
||||
frame2 = restored_stack._call_stack[2]
|
||||
assert frame2.return_addr == 0x2000
|
||||
assert frame2.local_vars[:1] == [0xCCCC]
|
||||
assert frame2.stack == [0x2222, 0x3333]
|
||||
|
||||
def test_round_trip_preserves_unchanged_memory(self):
|
||||
"""Test that unchanged memory bytes are preserved correctly."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
|
||||
from mudlib.zmachine.zmemory import ZMemory
|
||||
from mudlib.zmachine.zstackmanager import ZStackManager
|
||||
|
||||
# Create story with specific pattern in dynamic memory
|
||||
story_data = bytearray(8192)
|
||||
story_data[0] = 3
|
||||
story_data[0x02:0x04] = [0x10, 0x00]
|
||||
story_data[0x04:0x06] = [0x10, 0x00]
|
||||
story_data[0x06:0x08] = [0x08, 0x00]
|
||||
story_data[0x0E:0x10] = [0x04, 0x00]
|
||||
story_data[0x0C:0x0E] = [0x02, 0x00]
|
||||
story_data[0x12:0x18] = b"860101"
|
||||
story_data[0x1C:0x1E] = [0x00, 0x00]
|
||||
|
||||
# Set a pattern in dynamic memory
|
||||
for i in range(0x100, 0x200):
|
||||
story_data[i] = i & 0xFF
|
||||
|
||||
pristine_mem = ZMemory(bytes(story_data))
|
||||
current_mem = ZMemory(bytes(story_data))
|
||||
|
||||
# Only modify a few bytes
|
||||
current_mem[0x150] = 0xFF
|
||||
current_mem[0x180] = 0xAA
|
||||
|
||||
stack_manager = ZStackManager(current_mem)
|
||||
|
||||
zmachine = Mock()
|
||||
zmachine._pristine_mem = pristine_mem
|
||||
zmachine._mem = current_mem
|
||||
zmachine._stackmanager = stack_manager
|
||||
zmachine._cpu = Mock()
|
||||
zmachine._cpu._program_counter = 0x0800
|
||||
zmachine._opdecoder = Mock()
|
||||
|
||||
# Save and restore
|
||||
writer = QuetzalWriter(zmachine)
|
||||
save_data = writer.generate_save_data()
|
||||
|
||||
# Corrupt all dynamic memory
|
||||
for i in range(0x100, 0x200):
|
||||
current_mem[i] = 0x00
|
||||
|
||||
parser = QuetzalParser(zmachine)
|
||||
parser.load_from_bytes(save_data)
|
||||
|
||||
# Verify all bytes restored correctly
|
||||
for i in range(0x100, 0x200):
|
||||
if i == 0x150:
|
||||
assert current_mem[i] == 0xFF
|
||||
elif i == 0x180:
|
||||
assert current_mem[i] == 0xAA
|
||||
else:
|
||||
assert current_mem[i] == (i & 0xFF)
|
||||
|
||||
def test_round_trip_empty_stack(self):
|
||||
"""Test save/restore with no routine frames (just bottom sentinel)."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
|
||||
from mudlib.zmachine.zmemory import ZMemory
|
||||
from mudlib.zmachine.zstackmanager import ZStackManager
|
||||
|
||||
story_data = bytearray(8192)
|
||||
story_data[0] = 3
|
||||
story_data[0x02:0x04] = [0x10, 0x00]
|
||||
story_data[0x04:0x06] = [0x10, 0x00]
|
||||
story_data[0x06:0x08] = [0x08, 0x00]
|
||||
story_data[0x0E:0x10] = [0x04, 0x00]
|
||||
story_data[0x0C:0x0E] = [0x02, 0x00]
|
||||
story_data[0x12:0x18] = b"860101"
|
||||
story_data[0x1C:0x1E] = [0x00, 0x00]
|
||||
|
||||
pristine_mem = ZMemory(bytes(story_data))
|
||||
current_mem = ZMemory(bytes(story_data))
|
||||
current_mem[0x100] = 0x42
|
||||
|
||||
stack_manager = ZStackManager(current_mem)
|
||||
|
||||
zmachine = Mock()
|
||||
zmachine._pristine_mem = pristine_mem
|
||||
zmachine._mem = current_mem
|
||||
zmachine._stackmanager = stack_manager
|
||||
zmachine._cpu = Mock()
|
||||
zmachine._cpu._program_counter = 0x0800
|
||||
zmachine._opdecoder = Mock()
|
||||
|
||||
# Save with empty stack
|
||||
writer = QuetzalWriter(zmachine)
|
||||
save_data = writer.generate_save_data()
|
||||
|
||||
# Add a dummy frame
|
||||
from mudlib.zmachine.zstackmanager import ZRoutine
|
||||
|
||||
dummy = ZRoutine(
|
||||
start_addr=0x5000,
|
||||
return_addr=0x1000,
|
||||
zmem=current_mem,
|
||||
args=[],
|
||||
local_vars=[0x9999],
|
||||
stack=[],
|
||||
)
|
||||
stack_manager.push_routine(dummy)
|
||||
|
||||
# Restore
|
||||
parser = QuetzalParser(zmachine)
|
||||
parser.load_from_bytes(save_data)
|
||||
|
||||
# Should have only bottom sentinel (get fresh reference)
|
||||
restored_stack = zmachine._stackmanager
|
||||
assert len(restored_stack._call_stack) == 1
|
||||
assert current_mem[0x100] == 0x42
|
||||
Loading…
Reference in a new issue