зеркало из https://github.com/microsoft/clang-1.git
398 строки
14 KiB
Python
Executable File
398 строки
14 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
"""
|
|
MultiTestRunner - Harness for running multiple tests in the simple clang style.
|
|
|
|
TODO
|
|
--
|
|
- Fix Ctrl-c issues
|
|
- Use a timeout
|
|
- Detect signalled failures (abort)
|
|
- Better support for finding tests
|
|
"""
|
|
|
|
# TOD
|
|
import os, sys, re, random, time
|
|
import threading
|
|
import ProgressBar
|
|
import TestRunner
|
|
from TestRunner import TestStatus
|
|
from Queue import Queue
|
|
|
|
kTestFileExtensions = set(['.mi','.i','.c','.cpp','.m','.mm','.ll'])
|
|
|
|
kClangErrorRE = re.compile('(.*):([0-9]+):([0-9]+): error: (.*)')
|
|
kClangWarningRE = re.compile('(.*):([0-9]+):([0-9]+): warning: (.*)')
|
|
kAssertionRE = re.compile('Assertion failed: (.*, function .*, file .*, line [0-9]+\\.)')
|
|
|
|
def getTests(inputs):
|
|
for path in inputs:
|
|
if not os.path.exists(path):
|
|
print >>sys.stderr,"WARNING: Invalid test \"%s\""%(path,)
|
|
continue
|
|
|
|
if os.path.isdir(path):
|
|
for dirpath,dirnames,filenames in os.walk(path):
|
|
dotTests = os.path.join(dirpath,'.tests')
|
|
if os.path.exists(dotTests):
|
|
for ln in open(dotTests):
|
|
if ln.strip():
|
|
yield os.path.join(dirpath,ln.strip())
|
|
else:
|
|
# FIXME: This doesn't belong here
|
|
if 'Output' in dirnames:
|
|
dirnames.remove('Output')
|
|
for f in filenames:
|
|
base,ext = os.path.splitext(f)
|
|
if ext in kTestFileExtensions:
|
|
yield os.path.join(dirpath,f)
|
|
else:
|
|
yield path
|
|
|
|
class TestingProgressDisplay:
|
|
def __init__(self, opts, numTests, progressBar=None):
|
|
self.opts = opts
|
|
self.numTests = numTests
|
|
self.digits = len(str(self.numTests))
|
|
self.current = None
|
|
self.lock = threading.Lock()
|
|
self.progressBar = progressBar
|
|
self.progress = 0.
|
|
|
|
def update(self, index, tr):
|
|
# Avoid locking overhead in quiet mode
|
|
if self.opts.quiet and not tr.failed():
|
|
return
|
|
|
|
# Output lock
|
|
self.lock.acquire()
|
|
try:
|
|
self.handleUpdate(index, tr)
|
|
finally:
|
|
self.lock.release()
|
|
|
|
def finish(self):
|
|
if self.progressBar:
|
|
self.progressBar.clear()
|
|
elif self.opts.succinct:
|
|
sys.stdout.write('\n')
|
|
|
|
def handleUpdate(self, index, tr):
|
|
if self.progressBar:
|
|
if tr.failed():
|
|
self.progressBar.clear()
|
|
else:
|
|
# Force monotonicity
|
|
self.progress = max(self.progress, float(index)/self.numTests)
|
|
self.progressBar.update(self.progress, tr.path)
|
|
return
|
|
elif self.opts.succinct:
|
|
if not tr.failed():
|
|
sys.stdout.write('.')
|
|
sys.stdout.flush()
|
|
return
|
|
else:
|
|
sys.stdout.write('\n')
|
|
|
|
extra = ''
|
|
if tr.code==TestStatus.Invalid:
|
|
extra = ' - (Invalid test)'
|
|
elif tr.code==TestStatus.NoRunLine:
|
|
extra = ' - (No RUN line)'
|
|
elif tr.failed():
|
|
extra = ' - %s'%(TestStatus.getName(tr.code).upper(),)
|
|
print '%*d/%*d - %s%s'%(self.digits, index+1, self.digits,
|
|
self.numTests, tr.path, extra)
|
|
|
|
if tr.failed():
|
|
msgs = []
|
|
if tr.warnings:
|
|
msgs.append('%d warnings'%(len(tr.warnings),))
|
|
if tr.errors:
|
|
msgs.append('%d errors'%(len(tr.errors),))
|
|
if tr.assertions:
|
|
msgs.append('%d assertions'%(len(tr.assertions),))
|
|
|
|
if msgs:
|
|
print '\tFAIL (%s)'%(', '.join(msgs))
|
|
for i,error in enumerate(set([e for (_,_,_,e) in tr.errors])):
|
|
print '\t\tERROR: %s'%(error,)
|
|
if i>20:
|
|
print '\t\t\t(too many errors, skipping)'
|
|
break
|
|
for assertion in set(tr.assertions):
|
|
print '\t\tASSERTION: %s'%(assertion,)
|
|
if self.opts.showOutput:
|
|
TestRunner.cat(tr.testResults, sys.stdout)
|
|
|
|
class TestResult:
|
|
def __init__(self, path, code, testResults):
|
|
self.path = path
|
|
self.code = code
|
|
self.testResults = testResults
|
|
self.warnings = []
|
|
self.errors = []
|
|
self.assertions = []
|
|
|
|
if self.failed():
|
|
f = open(self.testResults)
|
|
data = f.read()
|
|
f.close()
|
|
self.warnings = [m.groups() for m in kClangWarningRE.finditer(data)]
|
|
self.errors = [m.groups() for m in kClangErrorRE.finditer(data)]
|
|
self.assertions = [m.group(1) for m in kAssertionRE.finditer(data)]
|
|
|
|
def failed(self):
|
|
return self.code in (TestStatus.Fail,TestStatus.XPass)
|
|
|
|
class TestProvider:
|
|
def __init__(self, opts, tests, display):
|
|
self.opts = opts
|
|
self.tests = tests
|
|
self.index = 0
|
|
self.lock = threading.Lock()
|
|
self.results = [None]*len(self.tests)
|
|
self.startTime = time.time()
|
|
self.progress = display
|
|
|
|
def get(self):
|
|
self.lock.acquire()
|
|
try:
|
|
if self.opts.maxTime is not None:
|
|
if time.time() - self.startTime > self.opts.maxTime:
|
|
return None
|
|
if self.index >= len(self.tests):
|
|
return None
|
|
item = self.tests[self.index],self.index
|
|
self.index += 1
|
|
return item
|
|
finally:
|
|
self.lock.release()
|
|
|
|
def setResult(self, index, result):
|
|
self.results[index] = result
|
|
self.progress.update(index, result)
|
|
|
|
class Tester(threading.Thread):
|
|
def __init__(self, provider):
|
|
threading.Thread.__init__(self)
|
|
self.provider = provider
|
|
|
|
def run(self):
|
|
while 1:
|
|
item = self.provider.get()
|
|
if item is None:
|
|
break
|
|
self.runTest(item)
|
|
|
|
def runTest(self, (path,index)):
|
|
command = path
|
|
# Use hand concatentation here because we want to override
|
|
# absolute paths.
|
|
output = 'Output/' + path + '.out'
|
|
testname = path
|
|
testresults = 'Output/' + path + '.testresults'
|
|
TestRunner.mkdir_p(os.path.dirname(testresults))
|
|
numTests = len(self.provider.tests)
|
|
digits = len(str(numTests))
|
|
code = None
|
|
try:
|
|
opts = self.provider.opts
|
|
if opts.debugDoNotTest:
|
|
code = None
|
|
else:
|
|
code = TestRunner.runOneTest(path, command, output, testname,
|
|
opts.clang,
|
|
useValgrind=opts.useValgrind,
|
|
useDGCompat=opts.useDGCompat,
|
|
useScript=opts.testScript,
|
|
output=open(testresults,'w'))
|
|
except KeyboardInterrupt:
|
|
# This is a sad hack. Unfortunately subprocess goes
|
|
# bonkers with ctrl-c and we start forking merrily.
|
|
print 'Ctrl-C detected, goodbye.'
|
|
os.kill(0,9)
|
|
|
|
self.provider.setResult(index, TestResult(path, code, testresults))
|
|
|
|
def detectCPUs():
|
|
"""
|
|
Detects the number of CPUs on a system. Cribbed from pp.
|
|
"""
|
|
# Linux, Unix and MacOS:
|
|
if hasattr(os, "sysconf"):
|
|
if os.sysconf_names.has_key("SC_NPROCESSORS_ONLN"):
|
|
# Linux & Unix:
|
|
ncpus = os.sysconf("SC_NPROCESSORS_ONLN")
|
|
if isinstance(ncpus, int) and ncpus > 0:
|
|
return ncpus
|
|
else: # OSX:
|
|
return int(os.popen2("sysctl -n hw.ncpu")[1].read())
|
|
# Windows:
|
|
if os.environ.has_key("NUMBER_OF_PROCESSORS"):
|
|
ncpus = int(os.environ["NUMBER_OF_PROCESSORS"]);
|
|
if ncpus > 0:
|
|
return ncpus
|
|
return 1 # Default
|
|
|
|
def main():
|
|
global options
|
|
from optparse import OptionParser
|
|
parser = OptionParser("usage: %prog [options] {inputs}")
|
|
parser.add_option("-j", "--threads", dest="numThreads",
|
|
help="Number of testing threads",
|
|
type=int, action="store",
|
|
default=detectCPUs())
|
|
parser.add_option("", "--clang", dest="clang",
|
|
help="Program to use as \"clang\"",
|
|
action="store", default="clang")
|
|
parser.add_option("", "--vg", dest="useValgrind",
|
|
help="Run tests under valgrind",
|
|
action="store_true", default=False)
|
|
parser.add_option("", "--dg", dest="useDGCompat",
|
|
help="Use llvm dejagnu compatibility mode",
|
|
action="store_true", default=False)
|
|
parser.add_option("", "--script", dest="testScript",
|
|
help="Default script to use",
|
|
action="store", default=None)
|
|
parser.add_option("-v", "--verbose", dest="showOutput",
|
|
help="Show all test output",
|
|
action="store_true", default=False)
|
|
parser.add_option("-q", "--quiet", dest="quiet",
|
|
help="Suppress no error output",
|
|
action="store_true", default=False)
|
|
parser.add_option("-s", "--succinct", dest="succinct",
|
|
help="Reduce amount of output",
|
|
action="store_true", default=False)
|
|
parser.add_option("", "--max-tests", dest="maxTests",
|
|
help="Maximum number of tests to run",
|
|
action="store", type=int, default=None)
|
|
parser.add_option("", "--max-time", dest="maxTime",
|
|
help="Maximum time to spend testing (in seconds)",
|
|
action="store", type=float, default=None)
|
|
parser.add_option("", "--shuffle", dest="shuffle",
|
|
help="Run tests in random order",
|
|
action="store_true", default=False)
|
|
parser.add_option("", "--seed", dest="seed",
|
|
help="Seed for random number generator (default: random).",
|
|
action="store", default=None)
|
|
parser.add_option("", "--no-progress-bar", dest="useProgressBar",
|
|
help="Do not use curses based progress bar",
|
|
action="store_false", default=True)
|
|
parser.add_option("", "--debug-do-not-test", dest="debugDoNotTest",
|
|
help="DEBUG: Skip running actual test script",
|
|
action="store_true", default=False)
|
|
(opts, args) = parser.parse_args()
|
|
|
|
if not args:
|
|
parser.error('No inputs specified')
|
|
|
|
allTests = list(getTests(args))
|
|
allTests.sort()
|
|
|
|
tests = allTests
|
|
if opts.seed is not None:
|
|
try:
|
|
seed = int(opts.seed)
|
|
except:
|
|
parser.error('--seed argument should be an integer')
|
|
random.seed(seed)
|
|
if opts.shuffle:
|
|
random.shuffle(tests)
|
|
if opts.maxTests is not None:
|
|
tests = tests[:opts.maxTests]
|
|
|
|
extra = ''
|
|
if len(tests) != len(allTests):
|
|
extra = ' of %d'%(len(allTests),)
|
|
header = '-- Testing: %d%s tests, %d threads --'%(len(tests),extra,opts.numThreads)
|
|
|
|
progressBar = None
|
|
if not opts.quiet:
|
|
if opts.useProgressBar:
|
|
try:
|
|
tc = ProgressBar.TerminalController()
|
|
progressBar = ProgressBar.ProgressBar(tc, header)
|
|
except ValueError:
|
|
pass
|
|
|
|
if not progressBar:
|
|
print header
|
|
|
|
display = TestingProgressDisplay(opts, len(tests), progressBar)
|
|
provider = TestProvider(opts, tests, display)
|
|
|
|
testers = [Tester(provider) for i in range(opts.numThreads)]
|
|
startTime = time.time()
|
|
for t in testers:
|
|
t.start()
|
|
try:
|
|
for t in testers:
|
|
t.join()
|
|
except KeyboardInterrupt:
|
|
sys.exit(1)
|
|
|
|
display.finish()
|
|
|
|
if not opts.quiet:
|
|
print 'Testing Time: %.2fs'%(time.time() - startTime)
|
|
|
|
xfails = [i for i in provider.results if i and i.code==TestStatus.XFail]
|
|
if xfails:
|
|
print '*'*20
|
|
print 'Expected Failures (%d):' % len(xfails)
|
|
for tr in xfails:
|
|
print '\t%s'%(tr.path,)
|
|
|
|
xpasses = [i for i in provider.results if i and i.code==TestStatus.XPass]
|
|
if xpasses:
|
|
print '*'*20
|
|
print 'Unexpected Passing Tests (%d):' % len(xpasses)
|
|
for tr in xpasses:
|
|
print '\t%s'%(tr.path,)
|
|
|
|
failures = [i for i in provider.results if i and i.code==TestStatus.Fail]
|
|
if failures:
|
|
print '*'*20
|
|
print 'Failing Tests (%d):' % len(failures)
|
|
for tr in failures:
|
|
if tr.code != TestStatus.XPass:
|
|
print '\t%s'%(tr.path,)
|
|
|
|
print '\nFailures: %d'%(len(failures),)
|
|
|
|
assertions = {}
|
|
errors = {}
|
|
errorFree = []
|
|
for tr in failures:
|
|
if not tr.errors and not tr.assertions:
|
|
errorFree.append(tr)
|
|
for (_,_,_,error) in tr.errors:
|
|
errors[error] = errors.get(error,0) + 1
|
|
for assertion in tr.assertions:
|
|
assertions[assertion] = assertions.get(assertion,0) + 1
|
|
if errorFree:
|
|
print 'Failures w/o Errors (%d):' % len(errorFree)
|
|
for tr in errorFree:
|
|
print '\t%s'%(tr.path,)
|
|
|
|
if errors:
|
|
print 'Error Summary (%d):' % sum(errors.values())
|
|
items = errors.items()
|
|
items.sort(key = lambda (_,v): -v)
|
|
for i,(error,count) in enumerate(items):
|
|
print '\t%3d: %s'%(count,error)
|
|
if i>100:
|
|
print '\t\t(too many errors, skipping)'
|
|
break
|
|
|
|
if assertions:
|
|
print 'Assertion Summary (%d):' % sum(assertions.values())
|
|
items = assertions.items()
|
|
items.sort(key = lambda (_,v): -v)
|
|
for assertion,count in items:
|
|
print '\t%3d: %s'%(count,assertion)
|
|
|
|
if __name__=='__main__':
|
|
main()
|