12 b2test_utils - Helper functions useful for test scripts
13 -------------------------------------------------------
15 This module contains functions which are commonly needed for tests like changing
16 log levels or switching to an empty working directory
22 from contextlib
import contextmanager
23 from collections
import OrderedDict
24 import multiprocessing
28 from b2test_utils
import logfilter
31 def skip_test(reason, py_case=None):
32 """Skip a test script with a given reason. This function will end the script
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.
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.
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
48 py_case.skipTest(reason)
50 print(
"TEST SKIPPED: %s" % reason, file=sys.stderr, flush=
True)
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.
58 Wraps `basf2.find_file` for use in test scripts run as
59 :ref`b2test-scripts <b2test-scripts>`
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
67 Full path to the test input file
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)
77 def set_loglevel(loglevel):
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:
82 >>> with set_log_level(LogLevel.ERROR):
83 >>> # during this block the log level is set to ERROR
85 old_loglevel = basf2.logging.log_level
86 basf2.set_log_level(loglevel)
90 basf2.set_log_level(old_loglevel)
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
98 >>> with show_only_errors():
99 >>> B2INFO("this will not be shown")
100 >>> B2INFO("but this might")
102 with set_loglevel(basf2.LogLevel.ERROR):
106 def configure_logging_for_tests(user_replacements=None):
108 Change the log system to behave a bit more appropriately for testing scenarios:
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
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):
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`
129 user_replacements (dict(str, str)): Additional strings and their replacements to replace in the output
132 This function should be called **after** switching directory to replace the correct directory name
134 .. versionadded:: release-04-00-00
136 basf2.logging.reset()
137 basf2.logging.enable_summary(
False)
138 basf2.logging.enable_python_logging =
True
139 basf2.logging.add_console()
142 for level
in basf2.LogLevel.values.values():
143 basf2.logging.set_info(level, basf2.LogInfo.LEVEL | basf2.LogInfo.MESSAGE)
149 replacements = OrderedDict()
150 replacements[
", ".join(basf2.conditions.default_globaltags)] =
"${default_globaltag}"
152 replacements[basf2.conditions.default_metadata_provider_url] =
"${BELLE2_CONDB_METADATA}"
154 for env_name, replacement
in re.findall(
":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
156 replacement = env_name
157 if env_name
in os.environ:
160 replacements[os.environ[env_name].rstrip(
'/ ')] = f
"${{{replacement}}}"
162 if user_replacements
is not None:
163 replacements.update(user_replacements)
165 replacements.setdefault(os.getcwd(),
"${cwd}")
166 sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
170 def working_directory(path):
171 """temporarily change the working directory to path
173 >>> with working_directory("testing"):
174 >>> # now in subdirectory "./testing/"
175 >>> # back to parent directory
177 This function will not create the directory for you. If changing into the
178 directory fails a `FileNotFoundError` will be raised.
180 dirname = os.getcwd()
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.
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
199 with tempfile.TemporaryDirectory()
as tempdir:
200 with working_directory(tempdir):
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.
209 >>> with local_software_directory():
210 >>> assert(os.listdir().contains("analysis"))
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?")
217 with working_directory(directory):
221 def run_in_subprocess(*args, target, **kwargs):
222 """Run the given ``target`` function in a child process using `multiprocessing.Process`
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.
228 It will return the exitcode of the child process which should be 0 in case of no error
230 process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
233 return process.exitcode
236 def safe_process(*args, **kwargs):
237 """Run `basf2.process` with the given path in a child process using
238 `multiprocessing.Process`
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.
244 It will return the exitcode of the child process which should be 0 in case of no error
246 return run_in_subprocess(target=basf2.process, *args, **kwargs)
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
254 In case there is some output left, then prints the error message and exits
258 If the test is skipped or the test contains errors this function does
259 not return but will directly end the program.
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.
270 if "BELLE2_LOCAL_DIR" not in os.environ
and "BELLE2_RELEASE_DIR" not in os.environ:
271 skip_test(
"No release is setup")
276 if package
is not None:
279 with local_software_directory():
281 output = subprocess.check_output(args, encoding=
"utf8")
282 except subprocess.CalledProcessError
as error:
284 output = error.output
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"
290 The {subject} has some {toolname} issues, which is now not allowed.
295 and fix any issues you have introduced. Here is what {toolname} found:\n""")
296 print(
"\n".join(clean_log))
300 def get_streamer_checksums(objects):
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.
308 Returns a dictionary object name -> (version, checksum).
314 f = ROOT.TMemFile(
"test_mem_file",
"RECREATE")
322 streamer_checksums = dict()
323 for streamer_info
in f.GetStreamerInfoList():
324 if not isinstance(streamer_info, ROOT.TStreamerInfo):
326 streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
329 return streamer_checksums
332 def get_object_with_name(object_name, root=None):
334 (Possibly) recursively get the object with the given name from the Belle2 namespace.
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.
339 If not, the object is extracted via a getattr call.
342 from ROOT
import Belle2
345 if "." in object_name:
346 namespace, object_name = object_name.split(
".", 1)
348 return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
350 return getattr(root, object_name)
353 def skip_test_if_light(py_case=None):
355 Skips the test if we are running in a light build (maybe this test tests
356 some generation example or whatever)
359 py_case (unittest.TestCase): if this is to be skipped within python's
360 native unittest then pass the TestCase instance
364 except ModuleNotFoundError:
365 skip_test(reason=
"We're in a light build.", py_case=py_case)
368 def print_belle2_environment():
370 Prints all the BELLE2 environment variables on the screen.
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}')
379 def temporary_set_environment(**environ):
381 Temporarily set the process environment variables.
382 Inspired by https://stackoverflow.com/a/34333710
384 >>> with temporary_set_environment(BELLE2_TEMP_DIR='/tmp/belle2'):
385 ... "BELLE2_TEMP_DIR" in os.environ
388 >>> "BELLE2_TEMP_DIR" in os.environ
392 environ(dict): Dictionary of environment variables to set
394 old_environ = dict(os.environ)
395 os.environ.update(environ)
400 os.environ.update(old_environ)
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
409 return os.environ.get(
"BELLE2_IS_CI",
"no").lower()
in [
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