mud/tests/test_quetzal_writer.py
Jared Miller 8526e48247
Fix Quetzal Stks field mapping: return_pc to caller, varnum to frame
return_pc for each frame belongs on the caller's program_counter (the
resume address when this routine exits). varnum is the store variable
that receives the return value, stored as return_addr on the frame
itself. Also handle flags bit 4 (discard result → return_addr=None).
2026-02-10 12:39:40 -05:00

406 lines
12 KiB
Python

"""Tests for QuetzalWriter Stks chunk generation.
Field mapping:
- Quetzal return_pc → previous frame's program_counter (caller resume PC)
- Quetzal varnum → frame.return_addr (store variable for return value)
"""
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
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()
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)
# Set caller resume PC on sentinel
stack_manager._call_stack[0].program_counter = 0x1234
routine = ZRoutine(
start_addr=0x5000,
return_addr=7, # varnum: store to local var 6
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 = bytes(
[
0x00,
0x12,
0x34, # return_pc (from sentinel.program_counter)
0x00, # flags (0 locals)
0x07, # varnum (frame.return_addr)
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)
stack_manager._call_stack[0].program_counter = 0x2000
routine = ZRoutine(
start_addr=0x5000,
return_addr=0x10, # varnum: store to global var 0x10
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 = bytes(
[
0x00,
0x20,
0x00, # return_pc (from sentinel.program_counter)
0x03, # flags (3 locals)
0x10, # varnum (frame.return_addr)
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)
stack_manager._call_stack[0].program_counter = 0x3000
routine = ZRoutine(
start_addr=0x5000,
return_addr=0, # varnum 0: push result to eval stack
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 = bytes(
[
0x00,
0x30,
0x00, # return_pc
0x00, # flags (0 locals)
0x00, # varnum (push to stack)
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)
stack_manager._call_stack[0].program_counter = 0x4567
routine = ZRoutine(
start_addr=0x5000,
return_addr=2, # varnum: store to local var 1
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 (from sentinel.program_counter)
0x02, # flags (2 locals)
0x02, # 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)
# Sentinel has return PC for frame 1
stack_manager._call_stack[0].program_counter = 0x1000
routine1 = ZRoutine(
start_addr=0x5000,
return_addr=0, # varnum: push to stack
zmem=zmachine._mem,
args=[],
local_vars=[0xAAAA],
stack=[0xBBBB],
)
stack_manager.push_routine(routine1)
# Frame1 has return PC for frame 2
routine1.program_counter = 0x2000
routine2 = ZRoutine(
start_addr=0x6000,
return_addr=5, # varnum: store to local var 4
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 = bytes(
[
# Frame 1: return_pc from sentinel (0x1000), varnum=0
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: return_pc from routine1 (0x2000), varnum=5
0x00,
0x20,
0x00, # return_pc
0x01, # flags (1 local)
0x05, # 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)
# Sentinel PC = 0 (main routine has no caller)
stack_manager._call_stack[0].program_counter = 0
routine = ZRoutine(
start_addr=0x5000,
return_addr=0, # varnum: push to stack
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 = bytes(
[
0x00,
0x00,
0x00, # return_pc = 0
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
original_stack = ZStackManager(zmachine._mem)
# Sentinel has return PC for frame 1
original_stack._call_stack[0].program_counter = 0
routine1 = ZRoutine(
start_addr=0x5000,
return_addr=5, # varnum: store to local var 4
zmem=zmachine._mem,
args=[],
local_vars=[0x0001, 0x0002, 0x0003],
stack=[0x1111, 0x2222],
)
original_stack.push_routine(routine1)
routine1.program_counter = 0x5678 # resume PC for after routine2
routine2 = ZRoutine(
start_addr=0x6000,
return_addr=3, # varnum: store to local var 2
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)
restored_stack = zmachine._stackmanager
assert len(restored_stack._call_stack) == 3
# Check frame 1: return_addr is varnum
frame1 = restored_stack._call_stack[1]
assert frame1.return_addr == 5
assert frame1.local_vars[:3] == [0x0001, 0x0002, 0x0003]
assert frame1.stack == [0x1111, 0x2222]
# Frame1's program_counter was set from frame2's return_pc
assert frame1.program_counter == 0x5678
# Check frame 2
frame2 = restored_stack._call_stack[2]
assert frame2.return_addr == 3
assert frame2.local_vars[:1] == [0xAAAA]
assert frame2.stack == [0xBBBB, 0xCCCC, 0xDDDD]