diff options
author | Paul J. Davis <paul.joseph.davis@gmail.com> | 2019-12-19 09:46:38 -0600 |
---|---|---|
committer | Paul J. Davis <paul.joseph.davis@gmail.com> | 2019-12-19 10:24:30 -0600 |
commit | 0cb01bf39bf110d5ca9d0e62b679438081dd80ca (patch) | |
tree | eb9b6e53e7071c0469f96a4d298691533bc44d52 | |
parent | a9ce49de1b67848f98cdf60a5ac81f34362ee8be (diff) | |
download | couchdb-0cb01bf39bf110d5ca9d0e62b679438081dd80ca.tar.gz |
Add show-test-results.py script
This allows developers to locate long running tests and errors after
running `make check`.
-rwxr-xr-x | build-aux/show-test-results.py | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/build-aux/show-test-results.py b/build-aux/show-test-results.py new file mode 100755 index 000000000..eecfc2e9d --- /dev/null +++ b/build-aux/show-test-results.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python2.7 + +import argparse +import glob +import json +import os +import re +import xml.dom.minidom as md + + +TEST_COLLECTIONS = { + "EUnit": "src/**/.eunit/*.xml", + "EXUnit": "_build/integration/lib/couchdbtest/*.xml", + "Mango": "src/mango/*.xml" +} + + + +def _attrs(elem): + ret = {} + for (k, v) in elem.attributes.items(): + ret[k.lower()] = v + return ret + + +def _text(elem): + rc = [] + for node in elem.childNodes: + if node.nodeType == node.TEXT_NODE: + rc.append(node.data) + else: + rc.append(self._text(node)) + return ''.join(rc) + + +class TestCase(object): + def __init__(self, elem): + self.elem = elem + + attrs = _attrs(elem) + + self.name = self._name(attrs) + self.time = float(attrs["time"]) + + self.failure = False + self._check_failure(elem, attrs) + + self.error = False + self._check_error(elem, attrs) + + self.skipped = False + self._check_skipped(elem, attrs) + + def _check_failure(self, elem, attrs): + failures = elem.getElementsByTagName("failure") + if not failures: + return + + self.failure = True + self.failure_msg = _text(failures[0]).strip() + + def _check_error(self, elem, attrs): + errors = elem.getElementsByTagName("error") + if not errors: + return + + self.error = True + self.error_msg = _text(errors[0]).strip() + + def _check_skipped(self, elem, attrs): + skipped = elem.getElementsByTagName("skipped") + if not skipped: + return + + attrs = _attrs(skipped[0]) + self.skipped = True + self.skipped_msg = attrs.get("message", attrs.get("type", "<unknown>")) + + def _name(self, attrs): + klass = attrs.get("classname", "") + if klass.startswith("Elixir."): + klass = klass[len("Elixir."):] + if klass: + return "%s - %s" % (klass, attrs["name"]) + return attrs["name"] + + +class TestSuite(object): + SUITE_NAME_PATTERNS = [ + re.compile("module '([^']+)'"), + re.compile("Elixir\.(.+)") + ] + + def __init__(self, elem): + self.elem = elem + + attrs = _attrs(elem) + + self.name = self._name(attrs) + + self.time = 0.0 + if "time" in attrs: + self.time = float(attrs["time"]) + + self.num_tests = int(attrs["tests"]) + self.num_failures = int(attrs["failures"]) + self.num_errors = int(attrs["errors"]) + self.num_skipped = 0 + + self.tests = [] + self.test_time = 0.0 + + for t_elem in elem.getElementsByTagName("testcase"): + self.tests.append(TestCase(t_elem)) + self.test_time += self.tests[-1].time + if self.tests[-1].skipped: + self.num_skipped += 1 + + if self.time == 0.0 and self.test_time > 0.0: + self.time = self.test_time + + def _name(self, attrs): + raw_name = attrs["name"] + for p in self.SUITE_NAME_PATTERNS: + match = p.match(raw_name) + if match: + return match.group(1) + return raw_name + + +class TestCollection(object): + def __init__(self, name, pattern): + self.name = name + self.pattern = pattern + self.suites = [] + self.bad_files = [] + + for fname in glob.glob(pattern): + self._load_file(fname) + + def _load_file(self, filename): + try: + dom = md.parse(filename) + except: + self.bad_files.append(filename) + return + for elem in dom.getElementsByTagName("testsuite"): + self.suites.append(TestSuite(elem)) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Show test result summaries") + parser.add_argument( + "--ignore-failures", + action="store_true", + default=False, + help="Don't display test failures" + ) + parser.add_argument( + "--ignore-errors", + action="store_true", + default=False, + help="Don't display test errors" + ) + parser.add_argument( + "--ignore-skipped", + action="store_true", + default=False, + help="Don't display skipped tests" + ) + parser.add_argument( + "--all", + type=int, + default=0, + help="Number of rows to show for all groups" + ) + parser.add_argument( + "--collection", + action="append", + default=[], + help="Which collection to display. May be repeated." + ) + parser.add_argument( + "--suites", + type=int, + default=0, + help="Number of suites to show" + ) + parser.add_argument( + "--tests", + type=int, + default=0, + help="Number of tests to show" + ) + parser.add_argument( + "--sort", + default="total", + choices=["test", "fixture", "total"], + help="Timing column to sort on" + ) + return parser.parse_args() + + +def display_failures(collections): + failures = [] + for collection in collections: + for suite in collection.suites: + for test in suite.tests: + if not test.failure: + continue + failures.append((test.name, test.failure_msg)) + + if not len(failures): + return + print "Failures" + print "========" + print + for failure in failures: + print failure[0] + print "-" * len(failure[0]) + print + print failure[1] + print + + +def display_errors(collections): + errors = [] + for collection in collections: + for suite in collection.suites: + for test in suite.tests: + if not test.error: + continue + errors.append((test.name, test.error_msg)) + + if not len(errors): + return + print "Errors" + print "======" + print + for error in errors: + print error[0] + print "-" * len(error[0]) + print + print error[1] + print + + +def display_skipped(collections): + skipped = [] + for collection in collections: + for suite in collection.suites: + for test in suite.tests: + if not test.skipped: + continue + name = "%s - %s - %s" % (collection.name, suite.name, test.name) + skipped.append((name, test.skipped_msg)) + if not skipped: + return + print "Skipped" + print "=======" + print + for row in sorted(skipped): + print " %s: %s" % row + print + + +def display_table(table): + for ridx, row in enumerate(table): + new_row = [] + for col in row: + if isinstance(col, float): + new_row.append("%4.1fs" % col) + elif isinstance(col, int): + new_row.append("%d" % col) + else: + new_row.append(col) + table[ridx] = new_row + for row in table: + fmt = " ".join(["%10s"] * len(row)) + print fmt % tuple(row) + + +def display_collections(collections, sort): + rows = [] + for collection in collections: + total_time = 0.0 + test_time = 0.0 + num_tests = 0 + num_failures = 0 + num_errors = 0 + num_skipped = 0 + for suite in collection.suites: + total_time += suite.time + test_time += suite.test_time + num_tests += suite.num_tests + num_failures += suite.num_failures + num_errors += suite.num_errors + num_skipped += suite.num_skipped + cols = ( + total_time, + max(0.0, total_time - test_time), + test_time, + num_tests, + num_failures, + num_errors, + num_skipped, + collection.name + " " + ) + rows.append(cols) + + scol = 0 + if sort == "fixture": + scol = 1 + elif sort == "test": + scol = 2 + def skey(row): + return (-1.0 * row[scol], row[-1]) + rows.sort(key=skey) + + print "Collections" + print "===========" + print + headers = [ + "Total", + "Fixture", + "Test", + "Count", + "Failed", + "Errors", + "Skipped" + ] + display_table([headers] + rows) + print + + +def display_suites(collections, count, sort): + rows = [] + for collection in collections: + for suite in collection.suites: + cols = [ + suite.time, + max(0.0, suite.time - suite.test_time), + suite.test_time, + suite.num_tests, + suite.num_failures, + suite.num_errors, + suite.num_skipped, + collection.name + " - " + suite.name + ] + rows.append(cols) + + scol = 0 + if sort == "fixture": + scol = 1 + elif sort == "test": + scol = 2 + def skey(row): + return (-1.0 * row[scol], row[-1]) + rows.sort(key=skey) + + rows = rows[:count] + + print "Suites" + print "======" + print + headers = [ + "Total", + "Fixture", + "Test", + "Count", + "Failed", + "Errors", + "Skipped" + ] + display_table([headers] + rows) + print + + +def display_tests(collections, count): + rows = [] + for collection in collections: + for suite in collection.suites: + for test in suite.tests: + if test.failure or test.error or test.skipped: + continue + fmt = "%s - %s - %s" + display = fmt % (collection.name, suite.name, test.name) + rows.append((test.time, display)) + + def skey(row): + return (-1.0 * row[0], row[-1]) + rows.sort(key=skey) + rows = rows[:count] + + print "Tests" + print "=====" + print + display_table(rows) + print + + +def main(): + args = parse_args() + + if not args.collection: + args.collection = ["eunit", "exunit", "mango"] + + collections = [] + for (name, pattern) in TEST_COLLECTIONS.items(): + if name.lower() not in args.collection: + continue + collections.append(TestCollection(name, pattern)) + + if not args.ignore_failures: + display_failures(collections) + + if not args.ignore_errors: + display_errors(collections) + + if not args.ignore_skipped: + display_skipped(collections) + + display_collections(collections, args.sort) + + if args.all > 0: + args.suites = args.all + args.tests = args.all + + if args.suites > 0: + display_suites(collections, args.suites, args.sort) + + if args.tests > 0: + display_tests(collections, args.tests) + + +if __name__ == "__main__": + main() |