mud/tests/test_zmachine_undo.py

245 lines
7.8 KiB
Python

"""Tests for Z-machine save_undo / restore_undo opcodes (V5+, EXT:9/10)."""
from unittest import TestCase
from unittest.mock import Mock
from mudlib.zmachine.zcpu import ZCpu
from mudlib.zmachine.zmemory import ZMemory
from mudlib.zmachine.zstackmanager import ZStackManager
class MockOpDecoder:
"""Mock opcode decoder for undo tests."""
def __init__(self):
self.program_counter = 0x800
self.store_address = None
self.branch_condition = True
self.branch_offset = 2
def get_store_address(self):
return self.store_address
def get_branch_offset(self):
return (self.branch_condition, self.branch_offset)
class MockUI:
def __init__(self):
self.screen = Mock()
self.keyboard_input = Mock()
self.filesystem = Mock()
def make_v8_story(static_start=0x0800, globals_start=0x0400):
"""Create a minimal V8 story with proper memory layout."""
size = max(static_start + 512, 2048)
story = bytearray(size)
story[0] = 8 # V8
story[0x04] = (static_start >> 8) & 0xFF # high memory start
story[0x05] = static_start & 0xFF
story[0x0C] = (globals_start >> 8) & 0xFF
story[0x0D] = globals_start & 0xFF
story[0x0E] = (static_start >> 8) & 0xFF
story[0x0F] = static_start & 0xFF
return story
class SaveUndoTests(TestCase):
"""Tests for op_save_undo and op_restore_undo."""
def setUp(self):
story = make_v8_story()
self.memory = ZMemory(bytes(story))
self.stack = ZStackManager(self.memory)
self.decoder = MockOpDecoder()
self.ui = MockUI()
self.cpu = ZCpu(
self.memory,
self.decoder,
self.stack,
Mock(), # objects
Mock(), # string
Mock(), # stream manager
self.ui,
Mock(), # lexer
zmachine=None,
)
def test_save_undo_stores_1(self):
"""save_undo stores 1 (success), not -1 (not available)."""
self.decoder.store_address = 0x10
self.cpu.op_save_undo()
self.assertEqual(self.memory.read_global(0x10), 1)
def test_restore_undo_no_snapshot_stores_0(self):
"""restore_undo with no prior save stores 0."""
self.decoder.store_address = 0x10
self.cpu.op_restore_undo()
self.assertEqual(self.memory.read_global(0x10), 0)
def test_save_then_restore_stores_2(self):
"""After save + restore, save_undo's store location holds 2."""
# save_undo stores result in global var 0x10
self.decoder.store_address = 0x10
self.decoder.program_counter = 0x900
self.cpu.op_save_undo()
self.assertEqual(self.memory.read_global(0x10), 1)
# restore — use a different store address for restore_undo itself
self.decoder.store_address = 0x11
self.decoder.program_counter = 0xA00
self.cpu.op_restore_undo()
# save_undo's store var (0x10) should hold 2
self.assertEqual(self.memory.read_global(0x10), 2)
def test_restore_reverts_dynamic_memory(self):
"""Memory changes after save_undo are reverted by restore_undo."""
self.decoder.store_address = 0x10
self.decoder.program_counter = 0x900
# Set initial memory state in dynamic region
self.memory[0x100] = 0xAA
self.memory[0x200] = 0xBB
self.cpu.op_save_undo()
# Modify memory after save
self.memory[0x100] = 0x11
self.memory[0x200] = 0x22
self.memory[0x300] = 0x33
# Restore
self.decoder.store_address = 0x11
self.cpu.op_restore_undo()
# Memory should be reverted to save-time state
self.assertEqual(self.memory[0x100], 0xAA)
self.assertEqual(self.memory[0x200], 0xBB)
self.assertEqual(self.memory[0x300], 0x00)
def test_restore_reverts_program_counter(self):
"""PC is restored to save_undo's save point."""
self.decoder.store_address = 0x10
self.decoder.program_counter = 0x900
self.cpu.op_save_undo()
# Move PC forward
self.decoder.program_counter = 0xB00
# Restore
self.decoder.store_address = 0x11
self.cpu.op_restore_undo()
self.assertEqual(self.decoder.program_counter, 0x900)
def test_restore_reverts_call_stack(self):
"""Call stack changes after save_undo are reverted."""
self.decoder.store_address = 0x10
self.decoder.program_counter = 0x900
# Set up a routine header at 0x500: 2 local vars (V8 = zero-init)
self.memory._memory[0x500] = 2
# Start a routine before saving
self.stack.start_routine(0x500, 0x01, 0x900, [42, 99])
self.assertEqual(len(self.stack._call_stack), 2)
self.cpu.op_save_undo()
# Push another routine after save
self.memory._memory[0x600] = 1
self.stack.start_routine(0x600, 0x02, 0xA00, [7])
self.assertEqual(len(self.stack._call_stack), 3)
# Restore
self.decoder.store_address = 0x11
self.cpu.op_restore_undo()
# Should be back to 2 frames
self.assertEqual(len(self.stack._call_stack), 2)
def test_multiple_saves_keeps_latest(self):
"""Second save_undo overwrites the first snapshot."""
self.decoder.store_address = 0x10
# First save
self.memory[0x100] = 0xAA
self.decoder.program_counter = 0x900
self.cpu.op_save_undo()
# Modify and save again
self.memory[0x100] = 0xBB
self.decoder.program_counter = 0x950
self.cpu.op_save_undo()
# Modify again
self.memory[0x100] = 0xCC
# Restore — should go to second save (0xBB), not first (0xAA)
self.decoder.store_address = 0x11
self.cpu.op_restore_undo()
self.assertEqual(self.memory[0x100], 0xBB)
self.assertEqual(self.decoder.program_counter, 0x950)
def test_snapshot_is_deep_copy_memory(self):
"""Modifying memory after save doesn't corrupt the snapshot."""
self.decoder.store_address = 0x10
self.decoder.program_counter = 0x900
self.memory[0x100] = 0xAA
self.cpu.op_save_undo()
# Overwrite the same address
self.memory[0x100] = 0xFF
# Restore
self.decoder.store_address = 0x11
self.cpu.op_restore_undo()
# Should be AA, not FF
self.assertEqual(self.memory[0x100], 0xAA)
def test_snapshot_is_deep_copy_stack(self):
"""Modifying stack frames after save doesn't corrupt the snapshot."""
self.decoder.store_address = 0x10
self.decoder.program_counter = 0x900
# Start a routine with known local vars
self.memory._memory[0x500] = 2
self.stack.start_routine(0x500, 0x01, 0x900, [42, 99])
self.stack.push_stack(555)
self.cpu.op_save_undo()
# Mutate the live stack frame
self.stack.set_local_variable(0, 0)
self.stack.push_stack(999)
# Restore
self.decoder.store_address = 0x11
self.cpu.op_restore_undo()
# Local var 0 should be restored to 42
self.assertEqual(self.stack.get_local_variable(0), 42)
# Eval stack should have the original 555 (not 999)
self.assertEqual(self.stack.pop_stack(), 555)
def test_restore_consumes_snapshot(self):
"""After restore, the snapshot is consumed (no double undo)."""
self.decoder.store_address = 0x10
self.decoder.program_counter = 0x900
self.cpu.op_save_undo()
# First restore succeeds
self.decoder.store_address = 0x11
self.cpu.op_restore_undo()
self.assertEqual(self.memory.read_global(0x10), 2)
# Second restore fails (no snapshot)
self.decoder.store_address = 0x12
self.cpu.op_restore_undo()
self.assertEqual(self.memory.read_global(0x12), 0)