Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add edmDumpClassVersion and checkDictionaryUpdate.py scripts to check class version and checksum updates #45423

Merged
merged 10 commits into from
Jul 26, 2024
118 changes: 118 additions & 0 deletions FWCore/Utilities/python/ClassesDefXmlUtils.py
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the code from edmCheckClassVersion that I wanted to re-use into this file (with a few changes that I'll note below).

There is enough of somewhat complicated code shared between edmCheckClassVersion and edmDumpClassVersion that I'd prefer to have the edmDumpClassVersion in CMSSW rather than in cms-bot (which then implies the need to backport to 14_0_X).

Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
class XmlParser(object):
"""Parses a classes_def.xml file looking for class declarations that contain
ClassVersion attributes. Once found looks for sub-elements named 'version'
which contain the ClassVersion to checksum mappings.
"""

#The following are constants used to describe what data is kept
# in which index in the 'classes' member data
originalNameIndex=0
classVersionIndex=1
versionsToChecksumIndex = 2

def __init__(self, filename, includeNonVersionedClasses=False, normalizeClassNames=True):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The includeNonVersionedClasses and normalizeClassNames are new options for this class. The default values are those used by edmCheckClassVersion (I could make that script to pass the values also explicitly). The edmDumpClassVersion uses opposite values.

self._file = filename
self.classes = dict()
self._presentClass = None
self._includeNonVersionedClasses = includeNonVersionedClasses
self._normalizeClassNames = normalizeClassNames
self.readClassesDefXML()
def readClassesDefXML(self):
import xml.parsers.expat
p = xml.parsers.expat.ParserCreate()
p.StartElementHandler = self.start_element
p.EndElementHandler = self.end_element
f = open(self._file)
# Replace any occurence of <>& in the attribute values by the xml parameter
rxml, nxml = f.read(), ''
q1,q2 = 0,0
for c in rxml :
if (q1 or q2) and c == '<' : nxml += '&lt;'
elif (q1 or q2) and c == '>' : nxml += '&gt;'
# elif (q1 or q2) and c == '&' : nxml += '&amp;'
else : nxml += c
if c == '"' : q1 = not q1
if c == "'" : q2 = not q2
try : p.Parse(nxml)
except xml.parsers.expat.ExpatError as e :
print ('--->> edmCheckClassVersion: ERROR: parsing selection file ',self._file)
print ('--->> edmCheckClassVersion: ERROR: Error is:', e)
raise
f.close()
def start_element(self,name,attrs):
if name in ('class','struct'):
if 'name' in attrs:
if 'ClassVersion' in attrs:
normalizedName = self.genNName(attrs['name'])
self.classes[normalizedName]=[attrs['name'],int(attrs['ClassVersion']),[]]
self._presentClass=normalizedName
elif self._includeNonVersionedClasses:
# skip transient data products
if not ('persistent' in attrs and attrs['persistent'] == "false"):
normalizedName = self.genNName(attrs['name'])
self.classes[normalizedName]=[attrs['name'],-1,[]]
if name == 'version':
self.classes[self._presentClass][XmlParser.versionsToChecksumIndex].append([int(attrs['ClassVersion']),
int(attrs['checksum'])])
pass
def end_element(self,name):
if name in ('class','struct'):
self._presentClass = None
def genNName(self, name ):
if not self._normalizeClassNames:
return name
n_name = " ".join(name.split())
for e in [ ['long long unsigned int', 'unsigned long long'],
['long long int', 'long long'],
['unsigned short int', 'unsigned short'],
['short unsigned int', 'unsigned short'],
['short int', 'short'],
['long unsigned int', 'unsigned long'],
['unsigned long int', 'unsigned long'],
['long int', 'long'],
['std::string', 'std::basic_string<char>']] :
n_name = n_name.replace(e[0],e[1])
n_name = n_name.replace(' ','')
return n_name

def initCheckClass():
"""Must be called before checkClass()"""
import ROOT
ROOT.gROOT.ProcessLine("class checkclass {public: int f(char const* name) {TClass* cl = TClass::GetClass(name); bool b = false; cl->GetCheckSum(b); return (int)b;} };")
ROOT.gROOT.ProcessLine("checkclass checkTheClass;")


#The following are error codes returned from checkClass
noError = 0
errorRootDoesNotMatchClassDef =1
errorMustUpdateClassVersion=2
errorMustAddChecksum=3

def checkClass(name,version,versionsToChecksums):
import ROOT
c = ROOT.TClass.GetClass(name)
if not c:
raise RuntimeError("failed to load dictionary for class '"+name+"'")
temp = "checkTheClass.f(" + '"' + name + '"' + ");"
retval = ROOT.gROOT.ProcessLine(temp)
if retval == 0 :
raise RuntimeError("TClass::GetCheckSum: Failed to load dictionary for base class. See previous Error message")
classChecksum = c.GetCheckSum()
classVersion = c.GetClassVersion()

#does this version match what is in the file?
if version != classVersion:
return (errorRootDoesNotMatchClassDef,classVersion,classChecksum)

#is the version already in our list?
found = False

for v,cs in versionsToChecksums:
if v == version:
found = True
if classChecksum != cs:
return (errorMustUpdateClassVersion,classVersion,classChecksum)
break
if not found and classVersion != 0:
return (errorMustAddChecksum,classVersion,classChecksum)
return (noError,classVersion,classChecksum)
123 changes: 9 additions & 114 deletions FWCore/Utilities/scripts/edmCheckClassVersion
Original file line number Diff line number Diff line change
@@ -1,77 +1,7 @@
#! /usr/bin/env python3
from sys import version_info
if version_info[0] > 2:
atol = int
else:
from string import atol
Comment on lines -2 to -6
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just replaced all atol() calls with int().


class XmlParser(object):
"""Parses a classes_def.xml file looking for class declarations that contain
ClassVersion attributes. Once found looks for sub-elements named 'version'
which contain the ClassVersion to checksum mappings.
"""

#The following are constants used to describe what data is kept
# in which index in the 'classes' member data
originalNameIndex=0
classVersionIndex=1
versionsToChecksumIndex = 2

def __init__(self,filename):
self._file = filename
self.classes = dict()
self._presentClass = None
self.readClassesDefXML()
def readClassesDefXML(self):
import xml.parsers.expat
p = xml.parsers.expat.ParserCreate()
p.StartElementHandler = self.start_element
p.EndElementHandler = self.end_element
f = open(self._file)
# Replace any occurence of <>& in the attribute values by the xml parameter
rxml, nxml = f.read(), ''
q1,q2 = 0,0
for c in rxml :
if (q1 or q2) and c == '<' : nxml += '&lt;'
elif (q1 or q2) and c == '>' : nxml += '&gt;'
# elif (q1 or q2) and c == '&' : nxml += '&amp;'
else : nxml += c
if c == '"' : q1 = not q1
if c == "'" : q2 = not q2
try : p.Parse(nxml)
except xml.parsers.expat.ExpatError as e :
print ('--->> edmCheckClassVersion: ERROR: parsing selection file ',self._file)
print ('--->> edmCheckClassVersion: ERROR: Error is:', e)
raise
f.close()
def start_element(self,name,attrs):
if name in ('class','struct'):
if 'name' in attrs:
if 'ClassVersion' in attrs:
normalizedName = self.genNName(attrs['name'])
self.classes[normalizedName]=[attrs['name'],atol(attrs['ClassVersion']),[]]
self._presentClass=normalizedName
if name == 'version':
self.classes[self._presentClass][XmlParser.versionsToChecksumIndex].append([atol(attrs['ClassVersion']),
atol(attrs['checksum'])])
pass
def end_element(self,name):
if name in ('class','struct'):
self._presentClass = None
def genNName(self, name ):
n_name = " ".join(name.split())
for e in [ ['long long unsigned int', 'unsigned long long'],
['long long int', 'long long'],
['unsigned short int', 'unsigned short'],
['short unsigned int', 'unsigned short'],
['short int', 'short'],
['long unsigned int', 'unsigned long'],
['unsigned long int', 'unsigned long'],
['long int', 'long'],
['std::string', 'std::basic_string<char>']] :
n_name = n_name.replace(e[0],e[1])
n_name = n_name.replace(' ','')
return n_name

from FWCore.Utilities.ClassesDefXmlUtils import XmlParser
makortel marked this conversation as resolved.
Show resolved Hide resolved
import FWCore.Utilities.ClassesDefXmlUtils as ClassesDefUtils

# recursively check the base classes for a class pointer
# as building the streamer will crash if base classes are
Expand Down Expand Up @@ -116,40 +46,6 @@ def checkDictionaries(name):

return missingDict

#The following are error codes returned from checkClass
noError = 0
errorRootDoesNotMatchClassDef =1
errorMustUpdateClassVersion=2
errorMustAddChecksum=3

def checkClass(name,version,versionsToChecksums):
c = ROOT.TClass.GetClass(name)
if not c:
raise RuntimeError("failed to load dictionary for class '"+name+"'")
temp = "checkTheClass.f(" + '"' + name + '"' + ");"
retval = ROOT.gROOT.ProcessLine(temp)
if retval == 0 :
raise RuntimeError("TClass::GetCheckSum: Failed to load dictionary for base class. See previous Error message")
classChecksum = c.GetCheckSum()
classVersion = c.GetClassVersion()

#does this version match what is in the file?
if version != classVersion:
return (errorRootDoesNotMatchClassDef,classVersion,classChecksum)

#is the version already in our list?
found = False

for v,cs in versionsToChecksums:
if v == version:
found = True
if classChecksum != cs:
return (errorMustUpdateClassVersion,classVersion,classChecksum)
break
if not found and classVersion != 0:
return (errorMustAddChecksum,classVersion,classChecksum)
return (noError,classVersion,classChecksum)

#Setup the options
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
oparser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
Expand Down Expand Up @@ -180,14 +76,13 @@ else:

missingDict = 0

ROOT.gROOT.ProcessLine("class checkclass {public: int f(char const* name) {TClass* cl = TClass::GetClass(name); bool b = false; cl->GetCheckSum(b); return (int)b;} };")
ROOT.gROOT.ProcessLine("checkclass checkTheClass;")
ClassesDefUtils.initCheckClass()

p = XmlParser(options.xmlfile)
foundErrors = dict()
for name,info in p.classes.items():
errorCode,rootClassVersion,classChecksum = checkClass(name,info[XmlParser.classVersionIndex],info[XmlParser.versionsToChecksumIndex])
if errorCode != noError:
errorCode,rootClassVersion,classChecksum = ClassesDefUtils.checkClass(name,info[XmlParser.classVersionIndex],info[XmlParser.versionsToChecksumIndex])
if errorCode != ClassesDefUtils.noError:
foundErrors[name]=(errorCode,classChecksum,rootClassVersion)
if options.checkdict :
missingDict += checkDictionaries(name)
Expand All @@ -201,10 +96,10 @@ for name,retValues in foundErrors.items():
classVersion = p.classes[name][XmlParser.classVersionIndex]
classChecksum = retValues[1]
rootClassVersion = retValues[2]
if code == errorRootDoesNotMatchClassDef:
if code == ClassesDefUtils.errorRootDoesNotMatchClassDef:
foundRootDoesNotMatchError=True
print ("error: for class '"+name+"' ROOT says the ClassVersion is "+str(rootClassVersion)+" but classes_def.xml says it is "+str(classVersion)+". Are you sure everything compiled correctly?")
elif code == errorMustUpdateClassVersion and not options.generate:
elif code == ClassesDefUtils.errorMustUpdateClassVersion and not options.generate:
print ("error: class '"+name+"' has a different checksum for ClassVersion "+str(classVersion)+". Increment ClassVersion to "+str(classVersion+1)+" and assign it to checksum "+str(classChecksum))
elif not options.generate:
print ("error:class '"+name+"' needs to include the following as part of its 'class' declaration")
Expand All @@ -227,7 +122,7 @@ if options.generate and not foundRootDoesNotMatchError and not missingDict:
classVersion = p.classes[normName][XmlParser.classVersionIndex]
code,checksum,rootClassVersion = foundErrors[normName]
hasNoSubElements = (-1 != l.find('/>'))
if code == errorMustUpdateClassVersion:
if code == ClassesDefUtils.errorMustUpdateClassVersion:
classVersion += 1
parts = splitArgs[:]
indexToClassVersion = 0
Expand Down
34 changes: 34 additions & 0 deletions FWCore/Utilities/scripts/edmDumpClassVersion
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python3

import argparse

import FWCore.Utilities.ClassesDefXmlUtils as ClassesDefUtils

def main(args):
#Need to not have ROOT load .rootlogon.(C|py) since it can cause interference.
makortel marked this conversation as resolved.
Show resolved Hide resolved
import ROOT
ROOT.PyConfig.DisableRootLogon = True

#Keep ROOT from trying to use X11
ROOT.gROOT.SetBatch(True)
ROOT.gROOT.ProcessLine(".autodict")
if args.library is not None:
if ROOT.gSystem.Load(args.library) < 0 :
raise RuntimeError("failed to load library '"+args.library+"'")

ClassesDefUtils.initCheckClass()
p = ClassesDefUtils.XmlParser(args.xmlfile, includeNonVersionedClasses=True, normalizeClassNames=False)
for name, info in p.classes.items():
(error, version, checksum) = ClassesDefUtils.checkClass(name, 0, {})
print(name, version, checksum)

if __name__ == "__main__":
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Prints class versions and checksums for all non-transient clases defined in a given classes_def.xml file")
parser.add_argument("-l","--lib", dest="library", type=str,
help="specify the library to load. If not set classes are found using the PluginManager")
parser.add_argument("-x","--xml_file", dest="xmlfile",default="./classes_def.xml", type=str,
help="the classes_def.xml file to read")

args = parser.parse_args()
main(args)