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).
406 lines
12 KiB
Python
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]
|