mirror of
https://github.com/rzzzwilson/pymlac.git
synced 2025-06-10 09:32:41 +00:00
365 lines
9.9 KiB
Python
Executable File
365 lines
9.9 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
A test harness for code to test 'pyasm'.
|
|
|
|
Usage: test_harness [ -h ] [ -d <directory> ] [ -p <prefix> ]
|
|
|
|
Where <prefix> is the test filename prefix of files to test,
|
|
<directory> is a directory of test files.
|
|
|
|
If <directory> is specified run tests in that directory. If not specified
|
|
the test directory is assumed to be './tests'.
|
|
|
|
If <prefix> is specified, run tests on files in the test subdirectory with
|
|
a filename of <prefix>*.asm. If <prefix> is not specified all files with
|
|
filename like *.asm are tested.
|
|
"""
|
|
|
|
|
|
import sys
|
|
import os
|
|
import glob
|
|
import getopt
|
|
import tempfile
|
|
import shutil
|
|
import string
|
|
import struct
|
|
|
|
|
|
TestDirectory = './tests'
|
|
TempPrefix = '_#TEMP#_'
|
|
|
|
BlockloaderSize = 8 * 16 # 8 lines of 16 bytes
|
|
|
|
HighBitMask = 0100000
|
|
|
|
|
|
def eval_expr(dot, expr):
|
|
"""Evaluate an expression that may contain '.'.
|
|
|
|
dot the current value of 'dot'
|
|
expr the expression string to avaluate
|
|
|
|
Returns the value of the expression.
|
|
Raises ArithmeticError if something is wrong.
|
|
"""
|
|
|
|
# replace any "." value with "dot" defined in the symbol table
|
|
expr = string.replace(expr, '.', 'DOT')
|
|
|
|
# evaluate the expression
|
|
globs = {'DOT': dot} # passed environment contains 'DOT'
|
|
try:
|
|
result = eval(expr, globs)
|
|
except (TypeError, NameError) as e:
|
|
Undefined = e.message
|
|
if 'is not defined' in e.message:
|
|
Undefined = e.message[len("name '"):-len("' is not defined")]
|
|
msg = "Test expression has '%s' undefined" % Undefined
|
|
raise ArithmeticError(msg)
|
|
msg = "Test expression has an error"
|
|
raise ArithmeticError(msg)
|
|
|
|
return result
|
|
|
|
def oct_dec_int(s):
|
|
"""Convert string 's' to binary integer."""
|
|
|
|
if s[0] == '0':
|
|
result = int(s, base=8)
|
|
else:
|
|
result = int(s)
|
|
|
|
return result
|
|
def pyword(word):
|
|
"""Convert a 16bit value to a real python integer.
|
|
|
|
That is, convert 0xFFFF to -1.
|
|
"""
|
|
|
|
byte = (word >> 8) & 0xff
|
|
byte2 = word & 0xff
|
|
bstr = chr(byte) + chr(byte2)
|
|
return struct.unpack(">h", bstr)[0]
|
|
|
|
def get_byte(handle):
|
|
"""Read one byte from the input file."""
|
|
|
|
byte = handle.read(1)
|
|
val = struct.unpack("B", byte)[0]
|
|
return val
|
|
|
|
def get_word(handle):
|
|
"""Read one word from input file."""
|
|
|
|
val = (get_byte(handle) << 8) + get_byte(handle)
|
|
return val
|
|
|
|
def read_block(handle, memory):
|
|
"""Read data blocks from file, update memory dict.
|
|
|
|
Returns None if the end block was read otherwise returns a tuple
|
|
(org, length) where 'org' is the start address of the block and 'length'
|
|
is the number of words in the block.
|
|
"""
|
|
|
|
# read load address, num data words and checksum
|
|
base_address = get_word(handle)
|
|
if base_address & HighBitMask:
|
|
return None
|
|
dot = base_address
|
|
|
|
neg_size = get_word(handle)
|
|
data_size = -pyword(neg_size)
|
|
|
|
checksum = get_word(handle)
|
|
|
|
# read data words, placing into dictionary
|
|
for _ in range(data_size):
|
|
data_word = get_word(handle)
|
|
memory[dot] = data_word
|
|
dot += 1
|
|
|
|
return (base_address, data_size)
|
|
|
|
def get_memory(ptp):
|
|
"""Read a PTP file, return memory contents as a dictionary.
|
|
|
|
Do not return the blockloader contents.
|
|
|
|
We don't check the checksum.
|
|
|
|
Returns a tuple (orgs, memory) where:
|
|
'orgs' is a list of ORG block addresses like [0100, 0200]
|
|
'memory' is a dictionary of memory contents: {addr: contents}
|
|
"""
|
|
|
|
orgs = []
|
|
memory = {}
|
|
last_org = None
|
|
last_length = 0
|
|
|
|
with open(ptp, 'rb') as handle:
|
|
# skip leading zeros
|
|
while get_byte(handle) == 0:
|
|
pass
|
|
# then skip block loader
|
|
for _ in range(BlockloaderSize - 1):
|
|
get_byte(handle)
|
|
|
|
# now read blocks until finished
|
|
while True:
|
|
result = read_block(handle, memory)
|
|
if result is None:
|
|
break
|
|
(org, length) = result
|
|
|
|
# this block may continue the last
|
|
if last_org and last_org + last_length == org:
|
|
last_length += length
|
|
else:
|
|
orgs.append(org)
|
|
last_org = org
|
|
last_length = length
|
|
|
|
return (orgs, memory)
|
|
|
|
def test_file(filename):
|
|
"""Test one ASM test file."""
|
|
|
|
# counter for number of errors
|
|
errors = 0
|
|
|
|
# assemble file into known listing file and PTP file
|
|
lst_file = '%s.lst' % TempPrefix
|
|
ptp_file = '%s.ptp' % TempPrefix
|
|
cmd = ('../pyasm -o %s -l %s %s >/dev/null 2>&1'
|
|
% (ptp_file, lst_file, filename))
|
|
status = os.system(cmd)
|
|
if status:
|
|
errors += 1
|
|
warn("Error assembling file '%s'" % filename)
|
|
return errors # must end testing
|
|
os.remove(lst_file)
|
|
|
|
# get the test instructions from the ASM file
|
|
with open(filename, 'rb') as handle:
|
|
lines = handle.readlines()
|
|
lines = [line.rstrip() for line in lines]
|
|
tests = []
|
|
for (lnum, line) in enumerate(lines):
|
|
if line.startswith(';|') or line.startswith(';!'):
|
|
prefix = line[:2]
|
|
must_pass = line.startswith(';|')
|
|
|
|
test = line[2:]
|
|
test = test.replace('\t', ' ')
|
|
if ';' in test:
|
|
ndx = test.index(';')
|
|
test = test[:ndx].rstrip()
|
|
tests.append((lnum+1, must_pass, prefix, test))
|
|
|
|
# make sure we have some tests
|
|
if not tests:
|
|
errors += 1
|
|
print("No tests found in file '%s'" % filename)
|
|
return errors # must end testing
|
|
|
|
# get the generated code and ORG block info from the PTP file
|
|
# memory locations are in a dictionary: {addr: contents, ...}
|
|
(orgs, memory) = get_memory(ptp_file)
|
|
|
|
# interpret the test instructions and check generated code
|
|
dot = None
|
|
for (lnum, must_pass, prefix, test) in tests:
|
|
# check for a label
|
|
if test[0] == ' ':
|
|
# no label
|
|
label = None
|
|
address = dot
|
|
value = test
|
|
else:
|
|
# have label, $n or octal/decimal?
|
|
# set 'dot' either way
|
|
(label, value) = test.split(' ', 1)
|
|
if label[0] == '$':
|
|
org_num = int(label[1:])
|
|
try:
|
|
dot = orgs[org_num-1]
|
|
except IndexError:
|
|
if must_pass:
|
|
errors += 1
|
|
warn("File '%s' has label '%s' with bad ORG number: %s%s"
|
|
% (filename, label, prefix, test))
|
|
return errors # must end testing
|
|
address = dot
|
|
else:
|
|
address = oct_dec_int(label)
|
|
dot = address
|
|
|
|
# evaluate the value field
|
|
value = eval_expr(dot, value)
|
|
|
|
if address is None:
|
|
if must_pass:
|
|
errors += 1
|
|
warn("File '%s' has label '%s' with bad ORG number: %s"
|
|
% (filename, label, test))
|
|
return errors # must end testing
|
|
try:
|
|
mem_value = memory[address]
|
|
except KeyError:
|
|
if must_pass:
|
|
errors += 1
|
|
warn("%s\n"
|
|
"%2d: %s\n"
|
|
"Test comment has check for address %04o which isn't in block"
|
|
% (filename, lnum, test, address))
|
|
else:
|
|
if mem_value != value:
|
|
if must_pass:
|
|
errors += 1
|
|
warn("%s\n"
|
|
"%2d: %s\n"
|
|
"Memory at address %04o should be %06o, got %06o"
|
|
% (filename, lnum, test, address, value, mem_value))
|
|
else:
|
|
if not must_pass:
|
|
errors += 1
|
|
warn("%s\n"
|
|
"%2d: %s\n"
|
|
"Memory contents check should fail but didn't?"
|
|
% (filename, lnum, test))
|
|
|
|
dot += 1
|
|
|
|
return errors
|
|
|
|
def warn(msg):
|
|
"""Print error message and continue."""
|
|
|
|
print('=================================\n'
|
|
'%s\n'
|
|
'---------------------------------' % msg)
|
|
|
|
def error(msg):
|
|
"""Print error message and stop."""
|
|
|
|
warn(msg)
|
|
sys.exit(10)
|
|
|
|
def run_tests(directory, prefix):
|
|
"""Run all appropriate test cases.
|
|
|
|
directory path to directory holding test cases
|
|
prefix filename prefix of tests to run (may be None).
|
|
"""
|
|
|
|
# handle 'all' case
|
|
if prefix is None:
|
|
prefix = ''
|
|
|
|
# get list of all test cases we are going to run
|
|
files = glob.glob('%s/%s*.asm' % (directory, prefix))
|
|
if not files:
|
|
if prefix:
|
|
error("No test files found in directory '%s' with prefix '%s'"
|
|
% (directory, prefix))
|
|
else:
|
|
error("No test files found in directory '%s'" % directory)
|
|
|
|
# test each found file
|
|
errors = 0
|
|
files.sort()
|
|
for filename in files:
|
|
errors += test_file(filename)
|
|
|
|
if errors:
|
|
if errors == 1:
|
|
warn('There was %d error!' % errors)
|
|
else:
|
|
warn('There were %d errors!' % errors)
|
|
|
|
# remove temporary files
|
|
for f in glob.glob('%s.*' % TempPrefix):
|
|
os.remove(f)
|
|
|
|
def usage(msg=None):
|
|
"""Print usage and optional error message."""
|
|
|
|
if msg is not None:
|
|
print('*'*60)
|
|
print(msg)
|
|
print('*'*60)
|
|
print(__doc__)
|
|
|
|
def main():
|
|
"""The test harness."""
|
|
|
|
# handle the options
|
|
try:
|
|
(opts, args) = getopt.gnu_getopt(sys.argv, 'd:hp:',
|
|
['directory=', 'help', 'prefix='])
|
|
except getopt.GetoptError:
|
|
usage()
|
|
sys.exit(10)
|
|
|
|
prefix = None
|
|
directory = TestDirectory
|
|
|
|
for opt, arg in opts:
|
|
if opt in ('-d', '--directory'):
|
|
directory = arg
|
|
elif opt in ('-h', '--help'):
|
|
usage()
|
|
sys.exit(0)
|
|
elif opt in ('-p', '--prefix'):
|
|
prefix = arg
|
|
|
|
run_tests(directory, prefix)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|