#
# TAP.py - TAP parser
#
# A pyparsing parser to process the output of the Perl
# "Test Anything Protocol"
# (http://search.cpan.org/~petdance/TAP-1.00/TAP.pm)
#
# TAP output lines are preceded or followed by a test number range:
# 1..n
# with 'n' TAP output lines.
#
# The general format of a TAP output line is:
# ok/not ok (required)
# Test number (recommended)
# Description (recommended)
# Directive (only when necessary)
#
# A TAP output line may also indicate abort of the test suit with the line:
# Bail out!
# optionally followed by a reason for bailing
#
# Copyright 2008, by Paul McGuire
#
from pyparsing import ParserElement,LineEnd,Optional,Word,nums,Regex,\
Literal,CaselessLiteral,Group,OneOrMore,Suppress,restOfLine,\
FollowedBy,empty
__all__ = ['tapOutputParser', 'TAPTest', 'TAPSummary']
# newlines are significant whitespace, so set default skippable
# whitespace to just spaces and tabs
ParserElement.setDefaultWhitespaceChars(" \t")
NL = LineEnd().suppress()
integer = Word(nums)
plan = '1..' + integer("ubound")
OK,NOT_OK = map(Literal,['ok','not ok'])
testStatus = (OK | NOT_OK)
description = Regex("[^#\n]+")
description.setParseAction(lambda t:t[0].lstrip('- '))
TODO,SKIP = map(CaselessLiteral,'TODO SKIP'.split())
directive = Group(Suppress('#') + (TODO + restOfLine |
FollowedBy(SKIP) +
restOfLine.copy().setParseAction(lambda t:['SKIP',t[0]]) ))
commentLine = Suppress("#") + empty + restOfLine
testLine = Group(
Optional(OneOrMore(commentLine + NL))("comments") +
testStatus("passed") +
Optional(integer)("testNumber") +
Optional(description)("description") +
Optional(directive)("directive")
)
bailLine = Group(Literal("Bail out!")("BAIL") +
empty + Optional(restOfLine)("reason"))
tapOutputParser = Optional(Group(plan)("plan") + NL) & \
Group(OneOrMore((testLine|bailLine) + NL))("tests")
class TAPTest(object):
def __init__(self,results):
self.num = results.testNumber
self.passed = (results.passed=="ok")
self.skipped = self.todo = False
if results.directive:
self.skipped = (results.directive[0][0]=='SKIP')
self.todo = (results.directive[0][0]=='TODO')
@classmethod
def bailedTest(cls,num):
ret = TAPTest(empty.parseString(""))
ret.num = num
ret.skipped = True
return ret
class TAPSummary(object):
def __init__(self,results):
self.passedTests = []
self.failedTests = []
self.skippedTests = []
self.todoTests = []
self.bonusTests = []
self.bail = False
if results.plan:
expected = range(1, int(results.plan.ubound)+1)
else:
expected = range(1,len(results.tests)+1)
for i,res in enumerate(results.tests):
# test for bail out
if res.BAIL:
#~ print "Test suite aborted: " + res.reason
#~ self.failedTests += expected[i:]
self.bail = True
self.skippedTests += [ TAPTest.bailedTest(ii) for ii in expected[i:] ]
self.bailReason = res.reason
break
#~ print res.dump()
testnum = i+1
if res.testNumber != "":
if testnum != int(res.testNumber):
print "ERROR! test %(testNumber)s out of sequence" % res
testnum = int(res.testNumber)
res["testNumber"] = testnum
test = TAPTest(res)
if test.passed:
self.passedTests.append(test)
else:
self.failedTests.append(test)
if test.skipped: self.skippedTests.append(test)
if test.todo: self.todoTests.append(test)
if test.todo and test.passed: self.bonusTests.append(test)
self.passedSuite = not self.bail and (set(self.failedTests)-set(self.todoTests) == set())
def summary(self, showPassed=False, showAll=False):
testListStr = lambda tl : "[" + ",".join(str(t.num) for t in tl) + "]"
summaryText = []
if showPassed or showAll:
summaryText.append( "PASSED: %s" % testListStr(self.passedTests) )
if self.failedTests or showAll:
summaryText.append( "FAILED: %s" % testListStr(self.failedTests) )
if self.skippedTests or showAll:
summaryText.append( "SKIPPED: %s" % testListStr(self.skippedTests) )
if self.todoTests or showAll:
summaryText.append( "TODO: %s" % testListStr(self.todoTests) )
if self.bonusTests or showAll:
summaryText.append( "BONUS: %s" % testListStr(self.bonusTests) )
if self.passedSuite:
summaryText.append( "PASSED" )
else:
summaryText.append( "FAILED" )
return "\n".join(summaryText)
# create TAPSummary objects from tapOutput parsed results, by setting
# class as parse action
tapOutputParser.setParseAction(TAPSummary)
if __name__ == "__main__":
test1 = """\
1..4
ok 1 - Input file opened
not ok 2 - First line of the input valid
ok 3 - Read the rest of the file
not ok 4 - Summarized correctly # TODO Not written yet
"""
test2 = """\
ok 1
not ok 2 some description # TODO with a directive
ok 3 a description only, no directive
ok 4 # TODO directive only
ok a description only, no directive
ok # Skipped only a directive, no description
ok
"""
test3 = """\
ok - created Board
ok
ok
not ok
ok
ok
ok
ok
# +------+------+------+------+
# | |16G | |05C |
# | |G N C | |C C G |
# | | G | | C +|
# +------+------+------+------+
# |10C |01G | |03C |
# |R N G |G A G | |C C C |
# | R | G | | C +|
# +------+------+------+------+
# | |01G |17C |00C |
# | |G A G |G N R |R N R |
# | | G | R | G |
# +------+------+------+------+
ok - board has 7 tiles + starter tile
1..9
"""
test4 = """\
1..4
ok 1 - Creating test program
ok 2 - Test program runs, no error
not ok 3 - infinite loop # TODO halting problem unsolved
not ok 4 - infinite loop 2 # TODO halting problem unsolved
"""
test5 = """\
1..20
ok - database handle
not ok - failed database login
Bail out! Couldn't connect to database.
"""
test6 = """\
ok 1 - retrieving servers from the database
# need to ping 6 servers
ok 2 - pinged diamond
ok 3 - pinged ruby
not ok 4 - pinged sapphire
ok 5 - pinged onyx
not ok 6 - pinged quartz
ok 7 - pinged gold
1..7
"""
for test in (test1,test2,test3,test4,test5,test6):
print test
tapResult = tapOutputParser.parseString(test)[0]
print tapResult.summary()
print
|