* * * * * 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 Email author at sean@conman.org .