aboutsummaryrefslogtreecommitdiffstats
path: root/regressions.py
blob: 0e813c2ea337a1bb7900626ca4997456237b364c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
#!/usr/bin/python -u
import glob, os, string, sys, thread, time
# import difflib
import libxml2

###
#
# This is a "Work in Progress" attempt at a python script to run the
# various regression tests.  The rationale for this is that it should be
# possible to run this on most major platforms, including those (such as
# Windows) which don't support gnu Make.
#
# The script is driven by a parameter file which defines the various tests
# to be run, together with the unique settings for each of these tests.  A
# script for Linux is included (regressions.xml), with comments indicating
# the significance of the various parameters.  To run the tests under Windows,
# edit regressions.xml and remove the comment around the default parameter
# "<execpath>" (i.e. make it point to the location of the binary executables).
#
# Note that this current version requires the Python bindings for libxml2 to
# have been previously installed and accessible
#
# See Copyright for the status of this software.
# William Brack (wbrack@mmm.com.hk)
#
###
defaultParams = {}	# will be used as a dictionary to hold the parsed params

# This routine is used for comparing the expected stdout / stdin with the results.
# The expected data has already been read in; the result is a file descriptor.
# Within the two sets of data, lines may begin with a path string.  If so, the
# code "relativises" it by removing the path component.  The first argument is a
# list already read in by a separate thread; the second is a file descriptor.
# The two 'base' arguments are to let me "relativise" the results files, allowing
# the script to be run from any directory.
def compFiles(res, expected, base1, base2):
    l1 = len(base1)
    exp = expected.readlines()
    expected.close()
    # the "relativisation" is done here
    for i in range(len(res)):
        j = string.find(res[i],base1)
        if (j == 0) or ((j == 2) and (res[i][0:2] == './')):
            col = string.find(res[i],':')
            if col > 0:
                start = string.rfind(res[i][:col], '/')
                if start > 0:
                    res[i] = res[i][start+1:]

    for i in range(len(exp)):
        j = string.find(exp[i],base2)
        if (j == 0) or ((j == 2) and (exp[i][0:2] == './')):
            col = string.find(exp[i],':')
            if col > 0:
                start = string.rfind(exp[i][:col], '/')
                if start > 0:
                    exp[i] = exp[i][start+1:]

    ret = 0
    # ideally we would like to use difflib functions here to do a
    # nice comparison of the two sets.  Unfortunately, during testing
    # (using python 2.3.3 and 2.3.4) the following code went into
    # a dead loop under windows.  I'll pursue this later.
#    diff = difflib.ndiff(res, exp)
#    diff = list(diff)
#    for line in diff:
#        if line[:2] != '  ':
#            print string.strip(line)
#            ret = -1

    # the following simple compare is fine for when the two data sets
    # (actual result vs. expected result) are equal, which should be true for
    # us.  Unfortunately, if the test fails it's not nice at all.
    rl = len(res)
    el = len(exp)
    if el != rl:
        print 'Length of expected is %d, result is %d' % (el, rl)
	ret = -1
    for i in range(min(el, rl)):
        if string.strip(res[i]) != string.strip(exp[i]):
            print '+:%s-:%s' % (res[i], exp[i])
            ret = -1
    if el > rl:
        for i in range(rl, el):
            print '-:%s' % exp[i]
            ret = -1
    elif rl > el:
        for i in range (el, rl):
            print '+:%s' % res[i]
            ret = -1
    return ret

# Separate threads to handle stdout and stderr are created to run this function
def readPfile(file, list, flag):
    data = file.readlines()	# no call by reference, so I cheat
    for l in data:
        list.append(l)
    file.close()
    flag.append('ok')

# This routine runs the test program (e.g. xmllint)
def runOneTest(testDescription, filename, inbase, errbase):
    if 'execpath' in testDescription:
        dir = testDescription['execpath'] + '/'
    else:
        dir = ''
    cmd = os.path.abspath(dir + testDescription['testprog'])
    if 'flag' in testDescription:
        for f in string.split(testDescription['flag']):
            cmd += ' ' + f
    if 'stdin' not in testDescription:
        cmd += ' ' + inbase + filename
    if 'extarg' in testDescription:
        cmd += ' ' + testDescription['extarg']

    noResult = 0
    expout = None
    if 'resext' in testDescription:
        if testDescription['resext'] == 'None':
            noResult = 1
        else:
            ext = '.' + testDescription['resext']
    else:
        ext = ''
    if not noResult:
        try:
            fname = errbase + filename + ext
            expout = open(fname, 'rt')
        except:
            print "Can't open result file %s - bypassing test" % fname
            return

    noErrors = 0
    if 'reserrext' in testDescription:
        if testDescription['reserrext'] == 'None':
            noErrors = 1
        else:
            if len(testDescription['reserrext'])>0:
                ext = '.' + testDescription['reserrext']
            else:
                ext = ''
    else:
        ext = ''
    if not noErrors:
        try:
            fname = errbase + filename + ext
            experr = open(fname, 'rt')
        except:
            experr = None
    else:
        experr = None

    pin, pout, perr = os.popen3(cmd)
    if 'stdin' in testDescription:
        infile = open(inbase + filename, 'rt')
        pin.writelines(infile.readlines())
        infile.close()
        pin.close()

    # popen is great fun, but can lead to the old "deadly embrace", because
    # synchronizing the writing (by the task being run) of stdout and stderr
    # with respect to the reading (by this task) is basically impossible.  I
    # tried several ways to cheat, but the only way I have found which works
    # is to do a *very* elementary multi-threading approach.  We can only hope
    # that Python threads are implemented on the target system (it's okay for
    # Linux and Windows)

    th1Flag = []	# flags to show when threads finish
    th2Flag = []
    outfile = []	# lists to contain the pipe data
    errfile = []
    th1 = thread.start_new_thread(readPfile, (pout, outfile, th1Flag))
    th2 = thread.start_new_thread(readPfile, (perr, errfile, th2Flag))
    while (len(th1Flag)==0) or (len(th2Flag)==0):
        time.sleep(0.001)
    if not noResult:
        ret = compFiles(outfile, expout, inbase, 'test/')
        if ret != 0:
            print 'trouble with %s' % cmd
    else:
        if len(outfile) != 0:
            for l in outfile:
                print l
            print 'trouble with %s' % cmd
    if experr != None:
        ret = compFiles(errfile, experr, inbase, 'test/')
        if ret != 0:
            print 'trouble with %s' % cmd
    else:
        if not noErrors:
            if len(errfile) != 0:
                for l in errfile:
                    print l
                print 'trouble with %s' % cmd

    if 'stdin' not in testDescription:
        pin.close()

# This routine is called by the parameter decoding routine whenever the end of a
# 'test' section is encountered.  Depending upon file globbing, a large number of
# individual tests may be run.
def runTest(description):
    testDescription = defaultParams.copy()		# set defaults
    testDescription.update(description)			# override with current ent
    if 'testname' in testDescription:
        print "## %s" % testDescription['testname']
    if not 'file' in testDescription:
        print "No file specified - can't run this test!"
        return
    # Set up the source and results directory paths from the decoded params
    dir = ''
    if 'srcdir' in testDescription:
        dir += testDescription['srcdir'] + '/'
    if 'srcsub' in testDescription:
        dir += testDescription['srcsub'] + '/'

    rdir = ''
    if 'resdir' in testDescription:
        rdir += testDescription['resdir'] + '/'
    if 'ressub' in testDescription:
        rdir += testDescription['ressub'] + '/'

    testFiles = glob.glob(os.path.abspath(dir + testDescription['file']))
    if testFiles == []:
        print "No files result from '%s'" % testDescription['file']
        return

    # Some test programs just don't work (yet).  For now we exclude them.
    count = 0
    excl = []
    if 'exclfile' in testDescription:
        for f in string.split(testDescription['exclfile']):
            glb = glob.glob(dir + f)
            for g in glb:
                excl.append(os.path.abspath(g))

    # Run the specified test program
    for f in testFiles:
        if not os.path.isdir(f):
            if f not in excl:
                count = count + 1
                runOneTest(testDescription, os.path.basename(f), dir, rdir)

#
# The following classes are used with the xmlreader interface to interpret the
# parameter file.  Once a test section has been identified, runTest is called
# with a dictionary containing the parsed results of the interpretation.
#

class testDefaults:
    curText = ''	# accumulates text content of parameter

    def addToDict(self, key):
        txt = string.strip(self.curText)
#        if txt == '':
#            return
        if key not in defaultParams:
            defaultParams[key] = txt
        else:
            defaultParams[key] += ' ' + txt
        
    def processNode(self, reader, curClass):
        if reader.Depth() == 2:
            if reader.NodeType() == 1:
                self.curText = ''	# clear the working variable
            elif reader.NodeType() == 15:
                if (reader.Name() != '#text') and (reader.Name() != '#comment'):
                    self.addToDict(reader.Name())
        elif reader.Depth() == 3:
            if reader.Name() == '#text':
                self.curText += reader.Value()

        elif reader.NodeType() == 15:	# end of element
            print "Defaults have been set to:"
            for k in defaultParams.keys():
                print "   %s : '%s'" % (k, defaultParams[k])
            curClass = rootClass()
        return curClass


class testClass:
    def __init__(self):
        self.testParams = {}	# start with an empty set of params
        self.curText = ''	# and empty text

    def addToDict(self, key):
        data = string.strip(self.curText)
        if key not in self.testParams:
            self.testParams[key] = data
        else:
            if self.testParams[key] != '':
                data = ' ' + data
            self.testParams[key] += data

    def processNode(self, reader, curClass):
        if reader.Depth() == 2:
            if reader.NodeType() == 1:
                self.curText = ''	# clear the working variable
                if reader.Name() not in self.testParams:
                    self.testParams[reader.Name()] = ''
            elif reader.NodeType() == 15:
                if (reader.Name() != '#text') and (reader.Name() != '#comment'):
                    self.addToDict(reader.Name())
        elif reader.Depth() == 3:
            if reader.Name() == '#text':
                self.curText += reader.Value()

        elif reader.NodeType() == 15:	# end of element
            runTest(self.testParams)
            curClass = rootClass()
        return curClass


class rootClass:
    def processNode(self, reader, curClass):
        if reader.Depth() == 0:
            return curClass
        if reader.Depth() != 1:
            print "Unexpected junk: Level %d, type %d, name %s" % (
                  reader.Depth(), reader.NodeType(), reader.Name())
            return curClass
        if reader.Name() == 'test':
            curClass = testClass()
            curClass.testParams = {}
        elif reader.Name() == 'defaults':
            curClass = testDefaults()
        return curClass

def streamFile(filename):
    try:
        reader = libxml2.newTextReaderFilename(filename)
    except:
        print "unable to open %s" % (filename)
        return

    curClass = rootClass()
    ret = reader.Read()
    while ret == 1:
        curClass = curClass.processNode(reader, curClass)
        ret = reader.Read()

    if ret != 0:
        print "%s : failed to parse" % (filename)

# OK, we're finished with all the routines.  Now for the main program:-
if len(sys.argv) != 2:
    print "Usage: maketest {filename}"
    sys.exit(-1)

streamFile(sys.argv[1])