mirror of
https://github.com/michaelcmartin/Ophis.git
synced 2024-09-07 13:54:30 +00:00
472 lines
18 KiB
Python
Executable File
472 lines
18 KiB
Python
Executable File
#!/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)
|