12from basf2
import statistics
13from ROOT
import PyConfig
15PyConfig.IgnoreCommandLineOptions =
True
27from typing
import List, Optional
35from validationscript
import Script, ScriptStatus
36from validationfunctions
import (
38 get_validation_folders,
40 parse_cmd_line_arguments,
44import validationfunctions
46import validationserver
48import validationscript
54import clustercontrolsge
55import clustercontroldrmaa
58pp = pprint.PrettyPrinter(depth=6, indent=1, width=80)
70 Add memory usage and execution time validation plots to the given root
71 file. The current root file will be used if the fileName is empty (
75 if not timing_methods:
76 timing_methods = [statistics.INIT, statistics.EVENT]
78 memory_methds = [statistics.INIT, statistics.EVENT]
81 save_dir = ROOT.gDirectory
84 plot_file = ROOT.TFile.Open(file_name,
"UPDATE")
87 job_desc = sys.argv[1]
91 h_global_timing = ROOT.TH1D(
92 prefix +
"GlobalTiming",
"Global Timing", 5, 0, 5
94 h_global_timing.SetStats(0)
95 h_global_timing.GetXaxis().SetTitle(
"method")
96 h_global_timing.GetYaxis().SetTitle(
"time/call [ms]")
97 h_global_timing.GetListOfFunctions().Add(
100 f
"""The (average) time of the different basf2 execution phases
101 for {job_desc}. The error bars show the rms of the time
105 h_global_timing.GetListOfFunctions().Add(
108 """There should be no significant and persistent increases in
109 the the run time of the methods. Only cases where the increase
110 compared to the reference or previous versions persists for at
111 least two consecutive revisions should be reported since the
112 measurements can be influenced by load from other processes on
113 the execution host.""",
117 h_global_timing.GetListOfFunctions().Add(
118 ROOT.TNamed(
"Contact", contact)
120 for (index, method)
in statistics.StatisticCounters.values.items():
121 method_name[method] = str(method)[0] + str(method).lower()[1:].replace(
126 h_global_timing.SetBinContent(
127 index + 1, statistics.get_global().time_mean(method) * 1e-6
129 h_global_timing.SetBinError(
130 index + 1, statistics.get_global().time_stddev(method) * 1e-6
132 h_global_timing.GetXaxis().SetBinLabel(index + 1, method_name[method])
133 h_global_timing.Write()
136 modules = statistics.modules
137 h_module_timing = ROOT.TH1D(
138 prefix +
"ModuleTiming",
"Module Timing", len(modules), 0, len(modules)
140 h_module_timing.SetStats(0)
141 h_module_timing.GetXaxis().SetTitle(
"module")
142 h_module_timing.GetYaxis().SetTitle(
"time/call [ms]")
143 h_module_timing.GetListOfFunctions().Add(
146 """There should be no significant and persistent increases in
147 the run time of a module. Only cases where the increase compared
148 to the reference or previous versions persists for at least two
149 consecutive revisions should be reported since the measurements
150 can be influenced by load from other processes on the execution
155 h_module_timing.GetListOfFunctions().Add(
156 ROOT.TNamed(
"Contact", contact)
158 for method
in timing_methods:
159 h_module_timing.SetTitle(f
"Module {method_name[method]} Timing")
160 h_module_timing.GetListOfFunctions().Add(
163 f
"""The (average) execution time of the {method_name[method]} method of modules
164 for {job_desc}. The error bars show the rms of the time
169 for modstat
in modules:
170 h_module_timing.SetBinContent(
171 index, modstat.time_mean(method) * 1e-6
173 h_module_timing.SetBinError(
174 index, modstat.time_stddev(method) * 1e-6
176 h_module_timing.GetXaxis().SetBinLabel(index, modstat.name)
178 h_module_timing.Write(f
"{prefix}{method_name[method]}Timing")
179 h_module_timing.GetListOfFunctions().RemoveLast()
182 memory_profile = ROOT.Belle2.PyStoreObj(
"VirtualMemoryProfile", 1)
184 memory_profile.obj().GetListOfFunctions().Add(
187 f
"The virtual memory usage vs. the event number for {job_desc}.",
190 memory_profile.obj().GetListOfFunctions().Add(
193 """The virtual memory usage should be flat for high event
194 numbers. If it keeps rising this is an indication of a memory
195 leak.<br>There should also be no significant increases with
196 respect to the reference (or previous revisions if no reference
201 memory_profile.obj().GetListOfFunctions().Add(
202 ROOT.TNamed(
"Contact", contact)
204 memory_profile.obj().Write(prefix +
"VirtualMemoryProfile")
207 memory_profile = ROOT.Belle2.PyStoreObj(
"RssMemoryProfile", 1)
209 memory_profile.obj().GetListOfFunctions().Add(
212 f
"The rss memory usage vs. the event number for {job_desc}.",
215 memory_profile.obj().GetListOfFunctions().Add(
218 """The rss memory usage should be flat for high event numbers.
219 If it keeps rising this is an indication of a memory
220 leak.<br>There should also be no significant increases with
221 respect to the reference (or previous revisions if no reference
222 exists). In the (rare) case that memory is swapped by the OS,
223 the rss memory usage can decrease.""",
227 memory_profile.obj().GetListOfFunctions().Add(
228 ROOT.TNamed(
"Contact", contact)
230 memory_profile.obj().Write(prefix +
"RssMemoryProfile")
233 sqrt_n = 1 / math.sqrt(statistics.get_global().calls() - 1)
234 h_module_memory = ROOT.TH1D(
235 prefix +
"ModuleMemory",
236 "Virtual Module Memory",
241 h_module_memory.SetStats(0)
242 h_module_memory.GetXaxis().SetTitle(
"module")
243 h_module_memory.GetYaxis().SetTitle(
"memory increase/call [kB]")
244 h_module_memory.GetListOfFunctions().Add(
247 f
"The (average) increase in virtual memory usage per call of the "
248 f
"{method_name[method]} method of modules for {job_desc}.",
251 h_module_memory.GetListOfFunctions().Add(
254 "The increase in virtual memory usage per call for each module "
255 "should be consistent with zero or the reference.",
259 h_module_memory.GetListOfFunctions().Add(
260 ROOT.TNamed(
"Contact", contact)
262 for method
in memory_methds:
263 h_module_memory.SetTitle(f
"Module {method_name[method]} Memory")
265 for modstat
in modules:
266 h_module_memory.SetBinContent(index, modstat.memory_mean(method))
267 h_module_memory.SetBinError(
268 index, modstat.memory_stddev(method) * sqrt_n
270 h_module_memory.GetXaxis().SetBinLabel(index, modstat.name)
272 h_module_memory.Write(f
"{prefix}{method_name[method]}Memory")
273 h_module_memory.GetListOfFunctions().RemoveLast()
280def event_timing_plot(
290 Add a validation histogram of event execution time to the given root file.
291 The current root file will be used if the fileName is empty (default).
292 The data file has to contain the profile information created by the Profile
297 job_desc = os.path.basename(sys.argv[0])
300 save_dir = ROOT.gDirectory
301 data = ROOT.TFile.Open(data_file)
302 tree = data.Get(
"tree")
303 entries = tree.GetEntries()
305 f
"Entry$>>hEventTime({int(entries)},-0.5,{int(entries - 1)}.5)",
306 "ProfileInfo.m_timeInSec",
310 h_event_time = data.Get(
"hEventTime")
311 h_event_time.SetDirectory(0)
318 plot_file = ROOT.TFile.Open(file_name,
"UPDATE")
321 stat = ROOT.gStyle.GetOptStat()
322 ROOT.gStyle.SetOptStat(101110)
323 h_timing = ROOT.TH1D(prefix +
"Timing",
"Event Timing", 100, 0, max_time)
324 h_timing.UseCurrentStyle()
325 h_timing.GetXaxis().SetTitle(
"time [s]")
326 h_timing.GetYaxis().SetTitle(
"events")
327 h_timing.GetListOfFunctions().Add(
330 f
"The distribution of event execution times for {job_desc}.",
333 h_timing.GetListOfFunctions().Add(
336 "The distribution should be consistent with the reference (or "
337 "previous revisions if no reference exists).",
341 h_timing.GetListOfFunctions().Add(ROOT.TNamed(
"Contact", contact))
342 for event
in range(1 + burn_in, entries + 1):
344 h_event_time.GetBinContent(event)
345 - h_event_time.GetBinContent(event - 1)
348 ROOT.gStyle.SetOptStat(stat)
355def draw_progress_bar(delete_lines: int, scripts: List[Script], barlength=50):
357 This function plots a progress bar of the validation, i.e. it shows which
358 percentage of the scripts has been executed yet.
359 It furthermore also shows which scripts are currently running, as well as
360 the total runtime of the validation.
362 @param delete_lines: The amount of lines which need to be deleted before
363 we can redraw the progress bar
364 @param scripts: List of all Script objects
365 @param barlength: The length of the progress bar (in characters)
366 @return: The number of lines that were printed by this function call.
367 Useful if this function is called repeatedly.
371 finished_scripts = len(
383 all_scripts = len(scripts)
384 percent = 100.0 * finished_scripts / all_scripts
387 runtime = int(timeit.default_timer() - get_start_time())
390 for i
in range(delete_lines):
391 print(
"\x1b[2K \x1b[1A", end=
" ")
395 for i
in range(barlength):
396 if i < int(barlength * percent / 100.0):
401 f
"\x1b[0G[{progressbar}] {percent:6.1f}% "
402 f
"({finished_scripts}/{all_scripts})"
406 print(f
"Runtime: {runtime}s")
410 os.path.basename(__.path)
419 print(f
"Running: {running[0]}")
420 for __
in running[1:]:
421 print(f
"{len('Running:') * ' '} {__}")
423 return len(running) + 2
428 This can be used to parse the execution intervals of validation scripts
429 and can check whether a script object is in the list of intervals
430 configured in this class.
435 Initializes the IntervalSelector class with a list of intervals which
444 checks whether the interval listed in a script object's header is
448 return script_object.interval
in self.
intervals
461 This is the class that provides all global variables, like 'list_of_files'
462 etc. There is only one instance of this class with name 'validation. This
463 allows to use some kind of namespace, i.e. global variables will always be
464 referenced as validation.[name of variable]. This makes it easier to
465 distinguish them from local variables that only exist within the scope of a
468 @var tag: The name of the folder within the results directory
469 @var log: Reference to the logging object for this validation instance
470 @var basepaths: The paths to the local and central release directory
471 @var scripts: List of all Script objects for steering files
472 @var packages: List of all packages which contributed scripts
473 @var basf2_options: The options to be given to the basf2 command
474 @var mode: Whether to run locally or on a cluster
475 @var quiet: No progress bar in quiet mode
476 @var dry: Dry runs do not actually start any scripts (for debugging)
481 The default constructor. Initializes all those variables that will be
482 globally accessible later on. Does not return anything.
494 self.work_folder = os.path.abspath(os.getcwd())
499 self.log = self.create_log()
503 self.scripts: List[Script] = []
506 self.packages: List[str] = []
512 self.ignored_packages = [
"validation-test"]
516 self.basf2_options =
""
531 self.ignore_dependencies =
False
537 self.running_script_reporting_interval = 30
542 self.script_max_runtime_in_minutes = 60 * 5
547 def get_useable_basepath(self):
549 Checks if a local path is available. If only a central release is
550 available, return the path to this central release
552 if self.basepaths[
"local"]:
553 return self.basepaths[
"local"]
555 return self.basepaths[
"central"]
558 def get_available_job_control():
560 insert the possible backend controls, they will be checked via their
561 is_supported method if they actually can be executed in the current
572 def get_available_job_control_names():
573 return [c.name()
for c
in Validation.get_available_job_control()]
575 def build_dependencies(self):
577 This method loops over all Script objects in self.scripts and
578 calls their compute_dependencies()-method.
581 for script_object
in self.scripts:
582 script_object.compute_dependencies(self.scripts)
585 for script_object
in self.scripts:
588 script_object, reason=f
"Depends on '{script_object.path}'"
591 def build_headers(self):
593 This method loops over all Script objects in self.scripts and
594 calls their load_header()-method.
597 for script_object
in self.scripts:
598 script_object.load_header()
600 def skip_script(self, script_object, reason=""):
602 This method sets the status of the given script and all dependent ones
604 @param script_object: Script object to be skipped.
605 @param reason: Reason for skipping object
610 if script_object.status
not in [
614 self.log.warning(
"Skipping " + script_object.path)
616 self.log.debug(f
"Reason for skipping: {reason}.")
620 for dependent_script
in self.scripts:
621 if script_object
in dependent_script.dependencies:
624 reason=f
"Depends on '{script_object.path}'",
627 def create_log(self) -> logging.Logger:
630 We use the logging module to create an object which allows us to
631 comfortably log everything that happens during the execution of
632 this script and even have different levels of importance, such as
638 log = logging.getLogger(
"validate_basf2")
639 log.setLevel(logging.DEBUG)
646 logging.addLevelName(logging.NOTE,
"NOTE")
647 log.note =
lambda msg, *args: log._log(logging.NOTE, msg, args)
655 console_handler = logging.StreamHandler()
656 console_handler.setLevel(logging.NOTE)
659 console_format = logging.Formatter(
"%(message)s")
660 console_handler.setFormatter(console_format)
663 log.addHandler(console_handler)
670 log_dir = self.get_log_folder()
671 if not os.path.exists(log_dir):
672 print(
"Creating " + log_dir)
676 file_handler = logging.FileHandler(
677 os.path.join(log_dir,
"validate_basf2.log"),
"w+"
679 file_handler.setLevel(logging.DEBUG)
683 file_format = logging.Formatter(
684 "%(asctime)s - %(module)s - " "%(levelname)s - %(message)s",
685 datefmt=
"%Y-%m-%d %H:%M:%S",
687 file_handler.setFormatter(file_format)
690 log.addHandler(file_handler)
693 def collect_steering_files(self, interval_selector):
695 This function will collect all steering files from the local and
696 central release directory.
701 validation_folders = get_validation_folders(
702 "local", self.basepaths, self.log
706 for (package, folder)
in get_validation_folders(
707 "central", self.basepaths, self.log
709 if package
not in validation_folders:
710 validation_folders[package] = folder
713 for ignored
in self.ignored_packages:
714 if ignored
in validation_folders:
715 del validation_folders[ignored]
718 self.packages = list(validation_folders.keys())
722 for (package, folder)
in validation_folders.items():
725 c_files = scripts_in_dir(folder, self.log,
".C")
726 py_files = scripts_in_dir(folder, self.log,
".py")
727 for steering_file
in c_files + py_files:
728 script = Script(steering_file, package, self.log)
732 interval_selector.in_interval(script)
733 and not script.noexecute
735 self.scripts.append(script)
740 def get_log_folder(self):
742 Get the log folder for this validation run. The command log
743 files (successful, failed) scripts will be recorded there
747 def log_failed(self):
749 This method logs all scripts with property failed into a single file
750 to be read in run_validation_server.py
753 failed_log_path = os.path.join(
754 self.get_log_folder(),
"list_of_failed_scripts.log"
756 self.log.note(f
"Writing list of failed scripts to {failed_log_path}.")
761 for script
in self.scripts
765 with open(failed_log_path,
"w+")
as list_failed:
767 for script
in failed_scripts:
768 list_failed.write(script.path.split(
"/")[-1] +
"\n")
770 def log_skipped(self):
772 This method logs all scripts with property skipped into a single file
773 to be read in run_validation_server.py
776 skipped_log_path = os.path.join(
777 self.get_log_folder(),
"list_of_skipped_scripts.log"
779 self.log.note(f
"Writing list of skipped scripts to {skipped_log_path}.")
784 for script
in self.scripts
788 with open(skipped_log_path,
"w+")
as list_skipped:
790 for script
in skipped_scripts:
791 list_skipped.write(script.path.split(
"/")[-1] +
"\n")
793 def report_on_scripts(self):
795 Print a summary about all scripts, especially highlighting
796 skipped and failed scripts.
800 script.package +
"/" + script.name
801 for script
in self.scripts
805 script.package +
"/" + script.name
806 for script
in self.scripts
812 terminal_title_line(
"Summary of script execution", level=0)
814 self.log.note(f
"Total number of scripts: {len(self.scripts)}")
818 f
"{len(skipped_scripts)}/{len(self.scripts)} scripts were skipped"
820 for s
in skipped_scripts:
821 self.log.note(f
"* {s}")
824 self.log.note(
"No scripts were skipped. Nice!")
829 f
"{len(failed_scripts)}/{len(self.scripts)} scripts failed"
831 for s
in failed_scripts:
832 self.log.note(f
"* {s}")
835 self.log.note(
"No scripts failed. Nice!")
840 total=len(self.scripts),
841 failure=len(failed_scripts) + len(skipped_scripts),
845 def set_runtime_data(self):
847 This method sets runtime property of each script.
852 with open(path)
as runtimes:
855 for line
in runtimes:
856 run_times[line.split(
"=")[0].strip()] = line.split(
"=")[
861 for script
in self.scripts:
863 script.runtime = float(run_times[script.name])
868 for dict_key
in run_times:
869 suma += float(run_times[dict_key])
870 script.runtime = suma / len(run_times)
872 def get_script_by_name(self, name: str) -> Optional[Script]:
877 l_arr = [s
for s
in self.scripts
if s.name == name]
883 def apply_package_selection(
884 self, selected_packages, ignore_dependencies=False
887 Only select packages from a specific set of packages, but still
888 honor the dependencies to outside scripts which may exist
891 to_keep_dependencies = set()
895 if not ignore_dependencies:
896 for script_obj
in self.scripts:
897 if script_obj.package
in selected_packages:
898 for dep
in script_obj.dependencies:
899 to_keep_dependencies.add(dep.unique_name())
904 for s
in self.scripts
905 if (s.package
in selected_packages)
906 or (s.unique_name()
in to_keep_dependencies)
910 packages = {s.package
for s
in self.scripts}
911 packages_not_found = list(set(selected_packages) - packages)
912 if packages_not_found:
914 f
"You asked to select the package(s) {', '.join(packages_not_found)}, but they were not found."
917 self.log.warning(msg)
919 def apply_script_selection(
920 self, script_selection, ignore_dependencies=False
923 This method will take the validation file name ( e.g.
924 "FullTrackingValidation.py" ), determine all the script it depends on
925 and set the status of these scripts to "waiting", The status of all
926 other scripts will be set to "skipped", which means they will not be
927 executed in the validation run. If ignore_dependencies is True,
928 dependencies will also be set to "skipped".
933 Script.sanitize_file_name(s)
for s
in script_selection
936 scripts_to_enable = set()
939 for script
in script_selection:
940 scripts_to_enable.add(script)
941 script_obj = self.get_script_by_name(script)
943 if script_obj
is None:
945 f
"Script with name {script} cannot be found, skipping for "
950 others = script_obj.get_recursive_dependencies(self.scripts)
951 if not ignore_dependencies:
952 scripts_to_enable = scripts_to_enable.union(others)
955 for script_obj
in self.scripts:
956 if script_obj.name
in scripts_to_enable:
958 f
"Enabling script {script_obj.name} because it was "
959 f
"selected or a selected script depends on it."
964 f
"Disabling script {script_obj.name} because it was "
970 script_names = {Script.sanitize_file_name(s.name)
for s
in self.scripts}
971 scripts_not_found = set(script_selection) - script_names
972 if scripts_not_found:
974 f
"You requested script(s) {', '.join(scripts_not_found)}, but they seem to not have been found."
977 self.log.warning(msg)
979 def apply_script_caching(self):
980 cacheable_scripts = [s
for s
in self.scripts
if s.is_cacheable]
983 self.work_folder, self.tag
986 for s
in cacheable_scripts:
988 outfiles = s.output_files
991 full_path = os.path.join(output_dir_datafiles, of)
992 files_exist = files_exist
and os.path.isfile(full_path)
1000 for script
in self.scripts:
1001 for dep_script
in script.dependencies:
1005 script.dependencies.remove(dep_script)
1007 def store_run_results_json(self, git_hash):
1012 for p
in self.packages:
1013 this_package_scrits = [s
for s
in self.scripts
if s.package == p]
1014 json_scripts = [s.to_json(self.tag)
for s
in this_package_scrits]
1020 json_package.append(
1022 p, scriptfiles=json_scripts, fail_count=fail_count
1029 creation_date=datetime.datetime.now().strftime(
"%Y-%m-%d %H:%M"),
1031 packages=json_package,
1036 self.work_folder, self.tag
1041 def add_script(self, script: Script):
1043 Explicitly add a script object. In normal operation, scripts are
1044 auto-discovered but this method is useful for testing
1047 self.scripts.append(script)
1050 def sort_scripts(script_list: List[Script]):
1052 Sort the list of scripts that have to be processed by runtime,
1053 execute slow scripts first If no runtime information is available
1054 from the last execution, run the scripts in the validation package
1055 first because they are log running and used as input for other scripts
1058 key=
lambda x: x.runtime
or x.package ==
"validation", reverse=
True
1061 def set_tag(self, tag):
1063 Update the validation tag to enable utils to fetch revision-wise files
1064 from the same validation instance.
1071 This method runs the actual validation, i.e. it loops over all
1072 scripts, checks which of them are ready for execution, and runs them.
1077 self.log.note(
"Initializing local job control for plotting.")
1078 local_control = localcontrol.Local(
1079 max_number_of_processes=self.parallel
1085 self.log.note(
"Selecting job control for all other jobs.")
1087 selected_controls = [
1088 c
for c
in self.get_available_job_control()
if c.name() == self.mode
1091 if not len(selected_controls) == 1:
1092 print(f
"Selected mode {self.mode} does not exist")
1095 selected_control = selected_controls[0]
1098 f
"Controller: {selected_control.name()} ({selected_control.description()})"
1101 if not selected_control.is_supported():
1102 print(f
"Selected mode {self.mode} is not supported on your system")
1106 if selected_control.name() ==
"local":
1107 control = selected_control(max_number_of_processes=self.parallel)
1109 control = selected_control()
1112 src_basepath = self.get_useable_basepath()
1115 f
"Git hash of repository located at {src_basepath} is {git_hash}"
1121 os.path.exists(
"./runtimes.dat")
1122 and os.stat(
"./runtimes.dat").st_size
1124 self.set_runtime_data()
1125 if os.path.exists(
"./runtimes-old.dat"):
1128 os.remove(
"./runtimes-old.dat")
1129 if self.mode ==
"local":
1131 shutil.copyfile(
"./runtimes.dat",
"./runtimes-old.dat")
1135 if self.mode ==
"local":
1136 runtimes = open(
"./runtimes.dat",
"w+")
1140 progress_bar_lines = 0
1144 remaining_scripts = [
1146 for script
in self.scripts
1152 self.sort_scripts(remaining_scripts)
1154 def handle_finished_script(script_obj: Script):
1156 self.log.debug(
"Finished: " + script_obj.path)
1159 script_obj.runtime = time.time() - script_obj.start_time
1160 if self.mode ==
"local":
1162 script_obj.name +
"=" + str(script_obj.runtime) +
"\n"
1167 script_obj.returncode = result[1]
1171 f
"exit_status was {result[1]} for {script_obj.path}"
1173 script_obj.remove_output_files()
1178 reason=f
"Script '{script_object.path}' failed and we set it's status to skipped so that all dependencies " +
1179 "are also skipped.",
1185 for dependent_script
in remaining_scripts:
1186 if script_obj
in dependent_script.dependencies:
1187 dependent_script.dependencies.remove(script_obj)
1193 for script
in remaining_scripts
1198 for script
in remaining_scripts
1202 f
"Finished [{len(waiting)},{len(running)}]: {script_obj.path} -> {script_obj.status}"
1205 def handle_unfinished_script(script_obj: Script):
1207 time.time() - script_obj.last_report_time
1208 ) / 60.0 > self.running_script_reporting_interval:
1210 f
"Script {script_obj.name_not_sanitized} running since {time.time() - script_obj.start_time} seconds"
1216 script_obj.last_report_time = time.time()
1220 total_runtime_in_minutes = (
1221 time.time() - script_obj.start_time
1224 total_runtime_in_minutes
1225 > self.script_max_runtime_in_minutes
1230 f
"Script {script_obj.path} did not finish after "
1231 f
"{total_runtime_in_minutes} minutes, attempting to "
1235 script_obj.control.terminate(script_obj)
1239 reason=f
"Script '{script_object.path}' did not finish in "
1240 f
"time, so we're setting it to 'failed' so that all "
1241 f
"dependent scripts will be skipped.",
1244 def handle_waiting_script(script_obj: Script):
1247 if script_obj.output_files:
1248 script_obj.control = control
1250 script_obj.control = local_control
1253 if script_obj.control.available():
1256 self.log.debug(
"Starting " + script_obj.path)
1260 self.log.warning(f
"Starting of {script_obj.path} failed")
1265 script_obj.control.execute(
1266 script_obj, self.basf2_options, self.dry, self.tag
1270 script_obj.start_time = time.time()
1271 script_obj.last_report_time = time.time()
1277 for _
in remaining_scripts
1282 for _
in remaining_scripts
1286 f
"Started [{len(waiting)},{len(running)}]: {script_obj.path}"
1290 while remaining_scripts:
1293 for script_object
in remaining_scripts:
1299 result = script_object.control.is_job_finished(
1305 handle_finished_script(script_object)
1307 handle_unfinished_script(script_object)
1311 elif not script_object.dependencies:
1312 handle_waiting_script(script_object)
1315 remaining_scripts = [
1317 for script
in remaining_scripts
1322 self.sort_scripts(remaining_scripts)
1329 progress_bar_lines = draw_progress_bar(
1330 progress_bar_lines, self.scripts
1338 if self.mode ==
"local":
1342 self.store_run_results_json(git_hash)
1345 def create_plots(self):
1347 This method prepares the html directory for the plots if necessary
1348 and creates the plots that include the results from this validation.
1355 os.makedirs(html_folder, exist_ok=
True)
1357 if not os.path.exists(results_folder):
1359 f
"Folder {results_folder} not found in "
1360 f
"the work directory {self.work_folder}, please run "
1361 f
"b2validation first"
1366 def save_metadata(self):
1368 This method fetches the metadata of all the data files produced
1369 during the validation run and saves them in individual text files of the
1370 same name (with .txt appended at the end) inside the results folder.
1374 result_folder = self.get_log_folder()
1376 if not os.path.exists(result_folder):
1378 f
"Folder {result_folder} not found in "
1379 f
"the work directory {self.work_folder}, please run "
1380 f
"b2validation first"
1384 for file
in os.listdir(result_folder):
1385 if file.endswith(
".root"):
1387 os.path.join(result_folder, file))
1389 metadata_file = os.path.join(result_folder, f
'{file}.txt')
1392 os.remove(metadata_file)
1393 except FileNotFoundError:
1395 with open(metadata_file,
'a')
as out:
1396 out.write(f
'{metadata}\n')
1398 self.log.debug(f
"b2file-metadata-show failed for {file}.")
1401def execute(tag=None, is_test=None):
1403 Parses the command line and executes the full validation suite
1404 :param tag The name that will be used for the current revision.
1405 Default None means automatic.
1406 :param is_test Run in test mode? Default None means that we read this
1407 from the command line arguments (which default to False).
1420 os.environ.get(
"BELLE2_RELEASE_DIR",
None)
is None
1421 and os.environ.get(
"BELLE2_LOCAL_DIR",
None)
is None
1423 sys.exit(
"Error: No basf2 release set up!")
1433 cmd_arguments = parse_cmd_line_arguments(
1434 modes=Validation.get_available_job_control_names()
1440 cmd_arguments.tag = tag
1441 if is_test
is not None:
1442 cmd_arguments.test = is_test
1445 validation = Validation(cmd_arguments.tag)
1448 validation.log.note(
"Starting validation...")
1449 validation.log.note(
1450 f
'Results will stored in a folder named "{validation.tag}"...'
1452 validation.log.note(
1453 f
"The (full) log file(s) can be found at {', '.join(get_log_file_paths(validation.log))}"
1455 validation.log.note(
1456 "Please check these logs when encountering "
1457 "unexpected results, as most of the warnings and "
1458 "errors are not written to stdout/stderr."
1462 if cmd_arguments.options:
1463 validation.basf2_options =
" ".join(cmd_arguments.options)
1464 validation.log.note(
1465 f
"Received arguments for basf2: {validation.basf2_options}"
1469 validation.mode = cmd_arguments.mode
1472 validation.parallel = cmd_arguments.parallel
1475 if cmd_arguments.quiet:
1476 validation.log.note(
"Running in quiet mode (no progress bar).")
1477 validation.quiet =
True
1480 if cmd_arguments.dry:
1481 validation.log.note(
1482 "Performing a dry run; no scripts will be " "started."
1484 validation.dry =
True
1487 if cmd_arguments.test:
1488 validation.log.note(
"Running in test mode")
1489 validation.ignored_packages = []
1490 cmd_arguments.packages = [
"validation-test"]
1492 validation.log.note(
1493 f
"Release Folder: {validation.basepaths['central']}"
1495 validation.log.note(
1496 f
"Local Folder: {validation.basepaths['local']}"
1500 validation.log.note(
"Collecting steering files...")
1501 intervals = cmd_arguments.intervals.split(
",")
1505 validation.log.note(
"Building headers for Script objects...")
1506 validation.build_headers()
1510 if not cmd_arguments.select_ignore_dependencies:
1511 validation.log.note(
"Building dependencies for Script objects...")
1512 validation.build_dependencies()
1514 if cmd_arguments.packages:
1515 validation.log.note(
1516 "Applying package selection for the following package(s): "
1517 +
", ".join(cmd_arguments.packages)
1519 validation.apply_package_selection(cmd_arguments.packages)
1522 if cmd_arguments.select:
1523 validation.log.note(
"Applying selection for validation scripts")
1524 validation.apply_script_selection(
1525 cmd_arguments.select, ignore_dependencies=
False
1530 if cmd_arguments.select_ignore_dependencies:
1531 validation.log.note(
1532 "Applying selection for validation scripts, "
1533 "ignoring their dependencies"
1535 validation.apply_script_selection(
1536 cmd_arguments.select_ignore_dependencies,
1537 ignore_dependencies=
True,
1542 if cmd_arguments.use_cache:
1543 validation.log.note(
"Checking for cached script output")
1544 validation.apply_script_caching()
1547 if cmd_arguments.max_run_time
is not None:
1548 if cmd_arguments.max_run_time > 0:
1549 validation.log.note(
1550 f
"Setting maximal run time of the steering files "
1551 f
"to {cmd_arguments.max_run_time} minutes."
1554 validation.log.note(
1555 "Disabling run time limitation of steering files as "
1556 "requested (max run time set to <= 0)."
1558 validation.script_max_runtime_in_minutes = (
1559 cmd_arguments.max_run_time
1563 validation.log.note(
"Starting the validation...")
1564 validation.run_validation()
1567 validation.log.note(
"Validation finished...")
1568 if not validation.dry:
1569 validation.log.note(
"Start creating plots...")
1570 validation.create_plots()
1571 validation.log.note(
"Plots have been created...")
1573 validation.log.note(
1574 "Skipping plot creation " "(dry run)..."
1577 if cmd_arguments.send_mails:
1579 validation.log.note(
"Start sending mails...")
1581 if cmd_arguments.send_mails_mode ==
"incremental":
1583 elif cmd_arguments.send_mails_mode ==
"full":
1587 mails.send_all_mails(incremental=incremental)
1588 validation.log.note(
1589 f
"Save mail data to {validation.get_log_folder()}"
1593 validation.report_on_scripts()
1595 validation.save_metadata()
1598 validation.log.note(
1599 f
"Validation finished! Total runtime: {int(timeit.default_timer() - get_start_time())}s"
1602 if cmd_arguments.view:
1606 except KeyboardInterrupt:
1607 validation.log.note(
"Validation terminated by user!")
Provides functionality to send mails in case of failed scripts / validation plots.
list intervals
stores the intervals which have been selected
__init__(self, intervals)
bool in_interval(self, Script script_object)
str congratulator(Optional[Union[int, float]] success=None, Optional[Union[int, float]] failure=None, Optional[Union[int, float]] total=None, just_comment=False, rate_name="Success rate")
Optional[str] get_compact_git_hash(str repo_folder)
str get_file_metadata(str filename)
get_html_folder(output_base_dir)
get_results_tag_folder(output_base_dir, tag)
get_results_tag_revision_file(output_base_dir, tag)
get_results_folder(output_base_dir)
get_results_runtime_file(output_base_dir)
create_plots(revisions=None, force=False, Optional[Queue] process_queue=None, work_folder=".")
run_server(ip="127.0.0.1", port=8000, parse_command_line=False, open_site=False, dry_run=False)