Skip to content

Commit ba76dcb

Browse files
authored
Merge pull request #45423 from makortel/edmDumpClassVersion
Add edmDumpClassVersion and checkDictionaryUpdate.py scripts to check class version and checksum updates
2 parents 411f92c + 444d6a3 commit ba76dcb

23 files changed

+593
-255
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
class XmlParser(object):
2+
"""Parses a classes_def.xml file looking for class declarations that contain
3+
ClassVersion attributes. Once found looks for sub-elements named 'version'
4+
which contain the ClassVersion to checksum mappings.
5+
"""
6+
7+
#The following are constants used to describe what data is kept
8+
# in which index in the 'classes' member data
9+
originalNameIndex=0
10+
classVersionIndex=1
11+
versionsToChecksumIndex = 2
12+
13+
def __init__(self, filename, includeNonVersionedClasses=False, normalizeClassNames=True):
14+
self._file = filename
15+
self.classes = dict()
16+
self._presentClass = None
17+
self._presentClassForVersion = None
18+
self._includeNonVersionedClasses = includeNonVersionedClasses
19+
self._normalizeClassNames = normalizeClassNames
20+
self.readClassesDefXML()
21+
def readClassesDefXML(self):
22+
import xml.parsers.expat
23+
p = xml.parsers.expat.ParserCreate()
24+
p.StartElementHandler = self.start_element
25+
p.EndElementHandler = self.end_element
26+
f = open(self._file)
27+
# Replace any occurence of <>& in the attribute values by the xml parameter
28+
rxml, nxml = f.read(), ''
29+
q1,q2 = 0,0
30+
for c in rxml :
31+
if (q1 or q2) and c == '<' : nxml += '&lt;'
32+
elif (q1 or q2) and c == '>' : nxml += '&gt;'
33+
# elif (q1 or q2) and c == '&' : nxml += '&amp;'
34+
else : nxml += c
35+
if c == '"' : q1 = not q1
36+
if c == "'" : q2 = not q2
37+
try : p.Parse(nxml)
38+
except xml.parsers.expat.ExpatError as e :
39+
print ('--->> edmCheckClassVersion: ERROR: parsing selection file ',self._file)
40+
print ('--->> edmCheckClassVersion: ERROR: Error is:', e)
41+
raise
42+
f.close()
43+
def start_element(self,name,attrs):
44+
if name in ('class','struct'):
45+
if 'name' in attrs:
46+
self._presentClass=attrs['name']
47+
normalizedName = self.genNName(attrs['name'])
48+
if 'ClassVersion' in attrs:
49+
self.classes[normalizedName]=[attrs['name'],int(attrs['ClassVersion']),[]]
50+
self._presentClassForVersion=normalizedName
51+
elif self._includeNonVersionedClasses:
52+
# skip transient data products
53+
if not ('persistent' in attrs and attrs['persistent'] == "false"):
54+
self.classes[normalizedName]=[attrs['name'],-1,[]]
55+
else:
56+
raise RuntimeError(f"There is an element '{name}' without 'name' attribute.")
57+
if name == 'version':
58+
if self._presentClassForVersion is None:
59+
raise RuntimeError(f"Class element for type '{self._presentClass}' contains a 'version' element, but 'ClassVersion' attribute is missing from the 'class' element")
60+
try:
61+
classVersion = int(attrs['ClassVersion'])
62+
except KeyError:
63+
raise RuntimeError(f"Version element for type '{self._presentClass}' is missing 'ClassVersion' attribute")
64+
try:
65+
checksum = int(attrs['checksum'])
66+
except KeyError:
67+
raise RuntimeError(f"Version element for type '{self._presentClass}' is missing 'checksum' attribute")
68+
self.classes[self._presentClassForVersion][XmlParser.versionsToChecksumIndex].append([classVersion, checksum])
69+
pass
70+
def end_element(self,name):
71+
if name in ('class','struct'):
72+
self._presentClass = None
73+
self._presentClassForVersion = None
74+
def genNName(self, name ):
75+
if not self._normalizeClassNames:
76+
return name
77+
n_name = " ".join(name.split())
78+
for e in [ ['long long unsigned int', 'unsigned long long'],
79+
['long long int', 'long long'],
80+
['unsigned short int', 'unsigned short'],
81+
['short unsigned int', 'unsigned short'],
82+
['short int', 'short'],
83+
['long unsigned int', 'unsigned long'],
84+
['unsigned long int', 'unsigned long'],
85+
['long int', 'long'],
86+
['std::string', 'std::basic_string<char>']] :
87+
n_name = n_name.replace(e[0],e[1])
88+
n_name = n_name.replace(' ','')
89+
return n_name
90+
91+
def initROOT(library):
92+
#Need to not have ROOT load .rootlogon.(C|py) since it can cause interference.
93+
import ROOT
94+
ROOT.PyConfig.DisableRootLogon = True
95+
96+
#Keep ROOT from trying to use X11
97+
ROOT.gROOT.SetBatch(True)
98+
ROOT.gROOT.ProcessLine(".autodict")
99+
if library is not None:
100+
if ROOT.gSystem.Load(library) < 0 :
101+
raise RuntimeError("failed to load library '"+library+"'")
102+
103+
def initCheckClass():
104+
"""Must be called before checkClass()"""
105+
import ROOT
106+
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;} };")
107+
ROOT.gROOT.ProcessLine("checkclass checkTheClass;")
108+
109+
110+
#The following are error codes returned from checkClass
111+
noError = 0
112+
errorRootDoesNotMatchClassDef =1
113+
errorMustUpdateClassVersion=2
114+
errorMustAddChecksum=3
115+
116+
def checkClass(name,version,versionsToChecksums):
117+
import ROOT
118+
c = ROOT.TClass.GetClass(name)
119+
if not c:
120+
raise RuntimeError("failed to load dictionary for class '"+name+"'")
121+
temp = "checkTheClass.f(" + '"' + name + '"' + ");"
122+
retval = ROOT.gROOT.ProcessLine(temp)
123+
if retval == 0 :
124+
raise RuntimeError("TClass::GetCheckSum: Failed to load dictionary for base class. See previous Error message")
125+
classChecksum = c.GetCheckSum()
126+
classVersion = c.GetClassVersion()
127+
128+
#does this version match what is in the file?
129+
if version != classVersion:
130+
return (errorRootDoesNotMatchClassDef,classVersion,classChecksum)
131+
132+
#is the version already in our list?
133+
found = False
134+
135+
for v,cs in versionsToChecksums:
136+
if v == version:
137+
found = True
138+
if classChecksum != cs:
139+
return (errorMustUpdateClassVersion,classVersion,classChecksum)
140+
break
141+
if not found and classVersion != 0:
142+
return (errorMustAddChecksum,classVersion,classChecksum)
143+
return (noError,classVersion,classChecksum)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#! /usr/bin/env python3
2+
3+
import sys
4+
import FWCore.Reflection.ClassesDefXmlUtils as ClassesDefUtils
5+
6+
# recursively check the base classes for a class pointer
7+
# as building the streamer will crash if base classes are
8+
# incomplete
9+
def verifyBaseClasses(c) :
10+
missingBase = 0
11+
12+
# check that all bases are loaded
13+
bases = c.GetListOfBases()
14+
if not bases :
15+
print ("Incomplete class ", c.GetName())
16+
return 1
17+
18+
for b in bases :
19+
bc = b.GetClassPointer()
20+
if bc :
21+
missingBase += verifyBaseClasses(bc)
22+
else :
23+
print ("Incomplete base class for ", c.GetName(), ": ", b.GetName())
24+
missingBase += 1
25+
26+
return missingBase
27+
28+
def checkDictionaries(name):
29+
c = ROOT.TClass.GetClass(name)
30+
if not c:
31+
raise RuntimeError("failed to load dictionary for class '"+name+"'")
32+
33+
missingDict = verifyBaseClasses(c)
34+
if missingDict == 0 :
35+
si = c.GetStreamerInfo()
36+
if si :
37+
ts = si.GetElements()
38+
for telem in ts :
39+
clm = telem.GetClassPointer()
40+
if clm and not clm.IsLoaded() :
41+
print ("Missing dictionary for ", telem.GetName(), " type ", clm.GetName())
42+
missingDict += 1
43+
else :
44+
print ("No streamer info for ", c.GetName())
45+
missingDict += 1
46+
47+
return missingDict
48+
49+
#Setup the options
50+
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
51+
oparser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
52+
oparser.add_argument("-d","--check_dictionaries", dest="checkdict",action="store_true",default=False,
53+
help="check that all required dictionaries are loaded")
54+
oparser.add_argument("-l","--lib", dest="library", type=str,
55+
help="specify the library to load. If not set classes are found using the PluginManager")
56+
oparser.add_argument("-x","--xml_file", dest="xmlfile",default="./classes_def.xml", type=str,
57+
help="the classes_def.xml file to read")
58+
oparser.add_argument("-g","--generate_new",dest="generate", action="store_true",default=False,
59+
help="instead of issuing errors, generate a new classes_def.xml file.")
60+
61+
options=oparser.parse_args()
62+
63+
ClassesDefUtils.initROOT(options.library)
64+
if options.library is None and options.checkdict:
65+
print ("Dictionary checks require a specific library")
66+
67+
missingDict = 0
68+
69+
ClassesDefUtils.initCheckClass()
70+
71+
try:
72+
p = ClassesDefUtils.XmlParser(options.xmlfile)
73+
except RuntimeError as e:
74+
print(f"Parsing {options.xmlfile} failed: {e}")
75+
sys.exit(1)
76+
foundErrors = dict()
77+
for name,info in p.classes.items():
78+
errorCode,rootClassVersion,classChecksum = ClassesDefUtils.checkClass(name,info[ClassesDefUtils.XmlParser.classVersionIndex],info[ClassesDefUtils.XmlParser.versionsToChecksumIndex])
79+
if errorCode != ClassesDefUtils.noError:
80+
foundErrors[name]=(errorCode,classChecksum,rootClassVersion)
81+
if options.checkdict :
82+
missingDict += checkDictionaries(name)
83+
84+
foundRootDoesNotMatchError = False
85+
originalToNormalizedNames = dict()
86+
for name,retValues in foundErrors.items():
87+
origName = p.classes[name][ClassesDefUtils.XmlParser.originalNameIndex]
88+
originalToNormalizedNames[origName]=name
89+
code = retValues[0]
90+
classVersion = p.classes[name][ClassesDefUtils.XmlParser.classVersionIndex]
91+
classChecksum = retValues[1]
92+
rootClassVersion = retValues[2]
93+
if code == ClassesDefUtils.errorRootDoesNotMatchClassDef:
94+
foundRootDoesNotMatchError=True
95+
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?")
96+
elif code == ClassesDefUtils.errorMustUpdateClassVersion and not options.generate:
97+
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))
98+
elif not options.generate:
99+
print ("error:class '"+name+"' needs to include the following as part of its 'class' declaration")
100+
print (' <version ClassVersion="'+str(classVersion)+'" checksum="'+str(classChecksum)+'"/>')
101+
102+
103+
if options.generate and not foundRootDoesNotMatchError and not missingDict:
104+
f = open(options.xmlfile)
105+
outFile = open('classes_def.xml.generated','w')
106+
out = ''
107+
for l in f.readlines():
108+
newLine = l
109+
if -1 != l.find('<class') and -1 != l.find('ClassVersion'):
110+
splitArgs = l.split('"')
111+
name = splitArgs[1]
112+
normName = originalToNormalizedNames.get(name,None)
113+
if normName is not None:
114+
indent = l.find('<')
115+
#this is a class with a problem
116+
classVersion = p.classes[normName][XmlParser.classVersionIndex]
117+
code,checksum,rootClassVersion = foundErrors[normName]
118+
hasNoSubElements = (-1 != l.find('/>'))
119+
if code == ClassesDefUtils.errorMustUpdateClassVersion:
120+
classVersion += 1
121+
parts = splitArgs[:]
122+
indexToClassVersion = 0
123+
for pt in parts:
124+
indexToClassVersion +=1
125+
if -1 != pt.find('ClassVersion'):
126+
break
127+
parts[indexToClassVersion]=str(classVersion)
128+
newLine = '"'.join(parts)
129+
130+
if hasNoSubElements:
131+
newLine = newLine.replace('/','')
132+
out +=newLine
133+
newLine =' '*indent+' <version ClassVersion="'+str(classVersion)+'" checksum="'+str(checksum)+'"/>\n'
134+
if hasNoSubElements:
135+
out += newLine
136+
newLine=' '*indent+'</class>\n'
137+
out +=newLine
138+
139+
outFile.writelines(out)
140+
141+
if (len(foundErrors)>0 and not options.generate) or (options.generate and foundRootDoesNotMatchError) or missingDict:
142+
import sys
143+
sys.exit(1)
144+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import json
5+
import argparse
6+
7+
import FWCore.Reflection.ClassesDefXmlUtils as ClassesDefUtils
8+
9+
def main(args):
10+
ClassesDefUtils.initROOT(args.library)
11+
12+
ClassesDefUtils.initCheckClass()
13+
try:
14+
p = ClassesDefUtils.XmlParser(args.xmlfile, includeNonVersionedClasses=True, normalizeClassNames=False)
15+
except RuntimeError as e:
16+
print(f"Parsing {args.xmlfile} failed: {e}")
17+
sys.exit(1)
18+
19+
out = {}
20+
for name, info in p.classes.items():
21+
try:
22+
(error, version, checksum) = ClassesDefUtils.checkClass(name, 0, {})
23+
except RuntimeError as e:
24+
print(f"Ignoring class {name} as could not get its version and checksum, because: {e}")
25+
continue
26+
out[name] = dict(
27+
version = version,
28+
checksum = checksum
29+
)
30+
out_js = json.dumps(out, sort_keys=True, indent=1)
31+
if args.output is None:
32+
print(out_js)
33+
else:
34+
with open(args.output, "w") as f:
35+
f.write(out_js)
36+
return 0
37+
38+
if __name__ == "__main__":
39+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
40+
description="Extracts class versions and checksums, in JSON format, for all non-transient clases defined in a given classes_def.xml file")
41+
parser.add_argument("-l","--lib", dest="library", type=str,
42+
help="specify the library to load. If not set classes are found using the PluginManager")
43+
parser.add_argument("-x","--xml_file", dest="xmlfile",default="./classes_def.xml", type=str,
44+
help="the classes_def.xml file to read")
45+
parser.add_argument("-o", "--output", type=str, default=None,
46+
help="File to save the output. If no file is specified, the JSON document is printed in stdout")
47+
48+
args = parser.parse_args()
49+
sys.exit(main(args))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<library file="stubs/*.cc" name="FWCoreReflectionTestObjects">
2+
<flags LCG_DICT_HEADER="stubs/classes.h"/>
3+
<flags LCG_DICT_XML="stubs/classes_def.xml"/>
4+
<use name="DataFormats/Common"/>
5+
</library>
6+
7+
<test name="TestFWCoreReflectionCheckClassVersion" command="run_checkClassVersion.sh"/>
8+
<test name="TestFWCoreReflectionDumpClassVersion" command="run_dumpClassVersion.sh"/>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"edm::Wrapper<edmtest::reflection::IntObject>": {
3+
"checksum": 536952283,
4+
"version": 4
5+
},
6+
"edmtest::reflection::IntObject": {
7+
"checksum": 427917710,
8+
"version": 3
9+
}
10+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
3+
function die { echo Failure $1: status $2 ; exit $2 ; }
4+
5+
XMLPATH=${SCRAM_TEST_PATH}/stubs
6+
LIBFILE=${LOCALTOP}/lib/${SCRAM_ARCH}/libFWCoreReflectionTestObjects.so
7+
8+
edmCheckClassVersion -l ${LIBFILE} -x ${XMLPATH}/classes_def.xml || die "edmCheckClassVersion failed" $?
9+
10+
function runFailure {
11+
edmCheckClassVersion -l ${LIBFILE} -x ${XMLPATH}/$1 > log.txt && die "edmCheckClassVersion for $1 did not fail" 1
12+
grep -q "$2" log.txt
13+
RET=$?
14+
if [ "$RET" != "0" ]; then
15+
echo "edmCheckClassVersion for $1 did not contain '$2', log is below"
16+
cat log.txt
17+
exit 1
18+
fi
19+
}
20+
21+
runFailure test_def_nameMissing.xml "There is an element 'class' without 'name' attribute"
22+
runFailure test_def_ClassVersionMissingInClass.xml "Class element for type 'edmtest::reflection::IntObject' contains a 'version' element, but 'ClassVersion' attribute is missing from the 'class' element"
23+
runFailure test_def_ClassVersionMissingInVersion.xml "Version element for type 'edmtest::reflection::IntObject' is missing 'ClassVersion' attribute"
24+
runFailure test_def_checksumMissingInVersion.xml "Version element for type 'edmtest::reflection::IntObject' is missing 'checksum' attribute"

0 commit comments

Comments
 (0)