As part of the NES homebrew development I’ve been working on, I wanted to have some way to unit test my code. Doing so provides peace of mind that code works, and also allows for future changes to made safely. This is particularly important for low-level assembly, where correctness and speed are both important, and attempts at optimization can easily break code. Not finding any pre-existing tools, I figured I could easily pull something together.
The first step was finding a 6502 / NES emulator that allowed programmatic inspection of state (memory / registers) from the command-line with minimal overhead. The project cl-6502 by redline6561 fit the bill, allowing tests to use Common Lisp code to setup state, run assembled code, and then make assertions on the final state.
Next, I needed to make a tiny framework to bridge between cl-6502’s features and how my NES code was organized. My project uses ca65 as its assembler, which is nice because it can assemble files separately, and also it matches the syntax of cl-6502’s assembler. On the other hand cl-6502, being quite minimalistic, doesn’t support directives, so we need to preprocess our source to filter these out. It also has a very granular interface, and we’d prefer something a little higher-level, at the level of subroutines.
Since it’s safe to assume most homebrew developers aren’t familiar with Common Lisp, I wanted to provide a very simple and obvious API for the unit tests. The result, nes_unit_testing.lisp, favors convention over configuration, making a number of assumptions about how subroutines are written. For example, unit test filenames match the assembly source except they end in “_test.lisp”. Also, the test framework starts executing at the very beginning of the source file, meaning only one subroutine can exist per file.
Provided is a simple calculator ROM, that enables the input of two numbers, and can add them together. As part of this, there’s a file convert_decimal.asm
which contains a function to convert an integer into 3 decimal digits. This is the documentation of the subroutine:
convert_decimal.asm
;ConvertDecimal
; Given a number in A, convert it to three decimal digits, in Y, X, A.
; .reg:a @in The number to convert.
; .reg:a @out The ones digit, 0-9.
; .reg:x @out The tens digit, 0-9.
; .reg:y @out The hundreds digit, 0-2.
.proc ConvertDecimal
...
rts
.endproc
Complete code is here.
And the complete test code, which verifies every possible 8-bit value, looks like this:
convert_decimal_test.lisp
(in-package :nes-unit-testing)
(deftest convert-decimal-test
(initialize-test-case :env `((remainder-mod8 :byte)))
(loop for num from 0 upto 255
do (run-test-case :a num)
(expect-result :y (floor (/ num 100))
:x (mod (floor (/ num 10)) 10)
:a (mod num 10))))
Most of what’s happening here should be self-explanatory, but just in case, here’s a line by line breakdown.
Lines 1-3 set our package and start defining a test.
Line 4 initializes the test by parsing the source code, clearing ram, and defining variables our subroutine uses.
Line 5 performs a loop, iterating from 0 to 255, inclusive.
Line 6 assigns the number to the A register, then executes the subroutine.
Lines 7-9 verify that the Y register is set to the hundreds digit, the X register is set to the tens digit, and the A register is set to the ones digit.
Running the unit test is simple, once all the dependencies are installed. The included script ./run_tests.sh
loads cl-6502, nes_unit_testing.lisp
, and then runs all-tests-in-current-directory
which loads every *_test.lisp script in the current directory and runs all the defined tests. Here’s what running the test looks like, with timing:
> time ./run_tests.sh
Success!
real 0m1.828s
user 0m1.468s
sys 0m0.249s
Even failures are pleasant. Removing line 57 from convert_decmal.asm
(adc #$0a) breaks the code as shown:
> ./run_tests.sh
Failed on test: ((A 88))
A - Actual 254, Expect 8
Failed on test: ((A 89))
A - Actual 255, Expect 9
Failed on test: ((A 96))
A - Actual 252, Expect 6
...
Code for the calculator rom is on github here, and code for the unit testing framework is here.
Leave a Reply