Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright lowRISC contributors. |
| 3 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 4 | # SPDX-License-Identifier: Apache-2.0 |
Philipp Wagner | 14a3fee | 2019-11-21 10:07:02 +0000 | [diff] [blame] | 5 | r"""Command-line tool to parse and process testplan Hjson into a data structure |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 6 | The data structure is used for expansion inline within DV plan documentation |
| 7 | as well as for annotating the regression results. |
| 8 | """ |
| 9 | import logging as log |
| 10 | import os |
| 11 | import sys |
| 12 | from pathlib import PurePath |
| 13 | |
| 14 | import hjson |
| 15 | import mistletoe |
| 16 | |
| 17 | from .class_defs import * |
| 18 | |
| 19 | |
| 20 | def parse_testplan(filename): |
Philipp Wagner | 14a3fee | 2019-11-21 10:07:02 +0000 | [diff] [blame] | 21 | '''Parse testplan Hjson file into a datastructure''' |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 22 | self_path = os.path.dirname(os.path.realpath(__file__)) |
| 23 | repo_root = os.path.abspath(os.path.join(self_path, os.pardir, os.pardir)) |
| 24 | |
| 25 | name = "" |
| 26 | imported_testplans = [] |
| 27 | substitutions = [] |
| 28 | obj = parse_hjson(filename) |
| 29 | for key in obj.keys(): |
| 30 | if key == "import_testplans": |
| 31 | imported_testplans = obj[key] |
| 32 | elif key != "entries": |
| 33 | if key == "name": name = obj[key] |
| 34 | substitutions.append({key: obj[key]}) |
| 35 | for imported_testplan in imported_testplans: |
| 36 | obj = merge_dicts( |
| 37 | obj, parse_hjson(os.path.join(repo_root, imported_testplan))) |
| 38 | |
| 39 | testplan = Testplan(name=name) |
| 40 | for entry in obj["entries"]: |
| 41 | if not TestplanEntry.is_valid_entry(entry): sys.exit(1) |
Weicai Yang | 1130f5b | 2019-11-01 18:36:18 -0700 | [diff] [blame] | 42 | testplan_entry = TestplanEntry(name=entry["name"], |
| 43 | desc=entry["desc"], |
| 44 | milestone=entry["milestone"], |
| 45 | tests=entry["tests"], |
| 46 | substitutions=substitutions) |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 47 | testplan.add_entry(testplan_entry) |
| 48 | testplan.sort() |
| 49 | return testplan |
| 50 | |
| 51 | |
| 52 | def gen_html_indent(lvl): |
| 53 | return " " * lvl |
| 54 | |
| 55 | |
| 56 | def gen_html_write_style(outbuf): |
| 57 | outbuf.write("<style>\n") |
| 58 | outbuf.write("table.dv {\n") |
| 59 | outbuf.write(" border: 1px solid black;\n") |
| 60 | outbuf.write(" border-collapse: collapse;\n") |
| 61 | outbuf.write(" width: 100%;\n") |
| 62 | outbuf.write(" text-align: center;\n") |
| 63 | outbuf.write(" vertical-align: middle;\n") |
| 64 | outbuf.write(" margin-left: auto;;\n") |
| 65 | outbuf.write(" margin-right: auto;;\n") |
| 66 | outbuf.write(" display: table;\n") |
| 67 | outbuf.write("}\n") |
| 68 | outbuf.write("th, td {\n") |
| 69 | outbuf.write(" border: 1px solid black;\n") |
| 70 | outbuf.write("}\n") |
| 71 | outbuf.write("</style>\n") |
| 72 | |
| 73 | |
| 74 | def gen_html_testplan_table(testplan, outbuf): |
Philipp Wagner | 14a3fee | 2019-11-21 10:07:02 +0000 | [diff] [blame] | 75 | '''generate HTML table from testplan with the following fields |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 76 | milestone, planned test name, description |
| 77 | ''' |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 78 | def print_row(ms, name, desc, tests, cell, outbuf): |
| 79 | cellb = "<" + cell + ">" |
| 80 | celle = "</" + cell + ">" |
| 81 | tests_str = "" |
| 82 | if cell == "td": |
| 83 | # remove leading and trailing whitespaces |
| 84 | desc = mistletoe.markdown(desc.strip()) |
Weicai Yang | 1130f5b | 2019-11-01 18:36:18 -0700 | [diff] [blame] | 85 | tests_str = "<ul>\n" |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 86 | for test in tests: |
Weicai Yang | 1130f5b | 2019-11-01 18:36:18 -0700 | [diff] [blame] | 87 | tests_str += "<li>" + str(test) + "</li>\n" |
| 88 | tests_str += "</ul>" |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 89 | else: |
| 90 | tests_str = tests |
| 91 | |
| 92 | outbuf.write(gen_html_indent(1) + "<tr>\n") |
| 93 | outbuf.write(gen_html_indent(2) + cellb + ms + celle + "\n") |
| 94 | outbuf.write(gen_html_indent(2) + cellb + name + celle + "\n") |
| 95 | |
| 96 | # make description text left aligned |
| 97 | cellb_desc = cellb |
| 98 | if cell == "td": |
| 99 | cellb_desc = cellb_desc[:-1] + " style=\"text-align: left\"" + ">" |
| 100 | outbuf.write(gen_html_indent(2) + cellb_desc + desc + celle + "\n") |
| 101 | outbuf.write(gen_html_indent(2) + cellb + tests_str + celle + "\n") |
| 102 | outbuf.write(gen_html_indent(1) + "</tr>\n") |
| 103 | |
| 104 | gen_html_write_style(outbuf) |
| 105 | outbuf.write("<table class=\"dv\">\n") |
| 106 | print_row("Milestone", "Name", "Description", "Tests", "th", outbuf) |
| 107 | for entry in testplan.entries: |
| 108 | print_row(entry.milestone, entry.name, entry.desc, entry.tests, "td", |
| 109 | outbuf) |
| 110 | outbuf.write("</table>\n") |
| 111 | return |
| 112 | |
| 113 | |
| 114 | def gen_html_regr_results_table(testplan, regr_results, outbuf): |
| 115 | '''map regr results to testplan and create a table with the following fields |
| 116 | milestone, planned test name, actual written tests, pass/total |
| 117 | ''' |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 118 | def print_row(ms, name, tests, results, cell, outbuf): |
| 119 | cellb = "<" + cell + ">" |
| 120 | celle = "</" + cell + ">" |
| 121 | tests_str = "" |
| 122 | results_str = "" |
| 123 | if cell == "td": |
| 124 | for test in tests: |
| 125 | if tests_str != "": tests_str += "<br>" |
| 126 | if results_str != "": results_str += "<br>" |
| 127 | tests_str += str(test["name"]) |
| 128 | results_str += str(test["passing"]) + "/" + str(test["total"]) |
| 129 | else: |
| 130 | tests_str = tests |
| 131 | results_str = results |
Srikrishna Iyer | 7cf7cad | 2020-01-08 11:32:53 -0800 | [diff] [blame] | 132 | if ms == "N.A.": ms = "" |
| 133 | if name == "N.A.": |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 134 | name = "" |
| 135 | tests_str = "<strong>" + tests_str + "</strong>" |
| 136 | results_str = "<strong>" + results_str + "</strong>" |
| 137 | |
| 138 | outbuf.write(gen_html_indent(1) + "<tr>\n") |
| 139 | outbuf.write(gen_html_indent(2) + cellb + ms + celle + "\n") |
| 140 | outbuf.write(gen_html_indent(2) + cellb + name + celle + "\n") |
| 141 | outbuf.write(gen_html_indent(2) + cellb + tests_str + celle + "\n") |
| 142 | outbuf.write(gen_html_indent(2) + cellb + results_str + celle + "\n") |
| 143 | outbuf.write(gen_html_indent(1) + "</tr>\n") |
| 144 | |
| 145 | testplan.map_regr_results(regr_results["test_results"]) |
| 146 | |
| 147 | gen_html_write_style(outbuf) |
| 148 | outbuf.write("<h1 style=\"text-align: center\" " + "id=\"" + |
| 149 | testplan.name + "-regression-results\">" + |
| 150 | testplan.name.upper() + " Regression Results</h1>\n") |
| 151 | |
| 152 | outbuf.write("<h2 style=\"text-align: center\">" + "Run on " + |
| 153 | regr_results["timestamp"] + "</h2>\n") |
| 154 | |
| 155 | # test results |
| 156 | outbuf.write("<h3 style=\"text-align: center\">Test Results</h2>\n") |
| 157 | outbuf.write("<table class=\"dv\">\n") |
| 158 | print_row("Milestone", "Name", "Tests", "Results", "th", outbuf) |
| 159 | for entry in testplan.entries: |
| 160 | print_row(entry.milestone, entry.name, entry.tests, None, "td", outbuf) |
| 161 | outbuf.write("</table>\n") |
| 162 | |
| 163 | # coverage results |
| 164 | outbuf.write("<h3 style=\"text-align: center\">Coverage Results</h2>\n") |
| 165 | outbuf.write("<table class=\"dv\">\n") |
| 166 | outbuf.write(gen_html_indent(1) + "<tr>\n") |
| 167 | # title |
| 168 | outbuf.write(gen_html_indent(1) + "<tr>\n") |
| 169 | for cov in regr_results["cov_results"]: |
| 170 | outbuf.write( |
| 171 | gen_html_indent(2) + "<th>" + cov["name"].capitalize() + "</th>\n") |
| 172 | outbuf.write(gen_html_indent(1) + "</tr>\n") |
| 173 | # result |
| 174 | outbuf.write(gen_html_indent(1) + "<tr>\n") |
| 175 | for cov in regr_results["cov_results"]: |
| 176 | outbuf.write( |
| 177 | gen_html_indent(2) + "<td>" + str(cov["result"]) + "</td>\n") |
| 178 | outbuf.write(gen_html_indent(1) + "</tr>\n") |
| 179 | outbuf.write("</table>\n") |
| 180 | return |
| 181 | |
| 182 | |
| 183 | def parse_regr_results(filename): |
| 184 | obj = parse_hjson(filename) |
| 185 | # TODO need additional syntax checks |
| 186 | if not "test_results" in obj.keys(): |
| 187 | print("Error: key \'test_results\' not found") |
| 188 | sys, exit(1) |
| 189 | return obj |
| 190 | |
| 191 | |
| 192 | def parse_hjson(filename): |
| 193 | try: |
Philipp Wagner | 57df62c | 2019-11-04 14:49:24 +0000 | [diff] [blame] | 194 | f = open(str(filename), 'rU') |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 195 | text = f.read() |
| 196 | odict = hjson.loads(text) |
| 197 | return odict |
| 198 | except IOError: |
| 199 | print('IO Error:', filename) |
| 200 | raise SystemExit(sys.exc_info()[1]) |
Philipp Wagner | e9dcfbb | 2019-11-14 13:29:43 +0000 | [diff] [blame] | 201 | except hjson.scanner.HjsonDecodeError as e: |
| 202 | print("Error: Unable to decode HJSON file %s: %s" % (str(filename), str(e))) |
| 203 | sys.exit(1) |
Srikrishna Iyer | d61724d | 2019-10-11 10:47:49 -0700 | [diff] [blame] | 204 | |
| 205 | |
| 206 | def merge_dicts(list1, list2): |
| 207 | '''merge 2 dicts into one |
| 208 | |
| 209 | This funciton takes 2 dicts as args list1 and list2. It recursively merges list2 into |
| 210 | list1 and returns list1. The recursion happens when the the value of a key in both lists |
| 211 | is a dict. If the values of the same key in both lists (at the same tree level) are of |
| 212 | type str (or of dissimilar type) then there is a conflict, and and error is thrown. |
| 213 | ''' |
| 214 | for key in list2.keys(): |
| 215 | if key in list1: |
| 216 | if type(list1[key]) is list and type(list2[key]) is list: |
| 217 | list1[key].extend(list2[key]) |
| 218 | elif type(list1[key]) is dict and type(list2[key]) is dict: |
| 219 | list1[key] = merge_dicts(list1[key], list2[key]) |
| 220 | else: |
| 221 | print("The type of value of key ", key, "in list1: ", str(type(list1[key])), \ |
| 222 | " does not match the type of value in list2: ", str(type(list2[key])), \ |
| 223 | " or they are not of type list or dict. The two lists cannot be merged.") |
| 224 | sys.exit(1) |
| 225 | else: |
| 226 | list1[key] = list2[key] |
| 227 | return list1 |
| 228 | |
| 229 | |
| 230 | def gen_html(testplan_file, regr_results_file, outbuf): |
| 231 | testplan = parse_testplan(os.path.abspath(testplan_file)) |
| 232 | if regr_results_file: |
| 233 | regr_results = parse_regr_results(os.path.abspath(regr_results_file)) |
| 234 | gen_html_regr_results_table(testplan, regr_results, outbuf) |
| 235 | else: |
| 236 | gen_html_testplan_table(testplan, outbuf) |
| 237 | outbuf.write('\n') |