* * * * *

                Unit testing from inside an assembler, part IV

I'm not terribly happy with how running unit tests inside my assembler [1]
work. I mean, it works, as in, it tests the code and show problems during the
assembly phase, but I don't like how you write the tests in the first place.
Here's one of the tests I added to my maze generation program [2] (and the
routine it tests):

-----[ Assembly ]-----
getpixel        bsr     point_addr      ; get video address
               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
done           rts                     ; return color in B

       .test
       .opt    test    pokew   ECB.beggrp , $0E00
       .opt    test    poke    $0E00 , %11_11_11_11
               lda     #0
               ldb     #0
               bsr     getpixel
       .assert /d = 3
       .assert /x = @@ECB.beggrp
               lda     #1
               ldb     #0
               bsr     getpixel
       .assert /d = 3
       .assert /x = @@ECB.beggrp
               lda     #2
               ldb     #0
               bsr     getpixel
       .assert /d = 3
       .assert /x = @@ECB.beggrp
               lda     #3
               ldb     #0
               bsr     getpixel
       .assert /d = 3
       .assert /x = @@ECB.beggrp
               rts
       .endtst
-----[ END OF LINE ]-----

The problem is the machine code for the test is included in the final binary
output, which is bad because I can't just set an option to run the tests in
addition to assembling the code into its final output, which I don't want
(and that means when I use the test backend, I tend to generate the output to
/dev/null). I've also found that I prefer table-style tests to writing code
(for reasons way beyond the scope of this entry). For example, for a C
function like this:

-----[ C ]-----
int max_monthday(int year,int month)
{
 static int const days[] = { 31,0,31,30,31,30,31,31,30,31,30,31 } ;

 assert(year  > 1969);
 assert(month >    0);
 assert(month <   13);

 if (month == 2)
 {
   /*----------------------------------------------------------------------
   ; in case you didn't know, leap years are those years that are divisible
   ; by 4, except if it's divisible by 100, then it's not, unless it's
   ; divisible by 400, then it is.  1800 and 1900 were NOT leap years, but
   ; 2000 is.
   ;----------------------------------------------------------------------*/

   if ((year % 400) == 0) return 29;
   if ((year % 100) == 0) return 28;
   if ((year %   4) == 0) return 29;
   return 28;
 }
 else
   return days[month - 1];
}
-----[ END OF LINE ]-----

I would prefer to write test code like:

Table: Test code for max_monthday()
output  year    month
------------------------------
28      1900    2
29      2000    2
28      2100    2
29      1904    2
29      2104    2
28      2001    2

Just specify the inputs and outputs for some corner cases, and let the
computer do what is necessary to call the function in question.

But it's not so easy with assembly language, given the large number of ways
to pass data into a function, and the number of output results one can have.
How would I specify that the inputs come in registers A and B, and the
outputs come in A, B and X? The above could be done in a table format, I
guess. It might not be pretty, but it's doable.

Then there's these subroutines and their associated tests:

-----[ Assembly ]-----
;***********************************************************************
;       RND4            Generate a random number 0 .. 3
;Entry: none
;Exit:  B - random number
;***********************************************************************

rnd4            dec     rnd4.cnt        ; any more cached random #s?
               bpl     .cached         ; yes, get next cached number
               ldb     #3              ; else reset count
               stb     rnd4.cnt
               bsr     random          ; get random number
               stb     rnd4.cache      ; save in the cache
               bra     .ret            ; and return the first number
cached         ldb     rnd4.cache      ; get cached value
               lsrb                    ; get next 2-bit random number
               lsrb
               stb     rnd4.cache      ; save ermaining bits
ret            andb    #3              ; mask off our result
               rts

;***********************************************************************
;       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
               ldx     #.result_array
               clra
               clrb
setmem         sta     ,x+
               decb
               bne     .setmem
               ldx     #.result_array + 128
               lda     #1
               sta     lfsr
               lda     #255
loop           bsr     random
       .assert /b <> 0         , "degenerate LFSR"
       .assert @/b,x = 0       , "non-repeating LFSR"
               inc     b,x
               deca
               bne     .loop

               clr     ,x
               clr     1,x
               clr     2,x
               clr     3,x
               lda     #255
chk4           bsr     rnd4
       .assert /b >= 0
       .assert /b <= 3
               inc     b,x
               deca
               bne     .chk4
       .tron
               ldb     ,x      ; to check the spread
               ldb     1,x     ; of results, basically
               ldb     2,x     ; these should be roughly
               ldb     3,x     ; 1/4 of 256
       .troff
       .assert @/,x + @/1,x + @/2,x + @/3,x = 255
               rts
result_array   rmb     256
       .endtst

       .test   "whole program"
       .opt    test    pokew   $A000 , KEYIN
       .opt    test    pokew   $FFFE , END
       .opt    test    prot    r,$A000,$A001

               lbsr    start
KEYIN           lda     #'Q'
END             rts

       .endtst
-----[ END OF LINE ]-----

And … just uhg. I mean, this checks that the 8-bit LFSR (Linear-Feedback
Shift Register) [3] I'm using to generate random numbers actually doesn't
repeat within it's 255-period cycle, and that the number of 2-bit random
numbers I generate from RND4 is more or less evenly spread, and for both of
those, I use an array to store the intermediate results. I leary about
including an interpreter just for the tests, because I don't think it would
be any better. At least the test code is largely written in the target
language of 6809 assembly.

Then again, I could embed Lua, and write the tests like:

-----[ Assembly ]-----
       .test
               local array = {}
               for i = 0 , 255 do array[i] = 0 end

               mem['lfsr'] = 1
               for i = 0 , 255 do
                 call 'random'
                 assert(cpu.B ~= 0)
                 assert(array[cpu.B] == 0)
                 array[cpu.B] = 1
               end

               array[0] = 0
               array[1] = 0
               array[2] = 0
               array[3] = 0

               for i = 0 , 255 do
                 call 'rnd4'
                 assert(cpu.B >= 0)
                 assert(cpu.B <= 3)
                 array[cpu.B] = array[cpu.B] + 1
               end

               assert(array[0] + array[1] + array[2] + array[3] == 255)
       .endtst
-----[ END OF LINE ]-----

I suppose? I would still need to somehow code the fake KEYIN and END routines
required for the test. And the first test at the start of this post would
then look like:

-----[ Assembly ]-----
       .test
               memw['ECB.beggrp'] = 0x0E00
               mem[0x0E00] = '%11_11_11_11'
               cpu.A = 0
               cpu.B = 0
               call 'getpixel'
               assert(cpu.D == 3)
               assert(cpu.X == memw['ECB.beggrp'])
               cpu.A = 1
               cpu.B = 0
               call 'getpixel'
               assert(cpu.D == 3)
               assert(cpu.X == memw['ECB.beggrp'])
               cpu.A = 2
               cpu.B = 0
               call 'getpixel'
               assert(cpu.D == 3)
               assert(cpu.X == memw['ECB.beggrp'])
               cpu.A = 3
               cpu.B = 0
               call 'getpixel'
               assert(cpu.D == 3)
               assert(cpu.X == memw['ECB.beggrp'])
       .endtst
-----[ END OF LINE ]-----

which isn't any longer than the original test, but still … uhg. But doing
this means I won't have 6809 code for testing in the final output, which
means I could run tests with any backend.

I'll have to think on this.

[1] gopher://gopher.conman.org/0Phlog:2023/12/06.1
[2] gopher://gopher.conman.org/0Phlog:2023/11/27.1
[3] https://en.wikipedia.org/wiki/Linear-feedback_shift_register
---

Discussions about this page

Unit testing from inside an assembler | Lobsters
 https://lobste.rs/s/tpvsa4/unit_testing_from_inside_assembler

Unit testing from inside an assembler - Lemmy: Bestiverse
 https://lemmy.bestiver.se/post/77867

Email author at [email protected]