* * * * *
Unit testing from inside an assembler, part II
I started working on unit tests from inside the assembler [1]. I'm not sure
how MOS [2] does it (as I don't read Rust [3]) so I'm making this up as I go
along. I'm using the following file as a test case for the work:
-----[ Assembly ]-----
lfsr equ $F6
org $4000
start bsr random
rts
the.byte fcb $55
the.word fdb $AAAA
;***********************************************
; RANDOM Generate a random number
;Entry: none
;Exit: B - random number (1 - 255)
;***********************************************
random ldb lfsr
andb #1
negb
andb #$B4
stb ,-s
ldb lfsr
lsrb
eorb ,s+
stb lfsr
rts
.test "random"
ldx #.result_array
clra
clrb
setmem sta ,x+
decb
bne .setmem
ldx #.result_array + 128
lda #1
sta lfsr
lda #255
.tron
loop bsr random
.assert /B <> 0 , "degenerate LFSR"
tst b,x
.assert /CC.z <> 1 , "non-repeating"
.troff
inc b,x
deca
bne .loop
.assert @the.byte == $55 && @@the.word == $AAAA , "tis a silly test"
rts
result_array rmb 256
.endtst
nop
;***********************************************
end start
-----[ END OF LINE ]-----
I've made the “unit test” … thing, a backend (like I have for binary and
Color Computer-specific output as backends) because it's less intrusive on
the code and I wasn't sure where to assemble the test code (within the memory
space of the 6809). By making this a specific backend, it should be apparent
that this is not for the final version of the code.
So far, I have it such that all the non-test backends don't see the code at
all:
-----[ Listing ]-----
| FILE test.asm
1 |
2 | lfsr equ $F6
3 |
4 | org $4000
4000: 8D 04 5 | start bsr random
4002: 39 6 | rts
7 |
4003: 55 8 | the.byte fcb $55
4004: AAAA 9 | the.word fdb $AAAA
10 |
11 | ;***********************************************
12 | ; RANDOM Generate a random number
13 | ;Entry: none
14 | ;Exit: B - random number (1 - 255)
15 | ;***********************************************
16 |
4006: D6 F6 17 | random ldb lfsr
4008: C4 01 18 | andb #1
400A: 50 19 | negb
400B: C4 B4 20 | andb #$B4
400D: E7 E2 21 | stb ,-s
400F: D6 F6 22 | ldb lfsr
4011: 54 23 | lsrb
4012: E8 E0 24 | eorb ,s+
4014: D7 F6 25 | stb lfsr
4016: 39 26 | rts
27 |
28 | .test "random"
29 | ldx #.result_array
30 | clra
31 | clrb
32 | .setmem sta ,x+
33 | decb
34 | bne .setmem
35 | ldx #.result_array + 128
36 | lda #1
37 | sta lfsr
38 | lda #255
39 | .tron
40 | .loop bsr random
41 | .assert /B <> 0 , "degenerate LFSR"
42 | tst b,x
43 | .assert /CC.z <> 1 , "non-repeating"
44 | .troff
45 | inc b,x
46 | deca
47 | bne .loop
48 | .assert @the.byte == $55 && @@the.word == $AAAA , "tis a silly test"
49 | rts
50 | .result_array rmb 256
51 |
52 | .endtst
52 | .endtst
53 |
4017: 12 54 | nop
55 |
56 | ;***********************************************
57 |
58 | end start
2 | equate 00F6 3 lfsr
17 | address 4006 1 random
5 | address 4000 1 start
-----[ END OF LINE ]-----
Ignore that line 52 shows up twice here—that's a bug that I'll work on (my
initial fix removed the duplicate line, but line 51 didn't show up—it's not a
show-stopping bug which I why it's going on the “fix it later” list). Also,
the labels the.byte and the.word don't show up on the symbol list at the end
due to a “feature” where labels that aren't referenced aren't printed (that
was to remove unused equates from the symbol list). So for the non-test
backends, the actual testcase isn't part of the build.
The other added directives, like .tron, .troff and .assert are also ignored
by the other backends if the directives appear outside a “unit test.”
With the .test backend though, all the directives are recognized and most of
them work, although I'm still working on .assert (see below).
One issue—when to run the actual tests. Right now, the code is run when then
endtst directive is hit, as running the code as it's assembled won't work
well I think, especially with branches and calls to other routines, and it
would be a nightmare to get correct. It's easier if all the code exists in
“memory,” but one issue I've noticed is that any code further down in the
file can't be used. I'll have to move the execution of tests to after the
assembly pass is done.
The .tron and .troff directives work, dumping out the instructions between
them as the code is run:
-----[ data ]-----
.. lots of lines cut
PC=402A X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=09 B=D2 CC=-f-i---c | 402A 8D DA - BSR 4006 ; ----- backwards
PC=402C X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=09 B=69 CC=-f-i---- | 402C 6D 85 - TST B,X ; -aa0- 411D = 00
PC=402A X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=08 B=69 CC=-f-i---- | 402A 8D DA - BSR 4006 ; ----- backwards
PC=402C X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=08 B=80 CC=-f-in--c | 402C 6D 85 - TST B,X ; -aa0- 4034 = 00
.. more lines cut
-----[ END OF LINE ]-----
Another issue is dealing with the .assert directive. I have to save the test
somehow since the assembler can't do the check when it parses the .assert
because not all the code for the test has been assembled yet. I could store
the text to the test expression and then evaluate it at run time, but as this
code shows, that would mean re-interpreting the text many, many times. No,
the solution I came up with is a mini-Forth-like language for evaluating the
test expression.
Yup, I'm embedding a mini-Forth interpreter in a 6809 assembler written in C.
A classic blunder I'm sure, like getting involved in a land war in Asia, or
going against a Sicilian when death is on the line, but I'm not sure of any
other way. The mini-Forth is very small though, only 41 words are defined,
but it's enough for my needs. The first .assert expression translates to:
-----[ miniForth ]-----
VM_CPUB ( push contents of the B register onto the stack )
VM_LIT 0 ( push a literal 0 onto the stack )
VM_NE ( compare the two, leaving a flag on the stack )
VM_EXIT ( exit the VM )
-----[ END OF LINE ]-----
The second one to:
-----[ miniForth ]-----
VM_CPUCCz ( push the CC zero flag )
VM_LIT 0 ( push a literal 0 )
VM_NE ( compare the two, leave flag on stack )
VM_EXIT ( exit the VM )
-----[ END OF LINE ]-----
And the last one to:
-----[ miniForth ]-----
VM_LIT 0x4003 ( push the literal 0x4003 )
VM_AT8 ( fetch the byte from the 6809 memory buffer )
VM_LIT 0x55 ( push the literal 0x55 )
VM_EQ ( compare the two, leave flag on stack )
VM_LIT 0x4004 ( push the literal 0x4004 )
VM_AT16 ( fetch two bytes from the 6809 memory buffer )
VM_LIT 0xAAAA ( push the literal 0xAAAA )
VM_EQ ( compare the two, leave flag on stack )
VM_LAND ( AND the two results, leaving flag on stack )
VM_EXIT ( exit the VM )
-----[ END OF LINE ]-----
This works, and it was easy to implement the VM (Virtual Machine). Now all I
have to do is parse the expression to assemble the VM code (right now the
addresses and VM functions are hard coded into the assembler just to prove it
works).
This feature is proving to be an interesting problem.
[1]
gopher://gopher.conman.org/0Phlog:2023/11/29.2
[2]
https://mos.datatra.sh/guide/unit-testing.html
[3]
https://github.com/datatrash/mos
Email author at
[email protected]