"""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]