#!/usr/bin/env python3 import sys import subprocess import os import os.path if len(sys.argv) > 1: pythonpath = sys.argv[1] else: 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 assembled(raw): return ' '.join(["%02X" % c for c in raw]) def assemble_raw(asm="", options=[]): p = subprocess.Popen([pythonpath, ophispath] + options, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return p.communicate(asm.encode("UTF-8")) def assemble_string(asm, options=[]): return assemble_raw(asm, ["-qo", "-", "-"] + 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" % test_name) print("Assembled code: ", assembled(out)) print("Expected code: ", assembled(expected)) if err != '': print("Error output:\n%s" % err.decode(sys.stderr.encoding)) 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 = b'' 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!"', b'Hello, world!') test_string('Newline/EOF passthrough', '.byte 10,26,13,4,0,"Hi",13,10', b'\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', b'\xa9A') test_string('Program counter math', '.org $41\nlda #^+3\n', b'\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('4502 extensions', 'test4502.oph', 'test4502.bin', ['-4']) 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() != b'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 raise # Test 2: Command line override try: assemble_raw('.byte "Hello, world!", 10', ['-', '-o', 'custom.bin']) f = open('custom.bin', 'rb') if f.read() != b'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() != b'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() != b'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() != b'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', b'\xa5\xfe\xa5\xfc\x60') test_string('Reversible collapse', '.org $fb \n bne ^+200 \n lda ^ \n', b'\xf0\x03\x4c\xc5\x01\xad\x00\x01') def test_expressions(): print("\n==== EXPRESSIONS AND LABELS ====") test_string('Basic addition', '.byte 3+2', b'\x05') test_string('Basic subtraction', '.byte 3-2', b'\x01') test_string('Basic multiplication', '.byte 3*2', b'\x06') test_string('Basic division', '.byte 6/2', b'\x03') test_string('Basic bit-union', '.byte 5|9', b'\x0d') test_string('Basic bit-intersection', '.byte 5&9', b'\x01') test_string('Basic bit-toggle', '.byte 5^9', b'\x0c') test_string('Division truncation', '.byte 5/2', b'\x02') test_string('Overflow', '.byte $FF*$10', b'') test_string('Multibyte overflow', '.word $FF*$10', b'\xf0\x0f') test_string('Masked overflow', '.byte $FF*$10&$FF', b'\xf0') test_string('Underflow', '.byte 2-3', b'') test_string('Masked underflow', '.byte 2-3&$FF', b'\xff') test_string('Arithmetic precedence', '.byte 2+3*4-6/2', b'\x0b') test_string('Parentheses', '.byte [2+3]*[4-6/2]', b'\x05') test_string('String escapes', '.byte "The man said, \\"The \\\\ is Windowsy.\\""', b'The man said, "The \\ is Windowsy."') test_string('Byte selector precedence', '.byte >$d000+32,>[$d000+32],<[$D000-275]', b'\xf0\xd0\xed') test_string('Named labels', '.org $6948\nl: .word l', b'Hi') test_string('.alias directive (basic)', '.alias hi $6948\n.word hi', b'Hi') test_string('.alias directive (derived)', '.alias hi $6948\n.alias ho hi+$600\n.word hi,ho', b'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', b'') test_string('.advance directive (basic)', 'lda #$05\n.advance $05\n.byte ^', b'\xa9\x05\x00\x00\x00\x05') test_string('.advance directive (filler)', 'lda #$05\nf: .advance $05,f+3\n.byte ^', b'\xa9\x05\x05\x05\x05\x05') test_string('.advance no-op', 'lda #$05\n.advance $02\n.byte ^', b'\xa9\x05\x02') test_string('.advance failure', 'lda #$05\n.advance $01\n.byte ^', b'') test_string('.checkpc, space > 0', 'lda #$05\n.checkpc $10', b'\xa9\x05') test_string('.checkpc, space = 0', 'lda #$05\n.checkpc 2', b'\xa9\x05') test_string('.checkpc, space < 0', 'lda $05\n.checkpc 1', b'') test_string('A X Y usable as labels', '.alias a 1\n.alias x 2\n.alias y 3\n' 'lda (a+x+y),y\nlda (x+y,x)', b'\xb1\x06\xa1\x05') test_string('Opcodes usable as labels', 'ldy #$00\n dey: dey\n bne dey', b'\xa0\x00\x88\xd0\xfd') def test_segments(): print("\n==== ASSEMBLY SEGMENTS ====") test_string('Segments (basic)', '.org $41\n' '.data\n' '.org $61\n' 'd:\n' '.text\n' 'l: .byte l, d', b'Aa') test_string('Data cleanliness', '.byte 65\n.data\n.byte 65', b'') test_string('.space directive', '.data\n.org $41\n.space a 2\n.space b 1\n.space c 1\n' '.text\n.byte a, b, c\n', b'ACD') test_string('Multiple named segments', '.data\n.org $41\n.data a\n.org $61\n.data b\n.org $4a\n' '.data\n.space q 1\n.data a\n.space r 1\n.data b\n.space s 1\n' '.text\n.org $10\n.text a\n.org $20\n' '.text\n.byte ^,q,r,s\n' '.text a\n.byte ^,q,r,s\n', b'\x10AaJ\x20AaJ') def test_scopes(): print("\n==== LABEL SCOPING ====") test_string('Repeated labels, different scopes', '.org $41\n' '.scope\n' '_l: .byte _l\n' '.scend\n' '.scope\n' '_l: .byte _l\n' '.scend\n', b'AB') test_string('Data hiding outside of scope', '.org $41\n' '.scope\n' '_l: .byte _l\n' '.scend\n' ' .byte _l\n', b'') test_string('Repeated labels, nested scopes', '.org $41\n' '.scope\n' '_l: .byte _l\n' '.scope\n' '_l: .byte _l\n' '.scend\n' ' .byte _l\n' '.scend\n', b'ABA') test_string('Anonymous labels (basic)', '.org $41\n' '* .byte -, +\n' '* .byte -, --\n', b'ACCA') test_string('Anonymous labels (across scopes)', '.org $41\n' '* .byte -, +\n' '.scope\n' '* .byte -, --\n' '.scend\n', b'ACCA') def test_macros(): print("\n==== MACROS ====") test_string('Basic macros', '.macro greet\n' ' .byte "hi"\n' '.macend\n' '`greet\n.invoke greet', b"hihi") test_string('Macros with arguments', '.macro greet\n' ' .byte "hi",_1\n' '.macend\n' "`greet 'A\n.invoke greet 'B", 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", b"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", b"") 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", b"") def test_subfiles(): print("\n==== COMPILATION UNITS ====") test_string(".include pragma", '.include "baseinc.oph"', b'BASIC\n') test_string(".include repeated", '.include "baseinc.oph"\n.include "baseinc.oph"', b'BASIC\nBASIC\n') test_string(".require pragma", '.require "baseinc.oph"', b'BASIC\n') test_string(".include before .require", '.include "baseinc.oph"\n.require "baseinc.oph"', b'BASIC\n') test_string(".require before .include", '.require "baseinc.oph"\n.include "baseinc.oph"', b'BASIC\nBASIC\n') test_string(".require same file twice with different paths", '.include "baseinc.oph"\n.include "sub/baseinc.oph"', b'BASIC\nSUB 1 START\nSUB 1 END\n') test_string(".require different files with identical paths", '.include "sub/sub/sub.oph"', b'SUB 2 START\nSUB 1 START\nBASIC\nSUB 1 END\nSUB 2 END\n') test_string(".charmap (basic)", '.charmap \'A, "abcdefghijklmnopqrstuvwxyz"\n' '.charmap \'a, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"\n' '.byte "hELLO, wORLD!"', b"Hello, World!") test_string(".charmap (reset)", '.charmap \'A, "abcdefghijklmnopqrstuvwxyz"\n' '.charmap \'a, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"\n' '.byte "hELLO, wORLD!",10\n' '.charmap\n' '.byte "hELLO, wORLD!",10\n', b"Hello, World!\nhELLO, wORLD!\n") test_string(".charmap (multi-arg)", '.charmap \'A, "abcdefghijklmnopqrstuvwxyz"\n' '.charmap \'a, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"\n' '.byte "hELLO, ", "wORLD!",10\n', b"Hello, World!\n") test_string(".charmap (out of range)", '.charmap 250, "ABCDEFGHIJKLM"\n.byte 250,251', b'') test_string(".charmapbin (basic)", '.charmapbin "../examples/petscii.map"\n.byte "hELLO, wORLD!"', b"Hello, World!") test_string(".charmapbin (illegal)", '.charmapbin "baseinc.bin"\n.byte "hELLO, wORLD!"', b'') test_string(".incbin (basic)", '.incbin "baseinc.bin"', b"BASIC\n") test_string(".incbin (hardcoded offset)", '.incbin "baseinc.bin",3', b"IC\n") test_string(".incbin (hardcoded offset and length)", '.incbin "baseinc.bin",3,2', b"IC") test_string(".incbin (softcoded offset and length)", '.alias off len+1\n.alias len 2\n' '.incbin "baseinc.bin",off,len', b"IC") test_string(".incbin (length too long)", '.byte 65\n.incbin "baseinc.bin",3,4', b"") test_string(".incbin (negative offset)", '.byte 65\n.incbin "baseinc.bin",1-5,4', b"") test_string(".incbin (offset = size)", '.byte 65\n.incbin "baseinc.bin",6', b"A") test_string(".incbin (offset > size)", '.byte 65\n.incbin "baseinc.bin",7', b"") test_string(".incbin (softcoded length too long)", '.alias off len\n.alias len 4\n' '.byte 65\n.incbin "baseinc.bin",off,len', b"") test_string(".incbin (softcoded negative offset)", '.alias off 1-5\n' '.byte 65\n.incbin "baseinc.bin",off,4', b"") test_string(".incbin (softcoded offset = size)", '.alias off 6\n' '.byte 65\n.incbin "baseinc.bin",off', b"A") test_string(".incbin (softcoded offset > size)", '.alias off 7\n' '.byte 65\n.incbin "baseinc.bin",off', b"") 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() os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) if failed == 0: test_systematic() else: print("\nBasic test cases failed, aborting test.") if failed > 0: print("\nTotal test case failures: %d" % failed) sys.exit(1) else: print("\nAll test cases succeeded") sys.exit(0)