Unit testing from inside an assembler

Plug plug: I've written an assembler[0] for the 6502 (with full LSP (Language Server Protocol) 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 (Central Processing Unit) register states and things like that.
[0] See https://mos.datatra.sh/guide/unit-testing.html [1]

“Plug plug: I've written an assembler[0] for the 6502 (with full LSP and debuggin... | Hacker News [2]”

This comment (from the Orange Site about a previous post [3]) 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 [4], as I've already written a 6809 emulator as a library [5].

If I already had this feature (and riffing off the sample [6]), 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 (Read Only Memory) routines (from the Color Computer BASIC (Beginners' All-purpose Symbolic Instruction Code) 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 …

[1] https://mos.datatra.sh/guide/unit-testing.html

[2] https://mos.datatra.sh/guide/unit-testing.html#assertions

[3] /boston/2023/11/27.1

[4] https://github.com/spc476/a09

[5] https://github.com/spc476/mc6809

[6] https://mos.datatra.sh/guide/unit-testing.html

Gemini Mention this post

Contact the author