regressions.py 12 KB

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