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