12b2test_utils - Helper functions useful for test scripts
13-------------------------------------------------------
15This module contains functions which are commonly needed for tests like changing
16log levels or switching to an empty working directory
22from contextlib
import contextmanager
23from collections
import OrderedDict
28from b2test_utils
import logfilter
31def 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(f
"TEST SKIPPED: {reason}", file=sys.stderr, flush=
True)
54def 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"``, ``"validation"``
65 py_case (unittest.TestCase): if this is to be skipped within python's native unittest then pass the TestCase instance
68 Full path to the test input file
71 fullpath = basf2.find_file(filename, data_type, silent=
False)
72 except FileNotFoundError
as fnf:
73 skip_test(f
'Cannot find: {fnf.filename}', py_case)
78def set_loglevel(loglevel):
80 temporarily set the log level to the specified `LogLevel <basf2.LogLevel>`. This returns a
81 context manager so it should be used in a ``with`` statement:
83 >>> with set_log_level(LogLevel.ERROR):
84 >>> # during this block the log level is set to ERROR
86 old_loglevel = basf2.logging.log_level
87 basf2.set_log_level(loglevel)
91 basf2.set_log_level(old_loglevel)
95def show_only_errors():
96 """temporarily set the log level to `ERROR <LogLevel.ERROR>`. This returns a
97 context manager so it should be used in a ``with`` statement
99 >>> with show_only_errors():
100 >>> B2INFO("this will not be shown")
101 >>> B2INFO("but this might")
103 with set_loglevel(basf2.LogLevel.ERROR):
107def configure_logging_for_tests(user_replacements=None, replace_cdb_provider=True):
109 Change the log system to behave a bit more appropriately for testing scenarios:
111 1. Simplify log message to be just ``[LEVEL] message``
112 2. Disable error summary, just additional noise
113 3. Intercept all log messages and replace
115 * the current working directory in log messaged with ``${cwd}``
116 * the current release version with ``${release_version}``
117 * the current default globaltags with ``${default_globaltag}``
118 * the contents of the following environment variables with their name
119 (or the listed replacement string):
121 - :envvar:`BELLE2_TOOLS`
122 - :envvar:`BELLE2_RELEASE_DIR` with ``BELLE2_SOFTWARE_DIR``
123 - :envvar:`BELLE2_LOCAL_DIR` with ``BELLE2_SOFTWARE_DIR``
124 - :envvar:`BELLE2_EXTERNALS_DIR`
125 - :envvar:`BELLE2_VALIDATION_DATA_DIR`
126 - :envvar:`BELLE2_EXAMPLES_DATA_DIR`
127 - :envvar:`BELLE2_BACKGROUND_DIR`
128 - :envvar:`BELLE2_CONDB_METADATA`
131 user_replacements (dict(str, str)): Additional strings and their replacements to replace in the output
132 replace_cdb_provider (bool): If False, it does not replace the conditions database metadata provider with
133 `BELLE2_CONDB_METADATA` (necessary for some specific tests)
136 This function should be called **after** switching directory to replace the correct directory name
138 .. versionadded:: release-04-00-00
140 basf2.logging.reset()
141 basf2.logging.enable_summary(
False)
142 basf2.logging.enable_python_logging =
True
143 basf2.logging.add_console()
146 for level
in basf2.LogLevel.values.values():
147 basf2.logging.set_info(level, basf2.LogInfo.LEVEL | basf2.LogInfo.MESSAGE)
153 replacements = OrderedDict()
158 replacements[
", ".join(basf2.conditions.default_globaltags)] =
"${default_globaltag}"
161 if replace_cdb_provider
and len(basf2.conditions.metadata_providers) > 0:
162 replacements[basf2.conditions.metadata_providers[0]] =
"${BELLE2_CONDB_METADATA}"
164 for env_name, replacement
in re.findall(
":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
166 replacement = env_name
167 if env_name
in os.environ:
170 replacements[os.environ[env_name].rstrip(
'/ ')] = f
"${{{replacement}}}"
172 if user_replacements
is not None:
173 replacements.update(user_replacements)
175 replacements.setdefault(os.getcwd(),
"${cwd}")
176 sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
180def working_directory(path):
181 """temporarily change the working directory to path
183 >>> with working_directory("testing"):
184 >>> # now in subdirectory "./testing/"
185 >>> # back to parent directory
187 This function will not create the directory for you. If changing into the
188 directory fails a `FileNotFoundError` will be raised.
190 dirname = os.getcwd()
199def clean_working_directory():
200 """Context manager to create a temporary directory and directly us it as
201 current working directory. The directory will automatically be deleted after
202 the with context is left.
204 >>> with clean_working_directory() as dirname:
205 >>> # now we are in an empty directory, name is stored in dirname
206 >>> assert(os.listdir() == [])
207 >>> # now we are back where we were before
209 with tempfile.TemporaryDirectory()
as tempdir:
210 with working_directory(tempdir):
215def local_software_directory():
216 """Context manager to make sure we are executed in the top software
217 directory by switching to $BELLE2_LOCAL_DIR or $BELLE2_RELEASE_DIR.
219 >>> with local_software_directory():
220 >>> assert(os.listdir().contains("analysis"))
222 directory = os.environ.get(
"BELLE2_LOCAL_DIR", os.environ.get(
"BELLE2_RELEASE_DIR",
None))
223 if directory
is None:
224 raise RuntimeError(
"Cannot find Belle II software directory, "
225 "have you setup the software correctly?")
227 with working_directory(directory):
231def run_in_subprocess(*args, target, **kwargs):
232 """Run the given ``target`` function in a child process using `multiprocessing.Process`
234 This avoids side effects: anything done in the target function will not
235 affect the current process. This is mostly useful for test scripts as
236 ``target`` can emit a `FATAL <LogLevel.FATAL>` error without killing script execution.
238 It will return the exitcode of the child process which should be 0 in case of no error
240 process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
243 return process.exitcode
246def safe_process(*args, **kwargs):
247 """Run `basf2.process` with the given path in a child process using
248 `multiprocessing.Process`
250 This avoids side effects (`safe_process` can be safely called multiple times)
251 and doesn't kill this script even if a segmentation violation or a `FATAL
252 <LogLevel.FATAL>` error occurs during processing.
254 It will return the exitcode of the child process which should be 0 in case of no error
256 return run_in_subprocess(target=basf2.process, *args, **kwargs)
259def check_error_free(tool, toolname, package, filter=lambda x:
False, toolopts=
None):
260 """Calls the ``tool`` with argument ``package`` and check that the output is
261 error-free. Optionally ``filter`` the output in case of error messages that
264 In case there is some output left, then prints the error message and exits
268 If the test is skipped or the test contains errors this function does
269 not return but will directly end the program.
272 tool(str): executable to call
273 toolname(str): human readable name of the tool
274 package(str): package to run over. Also the first argument to the tool
275 filter: function which gets called for each line of output and
276 if it returns True the line will be ignored.
277 toolopts(list(str)): extra options to pass to the tool.
280 if "BELLE2_LOCAL_DIR" not in os.environ
and "BELLE2_RELEASE_DIR" not in os.environ:
281 skip_test(
"No release is setup")
286 if package
is not None:
289 with local_software_directory():
291 output = subprocess.check_output(args, encoding=
"utf8")
292 except subprocess.CalledProcessError
as error:
294 output = error.output
296 clean_log = [e
for e
in output.splitlines()
if e
and not filter(e)]
297 if len(clean_log) > 0:
298 subject = f
"{package} package" if package
is not None else "repository"
300The {subject} has some {toolname} issues, which is now not allowed.
305and fix any issues you have introduced. Here is what {toolname} found:\n""")
306 print(
"\n".join(clean_log))
310def get_streamer_checksums(objects):
312 Extract the version and streamer checksum of the C++ objects in the given list
313 by writing them all to a TMemFile and getting back the streamer info list
314 automatically created by ROOT afterwards.
315 Please note, that this list also includes the streamer infos of all
316 base objects of the objects you gave.
318 Returns a dictionary object name -> (version, checksum).
324 f = ROOT.TMemFile(
"test_mem_file",
"RECREATE")
332 streamer_checksums = dict()
333 for streamer_info
in f.GetStreamerInfoList():
334 if not isinstance(streamer_info, ROOT.TStreamerInfo):
336 streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
339 return streamer_checksums
342def get_object_with_name(object_name, root=None):
344 (Possibly) recursively get the object with the given name from the Belle2 namespace.
346 If the object name includes a ".", the first part will be turned into an object (probably a module)
347 and the function is continued with this object as the root and the rest of the name.
349 If not, the object is extracted via a getattr call.
352 from ROOT
import Belle2
355 if "." in object_name:
356 namespace, object_name = object_name.split(
".", 1)
358 return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
360 return getattr(root, object_name)
363def skip_test_if_light(py_case=None):
365 Skips the test if we are running in a light build (maybe this test tests
366 some generation example or whatever)
369 py_case (unittest.TestCase): if this is to be skipped within python's
370 native unittest then pass the TestCase instance
374 except ModuleNotFoundError:
375 skip_test(reason=
"We're in a light build.", py_case=py_case)
378def skip_test_if_central(py_case=None):
380 Skips the test if we are using a central release (and have no local
384 py_case (unittest.TestCase): if this is to be skipped within python's
385 native unittest then pass the TestCase instance
387 if "BELLE2_RELEASE_DIR" in os.environ:
388 skip_test(reason=
"We're in a central release.", py_case=py_case)
391def print_belle2_environment():
393 Prints all the BELLE2 environment variables on the screen.
395 basf2.B2INFO(
'The BELLE2 environment variables are:')
396 for key, value
in sorted(dict(os.environ).items()):
397 if 'BELLE2' in key.upper():
398 print(f
' {key}={value}')
402def temporary_environment(**environ):
404 Context manager that temporarily sets environment variables
405 for the current process. Inspired by https://stackoverflow.com/a/34333710
407 >>> with temporary_environment(BELLE2_TEMP_DIR='/tmp/belle2'):
408 ... "BELLE2_TEMP_DIR" in os.environ
411 >>> "BELLE2_TEMP_DIR" in os.environ
415 **environ: Arbitrary keyword arguments specifying environment
416 variables and their values.
418 old_environ = dict(os.environ)
419 os.environ.update(environ)
424 os.environ.update(old_environ)
429 Returns true if we are running a test on our CI system (currently GitLab pipeline).
430 The 'BELLE2_IS_CI' environment variable is set on CI only when the unit
433 return os.environ.get(
"BELLE2_IS_CI",
"no").lower()
in [
441def is_cdb_down() -> bool:
443 Returns true if the Conditions Database (CDB) is currently unavailable or slow to respond.
444 The 'BELLE2_IS_CDB_DOWN' environment variable can be used to dynamically exclude some
445 tests that rely on the CDB in case of problems.
447 return os.environ.get(
"BELLE2_IS_CDB_DOWN",
"no").lower()
in [