#!/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 != ' ': # 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)