2020-04-27 12:01:47 +02:00
' Gambas module file
2019-11-15 22:33:54 +01:00
Export
2020-05-25 21:24:28 +02:00
Class __Test
2020-06-05 10:56:37 +02:00
''' The class Test is the central class which orchestrates
''' the execution of tests and also gives a couple of tools to
''' manipulate running tests.
2020-05-07 12:57:55 +02:00
'' Special value for unspecified test plan
Public Const NO_PLAN As Integer = -1
Property Read _Printer As TapPrinter
2020-05-02 18:35:48 +02:00
Property Read _Finished As Boolean
2020-04-27 12:01:47 +02:00
Property _Next As TestAssertion
2020-06-06 12:08:14 +02:00
Public _InSetup As Boolean
2020-04-27 12:01:47 +02:00
Private $hPrinter As New TapPrinter
Private $hNext As TestAssertion
2020-05-23 08:10:33 +02:00
Private $Suite As TestSuite
2020-05-26 20:07:58 +02:00
'' Used to trigger Test.Main to print all tests as comma separated string.
Public Const _TRIGGER_GET_ALLTESTS As String = "#GetAllTests"
2020-06-05 10:56:37 +02:00
'' The static procedure Test.Main() runs tests. By default it runs all tests (aka testmethods) in all
'' testmodules of a project ordered by name and prints the result to stdout.
''
'' A testmodule is a special class with the ending test. It must contain at minimum one test.
2020-05-23 12:09:34 +02:00
''
2020-06-05 10:56:37 +02:00
'' A test is a public sub in a testmodule whose name does not contain an underscore.
''
'' On the command line the interpreter is commanded with this line to execute Test.Main():
''
'' gbx3 -T "*" /path/to/project
2020-05-23 12:09:34 +02:00
''
2020-06-05 10:56:37 +02:00
'' The optional argument ca be used to create a testsuite which chooses any
'' combination of testmodule and test(s). The format of the string is like so: Testmodules
'' are separated by a comma. If not all tests of a testmodule should be called then the
'' name of the testmodule is followed by a dot and a string containing the tests separated
'' by a semikolon. For example:
''
'' gbx3 -T "testmodule1,testmodule2.test1;test2,testmodule3" /path/to/project
2020-05-23 12:09:34 +02:00
''
2020-06-05 10:56:37 +02:00
'' In the Gambas IDE one is able to define testsuites and store them with a individual name
'' in a file named ".test" in the project's path.
''
'' A test suite stored like that can be called by it's name with a preceeding "@", for example:
''
2020-07-09 13:25:24 +02:00
'' gbx3 -T "@My production testsuite1" /path/to/project
2020-05-23 12:09:34 +02:00
''
2020-06-05 10:56:37 +02:00
'' Public methods called "_Setup", "_Teardown", "_SetupEach" or "_TeardownEach" inside a testmodule
'' can be used to create a testfixture, which defines a special environment for the tests.
2020-05-23 12:09:34 +02:00
Public Sub Main(Optional Tests As String)
2020-05-27 14:27:38 +02:00
Dim sTestsuite As String
2020-05-25 21:24:28 +02:00
If Not Tests Then
2020-05-26 20:07:58 +02:00
'prints the names of all testmodules
PrintAllTestModules()
2020-05-25 21:24:28 +02:00
Return
Endif
2020-05-26 20:07:58 +02:00
If Tests = _TRIGGER_GET_ALLTESTS Then
Print AllTests()
Return
Endif
2020-05-27 14:27:38 +02:00
2020-05-23 12:09:34 +02:00
If Tests = "*" Then
'triggers all tests
2020-05-25 21:24:28 +02:00
Tests = ""
2020-05-23 12:09:34 +02:00
Endif
2020-05-27 14:27:38 +02:00
If Tests Begins "@" Then
'a test suite was called by name
sTestsuite = Tests
2020-05-31 00:58:50 +02:00
Tests = Helper.GetTestSuiteByName(Tests)
2020-05-27 14:27:38 +02:00
Endif
' run tests
2020-05-23 12:09:34 +02:00
Test._Reset() ' only if you run this Main multiple times per process, which you shouldn't
If Tests Then
FromString(Tests)
Endif
RunTests()
2020-05-27 14:27:38 +02:00
If sTestsuite Then
sTestsuite = Replace(sTestsuite, "@", "Testsuite: ")
$hPrinter.Session.Summary.Description = sTestsuite
Else
$hPrinter.Session.Summary.Description = TestCommand.ToString(TestCommand.FromString(Tests))
Endif
2020-05-23 12:09:34 +02:00
PrintSummary()
Quit ExitCode()
TheEnd:
2020-05-27 14:27:38 +02:00
Catch
Test.BailOut(Error.Text)
2020-05-23 12:09:34 +02:00
End
2020-02-24 17:59:14 +01:00
Private Sub PrintSummary()
2020-05-07 12:57:55 +02:00
With $hPrinter.Session
2020-05-26 20:45:18 +02:00
Test._Print(Null) ' better readability for humans
2020-05-27 14:27:38 +02:00
2020-05-23 12:09:34 +02:00
' even if the tests came in unsorted, print it sorted
2020-05-27 14:27:38 +02:00
Test.Note(Subst$(("Ran '&1' "), TestCommand.ToString(TestCommand.FromString(.Summary.Description))))
2020-04-27 12:01:47 +02:00
If .TestsRun <> .Plan Then Test.Note(Subst$(("Planned &1 tests but ran &2"), .Plan, .TestsRun))
2020-05-01 15:57:53 +02:00
Test.Note(gb.Lf)
2020-05-01 18:59:20 +02:00
ShowTestCollection(("&1 skipped:"), FindSkips(.Summary.Subtests, ""))
ShowTestCollection(("&1 todo:"), FindTodos(.Summary.Subtests, ""))
ShowTestCollection(("&1 bonus:"), FindBonus(.Summary.Subtests, ""))
2020-05-07 12:57:55 +02:00
2020-06-07 00:37:59 +02:00
If Not .Summary.Success Then
ShowTestCollection(("&1 test plans failed:"), FindBadPlans(.Summary.Subtests, ""))
ShowTestCollection(("&1 tests failed:"), FindFailures(.Summary.Subtests, ""))
Endif
2020-05-01 18:59:20 +02:00
Test.Note(IIf(.Summary.Success, "PASSED", "FAILED"))
2020-02-24 17:59:14 +01:00
End With
2019-11-15 22:33:54 +01:00
End
2020-05-07 13:01:31 +02:00
Private Function ExitCode() As Integer
Return IIf($hPrinter.Session.Summary.Success, 0, 1)
End
2020-05-01 15:57:53 +02:00
'' Prints a Collection[] of tests as returned by FindFailures, FindSkips, FindTodos.
'' _Description_ can contain '&1' which is substituted for _TestCollection_.Count.
Private Sub ShowTestCollection(Description As String, TestCollection As Collection[])
Dim cTest As Collection
If TestCollection.Count Then
Test.Note(Subst$(Description, TestCollection.Count))
For Each cTest In TestCollection
Dim hTest As TestAssertion = cTest!Assertion
2020-06-07 00:37:59 +02:00
Dim sName, sNote As String
sName = hTest.Description
If cTest["Note"] Then
sNote = Subst$((": &1"), cTest["Note"])
Else If hTest.Comment Then
sNote = Subst$(("# &1"), hTest.Comment)
Endif
Test.Note(Subst$(("&2: &1 -- &3&4"), cTest["Path"], hTest.Id, sName, IIf(sNote, " " & sNote, "")))
2020-05-01 15:57:53 +02:00
Next
Test.Note(gb.Lf)
Endif
End
2020-06-07 00:37:59 +02:00
Private Function FindBadPlans(Tests As TestAssertion[], Prefix As String) As Collection[]
Dim hTest As TestAssertion
Dim sName As String
Dim aRet As New Collection[]
For Each hTest In Tests
sName = Prefix &/ hTest.Description
If hTest.Subtests.Count <> hTest.SubPlanned Then aRet.Add(["Path": Prefix, "Assertion": hTest, "Note": Subst$(("Planned &1 but ran &2"), hTest.SubPlanned, hTest.Subtests.Count)])
aRet.Insert(FindBadPlans(hTest.Subtests, sName))
Next
Return aRet
End
2020-05-01 15:57:53 +02:00
Private Function FindFailures(Tests As TestAssertion[], Prefix As String) As Collection[]
2020-04-25 05:22:10 +02:00
Dim hTest As TestAssertion
Dim sName As String
2020-05-01 15:57:53 +02:00
Dim aRet As New Collection[]
2020-04-25 05:22:10 +02:00
For Each hTest In Tests
sName = Prefix &/ hTest.Description
' Only show the deepest subtests that caused failures.
2020-05-01 15:57:53 +02:00
If Not hTest.Success And If Not hTest.Subtests.Count Then aRet.Add(["Path": Prefix, "Assertion": hTest])
aRet.Insert(FindFailures(hTest.Subtests, sName))
Next
Return aRet
End
Private Function FindSkips(Tests As TestAssertion[], Prefix As String) As Collection[]
Dim hTest As TestAssertion
Dim sName As String
Dim aRet As New Collection[]
For Each hTest In Tests
sName = Prefix &/ hTest.Description
2020-05-07 12:57:55 +02:00
If hTest.Directive = TestAssertion.SKIP Then aRet.Add(["Path": Prefix, "Assertion": hTest])
2020-05-01 15:57:53 +02:00
aRet.Insert(FindSkips(hTest.Subtests, sName))
Next
Return aRet
End
Private Function FindTodos(Tests As TestAssertion[], Prefix As String) As Collection[]
Dim hTest As TestAssertion
Dim sName As String
Dim aRet As New Collection[]
For Each hTest In Tests
sName = Prefix &/ hTest.Description
2020-05-07 12:57:55 +02:00
If hTest.Directive = TestAssertion.TODO And If Not hTest.Ok Then aRet.Add(["Path": Prefix, "Assertion": hTest])
2020-05-01 15:57:53 +02:00
aRet.Insert(FindTodos(hTest.Subtests, sName))
Next
Return aRet
End
Private Function FindBonus(Tests As TestAssertion[], Prefix As String) As Collection[]
Dim hTest As TestAssertion
Dim sName As String
Dim aRet As New Collection[]
For Each hTest In Tests
sName = Prefix &/ hTest.Description
2020-05-07 12:57:55 +02:00
If hTest.Directive = TestAssertion.TODO And If hTest.Ok Then aRet.Add(["Path": Prefix, "Assertion": hTest])
2020-05-01 15:57:53 +02:00
aRet.Insert(FindBonus(hTest.Subtests, sName))
2020-04-25 05:22:10 +02:00
Next
2020-05-01 15:57:53 +02:00
Return aRet
2020-04-25 05:22:10 +02:00
End
2020-05-23 12:09:34 +02:00
'' Run all tests in $Suite. If $Suite is Null, run all tests.
2019-11-15 22:33:54 +01:00
2020-05-23 08:10:33 +02:00
Private Function RunTests()
2019-11-15 22:33:54 +01:00
2020-05-23 08:10:33 +02:00
Dim Testmodule As Class
2019-11-15 22:33:54 +01:00
2020-05-23 08:10:33 +02:00
If Not $Suite Then
2020-05-23 12:09:34 +02:00
' create as Suite with all tests
2020-05-23 08:10:33 +02:00
$Suite = New TestSuite
2020-05-26 12:50:39 +02:00
For Each TestModule In GetAllTestModules()
2020-05-23 08:10:33 +02:00
$Suite.AddAllTestCases(TestModule)
Next
Endif
2019-11-15 22:33:54 +01:00
2020-05-23 08:10:33 +02:00
$Suite.Run()
2020-05-02 18:35:48 +02:00
If Not Test._Finished Then Test._Finish()
2020-02-27 20:28:49 +01:00
2019-11-15 22:33:54 +01:00
End
2020-05-26 12:50:39 +02:00
Private Function GetAllTestModules() As Class[]
2019-11-15 22:33:54 +01:00
Dim TestClass As Class
2020-05-26 12:50:39 +02:00
Dim TestModules As New Class[]
2020-05-31 00:58:50 +02:00
Dim aNames As New String[]
2019-11-15 22:33:54 +01:00
Dim sName As String
2020-05-23 12:22:12 +02:00
Component.Load("gb.util")
2020-05-23 08:10:33 +02:00
For Each sName In Dir(".../.gambas")
2020-05-31 00:58:50 +02:00
sName = Helper.CheckTestModule(sName)
If sName Then aNames.Add(sName)
2020-05-23 08:10:33 +02:00
Next
2019-11-15 22:33:54 +01:00
2020-05-31 00:58:50 +02:00
Assert aNames
2019-11-15 22:33:54 +01:00
2020-05-31 00:58:50 +02:00
aNames.Sort
2020-04-25 11:38:52 +02:00
2020-05-31 00:58:50 +02:00
For Each sName In aNames
2020-05-25 21:24:28 +02:00
TestClass = __Test.Load(sName)
2020-04-07 15:03:13 +02:00
If Not TestClass Then Error.Raise(Subst$(("Could not load test module '&1'"), sName))
2020-05-26 12:50:39 +02:00
TestModules.Add(TestClass)
2020-04-07 15:03:13 +02:00
Next
2019-11-15 22:33:54 +01:00
2020-05-26 12:50:39 +02:00
Return TestModules
2019-11-15 22:33:54 +01:00
2020-01-04 14:48:51 +01:00
Catch
2020-04-27 12:01:47 +02:00
Test.BailOut("Error in " & Error.Where & ": " & Error.Text)
End
2020-06-05 10:56:37 +02:00
'' Returns an collection of all the testmodules and their tests of the project.
'' Key is the name of the testmodule, Value is a string array with the names of the tests it contains
2020-05-13 11:14:19 +02:00
Public Function AllTestsCollection() As Collection
2020-05-26 12:50:39 +02:00
Dim aoTestModules As Class[]
2020-05-14 13:08:03 +02:00
Dim cAlltests As New Collection
2020-05-26 12:50:39 +02:00
Dim oTestModule As Class
2020-05-11 23:28:42 +02:00
2020-05-26 12:50:39 +02:00
aoTestModules = GetAllTestModules()
2020-05-11 23:28:42 +02:00
2020-05-26 12:50:39 +02:00
For Each oTestModule In aoTestModules
cAlltests.Add(TestSuite.GetTestsFromTestModule(oTestModule), oTestModule.Name)
2020-05-13 11:14:19 +02:00
Next
2020-05-11 22:26:08 +02:00
2020-05-14 13:08:03 +02:00
Return cAlltests
2020-05-11 22:26:08 +02:00
End
2020-06-05 10:56:37 +02:00
'' Returns a comma separated string with all tests of the project in the same way Test.Main() wants it.
2020-05-13 11:14:19 +02:00
2020-05-14 13:08:03 +02:00
Public Function AllTests() As String
2020-05-13 11:14:19 +02:00
Dim tests As Collection
Dim asTests As String[]
Dim asLines As New String[]
2020-05-25 21:24:28 +02:00
Dim sModule As String
2020-05-13 11:14:19 +02:00
tests = AllTestsCollection()
For Each tests
2020-05-14 13:08:03 +02:00
sModule = tests.Key
2020-05-13 11:14:19 +02:00
asTests = tests[tests.Key]
2020-05-23 12:09:34 +02:00
asTests.Sort
If asTests.Count > 0 Then
asLines.Add(sModule & "." & asTests.Join(";"))
Endif
2020-05-13 11:14:19 +02:00
Next
2020-05-14 13:08:03 +02:00
asLines.Sort()
Return asLines.Join(",")
2020-05-13 11:14:19 +02:00
End
2020-04-27 12:01:47 +02:00
' ------------------------------------------------- Test controls
2020-06-05 10:56:37 +02:00
'' Prints "Bail out!" to Stdout and stops all testing immediately.
2020-04-27 12:01:47 +02:00
Public Sub BailOut(Optional Comment As String)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
$hPrinter.BailOut(Comment)
2020-02-27 20:28:49 +01:00
Quit 1
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-13 11:14:19 +02:00
2020-06-05 10:56:37 +02:00
'' Synonym for Note, prints Comment with leading #
2020-04-27 12:01:47 +02:00
Public Sub Diagnostic(Comment As String)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
$hPrinter.Diagnostic(Comment)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-28 11:14:58 +02:00
'' If the next assertion is a failure it reports ok.
'' In other words: Reverses the result of the following assertion.
'' "not ok" will be "ok" and vice versa.
Public Sub IntendedFailure()
Assert._IntendedFailure = True
End
2020-05-02 18:35:48 +02:00
'' Synonym for Diagnostic, prints Comment with leading #
2020-04-27 12:01:47 +02:00
Public Sub Note(Comment As String)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
$hPrinter.Note(Comment)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-28 09:30:15 +02:00
2020-06-05 10:56:37 +02:00
'' Tell the testing system that the next assertion is a TODO.
'' Even if it fails it reports ok, but will be reported as Todo.
'' If the next assertion does not fail, it will be reported as bonus.
2020-05-28 09:30:15 +02:00
Public Sub Todo(Optional Comment As String)
Test._Next.Directive = TestAssertion.TODO
Test._Next.Comment = Comment
End
'' Tell the testing system that a test is skipped. Reports ok.
Public Sub Skip(Optional Comment As String)
Test._Next.Directive = TestAssertion.SKIP
Test._Next.Comment = Comment
Assert.Pass()
End
2020-05-02 18:35:48 +02:00
'' Prints Line to Stdout
2020-05-07 12:57:55 +02:00
Public Sub _Print({Line} As String)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
$hPrinter.Print({Line})
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-07 12:57:55 +02:00
Private Function _Printer_Read() As TapPrinter
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
Return $hPrinter
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-02 18:35:48 +02:00
Private Function _Finished_Read() As Boolean
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
Return $hPrinter.Session.Finished
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-02 18:35:48 +02:00
Public Sub _Reset()
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
$hPrinter = New TapPrinter As "Printer"
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
Private Function _Next_Read() As TestAssertion
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
If $hNext = Null Then
$hNext = New TestAssertion
Endif
Return $hNext
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
Private Sub _Next_Write(Value As TestAssertion)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
$hNext = Value
2020-05-13 11:14:19 +02:00
2019-11-15 22:33:54 +01:00
End
2020-04-27 12:01:47 +02:00
2020-05-02 18:35:48 +02:00
Public Sub _Subtest(Description As String, Optional Tests As Integer, Optional Comment As String)
2020-05-13 11:14:19 +02:00
2020-05-07 12:57:55 +02:00
Test._Printer.Subtest(Description, Tests, Comment)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-06-05 10:56:37 +02:00
'' Plan the number of assertions within a test method.
'' If the number is not correct, the test fails.
2020-04-27 12:01:47 +02:00
Public Sub Plan(Tests As Integer, Optional Comment As String)
2020-05-13 11:14:19 +02:00
2020-05-07 12:57:55 +02:00
Test._Printer.Plan(Tests, Comment)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-06-05 10:56:37 +02:00
'' Skip the current test.
2020-04-27 12:01:47 +02:00
Public Sub SkipAll(Optional Comment As String)
2020-05-13 11:14:19 +02:00
2020-05-07 12:57:55 +02:00
Test._Printer.SkipAll(Comment)
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-02 18:35:48 +02:00
Public Sub _Finish()
2020-05-13 11:14:19 +02:00
2020-05-07 12:57:55 +02:00
Test._Printer.Finish()
2020-05-13 11:14:19 +02:00
2020-04-27 12:01:47 +02:00
End
2020-05-22 16:07:50 +02:00
2020-05-26 20:07:58 +02:00
Private Sub PrintAllTestModules()
2020-05-22 16:07:50 +02:00
Dim sName As String
2020-05-25 11:08:21 +02:00
Dim aTest As New String[]
2020-05-23 08:10:33 +02:00
2020-05-22 16:07:50 +02:00
Component.Load("gb.util")
2020-05-23 08:10:33 +02:00
2020-05-22 16:07:50 +02:00
For Each sName In Dir(".../.gambas")
2020-05-31 00:58:50 +02:00
sName = Helper.CheckTestModule(sName)
If sName Then aTest.Add(sName)
2020-05-22 16:07:50 +02:00
Next
2020-05-27 14:27:38 +02:00
2020-05-25 11:08:21 +02:00
Print aTest.Join();
2020-05-23 08:10:33 +02:00
2020-05-22 16:07:50 +02:00
End
2020-05-26 12:50:39 +02:00
'Fill suite from String
2020-05-23 12:09:34 +02:00
Private Sub FromString(Tests As String)
Dim Commands As TestCommand[]
Dim Command As TestCommand
Dim TestModule As Class
Dim sName As String
Commands = TestCommand.FromString(Tests)
2020-05-27 14:27:38 +02:00
2020-05-23 12:09:34 +02:00
$Suite = New TestSuite
For Each Command In Commands
With Command
2020-05-25 21:24:28 +02:00
TestModule = __Test.Load(.ModuleName)
2020-05-23 12:09:34 +02:00
If Command.Methods.Count = 0 Then
Command.Methods = $Suite.GetTestsFromTestModule(TestModule)
Endif
For Each sName In Command.Methods
$Suite.AddTestCase(sName, Testmodule)
Next
End With
Next
2020-05-23 12:22:12 +02:00
End
2020-05-31 00:58:50 +02:00