Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 1 | # Copyright lowRISC contributors. |
| 2 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 3 | # SPDX-License-Identifier: Apache-2.0 |
| 4 | r""" |
| 5 | Utility functions common across dvsim. |
| 6 | """ |
| 7 | |
| 8 | import logging as log |
| 9 | import os |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 10 | import re |
| 11 | import shlex |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 12 | import shutil |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 13 | import subprocess |
| 14 | import sys |
| 15 | import time |
| 16 | from collections import OrderedDict |
Srikrishna Iyer | caa0d89 | 2021-03-01 13:15:52 -0800 | [diff] [blame] | 17 | from datetime import datetime |
| 18 | from pathlib import Path |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 19 | |
| 20 | import hjson |
Srikrishna Iyer | f578e7c | 2020-01-29 13:11:58 -0800 | [diff] [blame] | 21 | import mistletoe |
Cindy Chen | 1aff665 | 2020-04-23 18:49:18 -0700 | [diff] [blame] | 22 | from premailer import transform |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 23 | |
| 24 | # For verbose logging |
| 25 | VERBOSE = 15 |
| 26 | |
Srikrishna Iyer | 2b944ad | 2021-03-01 13:05:40 -0800 | [diff] [blame] | 27 | # Timestamp format when creating directory backups. |
| 28 | TS_FORMAT = "%y.%m.%d_%H.%M.%S" |
| 29 | |
| 30 | # Timestamp format when generating reports. |
| 31 | TS_FORMAT_LONG = "%A %B %d %Y %H:%M:%S UTC" |
| 32 | |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 33 | |
| 34 | # Run a command and get the result. Exit with error if the command did not |
| 35 | # succeed. This is a simpler version of the run_cmd function below. |
| 36 | def run_cmd(cmd): |
| 37 | (status, output) = subprocess.getstatusoutput(cmd) |
| 38 | if status: |
| 39 | sys.stderr.write("cmd " + cmd + " returned with status " + str(status)) |
| 40 | sys.exit(status) |
| 41 | return output |
| 42 | |
| 43 | |
| 44 | # Run a command with a specified timeout. If the command does not finish before |
| 45 | # the timeout, then it returns -1. Else it returns the command output. If the |
| 46 | # command fails, it throws an exception and returns the stderr. |
| 47 | def run_cmd_with_timeout(cmd, timeout=-1, exit_on_failure=1): |
| 48 | args = shlex.split(cmd) |
| 49 | p = subprocess.Popen(args, |
| 50 | stdout=subprocess.PIPE, |
| 51 | stderr=subprocess.STDOUT) |
| 52 | |
| 53 | # If timeout is set, poll for the process to finish until timeout |
| 54 | result = "" |
| 55 | status = -1 |
| 56 | if timeout == -1: |
| 57 | p.wait() |
| 58 | else: |
| 59 | start = time.time() |
| 60 | while time.time() - start < timeout: |
| 61 | if p.poll() is not None: |
| 62 | break |
Srikrishna Iyer | 544da8d | 2020-01-14 23:51:41 -0800 | [diff] [blame] | 63 | time.sleep(.01) |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 64 | |
| 65 | # Capture output and status if cmd exited, else kill it |
| 66 | if p.poll() is not None: |
| 67 | result = p.communicate()[0] |
| 68 | status = p.returncode |
| 69 | else: |
| 70 | log.error("cmd \"%s\" timed out!", cmd) |
| 71 | p.kill() |
| 72 | |
| 73 | if status != 0: |
| 74 | log.error("cmd \"%s\" exited with status %d", cmd, status) |
Rupert Swarbrick | 6cc2011 | 2020-04-24 09:44:35 +0100 | [diff] [blame] | 75 | if exit_on_failure == 1: |
| 76 | sys.exit(status) |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 77 | |
| 78 | return (result, status) |
| 79 | |
| 80 | |
Srikrishna Iyer | 544da8d | 2020-01-14 23:51:41 -0800 | [diff] [blame] | 81 | # Parse hjson and return a dict |
| 82 | def parse_hjson(hjson_file): |
| 83 | hjson_cfg_dict = None |
| 84 | try: |
| 85 | log.debug("Parsing %s", hjson_file) |
| 86 | f = open(hjson_file, 'rU') |
| 87 | text = f.read() |
| 88 | hjson_cfg_dict = hjson.loads(text, use_decimal=True) |
| 89 | f.close() |
Srikrishna Iyer | f578e7c | 2020-01-29 13:11:58 -0800 | [diff] [blame] | 90 | except Exception as e: |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 91 | log.fatal( |
| 92 | "Failed to parse \"%s\" possibly due to bad path or syntax error.\n%s", |
| 93 | hjson_file, e) |
Srikrishna Iyer | 544da8d | 2020-01-14 23:51:41 -0800 | [diff] [blame] | 94 | sys.exit(1) |
| 95 | return hjson_cfg_dict |
| 96 | |
| 97 | |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 98 | def _stringify_wildcard_value(value): |
| 99 | '''Make sense of a wildcard value as a string (see subst_wildcards) |
| 100 | |
| 101 | Strings are passed through unchanged. Integer or boolean values are printed |
| 102 | as numerical strings. Lists or other sequences have their items printed |
| 103 | separated by spaces. |
| 104 | |
| 105 | ''' |
| 106 | if type(value) is str: |
| 107 | return value |
| 108 | |
| 109 | if type(value) in [bool, int]: |
| 110 | return str(int(value)) |
| 111 | |
| 112 | try: |
| 113 | return ' '.join(_stringify_wildcard_value(x) for x in value) |
| 114 | except TypeError: |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 115 | raise ValueError('Wildcard had value {!r} which is not of a supported ' |
| 116 | 'type.'.format(value)) |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 117 | |
| 118 | |
| 119 | def _subst_wildcards(var, mdict, ignored, ignore_error, seen): |
| 120 | '''Worker function for subst_wildcards |
| 121 | |
| 122 | seen is a list of wildcards that have been expanded on the way to this call |
| 123 | (used for spotting circular recursion). |
| 124 | |
| 125 | Returns (expanded, seen_err) where expanded is the new value of the string |
| 126 | and seen_err is true if we stopped early because of an ignored error. |
| 127 | |
| 128 | ''' |
| 129 | wildcard_re = re.compile(r"{([A-Za-z0-9\_]+)}") |
| 130 | |
| 131 | # Work from left to right, expanding each wildcard we find. idx is where we |
| 132 | # should start searching (so that we don't keep finding a wildcard that |
| 133 | # we've decided to ignore). |
| 134 | idx = 0 |
| 135 | |
| 136 | any_err = False |
| 137 | |
| 138 | while True: |
| 139 | right_str = var[idx:] |
| 140 | match = wildcard_re.search(right_str) |
| 141 | |
| 142 | # If no match, we're done. |
| 143 | if match is None: |
| 144 | return (var, any_err) |
| 145 | |
| 146 | name = match.group(1) |
| 147 | |
| 148 | # If the name should be ignored, skip over it. |
| 149 | if name in ignored: |
| 150 | idx += match.end() |
| 151 | continue |
| 152 | |
| 153 | # If the name has been seen already, we've spotted circular recursion. |
| 154 | # That's not allowed! |
| 155 | if name in seen: |
| 156 | raise ValueError('String contains circular expansion of ' |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 157 | 'wildcard {!r}.'.format(match.group(0))) |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 158 | |
| 159 | # Treat eval_cmd specially |
| 160 | if name == 'eval_cmd': |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 161 | cmd = _subst_wildcards(right_str[match.end():], mdict, ignored, |
| 162 | ignore_error, seen)[0] |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 163 | |
| 164 | # Are there any wildcards left in cmd? If not, we can run the |
| 165 | # command and we're done. |
| 166 | cmd_matches = list(wildcard_re.finditer(cmd)) |
| 167 | if not cmd_matches: |
Srikrishna Iyer | 8ca91da | 2021-03-30 23:45:30 -0700 | [diff] [blame] | 168 | var = var[:match.start()] + run_cmd(cmd) |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 169 | continue |
| 170 | |
| 171 | # Otherwise, check that each of them is ignored, or that |
| 172 | # ignore_error is True. |
| 173 | bad_names = False |
| 174 | if not ignore_error: |
| 175 | for cmd_match in cmd_matches: |
| 176 | if cmd_match.group(1) not in ignored: |
| 177 | bad_names = True |
| 178 | |
| 179 | if bad_names: |
| 180 | raise ValueError('Cannot run eval_cmd because the command ' |
| 181 | 'expands to {!r}, which still contains a ' |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 182 | 'wildcard.'.format(cmd)) |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 183 | |
| 184 | # We can't run the command (because it still has wildcards), but we |
| 185 | # don't want to report an error either because ignore_error is true |
| 186 | # or because each wildcard that's left is ignored. Return the |
| 187 | # partially evaluated version. |
| 188 | return (var[:idx] + right_str[:match.end()] + cmd, True) |
| 189 | |
| 190 | # Otherwise, look up name in mdict. |
| 191 | value = mdict.get(name) |
| 192 | |
| 193 | # If the value isn't set, check the environment |
| 194 | if value is None: |
| 195 | value = os.environ.get(name) |
| 196 | |
| 197 | if value is None: |
| 198 | # Ignore missing values if ignore_error is True. |
| 199 | if ignore_error: |
| 200 | idx += match.end() |
| 201 | continue |
| 202 | |
| 203 | raise ValueError('String to be expanded contains ' |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 204 | 'unknown wildcard, {!r}.'.format(match.group(0))) |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 205 | |
| 206 | value = _stringify_wildcard_value(value) |
| 207 | |
| 208 | # Do any recursive expansion of value, adding name to seen (to avoid |
| 209 | # circular recursion). |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 210 | value, saw_err = _subst_wildcards(value, mdict, ignored, ignore_error, |
| 211 | seen + [name]) |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 212 | |
| 213 | # Replace the original match with the result and go around again. If |
| 214 | # saw_err, increment idx past what we just inserted. |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 215 | var = (var[:idx] + right_str[:match.start()] + value + |
| 216 | right_str[match.end():]) |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 217 | if saw_err: |
| 218 | any_err = True |
| 219 | idx += match.start() + len(value) |
| 220 | |
| 221 | |
Srikrishna Iyer | 86f6bce | 2020-02-27 19:02:04 -0800 | [diff] [blame] | 222 | def subst_wildcards(var, mdict, ignored_wildcards=[], ignore_error=False): |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 223 | '''Substitute any "wildcard" variables in the string var. |
| 224 | |
| 225 | var is the string to be substituted. mdict is a dictionary mapping |
| 226 | variables to strings. ignored_wildcards is a list of wildcards that |
| 227 | shouldn't be substituted. ignore_error means to partially evaluate rather |
| 228 | than exit on an error. |
| 229 | |
| 230 | A wildcard is written as a name (alphanumeric, allowing backslash and |
| 231 | underscores) surrounded by braces. For example, |
| 232 | |
| 233 | subst_wildcards('foo {x} baz', {'x': 'bar'}) |
| 234 | |
| 235 | returns "foo bar baz". Dictionary values can be strings, booleans, integers |
| 236 | or lists. For example: |
| 237 | |
| 238 | subst_wildcards('{a}, {b}, {c}, {d}', |
| 239 | {'a': 'a', 'b': True, 'c': 42, 'd': ['a', 10]}) |
| 240 | |
| 241 | returns 'a, 1, 42, a 10'. |
| 242 | |
| 243 | If a wildcard is in ignored_wildcards, it is ignored. For example, |
| 244 | |
| 245 | subst_wildcards('{a} {b}', {'b': 'bee'}, ignored_wildcards=['a']) |
| 246 | |
| 247 | returns "{a} bee". |
| 248 | |
| 249 | If a wildcard appears in var but is not in mdict, the environment is |
| 250 | checked for the variable. If the name still isn't found, the default |
| 251 | behaviour is to log an error and exit. If ignore_error is True, the |
| 252 | wildcard is ignored (as if it appeared in ignore_wildcards). |
| 253 | |
| 254 | If {eval_cmd} appears in the string and 'eval_cmd' is not in |
| 255 | ignored_wildcards then the following text is recursively expanded. The |
| 256 | result of this expansion is treated as a command to run and the text is |
| 257 | replaced by the output of the command. |
| 258 | |
| 259 | If a wildcard has been ignored (either because of ignored_wildcards or |
| 260 | ignore_error), the command to run in eval_cmd might contain a match for |
| 261 | wildcard_re. If ignore_error is True, the command is not run. So |
| 262 | |
| 263 | subst_wildcards('{eval_cmd}{foo}', {}, ignore_error=True) |
| 264 | |
| 265 | will return '{eval_cmd}{foo}' unchanged. If ignore_error is False, the |
| 266 | function logs an error and exits. |
| 267 | |
| 268 | Recursion is possible in subst_wildcards. For example, |
| 269 | |
| 270 | subst_wildcards('{a}', {'a': '{b}', 'b': 'c'}) |
| 271 | |
| 272 | returns 'c'. Circular recursion is detected, however. So |
| 273 | |
| 274 | subst_wildcards('{a}', {'a': '{b}', 'b': '{a}'}) |
| 275 | |
| 276 | will log an error and exit. This error is raised whether or not |
| 277 | ignore_error is set. |
| 278 | |
| 279 | Since subst_wildcards works from left to right, it's possible to compute |
| 280 | wildcard names with code like this: |
| 281 | |
| 282 | subst_wildcards('{a}b}', {'a': 'a {', 'b': 'bee'}) |
| 283 | |
| 284 | which returns 'a bee'. This is pretty hard to read though, so is probably |
| 285 | not a good idea to use. |
| 286 | |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 287 | ''' |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 288 | try: |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 289 | return _subst_wildcards(var, mdict, ignored_wildcards, ignore_error, |
| 290 | [])[0] |
Rupert Swarbrick | 592cd5d | 2020-06-10 16:45:03 +0100 | [diff] [blame] | 291 | except ValueError as err: |
| 292 | log.error(str(err)) |
| 293 | sys.exit(1) |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 294 | |
| 295 | |
Srikrishna Iyer | 86f6bce | 2020-02-27 19:02:04 -0800 | [diff] [blame] | 296 | def find_and_substitute_wildcards(sub_dict, |
| 297 | full_dict, |
| 298 | ignored_wildcards=[], |
| 299 | ignore_error=False): |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 300 | ''' |
| 301 | Recursively find key values containing wildcards in sub_dict in full_dict |
| 302 | and return resolved sub_dict. |
| 303 | ''' |
| 304 | for key in sub_dict.keys(): |
| 305 | if type(sub_dict[key]) in [dict, OrderedDict]: |
| 306 | # Recursively call this funciton in sub-dicts |
| 307 | sub_dict[key] = find_and_substitute_wildcards( |
Srikrishna Iyer | 86f6bce | 2020-02-27 19:02:04 -0800 | [diff] [blame] | 308 | sub_dict[key], full_dict, ignored_wildcards, ignore_error) |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 309 | |
| 310 | elif type(sub_dict[key]) is list: |
| 311 | sub_dict_key_values = list(sub_dict[key]) |
| 312 | # Loop through the list of key's values and substitute each var |
| 313 | # in case it contains a wildcard |
| 314 | for i in range(len(sub_dict_key_values)): |
| 315 | if type(sub_dict_key_values[i]) in [dict, OrderedDict]: |
| 316 | # Recursively call this funciton in sub-dicts |
| 317 | sub_dict_key_values[i] = \ |
| 318 | find_and_substitute_wildcards(sub_dict_key_values[i], |
Srikrishna Iyer | 86f6bce | 2020-02-27 19:02:04 -0800 | [diff] [blame] | 319 | full_dict, ignored_wildcards, ignore_error) |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 320 | |
| 321 | elif type(sub_dict_key_values[i]) is str: |
| 322 | sub_dict_key_values[i] = subst_wildcards( |
Srikrishna Iyer | 86f6bce | 2020-02-27 19:02:04 -0800 | [diff] [blame] | 323 | sub_dict_key_values[i], full_dict, ignored_wildcards, |
| 324 | ignore_error) |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 325 | |
| 326 | # Set the substituted key values back |
| 327 | sub_dict[key] = sub_dict_key_values |
| 328 | |
| 329 | elif type(sub_dict[key]) is str: |
| 330 | sub_dict[key] = subst_wildcards(sub_dict[key], full_dict, |
Srikrishna Iyer | 86f6bce | 2020-02-27 19:02:04 -0800 | [diff] [blame] | 331 | ignored_wildcards, ignore_error) |
Srikrishna Iyer | 09a81e9 | 2019-12-30 10:47:57 -0800 | [diff] [blame] | 332 | return sub_dict |
Srikrishna Iyer | f578e7c | 2020-01-29 13:11:58 -0800 | [diff] [blame] | 333 | |
| 334 | |
Cindy Chen | 1aff665 | 2020-04-23 18:49:18 -0700 | [diff] [blame] | 335 | def md_results_to_html(title, css_file, md_text): |
Srikrishna Iyer | f578e7c | 2020-01-29 13:11:58 -0800 | [diff] [blame] | 336 | '''Convert results in md format to html. Add a little bit of styling. |
| 337 | ''' |
| 338 | html_text = "<!DOCTYPE html>\n" |
| 339 | html_text += "<html lang=\"en\">\n" |
| 340 | html_text += "<head>\n" |
| 341 | if title != "": |
| 342 | html_text += " <title>{}</title>\n".format(title) |
Srikrishna Iyer | f578e7c | 2020-01-29 13:11:58 -0800 | [diff] [blame] | 343 | html_text += "</head>\n" |
| 344 | html_text += "<body>\n" |
| 345 | html_text += "<div class=\"results\">\n" |
| 346 | html_text += mistletoe.markdown(md_text) |
| 347 | html_text += "</div>\n" |
| 348 | html_text += "</body>\n" |
| 349 | html_text += "</html>\n" |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 350 | html_text = htmc_color_pc_cells(html_text) |
Cindy Chen | 1aff665 | 2020-04-23 18:49:18 -0700 | [diff] [blame] | 351 | # this function converts css style to inline html style |
Cindy Chen | 1aff665 | 2020-04-23 18:49:18 -0700 | [diff] [blame] | 352 | html_text = transform(html_text, |
| 353 | external_styles=css_file, |
Cindy Chen | ae19bec | 2020-05-01 10:24:52 -0700 | [diff] [blame] | 354 | cssutils_logging_level=log.ERROR) |
Srikrishna Iyer | f578e7c | 2020-01-29 13:11:58 -0800 | [diff] [blame] | 355 | return html_text |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 356 | |
| 357 | |
| 358 | def htmc_color_pc_cells(text): |
Michael Schaffner | 3d16099 | 2020-03-31 18:37:53 -0700 | [diff] [blame] | 359 | '''This function finds cells in a html table that contain numerical values |
| 360 | (and a few known strings) followed by a single space and an identifier. |
| 361 | Depending on the identifier, it shades the cell in a specific way. A set of |
| 362 | 12 color palettes for setting those shades are encoded in ./style.css. |
| 363 | These are 'cna' (grey), 'c0' (red), 'c1' ... 'c10' (green). The shade 'cna' |
| 364 | is used for items that are maked as 'not applicable'. The shades 'c1' to |
| 365 | 'c9' form a gradient from red to lime-green to indicate 'levels of |
| 366 | completeness'. 'cna' is used for greying out a box for 'not applicable' |
| 367 | items, 'c0' is for items that are considered risky (or not yet started) and |
| 368 | 'c10' for items that have completed successfully, or that are |
| 369 | 'in good standing'. |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 370 | |
Michael Schaffner | 3d16099 | 2020-03-31 18:37:53 -0700 | [diff] [blame] | 371 | These are the supported identifiers: %, %u, G, B, E, W, EN, WN. |
| 372 | The shading behavior for these is described below. |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 373 | |
Michael Schaffner | 3d16099 | 2020-03-31 18:37:53 -0700 | [diff] [blame] | 374 | %: Coloured percentage, where the number in front of the '%' sign is mapped |
| 375 | to a color for the cell ranging from red ('c0') to green ('c10'). |
| 376 | %u: Uncoloured percentage, where no markup is applied and '%u' is replaced |
| 377 | with '%' in the output. |
| 378 | G: This stands for 'Good' and results in a green cell. |
| 379 | B: This stands for 'Bad' and results in a red cell. |
| 380 | E: This stands for 'Errors' and the cell is colored with red if the number |
| 381 | in front of the indicator is larger than 0. Otherwise the cell is |
| 382 | colored with green. |
| 383 | W: This stands for 'Warnings' and the cell is colored with yellow ('c6') |
| 384 | if the number in front of the indicator is larger than 0. Otherwise |
| 385 | the cell is colored with green. |
| 386 | EN: This stands for 'Errors Negative', which behaves the same as 'E' except |
| 387 | that the cell is colored red if the number in front of the indicator is |
| 388 | negative. |
| 389 | WN: This stands for 'Warnings Negative', which behaves the same as 'W' |
| 390 | except that the cell is colored yellow if the number in front of the |
| 391 | indicator is negative. |
| 392 | |
| 393 | N/A items can have any of the following indicators and need not be |
| 394 | preceeded with a numerical value: |
| 395 | |
| 396 | '--', 'NA', 'N.A.', 'N.A', 'N/A', 'na', 'n.a.', 'n.a', 'n/a' |
| 397 | |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 398 | ''' |
| 399 | |
| 400 | # Replace <td> with <td class="color-class"> based on the fp |
| 401 | # value. "color-classes" are listed in ./style.css as follows: "cna" |
| 402 | # for NA value, "c0" to "c10" for fp value falling between 0.00-9.99, |
| 403 | # 10.00-19.99 ... 90.00-99.99, 100.0 respetively. |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 404 | def color_cell(cell, cclass, indicator="%"): |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 405 | op = cell.replace("<td", "<td class=\"" + cclass + "\"") |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 406 | # Remove the indicator. |
Rupert Swarbrick | 6cc2011 | 2020-04-24 09:44:35 +0100 | [diff] [blame] | 407 | op = re.sub(r"\s*" + indicator + r"\s*", "", op) |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 408 | return op |
| 409 | |
| 410 | # List of 'not applicable' identifiers. |
Srikrishna Iyer | 86f6bce | 2020-02-27 19:02:04 -0800 | [diff] [blame] | 411 | na_list = ['--', 'NA', 'N.A.', 'N.A', 'N/A', 'na', 'n.a.', 'n.a', 'n/a'] |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 412 | na_list_patterns = '|'.join(na_list) |
| 413 | |
| 414 | # List of floating point patterns: '0', '0.0' & '.0' |
Rupert Swarbrick | 6cc2011 | 2020-04-24 09:44:35 +0100 | [diff] [blame] | 415 | fp_patterns = r"[\+\-]?\d+\.?\d*" |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 416 | |
| 417 | patterns = fp_patterns + '|' + na_list_patterns |
Michael Schaffner | 3d16099 | 2020-03-31 18:37:53 -0700 | [diff] [blame] | 418 | indicators = "%|%u|G|B|E|W|EN|WN" |
Srikrishna Iyer | c93b483 | 2020-08-06 17:54:16 -0700 | [diff] [blame] | 419 | match = re.findall( |
| 420 | r"(<td.*>\s*(" + patterns + r")\s+(" + indicators + r")\s*</td>)", |
| 421 | text) |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 422 | if len(match) > 0: |
| 423 | subst_list = {} |
| 424 | fp_nums = [] |
| 425 | for item in match: |
| 426 | # item is a tuple - first is the full string indicating the table |
| 427 | # cell which we want to replace, second is the floating point value. |
| 428 | cell = item[0] |
| 429 | fp_num = item[1] |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 430 | indicator = item[2] |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 431 | # Skip if fp_num is already processed. |
Rupert Swarbrick | 6cc2011 | 2020-04-24 09:44:35 +0100 | [diff] [blame] | 432 | if (fp_num, indicator) in fp_nums: |
| 433 | continue |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 434 | fp_nums.append((fp_num, indicator)) |
Rupert Swarbrick | 6cc2011 | 2020-04-24 09:44:35 +0100 | [diff] [blame] | 435 | if fp_num in na_list: |
| 436 | subst = color_cell(cell, "cna", indicator) |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 437 | else: |
| 438 | # Item is a fp num. |
| 439 | try: |
| 440 | fp = float(fp_num) |
| 441 | except ValueError: |
Srikrishna Iyer | c93b483 | 2020-08-06 17:54:16 -0700 | [diff] [blame] | 442 | log.error( |
| 443 | "Percentage item \"%s\" in cell \"%s\" is not an " |
| 444 | "integer or a floating point number", fp_num, cell) |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 445 | continue |
Michael Schaffner | 3d16099 | 2020-03-31 18:37:53 -0700 | [diff] [blame] | 446 | # Percentage, colored. |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 447 | if indicator == "%": |
Rupert Swarbrick | 6cc2011 | 2020-04-24 09:44:35 +0100 | [diff] [blame] | 448 | if fp >= 0.0 and fp < 10.0: |
| 449 | subst = color_cell(cell, "c0") |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 450 | elif fp >= 10.0 and fp < 20.0: |
| 451 | subst = color_cell(cell, "c1") |
| 452 | elif fp >= 20.0 and fp < 30.0: |
| 453 | subst = color_cell(cell, "c2") |
| 454 | elif fp >= 30.0 and fp < 40.0: |
| 455 | subst = color_cell(cell, "c3") |
| 456 | elif fp >= 40.0 and fp < 50.0: |
| 457 | subst = color_cell(cell, "c4") |
| 458 | elif fp >= 50.0 and fp < 60.0: |
| 459 | subst = color_cell(cell, "c5") |
| 460 | elif fp >= 60.0 and fp < 70.0: |
| 461 | subst = color_cell(cell, "c6") |
| 462 | elif fp >= 70.0 and fp < 80.0: |
| 463 | subst = color_cell(cell, "c7") |
| 464 | elif fp >= 80.0 and fp < 90.0: |
| 465 | subst = color_cell(cell, "c8") |
| 466 | elif fp >= 90.0 and fp < 100.0: |
| 467 | subst = color_cell(cell, "c9") |
| 468 | elif fp >= 100.0: |
| 469 | subst = color_cell(cell, "c10") |
Michael Schaffner | 3d16099 | 2020-03-31 18:37:53 -0700 | [diff] [blame] | 470 | # Percentage, uncolored. |
| 471 | elif indicator == "%u": |
| 472 | subst = cell.replace("%u", "%") |
| 473 | # Good: green |
| 474 | elif indicator == "G": |
| 475 | subst = color_cell(cell, "c10", indicator) |
| 476 | # Bad: red |
| 477 | elif indicator == "B": |
| 478 | subst = color_cell(cell, "c0", indicator) |
| 479 | # Bad if positive: red for errors, yellow for warnings, |
| 480 | # otherwise green. |
| 481 | elif indicator in ["E", "W"]: |
| 482 | if fp <= 0: |
Srikrishna Iyer | 0596a85 | 2020-03-02 11:53:31 -0800 | [diff] [blame] | 483 | subst = color_cell(cell, "c10", indicator) |
| 484 | elif indicator == "W": |
| 485 | subst = color_cell(cell, "c6", indicator) |
| 486 | elif indicator == "E": |
| 487 | subst = color_cell(cell, "c0", indicator) |
Michael Schaffner | 3d16099 | 2020-03-31 18:37:53 -0700 | [diff] [blame] | 488 | # Bad if negative: red for errors, yellow for warnings, |
| 489 | # otherwise green. |
| 490 | elif indicator in ["EN", "WN"]: |
| 491 | if fp >= 0: |
| 492 | subst = color_cell(cell, "c10", indicator) |
| 493 | elif indicator == "WN": |
| 494 | subst = color_cell(cell, "c6", indicator) |
| 495 | elif indicator == "EN": |
| 496 | subst = color_cell(cell, "c0", indicator) |
Srikrishna Iyer | 2a710a4 | 2020-02-10 10:39:15 -0800 | [diff] [blame] | 497 | subst_list[cell] = subst |
| 498 | for item in subst_list: |
| 499 | text = text.replace(item, subst_list[item]) |
| 500 | return text |
Michael Schaffner | 8fc927c | 2020-06-22 15:43:32 -0700 | [diff] [blame] | 501 | |
| 502 | |
| 503 | def print_msg_list(msg_list_title, msg_list, max_msg_count=-1): |
| 504 | '''This function prints a list of messages to Markdown. |
| 505 | |
| 506 | The argument msg_list_title contains a string for the list title, whereas |
| 507 | the msg_list argument contains the actual list of message strings. |
| 508 | max_msg_count limits the number of messages to be printed (set to negative |
| 509 | number to print all messages). |
| 510 | |
| 511 | Example: |
| 512 | |
| 513 | print_msg_list("### Tool Warnings", ["Message A", "Message B"], 10) |
| 514 | ''' |
| 515 | md_results = "" |
| 516 | if msg_list: |
Michael Schaffner | 16102e5 | 2020-06-24 11:28:37 -0700 | [diff] [blame] | 517 | md_results += msg_list_title + "\n" |
Michael Schaffner | 8fc927c | 2020-06-22 15:43:32 -0700 | [diff] [blame] | 518 | md_results += "```\n" |
| 519 | for k, msg in enumerate(msg_list): |
| 520 | if k <= max_msg_count or max_msg_count < 0: |
| 521 | md_results += msg + "\n\n" |
| 522 | else: |
Srikrishna Iyer | c93b483 | 2020-08-06 17:54:16 -0700 | [diff] [blame] | 523 | md_results += "Note: %d more messages have been suppressed " % ( |
| 524 | len(msg_list) - max_msg_count) |
| 525 | md_results += "(max_msg_count = %d) \n\n" % (max_msg_count) |
Michael Schaffner | 8fc927c | 2020-06-22 15:43:32 -0700 | [diff] [blame] | 526 | break |
| 527 | md_results += "```\n" |
| 528 | return md_results |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 529 | |
| 530 | |
| 531 | def rm_path(path, ignore_error=False): |
| 532 | '''Removes the specified path if it exists. |
| 533 | |
| 534 | 'path' is a Path-like object. If it does not exist, the function simply |
| 535 | returns. If 'ignore_error' is set, then exception caught by the remove |
| 536 | operation is raised, else it is ignored. |
| 537 | ''' |
| 538 | |
Srikrishna Iyer | 57af049 | 2021-03-04 18:31:15 -0800 | [diff] [blame] | 539 | exc = None |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 540 | try: |
Srikrishna Iyer | 57af049 | 2021-03-04 18:31:15 -0800 | [diff] [blame] | 541 | os.remove(path) |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 542 | except FileNotFoundError: |
| 543 | pass |
Srikrishna Iyer | 57af049 | 2021-03-04 18:31:15 -0800 | [diff] [blame] | 544 | except IsADirectoryError: |
| 545 | try: |
| 546 | shutil.rmtree(path) |
| 547 | except OSError as e: |
| 548 | exc = e |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 549 | except OSError as e: |
Srikrishna Iyer | 57af049 | 2021-03-04 18:31:15 -0800 | [diff] [blame] | 550 | exc = e |
| 551 | |
| 552 | if exc: |
| 553 | log.error("Failed to remove {}:\n{}.".format(path, exc)) |
Srikrishna Iyer | 2d15119 | 2021-02-10 16:56:16 -0800 | [diff] [blame] | 554 | if not ignore_error: |
Srikrishna Iyer | 57af049 | 2021-03-04 18:31:15 -0800 | [diff] [blame] | 555 | raise exc |
Srikrishna Iyer | caa0d89 | 2021-03-01 13:15:52 -0800 | [diff] [blame] | 556 | |
| 557 | |
Cindy Chen | 635d311 | 2021-06-01 16:26:12 -0700 | [diff] [blame] | 558 | def mk_path(path): |
| 559 | '''Create the specified path if it does not exist. |
| 560 | |
| 561 | 'path' is a Path-like object. If it does exist, the function simply |
| 562 | returns. If it does not exist, the function creates the path and its |
| 563 | parent dictories if necessary. |
| 564 | ''' |
| 565 | try: |
| 566 | Path(path).mkdir(parents=True, exist_ok=True) |
| 567 | except PermissionError as e: |
| 568 | log.fatal("Failed to create dirctory {}:\n{}.".format(path, e)) |
| 569 | sys.exit(1) |
| 570 | |
| 571 | |
Srikrishna Iyer | caa0d89 | 2021-03-01 13:15:52 -0800 | [diff] [blame] | 572 | def clean_odirs(odir, max_odirs, ts_format=TS_FORMAT): |
| 573 | """Clean previous output directories. |
| 574 | |
| 575 | When running jobs, we may want to maintain a limited history of |
| 576 | previous invocations. This method finds and deletes the output |
| 577 | directories at the base of input arg 'odir' with the oldest timestamps, |
| 578 | if that limit is reached. It returns a list of directories that |
| 579 | remain after deletion. |
| 580 | """ |
| 581 | |
| 582 | if os.path.exists(odir): |
| 583 | # If output directory exists, back it up. |
| 584 | ts = datetime.fromtimestamp(os.stat(odir).st_ctime).strftime(ts_format) |
| 585 | shutil.move(odir, "{}_{}".format(odir, ts)) |
| 586 | |
| 587 | # Get list of past output directories sorted by creation time. |
| 588 | pdir = Path(odir).resolve().parent |
| 589 | if not pdir.exists(): |
| 590 | return [] |
| 591 | |
| 592 | dirs = sorted([old for old in pdir.iterdir() if old.is_dir()], |
| 593 | key=os.path.getctime, |
| 594 | reverse=True) |
| 595 | |
Srikrishna Iyer | 15f3a60 | 2021-03-30 23:57:27 -0700 | [diff] [blame] | 596 | for old in dirs[max(0, max_odirs - 1):]: |
Srikrishna Iyer | 57af049 | 2021-03-04 18:31:15 -0800 | [diff] [blame] | 597 | shutil.rmtree(old, ignore_errors=True) |
Srikrishna Iyer | caa0d89 | 2021-03-01 13:15:52 -0800 | [diff] [blame] | 598 | |
Srikrishna Iyer | 15f3a60 | 2021-03-30 23:57:27 -0700 | [diff] [blame] | 599 | return [] if max_odirs == 0 else dirs[:max_odirs - 1] |