Wednesday, November 29, 2023
Unit testing from inside an assembler
Plug plug: I've written an assembler[0] for the 6502 (with full LSP 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 register states and things like that.
Plug plug: I've written an assembler[0] for the 6502 (with full LSP and debuggin... | Hacker News
This comment
(from the Orange Site about a previous post)
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,
as I've already written a 6809 emulator as a library.
If I already had this feature
(and riffing off the sample),
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:
;*********************************************************************** ; 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
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:
;************************************************************************* ; 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
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?
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
The two routines INTCNV
and GIVABF
are ROM routines
(from the Color Computer BASIC 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:
.iftest .value fdb 0 INTCNV ldd .value rts GIVABF std INTCNV.value rts .else INVCNV equ $B3ED GIVABF equ $B4F4 .endif
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 …