2012-06-08 09:49:29 +00:00
|
|
|
#!/usr/bin/python
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import subprocess
|
2012-06-09 04:50:28 +00:00
|
|
|
import os
|
2012-06-08 09:49:29 +00:00
|
|
|
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
|
|
|
|
|
2012-06-09 04:50:28 +00:00
|
|
|
# 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.
|
2012-06-08 09:49:29 +00:00
|
|
|
|
2012-06-09 04:50:28 +00:00
|
|
|
|
|
|
|
def assemble_raw(asm="", options=[]):
|
|
|
|
p = subprocess.Popen([pythonpath, ophispath] + options,
|
2012-06-08 09:49:29 +00:00
|
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
return p.communicate(asm)
|
|
|
|
|
|
|
|
|
2012-06-09 04:50:28 +00:00
|
|
|
def assemble_string(asm, options=[]):
|
|
|
|
return assemble_raw(asm, ["-o", "-", "-"] + options)
|
|
|
|
|
|
|
|
|
2012-06-08 09:49:29 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2012-06-09 04:50:28 +00:00
|
|
|
# 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 ===="
|
2012-06-11 02:08:20 +00:00
|
|
|
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')
|
2012-06-08 09:49:29 +00:00
|
|
|
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'])
|
2012-06-09 04:50:28 +00:00
|
|
|
test_file('Wide instructions', 'testwide.oph', 'testwide.bin', ['-c'])
|
2012-06-08 09:49:29 +00:00
|
|
|
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'])
|
|
|
|
|
2012-06-09 04:50:28 +00:00
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
|
|
|
2012-06-11 02:08:20 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2012-06-09 04:50:28 +00:00
|
|
|
def test_systematic():
|
|
|
|
test_outfile()
|
|
|
|
test_transforms()
|
2012-06-11 02:08:20 +00:00
|
|
|
test_expressions()
|
|
|
|
test_segments()
|
|
|
|
test_scopes()
|
|
|
|
test_macros()
|
|
|
|
test_subfiles()
|
2012-06-09 04:50:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
print "Using Python interpreter:", pythonpath
|
|
|
|
|
|
|
|
test_basic()
|
|
|
|
|
|
|
|
if failed == 0:
|
|
|
|
test_systematic()
|
|
|
|
else:
|
|
|
|
print "\nBasic test cases failed, aborting test."
|
|
|
|
|
2012-06-08 09:49:29 +00:00
|
|
|
if failed > 0:
|
2012-06-09 04:50:28 +00:00
|
|
|
print "\nTotal test case failures: %d" % failed
|
2012-06-08 09:49:29 +00:00
|
|
|
else:
|
2012-06-09 04:50:28 +00:00
|
|
|
print "\nAll test cases succeeded"
|