9 b2test_utils.classversion - Helper functions to inspect and verify ROOT dictionaries 
   10 ------------------------------------------------------------------------------------ 
   12 This module contains some functions to make sure classes which have a ROOT 
   13 dictionary are healthy by allowing to check that 
   15 * All base classes are fully defined and not forward declared 
   16 * All members have a dictionary 
   17 * The class version and checksum is as expected to avoid mistakes where the 
   18   classdef is not increased after class changes 
   20 Users should only need to call ``b2code-classversion-check`` and 
   21 ``b2code-classversion-update``. The documentation of 
   22 ``b2code-classversion-check`` also contains a bit more detailed explanation of 
   23 class versions and checksums 
   30 from basf2 
import B2INFO, B2ERROR, B2WARNING
 
   34     """Exception to report class version errors""" 
   38     """Exception class with extra keyword arguments to show in log message""" 
   47 def check_base_classes(tclass):
 
   48     """Recursively check all base classes of a TClass to make sure all are well defined""" 
   49     bases = tclass.GetListOfBases()
 
   53         baseclass = base.GetClassPointer()
 
   56         check_base_classes(baseclass)
 
   59 def check_dictionary(classname):
 
   60     """Make sure we have a dictionary for the class and all its members""" 
   61     tclass = ROOT.TClass.GetClass(classname)
 
   64     streamerinfo = tclass.GetStreamerInfo()
 
   66         for element 
in streamerinfo.GetElements():
 
   67             elementclass = element.GetClassPointer()
 
   68             if elementclass 
and not elementclass.IsLoaded():
 
   70                                         f
"type {elementclass.GetName()}")
 
   73 def get_class_version(classname):
 
   74     """Get the Class version and checksum for a fully qualified C++ class name""" 
   75     tclass = ROOT.TClass.GetClass(classname)
 
   79     check_base_classes(tclass)
 
   80     version = tclass.GetClassVersion()
 
   81     checksum = tclass.GetCheckSum()
 
   82     return version, checksum
 
   85 def iterate_linkdef(filename):
 
   86     """Iterate through the lines of a ROOT linkdef file. 
   88     This function is a generator that foreach line returns a tuple where the 
   89     first element is True if there was a class link pragma on that line and 
   92     If it was a class link pragma the second element will be a tuple 
   93     ``(line, startcolumn, classname, linkoptions, options)`` where 
   95      * ``line`` is the content of the line 
   96      * ``startcolumn`` is the column the content of the comment started in the line. 
   97        Everything before should be kept as is 
   98      * ``classname`` is the class name 
   99      * ``clingflags`` is the link flags requested by either the optional +-! after 
  100        the class or the ``options=`` in front of the class and a set of the value 
  101        "evolution", "nostreamer", and "noinputoper" 
  102      * ``options`` is a dictionary of the options found in the comment. The 
  103        content of the comment is split at commas and the result are the options. 
  104        If a options has a value after ``=`` that will be assigned, otherwise the 
  105        value is None. For example a comment ``// implicit, version=bar`` would 
  106        return ``{'implicit':None, 'version':'bar}`` 
  108     If it wasn't  link pragma the second element is just the unmodified line 
  111     class_regex = re.compile(
r"^\s*#pragma\s+link\s+C\+\+\s+(?:options=(.*?)\s)?\s*class\s+(.*?)([+\-!]*);(\s*//\s*(.*))?$")
 
  112     with open(filename) 
as linkdef:
 
  114             match = class_regex.match(line)
 
  120                 linkflags, classname, flags, comment, content = match.groups()
 
  121                 start = match.start(4) 
if comment 
else len(line) - 1  
 
  125                     clingflags.add({
"+": 
"evolution", 
"-": 
"nostreamer", 
"!": 
"noinputoper"}.get(char))
 
  126                 if linkflags 
is not None:
 
  127                     for flag 
in linkflags.split(
","):
 
  128                         clingflags.add(flag.strip())
 
  131                 if content 
is not None:
 
  132                     optionlist = (e.strip().split(
"=", 1) 
for e 
in content.split(
","))
 
  133                     options = {e[0]: e[1] 
if len(e) > 1 
else None for e 
in optionlist}
 
  135                 yield True, (line, start, classname, clingflags, options)
 
  138 def check_linkdef(filename, message_style="belle2"):
 
  139     """Check a linkdef file for expected versions/checksums 
  141     * If the class has ``version=N, checksum=0x1234`` in the comment in the 
  142       linkdef compare that to what ROOT has created 
  143     * If not warn about the missing version and checksum 
  144     * But skip classes which are not stored to file. 
  147     relative_filename = os.path.relpath(filename)
 
  149     def print_message(severity, text, **argk):
 
  150         """Print a message similar to GCC or Clang: ``filename:line:column: error: text`` 
  151         so that it gets picked up by the build system 
  153         nonlocal message_style, errors, relative_filename, nr, column, classname
 
  155         if message_style == 
"gcc":
 
  156             print(f
"{relative_filename}:{nr}:{column}: {severity}: class {classname}: {text}", file=sys.stderr, flush=
True)
 
  158             variables = {
"linkdef": f
"{relative_filename}:{nr}", 
"classname": classname}
 
  159             variables.update(argk)
 
  160             if severity == 
"error":
 
  161                 B2ERROR(text, **variables)
 
  163                 B2WARNING(text, **variables)
 
  165         if severity == 
"error":
 
  168     def get_int(container, key):
 
  169         """Get the value from a container if it exists and try to convert to int. 
  170         Otherwise show error""" 
  171         value = options.get(key, 
None)
 
  172         if value 
is not None:
 
  175             except ValueError 
as e:
 
  176                 print_message(
"error", f
"expected {key} is not valid: {e}")
 
  179     for nr, (isclass, content) 
in enumerate(iterate_linkdef(filename)):
 
  184         line, column, classname, clingflags, options = content
 
  187         if "nostreamer" in clingflags:
 
  188             if "evolution" in clingflags:
 
  189                 print_message(
"error", 
"both, no streamer and class evolution requested.")
 
  195         elif "evolution" not in clingflags:
 
  196             print_message(
"warning", 
"using old ROOT3 streamer format without evolution. " 
  197                           "Please add + or - (for classes not to be written to file) after the classname.")
 
  201             version, checksum = get_class_version(classname)
 
  202         except ClassVersionError 
as e:
 
  203             print_message(
"error", e)
 
  206         if "version" in locals() 
and version != 0:
 
  208                 check_dictionary(classname)
 
  209             except ClassVersionError 
as e:
 
  210                 print_message(
"error", e)
 
  215         expected_version = get_int(options, 
"version")
 
  216         expected_checksum = get_int(options, 
"checksum")
 
  217         if expected_version 
is None or expected_checksum 
is None:
 
  218             print_message(
"warning", 
"no expected version and checksum, cannot spot changes to class")
 
  222         if version != expected_version:
 
  223             print_message(
"error", 
"class version has changed, please update the linkdef",
 
  224                           expected_version=expected_version, actual_Version=version)
 
  226         elif checksum != expected_checksum 
and version != 0:
 
  227             print_message(
"error", 
"class checksum has changed! " 
  228                           "Did you forget increasing the linkdef version?",
 
  229                           expected_checksum=expected_checksum, actual_checksum=checksum)
 
  234 def verify_classlink_version(classname, options):
 
  235     """This function verifies the version and checksum of a class from a given 
  236     linkdef classlink line and returns new values if they can be updated 
  239         classname (str): The classname as in the linkdef file 
  240         options (dict): The options currently present in the linkdef file. 
  241             Previous version and checksum are taken from here if they exist 
  243     * If there's nothing to do it will return None 
  244     * If there's any problem it will raise an exception 
  245     * Otherwise it will return a dictionary containing the new ``version`` and 
  246       ``checksum`` to be put in the file 
  250     version, checksum = get_class_version(classname)
 
  254     if "version" in options:
 
  255         previous_version = int(options[
'version'], 0)
 
  257         if previous_version > version:
 
  259                 "actual class version lower than previous version in linkdef. " 
  260                 "Class version cannot decrease",
 
  261                 previous_version=previous_version, actual_version=version,
 
  265         elif previous_version > -1 
and version > (previous_version + 1):
 
  267                 "actual class version increased by more than one compared to the " 
  268                 "previous version set in linkdef. Please increase ClassDef in single steps",
 
  269                 previous_version=previous_version, actual_version=version,
 
  273         if "checksum" in options:
 
  274             previous_checksum = int(options[
'checksum'], 0)
 
  275             if version == previous_version 
and checksum != previous_checksum:
 
  277                     "checksum changed but class version didn't change. " 
  278                     "Please increase the class version if you modified the class contents",
 
  279                     version=version, actual_checksum=hex(checksum),
 
  280                     previous_checksum=hex(previous_checksum),
 
  283             elif (version, checksum) == (previous_version, previous_checksum):
 
  287     return {
"version": version, 
"checksum": hex(checksum)}
 
  290 def update_linkdef(filename):
 
  292     Update a given linkdef file and update all the versions and checksums 
  295         filename (str): The linkdef.h file to be updated 
  300     for nr, (isclass, content) 
in enumerate(iterate_linkdef(filename)):
 
  303             lines.append(content)
 
  307         line, column, classname, clingflags, options = content
 
  310             if "nostreamer" not in clingflags:
 
  312                 version = verify_classlink_version(classname, options)
 
  313                 if version 
is not None:
 
  314                     options.update(version)
 
  315                     B2INFO(f
"Updating ROOT class version and checksum for {classname}",
 
  316                            linkdef=f
"{os.path.relpath(filename)}:{nr}", **version)
 
  319                     for key, value 
in sorted(options.items()):
 
  320                         optionstr.append(key + (f
"={value}" if value 
is not None else ""))
 
  321                     comment = 
" // " + 
", ".join(optionstr)
 
  322                     lines.append(f
"{line[:column]}{comment}\n")
 
  325         except Exception 
as e:
 
  326             variables = {
"classname": classname, 
"linkdef": f
"{os.path.relpath(filename)}:{nr}"}
 
  327             if isinstance(e, ErrorWithExtraVariables):
 
  328                 variables.update(e.variables)
 
  329             B2ERROR(str(e), **variables)
 
  335     with open(filename, 
"w") 
as newlinkdef:
 
  336         newlinkdef.writelines(lines)