Belle II Software development
test_support.py
1
8
9import os
10import random
11import shutil
12import subprocess
13import sys
14from glob import glob
15
16import b2test_utils
17import basf2
18import generators
19from simulation import add_simulation
20from rawdata import add_packers
21from softwaretrigger import constants
22from softwaretrigger.constants import DEFAULT_EXPRESSRECO_COMPONENTS, RAWDATA_OBJECTS, DEFAULT_HLT_COMPONENTS
23from ROOT import Belle2
24
25
26class CheckForCorrectHLTResults(basf2.Module):
27 """Test module for assuring correct data store content"""
28
29 def event(self):
30 """reimplementation of Module::event()."""
31 sft_trigger = Belle2.PyStoreObj("SoftwareTriggerResult")
32
33 if not sft_trigger.isValid():
34 basf2.B2FATAL("SoftwareTriggerResult object not created")
35 elif len(sft_trigger.getResults()) == 0:
36 basf2.B2FATAL("SoftwareTriggerResult exists but has no entries")
37
38 if not Belle2.PyStoreArray("ROIs").isValid():
39 basf2.B2FATAL("ROIs are not present")
40
41
42def get_file_name(base_path, run_type, location, passthrough, simulate_events_of_doom_buster):
43 mode = ""
44 if passthrough:
45 mode += "_passthrough"
46 if simulate_events_of_doom_buster:
47 mode += "_eodb"
48 return os.path.join(base_path, f"{location.name}_{run_type.name}{mode}.root")
49
50
51def generate_input_file(run_type, location, output_file_name, exp_number, passthrough,
52 simulate_events_of_doom_buster):
53 """
54 Generate an input file for usage in the test.
55 Simulate uubar for "beam" and two muons for "cosmic" setting.
56
57 Only raw data will be stored to the given output file.
58 :param run_type: Whether to simulate cosmic or beam
59 :param location: Whether to simulate expressreco (with ROIs) or hlt (no PXD)
60 :param output_file_name: where to store the result file
61 :param exp_number: which experiment number to simulate
62 :param passthrough: if true don't generate a trigger result in the input file
63 :param simulate_events_of_doom_buster: if true, simulate the effect of the
64 EventsOfDoomBuster module by inflating the number of CDC hits
65 """
66 if os.path.exists(output_file_name):
67 return 1
68
69 basf2.set_random_seed(12345)
70
71 path = basf2.Path()
72 path.add_module('EventInfoSetter', evtNumList=[4], expList=[exp_number])
73
74 if run_type == constants.RunTypes.beam:
75 generators.add_continuum_generator(path, finalstate="uubar")
76 elif run_type == constants.RunTypes.cosmic:
77 # add something which looks a tiny bit like a cosmic generator. We
78 # cannot use the normal cosmic generator as that needs a bigger
79 # simulation top volume than the default geometry from the database.
80 path.add_module("ParticleGun", pdgCodes=[-13, 13], momentumParams=[10, 200])
81
82 add_simulation(path, usePXDDataReduction=(location == constants.Location.expressreco))
83
84 # inflate the number of CDC hits in order to later simulate the effect of the
85 # EventsOfDoomBuster module
86 if simulate_events_of_doom_buster:
87
88 class InflateCDCHits(basf2.Module):
89 """Artificially inflate the number of CDC hits."""
90
91 def initialize(self):
92 """Initialize."""
93 self.cdc_hits = Belle2.PyStoreArray("CDCHits")
94 self.cdc_hits.isRequired()
95 eodb_parameters = Belle2.PyDBObj("EventsOfDoomParameters")
96 if not eodb_parameters.isValid():
97 basf2.B2FATAL("EventsOfDoomParameters is not valid")
98 self.cdc_hits_threshold = eodb_parameters.getNCDCHitsMax() + 1
99
100 def event(self):
101 """Event"""
102 if self.cdc_hits.isValid():
103 # Let's simply append a (default) CDC hit multiple times
104 for i in range(self.cdc_hits_threshold):
105 self.cdc_hits.appendNew()
106
107 path.add_module(InflateCDCHits())
108
109 if location == constants.Location.hlt:
110 components = DEFAULT_HLT_COMPONENTS
111 elif location == constants.Location.expressreco:
112 components = DEFAULT_EXPRESSRECO_COMPONENTS
113 else:
114 basf2.B2FATAL(f"Location {location.name} for test is not supported")
115
116 components.append("TRG")
117
118 add_packers(path, components=components)
119
120 # express reco expects to have an HLT results, so lets add a fake one
121 if location == constants.Location.expressreco and not passthrough:
122 class FakeHLTResult(basf2.Module):
123 def initialize(self):
124 self.results = Belle2.PyStoreObj(Belle2.SoftwareTriggerResult.Class(), "SoftwareTriggerResult")
125 self.results.registerInDataStore()
126
127 self.EventMetaData = Belle2.PyStoreObj("EventMetaData")
128
129 def event(self):
130 self.results.create()
131 # First event: Add all the results that are used on express reco just to test all paths
132 if (self.EventMetaData.obj().getEvent() == 1):
133 self.results.addResult("software_trigger_cut&all&total_result", 1)
134 self.results.addResult("software_trigger_cut&filter&total_result", 1)
135 self.results.addResult("software_trigger_cut&skim&total_result", 1)
136 self.results.addResult("software_trigger_cut&skim&accept_mumutight", 1)
137 self.results.addResult("software_trigger_cut&skim&accept_dstar_1", 1)
138 self.results.addResult("software_trigger_cut&filter&L1_trigger", 1)
139 # Second event: No skim lines to replicate a HLT discarded event with filter ON
140 elif (self.EventMetaData.obj().getEvent() == 2):
141 self.results.addResult("software_trigger_cut&all&total_result", 1)
142 self.results.addResult("software_trigger_cut&filter&total_result", 1)
143 self.results.addResult("software_trigger_cut&skim&total_result", 0)
144 self.results.addResult("software_trigger_cut&filter&L1_trigger", 1)
145 # Third event: Does not pass through L1 passthrough
146 elif (self.EventMetaData.obj().getEvent() == 3):
147 self.results.addResult("software_trigger_cut&all&total_result", 1)
148 self.results.addResult("software_trigger_cut&filter&total_result", 0)
149 self.results.addResult("software_trigger_cut&skim&total_result", 1)
150 self.results.addResult("software_trigger_cut&skim&accept_mumutight", 1)
151 self.results.addResult("software_trigger_cut&skim&accept_dstar_1", 1)
152 self.results.addResult("software_trigger_cut&filter&L1_trigger", 0)
153 # Fourth event: HLT discarded but passes HLT skims (possible in HLT filter OFF mode)
154 elif (self.EventMetaData.obj().getEvent() == 4):
155 self.results.addResult("software_trigger_cut&all&total_result", 0)
156 self.results.addResult("software_trigger_cut&filter&total_result", 0)
157 self.results.addResult("software_trigger_cut&skim&total_result", 1)
158 self.results.addResult("software_trigger_cut&skim&accept_mumutight", 1)
159 self.results.addResult("software_trigger_cut&skim&accept_dstar_1", 1)
160 self.results.addResult("software_trigger_cut&filter&L1_trigger", 0)
161
162 path.add_module(FakeHLTResult())
163
164 # remove everything but HLT input raw objects
165 branch_names = RAWDATA_OBJECTS + ["EventMetaData", "TRGSummary"]
166 if not passthrough:
167 branch_names += ["SoftwareTriggerResult"]
168
169 if location == constants.Location.hlt:
170 branch_names.remove("RawPXDs")
171 branch_names.remove("ROIs")
172
173 # There is no packer for the following objects :(
174 branch_names.remove("RawTRGs")
175
176 path.add_module("RootOutput", outputFileName=output_file_name, branchNames=branch_names)
177
178 basf2.process(path)
179
180 return 0
181
182
183def test_script(script_location, input_file_name, temp_dir):
184 """
185 Test a script with the given file path using the given input file.
186 Raises an exception if the execution fails or if the needed output is not in
187 the output file.
188 The results are stored in the temporary directory location.
189
190 :param script_location: the script to test
191 :param input_file_name: the file path of the input file
192 :param temp_dir: where to store and run
193 """
194 input_buffer = "UNUSED" # unused
195 output_buffer = "UNUSED" # unused
196 histo_port = 6666 # unused
197
198 random_seed = "".join(random.choices("abcdef", k=4))
199
200 histos_file_name = f"{random_seed}_histos.root"
201 output_file_name = os.path.join(temp_dir, f"{random_seed}_output.root")
202 globaltags = list(basf2.conditions.default_globaltags)
203 num_processes = 1
204
205 # Because the script name is hard-coded in the run control GUI,
206 # we must jump into the script directory
207 cwd = os.getcwd()
208 os.chdir(os.path.dirname(script_location))
209 cmd1 = [sys.executable, script_location, "--central-db-tag"] + globaltags + [
210 "--input-file", os.path.abspath(input_file_name),
211 "--histo-output-file", os.path.join(temp_dir, f"{histos_file_name}"),
212 "--output-file", os.path.abspath(output_file_name),
213 "--number-processes", str(num_processes),
214 input_buffer, output_buffer, str(histo_port)
215 ]
216 subprocess.check_call(cmd1)
217
218 # Move the output file with DQM histograms under the expected location:
219 # for reasons we don't want to know, they are saved under the current directory
220 # even if a valid and existing working directory is specified
221 final_histos_file_name = os.path.join(temp_dir, histos_file_name)
222 if os.path.exists(histos_file_name):
223 shutil.copy(histos_file_name, os.path.join(temp_dir, final_histos_file_name))
224 os.unlink(histos_file_name)
225
226 # Go back to the original directory for safety
227 os.chdir(cwd)
228
229 if "beam_reco" in script_location:
230
231 if "expressreco" not in script_location:
232 # Check the integrity of HLT result
233 test_path = basf2.Path()
234 test_path.add_module("RootInput", inputFileName=output_file_name)
235 test_path.add_module(CheckForCorrectHLTResults())
236 assert (b2test_utils.safe_process(test_path) == 0)
237
238 # Check the size of DQM histograms
239 cmd2 = ["hlt-check-dqm-size", final_histos_file_name]
240 subprocess.check_call(cmd2)
241
242
243def test_folder(location, run_type, exp_number, phase, passthrough=False,
244 simulate_events_of_doom_buster=False):
245 """
246 Run all hlt operation scripts in a given folder
247 and test the outputs of the files.
248
249 Will call the test_script function on all files in the folder given
250 by the location after having created a suitable input file with the given
251 experiment number.
252
253 :param location: hlt or expressreco, depending on which operation files to run
254 and which input to simulate
255 :param run_type: cosmic or beam, depending on which operation files to run
256 and which input to simulate
257 :param exp_number: which experiment number to simulate
258 :param phase: where to look for the operation files (will search in the folder
259 hlt/operation/{phase}/global/{location}/evp_scripts/)
260 :param passthrough: only relevant for express reco: If true don't create a
261 software trigger result in the input file to test running
262 express reco if hlt is in passthrough mode
263 :param simulate_events_of_doom_buster: if true, simulate the effect of the
264 EventsOfDoomBuster module by inflating the number of CDC hits
265 """
266
267 # The beam tests always fail on buildbot with a permission error
268 if run_type == constants.RunTypes.beam and not b2test_utils.is_ci():
269 b2test_utils.skip_test("Test not runnable on build bot because of permission issue.")
270 # The test is already run in a clean, temporary directory
271 temp_dir = os.getcwd()
272 prepare_path = os.environ["BELLE2_PREPARE_PATH"]
273 input_file_name = get_file_name(
274 prepare_path, run_type, location, passthrough, simulate_events_of_doom_buster)
275
276 script_dir = basf2.find_file(f"hlt/operation/{phase}/global/{location.name}/evp_scripts/")
277 run_at_least_one = False
278 for script_location in glob(os.path.join(script_dir, f"run_{run_type.name}*.py")):
279 run_at_least_one = True
280 test_script(script_location, input_file_name=input_file_name, temp_dir=temp_dir)
281
282 assert run_at_least_one
Class to access a DBObjPtr from Python.
Definition: PyDBObj.h:50
A (simplified) python wrapper for StoreArray.
Definition: PyStoreArray.h:72
a (simplified) python wrapper for StoreObjPtr.
Definition: PyStoreObj.h:67
def safe_process(*args, **kwargs)
Definition: __init__.py:241
def skip_test(reason, py_case=None)
Definition: __init__.py:31
bool is_ci()
Definition: __init__.py:421
def add_continuum_generator(path, finalstate, userdecfile='', *skip_on_failure=True, eventType='')
Definition: generators.py:349