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 native unittest then pass the TestCase instance
47 py_case.skipTest(reason)
49 print(f
"TEST SKIPPED: {reason}", file=sys.stderr, flush=
True)
53def require_file(filename, data_type="", py_case=None):
54 """Check for the existence of a test input file before attempting to open it.
55 Skips the test if not found.
57 Wraps `basf2.find_file`
for use
in test scripts run
as
58 :ref`b2test-scripts <b2test-scripts>`
61 filename (str): relative filename to look
for, either
in a central place
or in the current working directory
62 data_type (str): case insensitive data type to find. Either empty string
or one of ``
"examples"``
or ``
"validation"``.
63 py_case (unittest.TestCase):
if this
is to be skipped within python
's native unittest then pass the TestCase instance
66 Full path to the test input file
69 fullpath = basf2.find_file(filename, data_type, silent=
False)
70 except FileNotFoundError
as fnf:
71 skip_test(f
'Cannot find: {fnf.filename}', py_case)
76def set_loglevel(loglevel):
78 temporarily set the log level to the specified `LogLevel <basf2.LogLevel>`. This returns a
79 context manager so it should be used in a ``
with`` statement:
81 >>>
with set_log_level(LogLevel.ERROR):
84 old_loglevel = basf2.logging.log_level
85 basf2.set_log_level(loglevel)
89 basf2.set_log_level(old_loglevel)
93def show_only_errors():
94 """temporarily set the log level to `ERROR <LogLevel.ERROR>`. This returns a
95 context manager so it should be used in a ``
with`` statement
97 >>>
with show_only_errors():
98 >>> B2INFO(
"this will not be shown")
99 >>> B2INFO(
"but this might")
101 with set_loglevel(basf2.LogLevel.ERROR):
105def configure_logging_for_tests(user_replacements=None):
107 Change the log system to behave a bit more appropriately for testing scenarios:
109 1. Simplify log message to be just ``[LEVEL] message``
110 2. Disable error summary, just additional noise
111 3. Intercept all log messages
and replace
113 * the current working directory
in log messaged
with ``${cwd}``
114 * the current release version
with ``${release_version}``
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()
154 replacements[
", ".join(basf2.conditions.default_globaltags)] =
"${default_globaltag}"
156 replacements[basf2.conditions.default_metadata_provider_url] =
"${BELLE2_CONDB_METADATA}"
158 for env_name, replacement
in re.findall(
":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
160 replacement = env_name
161 if env_name
in os.environ:
164 replacements[os.environ[env_name].rstrip(
'/ ')] = f
"${{{replacement}}}"
166 if user_replacements
is not None:
167 replacements.update(user_replacements)
169 replacements.setdefault(os.getcwd(),
"${cwd}")
170 sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
174def working_directory(path):
175 """temporarily change the working directory to path
177 >>> with working_directory(
"testing"):
181 This function will
not create the directory
for you. If changing into the
182 directory fails a `FileNotFoundError` will be raised.
184 dirname = os.getcwd()
193def clean_working_directory():
194 """Context manager to create a temporary directory and directly us it as
195 current working directory. The directory will automatically be deleted after
196 the with context
is left.
198 >>>
with clean_working_directory()
as dirname:
200 >>> assert(os.listdir() == [])
203 with tempfile.TemporaryDirectory()
as tempdir:
204 with working_directory(tempdir):
209def local_software_directory():
210 """Context manager to make sure we are executed in the top software
211 directory by switching to $BELLE2_LOCAL_DIR or $BELLE2_RELEASE_DIR.
213 >>>
with local_software_directory():
214 >>> assert(os.listdir().contains(
"analysis"))
216 directory = os.environ.get("BELLE2_LOCAL_DIR", os.environ.get(
"BELLE2_RELEASE_DIR",
None))
217 if directory
is None:
218 raise RuntimeError(
"Cannot find Belle II software directory, "
219 "have you setup the software correctly?")
221 with working_directory(directory):
225def run_in_subprocess(*args, target, **kwargs):
226 """Run the given ``target`` function in a child process using `multiprocessing.Process`
228 This avoids side effects: anything done in the target function will
not
229 affect the current process. This
is mostly useful
for test scripts
as
230 ``target`` can emit a `FATAL <LogLevel.FATAL>` error without killing script execution.
232 It will
return the exitcode of the child process which should be 0
in case of no error
234 process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
237 return process.exitcode
240def safe_process(*args, **kwargs):
241 """Run `basf2.process` with the given path in a child process using
242 `multiprocessing.Process`
244 This avoids side effects (`safe_process` can be safely called multiple times)
245 and doesn
't kill this script even if a segmentation violation or a `FATAL
246 <LogLevel.FATAL>` error occurs during processing.
248 It will return the exitcode of the child process which should be 0
in case of no error
250 return run_in_subprocess(target=basf2.process, *args, **kwargs)
253def check_error_free(tool, toolname, package, filter=lambda x:
False, toolopts=
None):
254 """Calls the ``tool`` with argument ``package`` and check that the output is
255 error-free. Optionally ``filter`` the output in case of error messages that
258 In case there
is some output left, then prints the error message
and exits
262 If the test
is skipped
or the test contains errors this function does
263 not return but will directly end the program.
266 tool(str): executable to call
267 toolname(str): human readable name of the tool
268 package(str): package to run over. Also the first argument to the tool
269 filter: function which gets called
for each line of output
and
270 if it returns
True the line will be ignored.
271 toolopts(list(str)): extra options to
pass to the tool.
274 if "BELLE2_LOCAL_DIR" not in os.environ
and "BELLE2_RELEASE_DIR" not in os.environ:
275 skip_test(
"No release is setup")
280 if package
is not None:
283 with local_software_directory():
285 output = subprocess.check_output(args, encoding=
"utf8")
286 except subprocess.CalledProcessError
as error:
288 output = error.output
290 clean_log = [e
for e
in output.splitlines()
if e
and not filter(e)]
291 if len(clean_log) > 0:
292 subject = f
"{package} package" if package
is not None else "repository"
294The {subject} has some {toolname} issues, which is now not allowed.
299and fix any issues you have introduced. Here
is what {toolname} found:\n
""")
300 print("\n".join(clean_log))
304def get_streamer_checksums(objects):
306 Extract the version and streamer checksum of the C++ objects
in the given list
307 by writing them all to a TMemFile
and getting back the streamer info list
308 automatically created by ROOT afterwards.
309 Please note, that this list also includes the streamer infos of all
310 base objects of the objects you gave.
312 Returns a dictionary object name -> (version, checksum).
318 f = ROOT.TMemFile(
"test_mem_file",
"RECREATE")
326 streamer_checksums = dict()
327 for streamer_info
in f.GetStreamerInfoList():
328 if not isinstance(streamer_info, ROOT.TStreamerInfo):
330 streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
333 return streamer_checksums
336def get_object_with_name(object_name, root=None):
338 (Possibly) recursively get the object with the given name
from the Belle2 namespace.
340 If the object name includes a
".", the first part will be turned into an object (probably a module)
341 and the function
is continued
with this object
as the root
and the rest of the name.
343 If
not, the object
is extracted via a getattr call.
346 from ROOT
import Belle2
349 if "." in object_name:
350 namespace, object_name = object_name.split(
".", 1)
352 return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
354 return getattr(root, object_name)
357def skip_test_if_light(py_case=None):
359 Skips the test if we are running
in a light build (maybe this test tests
360 some generation example
or whatever)
363 py_case (unittest.TestCase):
if this
is to be skipped within python
's
364 native unittest then pass the TestCase instance
368 except ModuleNotFoundError:
369 skip_test(reason=
"We're in a light build.", py_case=py_case)
372def skip_test_if_central(py_case=None):
374 Skips the test if we are using a central release (
and have no local
378 py_case (unittest.TestCase):
if this
is to be skipped within python
's
379 native unittest then pass the TestCase instance
381 if "BELLE2_RELEASE_DIR" in os.environ:
382 skip_test(reason=
"We're in a central release.", py_case=py_case)
385def print_belle2_environment():
387 Prints all the BELLE2 environment variables on the screen.
389 basf2.B2INFO('The BELLE2 environment variables are:')
390 for key, value
in sorted(dict(os.environ).items()):
391 if 'BELLE2' in key.upper():
392 print(f
' {key}={value}')
396def temporary_set_environment(**environ):
398 Temporarily set the process environment variables.
399 Inspired by https://stackoverflow.com/a/34333710
401 >>> with temporary_set_environment(BELLE2_TEMP_DIR=
'/tmp/belle2'):
402 ...
"BELLE2_TEMP_DIR" in os.environ
405 >>>
"BELLE2_TEMP_DIR" in os.environ
409 environ(dict): Dictionary of environment variables to set
411 old_environ = dict(os.environ)
412 os.environ.update(environ)
417 os.environ.update(old_environ)
422 Returns true if we are running a test on our CI system (currently GitLab pipeline).
423 The
'BELLE2_IS_CI' environment variable
is set on CI only when the unit
426 return os.environ.get(
"BELLE2_IS_CI",
"no").lower()
in [