Skip to content

Commit

Permalink
Improve test coverage for trace.py
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Jan 9, 2025
1 parent f64ad6c commit a673c52
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 67 deletions.
19 changes: 11 additions & 8 deletions skoolkit/trace.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022-2024 Richard Dymond (rjdymond@gmail.com)
# Copyright 2022-2025 Richard Dymond (rjdymond@gmail.com)
#
# This file is part of SkoolKit.
#
Expand Down Expand Up @@ -61,7 +61,7 @@ def run(self, start, stop, max_operations, max_tstates, interrupts, draw, exec_m
if trace_line:
r = Registers(registers)

if hasattr(simulator, 'trace'): # pragma: no cover
if hasattr(simulator, 'trace'):
if trace_line:
df = lambda pc: disassemble(memory, pc, prefix, byte_fmt, word_fmt)[0]
tf = lambda pc, i, t0: print(trace_line.format(pc=pc, i=i, r=r, t=t0, m=memory))
Expand Down Expand Up @@ -99,7 +99,7 @@ def run(self, start, stop, max_operations, max_tstates, interrupts, draw, exec_m
if draw:
frame = tstates // frame_duration
if frame > prev_frame:
if not draw(memory[16384:23296], frame, keyboard): # pragma: no cover
if not draw(memory[16384:23296], frame, keyboard):
stop_cond = 0
break
prev_frame = frame
Expand All @@ -114,18 +114,21 @@ def run(self, start, stop, max_operations, max_tstates, interrupts, draw, exec_m
stop_cond = 3
break

if stop_cond == 1:
print(f'Stopped at {prefix}{registers[PC]:{word_fmt}}: {operations} operations')
stop_msg = f'Stopped at {prefix}{registers[PC]:{word_fmt}}'
if stop_cond == 0:
print(f'{stop_msg}: screen closed')
elif stop_cond == 1:
print(f'{stop_msg}: {operations} operations')
elif stop_cond == 2:
print(f'Stopped at {prefix}{registers[PC]:{word_fmt}}: {registers[T] - start_time} T-states')
print(f'{stop_msg}: {registers[T] - start_time} T-states')
elif stop_cond == 3:
print(f'Stopped at {prefix}{registers[PC]:{word_fmt}}')
print(stop_msg)
self.operations = operations

def read_port(self, registers, port):
if port == 0xFFFD:
return self.ay[self.outfffd % 16]
if port % 2 == 0 and self.keyboard: # pragma: no cover
if port % 2 == 0 and self.keyboard:
h = (port // 256) ^ 0xFF
v = 0x40
i = 0
Expand Down
66 changes: 66 additions & 0 deletions tests/skoolkittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from textwrap import dedent
import zlib
from unittest import TestCase
from unittest.mock import Mock

SKOOLKIT_HOME = abspath(dirname(dirname(__file__)))
sys.path.insert(0, SKOOLKIT_HOME)
Expand All @@ -24,6 +25,71 @@
'pc': 32
}

BLACK, BLUE, RED, MAGENTA, GREEN, CYAN, YELLOW, WHITE = range(8)
BRIGHT_BLUE, BRIGHT_RED, BRIGHT_MAGENTA, BRIGHT_GREEN, BRIGHT_CYAN, BRIGHT_YELLOW, BRIGHT_WHITE = range(9, 16)

COLOURS = {
(0x00, 0x00, 0x00): BLACK,
(0x00, 0x00, 0xc5): BLUE,
(0xc5, 0x00, 0x00): RED,
(0xc5, 0x00, 0xc5): MAGENTA,
(0x00, 0xc6, 0x00): GREEN,
(0x00, 0xc6, 0xc5): CYAN,
(0xc5, 0xc6, 0x00): YELLOW,
(0xcd, 0xc6, 0xcd): WHITE,
(0x00, 0x00, 0xff): BRIGHT_BLUE,
(0xff, 0x00, 0x00): BRIGHT_RED,
(0xff, 0x00, 0xff): BRIGHT_MAGENTA,
(0x00, 0xff, 0x00): BRIGHT_GREEN,
(0x00, 0xff, 0xff): BRIGHT_CYAN,
(0xff, 0xff, 0x00): BRIGHT_YELLOW,
(0xff, 0xff, 0xff): BRIGHT_WHITE,
}

QUIT = 1

class MockPygameIO:
def getvalue(self):
return 'Using pygame'

class MockSurface:
def __init__(self):
self.scale = None
self.pixels = [None] * 49152

def fill(self, colour, rect):
x0, y0, w, h = rect
for x in range(x0 // self.scale, (x0 + w) // self.scale):
for y in range(y0 // self.scale, (y0 + h) // self.scale):
self.pixels[256 * y + x] = colour

def set_mode(self, resolution):
self.scale = resolution[0] // 256

class MockPygame:
def __init__(self, events=()):
self.init_called = False
self.Color = lambda r, g, b: COLOURS.get((r, g, b))
self.display = Mock()
surface = MockSurface()
self.display.set_mode.side_effect = surface.set_mode
self.display.get_surface.return_value = surface
self.time = Mock()
self.event = Mock()
self.event.get.return_value = events
self.key = Mock()
self.Rect = lambda x, y, w, h: (x, y, w, h)

def __getattr__(self, name):
if name == 'QUIT':
return QUIT
if name.startswith('K_'):
return name[2:]
raise AttributeError

def init(self):
self.init_called = True

def as_dword(num):
return (num % 256, (num >> 8) % 256, (num >> 16) % 256, (num >> 24) % 256)

Expand Down
57 changes: 1 addition & 56 deletions tests/test_screen.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,9 @@
from collections import defaultdict
from unittest.mock import patch, Mock

from skoolkittest import SkoolKitTestCase
from skoolkittest import BLACK, BLUE, MAGENTA, GREEN, CYAN, YELLOW, QUIT, MockPygame, SkoolKitTestCase
from skoolkit import screen

BLACK, BLUE, RED, MAGENTA, GREEN, CYAN, YELLOW, WHITE = range(8)
BRIGHT_BLUE, BRIGHT_RED, BRIGHT_MAGENTA, BRIGHT_GREEN, BRIGHT_CYAN, BRIGHT_YELLOW, BRIGHT_WHITE = range(9, 16)

COLOURS = {
(0x00, 0x00, 0x00): BLACK,
(0x00, 0x00, 0xc5): BLUE,
(0xc5, 0x00, 0x00): RED,
(0xc5, 0x00, 0xc5): MAGENTA,
(0x00, 0xc6, 0x00): GREEN,
(0x00, 0xc6, 0xc5): CYAN,
(0xc5, 0xc6, 0x00): YELLOW,
(0xcd, 0xc6, 0xcd): WHITE,
(0x00, 0x00, 0xff): BRIGHT_BLUE,
(0xff, 0x00, 0x00): BRIGHT_RED,
(0xff, 0x00, 0xff): BRIGHT_MAGENTA,
(0x00, 0xff, 0x00): BRIGHT_GREEN,
(0x00, 0xff, 0xff): BRIGHT_CYAN,
(0xff, 0xff, 0x00): BRIGHT_YELLOW,
(0xff, 0xff, 0xff): BRIGHT_WHITE,
}

QUIT = 1

KEYS = (
('1', 3, 0b00001),
('2', 3, 0b00010),
Expand Down Expand Up @@ -94,38 +71,6 @@
('KP_PERIOD', 7, 0b00100), # SYMBOL SHIFT + M
)

class MockSurface:
def __init__(self):
self.pixels = [None] * 49152

def fill(self, colour, rect):
x0, y0, w, h = rect
for x in range(x0, x0 + w):
for y in range(y0, y0 + h):
self.pixels[256 * y + x] = colour

class MockPygame:
def __init__(self, events=()):
self.init_called = False
self.Color = lambda r, g, b: COLOURS.get((r, g, b))
self.display = Mock()
self.display.get_surface.return_value = MockSurface()
self.time = Mock()
self.event = Mock()
self.event.get.return_value = events
self.key = Mock()
self.Rect = lambda x, y, w, h: (x, y, w, h)

def __getattr__(self, name):
if name == 'QUIT':
return QUIT
if name.startswith('K_'):
return name[2:]
raise AttributeError

def init(self):
self.init_called = True

class ScreenTest(SkoolKitTestCase):
def _test_init(self, mock_pygame, scale, caption):
s = screen.Screen(scale, 50, caption)
Expand Down
117 changes: 114 additions & 3 deletions tests/test_trace.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from collections import defaultdict
from functools import partial
import hashlib
import os
from textwrap import dedent
from unittest.mock import patch
from unittest.mock import patch, Mock

from skoolkittest import SkoolKitTestCase
from skoolkit import SkoolKitError, VERSION, CSimulator, components, trace
from skoolkittest import QUIT, MockPygame, MockPygameIO, SkoolKitTestCase
from skoolkit import SkoolKitError, VERSION, CSimulator, components, screen, trace
from skoolkit.config import COMMANDS
from skoolkit.simulator import Simulator
from skoolkit.simutils import PC, IFF, IM, T
Expand Down Expand Up @@ -35,6 +36,22 @@ def nop(self):
self.registers[PC] = self.pc
self.registers[T] = self.t1

class MockSimulatorWithTraceMethod(MockSimulator):
def trace(self, start, stop, max_operations, max_time, interrupts, draw, exec_map, keyboard, df, tf):
self.start = start
self.stop = stop
self.max_operations = max_operations
self.max_time = max_time
self.draw = draw
self.exec_map = exec_map
self.keyboard = keyboard
self.df = df
self.tf = tf
if df and tf:
tf(start, df(start), 0)
self.registers[PC] = self.pc
return 3, 1

class MockAudioWriter:
def __init__(self, config):
global audio_writer
Expand Down Expand Up @@ -3294,3 +3311,97 @@ def test_bit_5_of_output_to_port_0x7ffd(self):
self.assertEqual(len(out7ffd), 1)
self.assertEqual(out7ffd[0], '32')
self.assertEqual(s_memory[49152], 255)

@patch.object(screen, 'pygame_io', MockPygameIO())
@patch.object(screen, 'pygame', new_callable=MockPygame)
def test_keypresses(self, mock_pygame):
data = (
0x3E, 0xFD, # $8000 LD A,$FD ; T=69886 (keyboard check follows)
0xDB, 0xFE, # $8002 IN A,($FE) ; Read keys A-S-D-F-G
0x0F, # $8004 RRCA
0x30, 0x01, # $8005 JR NC,$8008 ; Jump if 'A' was pressed
0x00, # $8007 NOP ; This should not be executed
0x0F, # $8008 RRCA
0x30, 0x01, # $8009 JR NC,$800C ; Jump if 'S' was pressed
0x00, # $800B NOP ; This should not be executed
)
start = 0x8000
stop = start + len(data)
ram = [0] * 49152
ram[start - 0x4000:stop - 0x4000] = data
registers = {'PC': start, 'tstates': 69886}
z80file = self.write_z80_file(None, ram, registers=registers)
mock_pygame.key.get_pressed.return_value = defaultdict(int, {'a': 1, 's': 1})
output, error = self.run_trace(f'-S {stop} -v --screen {z80file}')
self.assertEqual(error, '')
exp_output = f"""
Using pygame
$8000 LD A,$FD
$8002 IN A,($FE)
$8004 RRCA
$8005 JR NC,$8008
$8008 RRCA
$8009 JR NC,$800C
Stopped at ${stop:04X}
"""
self.assertEqual(dedent(exp_output).strip(), output.rstrip())

@patch.object(screen, 'pygame_io', MockPygameIO())
@patch.object(screen, 'pygame', MockPygame([Mock(type=QUIT)]))
def test_screen_closed(self):
data = (
0x00, # $8000 NOP ; T=69886 (quit check follows)
0x00, # $8001 NOP ; This should not be executed
)
start = 0x8000
stop = start + len(data)
ram = [0] * 49152
ram[start - 0x4000:stop - 0x4000] = data
registers = {'PC': start, 'tstates': 69886}
z80file = self.write_z80_file(None, ram, registers=registers)
output, error = self.run_trace(f'-S {stop} -v --screen {z80file}')
self.assertEqual(error, '')
exp_output = f"""
Using pygame
$8000 NOP
Stopped at $8001: screen closed
"""
self.assertEqual(dedent(exp_output).strip(), output.rstrip())

@patch.object(trace, 'CSimulator', partial(MockSimulatorWithTraceMethod, pc=0x0001))
@patch.object(trace, 'Simulator', partial(MockSimulatorWithTraceMethod, pc=0x0001))
def test_simulator_with_trace_method(self):
stop = 0x0001
output, error = self.run_trace(f'-S {stop} 48')
self.assertEqual(error, '')
self.assertEqual("Stopped at $0001", output.rstrip())
self.assertEqual(simulator.start, 0)
self.assertEqual(simulator.stop, stop)
self.assertEqual(simulator.max_operations, 0)
self.assertEqual(simulator.max_time, 0)
self.assertIsNone(simulator.draw)
self.assertIsNone(simulator.exec_map)
self.assertIsNone(simulator.keyboard)
self.assertIsNone(simulator.df)
self.assertIsNone(simulator.tf)

@patch.object(trace, 'CSimulator', partial(MockSimulatorWithTraceMethod, pc=0x0001))
@patch.object(trace, 'Simulator', partial(MockSimulatorWithTraceMethod, pc=0x0001))
def test_simulator_with_trace_method_in_verbose_mode(self):
stop = 0x0001
output, error = self.run_trace(f'-S {stop} -v 48')
self.assertEqual(error, '')
exp_output = f"""
$0000 DI
Stopped at $0001
"""
self.assertEqual(dedent(exp_output).strip(), output.rstrip())
self.assertEqual(simulator.start, 0)
self.assertEqual(simulator.stop, stop)
self.assertEqual(simulator.max_operations, 0)
self.assertEqual(simulator.max_time, 0)
self.assertIsNone(simulator.draw)
self.assertIsNone(simulator.exec_map)
self.assertIsNone(simulator.keyboard)
self.assertIsNotNone(simulator.df)
self.assertIsNotNone(simulator.tf)

0 comments on commit a673c52

Please sign in to comment.