blob: 4916a8dbc8e23dfcfc013179e5566ffa3cc8851e [file] [log] [blame]
Srikrishna Iyer09a81e92019-12-30 10:47:57 -08001# Copyright lowRISC contributors.
2# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3# SPDX-License-Identifier: Apache-2.0
4r"""
5Utility functions common across dvsim.
6"""
7
8import logging as log
9import os
Srikrishna Iyer09a81e92019-12-30 10:47:57 -080010import re
11import shlex
Srikrishna Iyer2d151192021-02-10 16:56:16 -080012import shutil
Srikrishna Iyer09a81e92019-12-30 10:47:57 -080013import subprocess
14import sys
15import time
16from collections import OrderedDict
Srikrishna Iyercaa0d892021-03-01 13:15:52 -080017from datetime import datetime
18from pathlib import Path
Srikrishna Iyer09a81e92019-12-30 10:47:57 -080019
20import hjson
Srikrishna Iyerf578e7c2020-01-29 13:11:58 -080021import mistletoe
Cindy Chen1aff6652020-04-23 18:49:18 -070022from premailer import transform
Srikrishna Iyer09a81e92019-12-30 10:47:57 -080023
24# For verbose logging
25VERBOSE = 15
26
Srikrishna Iyer2b944ad2021-03-01 13:05:40 -080027# Timestamp format when creating directory backups.
28TS_FORMAT = "%y.%m.%d_%H.%M.%S"
29
30# Timestamp format when generating reports.
31TS_FORMAT_LONG = "%A %B %d %Y %H:%M:%S UTC"
32
Srikrishna Iyer09a81e92019-12-30 10:47:57 -080033
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.
36def 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.
47def 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 Iyer544da8d2020-01-14 23:51:41 -080063 time.sleep(.01)
Srikrishna Iyer09a81e92019-12-30 10:47:57 -080064
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 Swarbrick6cc20112020-04-24 09:44:35 +010075 if exit_on_failure == 1:
76 sys.exit(status)
Srikrishna Iyer09a81e92019-12-30 10:47:57 -080077
78 return (result, status)
79
80
Srikrishna Iyer544da8d2020-01-14 23:51:41 -080081# Parse hjson and return a dict
82def 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 Iyerf578e7c2020-01-29 13:11:58 -080090 except Exception as e:
Srikrishna Iyer2a710a42020-02-10 10:39:15 -080091 log.fatal(
92 "Failed to parse \"%s\" possibly due to bad path or syntax error.\n%s",
93 hjson_file, e)
Srikrishna Iyer544da8d2020-01-14 23:51:41 -080094 sys.exit(1)
95 return hjson_cfg_dict
96
97
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +010098def _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 Iyer2d151192021-02-10 16:56:16 -0800115 raise ValueError('Wildcard had value {!r} which is not of a supported '
116 'type.'.format(value))
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100117
118
119def _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 Iyer2d151192021-02-10 16:56:16 -0800157 'wildcard {!r}.'.format(match.group(0)))
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100158
159 # Treat eval_cmd specially
160 if name == 'eval_cmd':
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800161 cmd = _subst_wildcards(right_str[match.end():], mdict, ignored,
162 ignore_error, seen)[0]
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100163
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 Iyer8ca91da2021-03-30 23:45:30 -0700168 var = var[:match.start()] + run_cmd(cmd)
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100169 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 Iyer2d151192021-02-10 16:56:16 -0800182 'wildcard.'.format(cmd))
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100183
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 Iyer2d151192021-02-10 16:56:16 -0800204 'unknown wildcard, {!r}.'.format(match.group(0)))
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100205
206 value = _stringify_wildcard_value(value)
207
208 # Do any recursive expansion of value, adding name to seen (to avoid
209 # circular recursion).
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800210 value, saw_err = _subst_wildcards(value, mdict, ignored, ignore_error,
211 seen + [name])
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100212
213 # Replace the original match with the result and go around again. If
214 # saw_err, increment idx past what we just inserted.
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800215 var = (var[:idx] + right_str[:match.start()] + value +
216 right_str[match.end():])
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100217 if saw_err:
218 any_err = True
219 idx += match.start() + len(value)
220
221
Srikrishna Iyer86f6bce2020-02-27 19:02:04 -0800222def subst_wildcards(var, mdict, ignored_wildcards=[], ignore_error=False):
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100223 '''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 Iyer09a81e92019-12-30 10:47:57 -0800287 '''
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100288 try:
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800289 return _subst_wildcards(var, mdict, ignored_wildcards, ignore_error,
290 [])[0]
Rupert Swarbrick592cd5d2020-06-10 16:45:03 +0100291 except ValueError as err:
292 log.error(str(err))
293 sys.exit(1)
Srikrishna Iyer09a81e92019-12-30 10:47:57 -0800294
295
Srikrishna Iyer86f6bce2020-02-27 19:02:04 -0800296def find_and_substitute_wildcards(sub_dict,
297 full_dict,
298 ignored_wildcards=[],
299 ignore_error=False):
Srikrishna Iyer09a81e92019-12-30 10:47:57 -0800300 '''
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 Iyer86f6bce2020-02-27 19:02:04 -0800308 sub_dict[key], full_dict, ignored_wildcards, ignore_error)
Srikrishna Iyer09a81e92019-12-30 10:47:57 -0800309
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 Iyer86f6bce2020-02-27 19:02:04 -0800319 full_dict, ignored_wildcards, ignore_error)
Srikrishna Iyer09a81e92019-12-30 10:47:57 -0800320
321 elif type(sub_dict_key_values[i]) is str:
322 sub_dict_key_values[i] = subst_wildcards(
Srikrishna Iyer86f6bce2020-02-27 19:02:04 -0800323 sub_dict_key_values[i], full_dict, ignored_wildcards,
324 ignore_error)
Srikrishna Iyer09a81e92019-12-30 10:47:57 -0800325
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 Iyer86f6bce2020-02-27 19:02:04 -0800331 ignored_wildcards, ignore_error)
Srikrishna Iyer09a81e92019-12-30 10:47:57 -0800332 return sub_dict
Srikrishna Iyerf578e7c2020-01-29 13:11:58 -0800333
334
Cindy Chen1aff6652020-04-23 18:49:18 -0700335def md_results_to_html(title, css_file, md_text):
Srikrishna Iyerf578e7c2020-01-29 13:11:58 -0800336 '''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 Iyerf578e7c2020-01-29 13:11:58 -0800343 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 Iyer2a710a42020-02-10 10:39:15 -0800350 html_text = htmc_color_pc_cells(html_text)
Cindy Chen1aff6652020-04-23 18:49:18 -0700351 # this function converts css style to inline html style
Cindy Chen1aff6652020-04-23 18:49:18 -0700352 html_text = transform(html_text,
353 external_styles=css_file,
Cindy Chenae19bec2020-05-01 10:24:52 -0700354 cssutils_logging_level=log.ERROR)
Srikrishna Iyerf578e7c2020-01-29 13:11:58 -0800355 return html_text
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800356
357
358def htmc_color_pc_cells(text):
Michael Schaffner3d160992020-03-31 18:37:53 -0700359 '''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 Iyer0596a852020-03-02 11:53:31 -0800370
Michael Schaffner3d160992020-03-31 18:37:53 -0700371 These are the supported identifiers: %, %u, G, B, E, W, EN, WN.
372 The shading behavior for these is described below.
Srikrishna Iyer0596a852020-03-02 11:53:31 -0800373
Michael Schaffner3d160992020-03-31 18:37:53 -0700374 %: 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 Iyer2a710a42020-02-10 10:39:15 -0800398 '''
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 Iyer0596a852020-03-02 11:53:31 -0800404 def color_cell(cell, cclass, indicator="%"):
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800405 op = cell.replace("<td", "<td class=\"" + cclass + "\"")
Srikrishna Iyer0596a852020-03-02 11:53:31 -0800406 # Remove the indicator.
Rupert Swarbrick6cc20112020-04-24 09:44:35 +0100407 op = re.sub(r"\s*" + indicator + r"\s*", "", op)
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800408 return op
409
410 # List of 'not applicable' identifiers.
Srikrishna Iyer86f6bce2020-02-27 19:02:04 -0800411 na_list = ['--', 'NA', 'N.A.', 'N.A', 'N/A', 'na', 'n.a.', 'n.a', 'n/a']
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800412 na_list_patterns = '|'.join(na_list)
413
414 # List of floating point patterns: '0', '0.0' & '.0'
Rupert Swarbrick6cc20112020-04-24 09:44:35 +0100415 fp_patterns = r"[\+\-]?\d+\.?\d*"
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800416
417 patterns = fp_patterns + '|' + na_list_patterns
Michael Schaffner3d160992020-03-31 18:37:53 -0700418 indicators = "%|%u|G|B|E|W|EN|WN"
Srikrishna Iyerc93b4832020-08-06 17:54:16 -0700419 match = re.findall(
420 r"(<td.*>\s*(" + patterns + r")\s+(" + indicators + r")\s*</td>)",
421 text)
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800422 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 Iyer0596a852020-03-02 11:53:31 -0800430 indicator = item[2]
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800431 # Skip if fp_num is already processed.
Rupert Swarbrick6cc20112020-04-24 09:44:35 +0100432 if (fp_num, indicator) in fp_nums:
433 continue
Srikrishna Iyer0596a852020-03-02 11:53:31 -0800434 fp_nums.append((fp_num, indicator))
Rupert Swarbrick6cc20112020-04-24 09:44:35 +0100435 if fp_num in na_list:
436 subst = color_cell(cell, "cna", indicator)
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800437 else:
438 # Item is a fp num.
439 try:
440 fp = float(fp_num)
441 except ValueError:
Srikrishna Iyerc93b4832020-08-06 17:54:16 -0700442 log.error(
443 "Percentage item \"%s\" in cell \"%s\" is not an "
444 "integer or a floating point number", fp_num, cell)
Srikrishna Iyer2a710a42020-02-10 10:39:15 -0800445 continue
Michael Schaffner3d160992020-03-31 18:37:53 -0700446 # Percentage, colored.
Srikrishna Iyer0596a852020-03-02 11:53:31 -0800447 if indicator == "%":
Rupert Swarbrick6cc20112020-04-24 09:44:35 +0100448 if fp >= 0.0 and fp < 10.0:
449 subst = color_cell(cell, "c0")
Srikrishna Iyer0596a852020-03-02 11:53:31 -0800450 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 Schaffner3d160992020-03-31 18:37:53 -0700470 # 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 Iyer0596a852020-03-02 11:53:31 -0800483 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 Schaffner3d160992020-03-31 18:37:53 -0700488 # 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 Iyer2a710a42020-02-10 10:39:15 -0800497 subst_list[cell] = subst
498 for item in subst_list:
499 text = text.replace(item, subst_list[item])
500 return text
Michael Schaffner8fc927c2020-06-22 15:43:32 -0700501
502
503def 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 Schaffner16102e52020-06-24 11:28:37 -0700517 md_results += msg_list_title + "\n"
Michael Schaffner8fc927c2020-06-22 15:43:32 -0700518 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 Iyerc93b4832020-08-06 17:54:16 -0700523 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 Schaffner8fc927c2020-06-22 15:43:32 -0700526 break
527 md_results += "```\n"
528 return md_results
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800529
530
531def 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 Iyer57af0492021-03-04 18:31:15 -0800539 exc = None
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800540 try:
Srikrishna Iyer57af0492021-03-04 18:31:15 -0800541 os.remove(path)
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800542 except FileNotFoundError:
543 pass
Srikrishna Iyer57af0492021-03-04 18:31:15 -0800544 except IsADirectoryError:
545 try:
546 shutil.rmtree(path)
547 except OSError as e:
548 exc = e
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800549 except OSError as e:
Srikrishna Iyer57af0492021-03-04 18:31:15 -0800550 exc = e
551
552 if exc:
553 log.error("Failed to remove {}:\n{}.".format(path, exc))
Srikrishna Iyer2d151192021-02-10 16:56:16 -0800554 if not ignore_error:
Srikrishna Iyer57af0492021-03-04 18:31:15 -0800555 raise exc
Srikrishna Iyercaa0d892021-03-01 13:15:52 -0800556
557
Cindy Chen635d3112021-06-01 16:26:12 -0700558def 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 Iyercaa0d892021-03-01 13:15:52 -0800572def 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 Iyer15f3a602021-03-30 23:57:27 -0700596 for old in dirs[max(0, max_odirs - 1):]:
Srikrishna Iyer57af0492021-03-04 18:31:15 -0800597 shutil.rmtree(old, ignore_errors=True)
Srikrishna Iyercaa0d892021-03-01 13:15:52 -0800598
Srikrishna Iyer15f3a602021-03-30 23:57:27 -0700599 return [] if max_odirs == 0 else dirs[:max_odirs - 1]