#
# 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(showAll=True)
        print
