#!/usr/bin/python import sys import subprocess import os import os.path pythonpath = sys.executable homepath = os.path.realpath(os.path.dirname(sys.argv[0])) ophispath = os.path.join(homepath, "..", "bin", "ophis") failed = 0 # These are some simple routines for forwarding to Ophis. It relies # on the standard input/output capabilities; we'll only go around it # when explicitly testing the input/output file capabilities. def assemble_raw(asm="", options=[]): p = subprocess.Popen([pythonpath, ophispath] + options, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return p.communicate(asm) def assemble_string(asm, options=[]): return assemble_raw(asm, ["-o", "-", "-"] + options) def test_string(test_name, asm, expected, options=[]): (out, err) = assemble_string(asm, options) if out == expected: print "%s: SUCCESS" % test_name else: global failed failed += 1 print "%s: FAILED\nError output:\n%s" % (test_name, err) def test_file(test_name, fname, ename, options=[]): f = open(os.path.join(homepath, fname), 'rt') asm = f.read() f.close() if ename is not None: f = open(os.path.join(homepath, ename), 'rb') expected = f.read() f.close() else: # a test where we expect failure expected = '' test_string(test_name, asm, expected, options) # And now, the actual test suites. First, the most basic techniques # are tested - these are the ones the test harness *itself* depends # on, then we start running through the features. def test_basic(): print print "==== BASIC OPERATION ====" test_string('Basic Ophis operation', '.byte "Hello, world!"', 'Hello, world!') test_string('Newline/EOF passthrough', '.byte 10,26,13,4,0,"Hi",13,10', '\n\x1a\r\x04\x00Hi\r\n') # Normally these would go in Expressions but we need them to run the # tests for relative instructions. test_string('Program counter recognition', '.org $41\nlda #^\n', '\xa9A') test_string('Program counter math', '.org $41\nlda #^+3\n', '\xa9D') if failed == 0: test_file('Basic instructions', 'testbase.oph', 'testbase.bin') test_file('Basic data pragmas', 'testdata.oph', 'testdata.bin') test_file('Undocumented instructions', 'test6510.oph', 'test6510.bin', ['-u']) test_file('65c02 extensions', 'test65c02.oph', 'test65c02.bin', ['-c']) test_file('Wide instructions', 'testwide.oph', 'testwide.bin', ['-c']) test_file('Branch restrictions (6502)', 'longbranch.oph', None, ['--no-branch-extend']) test_file('Branch restrictions (65c02)', 'branch_c02.oph', None, ['-c', '--no-branch-extend']) test_file('Branch extension, error-free (6502)', 'longbranch.oph', 'longbranch.bin') test_file('Branch extension, correct code (6502)', 'longbranch_ref.oph', 'longbranch.bin') test_file('Branch extension, error-free (65c02)', 'branch_c02.oph', 'branch_c02.bin', ['-c']) test_file('Branch extension, correct code (65c02)', 'branch_c02_ref.oph', 'branch_c02.bin', ['-c']) def test_outfile(): global failed print "\n==== INPUT AND OUTPUT ====" if os.path.exists("ophis.bin"): print "TEST SUITE FAILED: unclean test environment (ophis.bin exists)" failed += 1 return elif os.path.exists("custom.bin"): print "TEST SUITE FAILED: unclean test environment (custom.bin exists)" failed += 1 return # Test 1: Defaults try: assemble_raw('.byte "Hello, world!", 10', ['-']) f = open('ophis.bin', 'rb') if f.read() != 'Hello, world!\n': print "Default output filename: FAILED (bad output)" failed += 1 else: print "Default output filename: SUCCESS" f.close() os.unlink('ophis.bin') except: print "Default output filename: FAILED (exception)" failed += 1 # Test 2: Command line override try: assemble_raw('.byte "Hello, world!", 10', ['-', '-o', 'custom.bin']) f = open('custom.bin', 'rb') if f.read() != 'Hello, world!\n': print "Commandline output filename: FAILED (bad output)" failed += 1 else: print "Commandline output filename: SUCCESS" f.close() os.unlink('custom.bin') except: print "Commandline output filename: FAILED (exception)" failed += 1 # Test 3: Pragma override try: assemble_raw('.outfile "custom.bin"\n.byte "Hello, world!", 10', ['-']) f = open('custom.bin', 'rb') if f.read() != 'Hello, world!\n': print "Commandline output filename: FAILED (bad output)" failed += 1 else: print "Commandline output filename: SUCCESS" f.close() os.unlink('custom.bin') except: print "Commandline output filename: FAILED (exception)" failed += 1 # Test 4: Command line override of .outfile try: assemble_raw('.outfile "custom2.bin"\n' '.byte "Hello, world!", 10', ['-', '-o', 'custom.bin']) f = open('custom.bin', 'rb') if f.read() != 'Hello, world!\n': print "Commandline override of pragma: FAILED (bad output)" failed += 1 else: print "Commandline override of pragma: SUCCESS" f.close() os.unlink('custom.bin') except: print "Commandline override of pragma: FAILED (exception)" failed += 1 # Test 5: Pragma repetition priority try: assemble_raw('.outfile "custom.bin"\n' '.outfile "custom2.bin"\n' '.byte "Hello, world!", 10', ['-']) f = open('custom.bin', 'rb') if f.read() != 'Hello, world!\n': print "Pragma repetition: FAILED (bad output)" failed += 1 else: print "Pragma repetition: SUCCESS" f.close() os.unlink('custom.bin') except: print "Pragma repetition: FAILED (exception)" failed += 1 # Test 6: multiple input files try: out = assemble_raw('', ['-o', '-', '-u', os.path.join(homepath, "testbase.oph"), os.path.join(homepath, "test6510.oph")])[0] f = open(os.path.join(homepath, "testbase.bin"), 'rb') s = f.read() f.close() f = open(os.path.join(homepath, "test6510.bin"), 'rb') s += f.read() f.close() if out != s: print "Multiple input files: FAILED (bad output)" failed += 1 else: print "Multiple input files: SUCCESS" except: print "Multiple input files: FAILED (exception)" failed += 1 def test_transforms(): print "\n==== BINARY TRANSFORM PASSES ====" print "Simple zero page selection: SUCCESS (covered in basic tests)" test_string('Chained collapse', '.org $fa \n lda + \n lda ^ \n * rts \n', '\xa5\xfe\xa5\xfc\x60') test_string('Reversible collapse', '.org $fb \n bne ^+200 \n lda ^ \n', '\xf0\x03\x4c\xc5\x01\xad\x00\x01') def test_expressions(): print "\n==== EXPRESSIONS AND LABELS ====" test_string('Basic addition', '.byte 3+2', '\x05') test_string('Basic subtraction', '.byte 3-2', '\x01') test_string('Basic multiplication', '.byte 3*2', '\x06') test_string('Basic division', '.byte 6/2', '\x03') test_string('Basic bit-union', '.byte 5|9', '\x0d') test_string('Basic bit-intersection', '.byte 5&9', '\x01') test_string('Basic bit-toggle', '.byte 5^9', '\x0c') test_string('Division truncation', '.byte 5/2', '\x02') test_string('Overflow', '.byte $FF*$10', '') test_string('Multibyte overflow', '.word $FF*$10', '\xf0\x0f') test_string('Masked overflow', '.byte $FF*$10&$FF', '\xf0') test_string('Underflow', '.byte 2-3', '') test_string('Masked underflow', '.byte 2-3&$FF', '\xff') test_string('Arithmetic precedence', '.byte 2+3*4-6/2', '\x0b') test_string('Parentheses', '.byte [2+3]*[4-6/2]', '\x05') # The manual gets this one wrong! $D000-275 needs brackets. test_string('Byte selector precedence', '.byte >$d000+32,>[$d000+32],<[$D000-275]', '\xf0\xd0\xed') test_string('Named labels', '.org $6948\nl: .word l', 'Hi') test_string('.alias directive (basic)', '.alias hi $6948\n.word hi', 'Hi') test_string('.alias directive (derived)', '.alias hi $6948\n.alias ho hi+$600\n.word hi,ho', 'HiHo') test_string('.alias directive (circular)', '.alias a c+1\n.alias b a+3\n.alias c b-4\n.word a, b, c', '') test_string('.advance directive (basic)', 'lda #$05\n.advance $05\n.byte ^', '\xa9\x05\x00\x00\x00\x05') test_string('.advance directive (filler)', 'lda #$05\nf: .advance $05,f+3\n.byte ^', '\xa9\x05\x05\x05\x05\x05') test_string('.advance no-op', 'lda #$05\n.advance $02\n.byte ^', '\xa9\x05\x02') test_string('.advance failure', 'lda #$05\n.advance $01\n.byte ^', '') test_string('.checkpc, space > 0', 'lda #$05\n.checkpc $10', '\xa9\x05') test_string('.checkpc, space = 0', 'lda #$05\n.checkpc 2', '\xa9\x05') test_string('.checkpc, space < 0', 'lda $$05\n.checkpc 1', '') def test_segments(): print("\n==== ASSEMBLY SEGMENTS ====") # Basic - make sure PC is tracked separately when segment changes # No writing to .data segments # .space directive # multiple named segments def test_scopes(): print("\n==== LABEL SCOPING ====") # Duplicate labels in separate unnested scopes OK # Data hiding - invisible outside scope, overwritten in nested # Anonymous labels: basic support but also across scope boundaries def test_macros(): print("\n==== MACROS ====") test_string('Basic macros', '.macro greet\n' ' .byte "hi"\n' '.macend\n' '`greet\n.invoke greet', "hihi") test_string('Macros with arguments', '.macro greet\n' ' .byte "hi",_1\n' '.macend\n' "`greet 'A\n.invoke greet 'B", "hiAhiB") test_string('Macros invoking macros', '.macro inner\n' ' .byte " there"\n' '.macend\n' '.macro greet\n' ' .byte "hi"\n' ' `inner\n' '.macend\n' "`greet", "hi there") test_string('Macros defining macros (illegal)', '.macro greet\n' '.macro inner\n' ' .byte " there"\n' '.macend\n' ' .byte "hi"\n' ' `inner\n' '.macend\n' "`greet", "") test_string('Fail on infinite recursion', '.macro inner\n' ' .byte " there"\n' ' `greet\n' '.macend\n' '.macro greet\n' ' .byte "hi"\n' ' `inner\n' '.macend\n' "`greet", "") def test_subfiles(): print("\n==== COMPILATION UNITS ====") # .include, basic and repeated # .require, basic and repeated # .include before .require of same file # .require before .include of same file # .require the same file twice but with different names # (that is, a/header.oph and header.oph) # .require different files with the same pathname (../base.oph) # .charmap: set, reset, out-of-range # .charmapbin: legal and illegal charmaps # .incbin, basic usage # .incbin, hardcoded offset # .incbin, hardcoded offset and length # .incbin, softcoded offset and length # .incbin, length too long # .incbin, negative offset # .incbin, offset = size of file # .incbin, offset > size of file def test_systematic(): test_outfile() test_transforms() test_expressions() test_segments() test_scopes() test_macros() test_subfiles() if __name__ == '__main__': print "Using Python interpreter:", pythonpath test_basic() if failed == 0: test_systematic() else: print "\nBasic test cases failed, aborting test." if failed > 0: print "\nTotal test case failures: %d" % failed else: print "\nAll test cases succeeded"