* * * * *

                    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]