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 default globaltags
with ``${default_globaltag}``
115 * the contents of the following environment variables
with their name
116 (
or the listed replacement string):
118 - :envvar:`BELLE2_TOOLS`
119 - :envvar:`BELLE2_RELEASE_DIR`
with ``BELLE2_SOFTWARE_DIR``
120 - :envvar:`BELLE2_LOCAL_DIR`
with ``BELLE2_SOFTWARE_DIR``
121 - :envvar:`BELLE2_EXTERNALS_DIR`
122 - :envvar:`BELLE2_VALIDATION_DATA_DIR`
123 - :envvar:`BELLE2_EXAMPLES_DATA_DIR`
124 - :envvar:`BELLE2_BACKGROUND_DIR`
125 - :envvar:`BELLE2_CONDB_METADATA`
128 user_replacements (dict(str, str)): Additional strings
and their replacements to replace
in the output
131 This function should be called **after** switching directory to replace the correct directory name
133 .. versionadded:: release-04-00-00
135 basf2.logging.reset()
136 basf2.logging.enable_summary(False)
137 basf2.logging.enable_python_logging =
True
138 basf2.logging.add_console()
141 for level
in basf2.LogLevel.values.values():
142 basf2.logging.set_info(level, basf2.LogInfo.LEVEL | basf2.LogInfo.MESSAGE)
148 replacements = OrderedDict()
149 replacements[
", ".join(basf2.conditions.default_globaltags)] =
"${default_globaltag}"
151 replacements[basf2.conditions.default_metadata_provider_url] =
"${BELLE2_CONDB_METADATA}"
153 for env_name, replacement
in re.findall(
":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
155 replacement = env_name
156 if env_name
in os.environ:
159 replacements[os.environ[env_name].rstrip(
'/ ')] = f
"${{{replacement}}}"
161 if user_replacements
is not None:
162 replacements.update(user_replacements)
164 replacements.setdefault(os.getcwd(),
"${cwd}")
165 sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
169def working_directory(path):
170 """temporarily change the working directory to path
172 >>> with working_directory(
"testing"):
176 This function will
not create the directory
for you. If changing into the
177 directory fails a `FileNotFoundError` will be raised.
179 dirname = os.getcwd()
188def clean_working_directory():
189 """Context manager to create a temporary directory and directly us it as
190 current working directory. The directory will automatically be deleted after
191 the with context
is left.
193 >>>
with clean_working_directory()
as dirname:
195 >>> assert(os.listdir() == [])
198 with tempfile.TemporaryDirectory()
as tempdir:
199 with working_directory(tempdir):
204def local_software_directory():
205 """Context manager to make sure we are executed in the top software
206 directory by switching to $BELLE2_LOCAL_DIR or $BELLE2_RELEASE_DIR.
208 >>>
with local_software_directory():
209 >>> assert(os.listdir().contains(
"analysis"))
211 directory = os.environ.get("BELLE2_LOCAL_DIR", os.environ.get(
"BELLE2_RELEASE_DIR",
None))
212 if directory
is None:
213 raise RuntimeError(
"Cannot find Belle II software directory, "
214 "have you setup the software correctly?")
216 with working_directory(directory):
220def run_in_subprocess(*args, target, **kwargs):
221 """Run the given ``target`` function in a child process using `multiprocessing.Process`
223 This avoids side effects: anything done in the target function will
not
224 affect the current process. This
is mostly useful
for test scripts
as
225 ``target`` can emit a `FATAL <LogLevel.FATAL>` error without killing script execution.
227 It will
return the exitcode of the child process which should be 0
in case of no error
229 process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
232 return process.exitcode
235def safe_process(*args, **kwargs):
236 """Run `basf2.process` with the given path in a child process using
237 `multiprocessing.Process`
239 This avoids side effects (`safe_process` can be safely called multiple times)
240 and doesn
't kill this script even if a segmentation violation or a `FATAL
241 <LogLevel.FATAL>` error occurs during processing.
243 It will return the exitcode of the child process which should be 0
in case of no error
245 return run_in_subprocess(target=basf2.process, *args, **kwargs)
248def check_error_free(tool, toolname, package, filter=lambda x:
False, toolopts=
None):
249 """Calls the ``tool`` with argument ``package`` and check that the output is
250 error-free. Optionally ``filter`` the output in case of error messages that
253 In case there
is some output left, then prints the error message
and exits
257 If the test
is skipped
or the test contains errors this function does
258 not return but will directly end the program.
261 tool(str): executable to call
262 toolname(str): human readable name of the tool
263 package(str): package to run over. Also the first argument to the tool
264 filter: function which gets called
for each line of output
and
265 if it returns
True the line will be ignored.
266 toolopts(list(str)): extra options to
pass to the tool.
269 if "BELLE2_LOCAL_DIR" not in os.environ
and "BELLE2_RELEASE_DIR" not in os.environ:
270 skip_test(
"No release is setup")
275 if package
is not None:
278 with local_software_directory():
280 output = subprocess.check_output(args, encoding=
"utf8")
281 except subprocess.CalledProcessError
as error:
283 output = error.output
285 clean_log = [e
for e
in output.splitlines()
if e
and not filter(e)]
286 if len(clean_log) > 0:
287 subject = f
"{package} package" if package
is not None else "repository"
289The {subject} has some {toolname} issues, which is now not allowed.
294and fix any issues you have introduced. Here
is what {toolname} found:\n
""")
295 print("\n".join(clean_log))
299def get_streamer_checksums(objects):
301 Extract the version and streamer checksum of the C++ objects
in the given list
302 by writing them all to a TMemFile
and getting back the streamer info list
303 automatically created by ROOT afterwards.
304 Please note, that this list also includes the streamer infos of all
305 base objects of the objects you gave.
307 Returns a dictionary object name -> (version, checksum).
313 f = ROOT.TMemFile(
"test_mem_file",
"RECREATE")
321 streamer_checksums = dict()
322 for streamer_info
in f.GetStreamerInfoList():
323 if not isinstance(streamer_info, ROOT.TStreamerInfo):
325 streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
328 return streamer_checksums
331def get_object_with_name(object_name, root=None):
333 (Possibly) recursively get the object with the given name
from the Belle2 namespace.
335 If the object name includes a
".", the first part will be turned into an object (probably a module)
336 and the function
is continued
with this object
as the root
and the rest of the name.
338 If
not, the object
is extracted via a getattr call.
341 from ROOT
import Belle2
344 if "." in object_name:
345 namespace, object_name = object_name.split(
".", 1)
347 return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
349 return getattr(root, object_name)
352def skip_test_if_light(py_case=None):
354 Skips the test if we are running
in a light build (maybe this test tests
355 some generation example
or whatever)
358 py_case (unittest.TestCase):
if this
is to be skipped within python
's
359 native unittest then pass the TestCase instance
363 except ModuleNotFoundError:
364 skip_test(reason=
"We're in a light build.", py_case=py_case)
367def skip_test_if_central(py_case=None):
369 Skips the test if we are using a central release (
and have no local
373 py_case (unittest.TestCase):
if this
is to be skipped within python
's
374 native unittest then pass the TestCase instance
376 if "BELLE2_RELEASE_DIR" in os.environ:
377 skip_test(reason=
"We're in a central release.", py_case=py_case)
380def print_belle2_environment():
382 Prints all the BELLE2 environment variables on the screen.
384 basf2.B2INFO('The BELLE2 environment variables are:')
385 for key, value
in sorted(dict(os.environ).items()):
386 if 'BELLE2' in key.upper():
387 print(f
' {key}={value}')
391def temporary_set_environment(**environ):
393 Temporarily set the process environment variables.
394 Inspired by https://stackoverflow.com/a/34333710
396 >>> with temporary_set_environment(BELLE2_TEMP_DIR=
'/tmp/belle2'):
397 ...
"BELLE2_TEMP_DIR" in os.environ
400 >>>
"BELLE2_TEMP_DIR" in os.environ
404 environ(dict): Dictionary of environment variables to set
406 old_environ = dict(os.environ)
407 os.environ.update(environ)
412 os.environ.update(old_environ)
417 Returns true if we are running a test on our CI system (currently GitLab pipeline).
418 The
'BELLE2_IS_CI' environment variable
is set on CI only when the unit
421 return os.environ.get(
"BELLE2_IS_CI",
"no").lower()
in [