; -------------------------------------------------------------------------------- ; @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= 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= write log output to (default: lbtest.log)" PRINT " only applicable if --logging=allinone" PRINT " --summarylogfile= write summary log output to " PRINT " (default: test_summary.log)" PRINT " --logging= write log output to one file (default) or" PRINT " to individual files for each test case" PRINT " --printtime= print elapsed time of each test (default: on)" PRINT " --debugmode= stop script for debugging, available s:" PRINT " failedassertion: stop after a failed assertion" PRINT " unhandlederror: stop after an unhandled error" PRINT " --logcommands= 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 ¶m &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" ¶m=STRing.SCANAndExtract("&options","--logfile=","&workingdir/lbtest.log") ¶m=STRing.Replace("¶m","\","/",0) Var.Assign \LbTest_settings[0.]="¶m" ¶m=STRing.SCANAndExtract("&options","--summarylogfile=","&workingdir/lbtest_summary.log") ¶m=STRing.Replace("¶m","\","/",0) Var.Assign \LbTest_settings[1.]="¶m" ¶m=STRing.SCANAndExtract("&options","--logging=","allinone") Var.Assign \LbTest_settings[2.]="¶m" ¶m=STRing.SCANAndExtract("&options","--printtime=","on") Var.Assign \LbTest_settings[3.]="¶m" ¶m=STRing.SCANAndExtract("&options","--debugmode=","") Var.Assign \LbTest_settings[4.]="¶m" ¶m=STRing.SCANAndExtract("&options","--logcommands=","off") Var.Assign \LbTest_settings[5.]="¶m" 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= AREA.OPEN A000 "&logfile" /Create /NoFileCache // start logging ) AREA.Select A000 AREA.view A000 &testSummaryLog=Var.STRing(\LbTest_settings[1.]) // --summarylogfile= 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= 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= 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 ¤tDirectory &testCaseIndex &testCaseIndex=0. ¤tDirectory=OS.PresentWorkingDirectory() &pattern="test_*.cmm" &file=OS.FIRSTFILE("&pattern") WHILE ("&file"!="") ( PRIVATE &fullFile ; force forward slashes for c-strings: &fullFile=STRing.Replace("¤tDirectory/&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 ¤tLine READ #101 %LINE ¤tLine IF (STRing.FIND("¤tLine",":")) ( IF (("&setupTestCase"=="")&&(STRing.SCAN("¤tLine","SetupTestCase:",0.)==0.)) ( GOSUB LineIsLabel "¤tLine" RETURNVALUES &setupTestCase ) ELSE IF (("&setupTest"=="")&&(STRing.SCAN("¤tLine","SetupTest:",0.)==0.)) ( GOSUB LineIsLabel "¤tLine" RETURNVALUES &setupTest ) ELSE IF (("&tearDownTest"=="")&&(STRing.SCAN("¤tLine","TearDownTest:",0.)==0.)) ( GOSUB LineIsLabel "¤tLine" RETURNVALUES &tearDownTest ) ELSE IF (("&tearDownTestCase"=="")&&(STRing.SCAN("¤tLine","TearDownTestCase:",0.)==0.)) ( GOSUB LineIsLabel "¤tLine" RETURNVALUES &tearDownTestCase ) ELSE IF (STRing.SCAN("¤tLine","Test_",0.)==0.) ( PRIVATE &label GOSUB LineIsLabel "¤tLine" 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 [optional fail message] ; Assert that given 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 [optional fail message] ; Assert that given 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 [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 [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 [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 [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 [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 [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 [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 [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 [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 [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 [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 [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 [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 [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 )