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