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`
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 for env_name, replacement
in re.findall(
":envvar:`(.*?)`(?:.*``(.*?)``)?", configure_logging_for_tests.__doc__):
153 replacement = env_name
154 if env_name
in os.environ:
157 replacements[os.environ[env_name].rstrip(
'/ ')] = f
"${{{replacement}}}"
158 if user_replacements
is not None:
159 replacements.update(user_replacements)
161 replacements.setdefault(os.getcwd(),
"${cwd}")
162 sys.stdout = logfilter.LogReplacementFilter(sys.__stdout__, replacements)
166 def working_directory(path):
167 """temporarily change the working directory to path
169 >>> with working_directory("testing"):
170 >>> # now in subdirectory "./testing/"
171 >>> # back to parent directory
173 This function will not create the directory for you. If changing into the
174 directory fails a `FileNotFoundError` will be raised.
176 dirname = os.getcwd()
185 def clean_working_directory():
186 """Context manager to create a temporary directory and directly us it as
187 current working directory. The directory will automatically be deleted after
188 the with context is left.
190 >>> with clean_working_directory() as dirname:
191 >>> # now we are in an empty directory, name is stored in dirname
192 >>> assert(os.listdir() == [])
193 >>> # now we are back where we were before
195 with tempfile.TemporaryDirectory()
as tempdir:
196 with working_directory(tempdir):
201 def local_software_directory():
202 """Context manager to make sure we are executed in the top software
203 directory by switching to $BELLE2_LOCAL_DIR.
205 >>> with local_software_directory():
206 >>> assert(os.listdir().contains("analysis"))
209 directory = os.environ[
"BELLE2_LOCAL_DIR"]
211 raise RuntimeError(
"Cannot find local Belle 2 software directory, "
212 "have you setup the software correctly?")
214 with working_directory(directory):
218 def run_in_subprocess(*args, target, **kwargs):
219 """Run the given ``target`` function in a child process using `multiprocessing.Process`
221 This avoids side effects: anything done in the target function will not
222 affect the current process. This is mostly useful for test scripts as
223 ``target`` can emit a `FATAL <LogLevel.FATAL>` error without killing script execution.
225 It will return the exitcode of the child process which should be 0 in case of no error
227 process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
230 return process.exitcode
233 def safe_process(*args, **kwargs):
234 """Run `basf2.process` with the given path in a child process using
235 `multiprocessing.Process`
237 This avoids side effects (`safe_process` can be safely called multiple times)
238 and doesn't kill this script even if a segmentation violation or a `FATAL
239 <LogLevel.FATAL>` error occurs during processing.
241 It will return the exitcode of the child process which should be 0 in case of no error
243 return run_in_subprocess(target=basf2.process, *args, **kwargs)
246 def check_error_free(tool, toolname, package, filter=lambda x:
False, toolopts=
None):
247 """Calls the ``tool`` with argument ``package`` and check that the output is
248 error-free. Optionally ``filter`` the output in case of error messages that
251 In case there is some output left, then prints the error message and exits
254 The test is only executed for a full local checkout: If the ``BELLE2_RELEASE_DIR``
255 environment variable is set or if ``BELLE2_LOCAL_DIR`` is unset the test is
256 skipped: The program exits with an appropriate message.
259 If the test is skipped or the test contains errors this function does
260 not return but will directly end the program.
263 tool(str): executable to call
264 toolname(str): human readable name of the tool
265 package(str): package to run over. Also the first argument to the tool
266 filter: function which gets called for each line of output and
267 if it returns True the line will be ignored.
268 toolopts(list(str)): extra options to pass to the tool.
271 if "BELLE2_RELEASE_DIR" in os.environ:
272 skip_test(
"Central release is setup")
273 if "BELLE2_LOCAL_DIR" not in os.environ:
274 skip_test(
"No local release is setup")
279 if package
is not None:
282 with local_software_directory():
284 output = subprocess.check_output(args, encoding=
"utf8")
285 except subprocess.CalledProcessError
as error:
287 output = error.output
289 clean_log = [e
for e
in output.splitlines()
if e
and not filter(e)]
290 if len(clean_log) > 0:
291 subject = f
"{package} package" if package
is not None else "repository"
293 The {subject} has some {toolname} issues, which is now not allowed.
298 and fix any issues you have introduced. Here is what {toolname} found:\n""")
299 print(
"\n".join(clean_log))
303 def get_streamer_checksums(objects):
305 Extract the version and streamer checksum of the C++ objects in the given list
306 by writing them all to a TMemFile and getting back the streamer info list
307 automatically created by ROOT afterwards.
308 Please note, that this list also includes the streamer infos of all
309 base objects of the objects you gave.
311 Returns a dictionary object name -> (version, checksum).
316 f = ROOT.TMemFile(
"test_mem_file",
"RECREATE")
324 streamer_checksums = dict()
325 for streamer_info
in f.GetStreamerInfoList():
326 if not isinstance(streamer_info, ROOT.TStreamerInfo):
328 streamer_checksums[streamer_info.GetName()] = (streamer_info.GetClassVersion(), streamer_info.GetCheckSum())
331 return streamer_checksums
334 def get_object_with_name(object_name, root=None):
336 (Possibly) recursively get the object with the given name from the Belle2 namespace.
338 If the object name includes a ".", the first part will be turned into an object (probably a module)
339 and the function is continued with this object as the root and the rest of the name.
341 If not, the object is extracted via a getattr call.
344 from ROOT
import Belle2
347 if "." in object_name:
348 namespace, object_name = object_name.split(
".", 1)
350 return get_object_with_name(object_name, get_object_with_name(namespace, root=root))
352 return getattr(root, object_name)
355 def skip_test_if_light(py_case=None):
357 Skips the test if we are running in a light build (maybe this test tests
358 some generation example or whatever)
361 py_case (unittest.TestCase): if this is to be skipped within python's
362 native unittest then pass the TestCase instance
366 except ModuleNotFoundError:
367 skip_test(reason=
"We're in a light build.", py_case=py_case)
370 def print_belle2_environment():
372 Prints all the BELLE2 environment variables on the screen.
374 basf2.B2INFO(
'The BELLE2 environment variables are:')
375 for key, value
in sorted(dict(os.environ).items()):
376 if 'BELLE2' in key.upper():
377 print(f
' {key}={value}')
382 Returns true if we are running a test on our CI system (currently bamboo).
383 The 'BELLE2_IS_CI' environment variable is set on CI only when the unit
386 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