| #!/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]) |