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.
301 lines
11 KiB
Python
301 lines
11 KiB
Python
"""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
|