Belle II Software  release-06-02-00
__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 
127  Parameters:
128  user_replacements (dict(str, str)): Additional strings and their replacements to replace in the output
129 
130  Warning:
131  This function should be called **after** switching directory to replace the correct directory name
132 
133  .. versionadded:: release-04-00-00
134  """
135  basf2.logging.reset()
136  basf2.logging.enable_summary(False)
137  basf2.logging.enable_python_logging = True
138  basf2.logging.add_console()
139  # clang prints namespaces differently so no function names. Also let's skip the line number,
140  # we don't want failing tests just because we added a new line of code. In fact, let's just see the message
141  for level in basf2.LogLevel.values.values():
142  basf2.logging.set_info(level, basf2.LogInfo.LEVEL | basf2.LogInfo.MESSAGE)
143 
144  # now create dictionary of string replacements. Since each key can only be
145  # present once order is kind of important so the less portable ones like
146  # current directory should go first and might be overridden if for example
147  # the BELLE2_LOCAL_DIR is identical to the current working directory
148  replacements = OrderedDict()
149  replacements[", ".join(basf2.conditions.default_globaltags)] = "${default_globaltag}"
150  # Let's be lazy and take the environment variables from the docstring so we don't have to repeat them here
151  for env_name, replacement in re.findall(":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
152  if not replacement:
153  replacement = env_name
154  if env_name in os.environ:
155  # replace path from the environment with the name of the variable. But remove a trailing slash or whitespace so that
156  # the output doesn't depend on whether there is a tailing slash in the environment variable
157  replacements[os.environ[env_name].rstrip('/ ')] = f"${{{replacement}}}"
158  if user_replacements is not None:
159  replacements.update(user_replacements)
160  # add cwd only if it doesn't overwrite anything ...
161  replacements.setdefault(os.getcwd(), "${cwd}")
162  sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
163 
164 
165 @contextmanager
166 def working_directory(path):
167  """temporarily change the working directory to path
168 
169  >>> with working_directory("testing"):
170  >>> # now in subdirectory "./testing/"
171  >>> # back to parent directory
172 
173  This function will not create the directory for you. If changing into the
174  directory fails a `FileNotFoundError` will be raised.
175  """
176  dirname = os.getcwd()
177  try:
178  os.chdir(path)
179  yield
180  finally:
181  os.chdir(dirname)
182 
183 
184 @contextmanager
185 def clean_working_directory():
186  """Context manager to create a temporary directory and directly us it as
187  current working directory. The directory will automatically be deleted after
188  the with context is left.
189 
190  >>> with clean_working_directory() as dirname:
191  >>> # now we are in an empty directory, name is stored in dirname
192  >>> assert(os.listdir() == [])
193  >>> # now we are back where we were before
194  """
195  with tempfile.TemporaryDirectory() as tempdir:
196  with working_directory(tempdir):
197  yield tempdir
198 
199 
200 @contextmanager
201 def local_software_directory():
202  """Context manager to make sure we are executed in the top software
203  directory by switching to $BELLE2_LOCAL_DIR.
204 
205  >>> with local_software_directory():
206  >>> assert(os.listdir().contains("analysis"))
207  """
208  try:
209  directory = os.environ["BELLE2_LOCAL_DIR"]
210  except KeyError:
211  raise RuntimeError("Cannot find local Belle 2 software directory, "
212  "have you setup the software correctly?")
213 
214  with working_directory(directory):
215  yield directory
216 
217 
218 def run_in_subprocess(*args, target, **kwargs):
219  """Run the given ``target`` function in a child process using `multiprocessing.Process`
220 
221  This avoids side effects: anything done in the target function will not
222  affect the current process. This is mostly useful for test scripts as
223  ``target`` can emit a `FATAL <LogLevel.FATAL>` error without killing script execution.
224 
225  It will return the exitcode of the child process which should be 0 in case of no error
226  """
227  process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
228  process.start()
229  process.join()
230  return process.exitcode
231 
232 
233 def safe_process(*args, **kwargs):
234  """Run `basf2.process` with the given path in a child process using
235  `multiprocessing.Process`
236 
237  This avoids side effects (`safe_process` can be safely called multiple times)
238  and doesn't kill this script even if a segmentation violation or a `FATAL
239  <LogLevel.FATAL>` error occurs during processing.
240 
241  It will return the exitcode of the child process which should be 0 in case of no error
242  """
243  return run_in_subprocess(target=basf2.process, *args, **kwargs)
244 
245 
246 def check_error_free(tool, toolname, package, filter=lambda x: False, toolopts=None):
247  """Calls the ``tool`` with argument ``package`` and check that the output is
248  error-free. Optionally ``filter`` the output in case of error messages that
249  can be ignored.
250 
251  In case there is some output left, then prints the error message and exits
252  (failing the test).
253 
254  The test is only executed for a full local checkout: If the ``BELLE2_RELEASE_DIR``
255  environment variable is set or if ``BELLE2_LOCAL_DIR`` is unset the test is
256  skipped: The program exits with an appropriate message.
257 
258  Warnings:
259  If the test is skipped or the test contains errors this function does
260  not return but will directly end the program.
261 
262  Arguments:
263  tool(str): executable to call
264  toolname(str): human readable name of the tool
265  package(str): package to run over. Also the first argument to the tool
266  filter: function which gets called for each line of output and
267  if it returns True the line will be ignored.
268  toolopts(list(str)): extra options to pass to the tool.
269  """
270 
271  if "BELLE2_RELEASE_DIR" in os.environ:
272  skip_test("Central release is setup")
273  if "BELLE2_LOCAL_DIR" not in os.environ:
274  skip_test("No local release is setup")
275 
276  args = [tool]
277  if toolopts:
278  args += toolopts
279  if package is not None:
280  args += [package]
281 
282  with local_software_directory():
283  try:
284  output = subprocess.check_output(args, encoding="utf8")
285  except subprocess.CalledProcessError as error:
286  print(error)
287  output = error.output
288 
289  clean_log = [e for e in output.splitlines() if e and not filter(e)]
290  if len(clean_log) > 0:
291  subject = f"{package} package" if package is not None else "repository"
292  print(f"""\
293 The {subject} has some {toolname} issues, which is now not allowed.
294 Please run:
295 
296  $ {" ".join(args)}
297 
298 and fix any issues you have introduced. Here is what {toolname} found:\n""")
299  print("\n".join(clean_log))
300  sys.exit(1)
301 
302 
303 def get_streamer_checksums(objects):
304  """
305  Extract the version and streamer checksum of the C++ objects in the given list
306  by writing them all to a TMemFile and getting back the streamer info list
307  automatically created by ROOT afterwards.
308  Please note, that this list also includes the streamer infos of all
309  base objects of the objects you gave.
310 
311  Returns a dictionary object name -> (version, checksum).
312  """
313  import ROOT
314 
315  # Write out the objects to a mem file
316  f = ROOT.TMemFile("test_mem_file", "RECREATE")
317  f.cd()
318 
319  for o in objects:
320  o.Write()
321  f.Write()
322 
323  # Go through all streamer infos and extract checksum and version
324  streamer_checksums = dict()
325  for streamer_info in f.GetStreamerInfoList():
326  if not isinstance(streamer_info, ROOT.TStreamerInfo):
327  continue
328  streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
329 
330  f.Close()
331  return streamer_checksums
332 
333 
334 def get_object_with_name(object_name, root=None):
335  """
336  (Possibly) recursively get the object with the given name from the Belle2 namespace.
337 
338  If the object name includes a ".", the first part will be turned into an object (probably a module)
339  and the function is continued with this object as the root and the rest of the name.
340 
341  If not, the object is extracted via a getattr call.
342  """
343  if root is None:
344  from ROOT import Belle2
345  root = Belle2
346 
347  if "." in object_name:
348  namespace, object_name = object_name.split(".", 1)
349 
350  return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
351 
352  return getattr(root, object_name)
353 
354 
355 def skip_test_if_light(py_case=None):
356  """
357  Skips the test if we are running in a light build (maybe this test tests
358  some generation example or whatever)
359 
360  Parameters:
361  py_case (unittest.TestCase): if this is to be skipped within python's
362  native unittest then pass the TestCase instance
363  """
364  try:
365  import generators # noqa
366  except ModuleNotFoundError:
367  skip_test(reason="We're in a light build.", py_case=py_case)
368 
369 
370 def print_belle2_environment():
371  """
372  Prints all the BELLE2 environment variables on the screen.
373  """
374  basf2.B2INFO('The BELLE2 environment variables are:')
375  for key, value in sorted(dict(os.environ).items()):
376  if 'BELLE2' in key.upper():
377  print(f' {key}={value}')
378 
379 
380 def is_ci() -> bool:
381  """
382  Returns true if we are running a test on our CI system (currently bamboo).
383  The 'BELLE2_IS_CI' environment variable is set on CI only when the unit
384  tests are run.
385  """
386  return os.environ.get("BELLE2_IS_CI", "no").lower() in [
387  "yes",
388  "1",
389  "y",
390  "on",
391  ]
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:40