Testing
FantASM carries a Z80 and Z80N simulator, so a test runs your actual routines on an emulated CPU at build time and checks what they left behind. No emulator to launch, no harness to write, and nothing about the test reaches the output.
org $8000
double:
add a,a
ret
!test "double doubles"
init_reg a, 3
call double
halt
assert_reg a, 6
endtestfantasm build main.asm --testTests are collected whether or not you pass --test, and run only when you do.
Shaping a test
A block is !test "name" … endtest. Between them are two kinds of line:
init_*andassert_*are directives. They are collected from the block, not executed in place — so where you write an assert makes no difference. Before thehaltor after it, the result is the same.- Everything else is Z80 code, assembled and run.
So the body is: the machine is set up from the init_* lines, the code runs until it halts, and then the assert_* lines are checked against the final state.
⛔ The body must halt. The simulator runs until it does, up to a limit, and a body that never halts fails with Execution timed out or failed to halt rather than hanging the build.
⛔ A body with no code fails, even if its assertions would pass — Test body is empty or failed to assemble. A test that checks the initial state and runs nothing is almost certainly not what you meant.
Setting up and checking
| Directive | |
|---|---|
init_reg <reg>, <value> | Set a register before the code runs |
init_mem <addr>, <value> | Set a byte of memory |
assert_reg <reg>, <value> | Check a register afterwards |
assert_mem <addr>, <value> | Check a byte of memory |
assert_cycles <limit> | Check the run took no more T-states than this |
init_reg and assert_reg take 8-bit and 16-bit registers alike — a through l, bc, de, hl, ix, iy, sp and af. An indirect such as (hl) is not a register and is refused; use init_mem.
!test "adds a pair"
init_reg hl, $1000
init_reg de, $0234
call addhl
halt
assert_reg hl, $1234
endtest
!test "copies a byte"
init_mem $9000, $42
ld a,($9000)
ld ($9001),a
halt
assert_mem $9001, $42
endtestassert_cycles is how you keep an interrupt handler honest — it fails the build when a routine grows past its budget, which is the kind of regression nothing else catches.
Calling your own code
Calling the routine under test is the point, and the whole assembled image is loaded before the test runs, so any label is reachable — including one inside a module, by its qualified name:
MODULE Gfx
Double:
add a,a
ret
ENDMODULE
!test "calls into a module"
init_reg a, 4
call Gfx.Double
halt
assert_reg a, 8
endtestThe test body is assembled past the end of your program, so it never overlaps the code it is testing.
Keeping tests out of the output
A !test block is build-time only. The program above assembles to four bytes — 87 C9 19 C9, being the two routines — however many tests sit beside it.
Reading a failure
Info: Test: double doubles - FAILED: AssertReg A failed: expected 99, got 6
Info: Tests completed: 0 passed, 1 failed
Error [E1058]: General error: 1 tests failedThe build exits non-zero.
⛔ You need -v to see which test failed and why. Without it, a failing run prints only 1 tests failed — not the name, not expected-versus-got. Run fantasm build --test -v when something breaks, or leave verbose = true in fantasm.toml as fantasm init does.
With several targets, counts are reported against the target the tests came from — see Building Several Images.
Keeping a test-only routine
A routine called only from a !test is reported unused by -W, and discarded by --gc-modules if it is inside a module: a reference from inside a test block does not count as a use. Mark it GLOBAL, or KEEP it — see Modules for what KEEP does.
Under --gc-modules the build says so rather than leaving you to work it out from a test that will not finish:
Warning [W1084]: `Thing.First` is referenced only by a `!test` block and has been
discarded by --gc-modules. Pin it with `KEEP Thing.First` or
`GLOBAL Thing.First` if the test should runPinning it is a statement about the program — this routine must survive — so the image the tests run against stays the image that ships.