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