diff --git a/scripts/summarize_test_results.py b/scripts/summarize_test_results.py new file mode 100755 index 0000000000..32f1879ed4 --- /dev/null +++ b/scripts/summarize_test_results.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +"""Summarize the results of running all the tests. + +See the report_all_files docstring for details, or run this with --help. + +""" + +import collections +import os + +import click +from lxml import etree + + +@click.command() +@click.option("--errors/--no-errors", help="Show details of errors") +@click.option("--names/--no-names", help="Show all test names") +@click.option("--outcomes/--no-outcomes", help="Show pass/fail/error with names") +@click.argument("start", default="reports") +def report_all_files(errors, names, outcomes, start): + """Find all the nosetests.xml files, and report on them. + + For every nosetests.xml file found, prints a summary of the number of + tests, fails, errors, etc. If --details is used, then the error messages + from all of the fails and errors will be shown, most frequent first, with a + count of how many tests failed for that reason. + + """ + totals = TestResults() + for dirpath, _, filenames in os.walk(start): + if "nosetests.xml" in filenames: + results = report_file( + os.path.join(dirpath, "nosetests.xml"), + errors=errors, + names=names, + outcomes=outcomes, + ) + totals += results + + print "\nTotals:\n{}".format(totals) + + +class Summable(object): + """An object whose attributes can be added together easily. + + Subclass this and define `fields` on your derived class. + + """ + def __init__(self): + for name in self.fields: + setattr(self, name, 0) + + @classmethod + def from_element(cls, element): + """Construct a Summable from an xml element with the same attributes.""" + self = cls() + for name in self.fields: + setattr(self, name, int(element.get(name))) + return self + + def __add__(self, other): + result = type(self)() + for name in self.fields: + setattr(result, name, getattr(self, name) + getattr(other, name)) + return result + + +class TestResults(Summable): + """A test result, makeable from a nosetests.xml element.""" + + fields = ["tests", "errors", "failures", "skip"] + + def __str__(self): + msg = "{0.tests:4d} tests, {0.errors} errors, {0.failures} failures, {0.skip} skipped" + return msg.format(self) + + +def error_line_from_error_element(element): + """Given an element, get the important error line from it.""" + return element.get("message").splitlines()[0] + + +def report_file(path, errors, names, outcomes): + """Report on one nosetests.xml file.""" + print "\n{}".format(path) + with open(path) as xml_file: + tree = etree.parse(xml_file) # pylint: disable=no-member + suite = tree.xpath("/testsuite")[0] + + results = TestResults.from_element(suite) + print results + + if errors: + errors = collections.Counter() + for error_element in tree.xpath(".//error|.//failure"): + errors[error_line_from_error_element(error_element)] += 1 + + if errors: + print "" + for error_message, number in errors.most_common(): + print "{0:4d}: {1}".format(number, error_message) + + if names: + for testcase in tree.xpath(".//testcase"): + if outcomes: + result = testcase.xpath("*") + if result: + outcome = result[0].tag + if outcome == "system-out": + outcome = "." + else: + outcome = outcome[0].upper() + else: + outcome = "." + else: + outcome = "" + print " {outcome} {classname}.{name}".format( + outcome=outcome, + classname=testcase.get("classname"), + name=testcase.get("name"), + ) + + return results + + +if __name__ == "__main__": + report_all_files() # pylint: disable=no-value-for-parameter