Unit testing on an 8-bit CPU – The Boston Diaries

Unit testing on an 8-bit CPU
I have been utilizing my assembler to jot down foolish little packages for the Color Computer
(simulated—simpler than establishing my Shade Laptop, the primary pc I owned as a child).
It took me completely too lengthy to find a bug in a maze-drawing program I have been writing,
and I need to do a deep dive on this.
This system is easy,
it simply attracts a maze,
following a number of easy guidelines:
- choose a random path (up, proper, left or down);
- try to attract a maze phase (utilizing coloration 1) within the given path;
- if we’re boxed in (no path to go in)—
- if we’re on the beginning location, we’re achieved;
- backtrack alongside a phase of the trail we have drawn (utilizing coloration 2);
- if we’re now not boxed in, return to step 1;
- in any other case, return to step 3.
Nothing laborious about it,
but this system stored getting caught.
![It's hardly a maze, thus, it's hardly amazing [Image of a partially drawn maze]](https://blinkingrobots.com/wp-content/uploads/2023/11/Unit-testing-on-an-8-bit-CPU-The-Boston-Diaries.png)
It begins within the higher left,
meanders a bit
(in blue),
backtracks a bit
(in pink)
till it will get caught.
I might then stare on the code till blood shaped on my forehead,
simplify the code if I might,
and take a look at once more solely to look at it get caught,
once more.
The difficulty is that I’ve no debugger on this method.
I haven’t got two screens upon which to indicate debugging output.
I’ve no method of single-stepping although the code.
I do not also have a log to undergo.
Debugging consists of operating the code,
then pondering actual laborious.
And sure,
it was a silly mistake that took all of two traces of code to repair.
![Woot! A maze! That's inging! Amazing! [Image of a fully drawn maze]](https://blinkingrobots.com/wp-content/uploads/2023/11/1701237895_443_Unit-testing-on-an-8-bit-CPU-The-Boston-Diaries.png)
Now,
the query I need to ask is—would I’ve saved time if I did “unit testing?”
Not simply testing,
which I used to be doing all alongside,
however the typical type of “unit testing” the place you check a “unit” of code
(getting back to that question again).
Trying over the code
(and that model has the bug—see if you could find it;
you will additionally want these
two files),
probably the most remoted “unit” is random
(the final subroutine within the code).
;*********************************************************************** ; RANDOM Generate a random quantity ;Entry: none ;Exit: B - random quantity (1 - 255) ;*********************************************************************** random ldb lfsr andb #1 negb andb #$B4 stb ,-s ; lsb = -(lfsr & 1) & faucets ldb lfsr lsrb ; lfsr >>= 1 eorb ,s+ ; lfsr ^= lsb stb lfsr rts
It implements a linear-feedback shift register
with a interval of 255
(in that it’s going to return every worth within the vary of 1‥255 as soon as in some randomesque order earlier than repeating,
which is Good Sufficient™ for this code).
So what if we need to check that the code is right?
And it is right here I got here to a realization—“unit testing” actually relies upon upon the language and tooling round it.
Trendy orthodoxy holds “unit testing über alles” and subsequently,
trendy languages and tooling is created to assist “unit testing über alles”
(and woe betide those that query it).
I believe my battle with “unit testing” is that the environments I discover myself in do not lend themselves very properly to “unit testing.”
Even once I labored at The Enterprise,
we had been utilizing C (C99 at greatest) and C++ (C++98, perhaps C++03?) which take loads of work upfront to assist “unit testing” properly,
and there wasn’t loads of work upfront to assist “unit testing” properly,
and there was a decidely lack of a “unit testing” framework.
And right here,
there’s positively not a “unit testing” framework.
Any “unit testing” I’ve to do entails writing but extra code.
So let’s write but extra code and check this sucker.
CHROUT equ $A002 ; BASIC character output routine lfsr equ $F6 ; unused location in direct web page org $4000 begin ldx #result_array ; level to reminiscence clra ; storing 0 to reminiscence clrb ; 256 bytes to clear .clrarray sta ,x+ ; clear reminiscence decb ; decrement rely bne .clrarray ; maintain going till rely = 0 ldx #result_array ; level to array lda #255 ; cycle size checkrnd bsr random ; get random quantity in B tst b,x ; have we seen this quantity? bne .failed ; in that case, we've got failed inc b,x ; set flag for this quantity deca ; are we achieved with the cycle bne checkrnd ; if not, maintain going ldx #msg.success ; SUCCESS! bsr places rts ;--------------------------------------------------- ; Retailer rely (register A) and random # (register B) ; so we will use PEEK in BASIC to see the place we failed ;--------------------------------------------------- .failed std lfsr+1 ldx #msg.failed ; failed message bsr places ; show it rts ; return to BASIC places.10 jsr [CHROUT] ; print character places lda ,x+ ; get character bne places.10 ; if not NUL, print it rts ; return to BASIC ;****************************************************************** random ldb lfsr andb #1 negb andb #$B4 stb ,-s ; lsb = -(lfsr & 1) & faucets 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 * finish begin
The tooling right here would not assist linking 6809 code,
and I would fairly not must maintain every routine in its personal file for the reason that program is so easy and makes modifying it simpler if every thing is in a single file
(no IDE right here—and sure,
I’ve ideas about testing and IDEs however that is past the scope for this submit).
So I’ve to repeat the routine to the check program.
This was one thing I stored making an attempt to inform my supervisor at The Enterprise—the check program itself may be buggy
(he personally handled the output as gospel—sigh).
And the “unit testing” proponents appear to hem and haw about testing the testing code,
implying that one doesn’t merely check the testing code.
But when programmers aren’t trusted to jot down code and should check,
then why are they trusted to jot down testing code with out testing?
It might sound I digress,
however I am not.
There are 4 bugs within the above check.
The code I am testing, random
?
It was advantageous.
And I wasn’t intending to jot down a buggy check harness,
it simply occurred as I used to be writing it.
Bug one—I forgot to check that random
did not return 0
(that is a degenerate case with LFSRs).
Second bug—I forgot to ininitialize the LFSR state with a non-zero worth,
so random
would return nothing however zero.
The third bug was pondering I had used the fallacious situation when branching to the failure case,
however no,
I had it proper the primary time
(the truth that I modified it, after which modified it again, is the bug).
The precise bug that induced this was the fourth bug,
however I’ve to digress a bit to elucidate it.
The 6809 has an in depth indexing addressing mode for an 8-bit CPU.
One of many modes enable one to make use of an accumulator register (A
, B
or D
)
as an offset to the index register.
I used the B
register,
which incorporates the random quantity,
as an offset right into a 256-element array to trace the return values,
thus using b,x
within the code above.
What I forgot about within the second of writing the code is that the “accumulator,index-register” indexing mode signal extends the accumulator.
And the primary worth from random
is,
as a result of LFSR I am utilizing,
if handled as signed,
a destructive worth—it might fail on the very first try.
Sigh.
Because of this I panicked and thought I botched the conditional department.
Now,
all of that simply to check probably the most remoted of subroutines in this system.
However had I continued,
would any type of “unit testing” been useful?
There’s the subroutine point_addr
—which converts an X,Y place right into a byte tackle within the body buffer,
and the pixel in mentioned byte.
I might have achieved an exhaustive check of all 4,096 factors,
once more,
that is code I might have write
(in 6809 Meeting code)
and sadly, check,
to have any confidence in it.
And dealing up the chain,
there’s getpixel
and setpixel
.
Testing these would require a little bit of thought—let’s examine … getpixel
returns the colour of the pixel on the given X,Y location on the display screen.
and assuming point_addr
is working,
it might solely take 4 checks
(one per pixel within the byte)
however at this level,
would I even belief myself to jot down the check code?
In truth,
would “unit testing” have saved me any time?
Provided that I must write the testing framework,
no,
I do not assume I might have saved time.
Maybe if I assumed the problem by means of earlier than diving into altering the code,
I might have solved this earlier.
And the clues had been there.
I did uncover fairly early on that the bug was within the backtracking code.
The highest degree code is fairly simple to visually examine:
backtrack lda #BACKTRACK sta coloration .loop ldd xpos ; verify to see if we're again cmpd xstart ; at the start line, beq achieved ; and in that case, we're achieved ldd xpos ; can we backtrack NORTH? decb lbsr getpixel cmpb #EXPLORE bne .check_east lbsr move_north.now ; in that case, transfer NORTH and see if bra .probe ; we've got to maintain 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 cease backtracking? bne discover ; in that case, return to exploring bra .loop ; else backtrack some extra
The factor to remember right here is that the D
register is a 16-bit register the place the higher 8-bits is the A
register,
and the decrease 8-bits are the B
register,
and that the 6809 is big-endian.
So once we do ldd xpos
we’re loading the A
register with the X coordinate,
and B
with the Y coordinate.
And the move_*.now
subroutines work,
or else we would not get the beginning maze segments in any respect.
So it is clear that setpixel
works advantageous.
The code is getting caught making an attempt to backtrack alongside already drawn segments,
and it does that by calling getpixel
,
subsequently,
it appears prudent to verify getpixel
.
And positive sufficient,
that’s the place the bug resides.
;************************************************************************* ; GETPIXEL Get the colour of a given pixel ;Entry: A - x pos ; B - y pos ;Exit: X - video tackle ; A - 0 ; B - coloration ;************************************************************************* getpixel bsr point_addr ; get video tackle comb ; reverse masks (since we're studying stb ,-s ; the display screen, not writing it) ldb ,x ; get video knowledge andb ,s+ ; masks off the pixel .rotate lsrb ; shift coloration bits deca bne .rotate .achieved rts ; return coloration in B ;************************************************************************* ; POINT_ADDR calculate the tackle of a pixel ;Entry: A - xpos ; B - ypos ;Exit: X - video tackle ; A - shift worth ; B - masks ;************************************************************************* point_addr.bits fcb %00111111,%11001111,%11110011,%11111100 ; masks fcb 6,4,2,0 ; bit shift counts
I’ve included a little bit of point_addr
to provide some context.
point_addr
returns the variety of shifts required to maneuver the colour worth into place,
and a kind of shift values is 0.
However getpixel
would not verify to see it is 0 earlier than decrementing it.
And thus,
getpixel
will return the fallacious worth for the final pixel in any byte.
The repair is easy:
tsta beq .achieved
simply earlier than the .rotate
label fixes the bug
(two directions, three bytes and this is a hyperlink to the fixed code).
Sure,
I freely admit {that a} “unit check“ of this subroutine would have proven the bug.
However how a lot time would I’ve spent writing the check code to start with?
The one cause it took me so long as it did to search out was as a result of the reference code I used to be utilizing was fairly convoluted,
and I frolicked simplifying the code as I went alongside
(which is worthy of doing anyway).
What I want the “unit testing” proponents would notice is that simple testing relies upon upon the language and tooling concerned within the venture,
and what a “unit” is really relies upon upon the language.
I think that “unit check” proponents additionally discover “unit testing” simpler to take care of than “integration testing” and even “end-to-end testing,”
thus why we get “unit checks über alles” shouted from the roof tops.
Discussions about this entry