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