William M. Brack | 3403add | 2004-06-27 02:07:51 +0000 | [diff] [blame] | 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]) |