MultiTestRunner.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. #!/usr/bin/env python
  2. """
  3. MultiTestRunner - Harness for running multiple tests in the simple clang style.
  4. TODO
  5. --
  6. - Fix Ctrl-c issues
  7. - Use a timeout
  8. - Detect signalled failures (abort)
  9. - Better support for finding tests
  10. """
  11. # TOD
  12. import os, sys, re, random, time
  13. import threading
  14. import ProgressBar
  15. import TestRunner
  16. from TestRunner import TestStatus
  17. from Queue import Queue
  18. kTestFileExtensions = set(['.mi','.i','.c','.cpp','.m','.mm','.ll'])
  19. def getTests(inputs):
  20. for path in inputs:
  21. if not os.path.exists(path):
  22. print >>sys.stderr,"WARNING: Invalid test \"%s\""%(path,)
  23. continue
  24. if os.path.isdir(path):
  25. for dirpath,dirnames,filenames in os.walk(path):
  26. dotTests = os.path.join(dirpath,'.tests')
  27. if os.path.exists(dotTests):
  28. for ln in open(dotTests):
  29. if ln.strip():
  30. yield os.path.join(dirpath,ln.strip())
  31. else:
  32. # FIXME: This doesn't belong here
  33. if 'Output' in dirnames:
  34. dirnames.remove('Output')
  35. for f in filenames:
  36. base,ext = os.path.splitext(f)
  37. if ext in kTestFileExtensions:
  38. yield os.path.join(dirpath,f)
  39. else:
  40. yield path
  41. class TestingProgressDisplay:
  42. def __init__(self, opts, numTests, progressBar=None):
  43. self.opts = opts
  44. self.numTests = numTests
  45. self.digits = len(str(self.numTests))
  46. self.current = None
  47. self.lock = threading.Lock()
  48. self.progressBar = progressBar
  49. self.progress = 0.
  50. def update(self, index, tr):
  51. # Avoid locking overhead in quiet mode
  52. if self.opts.quiet and not tr.failed():
  53. return
  54. # Output lock
  55. self.lock.acquire()
  56. try:
  57. self.handleUpdate(index, tr)
  58. finally:
  59. self.lock.release()
  60. def finish(self):
  61. if self.progressBar:
  62. self.progressBar.clear()
  63. elif self.opts.succinct:
  64. sys.stdout.write('\n')
  65. def handleUpdate(self, index, tr):
  66. if self.progressBar:
  67. if tr.failed():
  68. self.progressBar.clear()
  69. else:
  70. # Force monotonicity
  71. self.progress = max(self.progress, float(index)/self.numTests)
  72. self.progressBar.update(self.progress, tr.path)
  73. return
  74. elif self.opts.succinct:
  75. if not tr.failed():
  76. sys.stdout.write('.')
  77. sys.stdout.flush()
  78. return
  79. else:
  80. sys.stdout.write('\n')
  81. extra = ''
  82. if tr.code==TestStatus.Invalid:
  83. extra = ' - (Invalid test)'
  84. elif tr.code==TestStatus.NoRunLine:
  85. extra = ' - (No RUN line)'
  86. elif tr.failed():
  87. extra = ' - %s'%(TestStatus.getName(tr.code).upper(),)
  88. print '%*d/%*d - %s%s'%(self.digits, index+1, self.digits,
  89. self.numTests, tr.path, extra)
  90. if tr.failed() and self.opts.showOutput:
  91. TestRunner.cat(tr.testResults, sys.stdout)
  92. class TestResult:
  93. def __init__(self, path, code, testResults):
  94. self.path = path
  95. self.code = code
  96. self.testResults = testResults
  97. def failed(self):
  98. return self.code in (TestStatus.Fail,TestStatus.XPass)
  99. class TestProvider:
  100. def __init__(self, opts, tests, display):
  101. self.opts = opts
  102. self.tests = tests
  103. self.index = 0
  104. self.lock = threading.Lock()
  105. self.results = [None]*len(self.tests)
  106. self.startTime = time.time()
  107. self.progress = display
  108. def get(self):
  109. self.lock.acquire()
  110. try:
  111. if self.opts.maxTime is not None:
  112. if time.time() - self.startTime > self.opts.maxTime:
  113. return None
  114. if self.index >= len(self.tests):
  115. return None
  116. item = self.tests[self.index],self.index
  117. self.index += 1
  118. return item
  119. finally:
  120. self.lock.release()
  121. def setResult(self, index, result):
  122. self.results[index] = result
  123. self.progress.update(index, result)
  124. class Tester(threading.Thread):
  125. def __init__(self, provider):
  126. threading.Thread.__init__(self)
  127. self.provider = provider
  128. def run(self):
  129. while 1:
  130. item = self.provider.get()
  131. if item is None:
  132. break
  133. self.runTest(item)
  134. def runTest(self, (path,index)):
  135. command = path
  136. # Use hand concatentation here because we want to override
  137. # absolute paths.
  138. output = 'Output/' + path + '.out'
  139. testname = path
  140. testresults = 'Output/' + path + '.testresults'
  141. TestRunner.mkdir_p(os.path.dirname(testresults))
  142. numTests = len(self.provider.tests)
  143. digits = len(str(numTests))
  144. code = None
  145. try:
  146. opts = self.provider.opts
  147. if opts.debugDoNotTest:
  148. code = None
  149. else:
  150. code = TestRunner.runOneTest(path, command, output, testname,
  151. opts.clang, opts.clangcc,
  152. useValgrind=opts.useValgrind,
  153. useDGCompat=opts.useDGCompat,
  154. useScript=opts.testScript,
  155. output=open(testresults,'w'))
  156. except KeyboardInterrupt:
  157. # This is a sad hack. Unfortunately subprocess goes
  158. # bonkers with ctrl-c and we start forking merrily.
  159. print 'Ctrl-C detected, goodbye.'
  160. os.kill(0,9)
  161. self.provider.setResult(index, TestResult(path, code, testresults))
  162. def detectCPUs():
  163. """
  164. Detects the number of CPUs on a system. Cribbed from pp.
  165. """
  166. # Linux, Unix and MacOS:
  167. if hasattr(os, "sysconf"):
  168. if os.sysconf_names.has_key("SC_NPROCESSORS_ONLN"):
  169. # Linux & Unix:
  170. ncpus = os.sysconf("SC_NPROCESSORS_ONLN")
  171. if isinstance(ncpus, int) and ncpus > 0:
  172. return ncpus
  173. else: # OSX:
  174. return int(os.popen2("sysctl -n hw.ncpu")[1].read())
  175. # Windows:
  176. if os.environ.has_key("NUMBER_OF_PROCESSORS"):
  177. ncpus = int(os.environ["NUMBER_OF_PROCESSORS"]);
  178. if ncpus > 0:
  179. return ncpus
  180. return 1 # Default
  181. def main():
  182. global options
  183. from optparse import OptionParser
  184. parser = OptionParser("usage: %prog [options] {inputs}")
  185. parser.add_option("-j", "--threads", dest="numThreads",
  186. help="Number of testing threads",
  187. type=int, action="store",
  188. default=detectCPUs())
  189. parser.add_option("", "--clang", dest="clang",
  190. help="Program to use as \"clang\"",
  191. action="store", default=None)
  192. parser.add_option("", "--clang-cc", dest="clangcc",
  193. help="Program to use as \"clang-cc\"",
  194. action="store", default=None)
  195. parser.add_option("", "--vg", dest="useValgrind",
  196. help="Run tests under valgrind",
  197. action="store_true", default=False)
  198. parser.add_option("", "--dg", dest="useDGCompat",
  199. help="Use llvm dejagnu compatibility mode",
  200. action="store_true", default=False)
  201. parser.add_option("", "--script", dest="testScript",
  202. help="Default script to use",
  203. action="store", default=None)
  204. parser.add_option("-v", "--verbose", dest="showOutput",
  205. help="Show all test output",
  206. action="store_true", default=False)
  207. parser.add_option("-q", "--quiet", dest="quiet",
  208. help="Suppress no error output",
  209. action="store_true", default=False)
  210. parser.add_option("-s", "--succinct", dest="succinct",
  211. help="Reduce amount of output",
  212. action="store_true", default=False)
  213. parser.add_option("", "--max-tests", dest="maxTests",
  214. help="Maximum number of tests to run",
  215. action="store", type=int, default=None)
  216. parser.add_option("", "--max-time", dest="maxTime",
  217. help="Maximum time to spend testing (in seconds)",
  218. action="store", type=float, default=None)
  219. parser.add_option("", "--shuffle", dest="shuffle",
  220. help="Run tests in random order",
  221. action="store_true", default=False)
  222. parser.add_option("", "--seed", dest="seed",
  223. help="Seed for random number generator (default: random)",
  224. action="store", default=None)
  225. parser.add_option("", "--no-progress-bar", dest="useProgressBar",
  226. help="Do not use curses based progress bar",
  227. action="store_false", default=True)
  228. parser.add_option("", "--debug-do-not-test", dest="debugDoNotTest",
  229. help="DEBUG: Skip running actual test script",
  230. action="store_true", default=False)
  231. parser.add_option("", "--path", dest="path",
  232. help="Additional paths to add to testing environment",
  233. action="store", type=str, default=None)
  234. (opts, args) = parser.parse_args()
  235. if not args:
  236. parser.error('No inputs specified')
  237. if opts.clang is None:
  238. opts.clang = TestRunner.inferClang()
  239. if opts.clangcc is None:
  240. opts.clangcc = TestRunner.inferClangCC(opts.clang)
  241. # FIXME: It could be worth loading these in parallel with testing.
  242. allTests = list(getTests(args))
  243. allTests.sort()
  244. tests = allTests
  245. if opts.seed is not None:
  246. try:
  247. seed = int(opts.seed)
  248. except:
  249. parser.error('--seed argument should be an integer')
  250. random.seed(seed)
  251. if opts.shuffle:
  252. random.shuffle(tests)
  253. if opts.maxTests is not None:
  254. tests = tests[:opts.maxTests]
  255. if opts.path is not None:
  256. os.environ["PATH"] = opts.path + ":" + os.environ["PATH"];
  257. extra = ''
  258. if len(tests) != len(allTests):
  259. extra = ' of %d'%(len(allTests),)
  260. header = '-- Testing: %d%s tests, %d threads --'%(len(tests),extra,
  261. opts.numThreads)
  262. progressBar = None
  263. if not opts.quiet:
  264. if opts.useProgressBar:
  265. try:
  266. tc = ProgressBar.TerminalController()
  267. progressBar = ProgressBar.ProgressBar(tc, header)
  268. except ValueError:
  269. pass
  270. if not progressBar:
  271. print header
  272. display = TestingProgressDisplay(opts, len(tests), progressBar)
  273. provider = TestProvider(opts, tests, display)
  274. testers = [Tester(provider) for i in range(opts.numThreads)]
  275. startTime = time.time()
  276. for t in testers:
  277. t.start()
  278. try:
  279. for t in testers:
  280. t.join()
  281. except KeyboardInterrupt:
  282. sys.exit(1)
  283. display.finish()
  284. if not opts.quiet:
  285. print 'Testing Time: %.2fs'%(time.time() - startTime)
  286. # List test results organized organized by kind.
  287. byCode = {}
  288. for t in provider.results:
  289. if t:
  290. if t.code not in byCode:
  291. byCode[t.code] = []
  292. byCode[t.code].append(t)
  293. for title,code in (('Expected Failures', TestStatus.XFail),
  294. ('Unexpected Passing Tests', TestStatus.XPass),
  295. ('Failing Tests', TestStatus.Fail)):
  296. elts = byCode.get(code)
  297. if not elts:
  298. continue
  299. print '*'*20
  300. print '%s (%d):' % (title, len(elts))
  301. for tr in elts:
  302. print '\t%s'%(tr.path,)
  303. numFailures = len(byCode.get(TestStatus.Fail,[]))
  304. if numFailures:
  305. print '\nFailures: %d' % (numFailures,)
  306. sys.exit(1)
  307. if __name__=='__main__':
  308. main()