7 g_start_time = timeit.default_timer()
16 from typing
import Dict, Optional, List, Union
30 def get_timezone() -> str:
32 Returns the correct timezone as short string
34 tz_tuple = time.tzname
38 if time.daylight != 0:
44 def get_compact_git_hash(repo_folder: str) -> Optional[str]:
46 Returns the compact git hash from a folder inside of a git repository
49 cmd_output = subprocess.check_output(
50 [
"git",
"show",
"--oneline",
"-s"], cwd=repo_folder
53 cmd_output = cmd_output.split(
" ")
54 if len(cmd_output) > 1:
59 except subprocess.CalledProcessError:
63 def basf2_command_builder(steering_file: str, parameters: List[str],
64 use_multi_processing=
False) -> List[str]:
66 This utility function takes the steering file name and other basf2
67 parameters and returns a list which can be executed via the OS shell for
68 example to subprocess.Popen(params ...) If use_multi_processing is True,
69 the script will be executed in multi-processing mode with only 1
70 parallel process in order to test if the code also performs as expected
71 in multi-processing mode
73 cmd_params = [
'basf2']
74 if use_multi_processing:
76 cmd_params += [steering_file]
77 cmd_params += parameters
82 def available_revisions(work_folder: str) -> List[str]:
84 Loops over the results folder and looks for revisions. It then returns an
85 ordered list, with the most recent revision being the first element in the
86 list and the oldest revision being the last element.
87 The 'age' of a revision is determined by the 'Last-modified'-timestamp of
88 the corresponding folder.
89 :return: A list of all revisions available for plotting
95 subfolders = [p
for p
in os.scandir(search_folder)
if p.is_dir()]
97 p.name
for p
in sorted(subfolders, key=
lambda p: p.stat().st_mtime)
102 def get_start_time() -> float:
104 The function returns the value g_start_time which contain the start time
105 of the validation and is set just a few lines above.
107 @return: Time since the validation has been started
112 def get_validation_folders(
114 basepaths: Dict[str, str],
118 Collects the validation folders for all packages from the stated release
119 directory (either local or central). Returns a dict with the following
121 {'name of package':'absolute path to validation folder of package'}
123 @param location: The location where we want to search for validation
124 folders (either 'local' or 'central')
128 if location
not in [
'local',
'central']:
130 if basepaths[location]
is None:
134 log.debug(f
'Collecting {location} folders')
143 if os.path.isdir(basepaths[location] +
'/validation'):
144 results[
'validation'] = basepaths[location] +
'/validation'
147 if os.path.isdir(basepaths[location] +
'/validation/validation-test'):
148 results[
'validation-test'] = basepaths[location] \
149 +
'/validation/validation-test'
153 package_dirs = glob.glob(os.path.join(basepaths[location],
'*',
158 for package_dir
in package_dirs:
159 package_name = os.path.basename(os.path.dirname(package_dir))
160 results[package_name] = package_dir
166 def get_argument_parser(modes: Optional[List[str]] =
None) \
167 -> argparse.ArgumentParser:
173 parser = argparse.ArgumentParser()
179 help=
"Perform a dry run, i.e. run the validation module without "
180 "actually executing the steering files (for debugging purposes).",
186 help=
"The mode which will be used for running the validation. "
187 "Possible values: " +
", ".join(modes) +
". Default is 'local'",
195 help=
"Comma seperated list of intervals for which to execute the "
196 "validation scripts. Default is 'nightly'",
203 help=
"One or more strings that will be passed to basf2 as arguments. "
204 "Example: '-n 100'. Quotes are necessary!",
211 help=
"The maximum number of parallel processes to run the "
212 "validation. Only used for local execution. Default is number "
220 help=
"The name(s) of one or multiple packages. Validation will be "
221 "run only on these packages! E.g. -pkg analysis arich",
228 help=
"The file name(s) of one or more space separated validation "
229 "scripts that should be executed exclusively. All dependent "
230 "scripts will also be executed. E.g. -s ECL2D.C",
236 "--select-ignore-dependencies",
237 help=
"The file name of one or more space separated validation "
238 "scripts that should be executed exclusively. This will ignore "
239 "all dependencies. This is useful if you modified a script that "
240 "produces plots based on the output of its dependencies.",
246 help=
"Send email to the contact persons who have failed comparison "
247 "plots. Mail is sent from b2soft@mail.desy.de via "
248 "/usr/sbin/sendmail.",
252 help=
"How to send mails: Full report, incremental report (new/changed "
253 "warnings/failures only) or automatic (default; follow hard coded "
254 "rule, e.g. full reports every Monday).",
255 choices=[
"full",
"incremental",
"automatic"],
261 help=
"Suppress the progress bar",
267 help=
"The name that will be used for the current revision in the "
268 "results folder. Default is 'current'.",
274 help=
"Execute validation in testing mode where only the validation "
275 "scripts contained in the validation package are executed. "
276 "During regular validation, these scripts are ignored.",
281 help=
"If validation scripts are marked as cacheable and their output "
282 "files already exist, don't execute these scripts again",
287 help=
"Once the validation is finished, start the local web server and "
288 "display the validation results in the system's default browser.",
293 help=
"By default, running scripts (that is, steering files executed by"
294 "the validation framework) are terminated after a "
295 "certain time. Use this flag to change this setting by supplying "
296 "the maximal run time in minutes. Value <=0 disables the run "
297 "time upper limit entirely.",
305 def parse_cmd_line_arguments(modes: Optional[List[str]] =
None) -> argparse.Namespace:
307 Sets up a parser for command line arguments, parses them and returns the
309 @return: An object containing the parsed command line arguments.
310 Arguments are accessed like they are attributes of the object,
311 i.e. [name_of_object].[desired_argument]
318 return get_argument_parser(modes).parse_args()
321 def scripts_in_dir(dirpath: str, log: logging.Logger, ext=
'*') -> List[str]:
323 Returns all the files in the given dir (and its subdirs) that have
324 the extension 'ext', if an extension is given (default: all extensions)
326 @param dirpath: The directory in which we are looking for files
327 @param log: logging.Logger object
328 @param ext: The extension of the files, which we are looking for.
329 '*' is the wildcard-operator (=all extensions are accepted)
330 @return: A sorted list of all files with the specified extension in the
335 log.debug(f
'Collecting *{ext} files from {dirpath}')
346 validationpath.folder_name_html_static
350 for root, dirs, files
in os.walk(dirpath):
353 if os.path.basename(root)
in blacklist:
357 for current_file
in files:
360 if current_file.endswith(ext):
361 results.append(os.path.join(root, current_file))
364 return sorted(results)
367 def strip_ext(path: str) -> str:
369 Takes a path and returns only the name of the file, without the
370 extension on the file name
372 return os.path.splitext(os.path.split(path)[1])[0]
375 def get_style(index: Optional[int], overall_item_count=1):
377 Takes an index and returns the corresponding line attributes,
378 i.e. LineColor, LineWidth and LineStyle.
395 linestyles = {
'dashed': 2,
398 ls_index = {0:
'dashed', 1:
'solid', 2:
'dashdot'}
408 color = colors[index % len(colors)]
413 if overall_item_count == 1:
414 linestyle = linestyles[
'solid']
418 linestyle = linestyles[ls_index[index % len(ls_index)]]
420 return ROOT.TAttLine(color, linestyle, linewidth)
423 def index_from_revision(revision: str, work_folder: str) -> Optional[int]:
425 Takes the name of a revision and returns the corresponding index. Indices
426 are used to ensure that the color and style of a revision in a plot are
427 always the same, regardless of the displayed revisions.
428 Example: release-X is always red, and no other release get drawn in red if
429 release-X is not selected for display.
430 :param revision: A string containing the name of a revision
431 :param work_folder: The work folder containing the results and plots
432 :return: The index of the requested revision, or None, if no index could
433 be found for 'revision'
436 revisions = available_revisions(work_folder) + [
"reference"]
438 if revision
in revisions:
439 return revisions.index(revision)
444 def get_log_file_paths(logger: logging.Logger) -> List[str]:
446 Returns list of paths that the FileHandlers of logger write to.
447 :param logger: logging.logger object.
448 :return: List of paths
451 for handler
in logger.handlers:
453 ret.append(handler.baseFilename)
454 except AttributeError:
459 def get_terminal_width() -> int:
461 Returns width of terminal in characters, or 80 if unknown.
463 Copied from basf2 utils. However, we only compile the validation package
464 on b2master, so copy this here.
466 from shutil
import get_terminal_size
467 return get_terminal_size(fallback=(80, 24)).columns
471 success: Optional[Union[int, float]] =
None,
472 failure: Optional[Union[int, float]] =
None,
473 total: Optional[Union[int, float]] =
None,
475 rate_name=
"Success rate"
477 """ Keeping the morale up by commenting on success rates.
480 success: Number of successes
481 failure: Number of failures
482 total: success + failures (out of success, failure and total, exactly
483 2 have to be spefified. If you want to use your own figure of
484 merit, just set total = 1. and set success to a number between 0.0
485 (infernal) to 1.0 (stellar))
486 just_comment: Do not add calculated percentage to return string.
487 rate_name: How to refer to the calculated success rate.
490 Comment on your success rate (str).
493 n_nones = [success, failure, total].count(
None)
495 if n_nones == 0
and total != success + failure:
497 "ERROR (congratulator): Specify 2 of the arguments 'success',"
498 "'failure', 'total'.",
504 "ERROR (congratulator): Specify 2 of the arguments 'success',"
505 "'failure', 'total'.",
511 total = success + failure
513 failure = total - success
515 success = total - failure
519 return "That wasn't really exciting, was it?"
521 success_rate = 100 * success / total
524 00.0:
"You're grounded!",
526 20.0:
"That's terrible!",
527 40.0:
"You can do better than that.",
528 50.0:
"That still requires some work.",
529 75.0:
"Three quarters! Almost there!",
530 80.0:
"Way to go ;)",
533 99.0:
"Nobel price!",
537 for value
in sorted(comments.keys(), reverse=
True):
538 if success_rate >= value:
539 comment = comments[value]
543 comment = comments[0]
548 return "{} {}%. {}".format(
555 def terminal_title_line(title="", subtitle="", level=0) -> str:
556 """ Print a title line in the terminal.
559 title (str): The title. If no title is given, only a separating line
561 subtitle (str): Subtitle.
562 level (int): The lower, the more dominantly the line will be styled.
564 linewidth = get_terminal_width()
574 for key
in sorted(char_dict.keys(), reverse=
True):
576 char = char_dict[key]
582 line = char * linewidth
589 ret += title.capitalize() +
"\n"
591 ret += subtitle +
"\n"