mud/tests/test_zmachine_opcodes.py

251 lines
7.8 KiB
Python

"""
Unit tests for the 12 newly implemented Z-machine opcodes.
These tests verify the basic behavior of each opcode by mocking the
required dependencies (memory, stack, decoder, etc).
"""
from unittest import TestCase
from unittest.mock import Mock
from mudlib.zmachine.zcpu import ZCpu, ZCpuDivideByZero, ZCpuQuit
class MockMemory:
"""Mock memory for testing."""
def __init__(self):
self.data = bytearray(65536)
self.version = 3
self.globals = {}
def __getitem__(self, addr):
return self.data[addr]
def __setitem__(self, addr, value):
self.data[addr] = value & 0xFF
def read_word(self, addr):
return (self.data[addr] << 8) | self.data[addr + 1]
def write_word(self, addr, value):
self.data[addr] = (value >> 8) & 0xFF
self.data[addr + 1] = value & 0xFF
def read_global(self, varnum):
return self.globals.get(varnum, 0)
def write_global(self, varnum, value):
self.globals[varnum] = value
class MockStackManager:
"""Mock stack manager for testing."""
def __init__(self):
self.stack = []
self.locals = [0] * 15
def push_stack(self, value):
self.stack.append(value)
def pop_stack(self):
return self.stack.pop()
def get_local_variable(self, index):
return self.locals[index]
def set_local_variable(self, index, value):
self.locals[index] = value
def finish_routine(self, return_value):
# Mock implementation - just return a PC value
return 0x1000
class MockOpDecoder:
"""Mock opcode decoder for testing."""
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:
"""Mock UI for testing."""
def __init__(self):
self.screen = Mock()
self.screen.write = Mock()
class ZMachineOpcodeTests(TestCase):
"""Test suite for Z-machine opcodes."""
def setUp(self):
"""Create a minimal CPU for testing."""
self.memory = MockMemory()
self.stack = MockStackManager()
self.decoder = MockOpDecoder()
self.ui = MockUI()
# Create CPU with mocked dependencies
self.cpu = ZCpu(
self.memory,
self.decoder,
self.stack,
Mock(), # objects
Mock(), # string
Mock(), # stream manager
self.ui,
)
def test_op_nop(self):
"""Test NOP does nothing."""
# Should just return without error
self.cpu.op_nop()
def test_op_new_line(self):
"""Test new_line prints a newline."""
self.cpu.op_new_line()
self.ui.screen.write.assert_called_once_with("\n")
def test_op_ret_popped(self):
"""Test ret_popped pops stack and returns."""
self.stack.push_stack(42)
self.cpu.op_ret_popped()
# Should have popped the value and set PC
self.assertEqual(len(self.stack.stack), 0)
self.assertEqual(self.cpu._opdecoder.program_counter, 0x1000)
def test_op_pop(self):
"""Test pop discards top of stack."""
self.stack.push_stack(100)
self.stack.push_stack(200)
self.cpu.op_pop()
self.assertEqual(len(self.stack.stack), 1)
self.assertEqual(self.stack.stack[0], 100)
def test_op_quit(self):
"""Test quit raises exception."""
with self.assertRaises(ZCpuQuit):
self.cpu.op_quit()
def test_op_dec(self):
"""Test decrement variable."""
# Set local variable 1 to 10
self.stack.set_local_variable(0, 10)
# Decrement it (variable 1 = local 0)
self.cpu.op_dec(1)
# Should be 9 now
self.assertEqual(self.stack.get_local_variable(0), 9)
def test_op_dec_wrapping(self):
"""Test decrement wraps at zero."""
# Set local variable 1 to 0
self.stack.set_local_variable(0, 0)
# Decrement it
self.cpu.op_dec(1)
# Should wrap to 65535
self.assertEqual(self.stack.get_local_variable(0), 65535)
def test_op_not(self):
"""Test bitwise NOT."""
self.decoder.store_address = 0 # Store to stack
self.cpu.op_not(0x00FF)
result = self.stack.pop_stack()
self.assertEqual(result, 0xFF00)
def test_op_not_all_ones(self):
"""Test NOT of all ones gives zero."""
self.decoder.store_address = 0
self.cpu.op_not(0xFFFF)
result = self.stack.pop_stack()
self.assertEqual(result, 0)
def test_op_load(self):
"""Test load reads variable."""
# Set local variable 2 to 42
self.stack.set_local_variable(1, 42)
self.decoder.store_address = 0 # Store to stack
# Load variable 2
self.cpu.op_load(2)
result = self.stack.pop_stack()
self.assertEqual(result, 42)
def test_op_mod_positive(self):
"""Test modulo with positive numbers."""
self.decoder.store_address = 0
self.cpu.op_mod(17, 5)
result = self.stack.pop_stack()
self.assertEqual(result, 2)
def test_op_mod_negative_dividend(self):
"""Test modulo with negative dividend."""
self.decoder.store_address = 0
# -17 mod 5 = -2 (z-machine uses C-style truncation toward zero)
self.cpu.op_mod(self.cpu._unmake_signed(-17), 5)
result = self.cpu._make_signed(self.stack.pop_stack())
self.assertEqual(result, -2)
def test_op_mod_divide_by_zero(self):
"""Test modulo by zero raises exception."""
self.decoder.store_address = 0
with self.assertRaises(ZCpuDivideByZero):
self.cpu.op_mod(10, 0)
def test_op_storeb(self):
"""Test store byte to memory."""
self.cpu.op_storeb(0x1000, 5, 0x42)
self.assertEqual(self.memory[0x1005], 0x42)
def test_op_storeb_truncates(self):
"""Test store byte truncates to 8 bits."""
self.cpu.op_storeb(0x2000, 10, 0x1FF)
self.assertEqual(self.memory[0x200A], 0xFF)
def test_op_jg_true(self):
"""Test jump if greater (signed) - true case."""
self.decoder.branch_condition = True
self.decoder.branch_offset = 100
old_pc = self.cpu._opdecoder.program_counter
self.cpu.op_jg(10, 5)
# Should have branched (offset - 2)
self.assertEqual(self.cpu._opdecoder.program_counter, old_pc + 98)
def test_op_jg_false(self):
"""Test jump if greater (signed) - false case."""
self.decoder.branch_condition = True # Branch if true (but test is false)
self.decoder.branch_offset = 100
old_pc = self.cpu._opdecoder.program_counter
self.cpu.op_jg(5, 10)
# Should not have branched (test is false, condition is true)
self.assertEqual(self.cpu._opdecoder.program_counter, old_pc)
def test_op_jg_signed(self):
"""Test jump if greater handles signed comparison."""
# -1 (as unsigned 65535) should NOT be greater than 1
self.decoder.branch_condition = False
old_pc = self.cpu._opdecoder.program_counter
self.cpu.op_jg(65535, 1)
# Should not branch (false condition matches)
self.assertEqual(self.cpu._opdecoder.program_counter, old_pc)
def test_op_pull(self):
"""Test pull from stack to variable."""
# Push value onto stack
self.stack.push_stack(123)
# Pull into local variable 1
self.cpu.op_pull(1)
# Should have stored in local variable 0 (variable 1)
self.assertEqual(self.stack.get_local_variable(0), 123)
# Stack should be empty
self.assertEqual(len(self.stack.stack), 0)