Donnerstag, 12. Juni 2008

Unit Tests mit Assembler

Testgetriebene Entwicklung ist immer hilfreich, auch für Assemblerprogramme. Sowohl Modultests - das wären Tests der einzelnen Assemblerprozeduren und -macros "von innen" - als auch automatische Funktionstests - das sind Aufrufe der in der DLL publizierten Funktionen durch ein externes Programm - sind in Assembler möglich. Um Modultests zu implementieren, erzeuge ich beim Build nicht nur die gewünschte DLL, sondern auch ein Test-Executable, das neben dem produktiven Code auch noch Testroutinen enthält und diese automatisch ausführt. Zur Protokollierung empfiehlt sich das Test Anything Protocol (TAP) wie es beispielsweise auch in Perl verwendet wird. Ich entwickle mit dem Texteditor UltraEdit und habe im Projektordner mit einem Assemblerfile wie lpe.asm ein Batchfile namens Execlpe.bat, das die DLL und das Testprogramm aus den aktuellen Sourcen assembliert und danach gleich das Testprogramm aufruft. Dieses Batchfile wird auf Tastenkombination ausgeführt, so dass ich während des Entwickelns im Ausgabefenster stets verifizieren kann, dass der Code weiterhin syntaktisch korrekt ist und alle Modultests erfüllt. Wenn etwas bisher OK war und es nach meiner letzten Änderung plötzlich nicht mehr ist, weiss ich, dass es genau an dieser zuletzt gemachten Änderung liegen muss. Durch konsequenten und regelmässigen Einsatz der Modultest-Taste erspare ich mir aufwendiges Debugging.

Die zweite Gattung von Tests, die automatischen Funktionstests, lassen sich am besten durch eine bedingte Assemblierung erreichen. In meinem Projekt geht es um die Portierung einer Java-Klasse nach Assembler. Daher wäre es gut, die DLL-Funktionen von einem Java-Testprogramm aus aufrufen zu können, um sie mit der bereits bestehenden Java-Klasse zu vergleichen. Hierzu könnte man mit dem Java Native Interface aufrufbare Wrapper der zu testenden Funktionen schreiben. Damit diese Wrapper nur für Testzwecke assembliert werden, rufen wir den Assembler in der Kommandozeile mit einem /dFTEST Parameter auf und können dann mit der Direktive ifdef FTEST, wie es auch der C-Präprozessor kennt, die Java-Wrapper nur für die Funktionstest-DLL assemblieren. Bislang habe ich diese Art von Integrationstests jedoch in meinem Assemblerprojekt noch nicht benötigt. Denn die erwarteten Werte zu einer fixen komprimierten Ephemeridendatei wie j1950.dat sind ja apriori bekannt. Ich kann sie daher auch in Form von fixen Erwartungen als Direktwerte in meine Unit Tests mit aufnehmen und erspare mir den aufwendigen Umweg über ein Java-Testprogramm.

Für die Unit Tests selbst habe ich bei diesem meinem allerersten Assemblerprojekt noch keine Lust, ein Framework zu entwickeln. Ich beginne also adhoc mit einer do-Schleife, die der Reihe nach die Namen der Testroutinen einem Symbol testSub zuweist, die Routine aufruft und einen Returncode in EAX entgegennimmt. EAX = 0 bedeutet: Test bestanden, alles andere bedeutet einen Fehlschlag des Tests. Wenn neue Tests hinzukommen, füge ich den Namen der Routine in die Symbolliste ein und setze die Ausgabe im Testplanstring manuell hoch. Das ist gerade genug, um bei diesem ersten Assemblerprojekt mit Unit Tests arbeiten zu können. Bei Gelegenheit - spätestens beim nächsten Assemblerprojekt - werde ich diese Schleife verallgemeinern.

Die main-Prozedur des Unittestprogramms sieht zur Zeit so aus:


;----------------------------------------------------------------------
; main()-Prozedur für Konsolenaufruf
;----------------------------------------------------------------------
.data
testplan db "1..2",13,10,0
not_ok db "not "
ok db "ok ",0
cr_lf db 13,10,0
cmt db "# ",0
compare db " / ",0

.code
main proc

local lStatus:dword ; Zeiger auf "ok" oder "nicht ok"

print offset testplan

testCount = 0
for testsub, <test1, \
test2 \
>
testCount = testCount + 1
invoke testsub
mov ebx, offset ok
or eax, eax
je @F
mov ebx, offset not_ok
@@:
mov lStatus,ebx
print lStatus
print ustr$(testCount)
print offset cr_lf
endm

; tearDown
invoke freeAll
ret
main endp

Keine Kommentare :