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 [