Skip to content
Snippets Groups Projects
doctest.py 8.22 KiB
Newer Older
"""
    Defines a handler for the Sphinx 'build-finished' event.
    If the builder is doctest then it post-processes the
    output file to produce an XUnit-style XML file that can be
    more easily parse by CI servers such as Jenkins.

    Output file structure
    ~~~~~~~~~~~~~~~~~~~~~

    The following outcomes are possible for a given
    document:
     - all tests pass;
     - all tests fail;
     - and some test pass and some fail.

    Below are examples of the output for each of the above
    outcomes, given a document named 'FooDoc' in a
    directory 'bar'relative to the documentation root.

    - All Passed:
     ============

    Document: bar/FooDoc
    --------------------
    1 items passed all tests:
       2 tests in default
       1 tests in ExForFoo
    2 tests in 1 items.
    2 passed and 0 failed.
    Test passed.

    - All Failed:
     ============

    Document: bar/FooDoc
    --------------------
    **********************************************************************
    File "bar/FooDoc.rst", line 127, in Ex2
    Failed example:
        print "Multi-line failed"
        print "test"
    Expected:
        No match
    Got:
        Multi-line failed
        test
    **********************************************************************
    File "bar/FooDoc.rst", line 111, in Ex1
    Failed example:
        print "Single line failed test"
    Expected:
        No match
    Got:
        Single line failed test
    **********************************************************************
    2 items had failures:
       1 of   1 in Ex1
       1 of   1 in Ex2
    2 tests in 2 items.
    0 passed and 2 failed.
    ***Test Failed*** 2 failures.


    - Some pass some fail:
      ====================

    Document: bar/FooDoc
    --------------------
    **********************************************************************
    File "bar/FooDoc.rst", line 127, in default
    Failed example:
        print "A failed test"
    Expected:
        Not a success
    Got:
        A failed test
    **********************************************************************
    File "bar/FooDoc.rst", line 143, in Ex1
    Failed example:
        print "Second failed test"
    Expected:
        Not a success again
    Got:
        Second failed test
    1 items passed all tests:
        1 tests in Ex3
    **********************************************************************
    2 items had failures:
       1 of   1 in Ex1
       1 of   2 in default
    4 tests in 3 items.
    2 passed and 2 failed.
    ***Test Failed*** 2 failures.

"""

#-------------------------------------------------------------------------------
class TestSuite(object):

    def __init__(self, name, cases, package=None):
        self.name = name
        self.testcases = cases
        self.package = package

    @property
    def ntests(self):
        return len(self.testcases)

    @property
    def nfailures(self):
        def sum_failure(fails, case):
            if case.failed: return fails + 1
            else: return fails
        return reduce(sum_failure, self.testcases, 0)

    @property
    def npassed(self):
        return self.ntests - self.nfailures

#-------------------------------------------------------------------------------
class TestCase(object):

    def __init__(self, classname, name, failure_descr):
        self.classname = classname
        self.name = name
        self.failure_descr = failure_descr

    @property
    def passed(self):
        return (self.failure_descr is None)

    @property
    def failed(self):
        return not self.passed

#-------------------------------------------------------------------------------
class DocTestOutputParser(object):
    """
    Process a doctest output file and convert it
    to a different format
    """

    def __init__(self, filename):
        with open(filename,'r') as result_file:
            self.testsuite = self.__parse(result_file)

    def as_xunit(self, filename):
        """
        Write out the test results in Xunit-style format
        """
        cases = self.testsuite.testcases
        suite_node = ElementTree.Element("testsuite")
        suite_node.attrib["name"] = self.testsuite.name
        suite_node.attrib["tests"] = str(self.testsuite.ntests)
        suite_node.attrib["failures"] = str(self.testsuite.nfailures)
        for testcase in cases:
            case_node = ElementTree.SubElement(suite_node, "testcase")
            case_node.attrib["classname"] = testcase.classname
            case_node.attrib["name"] = testcase.name
            if testcase.failed:
                failure_node = ElementTree.SubElement(case_node, "failure")
                failure_node.attrib["type"] = TEST_FAILURE_TYPE
                failure_node.text = testcase.failure_descr
        # Serialize to file
        tree = ElementTree.ElementTree(suite_node)
        tree.write(filename, encoding="utf-8", xml_declaration=True)

    def __parse(self, result_file):
        """
        Parse a doctest output file and a TestSuite
        object that describe the results of the
        all tests on a single document

        Arguments:
          result_file (File): File-like object

        Returns:
          TestSuite: TestSuite object
        """
                in_doc = False
        document_txt = []
        cases = []
        for line in result_file:
            if line.startswith(DOCTEST_DOCUMENT_BEGIN):
                # parse previous results
                if document_txt:
                    cases.extend(self.__parse_document(document_txt))
                document_txt = [line]
                in_doc = True
                continue
            if line.startswith(DOCTEST_SUMMARY_TITLE):
                in_doc = False
            if in_doc and line != "":
                document_txt.append(line)
        # endif
        return TestSuite(name="doctests", cases=cases,
                         package="doctests")

    def __parse_document(self, text):
        """
        Create a list of TestCase object for this document

        Args:
          text (str): String containing doctest output
                      for document
        Returns:
          list: List of test cases in the document
        """
        fullname = self.__extract_fullname(text[0])
        if not text[1].startswith("-"):
            raise ValueError("Invalid second line of output: '%s'. "\
                             "Expected a title underline."
                             % text[1])

        text = text[2:] # trim off top two lines
        if text[0].startswith("*"):
            print "@todo: Do failure cases"
        else:
            # assume all passed
            testcases = self.__parse_success(fullname, text)

        return testcases

    def __extract_fullname(self, line):
        """
        Extract the document name from the line of text.

        Args:
          line (str): Line to test for title
        """
        if not line.startswith(DOCTEST_DOCUMENT_BEGIN):
            raise ValueError("First line of output text should be a line "
                             "beginning '%s'" % DOCTEST_DOCUMENT_BEGIN)
        return line.replace(DOCTEST_DOCUMENT_BEGIN, "").strip()

    def __parse_success(self, fullname, result_txt):
        """
        Parse text for success cases for a single document

        Args:
          fullname (str): String containing full name of document
          result_txt (str): String containing doctest output for
                            document
        """
        match = NUMBER_PASSED_RE.match(result_txt[0])
        if not match:
            raise ValueError("All passed line incorrect: '%s'"
                             % result_txt[0])
        classname = fullname.split("/")[-1] if "/" in fullname else fullname
        nitems = int(match.group(1))
        cases = []
        for line in result_txt[1:1+nitems]:
            match = ALLPASS_TEST_NAMES_RE.match(line)
            if not match:
                raise ValueError("Unexpected information line in "
                                 "all pass case: %s" % line)
            ntests, name = int(match.group(1)), match.group(2)
            for idx in range(ntests):
                cases.append(TestCase(classname, name, failure_descr=None))
        #endfor
        return cases

#-------------------------------------------------------------------------------