Files
2025-10-14 09:52:32 +09:00

1368 lines
40 KiB
Plaintext

; --------------------------------------------------------------------------------
; @Title: LBTEST - Unit tests in PRACTICE
; @Description:
; Unit test framework for automated tests
; @Keywords: assertion assertions lbtest test unit unittest
; @Author: MOB
; @Copyright: (C) 1989-2021 Lauterbach GmbH, licensed for use with TRACE32(R) only
; --------------------------------------------------------------------------------
; $Id: lbtest.cmm 21247 2023-08-10 05:43:04Z mobermeir $
PMACRO.EXPLICIT // force the script-writer to declare all macros explicitly
PRIVATE &lbTestOptions
ENTRY %LINE &lbTestOptions
ON CoMmanD PUTS GOSUB Puts // helper command for printing
Var.NEWLOCAL char[9.][4096.] \LbTest_settings
GOSUB ParseOptions "&lbTestOptions"
GOSUB Initialize
PRIVATE &statusAllTestCases
GOSUB RunAllTestCases
RETURNVALUES &statusAllTestCases
GOSUB Finalize
ENDDO "&statusAllTestCases"
; --------------------------------------------------------------------------------
; parse parameters and set configuration options:
ParseOptions:
(
PARAMETERS &options
IF (STRing.SCAN("&options","--help",0.)>=0.)
(
PRINT "Usage: "
PRINT " DO lbtest.cmm [options]"
PRINT ""
PRINT "Available options:"
PRINT " --workingdir=</path/to/dir> run in /path/to/dir, i.e. expects to find the"
PRINT " tests there and also writes logfiles there."
PRINT " default: current working directory (pwd)"
PRINT " --logfile=<file> write log output to <file> (default: lbtest.log)"
PRINT " only applicable if --logging=allinone"
PRINT " --summarylogfile=<file> write summary log output to <file> "
PRINT " (default: test_summary.log)"
PRINT " --logging=<allinone|individual> write log output to one file (default) or"
PRINT " to individual files for each test case"
PRINT " --printtime=<on|off> print elapsed time of each test (default: on)"
PRINT " --debugmode=<mode> stop script for debugging, available <mode>s:"
PRINT " failedassertion: stop after a failed assertion"
PRINT " unhandlederror: stop after an unhandled error"
PRINT " --logcommands=<off|all> print every command before executing it"
PRINT " --verbose print more log messages"
PRINT " --veryverbose print even more log messages"
PRINT " --help display this help and exit"
PRINT ""
ENDDO
)
PRIVATE &param &workingdir
&workingdir=STRing.Replace(OS.PresentWorkingDirectory(),"\","/",0)
Var.Assign \LbTest_settings[8.]="&workingdir" // backup initial working directory
&workingdir=STRing.SCANAndExtract("&options","--workingdir=","&workingdir")
&workingdir=STRing.Replace(OS.FILE.ABSPATH("&workingdir"),"\","/",0)
Var.Assign \LbTest_settings[7.]="&workingdir"
&param=STRing.SCANAndExtract("&options","--logfile=","&workingdir/lbtest.log")
&param=STRing.Replace("&param","\","/",0)
Var.Assign \LbTest_settings[0.]="&param"
&param=STRing.SCANAndExtract("&options","--summarylogfile=","&workingdir/lbtest_summary.log")
&param=STRing.Replace("&param","\","/",0)
Var.Assign \LbTest_settings[1.]="&param"
&param=STRing.SCANAndExtract("&options","--logging=","allinone")
Var.Assign \LbTest_settings[2.]="&param"
&param=STRing.SCANAndExtract("&options","--printtime=","on")
Var.Assign \LbTest_settings[3.]="&param"
&param=STRing.SCANAndExtract("&options","--debugmode=","")
Var.Assign \LbTest_settings[4.]="&param"
&param=STRing.SCANAndExtract("&options","--logcommands=","off")
Var.Assign \LbTest_settings[5.]="&param"
IF (STRing.SCAN("&options","--verbose",0.)>=0.)
(
Var.Assign \LbTest_settings[6.]="verbose"
)
IF (STRing.SCAN("&options","--veryverbose",0.)>=0.)
(
Var.Assign \LbTest_settings[6.]="veryverbose"
)
RETURN
)
Initialize:
(
PRIVATE &requiredSoftwareVersion &testSummaryLog
&requiredSoftwareVersion=143582.
IF (VERSION.BUILD.BASE()<&requiredSoftwareVersion)
(
PRINT %ERROR "lbtest.cmm requires at least TRACE32 software version &requiredSoftwareVersion (this is " SOFTWARE.VERSION() ")"
ENDDO "FAIL"
)
IF (Var.STRing(\LbTest_settings[5.])=="all") // --logcommands=all
(
LOG.toAREA ON /IndentCalls /COLOR GRAY
)
AREA.Create A000 1024. 32767.
AREA.CLEAR
IF (Var.STRing(\LbTest_settings[2.])=="allinone") // --logging=allinone
(
PRIVATE &logfile
&logfile=Var.STRing(\LbTest_settings[0.]) // --logfile=<file>
AREA.OPEN A000 "&logfile" /Create /NoFileCache // start logging
)
AREA.Select A000
AREA.view A000
&testSummaryLog=Var.STRing(\LbTest_settings[1.]) // --summarylogfile=<file>
PUTS "logging test summary to &testSummaryLog" "DEBUG1"
IF (OS.FILE.readable("&testSummaryLog"))
(
RM "&testSummaryLog"
)
IF (OS.PresentWorkingDirectory()!=Var.STRing(\LbTest_settings[7.]))
(
// change to working directory (defaults to pwd)
PRIVATE &workingdir
&workingdir=Var.STRing(\LbTest_settings[7.]) // --workingdir=</path/to/dir>
SILENT.ChDir "&workingdir"
)
RETURN
)
Finalize:
(
IF (Var.STRing(\LbTest_settings[2.])=="allinone") // --logging=allinone
(
AREA.CLOSE A000 // stop logging to lbtest.log
)
AREA.Select A000
AREA.view A000
IF (Var.STRing(\LbTest_settings[5.])=="all") // --logcommands=all
(
LOG.toAREA OFF
)
IF (OS.PresentWorkingDirectory()!=Var.STRing(\LbTest_settings[8.])) // initial working directory
(
// change back to initial working directory
PRIVATE &workingdir
&workingdir=Var.STRing(\LbTest_settings[8.])
SILENT.ChDir "&workingdir"
)
RETURN
)
RunAllTestCases:
(
PRIVATE &testCaseIndex &statusAllTests &numTestCases &unitTestStartTime &unitTestDuration
PRIVATE &numTestCasesPass &numTestCasesFail &numTestCasesNotExec
&numTestCasesPass=0.
&numTestCasesFail=0.
&numTestCasesNotExec=0.
Var.NEWLOCAL char[1000.][256.] \LbTest_testCases
GOSUB FindTestCases "\LbTest_testCases"
RETURNVALUES &numTestCases
GOSUB PutsStart "UNITTEST" "lbtest.cmm" "&numTestCases"
&unitTestStartTime=OS.TIMER()
&testCaseIndex=0.
Var.WHILE (\LbTest_testCases[&testCaseIndex][0])
(
PRIVATE &testCase &statusTestCase &numTests &setupTestCase &setupTest &tearDownTest &tearDownTestCase
&testCase=Var.STRing(\LbTest_testCases[&testCaseIndex])
PUTS "analyzing test case: &testCase" "DEBUG2"
Var.NEWLOCAL char[1000.][256.] \LbTest_tests
GOSUB FindTestCaseLabels "&testCase" "\LbTest_tests"
RETURNVALUES &numTests &setupTestCase &setupTest &tearDownTest &tearDownTestCase
GOSUB RunTestCase "&testCase" "\LbTest_tests" "&numTests" "&setupTestCase" "&setupTest" "&tearDownTest" "&tearDownTestCase"
RETURNVALUES &statusTestCase
GOSUB Statistics "&statusTestCase" "&numTestCasesPass" "&numTestCasesFail" "&numTestCasesNotExec"
RETURNVALUES &numTestCasesPass &numTestCasesFail &numTestCasesNotExec
&testCaseIndex=&testCaseIndex+1.
)
&unitTestDuration=OS.TIMER()-&unitTestStartTime
GOSUB PrintSummary "UNITTEST" "lbtest.cmm" "0." "&numTestCasesPass" "&numTestCasesFail" "&numTestCasesNotExec" "&unitTestDuration"
RETURNVALUES &statusAllTests
RETURN "&statusAllTests"
)
RunTestCase:
(
PARAMETERS &testCase &hllArray &numTests &setupTestCase &setupTest &tearDownTest &tearDownTestCase
PRIVATE &testCaseLog &status &numSetupTearDownFail &numTestsPass &numTestsFail &numTestsNotExec
PRIVATE &testId &unusedMessage &testCaseStartTime &testCaseDuration
&numSetupTearDownFail=0.
&numTestsPass=0.
&numTestsFail=0.
&numTestsNotExec=0.
IF (Var.STRing(\LbTest_settings[2.])=="individual") // --logging=individual
(
; log each test case to file:
&testCaseLog=STRing.Replace("&testCase",".cmm",".log",-1.)
AREA.OPEN A000 "&testCaseLog" /Create /NoFileCache // start logging to test_*.log file
)
AREA.Select A000
AREA.view A000
GOSUB PutsStart "TESTCASE" "&testCase" "&numTests"
&testCaseStartTime=OS.TIMER()
; SetupTestCase:
GOSUB RunTestRoutine "&testCase" "&setupTestCase"
RETURNVALUES &status &testId &unusedMessage
IF ("&status"=="FAIL")
(
PUTS "tests not executed, because &testId failed" "FAIL"
&numSetupTearDownFail=&numSetupTearDownFail+1.
)
ELSE
(
PRIVATE &testIndex
&testIndex=0.
Var.WHILE (&hllArray[&testIndex][0]!=0.)
(
PRIVATE &test &statusTest
&test=Var.STRing(&hllArray[&testIndex])
; Test_*:
GOSUB RunTest "&testCase" "&setupTest" "&test" "&tearDownTest"
RETURNVALUES &statusTest
GOSUB Statistics "&statusTest" "&numTestsPass" "&numTestsFail" "&numTestsNotExec"
RETURNVALUES &numTestsPass &numTestsFail &numTestsNotExec
&testIndex=&testIndex+1.
)
)
; TearDownTestCase:
GOSUB RunTestRoutine "&testCase" "&tearDownTestCase"
RETURNVALUES &status &testId &unusedMessage
IF ("&status"=="FAIL")
(
PUTS "&testId" "FAIL"
&numSetupTearDownFail=&numSetupTearDownFail+1.
)
&testCaseDuration=OS.TIMER()-&testCaseStartTime
GOSUB PrintSummary "TESTCASE" "&testCase" "&numSetupTearDownFail" "&numTestsPass" "&numTestsFail" "&numTestsNotExec" "&testCaseDuration"
RETURNVALUES &status
IF (Var.STRing(\LbTest_settings[2.])=="individual") // --logging=individual
(
AREA.CLOSE A000 // stop logging to test_*.log
)
AREA.Select A000
RETURN "&status"
)
RunTest:
(
PARAMETERS &testCase &setupTest &test &tearDownTest
PRIVATE &statusSetupTest &statusTest &statusTearDownTest &statusRunTest
PRIVATE &testId &unusedMessage &optionalMessage &testStartTime
GOSUB GetTestId "&testCase" "&test"
RETURNVALUES &testId
GOSUB PutsStart "TEST" "&testId" ""
(
PRIVATE &routineId
&testStartTime=OS.TIMER()
GOSUB RunTestRoutine "&testCase" "&setupTest"
RETURNVALUES &statusSetupTest &routineId &unusedMessage
IF ("&statusSetupTest"=="FAIL")
(
&optionalMessage="test setup failed"
GOSUB PutsResult "INSIDETEST" "&statusSetupTest" "&routineId" "&optionalMessage (actual test &testId will not be executed)" "&testStartTime"
)
ELSE
(
; Test_*:
GOSUB RunTestRoutine "&testCase" "&test"
RETURNVALUES &statusTest &routineId &optionalMessage
)
; TearDownTest:
GOSUB RunTestRoutine "&testCase" "&tearDownTest"
RETURNVALUES &statusTearDownTest &routineId &unusedMessage
IF ("&statusTearDownTest"=="FAIL")
(
&optionalMessage="test tear-down failed"
GOSUB PutsResult "INSIDETEST" "&statusTearDownTest" "&routineId" "&optionalMessage" "&testStartTime" // TODO message?
)
)
IF (("&statusSetupTest"=="FAIL")||("&statusTest"=="FAIL")||("&statusTearDownTest"=="FAIL"))
(
&statusRunTest="FAIL"
)
ELSE IF ("&statusTest"=="NOT_EXEC")
(
; if the test was not executed, we don't care whether setup or tear-down was NOT_EXEC or PASS
&statusRunTest="NOT_EXEC"
)
ELSE IF ("&statusTest"=="PASS")
(
; here we don't care neither whether setup or tear-down was NOT_EXEC or PASS
&statusRunTest="PASS"
)
ELSE
(
; fallback
&statusRunTest="FAIL"
)
GOSUB PutsResult "TEST" "&statusRunTest" "&testId" "&optionalMessage" "&testStartTime"
RETURN "&statusRunTest"
)
RunTestRoutine:
(
PARAMETERS &testCase &routine
PRIVATE &testId
PRIVATE &statusRoutine &optionalReturnMessage
&statusRoutine="NOT_EXEC"
IF ("&routine"=="")
(
; nothing to do
)
ELSE
(
LOCAL &LbTest_assertionResult // TRUE() or FALSE()
Var.NEWLOCAL int[4.] \LbTest_assertions
; \LbTest_assertions[0.]: number of failed assertions
; \LbTest_assertions[1.]: number of unhandled errors
; \LbTest_assertions[2.]: state of assertion (sanity check for assertion-nesting)
; \LbTest_assertions[3.]: state of printed in assertion
; set up hooks to assertions:
; - generic assertions
ON CoMmanD A_TRUE GOSUB AssertTrue
ON CoMmanD A_FALSE GOSUB AssertFalse
ON CoMmanD A_EQUALS GOSUB AssertEquals
ON CoMmanD A_UNEQUAL GOSUB AssertNotEquals
; - numbers
ON CoMmanD A_NUM_EQ GOSUB AssertNumEquals
ON CoMmanD A_NUM_NE GOSUB AssertNumNotEquals
ON CoMmanD A_NUM_GT GOSUB AssertNumGreaterThan
ON CoMmanD A_NUM_GE GOSUB AssertNumGreaterThanOrEqualTo
ON CoMmanD A_NUM_LT GOSUB AssertNumLessThan
ON CoMmanD A_NUM_LE GOSUB AssertNumLessThanOrEqualto
; - strings
ON CoMmanD A_STR_EQ GOSUB AssertStringEquals
ON CoMmanD A_STR_NE GOSUB AssertStringNotEquals
ON CoMmanD A_STR_Z GOSUB AssertStringEmpty
ON CoMmanD A_STR_N GOSUB AssertStringNotEmpty
; - execution
ON CoMmanD A_X_PASS GOSUB AssertExecutePass
ON CoMmanD A_X_FAIL GOSUB AssertExecuteFail
(
PRIVATE &statusTestRoutine
GOSUB GetTestId "&testCase" "&routine"
RETURNVALUES &testId
Var.Assign \LbTest_assertions[0.]=0. // reset counter of assertion failures
Var.Assign \LbTest_assertions[1.]=0. // reset counter of unhandled errors
Var.Assign \LbTest_assertions[2.]=0. // reset state of assertion-nesting
(
; run the actual test:
PUTS "running: &testId" "DEBUG1"
ON.ERROR.GOSUB UnhandledError
DO "&testCase" &routine
RETURNVALUES &statusTestRoutine
)
&statusRoutine="FAIL"
IF (Var.VALUE(\LbTest_assertions[0.])!=0.) // if at least one assertion failed
(
PRIVATE &s
IF (Var.VALUE(\LbTest_assertions[0.])>1.)
(
&s="s"
)
&optionalReturnMessage=FORMAT.Decimal(0.,Var.VALUE(\LbTest_assertions[0.]))+" assertion&s failed"
)
ELSE IF (Var.VALUE(\LbTest_assertions[1.])!=0.) // if at least one unhandled error occurred
(
PRIVATE &s
IF (Var.VALUE(\LbTest_assertions[1.])>1.)
(
&s="s"
)
&optionalReturnMessage=FORMAT.Decimal(0.,Var.VALUE(\LbTest_assertions[1.]))+" unhandled error&s"
)
ELSE IF ("&statusTestRoutine"=="FAIL")
(
; nothing to do (&statusRoutine already set to FAIL above)
)
ELSE IF ("&statusTestRoutine"=="NOT_EXEC")
(
&statusRoutine="NOT_EXEC"
)
ELSE IF (("&statusTestRoutine"=="PASS")||("&statusTestRoutine"==""))
(
&statusRoutine="PASS"
)
ELSE
(
&optionalReturnMessage="unknown return value"
)
)
)
RETURN "&statusRoutine" "&testId" "&optionalReturnMessage"
)
Statistics:
(
PARAMETERS &status &numPass &numFail &numNotExec
IF ("&status"=="PASS")
(
&numPass=&numPass+1.
)
ELSE IF ("&status"=="NOT_EXEC")
(
&numNotExec=&numNotExec+1.
)
ELSE
(
&numFail=&numFail+1.
)
RETURN "&numPass" "&numFail" "&numNotExec"
)
PutsResult:
(
PARAMETERS &putsLevel &status &testId &message &startTime
PRIVATE &out &testSummaryLog
&out="&testId"
IF ("&message"!="")
(
&out="&out: &message"
)
IF (Var.STRing(\LbTest_settings[3.])=="on") // --printtime=on
(
PRIVATE &duration
&duration=OS.TIMER()-&startTime
GOSUB FormatDuration "&duration"
RETURNVALUES &duration
&out="&out (&duration)"
)
PUTS "&out" "&status" "&putsLevel"
&testSummaryLog=Var.STRing(\LbTest_settings[1.]) // --summarylogfile=<file>
APPEND "&testSummaryLog" %String "&status: &out"
RETURN
)
PutsStart:
(
PARAMETERS &putsLevel &module &numTests
PRIVATE &out
IF ("&putsLevel"=="TEST")
(
&out="&module"
)
ELSE
(
PRIVATE &type &s
IF ("&putsLevel"=="UNITTEST")
(
&type="test case"
)
ELSE // IF ("&putsLevel"=="TESTCASE")
(
&type="test"
)
IF (&numTests>1.)
(
&s="s"
)
&module=OS.FILE.NAME("&module")
&out="Running "+FORMAT.Decimal(0.,&numTests)+" &type&s from &module"
)
PUTS "&out" "RUN" "&putsLevel"
RETURN
)
GetTestId:
(
PARAMS &testCase &routine
PRIVATE &testId
&testId=OS.FILE.NAME(&testCase) // strip path
&testId=STRing.Replace("&testId","test_","",1.)
&testId=STRing.Replace("&testId",".cmm","",-1.) // strip extension
&routine=STRing.Replace("&routine","Test_","",1.)
IF ("&routine"!="")
(
&testId="&testId.&routine"
)
RETURN "&testId"
)
PrintSummary:
(
PARAMETERS &putsLevel &module &numSetupTearDownFail &numPass &numFail &numNotExec &duration
PRIVATE &summaryString &status
&module=OS.FILE.NAME("&module")
IF (&numSetupTearDownFail!=0.)
(
&summaryString=FORMAT.Decimal(0.,&numSetupTearDownFail)+" failures in Setup / TearDown"
&status="FAIL"
)
ELSE
(
PRIVATE &nTotal
&nTotal=&numFail+&numPass+&numNotExec
&summaryString=FORMAT.Decimal(0.,&nTotal)+" total"
&status="PASS"
IF (&numPass>0.)
(
&summaryString="&summaryString, "+FORMAT.Decimal(0.,&numPass)+" passed"
)
IF (&numFail>0.)
(
&summaryString="&summaryString, "+FORMAT.Decimal(0.,&numFail)+" failed"
&status="FAIL"
)
IF (&numNotExec>0.)
(
&summaryString="&summaryString, "+FORMAT.Decimal(0.,&numNotExec)+" not executed"
)
)
&summaryString="Summary of &module: &summaryString"
IF (Var.STRing(\LbTest_settings[3.])=="on") // --printtime=on
(
GOSUB FormatDuration "&duration"
RETURNVALUES &duration
&summaryString="&summaryString (&duration total)"
)
PUTS "&summaryString" "&status" "&putsLevel"
PRINT "" // emtpy line
RETURN "&status"
)
FormatDuration:
(
PARAMS &duration
PRIVATE &ret
IF (&duration<1000.)
(
&ret=FORMAT.Decimal(0.,&duration)+" ms"
)
ELSE
(
&ret=FORMAT.Decimal(0.,&duration/1000.)+"."+FORMAT.Decimal(0.,&duration%1000.)+" s"
)
RETURN "&ret"
)
FindTestCases:
(
PARAMETERS &hllArray
PRIVATE &pattern &file &currentDirectory &testCaseIndex
&testCaseIndex=0.
&currentDirectory=OS.PresentWorkingDirectory()
&pattern="test_*.cmm"
&file=OS.FIRSTFILE("&pattern")
WHILE ("&file"!="")
(
PRIVATE &fullFile
; force forward slashes for c-strings:
&fullFile=STRing.Replace("&currentDirectory/&file","\","/",0)
PUTS "FindTestCases: found test case: &fullFile" "DEBUG2"
Var.Assign &hllArray[&testCaseIndex]="&fullFile"
&testCaseIndex=&testCaseIndex+1.
&file=OS.NEXTFILE()
)
RETURN "&testCaseIndex"
)
; scan for all Test_*: labels
FindTestCaseLabels:
(
PARAMETERS &testCase &hllArray
PRIVATE &setupTestCase &setupTest &tearDownTest &tearDownTestCase &labelIndex
&labelIndex=0.
OPEN #101 "&testCase" /Read
RePeaT
(
PRIVATE &currentLine
READ #101 %LINE &currentLine
IF (STRing.FIND("&currentLine",":"))
(
IF (("&setupTestCase"=="")&&(STRing.SCAN("&currentLine","SetupTestCase:",0.)==0.))
(
GOSUB LineIsLabel "&currentLine"
RETURNVALUES &setupTestCase
)
ELSE IF (("&setupTest"=="")&&(STRing.SCAN("&currentLine","SetupTest:",0.)==0.))
(
GOSUB LineIsLabel "&currentLine"
RETURNVALUES &setupTest
)
ELSE IF (("&tearDownTest"=="")&&(STRing.SCAN("&currentLine","TearDownTest:",0.)==0.))
(
GOSUB LineIsLabel "&currentLine"
RETURNVALUES &tearDownTest
)
ELSE IF (("&tearDownTestCase"=="")&&(STRing.SCAN("&currentLine","TearDownTestCase:",0.)==0.))
(
GOSUB LineIsLabel "&currentLine"
RETURNVALUES &tearDownTestCase
)
ELSE IF (STRing.SCAN("&currentLine","Test_",0.)==0.)
(
PRIVATE &label
GOSUB LineIsLabel "&currentLine"
RETURNVALUES &label
IF ("&label"!="")
(
PUTS "Found test: &label" "DEBUG2"
Var.Assign &hllArray[&labelIndex]="&label"
&labelIndex=&labelIndex+1.
)
)
)
)
WHILE (!FILE.EOFLASTREAD())
CLOSE #101
RETURN "&labelIndex" "&setupTestCase" "&setupTest" "&tearDownTest" "&tearDownTestCase"
)
LineIsLabel:
(
PARAMETERS &line
&line=STRing.TRIM("&line")
IF (STRing.FIND("&line",":"))
(
; contains a colon
PRIVATE &label &optionalComment
&label=STRing.SPLIT("&line",":",0.)
&optionalComment=STRing.SPLIT("&line",":",1.)
&optionalComment=STRing.TRIM("&optionalComment")
IF (("&optionalComment"=="")||(STRing.SCAN("&optionalComment",";",0.)==0.)||(STRing.SCAN("&optionalComment","//",0.)==0.))
(
; nothing illegal in the label
IF (STRing.TOKEN(STRing.LoWeR("&label"),"abcdefghijklmnopqrstuvwxyz_0123456789",0.)=="")
(
; no illegal characters in label
; PUTS "LineIsLabel found: &label" "DEBUG2"
RETURN "&label"
)
)
)
RETURN ""
)
; PUTS: colored printing
Puts:
(
PARAMETERS &message &optionalMode &optionalLevel
&message=STRing.TRIM("&message")
&optionalMode=STRing.TRIM("&optionalMode")
PRIVATE &prefix
IF ("&optionalMode"!="")
(
PRIVATE &colorPrefix
IF ("&optionalMode"=="FAIL")
(
&colorPrefix="%COLOR.RED"
&prefix="[ FAILED ]"
)
ELSE IF ("&optionalMode"=="PASS")
(
&colorPrefix="%COLOR.GREEN"
IF ("&optionalMode"=="PASS")
(
&prefix="[ PASS ]"
)
)
ELSE IF ("&optionalMode"=="NOT_EXEC")
(
&colorPrefix="%COLOR.OLIVE"
&prefix="[ NOT_EXEC ]"
)
ELSE IF ("&optionalMode"=="RUN")
(
&colorPrefix="%COLOR.NAVY"
&prefix="[ RUN ]"
)
ELSE IF ("&optionalMode"=="DEBUG1")
(
IF ((Var.STRing(\LbTest_settings[6.])!="verbose")&&(Var.STRing(\LbTest_settings[6.])!="veryverbose"))
(
RETURN // dont' print anything
)
&colorPrefix="%COLOR.GREY"
&prefix="[ DEBUG1 ]"
)
ELSE IF ("&optionalMode"=="DEBUG2")
(
IF (Var.STRing(\LbTest_settings[6.])!="veryverbose")
(
RETURN // dont' print anything
)
&colorPrefix="%COLOR.SILVER"
&prefix="[ DEBUG2 ]"
)
IF ("&optionalLevel"!="")
(
IF ("&optionalLevel"=="UNITTEST")
(
&prefix=STRing.Replace("&prefix"," ","=",0.)
)
ELSE IF ("&optionalLevel"=="TESTCASE")
(
&prefix=STRing.Replace("&prefix"," ","-",0.)
)
ELSE IF ("&optionalLevel"=="TEST")
(
; keep &prefix as is (spaces)
)
ELSE IF ("&optionalLevel"=="INSIDETEST")
(
&prefix=STRing.LoWeR("&prefix")
)
)
&prefix="&colorPrefix ""&prefix "" %COLOR.NAVY"
)
ELSE
(
&prefix=""" """
)
PRINT &prefix "&message"
RETURN
)
; --------------------------------------------------------------------------------
; Assertions:
; A_TRUE <condition> [optional fail message]
; Assert that given <condition> is true.
AssertTrue:
(
PRIVATE &testMe &optionalFailMessage
ENTRY &testMe %LINE &optionalFailMessage
GOSUB AssertBooleanGeneric "&testMe" "Expected true, got false (&testMe)" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_FALSE <condition> [optional fail message]
; Assert that given <condition> is false.
AssertFalse:
(
PRIVATE &testMe &optionalFailMessage
ENTRY &testMe %LINE &optionalFailMessage
GOSUB AssertBooleanGeneric "!(&testMe)" "Expected false, got true (&testMe)" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_EQUALS <expected> <actual> [optional fail message]
; Assert that given arguments are equal.
; This is a generic command which also makes sure that the types of both
; arguments are equal. Use more specialized assertions if possible
; (e.g. A_NUM_EQ or A_STR_EQ).
AssertEquals:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertEqualityGeneric "&compare1" "==" "&compare2" "equal to" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_UNEQUAL <expected> <actual> [optional fail message]
; Assert that given arguments are unequal.
AssertNotEquals:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertEqualityGeneric "&compare1" "!=" "&compare2" "unequal to" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_NUM_EQ <expected> <actual> [optional fail message]
; Assert that given arguments are numerically equal. This works for
; hexadecimal, integer and binary types.
AssertNumEquals:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertNumericalGeneric "&compare1" "==" "&compare2" "numerically equal to" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_NUM_NE <expected> <actual> [optional fail message]
; Assert that given arguments are numerically not equal. This works for
; hexadecimal, integer and binary types.
AssertNumNotEquals:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertNumericalGeneric "&compare1" "!=" "&compare2" "numerically unequal to" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_NUM_GT <arg1> <arg2> [optional fail message]
; Assert that arg1 is numerically greater than arg2. This works for
; hexadecimal, integer and binary types.
AssertNumGreaterThan:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertNumericalGeneric "&compare1" ">" "&compare2" "greater than" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_NUM_GE <arg1> <arg2> [optional fail message]
; Assert that arg1 is numerically greater than or equal to arg2. This works
; for hexadecimal, integer and binary types.
AssertNumGreaterThanOrEqualTo:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertNumericalGeneric "&compare1" ">=" "&compare2" "greater than or equal to" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_NUM_LT <arg1> <arg2> [optional fail message]
; Assert that arg1 is numerically less than arg2. This works for hexadecimal,
; integer and binary types.
AssertNumLessThan:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertNumericalGeneric "&compare1" "<" "&compare2" "less than" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_NUM_LE <arg1> <arg2> [optional fail message]
; Assert that arg1 is numerically less than or equal to arg2. This works for
; hexadecimal, integer and binary types.
AssertNumLessThanOrEqualto:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertNumericalGeneric "&compare1" "<=" "&compare2" "less than or equal to" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_STR_EQ <expected> <actual> [optional fail message]
; Assert that given argument strings are equal. This works only for string
; types.
AssertStringEquals:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertStringGeneric "&compare1" "==" "&compare2" "notEmpty" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_STR_NE <expected> <actual> [optional fail message]
; Assert that given argument strings are not equal. This works only for
; string types.
AssertStringNotEquals:
(
PRIVATE &compare1 &compare2 &optionalFailMessage
ENTRY &compare1 &compare2 %LINE &optionalFailMessage
GOSUB AssertStringGeneric "&compare1" "!=" "&compare2" "notEmpty" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_STR_Z <arg> [optional fail message]
; Assert that given argument string is empty.
AssertStringEmpty:
(
PRIVATE &testMe &optionalFailMessage
ENTRY &testMe %LINE &optionalFailMessage
; """""" is actually an empty string "" in quotes with escaped quote signs
GOSUB AssertStringGeneric "&testMe" "==" """""" "empty" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_STR_N <arg> [optional fail message]
; Assert that given argument string is not empty.
AssertStringNotEmpty:
(
PRIVATE &testMe &optionalFailMessage
ENTRY &testMe %LINE &optionalFailMessage
; """""" is actually an empty string "" in quotes with escaped quote signs
GOSUB AssertStringGeneric "&testMe" "!=" """""" "empty" "&optionalFailMessage" "1."
RETURN &LbTest_assertionResult
)
; A_X_PASS <command> [args] //no optional fail message
; Assert that execution of given command raises no error.
AssertExecutePass:
(
PRIVATE &execMe
ENTRY %LINE &execMe
GOSUB AssertExecuteGeneric "&execMe" "pass" "1."
RETURN &LbTest_assertionResult
)
; A_X_FAIL <command> [args] //no optional fail message
; Assert that execution of given command raises some error.
AssertExecuteFail:
(
PRIVATE &execMe
ENTRY %LINE &execMe
GOSUB AssertExecuteGeneric "&execMe" "fail" "1."
RETURN &LbTest_assertionResult
)
; --------------------------------------------------------------------------------
; Helper functions for assertions:
InitializeAssertion:
(
PARAMETERS &callerNesting
&LbTest_assertionResult=FALSE() // be pessimistic and assume the assertion will fail
Var.Assign \LbTest_assertions[2.]++ // increase counter for sanity check for assertion-nesting
IF (Var.VALUE(\LbTest_assertions[2.])!=1.)
(
&callerNesting=&callerNesting+1.
GOSUB FailAssertion "Wrong usage: Assertions can not be nested" "&callerNesting"
)
Var.Assign \LbTest_assertions[3.]=0. // no failure was printed yet
RETURN
)
FinalizeAssertion:
(
PARAMETERS &callerNesting
&callerNesting=&callerNesting+1.
Var.Assign \LbTest_assertions[2.]--
IF (Var.VALUE(\LbTest_assertions[2.])!=0.)
(
GOSUB FailAssertion "Wrong usage: Assertions can not be nested" "&callerNesting"
)
IF ((!&LbTest_assertionResult)&&(Var.VALUE(\LbTest_assertions[3.])!=1.))
(
; the assertion failed, but we forgot to print an error
; (framework issue here in lbtest.cmm)
GOSUB FailAssertion "unknown error in assertion" "&callerNesting"
)
RETURN
)
; This function may be used only from inside official assertions
CallOtherGenericAssertion:
(
PARAMETERS &genericAssertion &arg1 &arg2 &arg3 &arg4 &arg5 &arg6level
&arg6level=&arg6level+1.
; pre-decrement and post-increment \LbTest_assertions[2.] to
; avoid "Wrong usage: Assertions can not be nested" error
Var.Assign \LbTest_assertions[2.]--
GOSUB &genericAssertion "&arg1" "&arg2" "&arg3" "&arg4" "&arg5" "&arg6level"
Var.Assign \LbTest_assertions[2.]++
RETURN
)
AssertBooleanGeneric:
(
PARAMETERS &passCondition &defaultFailMessage &optionalFailMessage &callerNesting
PRIVATE &type1
&callerNesting=&callerNesting+1.
GOSUB InitializeAssertion "&callerNesting"
GOSUB EnsureType "&passCondition" "boolean" "&callerNesting"
RETURNVALUES &type1
IF (&type1!=0)
(
; this is the actual check:
IF (&passCondition)
(
&LbTest_assertionResult=TRUE()
)
ELSE
(
IF ("&optionalFailMessage"=="")
(
&optionalFailMessage="&defaultFailMessage"
)
GOSUB FailAssertion "&optionalFailMessage" "&callerNesting"
)
)
GOSUB FinalizeAssertion "&callerNesting"
RETURN
)
AssertNumericalGeneric:
(
PARAMETERS &compare1 &operator &compare2 &defaultFailMessagePart &optionalFailMessage &callerNesting
PRIVATE &type1 &type2
&callerNesting=&callerNesting+1.
GOSUB InitializeAssertion "&callerNesting"
GOSUB EnsureType "&compare1" "intnum" "&callerNesting"
RETURNVALUES &type1
GOSUB EnsureType "&compare2" "intnum" "&callerNesting"
RETURNVALUES &type2
IF ((&type1!=0.)&&(&type2!=0.))
(
; this is the actual check:
IF (&(compare1)&(operator)&(compare2))
(
&LbTest_assertionResult=TRUE()
)
ELSE
(
IF ("&optionalFailMessage"=="")
(
PRIVATE &message &compare1Expanded &compare2Expanded
&message="Expected &compare1"
&compare1Expanded=&compare1
IF ("&compare1Expanded"!="&compare1")
(
&message="&message (&compare1Expanded)"
)
&message="&message to be &defaultFailMessagePart &compare2"
&compare2Expanded=&compare2
IF ("&compare2Expanded"!="&compare2")
(
&message="&message (&compare2Expanded)"
)
&optionalFailMessage="&message"
)
GOSUB FailAssertion "&optionalFailMessage" "&callerNesting"
)
)
GOSUB FinalizeAssertion "&callerNesting"
RETURN
)
AssertStringGeneric:
(
PARAMETERS &compare1 &operator &compare2 &empty &optionalFailMessage &callerNesting
PRIVATE &type1 &type2
&callerNesting=&callerNesting+1.
GOSUB InitializeAssertion "&callerNesting"
GOSUB EnsureType "&compare1" "string" "&callerNesting"
RETURNVALUES &type1
GOSUB EnsureType "&compare2" "string" "&callerNesting"
RETURNVALUES &type2
IF ((&type1!=0.)&&(&type2!=0.))
(
; this is the actual check:
IF (&(compare1)&(operator)&(compare2))
(
&LbTest_assertionResult=TRUE()
)
ELSE
(
IF ("&optionalFailMessage"=="")
(
IF ("&operator"=="==")
(
IF ("&empty"=="empty")
(
&optionalFailMessage="Expected empty string, but it contains '"+&compare1+"'"
)
ELSE
(
&optionalFailMessage="Expected string equality, but '"+&compare1+"'!='"+&compare2"'"
)
)
ELSE // IF ("&operator"=="!=")
(
IF ("&empty"=="empty")
(
&optionalFailMessage="Expected content in string, but it is emtpy"
)
ELSE
(
&optionalFailMessage="Expected string inequality, but '"+&compare1+"'=='"+&compare2"'"
)
)
)
GOSUB FailAssertion "&optionalFailMessage" "&callerNesting"
)
)
GOSUB FinalizeAssertion "&callerNesting"
RETURN
)
AssertEqualityGeneric:
(
PARAMETERS &compare1 &operator &compare2 &defaultFailMessagePart &optionalFailMessage &callerNesting
PRIVATE &type1 &type2
&callerNesting=&callerNesting+1.
GOSUB InitializeAssertion "&callerNesting"
GOSUB EnsureType "&compare1" "any" "&callerNesting"
RETURNVALUES &type1
GOSUB EnsureType "&compare2" "any" "&callerNesting"
RETURNVALUES &type2
IF ((&type1!=0.)&&(&type2!=0.))
(
; preconditions:
IF (&type1!=&type2)
(
GOSUB FailAssertion "Wrong usage: Given arguments (&compare1,&compare2) are of different types (&type1,&type2)" "&callerNesting"
)
ELSE IF (((&type1)&((0x8000)))!=0x0) //empty/no expression parameter
(
GOSUB FailAssertion "Wrong usage: Both arguments are empty" "&callerNesting"
)
ELSE IF (((&type1)&((0x4000)))!=0x0) //bitmask
(
GOSUB FailAssertion "Wrong usage: Unable to compare bitmasks" "&callerNesting"
)
ELSE
(
; redirect to more specialized assertions if possible:
IF (((&type1)&((0x000e)))!=0x0) //hex, integer or binary
(
IF ("&operator"=="==")
(
GOSUB CallOtherGenericAssertion "AssertNumericalGeneric" "&compare1" "==" "&compare2" "numerically equal to" "&optionalFailMessage" "&callerNesting"
)
ELSE
(
GOSUB CallOtherGenericAssertion "AssertNumericalGeneric" "&compare1" "!=" "&compare2" "numerically unequal to" "&optionalFailMessage" "&callerNesting"
)
)
ELSE IF (((&type1)&((0x0040)))!=0x0) //string
(
IF ("&operator"=="==")
(
GOSUB CallOtherGenericAssertion "AssertStringGeneric" "&compare1" "==" "&compare2" "notEmpty" "&optionalFailMessage" "&callerNesting"
)
ELSE
(
GOSUB CallOtherGenericAssertion "AssertStringGeneric" "&compare1" "!=" "&compare2" "notEmpty" "&optionalFailMessage" "&callerNesting"
)
)
ELSE
(
; this is the actual check:
IF (&(compare1)&(operator)&(compare2))
(
&LbTest_assertionResult=TRUE()
)
ELSE
(
IF ("&optionalFailMessage"=="")
(
PRIVATE &message &compare1Expanded &compare2Expanded
&message="Expected &compare1"
&compare1Expanded=&compare1
IF ("&compare1Expanded"!="&compare1")
(
&message="&message (&compare1Expanded)"
)
&message="&message to be &defaultFailMessagePart &compare2"
&compare2Expanded=&compare2
IF ("&compare2Expanded"!="&compare2")
(
&message="&message (&compare2Expanded)"
)
&optionalFailMessage="&message"
)
GOSUB FailAssertion "&optionalFailMessage" "&callerNesting"
)
)
)
)
GOSUB FinalizeAssertion "&callerNesting"
RETURN
)
AssertExecuteGeneric:
(
PARAMETERS &execMe &expect &callerNesting
&callerNesting=&callerNesting+1.
LOCAL &gotError
&gotError=FALSE()
ON.ERROR.GOSUB // install error handler
(
&gotError=TRUE()
RETURN
)
GOSUB InitializeAssertion "&callerNesting"
; this is the actual check (execute command):
&execMe
IF ("&expect"=="pass")
(
IF (&gotError)
(
GOSUB FailAssertion "Expected proper execution, but errors occurred executing '&execMe'" "&callerNesting"
)
ELSE
(
&LbTest_assertionResult=TRUE()
)
)
ELSE
(
IF (!&gotError)
(
GOSUB FailAssertion "Expected errors, but no errors occurred executing '&execMe'" "&callerNesting"
)
ELSE
(
&LbTest_assertionResult=TRUE()
)
)
GOSUB FinalizeAssertion "&callerNesting"
RETURN
)
TryEval:
(
LOCAL &gotError
&gotError=FALSE()
ON ERROR GOSUB
(
&gotError=TRUE()
RETURN
)
PARAMETERS &evalme
Eval &evalme
PRIVATE &evaltype
&evaltype=EVAL.TYPE()
IF (!&gotError)
(
RETURN "&evaltype"
)
PUTS "TryEval failed" "DEBUG1"
RETURN "0."
)
EnsureType:
(
PARAMETERS &checkVar &expectedType &callerNesting
PRIVATE &expectedMask &actualType
&callerNesting=&callerNesting+1.
IF ("&expectedType"=="boolean")
(
&expectedMask=0x0001
)
ELSE IF ("&expectedType"=="intnum")
(
&expectedMask=0x000e
)
ELSE IF ("&expectedType"=="string")
(
&expectedMask=0x0040
)
ELSE IF ("&expectedType"=="any")
(
&expectedMask=0xffff
)
ELSE
(
; PRINT "Values Expression Types"
; PRINT "0x0001 boolean"
; PRINT "0x0002 binary"
; PRINT "0x0004 hex"
; PRINT "0x0008 integer"
; PRINT "0x0010 float"
; PRINT "0x0020 ASCII constant"
; PRINT "0x0040 string"
; PRINT "0x0080 numeric range"
; PRINT "0x0100 address"
; PRINT "0x0200 address range"
; PRINT "0x0400 time"
; PRINT "0x0800 time range"
; PRINT "0x4000 bitmask"
; PRINT "0x8000 empty/no expression parameter"
&expectedMask=0x0
)
GOSUB TryEval "&checkVar"
RETURNVALUES &actualType
IF (&expectedMask==0x0)
(
GOSUB FailAssertion "Wrong usage: Unknown type '&expectedType'" "&callerNesting"
&actualType=0.
)
ELSE IF (((&actualType)&((0xffff)))==0x0)
(
GOSUB FailAssertion "Wrong usage: Can not determine type of argument (&checkVar)" "&callerNesting"
&actualType=0.
)
ELSE IF (((&actualType)&(&expectedMask))==0x0)
(
IF (&expectedMask==0x0001)
(
GOSUB FailAssertion "Wrong usage: type (&actualType) of given argument (&checkVar) is not boolean" "&callerNesting"
)
ELSE IF (&expectedMask==0x000e)
(
GOSUB FailAssertion "Wrong usage: type (&actualType) of given argument (&checkVar) is not numerical (hex/int/binary)" "&callerNesting"
)
ELSE IF (&expectedMask==0x0040)
(
GOSUB FailAssertion "Wrong usage: type (&actualType) of given argument (&checkVar) is not a string" "&callerNesting"
)
ELSE
(
GOSUB FailAssertion "Wrong usage: type (&actualType) of given argument (&checkVar) does not match mask (&expectedMask) as expected" "&callerNesting"
)
&actualType=0.
)
RETURN "&actualType"
)
UnhandledError:
(
PRIVATE &errorId &callerFile &callerLine
&errorId=ERROR.ID()
&callerFile=PRACTICE.CALLER.FILE(1.)
&callerLine=PRACTICE.CALLER.LINE(1.)
IF (Var.STRing(\LbTest_settings[4.])=="unhandlederror") // --debugmode=unhandlederror
(
PLIST
PBREAK.Set &callerLine+1. "&callerFile" /TeMPorary
)
&callerFile=OS.FILE.NAME("&callerFile")
&callerLine=FORMAT.Decimal(0.,&callerLine)
PUTS "&callerFile:&callerLine: unhandled error (&errorId)" "FAIL" "INSIDETEST"
Var.Assign \LbTest_assertions[1.]++
RETURN
)
; Fail an assertion
; This function is called by every assertion (A_*)
FailAssertion:
(
PARAMETERS &failMessage &callerNesting
&callerNesting=&callerNesting+1.
&LbTest_assertionResult=FALSE() // marke assertion as failed
Var.Assign \LbTest_assertions[0.]++ //increase assertion failure counter
PRIVATE &scriptFile &assertionLine
&scriptFile=PRACTICE.CALLER.FILE(&callerNesting)
&assertionLine=PRACTICE.CALLER.LINE(&callerNesting)
IF (Var.STRing(\LbTest_settings[4.])=="failedassertion") // --debugmode=failedassertion
(
PLIST
PBREAK.Set &assertionLine+1. "&scriptFile" /TeMPorary
)
&scriptFile=OS.FILE.NAME("&scriptFile")
&assertionLine=FORMAT.Decimal(0.,&assertionLine)
&failMessage="&scriptFile:&assertionLine: &failMessage"
PUTS "&failMessage" "FAIL" "INSIDETEST"
Var.Assign \LbTest_assertions[3.]=1. // assertion failure was printed
RETURN
)