1
0
mirror of https://github.com/rzzzwilson/pymlac.git synced 2025-06-10 09:32:41 +00:00
2018-07-23 17:26:56 +07:00

867 lines
26 KiB
Python
Executable File

"""
Test pymlac CPU opcodes DIRECTLY.
Usage: test_CPU.py [<options>] <filename>
where <filename> is a file of test instructions and
<options> is one or more of
-h prints this help and stops
"""
import time
import collections
import filecmp
# We implement a small interpreter to test the CPU.
# The DSL is documented here: [github.com/rzzzwilson/pymlac].
import sys
import os
import json
import collections
from Globals import *
import MainCPU
import Memory
import PtrPtp
import TtyIn
import TtyOut
import Trace
trace = Trace.Trace(TRACE_FILENAME)
import log
log = log.Log('test.log', log.Log.DEBUG)
def octword_line(s, offset):
"""Generate one line of fdump output.
s integer list to convert (8 words or less)
offset offset from start of dump
"""
HEXLEN = 55
CHARLEN = 16
hex = ''
char = ''
for v in s:
hex += '%06o ' % v
hi = v >> 8
lo = v & 0xff
if ord(' ') <= hi <= ord('~'):
char += chr(hi)
else:
char += '.'
if ord(' ') <= lo <= ord('~'):
char += chr(lo)
else:
char += '.'
hex = (hex + ' '*HEXLEN)[:HEXLEN]
char = (char + '|' + ' '*CHARLEN)[:CHARLEN+1]
return '%06o %s |%s' % (offset, hex, char)
class TestCPU(object):
# temporary assembler file and listfile prefix
AsmFilename = '_#ASM#_'
ProgressChar = '|\\-/'
ProgressCount = 0
def __init__(self):
"""Initialize the test."""
pass
def show_progress(self):
"""Show progress to stdout. A spinning line."""
print('%s\r' % self.ProgressChar[self.ProgressCount], end='')
self.ProgressCount += 1
if self.ProgressCount >= len(self.ProgressChar):
self.ProgressCount = 0
sys.stdout.flush()
def list2int(self, values):
"""Convert a string of multiple values to a list of ints.
values a string like: '123;4;-2'
(could be just '5')
Returns a list of integers, for example: [123, 4, -2]
"""
values = values.split(';')
result = []
for value in values:
result.append(self.str2int(value))
return result
def str2int(self, s):
"""Convert string to numeric value.
s numeric string (decimal or octal)
(could be None)
Returns the numeric value.
Returns None if value incorrect.
"""
if s is None:
return None
base = 10
if s[0] == '0':
base = 8
try:
value = int(s, base=base)
except:
return None
return value
def assemble(self, addr, opcodes):
"""Assemble a set of instructions, return opcode values.
addr address to assemble at
opcodes list of ASM lines to assemble
Returns a list of integer opcodes.
"""
# split possible multiple instructions
opcodes = opcodes.split('|')
# create ASM file with instruction
with open(self.AsmFilename+'.asm', 'w') as fd:
fd.write('\torg\t%07o\n' % addr)
for line in opcodes:
fd.write('\t%s\n' % line)
fd.write('\tend\n')
# now assemble file
cmd = ('../iasm/iasm -l %s.lst %s.asm'
% (self.AsmFilename, self.AsmFilename))
res = os.system(cmd)
# read the listing file to get assembled opcode (second line)
with open(self.AsmFilename+'.lst', 'r') as fd:
lines = fd.readlines()
result = []
for line in lines[1:-1]:
(opcode, _) = line.split(None, 1)
result.append(int(opcode, base=8))
return result
# Here we implement the DSL primitives. They all take two parameters which are
# the DSL field1 and field2 values (lowercase strings). If one or both are
# missing the parameters are None.
#
# setreg <name> <value>
# setmem <address> <value>
# allreg <value>
# allmem <value>
# bootrom <type>
# romwrite <bool>
# run [<number>]
# rununtil <address>
# checkcycles <number>
# checkreg <name> <value>
# checkmem <addr> <value>
# checkcpu <state>
# checkdcpu <state>
# mount <device> <filename>
# dismount <device>
# checkfile <file1> <file2>
# dumpmem file begin,end
# cmpmem file
# trace <range>[:<range>:...]
# where <range> ::= <addr>,<addr>
# onerror ('ignore'|'abort')
# initially, "onerror abort" assumed
def setreg(self, name, value):
"""Set register to a value.
Remember value to check later.
"""
value = self.str2int(value)
self.reg_values[name] = value
if name == 'ac':
self.cpu.AC = value
elif name == 'l':
self.cpu.L = value & 1
elif name == 'pc':
self.cpu.PC = value
elif name == 'ds':
self.cpu.DS = value
else:
raise Exception('setreg: bad register name: %s' % name)
def setmem(self, addr, values):
"""Set memory location to a value.
addr address of memory location to set
values value to store at 'addr'
"""
addr = self.str2int(addr)
# check if we must assemble values
if values[0] == '[':
# assemble one or more instructions
values = self.assemble(addr, values[1:-1])
else:
values = self.list2int(values)
for v in values:
self.mem_values[addr] = v
self.memory.put(v, addr, False)
addr += 1
def allreg(self, value, ignore):
"""Set all registers to a value."""
new_value = self.str2int(value)
if new_value is None:
return 'allreg: bad value: %s' % str(value)
self.reg_all_value = new_value
self.cpu.AC = new_value
self.cpu.L = new_value & 1
self.cpu.PC = new_value
self.cpu.DS = new_value
def allmem(self, value, ignore):
"""Set all of memory to a value.
Remember value to check later.
"""
new_value = self.str2int(value)
if new_value is None:
return 'allmem: bad value: %s' % str(value)
self.mem_all_value = new_value
for mem in range(MEMORY_SIZE):
self.memory.put(new_value, mem, False)
def bootrom(self, loader_type, ignore):
"""Set bootloader memory range to appropriate bootloader code.
loader_type either 'ptr' or 'tty'
"""
if loader_type not in ['ptr', 'tty']:
return 'bootrom: invalid bootloader type: %s' % loader_type
self.memory.set_ROM(loader_type)
def romwrite(self, writable, ignore):
"""Set ROM to be writable or not."""
self.memory.rom_protected = writable
def run(self, num_instructions, ignore):
"""Execute one or more instructions.
num_instructions number of instructions to execute
Also sets self.abort to False.
"""
if num_instructions is None:
# assume number of instructions is 1
number = 1
else:
number = self.str2int(num_instructions)
if number is None:
return 'Invalid number of instructions: %s' % num_instructions
self.used_cycles= 0
while number > 0:
# for _ in range(number):
(cycles, _) = self.cpu.execute_one_instruction()
trace.itraceend(False)
self.ptrptp.ptr_tick(cycles)
self.ptrptp.ptp_tick(cycles)
self.used_cycles += cycles
number -= 1
self.abort = False
def rununtil(self, address, ignore):
"""Execute instructions until PC == address.
address address at which to stop
We must handle the case where PC initial address is the actual stop
address, that is, execute one instruction before checking for the
stop address.
We also stop if the CPU executes the HLT instruction.
Also sets self.abort to False.
"""
new_address = self.str2int(address)
if new_address is None:
return 'rununtil: invalid stop address: %s' % address
self.used_cycles = 0
self.cpu.running = True
while self.cpu.running:
(cycles, tracestr) = self.cpu.execute_one_instruction()
if tracestr:
endstr = trace.itraceend(False)
trace.comment('%s\t%s' % (tracestr, endstr))
trace.flush()
self.ptrptp.ptr_tick(cycles)
self.ptrptp.ptp_tick(cycles)
self.used_cycles += cycles
if self.cpu.PC == new_address:
break
self.abort = False
def checkcycles(self, cycles, ignore):
"""Check that opcode cycles used is correct.
cycles expected number of cycles used
"""
num_cycles = self.str2int(cycles)
if num_cycles is None:
return 'checkcycles: invalid number of cycles: %s' % cycles
if num_cycles != self.used_cycles:
return ('Run used %d cycles, expected %d'
% (self.used_cycles, num_cycles))
def checkreg(self, reg, value):
"""Check register is as it should be."""
new_value = self.str2int(value)
if new_value is None:
return 'checkreg: bad value: %s' % str(value)
if reg == 'ac':
self.reg_values[reg] = self.cpu.AC
if self.cpu.AC != new_value:
return ('AC wrong, is %07o, should be %07o'
% (self.cpu.AC, new_value))
elif reg == 'l':
self.reg_values[reg] = self.cpu.L
if self.cpu.L != new_value:
return ('L wrong, is %02o, should be %02o'
% (self.cpu.L, new_value))
elif reg == 'pc':
self.reg_values[reg] = self.cpu.PC
if self.cpu.PC != new_value:
return ('PC wrong, is %07o, should be %07o'
% (self.cpu.PC, new_value))
elif reg == 'ds':
self.reg_values[reg] = self.cpu.DS
if self.cpu.DS != new_value:
return ('DS wrong, is %07o, should be %07o'
% (self.cpu.DS, new_value))
else:
return 'checkreg: bad register name: %s' % str(name)
def checkmem(self, addr, value):
"""Check a memory location is as it should be."""
new_addr = self.str2int(addr)
if new_addr is None:
return 'checkmem: bad address: %s' % str(addr)
new_value = self.str2int(value)
if new_value is None:
return 'checkmem: bad value: %s' % str(value)
self.mem_values[new_addr] = new_value
memvalue = self.memory.fetch(new_addr, False)
if memvalue != new_value:
return ('Memory wrong at address %07o, is %07o, should be %07o'
% (new_addr, memvalue, new_value))
def checkcpu(self, state, ignore):
"""Check main CPU run state is as desired."""
if state not in ('on', 'off'):
return 'checkcpu: bad state: %s' % str(state)
cpu_state = str(self.cpu.running).lower()
if ((state == "on" and cpu_state != "true") or
(state == "off" and cpu_state != "false")):
return ('Main CPU run state is %s, should be %s'
% (str(self.cpu.running), str(state)))
def checkdcpu(self, state, ignore):
"""Check display CPU run state is as desired."""
if state not in ('on', 'off'):
return 'checkdcpu: bad state: %s' % str(state)
dcpu_state = str(self.display.running).lower()
if ((state == "on" and dcpu_state != "true") or
(state == "off" and dcpu_state != "false")):
return ('Display CPU run state is %s, should be %s'
% (str(self.cpu.running), str(state)))
def mount(self, device, filename):
"""Mount a file on a device.
device name of device
filename path to file to mount
If the device is an input device, the file must exist.
"""
if device == 'ptr':
if not os.path.exists(filename) or not os.path.isfile(filename):
return "mount: '%s' doesn't exist or isn't a file" % filename
self.ptrptp.ptr_mount(filename)
elif device == 'ptp':
self.ptrptp.ptp_mount(filename)
elif device == 'ttyin':
if not os.path.exists(filename) or not os.path.isfile(filename):
return "mount: '%s' doesn't exist or isn't a file" % filename
self.ttyin.mount(filename)
elif device == 'ttyout':
self.ttyout.mount(filename)
else:
print('mount: bad device: %s' % device)
return 'mount: bad device: %s' % device
def dismount(self, device, ignore):
"""Dismount a file from a device.
device name of device
"""
if device == 'ptr':
self.ptrptp.ptr_dismount()
elif device == 'ptp':
self.ptrptp.ptp_dismount()
else:
return 'dismount: bad device: %s' % device
def checkfile(self, file1, file2):
"""Compare two files, error if different."""
cmd = 'cmp -s %s %s' % (file1, file2)
if filecmp.cmp(file1, file2, shallow=False):
return 'Files %s and %s are different' % (file1, file2)
def dumpmem(self, filename, addresses):
"""Dump memory to a file.
filename path to the file to create
addresses string containing "begin,end" addresses
"""
# get address limits
if addresses is None:
# dump everything
begin = 0
end = 0x3fff
else:
s = addresses.split(',')
if len(s) != 2:
return "dumpmem: dump limits are bad: %s" % addresses
(begin, end) = s
begin = self.str2int(begin)
end = self.str2int(end)
if begin is None or end is None:
return "dumpmem: dump limits are bad: %s" % addresses
# create octdump-like text file with required memory locations
with open(filename, 'w') as handle:
addr = begin
offset = addr
chunk = []
while addr <= end:
word = self.memory.fetch(addr, False)
addr += 1
chunk.append(word)
if len(chunk) == 8:
line = octword_line(chunk, offset)
handle.write(line + '\n')
chunk = []
offset = addr
if len(chunk):
line = octword_line(chunk, offset)
handle.write(line + '\n')
def cmpmem(self, filename, ignore):
"""Compare a 'dumpmem' file with memory.
filename path of the 'dumpmem' file
A dumpmem file has the following format:
000100 100000 010104 000000 000000 004111 000000 000000 000000 |...D.....I......|
"""
# get file contents into memory
try:
with open(filename, 'r') as handle:
lines = handle.readlines()
except IOError as e:
return "Error opening file '%s': %s" % (filename, e.strerror)
# read dumpmem file, get address and expected contents
for line in lines:
line = line.split('|', 1)[0]
values = line.split()
address = self.str2int(values[0])
for value in values[1:]:
expected = self.str2int('0'+value) # force octal evaluation
actual = self.memory.fetch(address, False)
if actual != expected:
return ('Address %06o is wrong, expected %06o, is %06o'
% (address, expected, actual))
address += 1
def trace(self, ranges, ignore):
"""Set the trace range(s).
ranges trace ranges of the form <range>[:<range>:...]
where <range> ::= <addr>,<addr>
Puts the trace range(s) into the Trace object.
"""
trace_map = collections.defaultdict(bool)
for rng in ranges.split(':'):
be = rng.split(',')
if len(be) != 2:
return("Trace ranges must have the form 'begin,end'")
(begin, end) = be
begin = self.str2int(begin)
end = self.str2int(end)
if begin > end:
return("Trace begin address must be <= end address. Got: %s" % rng)
for addr in range(begin, end+1):
trace_map[addr] = True
trace.set_trace_map(trace_map)
def onerror(self, state, ignore):
"""Set the action upon an error.
state either 'ignore' or 'error'
"""
if state == 'ignore':
self.abort = False
elif state == 'error':
self.abort = True
else:
return 'onerror: bad action name: %s' % state
# end of DSL primitives
def check_all_mem(self):
"""Check memory for unwanted changes."""
result = []
for mem in range(MEMORY_SIZE):
value = self.memory.fetch(mem, False)
if mem in self.mem_values:
if value != self.mem_values[mem]:
result.append('Memory at %07o changed, is %07o, should be %07o'
% (mem, value, self.mem_values[mem]))
else:
if value != self.mem_all_value:
result.append('Memory at %07o changed, is %07o, should be %07o'
% (mem, value, self.mem_all_value))
def check_all_regs(self):
"""Check registers for unwanted changes."""
result = []
if 'ac' in self.reg_values:
if self.cpu.AC != self.reg_values['ac']:
result.append('AC changed, is %07o, should be %07o'
% (self.cpu.AC, self.reg_values['ac']))
else:
if self.cpu.AC != self.reg_all_value:
result.append('AC changed, is %07o, should be %07o'
% (self.cpu.AC, self.reg_all_value))
if 'l' in self.reg_values:
if self.cpu.L != self.reg_values['l']:
result.append('L changed, is %02o, should be %02o'
% (self.cpu.L, self.reg_values['l']))
else:
if self.cpu.L != self.reg_all_value & 1:
result.append('L changed, is %02o, should be %02o'
% (self.cpu.L, self.reg_all_value & 1))
if 'pc' in self.reg_values:
if self.cpu.PC != self.reg_values['pc']:
result.append('PC changed, is %07o, should be %07o'
% (self.cpu.PC, self.reg_values['pc']))
else:
if self.cpu.PC != self.reg_all_value:
result.append('PC changed, is %07o, should be %07o'
% (self.cpu.PC, self.reg_all_value))
if 'ds' in self.reg_values:
if self.cpu.DS != self.reg_values['ds']:
result.append('DS changed, is %07o, should be %07o'
% (self.cpu.DS, self.reg_values['ds']))
else:
if self.cpu.DS != self.reg_all_value:
result.append('DS changed, is %07o, should be %07o'
% (self.cpu.DS, self.reg_all_value))
return result
def setd(self, state, var2):
"""Set display state."""
if state == 'on':
self.display_state = True
elif state == 'off':
self.display_state = False
else:
raise Exception('setd: bad state: %s' % str(state))
def execute(self, test, filename):
"""Execute test string in 'test'."""
# set globals
self.reg_values = {}
self.mem_values = {}
self.reg_all_value = '0'
self.mem_all_value = '0'
result = []
self.memory = Memory.Memory()
self.ptrptp = PtrPtp.PtrPtp()
self.ttyin = TtyIn.TtyIn()
self.ttyout = TtyOut.TtyOut()
self.cpu = MainCPU.MainCPU(self.memory, None, None,
None, self.ttyin, self.ttyout, self.ptrptp)
# turn trace OFF, initially
trace_map = collections.defaultdict(bool)
trace.set_trace_map(trace_map)
self.cpu.running = True
self.display_state = False
self.abort = True # abort on errors if True
# prepare the trace
trace.add_maincpu(self.cpu)
# clear registers and memory to 0 first
self.allreg(self.reg_all_value, None)
self.allmem(self.mem_all_value, None)
# show the DSL we are about execute
trace.comment('')
trace.comment(test)
trace.comment('-'*80)
# interpret the test instructions
suite = test.split(';')
for instruction in suite:
fields = instruction.split(None, 2)
if not fields:
continue;
opcode = fields[0].strip().lower()
if len(fields) == 1:
fld1 = fld2 = None
elif len(fields) == 2:
fld1 = fields[1].strip().lower()
fld2 = None
elif len(fields) == 3:
fld1 = fields[1].strip().lower()
fld2 = fields[2].strip().lower()
# cll the correct DSL primitive
if opcode == 'setreg':
r = self.setreg(fld1, fld2)
elif opcode == 'setmem':
r = self.setmem(fld1, fld2)
elif opcode == 'run':
r = self.run(fld1, fld2)
elif opcode == 'rununtil':
r = self.rununtil(fld1, fld2)
elif opcode == 'checkcycles':
r = self.checkcycles(fld1, fld2)
elif opcode == 'checkreg':
r = self.checkreg(fld1, fld2)
elif opcode == 'checkmem':
r = self.checkmem(fld1, fld2)
elif opcode == 'allreg':
r = self.allreg(fld1, fld2)
elif opcode == 'allmem':
r = self.allmem(fld1, fld2)
elif opcode == 'checkcpu':
r = self.checkcpu(fld1, fld2)
elif opcode == 'checkdcpu':
r = self.checkdcpu(fld1, fld2)
elif opcode == 'setd':
r = self.setd(fld1, fld2)
elif opcode == 'bootrom':
r = self.bootrom(fld1, fld2)
elif opcode == 'romwrite':
r = self.romwrite(fld1, fld2)
elif opcode == 'mount':
r = self.mount(fld1, fld2)
elif opcode == 'dismount':
r = self.dismount(fld1, fld2)
elif opcode == 'checkfile':
r = self.checkfile(fld1, fld2)
elif opcode == 'dumpmem':
r = self.dumpmem(fld1, fld2)
elif opcode == 'cmpmem':
r = self.cmpmem(fld1, fld2)
elif opcode == 'trace':
r = self.trace(fld1, fld2)
elif opcode == 'onerror':
r = self.onerror(fld1, fld2)
else:
print("Unrecognized opcode '%s' in: %s" % (opcode, test))
raise Exception("Unrecognized opcode '%s' in: %s" % (opcode, test))
if r is not None:
result.append(r)
if self.abort:
break
# now check all memory and regs for changes
r = self.check_all_mem()
if r is not None:
result.append(r)
r = self.check_all_regs()
if r is not None:
result.extend(r)
if result:
print(test)
print('\t' + '\n\t'.join(result))
def memdump(self, filename, start, number):
"""Dump memory from 'start' into 'filename', 'number' words dumped."""
with open(filename, 'w') as fd:
for addr in range(start, start+number, 8):
a = addr
llen = min(8, start+number - addr)
line = '%04o ' % addr
for _ in range(llen):
line += '%06o ' % self.memory.fetch(a, False)
a += 1
fd.write('%s\n' % line)
def main(self, filename):
"""Execute CPU tests from 'filename'."""
# get all tests from file
with open(filename, 'r') as fd:
lines = fd.readlines()
# read lines, join continued, get complete tests
tests = []
test = ''
for line in lines:
self.show_progress()
line = line[:-1] # strip newline
if not line:
continue # skip blank lines
if '#' in line:
index = line.find('#')
line = line[:index]
# remove trailing spaces
line = line.rstrip()
if not line:
continue # skip blank lines
if line[0] in ('\t', ' '): # continuation
if test:
if not test.endswith(';'):
test += ';'
test += ' ' + line[1:].strip()
else: # beginning of new test
if test:
tests.append(test)
test = line
# flush last test
if test:
tests.append(test)
# now do each test
for test in tests:
self.show_progress()
self.execute(test, filename)
################################################################################
if __name__ == '__main__':
import sys
import getopt
def usage(msg=None):
if msg:
print('*'*60)
print(msg)
print('*'*60)
print(__doc__)
try:
(opts, args) = getopt.gnu_getopt(sys.argv, "h", ["help"])
except getopt.GetoptError:
usage()
sys.exit(10)
for opt, arg in opts:
if opt in ("-h", "--help"):
usage()
sys.exit(0)
if len(args) != 2:
usage()
sys.exit(10)
filename = args[1]
try:
f = open(filename)
except IOError:
print("Sorry, can't find file '%s'" % filename)
sys.exit(10)
f.close()
test = TestCPU()
test.main(filename)