* * * * *
Unit testing from inside an assembler
> Plug plug: I've written an assembler[0] for the 6502 (with full LSP
> (Language Server Protocol) and debugging support). It also supports the
> concept of unit tests whereby your program gets assembled and every test
> individually gets assembled and run, whereby you can add certain asserts to
> check for CPU (Central Processing Unit) register states and things like
> that.
>
> [0] See
https://mos.datatra.sh/guide/unit-testing.html [1]
>
“Plug plug: I've written an assembler[0] for the 6502 (with full LSP and
debuggin... | Hacker News [2]”
This comment (from the Orange Site about a previous post [3]) grabbed my
attention. I'm fascinated by the feature, and I think that's because the test
is run in the assembler! (As a side note—I think they missed an opportunity
by not using TRON to enable tracing) I'm thinking I might try to add a
feature to my my assembler [4], as I've already written a 6809 emulator as a
library [5].
If I already had this feature (and riffing off the sample [6]), how might
this look? What are some of the issues that might come up? I marked up the
random function as I might have done during testing:
-----[ Assembly ]-----
;***********************************************************************
; RANDOM Generate a random number
;Entry: none
;Exit: B - random number (1 - 255)
;***********************************************************************
random ldb lfsr
andb #1
negb
andb #$B4
stb ,-s ; lsb = -(lfsr & 1) & taps
ldb lfsr
lsrb ; lfsr >>= 1
eorb ,s+ ; lfsr ^= lsb
stb lfsr
rts
; --------------------
.test "random"
.tron
ldx #.result_array + 128
.troff
lda #1
sta lfsr
lda #255
loop bsr random
.assert cpu.B <> 0 , "degenerate LFSR"
.tron
tst b,x
.troff
.asert cpu.CC.z <> 1
inc b,x
deca
bne .loop
rts
result_array rmb 256
.endtest
-----[ END OF LINE ]-----
First off, I would have the tracing always print results—that way I can
follow the flow to help see the issue. One open question—would that be a
command line option? Or as I have it here—a pseudo operation? Second, how
would I return from the code? The sample I'm going off uses BRK (the 6502
software interrrupt instruction). I suppose I could use SWI but I would also
want to fill unused memory with that instruction in case the code goes off
into the weeds, so I would need a way to detect the difference. I don't want
to juse use .endtest to end the code sequence, as I might also want to
include variables, like I did here.
Another example, this time the function that had the bug in it:
-----[ Assembly ]-----
;*************************************************************************
; GETPIXEL Get the color of a given pixel
;Entry: A - x pos
; B - y pos
;Exit: X - video address
; A - 0
; B - color
;*************************************************************************
getpixel bsr point_addr ; get video address
.tron
comb ; reverse mask (since we're reading
stb ,-s ; the screen, not writing it)
ldb ,x ; get video data
andb ,s+ ; mask off the pixel
tsta ; any shift?
beq .done
rotate lsrb ; shift color bits
deca
bne .rotate
.troff
done rts ; return color in B
.test "getpixel"
ldd #.screen
std ECB.beggrp
lda #0 ; X
lda #0 ; Y
bsr getpixel
.assert cpu.X = #.screen
.assert cpu.B = 3
lda #1
ldb #0
bsr getpixel
.assert cpu.X = #.screen
.assert cpu.B = 3
lda #2
ldb #0
bsr getpixel
.assert cpu.X = #.screen
.assert cpu.B = 3
lda #3
ldb #0
bsr getpixel
.assert cpu.X = #.screen
.assert cpu.B = 3
rts
screen fcb %11_11_11_11 ; our four pixels
.endtest
-----[ END OF LINE ]-----
More questions: should I be able to trace non-test code? Probably, as that
could help with debugging issues. Also, the function being tested is calling
another function which just happens to be a forward reference, which tells me
that calling the tests should happen on pass two of the assembler. And that
brings up further questions—what about code like this?
-----[ Assembly ]-----
INTCNV equ $B3ED
GIVABF equ $B4F4
org $7000
checksum jsr INTCNV ; get parameter from BASIC
tfr d,y ; it should point to a string variable
ldx 2,y ; get address
lda ,y ; get length
clrb ; clear checksum and Carry bit
sum adcb ,x+ ; add
deca
bne .sum
comb ; 1s compliment
clra ; return 0-255 result
jmp GIVABF ; return result to BASIC
.test "checksum"
ldd #.tmpstr ; our "string"
jsr GIVABF ; give address to BASIC
bsr checksum
jsr INTCNV ; get our result from BASIC
.assert cpu.D = 139 ; if I did my math right
rts
tmpstr fcb 5
fcb 0
fdb .text
fcb 0
text fcc /HELLO/
.endtest
-----[ END OF LINE ]-----
The two routines INTCNV and GIVABF are ROM (Read Only Memory) routines (from
the Color Computer BASIC (Beginners' All-purpose Symbolic Instruction Code)
system) so we don't have the code for the emulator, and therefore, this code
can't be tested as is. I suppose it could be rewritten such that it can be
tested (and use more memory, which could be an issue) but this does show the
limitation of this technique.
I suppose one fix would be conditional assembly:
-----[ Assembly ]-----
.iftest
value fdb 0
INTCNV ldd .value
rts
GIVABF std INTCNV.value
rts
.else
INVCNV equ $B3ED
GIVABF equ $B4F4
.endif
-----[ END OF LINE ]-----
but personally, I'm not a fan of conditional code, but I shouldn't discount
this as a solution.
Another issue is labels. I've been using local labels for the testing code,
thinking that there would be a unique non-local label for each test
(generated by the assembler) to avoid naming conflicts (naming is hard). I
need to think on how I want to handle this.
It's an interesting idea though …
[1]
https://mos.datatra.sh/guide/unit-testing.html
[2]
https://mos.datatra.sh/guide/unit-testing.html#assertions
[3]
gopher://gopher.conman.org/0Phlog:2023/11/27.1
[4]
https://github.com/spc476/a09
[5]
https://github.com/spc476/mc6809
[6]
https://mos.datatra.sh/guide/unit-testing.html
Email author at
[email protected]