mud/tests/test_quetzal_writer.py
Jared Miller b0fb9b5e2c
Wire op_restore to QuetzalParser and filesystem
Implement V3 restore opcode:
- Add QuetzalParser.load_from_bytes() to parse save data from memory
- Wire op_restore to call filesystem.restore_game() and parse result
- Validate IFhd matches current story (release/serial/checksum)
- Restore dynamic memory, call stack, and program counter
- Branch true on success, false on failure/cancellation

Fix IFF chunk padding bug:
- Add padding byte to odd-length chunks in QuetzalWriter
- Ensures proper chunk alignment for parser compatibility

Add comprehensive tests:
- Branch false when filesystem returns None
- Branch false without zmachine reference
- Branch true on successful restore
- Verify memory state matches saved values
- Handle malformed save data gracefully
2026-02-10 10:13:45 -05:00

417 lines
12 KiB
Python

"""Tests for QuetzalWriter Stks chunk generation."""
class TestStksChunkGeneration:
"""Test Stks chunk generation and serialization."""
def test_empty_stack_serialization(self):
"""Test serializing an empty stack (just the bottom sentinel)."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalWriter
from mudlib.zmachine.zstackmanager import ZStackManager
# Setup: create a mock zmachine with only the bottom sentinel
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
stack_manager = ZStackManager(zmachine._mem)
zmachine._stackmanager = stack_manager
writer = QuetzalWriter(zmachine)
result = writer._generate_stks_chunk()
# Empty stack should serialize to empty bytes (no frames)
assert result == b""
def test_single_frame_no_locals_no_stack(self):
"""Test serializing a single routine frame with no locals or stack values."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalWriter
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
stack_manager = ZStackManager(zmachine._mem)
# Create a routine with no locals and no stack values
routine = ZRoutine(
start_addr=0x5000,
return_addr=0x1234,
zmem=zmachine._mem,
args=[],
local_vars=[],
stack=[],
)
stack_manager.push_routine(routine)
zmachine._stackmanager = stack_manager
writer = QuetzalWriter(zmachine)
result = writer._generate_stks_chunk()
# Expected format:
# Bytes 0-2: return_pc = 0x1234 (24-bit big-endian)
# Byte 3: flags = 0 (0 local vars)
# Byte 4: varnum = 0 (which variable gets return value)
# Byte 5: argflag = 0
# Bytes 6-7: eval_stack_size = 0
expected = bytes(
[
0x00,
0x12,
0x34, # return_pc (24-bit)
0x00, # flags (0 locals)
0x00, # varnum
0x00, # argflag
0x00,
0x00, # eval_stack_size = 0
]
)
assert result == expected
def test_single_frame_with_locals(self):
"""Test serializing a frame with local variables."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalWriter
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
stack_manager = ZStackManager(zmachine._mem)
# Create a routine with 3 local variables
routine = ZRoutine(
start_addr=0x5000,
return_addr=0x2000,
zmem=zmachine._mem,
args=[],
local_vars=[0x1111, 0x2222, 0x3333],
stack=[],
)
stack_manager.push_routine(routine)
zmachine._stackmanager = stack_manager
writer = QuetzalWriter(zmachine)
result = writer._generate_stks_chunk()
# Expected format:
# Bytes 0-2: return_pc = 0x2000
# Byte 3: flags = 3 (3 local vars in bits 0-3)
# Byte 4: varnum = 0x00
# Byte 5: argflag = 0
# Bytes 6-7: eval_stack_size = 0
# Bytes 8-13: three local vars (0x1111, 0x2222, 0x3333)
expected = bytes(
[
0x00,
0x20,
0x00, # return_pc
0x03, # flags (3 locals)
0x00, # varnum
0x00, # argflag
0x00,
0x00, # eval_stack_size = 0
0x11,
0x11, # local_var[0]
0x22,
0x22, # local_var[1]
0x33,
0x33, # local_var[2]
]
)
assert result == expected
def test_single_frame_with_stack_values(self):
"""Test serializing a frame with evaluation stack values."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalWriter
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
stack_manager = ZStackManager(zmachine._mem)
# Create a routine with stack values but no locals
routine = ZRoutine(
start_addr=0x5000,
return_addr=0x3000,
zmem=zmachine._mem,
args=[],
local_vars=[],
stack=[0xABCD, 0xEF01],
)
stack_manager.push_routine(routine)
zmachine._stackmanager = stack_manager
writer = QuetzalWriter(zmachine)
result = writer._generate_stks_chunk()
# Expected format:
# Bytes 0-2: return_pc = 0x3000
# Byte 3: flags = 0 (0 locals)
# Byte 4: varnum = 0x00
# Byte 5: argflag = 0
# Bytes 6-7: eval_stack_size = 2
# Bytes 8-11: two stack values
expected = bytes(
[
0x00,
0x30,
0x00, # return_pc
0x00, # flags (0 locals)
0x00, # varnum
0x00, # argflag
0x00,
0x02, # eval_stack_size = 2
0xAB,
0xCD, # stack[0]
0xEF,
0x01, # stack[1]
]
)
assert result == expected
def test_single_frame_full(self):
"""Test serializing a frame with both locals and stack values."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalWriter
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
stack_manager = ZStackManager(zmachine._mem)
# Create a routine with 2 locals and 3 stack values
routine = ZRoutine(
start_addr=0x5000,
return_addr=0x4567,
zmem=zmachine._mem,
args=[],
local_vars=[0x0001, 0x0002],
stack=[0x1000, 0x2000, 0x3000],
)
stack_manager.push_routine(routine)
zmachine._stackmanager = stack_manager
writer = QuetzalWriter(zmachine)
result = writer._generate_stks_chunk()
expected = bytes(
[
0x00,
0x45,
0x67, # return_pc
0x02, # flags (2 locals)
0x00, # varnum
0x00, # argflag
0x00,
0x03, # eval_stack_size = 3
0x00,
0x01, # local_var[0]
0x00,
0x02, # local_var[1]
0x10,
0x00, # stack[0]
0x20,
0x00, # stack[1]
0x30,
0x00, # stack[2]
]
)
assert result == expected
def test_multiple_nested_frames(self):
"""Test serializing multiple nested routine frames."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalWriter
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
stack_manager = ZStackManager(zmachine._mem)
# Create first routine
routine1 = ZRoutine(
start_addr=0x5000,
return_addr=0x1000,
zmem=zmachine._mem,
args=[],
local_vars=[0xAAAA],
stack=[0xBBBB],
)
stack_manager.push_routine(routine1)
# Create second routine (nested)
routine2 = ZRoutine(
start_addr=0x6000,
return_addr=0x2000,
zmem=zmachine._mem,
args=[],
local_vars=[0xCCCC],
stack=[0xDDDD],
)
stack_manager.push_routine(routine2)
zmachine._stackmanager = stack_manager
writer = QuetzalWriter(zmachine)
result = writer._generate_stks_chunk()
# Expected: two frames concatenated
expected = bytes(
[
# Frame 1
0x00,
0x10,
0x00, # return_pc
0x01, # flags (1 local)
0x00, # varnum
0x00, # argflag
0x00,
0x01, # eval_stack_size = 1
0xAA,
0xAA, # local_var[0]
0xBB,
0xBB, # stack[0]
# Frame 2
0x00,
0x20,
0x00, # return_pc
0x01, # flags (1 local)
0x00, # varnum
0x00, # argflag
0x00,
0x01, # eval_stack_size = 1
0xCC,
0xCC, # local_var[0]
0xDD,
0xDD, # stack[0]
]
)
assert result == expected
def test_bottom_frame_zero_return_pc(self):
"""Test that a bottom/dummy frame with return_pc=0 is handled correctly."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalWriter
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
stack_manager = ZStackManager(zmachine._mem)
# Create a routine with return_addr=0 (main routine)
routine = ZRoutine(
start_addr=0x5000,
return_addr=0x0000,
zmem=zmachine._mem,
args=[],
local_vars=[0x1234],
stack=[],
)
stack_manager.push_routine(routine)
zmachine._stackmanager = stack_manager
writer = QuetzalWriter(zmachine)
result = writer._generate_stks_chunk()
# Expected format with return_pc = 0
expected = bytes(
[
0x00,
0x00,
0x00, # return_pc = 0 (main routine)
0x01, # flags (1 local)
0x00, # varnum
0x00, # argflag
0x00,
0x00, # eval_stack_size = 0
0x12,
0x34, # local_var[0]
]
)
assert result == expected
class TestStksRoundTrip:
"""Test that Stks serialization/deserialization is symmetrical."""
def test_round_trip_serialization(self):
"""Test that we can serialize and deserialize frames correctly."""
from unittest.mock import Mock
from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
zmachine = Mock()
zmachine._mem = Mock()
zmachine._mem.version = 5
# Create original stack with multiple frames
original_stack = ZStackManager(zmachine._mem)
routine1 = ZRoutine(
start_addr=0x5000,
return_addr=0x1234,
zmem=zmachine._mem,
args=[],
local_vars=[0x0001, 0x0002, 0x0003],
stack=[0x1111, 0x2222],
)
original_stack.push_routine(routine1)
routine2 = ZRoutine(
start_addr=0x6000,
return_addr=0x5678,
zmem=zmachine._mem,
args=[],
local_vars=[0xAAAA],
stack=[0xBBBB, 0xCCCC, 0xDDDD],
)
original_stack.push_routine(routine2)
zmachine._stackmanager = original_stack
# Serialize
writer = QuetzalWriter(zmachine)
stks_data = writer._generate_stks_chunk()
# Deserialize
parser = QuetzalParser(zmachine)
parser._parse_stks(stks_data)
# Verify the deserialized stack matches
restored_stack = zmachine._stackmanager
# Should have 2 frames plus the bottom sentinel
assert len(restored_stack._call_stack) == 3
# Check frame 1
frame1 = restored_stack._call_stack[1]
assert frame1.return_addr == 0x1234
assert frame1.local_vars[:3] == [0x0001, 0x0002, 0x0003]
assert frame1.stack == [0x1111, 0x2222]
# Check frame 2
frame2 = restored_stack._call_stack[2]
assert frame2.return_addr == 0x5678
assert frame2.local_vars[:1] == [0xAAAA]
assert frame2.stack == [0xBBBB, 0xCCCC, 0xDDDD]