| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| r"""Command-line tool to parse and process testplan Hjson into a data structure |
| The data structure is used for expansion inline within DV plan documentation |
| as well as for annotating the regression results. |
| """ |
| import os |
| import sys |
| |
| import hjson |
| import mistletoe |
| from tabulate import tabulate |
| |
| from .class_defs import Testplan, TestplanEntry |
| |
| |
| def parse_testplan(filename): |
| '''Parse testplan Hjson file into a datastructure''' |
| self_path = os.path.dirname(os.path.realpath(__file__)) |
| repo_root = os.path.abspath(os.path.join(self_path, os.pardir, os.pardir, os.pardir)) |
| |
| name = "" |
| imported_testplans = [] |
| substitutions = [] |
| obj = parse_hjson(filename) |
| for key in obj.keys(): |
| if key == "import_testplans": |
| imported_testplans = obj[key] |
| elif key != "entries": |
| if key == "name": |
| name = obj[key] |
| substitutions.append({key: obj[key]}) |
| for imported_testplan in imported_testplans: |
| obj = merge_dicts( |
| obj, parse_hjson(os.path.join(repo_root, imported_testplan))) |
| |
| testplan = Testplan(name=name) |
| for entry in obj["entries"]: |
| if not TestplanEntry.is_valid_entry(entry): |
| sys.exit(1) |
| testplan_entry = TestplanEntry(name=entry["name"], |
| desc=entry["desc"], |
| milestone=entry["milestone"], |
| tests=entry["tests"], |
| substitutions=substitutions) |
| testplan.add_entry(testplan_entry) |
| testplan.sort() |
| return testplan |
| |
| |
| def gen_html_indent(lvl): |
| return " " * lvl |
| |
| |
| def gen_html_write_style(outbuf): |
| outbuf.write("<style>\n") |
| outbuf.write("table.dv {\n") |
| outbuf.write(" border: 1px solid black;\n") |
| outbuf.write(" border-collapse: collapse;\n") |
| outbuf.write(" text-align: left;\n") |
| outbuf.write(" vertical-align: middle;\n") |
| outbuf.write(" display: table;\n") |
| outbuf.write("}\n") |
| outbuf.write("th, td {\n") |
| outbuf.write(" border: 1px solid black;\n") |
| outbuf.write("}\n") |
| outbuf.write("</style>\n") |
| |
| |
| def gen_html_testplan_table(testplan, outbuf): |
| '''generate HTML table from testplan with the following fields |
| milestone, planned test name, description |
| ''' |
| |
| text = testplan.testplan_table(fmt="html") |
| text = text.replace("<table>", "<table class=\"dv\">") |
| gen_html_write_style(outbuf) |
| outbuf.write(text) |
| return |
| |
| |
| def gen_html_regr_results_table(testplan, regr_results, outbuf): |
| '''map regr results to testplan and create a table with the following fields |
| milestone, planned test name, actual written tests, pass/total |
| ''' |
| text = "# Regression Results\n" |
| text += "## Run on{}\n".format(regr_results["timestamp"]) |
| text += "### Test Results\n\n" |
| text += testplan.results_table(regr_results["test_results"]) |
| if "cov_results" in regr_results.keys(): |
| text += "\n### Coverage Results\n\n" |
| cov_header = [] |
| cov_values = [] |
| for cov in regr_results["cov_results"]: |
| cov_header.append(cov["name"]) |
| cov_values.append(str(cov["result"])) |
| colalign = (("center", ) * len(cov_header)) |
| text += tabulate([cov_header, cov_values], |
| headers="firstrow", |
| tablefmt="pipe", |
| colalign=colalign) |
| text = mistletoe.markdown(text) |
| text = text.replace("<table>", "<table class=\"dv\">") |
| gen_html_write_style(outbuf) |
| outbuf.write(text) |
| return |
| |
| |
| def parse_regr_results(filename): |
| obj = parse_hjson(filename) |
| # TODO need additional syntax checks |
| if "test_results" not in obj.keys(): |
| print("Error: key \'test_results\' not found") |
| sys, exit(1) |
| return obj |
| |
| |
| def parse_hjson(filename): |
| try: |
| f = open(str(filename), 'rU') |
| text = f.read() |
| odict = hjson.loads(text) |
| return odict |
| except IOError: |
| print('IO Error:', filename) |
| raise SystemExit(sys.exc_info()[1]) |
| except hjson.scanner.HjsonDecodeError as e: |
| print("Error: Unable to decode HJSON file %s: %s" % |
| (str(filename), str(e))) |
| sys.exit(1) |
| |
| |
| def merge_dicts(list1, list2, use_list1_for_defaults=True): |
| '''Merge 2 dicts into one |
| |
| This function takes 2 dicts as args list1 and list2. It recursively merges list2 into |
| list1 and returns list1. The recursion happens when the the value of a key in both lists |
| is a dict. If the values of the same key in both lists (at the same tree level) are of |
| dissimilar type, then there is a conflict and an error is thrown. If they are of the same |
| scalar type, then the third arg "use_list1_for_defaults" is used to pick the final one. |
| ''' |
| for key, item2 in list2.items(): |
| item1 = list1.get(key) |
| if item1 is None: |
| list1[key] = item2 |
| continue |
| |
| # Both dictionaries have an entry for this key. Are they both lists? If |
| # so, append. |
| if isinstance(item1, list) and isinstance(item2, list): |
| list1[key] = item1 + item2 |
| continue |
| |
| # Are they both dictionaries? If so, recurse. |
| if isinstance(item1, dict) and isinstance(item2, dict): |
| merge_dicts(item1, item2) |
| continue |
| |
| # We treat other types as atoms. If the types of the two items are |
| # equal pick one or the other (based on use_list1_for_defaults). |
| if isinstance(item1, type(item2)) and isinstance(item2, type(item1)): |
| list1[key] = item1 if use_list1_for_defaults else item2 |
| continue |
| |
| # Oh no! We can't merge this. |
| print("ERROR: Cannot merge dictionaries at key {!r} because items have " |
| "conflicting types ({} in 1st; {} in 2nd)." |
| .format(type(item1), type(item2))) |
| sys.exit(1) |
| |
| return list1 |
| |
| |
| def gen_html(testplan_file, regr_results_file, outbuf): |
| testplan = parse_testplan(os.path.abspath(testplan_file)) |
| if regr_results_file: |
| regr_results = parse_regr_results(os.path.abspath(regr_results_file)) |
| gen_html_regr_results_table(testplan, regr_results, outbuf) |
| else: |
| gen_html_testplan_table(testplan, outbuf) |
| outbuf.write('\n') |