Git init
[external/libxml2.git] / regressions.py
1 #!/usr/bin/python -u
2 import glob, os, string, sys, thread, time
3 # import difflib
4 import libxml2
5
6 ###
7 #
8 # This is a "Work in Progress" attempt at a python script to run the
9 # various regression tests.  The rationale for this is that it should be
10 # possible to run this on most major platforms, including those (such as
11 # Windows) which don't support gnu Make.
12 #
13 # The script is driven by a parameter file which defines the various tests
14 # to be run, together with the unique settings for each of these tests.  A
15 # script for Linux is included (regressions.xml), with comments indicating
16 # the significance of the various parameters.  To run the tests under Windows,
17 # edit regressions.xml and remove the comment around the default parameter
18 # "<execpath>" (i.e. make it point to the location of the binary executables).
19 #
20 # Note that this current version requires the Python bindings for libxml2 to
21 # have been previously installed and accessible
22 #
23 # See Copyright for the status of this software.
24 # William Brack (wbrack@mmm.com.hk)
25 #
26 ###
27 defaultParams = {}      # will be used as a dictionary to hold the parsed params
28
29 # This routine is used for comparing the expected stdout / stdin with the results.
30 # The expected data has already been read in; the result is a file descriptor.
31 # Within the two sets of data, lines may begin with a path string.  If so, the
32 # code "relativises" it by removing the path component.  The first argument is a
33 # list already read in by a separate thread; the second is a file descriptor.
34 # The two 'base' arguments are to let me "relativise" the results files, allowing
35 # the script to be run from any directory.
36 def compFiles(res, expected, base1, base2):
37     l1 = len(base1)
38     exp = expected.readlines()
39     expected.close()
40     # the "relativisation" is done here
41     for i in range(len(res)):
42         j = string.find(res[i],base1)
43         if (j == 0) or ((j == 2) and (res[i][0:2] == './')):
44             col = string.find(res[i],':')
45             if col > 0:
46                 start = string.rfind(res[i][:col], '/')
47                 if start > 0:
48                     res[i] = res[i][start+1:]
49
50     for i in range(len(exp)):
51         j = string.find(exp[i],base2)
52         if (j == 0) or ((j == 2) and (exp[i][0:2] == './')):
53             col = string.find(exp[i],':')
54             if col > 0:
55                 start = string.rfind(exp[i][:col], '/')
56                 if start > 0:
57                     exp[i] = exp[i][start+1:]
58
59     ret = 0
60     # ideally we would like to use difflib functions here to do a
61     # nice comparison of the two sets.  Unfortunately, during testing
62     # (using python 2.3.3 and 2.3.4) the following code went into
63     # a dead loop under windows.  I'll pursue this later.
64 #    diff = difflib.ndiff(res, exp)
65 #    diff = list(diff)
66 #    for line in diff:
67 #        if line[:2] != '  ':
68 #            print string.strip(line)
69 #            ret = -1
70
71     # the following simple compare is fine for when the two data sets
72     # (actual result vs. expected result) are equal, which should be true for
73     # us.  Unfortunately, if the test fails it's not nice at all.
74     rl = len(res)
75     el = len(exp)
76     if el != rl:
77         print 'Length of expected is %d, result is %d' % (el, rl)
78         ret = -1
79     for i in range(min(el, rl)):
80         if string.strip(res[i]) != string.strip(exp[i]):
81             print '+:%s-:%s' % (res[i], exp[i])
82             ret = -1
83     if el > rl:
84         for i in range(rl, el):
85             print '-:%s' % exp[i]
86             ret = -1
87     elif rl > el:
88         for i in range (el, rl):
89             print '+:%s' % res[i]
90             ret = -1
91     return ret
92
93 # Separate threads to handle stdout and stderr are created to run this function
94 def readPfile(file, list, flag):
95     data = file.readlines()     # no call by reference, so I cheat
96     for l in data:
97         list.append(l)
98     file.close()
99     flag.append('ok')
100
101 # This routine runs the test program (e.g. xmllint)
102 def runOneTest(testDescription, filename, inbase, errbase):
103     if 'execpath' in testDescription:
104         dir = testDescription['execpath'] + '/'
105     else:
106         dir = ''
107     cmd = os.path.abspath(dir + testDescription['testprog'])
108     if 'flag' in testDescription:
109         for f in string.split(testDescription['flag']):
110             cmd += ' ' + f
111     if 'stdin' not in testDescription:
112         cmd += ' ' + inbase + filename
113     if 'extarg' in testDescription:
114         cmd += ' ' + testDescription['extarg']
115
116     noResult = 0
117     expout = None
118     if 'resext' in testDescription:
119         if testDescription['resext'] == 'None':
120             noResult = 1
121         else:
122             ext = '.' + testDescription['resext']
123     else:
124         ext = ''
125     if not noResult:
126         try:
127             fname = errbase + filename + ext
128             expout = open(fname, 'rt')
129         except:
130             print "Can't open result file %s - bypassing test" % fname
131             return
132
133     noErrors = 0
134     if 'reserrext' in testDescription:
135         if testDescription['reserrext'] == 'None':
136             noErrors = 1
137         else:
138             if len(testDescription['reserrext'])>0:
139                 ext = '.' + testDescription['reserrext']
140             else:
141                 ext = ''
142     else:
143         ext = ''
144     if not noErrors:
145         try:
146             fname = errbase + filename + ext
147             experr = open(fname, 'rt')
148         except:
149             experr = None
150     else:
151         experr = None
152
153     pin, pout, perr = os.popen3(cmd)
154     if 'stdin' in testDescription:
155         infile = open(inbase + filename, 'rt')
156         pin.writelines(infile.readlines())
157         infile.close()
158         pin.close()
159
160     # popen is great fun, but can lead to the old "deadly embrace", because
161     # synchronizing the writing (by the task being run) of stdout and stderr
162     # with respect to the reading (by this task) is basically impossible.  I
163     # tried several ways to cheat, but the only way I have found which works
164     # is to do a *very* elementary multi-threading approach.  We can only hope
165     # that Python threads are implemented on the target system (it's okay for
166     # Linux and Windows)
167
168     th1Flag = []        # flags to show when threads finish
169     th2Flag = []
170     outfile = []        # lists to contain the pipe data
171     errfile = []
172     th1 = thread.start_new_thread(readPfile, (pout, outfile, th1Flag))
173     th2 = thread.start_new_thread(readPfile, (perr, errfile, th2Flag))
174     while (len(th1Flag)==0) or (len(th2Flag)==0):
175         time.sleep(0.001)
176     if not noResult:
177         ret = compFiles(outfile, expout, inbase, 'test/')
178         if ret != 0:
179             print 'trouble with %s' % cmd
180     else:
181         if len(outfile) != 0:
182             for l in outfile:
183                 print l
184             print 'trouble with %s' % cmd
185     if experr != None:
186         ret = compFiles(errfile, experr, inbase, 'test/')
187         if ret != 0:
188             print 'trouble with %s' % cmd
189     else:
190         if not noErrors:
191             if len(errfile) != 0:
192                 for l in errfile:
193                     print l
194                 print 'trouble with %s' % cmd
195
196     if 'stdin' not in testDescription:
197         pin.close()
198
199 # This routine is called by the parameter decoding routine whenever the end of a
200 # 'test' section is encountered.  Depending upon file globbing, a large number of
201 # individual tests may be run.
202 def runTest(description):
203     testDescription = defaultParams.copy()              # set defaults
204     testDescription.update(description)                 # override with current ent
205     if 'testname' in testDescription:
206         print "## %s" % testDescription['testname']
207     if not 'file' in testDescription:
208         print "No file specified - can't run this test!"
209         return
210     # Set up the source and results directory paths from the decoded params
211     dir = ''
212     if 'srcdir' in testDescription:
213         dir += testDescription['srcdir'] + '/'
214     if 'srcsub' in testDescription:
215         dir += testDescription['srcsub'] + '/'
216
217     rdir = ''
218     if 'resdir' in testDescription:
219         rdir += testDescription['resdir'] + '/'
220     if 'ressub' in testDescription:
221         rdir += testDescription['ressub'] + '/'
222
223     testFiles = glob.glob(os.path.abspath(dir + testDescription['file']))
224     if testFiles == []:
225         print "No files result from '%s'" % testDescription['file']
226         return
227
228     # Some test programs just don't work (yet).  For now we exclude them.
229     count = 0
230     excl = []
231     if 'exclfile' in testDescription:
232         for f in string.split(testDescription['exclfile']):
233             glb = glob.glob(dir + f)
234             for g in glb:
235                 excl.append(os.path.abspath(g))
236
237     # Run the specified test program
238     for f in testFiles:
239         if not os.path.isdir(f):
240             if f not in excl:
241                 count = count + 1
242                 runOneTest(testDescription, os.path.basename(f), dir, rdir)
243
244 #
245 # The following classes are used with the xmlreader interface to interpret the
246 # parameter file.  Once a test section has been identified, runTest is called
247 # with a dictionary containing the parsed results of the interpretation.
248 #
249
250 class testDefaults:
251     curText = ''        # accumulates text content of parameter
252
253     def addToDict(self, key):
254         txt = string.strip(self.curText)
255 #        if txt == '':
256 #            return
257         if key not in defaultParams:
258             defaultParams[key] = txt
259         else:
260             defaultParams[key] += ' ' + txt
261         
262     def processNode(self, reader, curClass):
263         if reader.Depth() == 2:
264             if reader.NodeType() == 1:
265                 self.curText = ''       # clear the working variable
266             elif reader.NodeType() == 15:
267                 if (reader.Name() != '#text') and (reader.Name() != '#comment'):
268                     self.addToDict(reader.Name())
269         elif reader.Depth() == 3:
270             if reader.Name() == '#text':
271                 self.curText += reader.Value()
272
273         elif reader.NodeType() == 15:   # end of element
274             print "Defaults have been set to:"
275             for k in defaultParams.keys():
276                 print "   %s : '%s'" % (k, defaultParams[k])
277             curClass = rootClass()
278         return curClass
279
280
281 class testClass:
282     def __init__(self):
283         self.testParams = {}    # start with an empty set of params
284         self.curText = ''       # and empty text
285
286     def addToDict(self, key):
287         data = string.strip(self.curText)
288         if key not in self.testParams:
289             self.testParams[key] = data
290         else:
291             if self.testParams[key] != '':
292                 data = ' ' + data
293             self.testParams[key] += data
294
295     def processNode(self, reader, curClass):
296         if reader.Depth() == 2:
297             if reader.NodeType() == 1:
298                 self.curText = ''       # clear the working variable
299                 if reader.Name() not in self.testParams:
300                     self.testParams[reader.Name()] = ''
301             elif reader.NodeType() == 15:
302                 if (reader.Name() != '#text') and (reader.Name() != '#comment'):
303                     self.addToDict(reader.Name())
304         elif reader.Depth() == 3:
305             if reader.Name() == '#text':
306                 self.curText += reader.Value()
307
308         elif reader.NodeType() == 15:   # end of element
309             runTest(self.testParams)
310             curClass = rootClass()
311         return curClass
312
313
314 class rootClass:
315     def processNode(self, reader, curClass):
316         if reader.Depth() == 0:
317             return curClass
318         if reader.Depth() != 1:
319             print "Unexpected junk: Level %d, type %d, name %s" % (
320                   reader.Depth(), reader.NodeType(), reader.Name())
321             return curClass
322         if reader.Name() == 'test':
323             curClass = testClass()
324             curClass.testParams = {}
325         elif reader.Name() == 'defaults':
326             curClass = testDefaults()
327         return curClass
328
329 def streamFile(filename):
330     try:
331         reader = libxml2.newTextReaderFilename(filename)
332     except:
333         print "unable to open %s" % (filename)
334         return
335
336     curClass = rootClass()
337     ret = reader.Read()
338     while ret == 1:
339         curClass = curClass.processNode(reader, curClass)
340         ret = reader.Read()
341
342     if ret != 0:
343         print "%s : failed to parse" % (filename)
344
345 # OK, we're finished with all the routines.  Now for the main program:-
346 if len(sys.argv) != 2:
347     print "Usage: maketest {filename}"
348     sys.exit(-1)
349
350 streamFile(sys.argv[1])