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 "
373 "(use -si instead to execute script(s) ignoring dependencies)",
379 "--select-ignore-dependencies",
380 help=
"The file name of one or more space separated validation "
381 "scripts that should be executed exclusively. This will ignore "
382 "all dependencies. This is useful if you modified a script that "
383 "produces plots based on the output of its dependencies.",
389 help=
"Send email to the contact persons who have failed comparison "
390 "plots. Mail is sent from b2soft@mail.desy.de via "
391 "/usr/sbin/sendmail.",
396 help=
"How to send mails: Full report, incremental report (new/changed "
397 "warnings/failures only) or automatic (default; follow hard coded "
398 "rule, e.g. full reports every Monday).",
399 choices=[
"full",
"incremental",
"automatic"],
403 "-q",
"--quiet", help=
"Suppress the progress bar", action=
"store_true"
408 help=
"The name that will be used for the current revision in the "
409 "results folder. Default is 'current'.",
415 help=
"Execute validation in testing mode where only the validation "
416 "scripts contained in the validation package are executed. "
417 "During regular validation, these scripts are ignored.",
422 help=
"If validation scripts are marked as cacheable and their output "
423 "files already exist, don't execute these scripts again",
428 help=
"Once the validation is finished, start the local web server and "
429 "display the validation results in the system's default browser.",
434 help=
"By default, running scripts (that is, steering files executed by"
435 "the validation framework) are terminated after a "
436 "certain time. Use this flag to change this setting by supplying "
437 "the maximal run time in minutes. Value <=0 disables the run "
438 "time upper limit entirely.",
446def parse_cmd_line_arguments(
447 modes: Optional[List[str]] =
None,
448) -> argparse.Namespace:
450 Sets up a parser for command line arguments, parses them
and returns the
452 @return: An object containing the parsed command line arguments.
453 Arguments are accessed like they are attributes of the object,
454 i.e. [name_of_object].[desired_argument]
461 return get_argument_parser(modes).parse_args()
464def scripts_in_dir(dirpath: str, log: logging.Logger, ext=
"*") -> List[str]:
466 Returns all the files in the given dir (
and its subdirs) that have
467 the extension
'ext',
if an extension
is given (default: all extensions)
469 @param dirpath: The directory
in which we are looking
for files
470 @param log: logging.Logger object
471 @param ext: The extension of the files, which we are looking
for.
472 '*' is the wildcard-operator (=all extensions are accepted)
473 @return: A sorted list of all files
with the specified extension
in the
478 log.debug(f
"Collecting *{ext} files from {dirpath}")
489 validationpath.folder_name_html_static,
493 for root, dirs, files
in os.walk(dirpath):
496 if os.path.basename(root)
in blacklist:
500 for current_file
in files:
503 if current_file.endswith(ext):
504 results.append(os.path.join(root, current_file))
507 return sorted(results)
510def strip_ext(path: str) -> str:
512 Takes a path and returns only the name of the file, without the
513 extension on the file name
515 return os.path.splitext(os.path.split(path)[1])[0]
518def get_style(index: Optional[int], overall_item_count=1):
520 Takes an index and returns the corresponding line attributes,
521 i.e. LineColor, LineWidth
and LineStyle.
545 ls_index = {0:
"dashed", 1:
"solid", 2:
"dashdot"}
555 color = colors[index % len(colors)]
560 if overall_item_count == 1:
561 linestyle = linestyles[
"solid"]
565 linestyle = linestyles[ls_index[index % len(ls_index)]]
567 return ROOT.TAttLine(color, linestyle, linewidth)
570def index_from_revision(revision: str, work_folder: str) -> Optional[int]:
572 Takes the name of a revision and returns the corresponding index. Indices
573 are used to ensure that the color
and style of a revision
in a plot are
574 always the same, regardless of the displayed revisions.
575 Example: release-X
is always red,
and no other release get drawn
in red
if
576 release-X
is not selected
for display.
577 :param revision: A string containing the name of a revision
578 :param work_folder: The work folder containing the results
and plots
579 :
return: The index of the requested revision,
or None,
if no index could
580 be found
for 'revision'
583 revisions = available_revisions(work_folder) + ["reference"]
585 if revision
in revisions:
586 return revisions.index(revision)
591def get_log_file_paths(logger: logging.Logger) -> List[str]:
593 Returns list of paths that the FileHandlers of logger write to.
594 :param logger: logging.logger object.
595 :return: List of paths
598 for handler
in logger.handlers:
600 ret.append(handler.baseFilename)
601 except AttributeError:
606def get_terminal_width() -> int:
608 Returns width of terminal in characters,
or 80
if unknown.
610 Copied
from basf2 utils. However, we only compile the validation package
611 on b2master, so copy this here.
613 from shutil
import get_terminal_size
615 return get_terminal_size(fallback=(80, 24)).columns
619 success: Optional[Union[int, float]] =
None,
620 failure: Optional[Union[int, float]] =
None,
621 total: Optional[Union[int, float]] =
None,
623 rate_name=
"Success rate",
625 """ Keeping the morale up by commenting on success rates.
628 success: Number of successes
629 failure: Number of failures
630 total: success + failures (out of success, failure and total, exactly
631 2 have to be specified. If you want to use your own figure of
632 merit, just set total = 1.
and set success to a number between 0.0
633 (infernal) to 1.0 (stellar))
634 just_comment: Do
not add calculated percentage to
return string.
635 rate_name: How to refer to the calculated success rate.
638 Comment on your success rate (str).
641 n_nones = [success, failure, total].count(None)
643 if n_nones == 0
and total != success + failure:
645 "ERROR (congratulator): Specify 2 of the arguments 'success',"
646 "'failure', 'total'.",
652 "ERROR (congratulator): Specify 2 of the arguments 'success',"
653 "'failure', 'total'.",
659 total = success + failure
661 failure = total - success
663 success = total - failure
667 return "That wasn't really exciting, was it?"
669 success_rate = 100 * success / total
672 00.0:
"You're grounded!",
674 20.0:
"That's terrible!",
675 40.0:
"You can do better than that.",
676 50.0:
"That still requires some work.",
677 75.0:
"Three quarters! Almost there!",
678 80.0:
"Way to go ;)",
681 99.0:
"Nobel price!",
685 for value
in sorted(comments.keys(), reverse=
True):
686 if success_rate >= value:
687 comment = comments[value]
691 comment = comments[0]
696 return f
"{rate_name} {int(success_rate)}%. {comment}"
699def terminal_title_line(title="", subtitle="", level=0) -> str:
700 """ Print a title line in the terminal.
703 title (str): The title. If no title is given, only a separating line
705 subtitle (str): Subtitle.
706 level (int): The lower, the more dominantly the line will be styled.
708 linewidth = get_terminal_width()
712 char_dict = {0:
"=", 1:
"-", 2:
"~"}
714 for key
in sorted(char_dict.keys(), reverse=
True):
716 char = char_dict[key]
722 line = char * linewidth
729 ret += title.capitalize() +
"\n"
731 ret += subtitle +
"\n"
736def get_file_metadata(filename: str) -> str:
738 Retrieve the metadata for a file using ``b2file-metadata-show -a``.
741 metadata (str): File to get number of events
from.
744 (str): Metadata of file.
746 if not Path(filename).exists():
747 raise FileNotFoundError(f
"Could not find file {filename}")
752 proc = subprocess.run(
753 [
"b2file-metadata-show",
"-a", str(filename)],
754 stdout=subprocess.PIPE,
757 metadata = proc.stdout.decode(
"utf-8")
758 except subprocess.CalledProcessError
as e:
def get_results_folder(output_base_dir)