#!/usr/bin/python # -*- coding: utf-8 -*- """ A test harness for code to test 'pyasm'. Usage: test_harness [ -h ] [ -d ] [ -p ] Where is the test filename prefix of files to test, is a directory of test files. If is specified run tests in that directory. If not specified the test directory is assumed to be './tests'. If is specified, run tests on files in the test subdirectory with a filename of *.asm. If 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()