Belle II Software  release-08-01-10
__init__.py
1 #!/usr/bin/env python3
2 
3 
10 
11 """
12 b2test_utils - Helper functions useful for test scripts
13 -------------------------------------------------------
14 
15 This module contains functions which are commonly needed for tests like changing
16 log levels or switching to an empty working directory
17 """
18 
19 import sys
20 import os
21 import tempfile
22 from contextlib import contextmanager
23 from collections import OrderedDict
24 import multiprocessing
25 import basf2
26 import subprocess
27 import re
28 from b2test_utils import logfilter
29 
30 
31 def skip_test(reason, py_case=None):
32  """Skip a test script with a given reason. This function will end the script
33  and not return.
34 
35  This is intended for scripts to be run in :ref:`b2test-scripts
36  <b2test-scripts>` and will flag the script as skipped with the given reason
37  when tests are executed.
38 
39  Useful if the test depends on some external condition like a web service and
40  missing this dependency should not fail the test run.
41 
42  Parameters:
43  reason (str): the reason to skip the test.
44  py_case (unittest.TestCase): if this is to be skipped within python's
45  native unittest then pass the TestCase instance
46  """
47  if py_case:
48  py_case.skipTest(reason)
49  else:
50  print("TEST SKIPPED: %s" % reason, file=sys.stderr, flush=True)
51  sys.exit(1)
52 
53 
54 def require_file(filename, data_type="", py_case=None):
55  """Check for the existence of a test input file before attempting to open it.
56  Skips the test if not found.
57 
58  Wraps `basf2.find_file` for use in test scripts run as
59  :ref`b2test-scripts <b2test-scripts>`
60 
61  Parameters:
62  filename (str): relative filename to look for, either in a central place or in the current working directory
63  data_type (str): case insensitive data type to find. Either empty string or one of ``"examples"`` or ``"validation"``.
64  py_case (unittest.TestCase): if this is to be skipped within python's native unittest then pass the TestCase instance
65 
66  Returns:
67  Full path to the test input file
68  """
69  try:
70  fullpath = basf2.find_file(filename, data_type, silent=False)
71  except FileNotFoundError as fnf:
72  skip_test('Cannot find: %s' % fnf.filename, py_case)
73  return fullpath
74 
75 
76 @contextmanager
77 def set_loglevel(loglevel):
78  """
79  temporarily set the log level to the specified `LogLevel <basf2.LogLevel>`. This returns a
80  context manager so it should be used in a ``with`` statement:
81 
82  >>> with set_log_level(LogLevel.ERROR):
83  >>> # during this block the log level is set to ERROR
84  """
85  old_loglevel = basf2.logging.log_level
86  basf2.set_log_level(loglevel)
87  try:
88  yield
89  finally:
90  basf2.set_log_level(old_loglevel)
91 
92 
93 @contextmanager
94 def show_only_errors():
95  """temporarily set the log level to `ERROR <LogLevel.ERROR>`. This returns a
96  context manager so it should be used in a ``with`` statement
97 
98  >>> with show_only_errors():
99  >>> B2INFO("this will not be shown")
100  >>> B2INFO("but this might")
101  """
102  with set_loglevel(basf2.LogLevel.ERROR):
103  yield
104 
105 
106 def configure_logging_for_tests(user_replacements=None):
107  """
108  Change the log system to behave a bit more appropriately for testing scenarios:
109 
110  1. Simplify log message to be just ``[LEVEL] message``
111  2. Disable error summary, just additional noise
112  3. Intercept all log messages and replace
113 
114  * the current working directory in log messaged with ``${cwd}``
115  * the current default globaltags with ``${default_globaltag}``
116  * the contents of the following environment variables with their name
117  (or the listed replacement string):
118 
119  - :envvar:`BELLE2_TOOLS`
120  - :envvar:`BELLE2_RELEASE_DIR` with ``BELLE2_SOFTWARE_DIR``
121  - :envvar:`BELLE2_LOCAL_DIR` with ``BELLE2_SOFTWARE_DIR``
122  - :envvar:`BELLE2_EXTERNALS_DIR`
123  - :envvar:`BELLE2_VALIDATION_DATA_DIR`
124  - :envvar:`BELLE2_EXAMPLES_DATA_DIR`
125  - :envvar:`BELLE2_BACKGROUND_DIR`
126  - :envvar:`BELLE2_CONDB_METADATA`
127 
128  Parameters:
129  user_replacements (dict(str, str)): Additional strings and their replacements to replace in the output
130 
131  Warning:
132  This function should be called **after** switching directory to replace the correct directory name
133 
134  .. versionadded:: release-04-00-00
135  """
136  basf2.logging.reset()
137  basf2.logging.enable_summary(False)
138  basf2.logging.enable_python_logging = True
139  basf2.logging.add_console()
140  # clang prints namespaces differently so no function names. Also let's skip the line number,
141  # we don't want failing tests just because we added a new line of code. In fact, let's just see the message
142  for level in basf2.LogLevel.values.values():
143  basf2.logging.set_info(level, basf2.LogInfo.LEVEL | basf2.LogInfo.MESSAGE)
144 
145  # now create dictionary of string replacements. Since each key can only be
146  # present once order is kind of important so the less portable ones like
147  # current directory should go first and might be overridden if for example
148  # the BELLE2_LOCAL_DIR is identical to the current working directory
149  replacements = OrderedDict()
150  replacements[", ".join(basf2.conditions.default_globaltags)] = "${default_globaltag}"
151  # add a special replacement for the CDB metadata provider URL, since it's not set via env. variable
152  replacements[basf2.conditions.default_metadata_provider_url] = "${BELLE2_CONDB_METADATA}"
153  # Let's be lazy and take the environment variables from the docstring so we don't have to repeat them here
154  for env_name, replacement in re.findall(":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
155  if not replacement:
156  replacement = env_name
157  if env_name in os.environ:
158  # replace path from the environment with the name of the variable. But remove a trailing slash or whitespace so that
159  # the output doesn't depend on whether there is a tailing slash in the environment variable
160  replacements[os.environ[env_name].rstrip('/ ')] = f"${{{replacement}}}"
161 
162  if user_replacements is not None:
163  replacements.update(user_replacements)
164  # add cwd only if it doesn't overwrite anything ...
165  replacements.setdefault(os.getcwd(), "${cwd}")
166  sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
167 
168 
169 @contextmanager
170 def working_directory(path):
171  """temporarily change the working directory to path
172 
173  >>> with working_directory("testing"):
174  >>> # now in subdirectory "./testing/"
175  >>> # back to parent directory
176 
177  This function will not create the directory for you. If changing into the
178  directory fails a `FileNotFoundError` will be raised.
179  """
180  dirname = os.getcwd()
181  try:
182  os.chdir(path)
183  yield
184  finally:
185  os.chdir(dirname)
186 
187 
188 @contextmanager
189 def clean_working_directory():
190  """Context manager to create a temporary directory and directly us it as
191  current working directory. The directory will automatically be deleted after
192  the with context is left.
193 
194  >>> with clean_working_directory() as dirname:
195  >>> # now we are in an empty directory, name is stored in dirname
196  >>> assert(os.listdir() == [])
197  >>> # now we are back where we were before
198  """
199  with tempfile.TemporaryDirectory() as tempdir:
200  with working_directory(tempdir):
201  yield tempdir
202 
203 
204 @contextmanager
205 def local_software_directory():
206  """Context manager to make sure we are executed in the top software
207  directory by switching to $BELLE2_LOCAL_DIR or $BELLE2_RELEASE_DIR.
208 
209  >>> with local_software_directory():
210  >>> assert(os.listdir().contains("analysis"))
211  """
212  directory = os.environ.get("BELLE2_LOCAL_DIR", os.environ.get("BELLE2_RELEASE_DIR", None))
213  if directory is None:
214  raise RuntimeError("Cannot find Belle II software directory, "
215  "have you setup the software correctly?")
216 
217  with working_directory(directory):
218  yield directory
219 
220 
221 def run_in_subprocess(*args, target, **kwargs):
222  """Run the given ``target`` function in a child process using `multiprocessing.Process`
223 
224  This avoids side effects: anything done in the target function will not
225  affect the current process. This is mostly useful for test scripts as
226  ``target`` can emit a `FATAL <LogLevel.FATAL>` error without killing script execution.
227 
228  It will return the exitcode of the child process which should be 0 in case of no error
229  """
230  process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
231  process.start()
232  process.join()
233  return process.exitcode
234 
235 
236 def safe_process(*args, **kwargs):
237  """Run `basf2.process` with the given path in a child process using
238  `multiprocessing.Process`
239 
240  This avoids side effects (`safe_process` can be safely called multiple times)
241  and doesn't kill this script even if a segmentation violation or a `FATAL
242  <LogLevel.FATAL>` error occurs during processing.
243 
244  It will return the exitcode of the child process which should be 0 in case of no error
245  """
246  return run_in_subprocess(target=basf2.process, *args, **kwargs)
247 
248 
249 def check_error_free(tool, toolname, package, filter=lambda x: False, toolopts=None):
250  """Calls the ``tool`` with argument ``package`` and check that the output is
251  error-free. Optionally ``filter`` the output in case of error messages that
252  can be ignored.
253 
254  In case there is some output left, then prints the error message and exits
255  (failing the test).
256 
257  Warnings:
258  If the test is skipped or the test contains errors this function does
259  not return but will directly end the program.
260 
261  Arguments:
262  tool(str): executable to call
263  toolname(str): human readable name of the tool
264  package(str): package to run over. Also the first argument to the tool
265  filter: function which gets called for each line of output and
266  if it returns True the line will be ignored.
267  toolopts(list(str)): extra options to pass to the tool.
268  """
269 
270  if "BELLE2_LOCAL_DIR" not in os.environ and "BELLE2_RELEASE_DIR" not in os.environ:
271  skip_test("No release is setup")
272 
273  args = [tool]
274  if toolopts:
275  args += toolopts
276  if package is not None:
277  args += [package]
278 
279  with local_software_directory():
280  try:
281  output = subprocess.check_output(args, encoding="utf8")
282  except subprocess.CalledProcessError as error:
283  print(error)
284  output = error.output
285 
286  clean_log = [e for e in output.splitlines() if e and not filter(e)]
287  if len(clean_log) > 0:
288  subject = f"{package} package" if package is not None else "repository"
289  print(f"""\
290 The {subject} has some {toolname} issues, which is now not allowed.
291 Please run:
292 
293  $ {" ".join(args)}
294 
295 and fix any issues you have introduced. Here is what {toolname} found:\n""")
296  print("\n".join(clean_log))
297  sys.exit(1)
298 
299 
300 def get_streamer_checksums(objects):
301  """
302  Extract the version and streamer checksum of the C++ objects in the given list
303  by writing them all to a TMemFile and getting back the streamer info list
304  automatically created by ROOT afterwards.
305  Please note, that this list also includes the streamer infos of all
306  base objects of the objects you gave.
307 
308  Returns a dictionary object name -> (version, checksum).
309  """
310  # Always avoid the top-level 'import ROOT'.
311  import ROOT # noqa
312 
313  # Write out the objects to a mem file
314  f = ROOT.TMemFile("test_mem_file", "RECREATE")
315  f.cd()
316 
317  for o in objects:
318  o.Write()
319  f.Write()
320 
321  # Go through all streamer infos and extract checksum and version
322  streamer_checksums = dict()
323  for streamer_info in f.GetStreamerInfoList():
324  if not isinstance(streamer_info, ROOT.TStreamerInfo):
325  continue
326  streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
327 
328  f.Close()
329  return streamer_checksums
330 
331 
332 def get_object_with_name(object_name, root=None):
333  """
334  (Possibly) recursively get the object with the given name from the Belle2 namespace.
335 
336  If the object name includes a ".", the first part will be turned into an object (probably a module)
337  and the function is continued with this object as the root and the rest of the name.
338 
339  If not, the object is extracted via a getattr call.
340  """
341  if root is None:
342  from ROOT import Belle2 # noqa
343  root = Belle2
344 
345  if "." in object_name:
346  namespace, object_name = object_name.split(".", 1)
347 
348  return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
349 
350  return getattr(root, object_name)
351 
352 
353 def skip_test_if_light(py_case=None):
354  """
355  Skips the test if we are running in a light build (maybe this test tests
356  some generation example or whatever)
357 
358  Parameters:
359  py_case (unittest.TestCase): if this is to be skipped within python's
360  native unittest then pass the TestCase instance
361  """
362  try:
363  import generators # noqa
364  except ModuleNotFoundError:
365  skip_test(reason="We're in a light build.", py_case=py_case)
366 
367 
368 def print_belle2_environment():
369  """
370  Prints all the BELLE2 environment variables on the screen.
371  """
372  basf2.B2INFO('The BELLE2 environment variables are:')
373  for key, value in sorted(dict(os.environ).items()):
374  if 'BELLE2' in key.upper():
375  print(f' {key}={value}')
376 
377 
378 @contextmanager
379 def temporary_set_environment(**environ):
380  """
381  Temporarily set the process environment variables.
382  Inspired by https://stackoverflow.com/a/34333710
383 
384  >>> with temporary_set_environment(BELLE2_TEMP_DIR='/tmp/belle2'):
385  ... "BELLE2_TEMP_DIR" in os.environ
386  True
387 
388  >>> "BELLE2_TEMP_DIR" in os.environ
389  False
390 
391  Arguments:
392  environ(dict): Dictionary of environment variables to set
393  """
394  old_environ = dict(os.environ)
395  os.environ.update(environ)
396  try:
397  yield
398  finally:
399  os.environ.clear()
400  os.environ.update(old_environ)
401 
402 
403 def is_ci() -> bool:
404  """
405  Returns true if we are running a test on our CI system (currently bamboo).
406  The 'BELLE2_IS_CI' environment variable is set on CI only when the unit
407  tests are run.
408  """
409  return os.environ.get("BELLE2_IS_CI", "no").lower() in [
410  "yes",
411  "1",
412  "y",
413  "on",
414  ]
std::map< ExpRun, std::pair< double, double > > filter(const std::map< ExpRun, std::pair< double, double >> &runs, double cut, std::map< ExpRun, std::pair< double, double >> &runsRemoved)
filter events to remove runs shorter than cut, it stores removed runs in runsRemoved
Definition: Splitter.cc:38