5 b2test_utils - Helper functions useful for test scripts
6 -------------------------------------------------------
8 This module contains functions which are commonly needed for tests like changing
9 log levels or switching to an empty working directory
15 from contextlib
import contextmanager
16 from collections
import OrderedDict
17 import multiprocessing
21 from .
import logfilter
24 def skip_test(reason, py_case=None):
25 """Skip a test script with a given reason. This function will end the script
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.
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.
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
41 py_case.skipTest(reason)
43 print(
"TEST SKIPPED: %s" % reason, file=sys.stderr, flush=
True)
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.
51 Wraps `basf2.find_file` for use in test scripts run as
52 :ref`b2test-scripts <b2test-scripts>`
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
60 Full path to the test input file
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)
70 def set_loglevel(loglevel):
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:
75 >>> with set_log_level(LogLevel.ERROR):
76 >>> # during this block the log level is set to ERROR
78 old_loglevel = basf2.logging.log_level
79 basf2.set_log_level(loglevel)
83 basf2.set_log_level(old_loglevel)
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
91 >>> with show_only_errors():
92 >>> B2INFO("this will not be shown")
93 >>> B2INFO("but this might")
95 with set_loglevel(basf2.LogLevel.ERROR):
99 def configure_logging_for_tests(user_replacements=None):
101 Change the log system to behave a bit more appropriately for testing scenarios:
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
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):
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`
121 user_replacements (dict(str, str)): Additional strings and their replacements to replace in the output
124 This function should be called **after** switching directory to replace the correct directory name
126 .. versionadded:: release-04-00-00
128 basf2.logging.reset()
129 basf2.logging.enable_summary(
False)
130 basf2.logging.enable_python_logging =
True
131 basf2.logging.add_console()
134 for level
in basf2.LogLevel.values.values():
135 basf2.logging.set_info(level, basf2.LogInfo.LEVEL | basf2.LogInfo.MESSAGE)
141 replacements = OrderedDict()
142 replacements[
", ".join(basf2.conditions.default_globaltags)] =
"${default_globaltag}"
144 for env_name, replacement
in re.findall(
":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
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)
152 replacements.setdefault(os.getcwd(),
"${cwd}")
153 sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
157 def working_directory(path):
158 """temprarily change the working directory to path
160 >>> with working_directory("testing"):
161 >>> # now in subdirectory "./testing/"
162 >>> # back to parent directory
164 This function will not create the directory for you. If changing into the
165 directory fails a `FileNotFoundError` will be raised.
167 dirname = os.getcwd()
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.
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
186 with tempfile.TemporaryDirectory()
as tempdir:
187 with working_directory(tempdir):
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.
196 >>> with local_software_directory():
197 >>> assert(os.listdir().contains("analysis"))
200 directory = os.environ[
"BELLE2_LOCAL_DIR"]
202 raise RuntimeError(
"Cannot find local Belle 2 software directory, "
203 "have you setup the software correctly?")
205 with working_directory(directory):
209 def run_in_subprocess(*args, target, **kwargs):
210 """Run the given ``target`` function in a child process using `multiprocessing.Process`
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.
216 It will return the exitcode of the child process which should be 0 in case of no error
218 process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
221 return process.exitcode
224 def safe_process(*args, **kwargs):
225 """Run `basf2.process` with the given path in a child process using
226 `multiprocessing.Process`
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.
232 It will return the exitcode of the child process which should be 0 in case of no error
234 return run_in_subprocess(target=basf2.process, *args, **kwargs)
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
242 In case there is some output left, then prints the error message and exits
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.
250 If the test is skipped or the test contains errors this function does
251 not return but will directly end the program.
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.
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")
270 if package
is not None:
273 with local_software_directory():
275 output = subprocess.check_output(args, encoding=
"utf8")
276 except subprocess.CalledProcessError
as error:
278 output = error.output
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"
284 The {subject} has some {toolname} issues, which is now not allowed.
289 and fix any issues you have introduced. Here is what {toolname} found:\n""")
290 print(
"\n".join(clean_log))
294 def get_streamer_checksums(objects):
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.
302 Returns a dictionary object name -> (version, checksum).
307 f = ROOT.TMemFile(
"test_mem_file",
"RECREATE")
315 streamer_checksums = dict()
316 for streamer_info
in f.GetStreamerInfoList():
317 if not isinstance(streamer_info, ROOT.TStreamerInfo):
319 streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
322 return streamer_checksums
325 def get_object_with_name(object_name, root=None):
327 (Possibly) recursively get the object with the given name from the Belle2 namespace.
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.
332 If not, the object is extracted via a getattr call.
335 from ROOT
import Belle2
338 if "." in object_name:
339 namespace, object_name = object_name.split(
".", 1)
341 return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
343 return getattr(root, object_name)
346 def skip_test_if_light(py_case=None):
348 Skips the test if we are running in a light build (maybe this test tests
349 some generation example or whatever)
352 py_case (unittest.TestCase): if this is to be skipped within python's
353 native unittest then pass the TestCase instance
357 except ModuleNotFoundError:
358 skip_test(reason=
"We're in a light build.", py_case=py_case)