File: //snap/google-cloud-cli/394/platform/gsutil/third_party/pyparsing/examples/TAP.py
#
# TAP.py - TAP parser
#
# A pyparsing parser to process the output of the Perl
# "Test Anything Protocol"
# (https://metacpan.org/pod/release/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,
autoname_elements,
)
__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(r"[^#\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")
autoname_elements()
class TAPTest:
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:
def __init__(self, results):
self.passedTests = []
self.failedTests = []
self.skippedTests = []
self.todoTests = []
self.bonusTests = []
self.bail = False
if results.plan:
expected = list(range(1, int(results.plan.ubound) + 1))
else:
expected = list(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(f"PASSED: {testListStr(self.passedTests)}")
if self.failedTests or showAll:
summaryText.append(f"FAILED: {testListStr(self.failedTests)}")
if self.skippedTests or showAll:
summaryText.append(f"SKIPPED: {testListStr(self.skippedTests)}")
if self.todoTests or showAll:
summaryText.append(f"TODO: {testListStr(self.todoTests)}")
if self.bonusTests or showAll:
summaryText.append(f"BONUS: {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)
def main():
import contextlib
with contextlib.suppress(Exception):
tapOutputParser.create_diagram("TAP_diagram.html", vertical=3)
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(showAll=True))
print()
if __name__ == "__main__":
main()