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)