#!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # run-tests.py -- Python harness for GDB SpiderMonkey support import os, re, subprocess, sys, traceback from threading import Thread # From this directory: import progressbar from taskpool import TaskPool, get_cpu_count # Backported from Python 3.1 posixpath.py def _relpath(path, start=None): """Return a relative version of a path""" if not path: raise ValueError("no path specified") if start is None: start = os.curdir start_list = os.path.abspath(start).split(os.sep) path_list = os.path.abspath(path).split(os.sep) # Work out how much of the filepath is shared by start and path. i = len(os.path.commonprefix([start_list, path_list])) rel_list = [os.pardir] * (len(start_list)-i) + path_list[i:] if not rel_list: return os.curdir return os.path.join(*rel_list) os.path.relpath = _relpath # Characters that need to be escaped when used in shell words. shell_need_escapes = re.compile('[^\w\d%+,-./:=@\'"]', re.DOTALL) # Characters that need to be escaped within double-quoted strings. shell_dquote_escapes = re.compile('[^\w\d%+,-./:=@"]', re.DOTALL) def make_shell_cmd(l): def quote(s): if shell_need_escapes.search(s): if s.find("'") < 0: return "'" + s + "'" return '"' + shell_dquote_escapes.sub('\\g<0>', s) + '"' return s return ' '.join([quote(_) for _ in l]) # An instance of this class collects the lists of passing, failing, and # timing-out tests, runs the progress bar, and prints a summary at the end. class Summary(object): class SummaryBar(progressbar.ProgressBar): def __init__(self, limit): super(Summary.SummaryBar, self).__init__('', limit, 24) def start(self): self.label = '[starting ]' self.update(0) def counts(self, run, failures, timeouts): self.label = '[%4d|%4d|%4d|%4d]' % (run - failures, failures, timeouts, run) self.update(run) def __init__(self, num_tests): self.run = 0 self.failures = [] # kind of judgemental; "unexpecteds"? self.timeouts = [] if not OPTIONS.hide_progress: self.bar = Summary.SummaryBar(num_tests) # Progress bar control. def start(self): if not OPTIONS.hide_progress: self.bar.start() def update(self): if not OPTIONS.hide_progress: self.bar.counts(self.run, len(self.failures), len(self.timeouts)) # Call 'thunk' to show some output, while getting the progress bar out of the way. def interleave_output(self, thunk): if not OPTIONS.hide_progress: self.bar.clear() thunk() self.update() def passed(self, test): self.run += 1 self.update() def failed(self, test): self.run += 1 self.failures.append(test) self.update() def timeout(self, test): self.run += 1 self.timeouts.append(test) self.update() def finish(self): if not OPTIONS.hide_progress: self.bar.finish() if self.failures: print "tests failed:" for test in self.failures: test.show(sys.stdout) if OPTIONS.worklist: try: with open(OPTIONS.worklist) as out: for test in self.failures: out.write(test.name + '\n') except IOError as err: sys.stderr.write("Error writing worklist file '%s': %s" % (OPTIONS.worklist, err)) sys.exit(1) if OPTIONS.write_failures: try: with open(OPTIONS.write_failures) as out: for test in self.failures: test.show(out) except IOError as err: sys.stderr.write("Error writing worklist file '%s': %s" % (OPTIONS.write_failures, err)) sys.exit(1) if self.timeouts: print "tests timed out:" for test in self.timeouts: test.show(sys.stdout) if self.failures or self.timeouts: sys.exit(2) class Test(TaskPool.Task): def __init__(self, path, summary): super(Test, self).__init__() self.test_path = path # path to .py test file self.summary = summary # test.name is the name of the test relative to the top of the test # directory. This is what we use to report failures and timeouts, # and when writing test lists. self.name = os.path.relpath(self.test_path, OPTIONS.testdir) self.stdout = '' self.stderr = '' self.returncode = None def cmd(self): testlibdir = os.path.normpath(os.path.join(OPTIONS.testdir, '..', 'lib-for-tests')) return [OPTIONS.gdb_executable, '-nw', # Don't create a window (unnecessary?) '-nx', # Don't read .gdbinit. '--ex', 'add-auto-load-safe-path %s' % (OPTIONS.builddir,), '--ex', 'set env LD_LIBRARY_PATH %s' % os.path.join(OPTIONS.objdir, 'js', 'src'), '--ex', 'file %s' % (os.path.join(OPTIONS.builddir, 'gdb-tests'),), '--eval-command', 'python testlibdir=%r' % (testlibdir,), '--eval-command', 'python testscript=%r' % (self.test_path,), '--eval-command', 'python exec(open(%r).read())' % os.path.join(testlibdir, 'catcher.py')] def start(self, pipe, deadline): super(Test, self).start(pipe, deadline) if OPTIONS.show_cmd: self.summary.interleave_output(lambda: self.show_cmd(sys.stdout)) def onStdout(self, text): self.stdout += text def onStderr(self, text): self.stderr += text def onFinished(self, returncode): self.returncode = returncode if OPTIONS.show_output: self.summary.interleave_output(lambda: self.show_output(sys.stdout)) if returncode != 0: self.summary.failed(self) else: self.summary.passed(self) def onTimeout(self): self.summary.timeout(self) def show_cmd(self, out): print "Command: ", make_shell_cmd(self.cmd()) def show_output(self, out): if self.stdout: out.write('Standard output:') out.write('\n' + self.stdout + '\n') if self.stderr: out.write('Standard error:') out.write('\n' + self.stderr + '\n') def show(self, out): out.write(self.name + '\n') if OPTIONS.write_failure_output: out.write('Command: %s\n' % (make_shell_cmd(self.cmd()),)) self.show_output(out) out.write('GDB exit code: %r\n' % (self.returncode,)) def find_tests(dir, substring = None): ans = [] for dirpath, dirnames, filenames in os.walk(dir): if dirpath == '.': continue for filename in filenames: if not filename.endswith('.py'): continue test = os.path.join(dirpath, filename) if substring is None or substring in os.path.relpath(test, dir): ans.append(test) return ans def build_test_exec(builddir): p = subprocess.check_call(['make', 'gdb-tests'], cwd=builddir) def run_tests(tests, summary): pool = TaskPool(tests, job_limit=OPTIONS.workercount, timeout=OPTIONS.timeout) pool.run_all() OPTIONS = None def main(argv): global OPTIONS script_path = os.path.abspath(__file__) script_dir = os.path.dirname(script_path) # OBJDIR is a standalone SpiderMonkey build directory. This is where we # find the SpiderMonkey shared library to link against. # # The [TESTS] optional arguments are paths of test files relative # to the jit-test/tests directory. from optparse import OptionParser op = OptionParser(usage='%prog [options] OBJDIR [TESTS...]') op.add_option('-s', '--show-cmd', dest='show_cmd', action='store_true', help='show GDB shell command run') op.add_option('-o', '--show-output', dest='show_output', action='store_true', help='show output from GDB') op.add_option('-x', '--exclude', dest='exclude', action='append', help='exclude given test dir or path') op.add_option('-t', '--timeout', dest='timeout', type=float, default=150.0, help='set test timeout in seconds') op.add_option('-j', '--worker-count', dest='workercount', type=int, help='Run [WORKERCOUNT] tests at a time') op.add_option('--no-progress', dest='hide_progress', action='store_true', help='hide progress bar') op.add_option('--worklist', dest='worklist', metavar='FILE', help='Read tests to run from [FILE] (or run all if [FILE] not found);\n' 'write failures back to [FILE]') op.add_option('-r', '--read-tests', dest='read_tests', metavar='FILE', help='Run test files listed in [FILE]') op.add_option('-w', '--write-failures', dest='write_failures', metavar='FILE', help='Write failing tests to [FILE]') op.add_option('--write-failure-output', dest='write_failure_output', action='store_true', help='With --write-failures=FILE, additionally write the output of failed tests to [FILE]') op.add_option('--gdb', dest='gdb_executable', metavar='EXECUTABLE', default='gdb', help='Run tests with [EXECUTABLE], rather than plain \'gdb\'.') op.add_option('--srcdir', dest='srcdir', default=os.path.abspath(os.path.join(script_dir, '..')), help='Use SpiderMonkey sources in [SRCDIR].') op.add_option('--testdir', dest='testdir', default=os.path.join(script_dir, 'tests'), help='Find tests in [TESTDIR].') op.add_option('--builddir', dest='builddir', help='Build test executable in [BUILDDIR].') (OPTIONS, args) = op.parse_args(argv) if len(args) < 1: op.error('missing OBJDIR argument') OPTIONS.objdir = os.path.abspath(args[0]) test_args = args[1:] if not OPTIONS.workercount: OPTIONS.workercount = get_cpu_count() # Compute default for OPTIONS.builddir now, since we've computed OPTIONS.objdir. if not OPTIONS.builddir: OPTIONS.builddir = os.path.join(OPTIONS.objdir, 'js', 'src', 'gdb') test_set = set() # All the various sources of test names accumulate. if test_args: for arg in test_args: test_set.update(find_tests(OPTIONS.testdir, arg)) if OPTIONS.worklist: try: with open(OPTIONS.worklist) as f: for line in f: test_set.update(os.path.join(test_dir, line.strip('\n'))) except IOError: # With worklist, a missing file means to start the process with # the complete list of tests. sys.stderr.write("Couldn't read worklist file '%s'; running all tests\n" % (OPTIONS.worklist,)) test_set = set(find_tests(OPTIONS.testdir)) if OPTIONS.read_tests: try: with open(OPTIONS.read_tests) as f: for line in f: test_set.update(os.path.join(test_dir, line.strip('\n'))) except IOError as err: sys.stderr.write("Error trying to read test file '%s': %s\n" % (OPTIONS.read_tests, err)) sys.exit(1) # If none of the above options were passed, and no tests were listed # explicitly, use the complete set. if not test_args and not OPTIONS.worklist and not OPTIONS.read_tests: test_set = set(find_tests(OPTIONS.testdir)) if OPTIONS.exclude: exclude_set = set() for exclude in OPTIONS.exclude: exclude_set.update(find_tests(test_dir, exclude)) test_set -= exclude_set if not test_set: sys.stderr.write("No tests found matching command line arguments.\n") sys.exit(1) summary = Summary(len(test_set)) test_list = [ Test(_, summary) for _ in sorted(test_set) ] # Build the test executable from all the .cpp files found in the test # directory tree. try: build_test_exec(OPTIONS.builddir) except subprocess.CalledProcessError as err: sys.stderr.write("Error building test executable: %s\n" % (err,)) sys.exit(1) # Run the tests. try: summary.start() run_tests(test_list, summary) summary.finish() except OSError as err: sys.stderr.write("Error running tests: %s\n" % (err,)) sys.exit(1) sys.exit(0) if __name__ == '__main__': main(sys.argv[1:])