diff options
Diffstat (limited to 'testing/web-platform/tests/tools/runner/report.py')
-rw-r--r-- | testing/web-platform/tests/tools/runner/report.py | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/runner/report.py b/testing/web-platform/tests/tools/runner/report.py new file mode 100644 index 0000000000..3dc6cfb8bb --- /dev/null +++ b/testing/web-platform/tests/tools/runner/report.py @@ -0,0 +1,308 @@ +# flake8: noqa +# mypy: ignore-errors + +import argparse +import json +import sys +import types + +from cgi import escape +from collections import defaultdict + + +def html_escape(item, escape_quote=False): + if isinstance(item, types.StringTypes): + rv = escape(item) + if escape_quote: + rv = rv.replace('"', """) + return rv + else: + return item + + +class Raw: + """Simple wrapper around a string to stop it being escaped by html_escape""" + def __init__(self, value): + self.value = value + + def __unicode__(self): + return unicode(self.value) + + +class Node: + """Node structure used when building HTML""" + def __init__(self, name, attrs, children): + #Need list of void elements + self.name = name + self.attrs = attrs + self.children = children + + def __unicode__(self): + if self.attrs: + #Need to escape + attrs_unicode = " " + " ".join("%s=\"%s\"" % (html_escape(key), + html_escape(value, + escape_quote=True)) + for key, value in self.attrs.items()) + else: + attrs_unicode = "" + return "<%s%s>%s</%s>\n" % (self.name, + attrs_unicode, + "".join(unicode(html_escape(item)) + for item in self.children), + self.name) + + def __str__(self): + return unicode(self).encode("utf8") + + +class RootNode: + """Special Node representing the document root""" + def __init__(self, *children): + self.children = ["<!DOCTYPE html>"] + list(children) + + def __unicode__(self): + return "".join(unicode(item) for item in self.children) + + def __str__(self): + return unicode(self).encode("utf8") + + +def flatten(iterable): + """Flatten a list of lists by one level so that + [1,["abc"], "def",[2, [3]]] + becomes + [1, "abc", "def", 2, [3]]""" + rv = [] + for item in iterable: + if hasattr(item, "__iter__") and not isinstance(item, types.StringTypes): + rv.extend(item) + else: + rv.append(item) + return rv + + +class HTML: + """Simple HTML templating system. An instance of this class can create + element nodes by calling methods with the same name as the element, + passing in children as positional arguments or as a list, and attributes + as keyword arguments, with _ replacing - and trailing _ for python keywords + + e.g. + + h = HTML() + print(h.html( + html.head(), + html.body([html.h1("Hello World!")], class_="body-class") + )) + Would give + <!DOCTYPE html><html><head></head><body class="body-class"><h1>Hello World!</h1></body></html>""" + def __getattr__(self, name): + def make_html(self, *content, **attrs): + for attr_name in attrs.keys(): + if "_" in attr_name: + new_name = attr_name.replace("_", "-") + if new_name.endswith("-"): + new_name = new_name[:-1] + attrs[new_name] = attrs.pop(attr_name) + return Node(name, attrs, flatten(content)) + + method = types.MethodType(make_html, self, HTML) + setattr(self, name, method) + return method + + def __call__(self, *children): + return RootNode(*flatten(children)) + + +h = HTML() + + +class TestResult: + """Simple holder for the results of a single test in a single UA""" + def __init__(self, test): + self.test = test + self.results = {} + + def __cmp__(self, other): + return self.test == other.test + + def __hash__(self): + return hash(self.test) + + +def load_data(args): + """Load data treating args as a list of UA name, filename pairs""" + pairs = [] + for i in xrange(0, len(args), 2): + pairs.append(args[i:i+2]) + + rv = {} + for UA, filename in pairs: + with open(filename) as f: + rv[UA] = json.load(f) + + return rv + + +def test_id(id): + """Convert a test id in JSON into an immutable object that + can be used as a dictionary key""" + if isinstance(id, list): + return tuple(id) + else: + return id + + +def all_tests(data): + tests = defaultdict(set) + for UA, results in iteritems(data): + for result in results["results"]: + id = test_id(result["test"]) + tests[id] |= {subtest["name"] for subtest in result["subtests"]} + return tests + + +def group_results(data): + """Produce a list of UAs and a dictionary mapping specific tests to their + status in all UAs e.g. + ["UA1", "UA2"], {"test_id":{"harness":{"UA1": (status1, message1), + "UA2": (status2, message2)}, + "subtests":{"subtest1": "UA1": (status1-1, message1-1), + "UA2": (status2-1, message2-1)}}} + Status and message are None if the test didn't run in a particular UA. + Message is None if the test didn't produce a message""" + tests = all_tests(data) + + UAs = data.keys() + + def result(): + return { + "harness": {UA: (None, None) for UA in UAs}, + "subtests": None # init this later + } + + results_by_test = defaultdict(result) + + for UA, results in iteritems(data): + for test_data in results["results"]: + id = test_id(test_data["test"]) + result = results_by_test[id] + + if result["subtests"] is None: + result["subtests"] = { + name: {UA: (None, None) for UA in UAs} for name in tests[id] + } + + result["harness"][UA] = (test_data["status"], test_data["message"]) + for subtest in test_data["subtests"]: + result["subtests"][subtest["name"]][UA] = (subtest["status"], + subtest["message"]) + + return UAs, results_by_test + + +def status_cell(status, message=None): + """Produce a table cell showing the status of a test""" + status = status if status is not None else "NONE" + kwargs = {} + if message: + kwargs["title"] = message + status_text = status.title() + return h.td(status_text, class_="status " + status, + **kwargs) + + +def test_link(test_id, subtest=None): + """Produce an <a> element linking to a test""" + if isinstance(test_id, types.StringTypes): + rv = [h.a(test_id, href=test_id)] + else: + rv = [h.a(test_id[0], href=test_id[0]), + " %s " % test_id[1], + h.a(test_id[2], href=test_id[2])] + if subtest is not None: + rv.append(" [%s]" % subtest) + return rv + + +def summary(UAs, results_by_test): + """Render the implementation report summary""" + not_passing = [] + for test, results in iteritems(results_by_test): + if not any(item[0] in ("PASS", "OK") for item in results["harness"].values()): + not_passing.append((test, None)) + for subtest_name, subtest_results in iteritems(results["subtests"]): + if not any(item[0] == "PASS" for item in subtest_results.values()): + not_passing.append((test, subtest_name)) + if not_passing: + rv = [ + h.p("The following tests failed to pass in all UAs:"), + h.ul([h.li(test_link(test, subtest)) + for test, subtest in not_passing]) + ] + else: + rv = "All tests passed in at least one UA" + return rv + + +def result_rows(UAs, test, result): + """Render the results for each test run""" + yield h.tr( + h.td( + test_link(test), + rowspan=(1 + len(result["subtests"])) + ), + h.td(), + [status_cell(status, message) + for UA, (status, message) in sorted(result["harness"].items())], + class_="test" + ) + + for name, subtest_result in sorted(iteritems(result["subtests"])): + yield h.tr( + h.td(name), + [status_cell(status, message) + for UA, (status, message) in sorted(subtest_result.items())], + class_="subtest" + ) + + +def result_bodies(UAs, results_by_test): + return [h.tbody(result_rows(UAs, test, result)) + for test, result in sorted(iteritems(results_by_test))] + + +def generate_html(UAs, results_by_test): + """Generate all the HTML output""" + return h(h.html( + h.head( + h.meta(charset="utf8"), + h.title("Implementation Report"), + h.link(href="report.css", rel="stylesheet")), + h.body( + h.h1("Implementation Report"), + h.h2("Summary"), + summary(UAs, results_by_test), + h.h2("Full Results"), + h.table( + h.thead( + h.tr( + h.th("Test"), + h.th("Subtest"), + [h.th(UA) for UA in sorted(UAs)])), + result_bodies(UAs, results_by_test))))) + + +def main(filenames): + data = load_data(filenames) + UAs, results_by_test = group_results(data) + return generate_html(UAs, results_by_test) + + +if __name__ == "__main__": + if not sys.argv[1:]: + print("""Please supply a list of UA name, filename pairs e.g. + +python report.py Firefox firefox.json Chrome chrome.json IE internet_explorer.json""") + print(main(sys.argv[1:])) |