15 g_start_time = timeit.default_timer()
24 from typing
import Dict, Optional, List, Union
26 from pathlib
import Path
39 def get_timezone() -> str:
41 Returns the correct timezone as short string
43 tz_tuple = time.tzname
47 if time.daylight != 0:
53 def get_compact_git_hash(repo_folder: str) -> Optional[str]:
55 Returns the compact git hash from a folder inside of a git repository
59 subprocess.check_output(
60 [
"git",
"show",
"--oneline",
"-s"], cwd=repo_folder
66 cmd_output = cmd_output.split(
" ")
67 if len(cmd_output) > 1:
72 except subprocess.CalledProcessError:
76 def basf2_command_builder(
77 steering_file: str, parameters: List[str], use_multi_processing=
False
80 This utility function takes the steering file name and other basf2
81 parameters and returns a list which can be executed via the OS shell for
82 example to subprocess.Popen(params ...) If use_multi_processing is True,
83 the script will be executed in multi-processing mode with only 1
84 parallel process in order to test if the code also performs as expected
85 in multi-processing mode
87 cmd_params = [
"basf2"]
88 if use_multi_processing:
90 cmd_params += [steering_file]
91 cmd_params += parameters
96 def available_revisions(work_folder: str) -> List[str]:
98 Loops over the results folder and looks for revisions. It then returns an
99 ordered list, with the most recent revision being the first element in the
100 list and the oldest revision being the last element.
101 The 'age' of a revision is determined by the 'Last-modified'-timestamp of
102 the corresponding folder.
103 :return: A list of all revisions available for plotting
109 subfolders = [p
for p
in os.scandir(search_folder)
if p.is_dir()]
111 p.name
for p
in sorted(subfolders, key=
lambda p: p.stat().st_mtime)
116 def get_start_time() -> float:
118 The function returns the value g_start_time which contain the start time
119 of the validation and is set just a few lines above.
121 @return: Time since the validation has been started
126 def get_validation_folders(
127 location: str, basepaths: Dict[str, str], log: logging.Logger
130 Collects the validation folders for all packages from the stated release
131 directory (either local or central). Returns a dict with the following
133 {'name of package':'absolute path to validation folder of package'}
135 @param location: The location where we want to search for validation
136 folders (either 'local' or 'central')
140 if location
not in [
"local",
"central"]:
142 if basepaths[location]
is None:
146 log.debug(f
"Collecting {location} folders")
155 if os.path.isdir(basepaths[location] +
"/validation"):
156 results[
"validation"] = basepaths[location] +
"/validation"
159 if os.path.isdir(basepaths[location] +
"/validation/validation-test"):
160 results[
"validation-test"] = (
161 basepaths[location] +
"/validation/validation-test"
166 package_dirs = glob.glob(
167 os.path.join(basepaths[location],
"*",
"validation")
172 for package_dir
in package_dirs:
173 package_name = os.path.basename(os.path.dirname(package_dir))
174 results[package_name] = package_dir
180 def get_argument_parser(
181 modes: Optional[List[str]] =
None,
182 ) -> argparse.ArgumentParser:
188 parser = argparse.ArgumentParser()
194 help=
"Perform a dry run, i.e. run the validation module without "
195 "actually executing the steering files (for debugging purposes).",
201 help=
"The mode which will be used for running the validation. "
202 "Possible values: " +
", ".join(modes) +
". Default is 'local'",
210 help=
"Comma seperated list of intervals for which to execute the "
211 "validation scripts. Default is 'nightly'",
218 help=
"One or more strings that will be passed to basf2 as arguments. "
219 "Example: '-n 100'. Quotes are necessary!",
226 help=
"The maximum number of parallel processes to run the "
227 "validation. Only used for local execution. Default is number "
235 help=
"The name(s) of one or multiple packages. Validation will be "
236 "run only on these packages! E.g. -pkg analysis arich",
243 help=
"The file name(s) of one or more space separated validation "
244 "scripts that should be executed exclusively. All dependent "
245 "scripts will also be executed. E.g. -s ECL2D.C",
251 "--select-ignore-dependencies",
252 help=
"The file name of one or more space separated validation "
253 "scripts that should be executed exclusively. This will ignore "
254 "all dependencies. This is useful if you modified a script that "
255 "produces plots based on the output of its dependencies.",
261 help=
"Send email to the contact persons who have failed comparison "
262 "plots. Mail is sent from b2soft@mail.desy.de via "
263 "/usr/sbin/sendmail.",
268 help=
"How to send mails: Full report, incremental report (new/changed "
269 "warnings/failures only) or automatic (default; follow hard coded "
270 "rule, e.g. full reports every Monday).",
271 choices=[
"full",
"incremental",
"automatic"],
275 "-q",
"--quiet", help=
"Suppress the progress bar", action=
"store_true"
280 help=
"The name that will be used for the current revision in the "
281 "results folder. Default is 'current'.",
287 help=
"Execute validation in testing mode where only the validation "
288 "scripts contained in the validation package are executed. "
289 "During regular validation, these scripts are ignored.",
294 help=
"If validation scripts are marked as cacheable and their output "
295 "files already exist, don't execute these scripts again",
300 help=
"Once the validation is finished, start the local web server and "
301 "display the validation results in the system's default browser.",
306 help=
"By default, running scripts (that is, steering files executed by"
307 "the validation framework) are terminated after a "
308 "certain time. Use this flag to change this setting by supplying "
309 "the maximal run time in minutes. Value <=0 disables the run "
310 "time upper limit entirely.",
318 def parse_cmd_line_arguments(
319 modes: Optional[List[str]] =
None,
320 ) -> argparse.Namespace:
322 Sets up a parser for command line arguments, parses them and returns the
324 @return: An object containing the parsed command line arguments.
325 Arguments are accessed like they are attributes of the object,
326 i.e. [name_of_object].[desired_argument]
333 return get_argument_parser(modes).parse_args()
336 def scripts_in_dir(dirpath: str, log: logging.Logger, ext=
"*") -> List[str]:
338 Returns all the files in the given dir (and its subdirs) that have
339 the extension 'ext', if an extension is given (default: all extensions)
341 @param dirpath: The directory in which we are looking for files
342 @param log: logging.Logger object
343 @param ext: The extension of the files, which we are looking for.
344 '*' is the wildcard-operator (=all extensions are accepted)
345 @return: A sorted list of all files with the specified extension in the
350 log.debug(f
"Collecting *{ext} files from {dirpath}")
361 validationpath.folder_name_html_static,
365 for root, dirs, files
in os.walk(dirpath):
368 if os.path.basename(root)
in blacklist:
372 for current_file
in files:
375 if current_file.endswith(ext):
376 results.append(os.path.join(root, current_file))
379 return sorted(results)
382 def strip_ext(path: str) -> str:
384 Takes a path and returns only the name of the file, without the
385 extension on the file name
387 return os.path.splitext(os.path.split(path)[1])[0]
390 def get_style(index: Optional[int], overall_item_count=1):
392 Takes an index and returns the corresponding line attributes,
393 i.e. LineColor, LineWidth and LineStyle.
417 ls_index = {0:
"dashed", 1:
"solid", 2:
"dashdot"}
427 color = colors[index % len(colors)]
432 if overall_item_count == 1:
433 linestyle = linestyles[
"solid"]
437 linestyle = linestyles[ls_index[index % len(ls_index)]]
439 return ROOT.TAttLine(color, linestyle, linewidth)
442 def index_from_revision(revision: str, work_folder: str) -> Optional[int]:
444 Takes the name of a revision and returns the corresponding index. Indices
445 are used to ensure that the color and style of a revision in a plot are
446 always the same, regardless of the displayed revisions.
447 Example: release-X is always red, and no other release get drawn in red if
448 release-X is not selected for display.
449 :param revision: A string containing the name of a revision
450 :param work_folder: The work folder containing the results and plots
451 :return: The index of the requested revision, or None, if no index could
452 be found for 'revision'
455 revisions = available_revisions(work_folder) + [
"reference"]
457 if revision
in revisions:
458 return revisions.index(revision)
463 def get_log_file_paths(logger: logging.Logger) -> List[str]:
465 Returns list of paths that the FileHandlers of logger write to.
466 :param logger: logging.logger object.
467 :return: List of paths
470 for handler
in logger.handlers:
472 ret.append(handler.baseFilename)
473 except AttributeError:
478 def get_terminal_width() -> int:
480 Returns width of terminal in characters, or 80 if unknown.
482 Copied from basf2 utils. However, we only compile the validation package
483 on b2master, so copy this here.
485 from shutil
import get_terminal_size
487 return get_terminal_size(fallback=(80, 24)).columns
491 success: Optional[Union[int, float]] =
None,
492 failure: Optional[Union[int, float]] =
None,
493 total: Optional[Union[int, float]] =
None,
495 rate_name=
"Success rate",
497 """ Keeping the morale up by commenting on success rates.
500 success: Number of successes
501 failure: Number of failures
502 total: success + failures (out of success, failure and total, exactly
503 2 have to be spefified. If you want to use your own figure of
504 merit, just set total = 1. and set success to a number between 0.0
505 (infernal) to 1.0 (stellar))
506 just_comment: Do not add calculated percentage to return string.
507 rate_name: How to refer to the calculated success rate.
510 Comment on your success rate (str).
513 n_nones = [success, failure, total].count(
None)
515 if n_nones == 0
and total != success + failure:
517 "ERROR (congratulator): Specify 2 of the arguments 'success',"
518 "'failure', 'total'.",
524 "ERROR (congratulator): Specify 2 of the arguments 'success',"
525 "'failure', 'total'.",
531 total = success + failure
533 failure = total - success
535 success = total - failure
539 return "That wasn't really exciting, was it?"
541 success_rate = 100 * success / total
544 00.0:
"You're grounded!",
546 20.0:
"That's terrible!",
547 40.0:
"You can do better than that.",
548 50.0:
"That still requires some work.",
549 75.0:
"Three quarters! Almost there!",
550 80.0:
"Way to go ;)",
553 99.0:
"Nobel price!",
557 for value
in sorted(comments.keys(), reverse=
True):
558 if success_rate >= value:
559 comment = comments[value]
563 comment = comments[0]
568 return "{} {}%. {}".format(rate_name, int(success_rate), comment)
571 def terminal_title_line(title="", subtitle="", level=0) -> str:
572 """ Print a title line in the terminal.
575 title (str): The title. If no title is given, only a separating line
577 subtitle (str): Subtitle.
578 level (int): The lower, the more dominantly the line will be styled.
580 linewidth = get_terminal_width()
584 char_dict = {0:
"=", 1:
"-", 2:
"~"}
586 for key
in sorted(char_dict.keys(), reverse=
True):
588 char = char_dict[key]
594 line = char * linewidth
601 ret += title.capitalize() +
"\n"
603 ret += subtitle +
"\n"
608 def get_file_metadata(filename: str) -> str:
610 Retrieve the metadata for a file using ``b2file-metadata-show -a``.
613 metadata (str): File to get number of events from.
616 (str): Metadata of file.
618 if not Path(filename).exists():
619 raise FileNotFoundError(f
"Could not find file {filename}")
621 proc = subprocess.run(
622 [
"b2file-metadata-show",
"-a", str(filename)],
623 stdout=subprocess.PIPE,
626 metadata = proc.stdout.decode(
"utf-8")
def get_results_folder(output_base_dir)