15g_start_time = timeit.default_timer()
24from typing
import Dict, Optional, List, Union
26from pathlib
import Path
41def get_timezone() -> str:
43 Returns the correct timezone as short string
45 tz_tuple = time.tzname
49 if time.daylight != 0:
55def get_compact_git_hash(repo_folder: str) -> Optional[str]:
57 Returns the compact git hash from a folder inside of a git repository
61 subprocess.check_output(
62 [
"git",
"show",
"--oneline",
"-s"], cwd=repo_folder
68 cmd_output = cmd_output.split(
" ")
69 if len(cmd_output) > 1:
74 except subprocess.CalledProcessError:
78def basf2_command_builder(
79 steering_file: str, parameters: List[str], use_multi_processing=
False
82 This utility function takes the steering file name and other basf2
83 parameters
and returns a list which can be executed via the OS shell
for
84 example to subprocess.Popen(params ...) If use_multi_processing
is True,
85 the script will be executed
in multi-processing mode
with only 1
86 parallel process
in order to test
if the code also performs
as expected
87 in multi-processing mode
89 cmd_params = ["basf2"]
90 if use_multi_processing:
92 cmd_params += [steering_file]
93 cmd_params += parameters
98def available_revisions(work_folder: str) -> List[str]:
100 Loops over the results folder and looks
for revisions. It then returns an
101 ordered list,
with the most recent revision being the first element
in the
102 list
and the oldest revision being the last element.
103 The
'age' of a revision
is determined by the
'Last-modified'-timestamp of
104 the corresponding folder.
105 :
return: A list of all revisions available
for plotting
111 subfolders = [p
for p
in os.scandir(search_folder)
if p.is_dir()]
113 p.name
for p
in sorted(subfolders, key=
lambda p: p.stat().st_mtime)
118def get_latest_nightly(work_folder: str) -> str:
120 Loops over the results folder and looks
for nightly builds. It then returns
121 the most recent nightly tag sorted by date
in the name. If no
122 nightly results are available then it returns the default
'current' tag.
123 :
return: the most recent nightly build
or current
125 available = available_revisions(work_folder)
126 available_nightlies = [
127 revision for revision
in available
128 if revision.startswith(
"nightly")
130 if available_nightlies:
131 return sorted(available_nightlies, reverse=
True)[0]
136def get_popular_revision_combinations(work_folder: str) -> List[str]:
138 Returns several combinations of available revisions that we might
139 want to pre-build on the server.
142 List[List of revisions (str)]
145 available_revisions(work_folder),
148 available_releases = [
149 revision
for revision
in available
150 if revision.startswith(
"release")
or revision.startswith(
"prerelease")
152 available_nightlies = [
153 revision
for revision
in available
154 if revision.startswith(
"nightly")
157 def atindex_or_none(lst, index):
158 """ Returns item at index from lst or None"""
164 def remove_duplicates_lstlst(lstlst):
165 """ Removes duplicate lists in a list of lists """
180 [
"reference"] + sorted(available),
183 [
"reference", atindex_or_none(available_releases, 0)],
184 [
"reference", atindex_or_none(available_nightlies, 0)],
187 [
"reference"] + sorted(list(filter(
190 atindex_or_none(available_releases, 0),
191 atindex_or_none(available_nightlies, 0)
196 [
"reference"] + sorted(available_nightlies)
201 list(filter(
None, comb))
for comb
in ret
204 ret = list(filter(
None, ret))
207 ret = remove_duplicates_lstlst(ret)
210 sys.exit(
"No revisions seem to be available. Exit.")
215def clear_plots(work_folder: str, keep_revisions: List[str]):
217 This function will clear the plots folder to get rid of all but the
218 skipped revisions' associated plot files. """
220 rainbow_file = os.path.join(work_folder, 'rainbow.json')
223 keep_revisions = [sorted(revs)
for revs
in keep_revisions]
225 with open(rainbow_file)
as rainbow:
226 entries = json.loads(rainbow.read())
227 for hash, revisions
in entries.items():
229 if sorted(revisions)
in keep_revisions:
230 print(f
'Retaining {hash}')
231 cleaned_rainbow[hash] = revisions
234 print(f
'Removing {hash}:{revisions}')
235 work_folder_path = Path(os.path.join(work_folder, hash))
236 if work_folder_path.exists()
and work_folder_path.is_dir():
237 shutil.rmtree(work_folder_path)
239 with open(rainbow_file,
'w')
as rainbow:
240 rainbow.write(json.dumps(cleaned_rainbow, indent=4))
243def get_start_time() -> float:
245 The function returns the value g_start_time which contain the start time
246 of the validation and is set just a few lines above.
248 @return: Time since the validation has been started
253def get_validation_folders(
254 location: str, basepaths: Dict[str, str], log: logging.Logger
257 Collects the validation folders for all packages
from the stated release
258 directory (either local
or central). Returns a dict
with the following
260 {
'name of package':
'absolute path to validation folder of package'}
262 @param location: The location where we want to search
for validation
263 folders (either
'local' or 'central')
267 if location
not in [
"local",
"central"]:
269 if basepaths[location]
is None:
273 log.debug(f
"Collecting {location} folders")
282 if os.path.isdir(basepaths[location] +
"/validation"):
283 results[
"validation"] = basepaths[location] +
"/validation"
286 if os.path.isdir(basepaths[location] +
"/validation/validation-test"):
287 results[
"validation-test"] = (
288 basepaths[location] +
"/validation/validation-test"
293 package_dirs = glob.glob(
294 os.path.join(basepaths[location],
"*",
"validation")
299 for package_dir
in package_dirs:
300 package_name = os.path.basename(os.path.dirname(package_dir))
301 results[package_name] = package_dir
307def get_argument_parser(
308 modes: Optional[List[str]] =
None,
309) -> argparse.ArgumentParser:
315 parser = argparse.ArgumentParser()
321 help=
"Perform a dry run, i.e. run the validation module without "
322 "actually executing the steering files (for debugging purposes).",
328 help=
"The mode which will be used for running the validation. "
329 "Possible values: " +
", ".join(modes) +
". Default is 'local'",
337 help=
"Comma separated list of intervals for which to execute the "
338 "validation scripts. Default is 'nightly'",
345 help=
"One or more strings that will be passed to basf2 as arguments. "
346 "Example: '-n 100'. Quotes are necessary!",
353 help=
"The maximum number of parallel processes to run the "
354 "validation. Only used for local execution. Default is number "
362 help=
"The name(s) of one or multiple packages. Validation will be "
363 "run only on these packages! E.g. -pkg analysis arich",
370 help=
"The file name(s) of one or more space separated validation "
371 "scripts that should be executed exclusively. All dependent "
372 "scripts will also be executed. E.g. -s ECL2D.C",
378 "--select-ignore-dependencies",
379 help=
"The file name of one or more space separated validation "
380 "scripts that should be executed exclusively. This will ignore "
381 "all dependencies. This is useful if you modified a script that "
382 "produces plots based on the output of its dependencies.",
388 help=
"Send email to the contact persons who have failed comparison "
389 "plots. Mail is sent from b2soft@mail.desy.de via "
390 "/usr/sbin/sendmail.",
395 help=
"How to send mails: Full report, incremental report (new/changed "
396 "warnings/failures only) or automatic (default; follow hard coded "
397 "rule, e.g. full reports every Monday).",
398 choices=[
"full",
"incremental",
"automatic"],
402 "-q",
"--quiet", help=
"Suppress the progress bar", action=
"store_true"
407 help=
"The name that will be used for the current revision in the "
408 "results folder. Default is 'current'.",
414 help=
"Execute validation in testing mode where only the validation "
415 "scripts contained in the validation package are executed. "
416 "During regular validation, these scripts are ignored.",
421 help=
"If validation scripts are marked as cacheable and their output "
422 "files already exist, don't execute these scripts again",
427 help=
"Once the validation is finished, start the local web server and "
428 "display the validation results in the system's default browser.",
433 help=
"By default, running scripts (that is, steering files executed by"
434 "the validation framework) are terminated after a "
435 "certain time. Use this flag to change this setting by supplying "
436 "the maximal run time in minutes. Value <=0 disables the run "
437 "time upper limit entirely.",
445def parse_cmd_line_arguments(
446 modes: Optional[List[str]] =
None,
447) -> argparse.Namespace:
449 Sets up a parser for command line arguments, parses them
and returns the
451 @return: An object containing the parsed command line arguments.
452 Arguments are accessed like they are attributes of the object,
453 i.e. [name_of_object].[desired_argument]
460 return get_argument_parser(modes).parse_args()
463def scripts_in_dir(dirpath: str, log: logging.Logger, ext=
"*") -> List[str]:
465 Returns all the files in the given dir (
and its subdirs) that have
466 the extension
'ext',
if an extension
is given (default: all extensions)
468 @param dirpath: The directory
in which we are looking
for files
469 @param log: logging.Logger object
470 @param ext: The extension of the files, which we are looking
for.
471 '*' is the wildcard-operator (=all extensions are accepted)
472 @return: A sorted list of all files
with the specified extension
in the
477 log.debug(f
"Collecting *{ext} files from {dirpath}")
488 validationpath.folder_name_html_static,
492 for root, dirs, files
in os.walk(dirpath):
495 if os.path.basename(root)
in blacklist:
499 for current_file
in files:
502 if current_file.endswith(ext):
503 results.append(os.path.join(root, current_file))
506 return sorted(results)
509def strip_ext(path: str) -> str:
511 Takes a path and returns only the name of the file, without the
512 extension on the file name
514 return os.path.splitext(os.path.split(path)[1])[0]
517def get_style(index: Optional[int], overall_item_count=1):
519 Takes an index and returns the corresponding line attributes,
520 i.e. LineColor, LineWidth
and LineStyle.
544 ls_index = {0:
"dashed", 1:
"solid", 2:
"dashdot"}
554 color = colors[index % len(colors)]
559 if overall_item_count == 1:
560 linestyle = linestyles[
"solid"]
564 linestyle = linestyles[ls_index[index % len(ls_index)]]
566 return ROOT.TAttLine(color, linestyle, linewidth)
569def index_from_revision(revision: str, work_folder: str) -> Optional[int]:
571 Takes the name of a revision and returns the corresponding index. Indices
572 are used to ensure that the color
and style of a revision
in a plot are
573 always the same, regardless of the displayed revisions.
574 Example: release-X
is always red,
and no other release get drawn
in red
if
575 release-X
is not selected
for display.
576 :param revision: A string containing the name of a revision
577 :param work_folder: The work folder containing the results
and plots
578 :
return: The index of the requested revision,
or None,
if no index could
579 be found
for 'revision'
582 revisions = available_revisions(work_folder) + ["reference"]
584 if revision
in revisions:
585 return revisions.index(revision)
590def get_log_file_paths(logger: logging.Logger) -> List[str]:
592 Returns list of paths that the FileHandlers of logger write to.
593 :param logger: logging.logger object.
594 :return: List of paths
597 for handler
in logger.handlers:
599 ret.append(handler.baseFilename)
600 except AttributeError:
605def get_terminal_width() -> int:
607 Returns width of terminal in characters,
or 80
if unknown.
609 Copied
from basf2 utils. However, we only compile the validation package
610 on b2master, so copy this here.
612 from shutil
import get_terminal_size
614 return get_terminal_size(fallback=(80, 24)).columns
618 success: Optional[Union[int, float]] =
None,
619 failure: Optional[Union[int, float]] =
None,
620 total: Optional[Union[int, float]] =
None,
622 rate_name=
"Success rate",
624 """ Keeping the morale up by commenting on success rates.
627 success: Number of successes
628 failure: Number of failures
629 total: success + failures (out of success, failure and total, exactly
630 2 have to be specified. If you want to use your own figure of
631 merit, just set total = 1.
and set success to a number between 0.0
632 (infernal) to 1.0 (stellar))
633 just_comment: Do
not add calculated percentage to
return string.
634 rate_name: How to refer to the calculated success rate.
637 Comment on your success rate (str).
640 n_nones = [success, failure, total].count(None)
642 if n_nones == 0
and total != success + failure:
644 "ERROR (congratulator): Specify 2 of the arguments 'success',"
645 "'failure', 'total'.",
651 "ERROR (congratulator): Specify 2 of the arguments 'success',"
652 "'failure', 'total'.",
658 total = success + failure
660 failure = total - success
662 success = total - failure
666 return "That wasn't really exciting, was it?"
668 success_rate = 100 * success / total
671 00.0:
"You're grounded!",
673 20.0:
"That's terrible!",
674 40.0:
"You can do better than that.",
675 50.0:
"That still requires some work.",
676 75.0:
"Three quarters! Almost there!",
677 80.0:
"Way to go ;)",
680 99.0:
"Nobel price!",
684 for value
in sorted(comments.keys(), reverse=
True):
685 if success_rate >= value:
686 comment = comments[value]
690 comment = comments[0]
695 return f
"{rate_name} {int(success_rate)}%. {comment}"
698def terminal_title_line(title="", subtitle="", level=0) -> str:
699 """ Print a title line in the terminal.
702 title (str): The title. If no title is given, only a separating line
704 subtitle (str): Subtitle.
705 level (int): The lower, the more dominantly the line will be styled.
707 linewidth = get_terminal_width()
711 char_dict = {0:
"=", 1:
"-", 2:
"~"}
713 for key
in sorted(char_dict.keys(), reverse=
True):
715 char = char_dict[key]
721 line = char * linewidth
728 ret += title.capitalize() +
"\n"
730 ret += subtitle +
"\n"
735def get_file_metadata(filename: str) -> str:
737 Retrieve the metadata for a file using ``b2file-metadata-show -a``.
740 metadata (str): File to get number of events
from.
743 (str): Metadata of file.
745 if not Path(filename).exists():
746 raise FileNotFoundError(f
"Could not find file {filename}")
751 proc = subprocess.run(
752 [
"b2file-metadata-show",
"-a", str(filename)],
753 stdout=subprocess.PIPE,
756 metadata = proc.stdout.decode(
"utf-8")
757 except subprocess.CalledProcessError
as e:
def get_results_folder(output_base_dir)