* * * * *
Unit testing on an 8-bit CPU
I've been using my assembler [1] to write silly little programs for the Color
Computer [2] (simulated [3]—easier than setting up my Color Computer, the
first computer I owned as a kid). It took me entirely too long to locate a
bug in a maze-drawing program I've been writing, and I want to do a deep dive
on this.
The program is simple, it just draws a maze, following a few simple rules:
1. pick a random direction (up, right, left or down);
2. attempt to draw a maze segment (using color 1) in the given direction;
3. if we're boxed in (no direction to go in)—
3. 1. if we're at the starting location, we're done;
3. 2. backtrack along a segment of the path we've drawn (using color 2);
3. 3. if we are no longer boxed in, go back to step 1;
3. 4. otherwise, go back to step 3.
Nothing hard about it, yet the program kept getting stuck.
[Image of a partially drawn maze] [4]
It starts in the upper left, meanders a bit (in blue), backtracks a bit (in
red) until it gets stuck. I would then stare at the code until blood formed
on my brow, simplify the code if I could, and try again only to watch it get
stuck, again.
The issue is that I have no debugger on this system. I don't have two screens
upon which to show debugging output. I have no way of single-stepping though
the code. I don't even have a log to go through. Debugging consists of
running the code, then thinking real hard. And yes, it was a stupid mistake
that took all of two lines of code to fix.
[Image of a fully drawn maze] [5]
Now, the question I want to ask is—would I have saved time if I did “unit
testing?” Not just testing, which I was doing all along, but the typical
style of “unit testing” where you test a “unit” of code (getting back to that
question again [6]). Looking over the code [7] (and that version has the bug—
see if you can find it; you'll also need these two [8] files [9]), the most
isolated “unit” is random (the last subroutine in the code).
-----[ 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
-----[ END OF LINE ]-----
It implements a linear-feedback shift register [10] with a period of 255 (in
that it will return each value in the range of 1‥255 once in some randomesque
order before repeating, which is Good Enough™ for this code). So what if we
want to test that the code is correct?
And it's here I came to a realization—“unit testing” really depends upon the
language and tooling around it. Modern orthodoxy holds “unit testing über
alles” and therefore, modern languages and tooling is created to support
“unit testing über alles” (and woe betide those who question it). I think my
struggle with “unit testing” is that the environments I find myself in don't
lend themselves very well to “unit testing.” Even when I worked at The
Enterprise, we were using C (C99 at best) and C++ (C++98, maybe C++03?) which
take a lot of work upfront to support “unit testing” well, and there wasn't a
lot of work upfront to support “unit testing” well, and there was a decidely
lack of a “unit testing” framework. And here, there's definitely not a “unit
testing” framework. Any “unit testing” I have to do involves writing yet more
code. So let's write yet more code and test this sucker.
-----[ Assembly ]-----
CHROUT equ $A002 ; BASIC character output routine
lfsr equ $F6 ; unused location in direct page
org $4000
start ldx #result_array ; point to memory
clra ; storing 0 to memory
clrb ; 256 bytes to clear
clrarray sta ,x+ ; clear memory
decb ; decrement count
bne .clrarray ; keep going until count = 0
ldx #result_array ; point to array
lda #255 ; cycle length
checkrnd bsr random ; get random number in B
tst b,x ; have we seen this number?
bne .failed ; if so, we have failed
inc b,x ; set flag for this number
deca ; are we done with the cycle
bne checkrnd ; if not, keep going
ldx #msg.success ; SUCCESS!
bsr puts
rts
;---------------------------------------------------
; Store count (register A) and random # (register B)
; so we can use PEEK in BASIC to see where we failed
;---------------------------------------------------
failed std lfsr+1
ldx #msg.failed ; failed message
bsr puts ; display it
rts ; return to BASIC
puts.10 jsr [CHROUT] ; print character
puts lda ,x+ ; get character
bne puts.10 ; if not NUL, print it
rts ; return to BASIC
;******************************************************************
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
;*******************************************************************
msg.success asciiz 'SUCCESS!\r'
msg.failed asciiz 'FAILED!\r'
result_array equ *
end start
-----[ END OF LINE ]-----
The tooling here doesn't support linking 6809 code, and I'd rather not have
to keep each routine in its own file since the program is so simple and makes
editing it easier if everything is in one file (no IDE (Integrated
Development Environment) here—and yes, I have thoughts about testing and IDEs
but that's beyond the scope for this post). So I have to copy the routine to
the test program.
This was something I kept trying to tell my manager at The Enterprise—the
test program itself might be buggy (he personally treated the output as
gospel—sigh). And the “unit testing” proponents seem to hem and haw about
testing the testing code, implying that one does not simply test the testing
code. But if programmers aren't trusted to write code and must test, then why
are they trusted to write testing code without testing?
It might seem I digress, but I'm not. There are four bugs in the above test.
The code I'm testing, random? It was fine. And I wasn't intending to write a
buggy test harness, it just happened as I was writing it. Bug one—I forgot to
test that random didn't return 0 (that's a degenerate case with LFSR (Linear-
Feedback Shift Register)s). Second bug—I forgot to ininitialize the LFSR
state with a non-zero value, so random would return nothing but zero. The
third bug was thinking I had used the wrong condition when branching to the
failure case, but no, I had it right the first time (the fact that I changed
it, and then changed it back, is the bug). The actual bug that caused this
was the fourth bug, but I have to digress a bit to explain it.
The 6809 has an extensive indexing addressing mode for an 8-bit CPU (Central
Processing Unit). One of the modes allow one to use an accumulator register
(A, B or D) as an offset to the index register. I used the B register, which
contains the random number, as an offset into a 256-element array to track
the return values, thus the use of b,x in the code above. What I forgot about
in the moment of writing the code is that the “accumulator,index-register”
indexing mode sign extends the accumulator. And the first value from random
is, due to the LFSR I'm using, if treated as signed, a negative value—it
would fail on the very first attempt.
Sigh.
This is why I panicked and thought I botched the conditional branch.
Now, all of that just to test the most isolated of subroutines in the
program.
But had I continued, would any form of “unit testing” been beneficial?
There's the subroutine point_addr—which converts an X,Y position into a byte
address in the frame buffer, and the pixel in said byte. I could have done an
exhaustive test of all 4,096 points, again, that's code I would have write
(in 6809 Assembly code) and unfortunately, test, to have any confidence in
it. And working up the chain, there's getpixel and setpixel. Testing those
would require a bit of thought—let's see … getpixel returns the color of the
pixel at the given X,Y location on the screen. and assuming point_addr is
working, it would only take four tests (one per pixel in the byte) but at
this point, would I even trust myself to write the test code?
In fact, would “unit testing” have saved me any time?
Given that I would have to write the testing framework, no, I don't think I
would have saved time. Perhaps if I thought the issue through before diving
into changing the code, I would have solved this earlier.
And the clues were there. I did discover pretty early on that the bug was in
the backtracking code. The top level code is pretty easy to visually inspect:
-----[ Assembly ]-----
backtrack lda #BACKTRACK
sta color
loop ldd xpos ; check to see if we're back
cmpd xstart ; at the starting point,
beq done ; and if so, we're done
ldd xpos ; can we backtrack NORTH?
decb
lbsr getpixel
cmpb #EXPLORE
bne .check_east
lbsr move_north.now ; if so, move NORTH and see if
bra .probe ; we have to keep backtracking
check_east ldd xpos ; east ...
inca
lbsr getpixel
cmpb #EXPLORE
bne .check_west
lbsr move_east.now
bra .probe
check_west ldd xpos ; yada yada ...
deca
lbsr getpixel
cmpb #EXPLORE
bne .check_south
lbsr move_west.now
bra .probe
check_south ldd xpos
incb
lbsr getpixel
cmpb #EXPLORE
bne .probe
lbsr move_south.now
probe bsr boxed_in ; can we stop backtracking?
bne explore ; if so, go back to exploring
bra .loop ; else backtrack some more
-----[ END OF LINE ]-----
The thing to keep in mind here is that the D register is a 16-bit register
where the upper 8-bits is the A register, and the lower 8-bits are the B
register, and that the 6809 is big-endian. So when we do ldd xpos we are
loading the A register with the X coordinate, and B with the Y coordinate.
And the move_*.now subroutines work, or else we wouldn't get the starting
maze segments at all. So it's clear that setpixel works fine.
The code is getting stuck trying to backtrack along already drawn segments,
and it does that by calling getpixel, therefore, it seems prudent to check
getpixel. And sure enough, that is where the bug resides.
-----[ 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
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
rotate lsrb ; shift color bits
deca
bne .rotate
done rts ; return color in B
;*************************************************************************
; POINT_ADDR calculate the address of a pixel
;Entry: A - xpos
; B - ypos
;Exit: X - video address
; A - shift value
; B - mask
;*************************************************************************
point_addr.bits fcb %00111111,%11001111,%11110011,%11111100 ; masks
fcb 6,4,2,0 ; bit shift counts
-----[ END OF LINE ]-----
I've included a bit of point_addr to give some context. point_addr returns
the number of shifts required to move the color value into place, and one of
those shift values is 0. But getpixel doesn't check to see it's 0 before
decrementing it. And thus, getpixel will return the wrong value for the last
pixel in any byte. The fix is simple:
-----[ Assembly ]-----
tsta
beq .done
-----[ END OF LINE ]-----
just before the .rotate label fixes the bug (two instructions, three bytes
and here's a link to the fixed code [11]).
Yes, I freely admit that a “unit test“ of this subroutine would have shown
the bug. But how much time would I have spent writing the test code to begin
with? The only reason it took me as long as it did to find was because the
reference code I was using was quite convoluted, and I spent time simplifying
the code as I went along (which is worthy of doing anyway).
What I wish the “unit testing” proponents would realize is that easy testing
depends upon the language and tooling involved in the project, and what a
“unit” is truly depends upon the language. I suspect that “unit test”
proponents also find “unit testing” easier to deal with than “integration
testing” or even “end-to-end testing,” thus why we get “unit tests über
alles” shouted from the roof tops.
[1]
gopher://gopher.conman.org/0Phlog:2023/11/24.1
[2]
https://en.wikipedia.org/wiki/TRS-80_Color_Computer
[3]
http://www.6809.org.uk/xroar/
[4]
gopher://gopher.conman.org/IPhlog:2023/11/27/stuck-maze.png
[5]
gopher://gopher.conman.org/IPhlog:2023/11/27/maze.png
[6]
gopher://gopher.conman.org/0Phlog:2023/11/19.1
[7]
gopher://gopher.conman.org/0Phlog:2023/11/27/maze-bug.asm
[8]
gopher://gopher.conman.org/0Phlog:2023/11/27/Coco-DP.i
[9]
gopher://gopher.conman.org/0Phlog:2023/11/27/Coco-video.i
[10]
https://en.wikipedia.org/wiki/Linear-feedback_shift_register
[11]
gopher://gopher.conman.org/0Phlog:2023/11/27/maze.asm
---
Discussions about this page
Unit testing on an 8-bit CPU - ZeroBytes
https://zerobytes.monster/post/3904613
Unit testing on an 8-bit CPU - derp.foo
https://derp.foo/post/444794
Unit testing on an 8-bit CPU | Hacker News
https://news.ycombinator.com/item?id=38443368
Two Stop Bits | Unit testing on an 8-bit CPU
https://twostopbits.com/item?id=1143
Email author at
[email protected]