9b2test_utils.classversion - Helper functions to inspect and verify ROOT dictionaries
10------------------------------------------------------------------------------------
12This module contains some functions to make sure classes which have a ROOT
13dictionary 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
19Users should only need to call ``b2code-classversion-check``
and
20``b2code-classversion-update``. The documentation of
21``b2code-classversion-check`` also contains a bit more detailed explanation of
22class versions and checksums
28from basf2 import B2INFO, B2ERROR, B2WARNING
31class ClassVersionError(Exception):
32 """Exception to report class version errors"""
36 """Exception class with extra keyword arguments to show in log message"""
45def check_base_classes(tclass):
46 """Recursively check all base classes of a TClass to make sure all are well defined"""
47 bases = tclass.GetListOfBases()
51 baseclass = base.GetClassPointer()
54 check_base_classes(baseclass)
57def check_dictionary(classname):
58 """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()}")
73def get_class_version(classname):
74 """Get the Class version and checksum for a fully qualified C++ class name"""
77 tclass = ROOT.TClass.GetClass(classname)
81 check_base_classes(tclass)
82 version = tclass.GetClassVersion()
83 checksum = tclass.GetCheckSum()
84 return version, checksum
87def iterate_linkdef(filename):
88 """Iterate through the lines of a ROOT linkdef file.
90 This function is a generator that foreach line returns a tuple where the
91 first element
is True if there was a
class link pragma on that line and
94 If it was a
class link pragma the second element will be a tuple
95 ``(line, startcolumn, classname, linkoptions, options)`` where
97 * ``line``
is the content of the line
98 * ``startcolumn``
is the column the content of the comment started
in the line.
99 Everything before should be kept
as is
100 * ``classname``
is the
class name
101 * ``clingflags``
is the link flags requested by either the optional +-! after
102 the
class or the ``options=``
in front of the
class and a set of the value
103 "evolution",
"nostreamer",
and "noinputoper"
104 * ``options``
is a dictionary of the options found
in the comment. The
105 content of the comment
is split at commas
and the result are the options.
106 If a options has a value after ``=`` that will be assigned, otherwise the
107 value
is None. For example a comment ``// implicit, version=bar`` would
108 return ``{
'implicit':
None,
'version':
'bar}``
110 If it wasn't link pragma the second element is just the unmodified line
113 class_regex = re.compile(
r"^\s*#pragma\s+link\s+C\+\+\s+(?:options=(.*?)\s)?\s*class\s+(.*?)([+\-!]*);(\s*//\s*(.*))?$")
114 with open(filename)
as linkdef:
116 match = class_regex.match(line)
122 linkflags, classname, flags, comment, content = match.groups()
123 start = match.start(4)
if comment
else len(line) - 1
127 clingflags.add({
"+":
"evolution",
"-":
"nostreamer",
"!":
"noinputoper"}.get(char))
128 if linkflags
is not None:
129 for flag
in linkflags.split(
","):
130 clingflags.add(flag.strip())
133 if content
is not None:
134 optionlist = (e.strip().split(
"=", 1)
for e
in content.split(
","))
135 options = {e[0]: e[1]
if len(e) > 1
else None for e
in optionlist}
137 yield True, (line, start, classname, clingflags, options)
140def check_linkdef(filename, message_style="belle2"):
141 """Check a linkdef file for expected versions/checksums
143 * If the class has ``version=N, checksum=0x1234``
in the comment
in the
144 linkdef compare that to what ROOT has created
145 * If
not warn about the missing version
and checksum
146 * But skip classes which are
not stored to file.
149 relative_filename = os.path.relpath(filename)
151 def print_message(severity, text, **argk):
152 """Print a message similar to GCC or Clang: ``filename:line:column: error: text``
153 so that it gets picked up by the build system
155 nonlocal message_style, errors, relative_filename, nr, column, classname
157 if message_style ==
"gcc":
158 print(f
"{relative_filename}:{nr}:{column}: {severity}: class {classname}: {text}", file=sys.stderr, flush=
True)
160 variables = {
"linkdef": f
"{relative_filename}:{nr}",
"classname": classname}
161 variables.update(argk)
162 if severity ==
"error":
163 B2ERROR(text, **variables)
165 B2WARNING(text, **variables)
167 if severity ==
"error":
170 def get_int(container, key):
171 """Get the value from a container if it exists and try to convert to int.
172 Otherwise show error"""
173 value = options.get(key, None)
174 if value
is not None:
177 except ValueError
as e:
178 print_message(
"error", f
"expected {key} is not valid: {e}")
181 for nr, (isclass, content)
in enumerate(iterate_linkdef(filename)):
186 line, column, classname, clingflags, options = content
189 if "nostreamer" in clingflags:
190 if "evolution" in clingflags:
191 print_message(
"error",
"both, no streamer and class evolution requested.")
197 elif "evolution" not in clingflags:
198 print_message(
"warning",
"using old ROOT3 streamer format without evolution. "
199 "Please add + or - (for classes not to be written to file) after the classname.")
203 version, checksum = get_class_version(classname)
204 except ClassVersionError
as e:
205 print_message(
"error", e)
208 if "version" in locals()
and version != 0:
210 check_dictionary(classname)
211 except ClassVersionError
as e:
212 print_message(
"error", e)
217 expected_version = get_int(options,
"version")
218 expected_checksum = get_int(options,
"checksum")
219 if expected_version
is None or expected_checksum
is None:
220 print_message(
"warning",
"no expected version and checksum, cannot spot changes to class")
224 if version != expected_version:
225 print_message(
"error",
"class version has changed, please update the linkdef",
226 expected_version=expected_version, actual_Version=version)
228 elif checksum != expected_checksum
and version != 0:
229 print_message(
"error",
"class checksum has changed! "
230 "Did you forget increasing the linkdef version?",
231 expected_checksum=expected_checksum, actual_checksum=checksum)
236def verify_classlink_version(classname, options):
237 """This function verifies the version and checksum of a class from a given
238 linkdef classlink line and returns new values
if they can be updated
241 classname (str): The classname
as in the linkdef file
242 options (dict): The options currently present
in the linkdef file.
243 Previous version
and checksum are taken
from here
if they exist
245 * If there
's nothing to do it will return None
246 * If there's any problem it will raise an exception
247 * Otherwise it will return a dictionary containing the new ``version``
and
248 ``checksum`` to be put
in the file
252 version, checksum = get_class_version(classname)
256 if "version" in options:
257 previous_version = int(options[
'version'], 0)
259 if previous_version > version:
261 "actual class version lower than previous version in linkdef. "
262 "Class version cannot decrease",
263 previous_version=previous_version, actual_version=version,
267 elif previous_version > -1
and version > (previous_version + 1):
269 "actual class version increased by more than one compared to the "
270 "previous version set in linkdef. Please increase ClassDef in single steps",
271 previous_version=previous_version, actual_version=version,
275 if "checksum" in options:
276 previous_checksum = int(options[
'checksum'], 0)
277 if version == previous_version
and checksum != previous_checksum:
279 "checksum changed but class version didn't change. "
280 "Please increase the class version if you modified the class contents",
281 version=version, actual_checksum=hex(checksum),
282 previous_checksum=hex(previous_checksum),
285 elif (version, checksum) == (previous_version, previous_checksum):
289 return {
"version": version,
"checksum": hex(checksum)}
292def update_linkdef(filename):
294 Update a given linkdef file and update all the versions
and checksums
297 filename (str): The linkdef.h file to be updated
302 for nr, (isclass, content)
in enumerate(iterate_linkdef(filename)):
305 lines.append(content)
309 line, column, classname, clingflags, options = content
312 if "nostreamer" not in clingflags:
314 version = verify_classlink_version(classname, options)
315 if version
is not None:
316 options.update(version)
317 B2INFO(f
"Updating ROOT class version and checksum for {classname}",
318 linkdef=f
"{os.path.relpath(filename)}:{nr}", **version)
321 for key, value
in sorted(options.items()):
322 optionstr.append(key + (f
"={value}" if value
is not None else ""))
323 comment =
" // " +
", ".join(optionstr)
324 lines.append(f
"{line[:column]}{comment}\n")
327 except Exception
as e:
328 variables = {
"classname": classname,
"linkdef": f
"{os.path.relpath(filename)}:{nr}"}
329 if isinstance(e, ErrorWithExtraVariables):
330 variables.update(e.variables)
331 B2ERROR(str(e), **variables)
337 with open(filename,
"w")
as newlinkdef:
338 newlinkdef.writelines(lines)