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
29 from basf2
import B2INFO, B2ERROR, B2WARNING
33 """Exception to report class version errors"""
37 """Exception class with extra keyword arguments to show in log message"""
46 def check_base_classes(tclass):
47 """Recursively check all base classes of a TClass to make sure all are well defined"""
48 bases = tclass.GetListOfBases()
52 baseclass = base.GetClassPointer()
55 check_base_classes(baseclass)
58 def check_dictionary(classname):
59 """Make sure we have a dictionary for the class and all its members"""
62 tclass = ROOT.TClass.GetClass(classname)
65 streamerinfo = tclass.GetStreamerInfo()
67 for element
in streamerinfo.GetElements():
68 elementclass = element.GetClassPointer()
69 if elementclass
and not elementclass.IsLoaded():
71 f
"type {elementclass.GetName()}")
74 def get_class_version(classname):
75 """Get the Class version and checksum for a fully qualified C++ class name"""
78 tclass = ROOT.TClass.GetClass(classname)
82 check_base_classes(tclass)
83 version = tclass.GetClassVersion()
84 checksum = tclass.GetCheckSum()
85 return version, checksum
88 def iterate_linkdef(filename):
89 """Iterate through the lines of a ROOT linkdef file.
91 This function is a generator that foreach line returns a tuple where the
92 first element is True if there was a class link pragma on that line and
95 If it was a class link pragma the second element will be a tuple
96 ``(line, startcolumn, classname, linkoptions, options)`` where
98 * ``line`` is the content of the line
99 * ``startcolumn`` is the column the content of the comment started in the line.
100 Everything before should be kept as is
101 * ``classname`` is the class name
102 * ``clingflags`` is the link flags requested by either the optional +-! after
103 the class or the ``options=`` in front of the class and a set of the value
104 "evolution", "nostreamer", and "noinputoper"
105 * ``options`` is a dictionary of the options found in the comment. The
106 content of the comment is split at commas and the result are the options.
107 If a options has a value after ``=`` that will be assigned, otherwise the
108 value is None. For example a comment ``// implicit, version=bar`` would
109 return ``{'implicit':None, 'version':'bar}``
111 If it wasn't link pragma the second element is just the unmodified line
114 class_regex = re.compile(
r"^\s*#pragma\s+link\s+C\+\+\s+(?:options=(.*?)\s)?\s*class\s+(.*?)([+\-!]*);(\s*//\s*(.*))?$")
115 with open(filename)
as linkdef:
117 match = class_regex.match(line)
123 linkflags, classname, flags, comment, content = match.groups()
124 start = match.start(4)
if comment
else len(line) - 1
128 clingflags.add({
"+":
"evolution",
"-":
"nostreamer",
"!":
"noinputoper"}.get(char))
129 if linkflags
is not None:
130 for flag
in linkflags.split(
","):
131 clingflags.add(flag.strip())
134 if content
is not None:
135 optionlist = (e.strip().split(
"=", 1)
for e
in content.split(
","))
136 options = {e[0]: e[1]
if len(e) > 1
else None for e
in optionlist}
138 yield True, (line, start, classname, clingflags, options)
141 def check_linkdef(filename, message_style="belle2"):
142 """Check a linkdef file for expected versions/checksums
144 * If the class has ``version=N, checksum=0x1234`` in the comment in the
145 linkdef compare that to what ROOT has created
146 * If not warn about the missing version and checksum
147 * But skip classes which are not stored to file.
150 relative_filename = os.path.relpath(filename)
152 def print_message(severity, text, **argk):
153 """Print a message similar to GCC or Clang: ``filename:line:column: error: text``
154 so that it gets picked up by the build system
156 nonlocal message_style, errors, relative_filename, nr, column, classname
158 if message_style ==
"gcc":
159 print(f
"{relative_filename}:{nr}:{column}: {severity}: class {classname}: {text}", file=sys.stderr, flush=
True)
161 variables = {
"linkdef": f
"{relative_filename}:{nr}",
"classname": classname}
162 variables.update(argk)
163 if severity ==
"error":
164 B2ERROR(text, **variables)
166 B2WARNING(text, **variables)
168 if severity ==
"error":
171 def get_int(container, key):
172 """Get the value from a container if it exists and try to convert to int.
173 Otherwise show error"""
174 value = options.get(key,
None)
175 if value
is not None:
178 except ValueError
as e:
179 print_message(
"error", f
"expected {key} is not valid: {e}")
182 for nr, (isclass, content)
in enumerate(iterate_linkdef(filename)):
187 line, column, classname, clingflags, options = content
190 if "nostreamer" in clingflags:
191 if "evolution" in clingflags:
192 print_message(
"error",
"both, no streamer and class evolution requested.")
198 elif "evolution" not in clingflags:
199 print_message(
"warning",
"using old ROOT3 streamer format without evolution. "
200 "Please add + or - (for classes not to be written to file) after the classname.")
204 version, checksum = get_class_version(classname)
205 except ClassVersionError
as e:
206 print_message(
"error", e)
209 if "version" in locals()
and version != 0:
211 check_dictionary(classname)
212 except ClassVersionError
as e:
213 print_message(
"error", e)
218 expected_version = get_int(options,
"version")
219 expected_checksum = get_int(options,
"checksum")
220 if expected_version
is None or expected_checksum
is None:
221 print_message(
"warning",
"no expected version and checksum, cannot spot changes to class")
225 if version != expected_version:
226 print_message(
"error",
"class version has changed, please update the linkdef",
227 expected_version=expected_version, actual_Version=version)
229 elif checksum != expected_checksum
and version != 0:
230 print_message(
"error",
"class checksum has changed! "
231 "Did you forget increasing the linkdef version?",
232 expected_checksum=expected_checksum, actual_checksum=checksum)
237 def verify_classlink_version(classname, options):
238 """This function verifies the version and checksum of a class from a given
239 linkdef classlink line and returns new values if they can be updated
242 classname (str): The classname as in the linkdef file
243 options (dict): The options currently present in the linkdef file.
244 Previous version and checksum are taken from here if they exist
246 * If there's nothing to do it will return None
247 * If there's any problem it will raise an exception
248 * Otherwise it will return a dictionary containing the new ``version`` and
249 ``checksum`` to be put in the file
253 version, checksum = get_class_version(classname)
257 if "version" in options:
258 previous_version = int(options[
'version'], 0)
260 if previous_version > version:
262 "actual class version lower than previous version in linkdef. "
263 "Class version cannot decrease",
264 previous_version=previous_version, actual_version=version,
268 elif previous_version > -1
and version > (previous_version + 1):
270 "actual class version increased by more than one compared to the "
271 "previous version set in linkdef. Please increase ClassDef in single steps",
272 previous_version=previous_version, actual_version=version,
276 if "checksum" in options:
277 previous_checksum = int(options[
'checksum'], 0)
278 if version == previous_version
and checksum != previous_checksum:
280 "checksum changed but class version didn't change. "
281 "Please increase the class version if you modified the class contents",
282 version=version, actual_checksum=hex(checksum),
283 previous_checksum=hex(previous_checksum),
286 elif (version, checksum) == (previous_version, previous_checksum):
290 return {
"version": version,
"checksum": hex(checksum)}
293 def update_linkdef(filename):
295 Update a given linkdef file and update all the versions and checksums
298 filename (str): The linkdef.h file to be updated
303 for nr, (isclass, content)
in enumerate(iterate_linkdef(filename)):
306 lines.append(content)
310 line, column, classname, clingflags, options = content
313 if "nostreamer" not in clingflags:
315 version = verify_classlink_version(classname, options)
316 if version
is not None:
317 options.update(version)
318 B2INFO(f
"Updating ROOT class version and checksum for {classname}",
319 linkdef=f
"{os.path.relpath(filename)}:{nr}", **version)
322 for key, value
in sorted(options.items()):
323 optionstr.append(key + (f
"={value}" if value
is not None else ""))
324 comment =
" // " +
", ".join(optionstr)
325 lines.append(f
"{line[:column]}{comment}\n")
328 except Exception
as e:
329 variables = {
"classname": classname,
"linkdef": f
"{os.path.relpath(filename)}:{nr}"}
330 if isinstance(e, ErrorWithExtraVariables):
331 variables.update(e.variables)
332 B2ERROR(str(e), **variables)
338 with open(filename,
"w")
as newlinkdef:
339 newlinkdef.writelines(lines)