Low-level printer and parser for TAP format

The Test Anything Protocol (TAP) is a text-based format for communicating
unit test results. It originated from perl and has a specification here:
http://testanything.org/
This commit is contained in:
Tobias Boege 2018-04-22 23:56:32 +02:00
parent 2d591f341b
commit 0929707aee
4 changed files with 368 additions and 0 deletions

3
.src/Tap/Tap.module Normal file
View file

@ -0,0 +1,3 @@
' Gambas module file
Public Enum NONE = 0, TODO = 1, SKIP

175
.src/Tap/TapParser.class Normal file
View file

@ -0,0 +1,175 @@
' Gambas class file
''' Low-level parser for Test Anything Protocol ([TAP]) output.
'''
''' [TAP]: http://testanything.org/tap-specification.html
Export
Event Version(Version As Integer)
Event Plan(Start As Integer, {End} As Integer)
Event Ok(TestNr As Integer, Description As String)
Event NotOk(TestNr As Integer, Description As String)
Event Todo(TestNr As Integer, Comment As String)
Event Skip(TestNr As Integer, Comment As String)
Event BailOut(Comment As String)
Event Diagnostic(Comment As String)
Event Else({Line} As String)
Private $hTapStream As Stream
Private $iTestsRun As Integer
Public Sub _new(TapStream As Stream)
$hTapStream = TapStream
End
Public Sub Parse()
Dim sLine As String
Dim iLine As Integer = 0
Dim iVersion As Integer
Dim bResult As Boolean
Dim iTestNr As Integer
Dim sDescription As String
Dim iDirective As Integer
Dim sComment As String
$iTestsRun = 0
For Each sLine In $hTapStream.Lines
If iLine = 0 And If sLine Begins "TAP version " Then
Try iVersion = CInt(Trim$(Mid$(sLine, 13)))
' At present a "TAP version" line is always an error:
' 1. It might not be an integer, which is an error
' 2. A version lower than 13 is an error by the specification
' 3. We don't support version 13 or above.
If Error Then Error.Raise(Subst$(("Unrecognised TAP version '&1'"), Trim$(Mid$(sLine, 13))))
If iVersion < 13 Then Error.Raise(Subst$(("Illegal TAP version '&1'"), iVersion))
If iVersion > 12 Then Error.Raise(Subst$(("Unsupported TAP version &1"), iVersion))
Endif
If iLine = 0 Then Raise Version(12)
If sLine Match "\\d+..\\d+" Then
With Scan(sLine, "*..*")
If Not IsInteger(.[0]) Or If Not IsInteger(.[1]) Then Error.Raise(Subst$(("Couldn't extract test plan from '&1'"), sLine))
Raise Plan(CInt(.[0]), CInt(.[1]))
End With
Else If sLine Begins "ok" Or If sLine Begins "not ok" Then
bResult = ParseTest(sLine, ByRef iTestNr, ByRef sDescription, ByRef iDirective, ByRef sComment)
' A single line may raise two events: Ok or NotOk, depending on the result
' and Todo or Skip or none of the two depending on the directive.
' Use the TestNr argument to link the two when counting tests for statistics.
If bResult Then
Raise Ok(iTestNr, sDescription)
Else
Raise NotOk(iTestNr, sDescription)
Endif
If iDirective = Tap.TODO Then
Raise Todo(iTestNr, sComment)
Else If iDirective = Tap.SKIP Then
Raise Skip(iTestNr, sComment)
Endif
Else If sLine Begins "Bail out!" Then
Raise BailOut(Trim$(Mid$(sLine, 10)))
Else If sLine Begins "#" Then
Raise Diagnostic(Trim$(Mid$(sLine, 2)))
Else
Raise Else(sLine)
Endif
Inc iLine
Next
End
' This pattern works in perl but not in gb.pcre... So doing it by hand below.
' Dim rTest As RegExp
' Try rTest = New RegExp(sLine, Replace$("(ok|not ok)WS(\\d+WS)?([^#]+WS)?(?:#\\s*((?i)TODO|SKIP(?-i))WS(.*)?)?", "WS", "(?:\\s+|$)"))
' Inc $iTestsRun
' Try TestNr = CInt(rTest[2].Text)
' If Error Then TestNr = $iTestsRun
' Description = rTest[3].Text
' Directive = IIf(rTest[4].Text, IIf(Upper$(rTest[4].Text) = "TODO", Tap.TODO, Tap.SKIP), Tap.NONE)
' Comment = rTest[5].Text
'
' Return rTest[1].Text = "ok"
Private Function ParseTest(sLine As String, ByRef TestNr As Integer, ByRef Description As String, ByRef Directive As Integer, ByRef Comment As String) As Boolean
Dim bResult As Boolean
Dim aWords As String[]
Dim sWord As String
' Tidy up caller's local variables
TestNr = 0
Description = ""
Directive = Tap.NONE
Comment = ""
Inc $iTestsRun
' "ok" or "not ok"
If sLine Begins "ok" Then
bResult = True
sLine = Trim$(Mid$(sLine, 3))
Else If sLine Begins "not ok"
bResult = False
sLine = Trim$(Mid$(sLine, 7))
Else
Error.Raise(Subst$(("Not a test line '&1'"), sLine))
Endif
' Make sure that if a "#" occurs, it will be after a space
sLine = Replace$(sLine, "#", " #")
aWords = Split(sLine, " ", "", True).Reverse()
' TestNr
Try sWord = aWords.Pop()
Try TestNr = CInt(sWord)
If Error Then
aWords.Push(sWord)
TestNr = $iTestsRun
Endif
' Description
While aWords.Count
sWord = aWords.Pop()
If sWord Begins "#" Then Break
Description &= sWord & " "
Wend
Description = Trim$(Description)
' Directive
If sWord Begins "#" Then
If sWord = "#" Then
Try sWord = aWords.Pop()
If Error Then Error.Raise(("Premature end of directive"))
Endif
Select Case Lower$(sWord)
Case "todo"
Directive = Tap.TODO
Case "skip"
Directive = Tap.SKIP
Default
Error.Raise(Subst$(("Invalid directive '&1'"), sWord))
End Select
Endif
' Comment
Comment = Trim$(aWords.Reverse().Join(" "))
Return bResult
End

79
.src/Tap/TapPrinter.class Normal file
View file

@ -0,0 +1,79 @@
' Gambas class file
''' Low-level class for planning and printing test results in Test Anything Protocol ([TAP]) format.
'''
''' [TAP]: http://testanything.org/tap-specification.html
Export
Private Const NO_PLAN As Integer = -1
Private $hOutput As Stream
Private $iPlan As Integer = NO_PLAN
Private $iTestsRun As Integer = 0
Public Sub _new(Optional Tests As Integer, Optional Comment As String, Optional {Output} As Stream = File.Out)
$hOutput = {Output}
If Not IsMissing(Tests) Then Plan(Tests, Comment)
End
Public Sub Plan(Tests As Integer, Optional Comment As String)
If $iTestsRun Then Error.Raise(Subst$(("Tests already started, at test #&1"), $iTestsRun))
' TAP specification lists '1..0 # Skipped: WWW::Mechanize not installed'
' as a valid example.
If Tests <= NO_PLAN Then Error.Raise(("Number of tests must be non-negative"))
$iPlan = Tests
Print #$hOutput, "1.."; $iPlan;
Print #$hOutput, IIf(Comment, " # " & Comment, "")
End
Public Sub Finish()
If $iPlan > NO_PLAN Then Return ' already printed the "plan" line
Print #$hOutput, "1.."; $iTestsRun
End
Public Sub Test(Result As Boolean, Optional TestNr As Integer, Optional Description As String, Optional Directive As Integer, Optional Comment As String)
Dim sDirective As String
' It is not advised to start a description with a number token because
' it will be interpreted as the (optional) test number. We issue a warning
' about this but fix it anyway by always printing the TestNr before *and*
' prefixing the Description with a dash, as is common.
If Description Match "^[0-9]+(\\s|$)" Then
Error Subst$(("Warning: Description '&1' should not start with a number"), Description)
Endif
If Description Like "*#*" Then
Error.Raise(Subst$(("Description '&1' may not contain a '#' character"), Description))
Endif
Inc $iTestsRun
If Not TestNr Then TestNr = $iTestsRun
If Not IsMissing(Directive) Then
' Matches the values of the Enum Tap.Todo, Tap.Skip
sDirective = Choose(Directive, "TODO", "SKIP")
If Not sDirective Then Error.Raise(Subst$(("Unsupported directive '&1'"), Directive))
Endif
Print #$hOutput, Subst$("&1 &2 - &3", IIf(Result, "ok", "not ok"), TestNr, Description);
Print #$hOutput, IIf(sDirective, Subst$(" # &1&2", sDirective, IIf(Comment, " " & Comment, "")), "")
End
Public Sub BailOut(Optional Comment As String)
Print #$hOutput, "Bail out!";; Comment
End
Public Sub Diagnostic(Comment As String)
Print #$hOutput, "#";; Comment
End

111
.src/Tap/TapTester.module Normal file
View file

@ -0,0 +1,111 @@
' Gambas module file
Public Sub Main()
Dim hPrinter As TapPrinter
Dim hStream As Stream
Dim hParser As TapParser
Dim sTap As String = ""
"ok - created Board\n"
"ok\n"
"ok\n"
"ok\n"
"ok\n"
"ok\n"
"ok\n"
"ok\n"
"not ok - gladly it's todo # TODO test\n"
"# +------+------+------+------+\n"
"# | |16G | |05C |\n"
"# | |G N C | |C C G |\n"
"# | | G | | C +|\n"
"# +------+------+------+------+\n"
"# |10C |01G | |03C |\n"
"# |R N G |G A G | |C C C |\n"
"# | R | G | | C +|\n"
"# +------+------+------+------+\n"
"# | |01G |17C |00C |\n"
"# | |G A G |G N R |R N R |\n"
"# | | G | R | G |\n"
"# +------+------+------+------+\n"
"ok - board has 7 tiles + starter tile\n"
"1..9"
hStream = Open String sTap For Read
hParser = New TapParser(hStream) As "Parser"
hParser.Parse()
Close #hStream
Print String$(50, "-")
hStream = Open String For Write
hPrinter = New TapPrinter(4, "", hStream)
hPrinter.Test(12 = 12)
hPrinter.Test(True,, "True is true")
hPrinter.Test(False,, "False is true", Tap.SKIP, "This is impossible")
hPrinter.Diagnostic("Omitting a test intentionally")
hPrinter.Finish()
sTap = Close #hStream
Print sTap
Print String$(50, "-")
hStream = Open String sTap For Read
hParser = New TapParser(hStream) As "Parser"
hParser.Parse()
Close #hStream
End
Public Sub Parser_Ok(TestNr As Integer, Description As String)
Debug "OK("; TestNr; ")";; Description
End
Public Sub Parser_NotOk(TestNr As Integer, Description As String)
Debug "NOTOK("; TestNr; ")";; Description
End
Public Sub Parser_Todo(TestNr As Integer, Comment As String)
Debug "TODO("; TestNr; ")";; Comment
End
Public Sub Parser_Skip(TestNr As Integer, Comment As String)
Debug "SKIP("; TestNr; ")";; Comment
End
Public Sub Parser_Version(Version As Integer)
Debug "TAP version";; Version
End
Public Sub Parser_Plan(Start As Integer, {End} As Integer)
Debug "PLAN";; Start; ".."; {End}
End
Public Sub Parser_BailOut(Comment As String)
Debug "BAIL";; Comment
End
Public Sub Parser_Diagnostic(Comment As String)
Debug "DIAG";; Comment
End
Public Sub Parser_Else({Line} As String)
Debug "ELSE";; {Line}
End