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' etc. There
is only one instance of this
class with name
'validation. This
462 allows to use some kind of namespace, i.e. global variables will always be
463 referenced
as validation.[name of variable]. This makes it easier to
464 distinguish them
from local variables that only exist within the scope of a
467 @var tag: The name of the folder within the results directory
468 @var log: Reference to the logging object
for this validation instance
469 @var basepaths: The paths to the local
and central release directory
470 @var scripts: List of all Script objects
for steering files
471 @var packages: List of all packages which contributed scripts
472 @var basf2_options: The options to be given to the basf2 command
473 @var mode: Whether to run locally
or on a cluster
474 @var quiet: No progress bar
in quiet mode
475 @var dry: Dry runs do
not actually start any scripts (
for debugging)
480 The default constructor. Initializes all those variables that will be
481 globally accessible later on. Does not return anything.
493 self.work_folder = os.path.abspath(os.getcwd())
498 self.log = self.create_log()
502 self.scripts: List[Script] = []
505 self.packages: List[str] = []
511 self.ignored_packages = [
"validation-test"]
515 self.basf2_options =
""
530 self.ignore_dependencies =
False
536 self.running_script_reporting_interval = 30
541 self.script_max_runtime_in_minutes = 60 * 5
546 def get_useable_basepath(self):
548 Checks if a local path
is available. If only a central release
is
549 available,
return the path to this central release
551 if self.basepaths[
"local"]:
552 return self.basepaths[
"local"]
554 return self.basepaths[
"central"]
557 def get_available_job_control():
559 insert the possible backend controls, they will be checked via their
560 is_supported method if they actually can be executed
in the current
571 def get_available_job_control_names():
572 return [c.name()
for c
in Validation.get_available_job_control()]
574 def build_dependencies(self):
576 This method loops over all Script objects in self.scripts
and
577 calls their compute_dependencies()-method.
580 for script_object
in self.scripts:
581 script_object.compute_dependencies(self.scripts)
584 for script_object
in self.scripts:
587 script_object, reason=f
"Depends on '{script_object.path}'"
590 def build_headers(self):
592 This method loops over all Script objects in self.scripts
and
593 calls their load_header()-method.
596 for script_object
in self.scripts:
597 script_object.load_header()
599 def skip_script(self, script_object, reason=""):
601 This method sets the status of the given script and all dependent ones
603 @param script_object: Script object to be skipped.
604 @param reason: Reason
for skipping object
609 if script_object.status
not in [
613 self.log.warning(
"Skipping " + script_object.path)
615 self.log.debug(f
"Reason for skipping: {reason}.")
619 for dependent_script
in self.scripts:
620 if script_object
in dependent_script.dependencies:
623 reason=f
"Depends on '{script_object.path}'",
626 def create_log(self) -> logging.Logger:
629 We use the logging module to create an object which allows us to
630 comfortably log everything that happens during the execution of
631 this script and even have different levels of importance, such
as
637 log = logging.getLogger(
"validate_basf2")
638 log.setLevel(logging.DEBUG)
645 logging.addLevelName(logging.NOTE,
"NOTE")
646 log.note =
lambda msg, *args: log._log(logging.NOTE, msg, args)
654 console_handler = logging.StreamHandler()
655 console_handler.setLevel(logging.NOTE)
658 console_format = logging.Formatter(
"%(message)s")
659 console_handler.setFormatter(console_format)
662 log.addHandler(console_handler)
669 log_dir = self.get_log_folder()
670 if not os.path.exists(log_dir):
671 print(
"Creating " + log_dir)
675 file_handler = logging.FileHandler(
676 os.path.join(log_dir,
"validate_basf2.log"),
"w+"
678 file_handler.setLevel(logging.DEBUG)
682 file_format = logging.Formatter(
683 "%(asctime)s - %(module)s - " "%(levelname)s - %(message)s",
684 datefmt=
"%Y-%m-%d %H:%M:%S",
686 file_handler.setFormatter(file_format)
689 log.addHandler(file_handler)
692 def collect_steering_files(self, interval_selector):
694 This function will collect all steering files from the local
and
695 central release directory.
700 validation_folders = get_validation_folders(
701 "local", self.basepaths, self.log
705 for (package, folder)
in get_validation_folders(
706 "central", self.basepaths, self.log
708 if package
not in validation_folders:
709 validation_folders[package] = folder
712 for ignored
in self.ignored_packages:
713 if ignored
in validation_folders:
714 del validation_folders[ignored]
717 self.packages = list(validation_folders.keys())
721 for (package, folder)
in validation_folders.items():
724 c_files = scripts_in_dir(folder, self.log,
".C")
725 py_files = scripts_in_dir(folder, self.log,
".py")
726 for steering_file
in c_files + py_files:
727 script = Script(steering_file, package, self.log)
731 interval_selector.in_interval(script)
732 and not script.noexecute
734 self.scripts.append(script)
739 def get_log_folder(self):
741 Get the log folder for this validation run. The command log
742 files (successful, failed) scripts will be recorded there
746 def log_failed(self):
748 This method logs all scripts with property failed into a single file
749 to be read
in run_validation_server.py
752 failed_log_path = os.path.join(
753 self.get_log_folder(), "list_of_failed_scripts.log"
755 self.log.note(f
"Writing list of failed scripts to {failed_log_path}.")
760 for script
in self.scripts
764 with open(failed_log_path,
"w+")
as list_failed:
766 for script
in failed_scripts:
767 list_failed.write(script.path.split(
"/")[-1] +
"\n")
769 def log_skipped(self):
771 This method logs all scripts with property skipped into a single file
772 to be read
in run_validation_server.py
775 skipped_log_path = os.path.join(
776 self.get_log_folder(), "list_of_skipped_scripts.log"
778 self.log.note(f
"Writing list of skipped scripts to {skipped_log_path}.")
783 for script
in self.scripts
787 with open(skipped_log_path,
"w+")
as list_skipped:
789 for script
in skipped_scripts:
790 list_skipped.write(script.path.split(
"/")[-1] +
"\n")
792 def report_on_scripts(self):
794 Print a summary about all scripts, especially highlighting
795 skipped and failed scripts.
799 script.package + "/" + script.name
800 for script
in self.scripts
804 script.package +
"/" + script.name
805 for script
in self.scripts
811 terminal_title_line(
"Summary of script execution", level=0)
813 self.log.note(f
"Total number of scripts: {len(self.scripts)}")
817 f
"{len(skipped_scripts)}/{len(self.scripts)} scripts were skipped"
819 for s
in skipped_scripts:
820 self.log.note(f
"* {s}")
823 self.log.note(
"No scripts were skipped. Nice!")
828 f
"{len(failed_scripts)}/{len(self.scripts)} scripts failed"
830 for s
in failed_scripts:
831 self.log.note(f
"* {s}")
834 self.log.note(
"No scripts failed. Nice!")
839 total=len(self.scripts),
840 failure=len(failed_scripts) + len(skipped_scripts),
844 def set_runtime_data(self):
846 This method sets runtime property of each script.
851 with open(path)
as runtimes:
854 for line
in runtimes:
855 run_times[line.split(
"=")[0].strip()] = line.split(
"=")[
860 for script
in self.scripts:
862 script.runtime = float(run_times[script.name])
867 for dict_key
in run_times:
868 suma += float(run_times[dict_key])
869 script.runtime = suma / len(run_times)
871 def get_script_by_name(self, name: str) -> Optional[Script]:
876 l_arr = [s for s
in self.scripts
if s.name == name]
882 def apply_package_selection(
883 self, selected_packages, ignore_dependencies=False
886 Only select packages from a specific set of packages, but still
887 honor the dependencies to outside scripts which may exist
890 to_keep_dependencies = set()
894 if not ignore_dependencies:
895 for script_obj
in self.scripts:
896 if script_obj.package
in selected_packages:
897 for dep
in script_obj.dependencies:
898 to_keep_dependencies.add(dep.unique_name())
903 for s
in self.scripts
904 if (s.package
in selected_packages)
905 or (s.unique_name()
in to_keep_dependencies)
909 packages = {s.package
for s
in self.scripts}
910 packages_not_found = list(set(selected_packages) - packages)
911 if packages_not_found:
913 f
"You asked to select the package(s) {', '.join(packages_not_found)}, but they were not found."
916 self.log.warning(msg)
918 def apply_script_selection(
919 self, script_selection, ignore_dependencies=False
922 This method will take the validation file name ( e.g.
923 "FullTrackingValidation.py" ), determine all the script it depends on
924 and set the status of these scripts to
"waiting", The status of all
925 other scripts will be set to
"skipped", which means they will
not be
926 executed
in the validation run. If ignore_dependencies
is True,
927 dependencies will also be set to
"skipped".
932 Script.sanitize_file_name(s)
for s
in script_selection
935 scripts_to_enable = set()
938 for script
in script_selection:
939 scripts_to_enable.add(script)
940 script_obj = self.get_script_by_name(script)
942 if script_obj
is None:
944 f
"Script with name {script} cannot be found, skipping for "
949 others = script_obj.get_recursive_dependencies(self.scripts)
950 if not ignore_dependencies:
951 scripts_to_enable = scripts_to_enable.union(others)
954 for script_obj
in self.scripts:
955 if script_obj.name
in scripts_to_enable:
957 f
"Enabling script {script_obj.name} because it was "
958 f
"selected or a selected script depends on it."
963 f
"Disabling script {script_obj.name} because it was "
969 script_names = {Script.sanitize_file_name(s.name)
for s
in self.scripts}
970 scripts_not_found = set(script_selection) - script_names
971 if scripts_not_found:
973 f
"You requested script(s) {', '.join(scripts_not_found)}, but they seem to not have been found."
976 self.log.warning(msg)
978 def apply_script_caching(self):
979 cacheable_scripts = [s
for s
in self.scripts
if s.is_cacheable]
982 self.work_folder, self.tag
985 for s
in cacheable_scripts:
987 outfiles = s.output_files
990 full_path = os.path.join(output_dir_datafiles, of)
991 files_exist = files_exist
and os.path.isfile(full_path)
999 for script
in self.scripts:
1000 for dep_script
in script.dependencies:
1004 script.dependencies.remove(dep_script)
1006 def store_run_results_json(self, git_hash):
1011 for p
in self.packages:
1012 this_package_scrits = [s
for s
in self.scripts
if s.package == p]
1013 json_scripts = [s.to_json(self.tag)
for s
in this_package_scrits]
1019 json_package.append(
1021 p, scriptfiles=json_scripts, fail_count=fail_count
1028 creation_date=datetime.datetime.now().strftime(
"%Y-%m-%d %H:%M"),
1030 packages=json_package,
1035 self.work_folder, self.tag
1040 def add_script(self, script: Script):
1042 Explicitly add a script object. In normal operation, scripts are
1043 auto-discovered but this method is useful
for testing
1046 self.scripts.append(script)
1049 def sort_scripts(script_list: List[Script]):
1051 Sort the list of scripts that have to be processed by runtime,
1052 execute slow scripts first If no runtime information is available
1053 from the last execution, run the scripts
in the validation package
1054 first because they are log running
and used
as input
for other scripts
1057 key=lambda x: x.runtime
or x.package ==
"validation", reverse=
True
1060 def set_tag(self, tag):
1062 Update the validation tag to enable utils to fetch revision-wise files
1063 from the same validation instance.
1070 This method runs the actual validation, i.e. it loops over all
1071 scripts, checks which of them are ready for execution,
and runs them.
1076 self.log.note(
"Initializing local job control for plotting.")
1077 local_control = localcontrol.Local(
1078 max_number_of_processes=self.parallel
1084 self.log.note(
"Selecting job control for all other jobs.")
1086 selected_controls = [
1087 c
for c
in self.get_available_job_control()
if c.name() == self.mode
1090 if not len(selected_controls) == 1:
1091 print(f
"Selected mode {self.mode} does not exist")
1094 selected_control = selected_controls[0]
1097 f
"Controller: {selected_control.name()} ({selected_control.description()})"
1100 if not selected_control.is_supported():
1101 print(f
"Selected mode {self.mode} is not supported on your system")
1105 if selected_control.name() ==
"local":
1106 control = selected_control(max_number_of_processes=self.parallel)
1108 control = selected_control()
1111 src_basepath = self.get_useable_basepath()
1114 f
"Git hash of repository located at {src_basepath} is {git_hash}"
1120 os.path.exists(
"./runtimes.dat")
1121 and os.stat(
"./runtimes.dat").st_size
1123 self.set_runtime_data()
1124 if os.path.exists(
"./runtimes-old.dat"):
1127 os.remove(
"./runtimes-old.dat")
1128 if self.mode ==
"local":
1130 shutil.copyfile(
"./runtimes.dat",
"./runtimes-old.dat")
1134 if self.mode ==
"local":
1135 runtimes = open(
"./runtimes.dat",
"w+")
1139 progress_bar_lines = 0
1143 remaining_scripts = [
1145 for script
in self.scripts
1151 self.sort_scripts(remaining_scripts)
1153 def handle_finished_script(script_obj: Script):
1155 self.log.debug(
"Finished: " + script_obj.path)
1158 script_obj.runtime = time.time() - script_obj.start_time
1159 if self.mode ==
"local":
1161 script_obj.name +
"=" + str(script_obj.runtime) +
"\n"
1166 script_obj.returncode = result[1]
1170 f
"exit_status was {result[1]} for {script_obj.path}"
1172 script_obj.remove_output_files()
1177 reason=f
"Script '{script_object.path}' failed and we set it's status to skipped so that all dependencies " +
1178 "are also skipped.",
1184 for dependent_script
in remaining_scripts:
1185 if script_obj
in dependent_script.dependencies:
1186 dependent_script.dependencies.remove(script_obj)
1192 for script
in remaining_scripts
1197 for script
in remaining_scripts
1201 f
"Finished [{len(waiting)},{len(running)}]: {script_obj.path} -> {script_obj.status}"
1204 def handle_unfinished_script(script_obj: Script):
1206 time.time() - script_obj.last_report_time
1207 ) / 60.0 > self.running_script_reporting_interval:
1209 f
"Script {script_obj.name_not_sanitized} running since {time.time() - script_obj.start_time} seconds"
1215 script_obj.last_report_time = time.time()
1219 total_runtime_in_minutes = (
1220 time.time() - script_obj.start_time
1223 total_runtime_in_minutes
1224 > self.script_max_runtime_in_minutes
1229 f
"Script {script_obj.path} did not finish after "
1230 f
"{total_runtime_in_minutes} minutes, attempting to "
1234 script_obj.control.terminate(script_obj)
1238 reason=f
"Script '{script_object.path}' did not finish in "
1239 f
"time, so we're setting it to 'failed' so that all "
1240 f
"dependent scripts will be skipped.",
1243 def handle_waiting_script(script_obj: Script):
1246 if script_obj.output_files:
1247 script_obj.control = control
1249 script_obj.control = local_control
1252 if script_obj.control.available():
1255 self.log.debug(
"Starting " + script_obj.path)
1259 self.log.warning(f
"Starting of {script_obj.path} failed")
1264 script_obj.control.execute(
1265 script_obj, self.basf2_options, self.dry, self.tag
1269 script_obj.start_time = time.time()
1270 script_obj.last_report_time = time.time()
1276 for _
in remaining_scripts
1281 for _
in remaining_scripts
1285 f
"Started [{len(waiting)},{len(running)}]: {script_obj.path}"
1289 while remaining_scripts:
1292 for script_object
in remaining_scripts:
1298 result = script_object.control.is_job_finished(
1304 handle_finished_script(script_object)
1306 handle_unfinished_script(script_object)
1310 elif not script_object.dependencies:
1311 handle_waiting_script(script_object)
1314 remaining_scripts = [
1316 for script
in remaining_scripts
1321 self.sort_scripts(remaining_scripts)
1328 progress_bar_lines = draw_progress_bar(
1329 progress_bar_lines, self.scripts
1337 if self.mode ==
"local":
1341 self.store_run_results_json(git_hash)
1344 def create_plots(self):
1346 This method prepares the html directory for the plots
if necessary
1347 and creates the plots that include the results
from this validation.
1354 os.makedirs(html_folder, exist_ok=True)
1356 if not os.path.exists(results_folder):
1358 f
"Folder {results_folder} not found in "
1359 f
"the work directory {self.work_folder}, please run "
1360 f
"b2validation first"
1365 def save_metadata(self):
1367 This method fetches the metadata of all the data files produced
1368 during the validation run and saves them
in individual text files of the
1369 same name (
with .txt appended at the end) inside the results folder.
1373 result_folder = self.get_log_folder()
1375 if not os.path.exists(result_folder):
1377 f
"Folder {result_folder} not found in "
1378 f
"the work directory {self.work_folder}, please run "
1379 f
"b2validation first"
1383 for file
in os.listdir(result_folder):
1384 if file.endswith(
".root"):
1386 os.path.join(result_folder, file))
1388 metadata_file = os.path.join(result_folder, f
'{file}.txt')
1391 os.remove(metadata_file)
1392 except FileNotFoundError:
1394 with open(metadata_file,
'a')
as out:
1395 out.write(f
'{metadata}\n')
1397 self.log.debug(f
"b2file-metadata-show failed for {file}.")
1400def execute(tag=None, is_test=None):
1402 Parses the command line and executes the full validation suite
1403 :param tag The name that will be used
for the current revision.
1404 Default
None means automatic.
1405 :param is_test Run
in test mode? Default
None means that we read this
1406 from the command line arguments (which default to
False).
1419 os.environ.get(
"BELLE2_RELEASE_DIR",
None)
is None
1420 and os.environ.get(
"BELLE2_LOCAL_DIR",
None)
is None
1422 sys.exit(
"Error: No basf2 release set up!")
1432 cmd_arguments = parse_cmd_line_arguments(
1433 modes=Validation.get_available_job_control_names()
1439 cmd_arguments.tag = tag
1440 if is_test
is not None:
1441 cmd_arguments.test = is_test
1444 validation = Validation(cmd_arguments.tag)
1447 validation.log.note(
"Starting validation...")
1448 validation.log.note(
1449 f
'Results will stored in a folder named "{validation.tag}"...'
1451 validation.log.note(
1452 f
"The (full) log file(s) can be found at {', '.join(get_log_file_paths(validation.log))}"
1454 validation.log.note(
1455 "Please check these logs when encountering "
1456 "unexpected results, as most of the warnings and "
1457 "errors are not written to stdout/stderr."
1461 if cmd_arguments.options:
1462 validation.basf2_options =
" ".join(cmd_arguments.options)
1463 validation.log.note(
1464 f
"Received arguments for basf2: {validation.basf2_options}"
1468 validation.mode = cmd_arguments.mode
1471 validation.parallel = cmd_arguments.parallel
1474 if cmd_arguments.quiet:
1475 validation.log.note(
"Running in quiet mode (no progress bar).")
1476 validation.quiet =
True
1479 if cmd_arguments.dry:
1480 validation.log.note(
1481 "Performing a dry run; no scripts will be " "started."
1483 validation.dry =
True
1486 if cmd_arguments.test:
1487 validation.log.note(
"Running in test mode")
1488 validation.ignored_packages = []
1489 cmd_arguments.packages = [
"validation-test"]
1491 validation.log.note(
1492 f
"Release Folder: {validation.basepaths['central']}"
1494 validation.log.note(
1495 f
"Local Folder: {validation.basepaths['local']}"
1499 validation.log.note(
"Collecting steering files...")
1500 intervals = cmd_arguments.intervals.split(
",")
1504 validation.log.note(
"Building headers for Script objects...")
1505 validation.build_headers()
1509 if not cmd_arguments.select_ignore_dependencies:
1510 validation.log.note(
"Building dependencies for Script objects...")
1511 validation.build_dependencies()
1513 if cmd_arguments.packages:
1514 validation.log.note(
1515 "Applying package selection for the following package(s): "
1516 +
", ".join(cmd_arguments.packages)
1518 validation.apply_package_selection(cmd_arguments.packages)
1521 if cmd_arguments.select:
1522 validation.log.note(
"Applying selection for validation scripts")
1523 validation.apply_script_selection(
1524 cmd_arguments.select, ignore_dependencies=
False
1529 if cmd_arguments.select_ignore_dependencies:
1530 validation.log.note(
1531 "Applying selection for validation scripts, "
1532 "ignoring their dependencies"
1534 validation.apply_script_selection(
1535 cmd_arguments.select_ignore_dependencies,
1536 ignore_dependencies=
True,
1541 if cmd_arguments.use_cache:
1542 validation.log.note(
"Checking for cached script output")
1543 validation.apply_script_caching()
1546 if cmd_arguments.max_run_time
is not None:
1547 if cmd_arguments.max_run_time > 0:
1548 validation.log.note(
1549 f
"Setting maximal run time of the steering files "
1550 f
"to {cmd_arguments.max_run_time} minutes."
1553 validation.log.note(
1554 "Disabling run time limitation of steering files as "
1555 "requested (max run time set to <= 0)."
1557 validation.script_max_runtime_in_minutes = (
1558 cmd_arguments.max_run_time
1562 validation.log.note(
"Starting the validation...")
1563 validation.run_validation()
1566 validation.log.note(
"Validation finished...")
1567 if not validation.dry:
1568 validation.log.note(
"Start creating plots...")
1569 validation.create_plots()
1570 validation.log.note(
"Plots have been created...")
1572 validation.log.note(
1573 "Skipping plot creation " "(dry run)..."
1576 if cmd_arguments.send_mails:
1578 validation.log.note(
"Start sending mails...")
1580 if cmd_arguments.send_mails_mode ==
"incremental":
1582 elif cmd_arguments.send_mails_mode ==
"full":
1586 mails.send_all_mails(incremental=incremental)
1587 validation.log.note(
1588 f
"Save mail data to {validation.get_log_folder()}"
1592 validation.report_on_scripts()
1594 validation.save_metadata()
1597 validation.log.note(
1598 f
"Validation finished! Total runtime: {int(timeit.default_timer() - get_start_time())}s"
1601 if cmd_arguments.view:
1605 except KeyboardInterrupt:
1606 validation.log.note(
"Validation terminated by user!")
Provides functionality to send mails in case of failed scripts / validation plots.
intervals
stores the intervals which have been selected
bool in_interval(self, Script script_object)
def __init__(self, intervals)
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)
def get_html_folder(output_base_dir)
def get_results_runtime_file(output_base_dir)
def get_results_folder(output_base_dir)
def get_results_tag_revision_file(output_base_dir, tag)
def get_results_tag_folder(output_base_dir, tag)
def create_plots(revisions=None, force=False, Optional[Queue] process_queue=None, work_folder=".")
def run_server(ip="127.0.0.1", port=8000, parse_command_line=False, open_site=False, dry_run=False)