15 g_start_time = timeit.default_timer()
24 from typing
import Dict, Optional, List, Union
38 def get_timezone() -> str:
40 Returns the correct timezone as short string
42 tz_tuple = time.tzname
46 if time.daylight != 0:
52 def get_compact_git_hash(repo_folder: str) -> Optional[str]:
54 Returns the compact git hash from a folder inside of a git repository
58 subprocess.check_output(
59 [
"git",
"show",
"--oneline",
"-s"], cwd=repo_folder
65 cmd_output = cmd_output.split(
" ")
66 if len(cmd_output) > 1:
71 except subprocess.CalledProcessError:
75 def basf2_command_builder(
76 steering_file: str, parameters: List[str], use_multi_processing=
False
79 This utility function takes the steering file name and other basf2
80 parameters and returns a list which can be executed via the OS shell for
81 example to subprocess.Popen(params ...) If use_multi_processing is True,
82 the script will be executed in multi-processing mode with only 1
83 parallel process in order to test if the code also performs as expected
84 in multi-processing mode
86 cmd_params = [
"basf2"]
87 if use_multi_processing:
89 cmd_params += [steering_file]
90 cmd_params += parameters
95 def available_revisions(work_folder: str) -> List[str]:
97 Loops over the results folder and looks for revisions. It then returns an
98 ordered list, with the most recent revision being the first element in the
99 list and the oldest revision being the last element.
100 The 'age' of a revision is determined by the 'Last-modified'-timestamp of
101 the corresponding folder.
102 :return: A list of all revisions available for plotting
108 subfolders = [p
for p
in os.scandir(search_folder)
if p.is_dir()]
110 p.name
for p
in sorted(subfolders, key=
lambda p: p.stat().st_mtime)
115 def get_start_time() -> float:
117 The function returns the value g_start_time which contain the start time
118 of the validation and is set just a few lines above.
120 @return: Time since the validation has been started
125 def get_validation_folders(
126 location: str, basepaths: Dict[str, str], log: logging.Logger
129 Collects the validation folders for all packages from the stated release
130 directory (either local or central). Returns a dict with the following
132 {'name of package':'absolute path to validation folder of package'}
134 @param location: The location where we want to search for validation
135 folders (either 'local' or 'central')
139 if location
not in [
"local",
"central"]:
141 if basepaths[location]
is None:
145 log.debug(f
"Collecting {location} folders")
154 if os.path.isdir(basepaths[location] +
"/validation"):
155 results[
"validation"] = basepaths[location] +
"/validation"
158 if os.path.isdir(basepaths[location] +
"/validation/validation-test"):
159 results[
"validation-test"] = (
160 basepaths[location] +
"/validation/validation-test"
165 package_dirs = glob.glob(
166 os.path.join(basepaths[location],
"*",
"validation")
171 for package_dir
in package_dirs:
172 package_name = os.path.basename(os.path.dirname(package_dir))
173 results[package_name] = package_dir
179 def get_argument_parser(
180 modes: Optional[List[str]] =
None,
181 ) -> argparse.ArgumentParser:
187 parser = argparse.ArgumentParser()
193 help=
"Perform a dry run, i.e. run the validation module without "
194 "actually executing the steering files (for debugging purposes).",
200 help=
"The mode which will be used for running the validation. "
201 "Possible values: " +
", ".join(modes) +
". Default is 'local'",
209 help=
"Comma seperated list of intervals for which to execute the "
210 "validation scripts. Default is 'nightly'",
217 help=
"One or more strings that will be passed to basf2 as arguments. "
218 "Example: '-n 100'. Quotes are necessary!",
225 help=
"The maximum number of parallel processes to run the "
226 "validation. Only used for local execution. Default is number "
234 help=
"The name(s) of one or multiple packages. Validation will be "
235 "run only on these packages! E.g. -pkg analysis arich",
242 help=
"The file name(s) of one or more space separated validation "
243 "scripts that should be executed exclusively. All dependent "
244 "scripts will also be executed. E.g. -s ECL2D.C",
250 "--select-ignore-dependencies",
251 help=
"The file name of one or more space separated validation "
252 "scripts that should be executed exclusively. This will ignore "
253 "all dependencies. This is useful if you modified a script that "
254 "produces plots based on the output of its dependencies.",
260 help=
"Send email to the contact persons who have failed comparison "
261 "plots. Mail is sent from b2soft@mail.desy.de via "
262 "/usr/sbin/sendmail.",
267 help=
"How to send mails: Full report, incremental report (new/changed "
268 "warnings/failures only) or automatic (default; follow hard coded "
269 "rule, e.g. full reports every Monday).",
270 choices=[
"full",
"incremental",
"automatic"],
274 "-q",
"--quiet", help=
"Suppress the progress bar", action=
"store_true"
279 help=
"The name that will be used for the current revision in the "
280 "results folder. Default is 'current'.",
286 help=
"Execute validation in testing mode where only the validation "
287 "scripts contained in the validation package are executed. "
288 "During regular validation, these scripts are ignored.",
293 help=
"If validation scripts are marked as cacheable and their output "
294 "files already exist, don't execute these scripts again",
299 help=
"Once the validation is finished, start the local web server and "
300 "display the validation results in the system's default browser.",
305 help=
"By default, running scripts (that is, steering files executed by"
306 "the validation framework) are terminated after a "
307 "certain time. Use this flag to change this setting by supplying "
308 "the maximal run time in minutes. Value <=0 disables the run "
309 "time upper limit entirely.",
317 def parse_cmd_line_arguments(
318 modes: Optional[List[str]] =
None,
319 ) -> argparse.Namespace:
321 Sets up a parser for command line arguments, parses them and returns the
323 @return: An object containing the parsed command line arguments.
324 Arguments are accessed like they are attributes of the object,
325 i.e. [name_of_object].[desired_argument]
332 return get_argument_parser(modes).parse_args()
335 def scripts_in_dir(dirpath: str, log: logging.Logger, ext=
"*") -> List[str]:
337 Returns all the files in the given dir (and its subdirs) that have
338 the extension 'ext', if an extension is given (default: all extensions)
340 @param dirpath: The directory in which we are looking for files
341 @param log: logging.Logger object
342 @param ext: The extension of the files, which we are looking for.
343 '*' is the wildcard-operator (=all extensions are accepted)
344 @return: A sorted list of all files with the specified extension in the
349 log.debug(f
"Collecting *{ext} files from {dirpath}")
360 validationpath.folder_name_html_static,
364 for root, dirs, files
in os.walk(dirpath):
367 if os.path.basename(root)
in blacklist:
371 for current_file
in files:
374 if current_file.endswith(ext):
375 results.append(os.path.join(root, current_file))
378 return sorted(results)
381 def strip_ext(path: str) -> str:
383 Takes a path and returns only the name of the file, without the
384 extension on the file name
386 return os.path.splitext(os.path.split(path)[1])[0]
389 def get_style(index: Optional[int], overall_item_count=1):
391 Takes an index and returns the corresponding line attributes,
392 i.e. LineColor, LineWidth and LineStyle.
416 ls_index = {0:
"dashed", 1:
"solid", 2:
"dashdot"}
426 color = colors[index % len(colors)]
431 if overall_item_count == 1:
432 linestyle = linestyles[
"solid"]
436 linestyle = linestyles[ls_index[index % len(ls_index)]]
438 return ROOT.TAttLine(color, linestyle, linewidth)
441 def index_from_revision(revision: str, work_folder: str) -> Optional[int]:
443 Takes the name of a revision and returns the corresponding index. Indices
444 are used to ensure that the color and style of a revision in a plot are
445 always the same, regardless of the displayed revisions.
446 Example: release-X is always red, and no other release get drawn in red if
447 release-X is not selected for display.
448 :param revision: A string containing the name of a revision
449 :param work_folder: The work folder containing the results and plots
450 :return: The index of the requested revision, or None, if no index could
451 be found for 'revision'
454 revisions = available_revisions(work_folder) + [
"reference"]
456 if revision
in revisions:
457 return revisions.index(revision)
462 def get_log_file_paths(logger: logging.Logger) -> List[str]:
464 Returns list of paths that the FileHandlers of logger write to.
465 :param logger: logging.logger object.
466 :return: List of paths
469 for handler
in logger.handlers:
471 ret.append(handler.baseFilename)
472 except AttributeError:
477 def get_terminal_width() -> int:
479 Returns width of terminal in characters, or 80 if unknown.
481 Copied from basf2 utils. However, we only compile the validation package
482 on b2master, so copy this here.
484 from shutil
import get_terminal_size
486 return get_terminal_size(fallback=(80, 24)).columns
490 success: Optional[Union[int, float]] =
None,
491 failure: Optional[Union[int, float]] =
None,
492 total: Optional[Union[int, float]] =
None,
494 rate_name=
"Success rate",
496 """ Keeping the morale up by commenting on success rates.
499 success: Number of successes
500 failure: Number of failures
501 total: success + failures (out of success, failure and total, exactly
502 2 have to be spefified. If you want to use your own figure of
503 merit, just set total = 1. and set success to a number between 0.0
504 (infernal) to 1.0 (stellar))
505 just_comment: Do not add calculated percentage to return string.
506 rate_name: How to refer to the calculated success rate.
509 Comment on your success rate (str).
512 n_nones = [success, failure, total].count(
None)
514 if n_nones == 0
and total != success + failure:
516 "ERROR (congratulator): Specify 2 of the arguments 'success',"
517 "'failure', 'total'.",
523 "ERROR (congratulator): Specify 2 of the arguments 'success',"
524 "'failure', 'total'.",
530 total = success + failure
532 failure = total - success
534 success = total - failure
538 return "That wasn't really exciting, was it?"
540 success_rate = 100 * success / total
543 00.0:
"You're grounded!",
545 20.0:
"That's terrible!",
546 40.0:
"You can do better than that.",
547 50.0:
"That still requires some work.",
548 75.0:
"Three quarters! Almost there!",
549 80.0:
"Way to go ;)",
552 99.0:
"Nobel price!",
556 for value
in sorted(comments.keys(), reverse=
True):
557 if success_rate >= value:
558 comment = comments[value]
562 comment = comments[0]
567 return "{} {}%. {}".format(rate_name, int(success_rate), comment)
570 def terminal_title_line(title="", subtitle="", level=0) -> str:
571 """ Print a title line in the terminal.
574 title (str): The title. If no title is given, only a separating line
576 subtitle (str): Subtitle.
577 level (int): The lower, the more dominantly the line will be styled.
579 linewidth = get_terminal_width()
583 char_dict = {0:
"=", 1:
"-", 2:
"~"}
585 for key
in sorted(char_dict.keys(), reverse=
True):
587 char = char_dict[key]
593 line = char * linewidth
600 ret += title.capitalize() +
"\n"
602 ret += subtitle +
"\n"
def get_results_folder(output_base_dir)