Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | #!/usr/bin/env python |
michael@0 | 2 | # This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 5 | |
michael@0 | 6 | # run-tests.py -- Python harness for GDB SpiderMonkey support |
michael@0 | 7 | |
michael@0 | 8 | import os, re, subprocess, sys, traceback |
michael@0 | 9 | from threading import Thread |
michael@0 | 10 | |
michael@0 | 11 | # From this directory: |
michael@0 | 12 | import progressbar |
michael@0 | 13 | from taskpool import TaskPool, get_cpu_count |
michael@0 | 14 | |
michael@0 | 15 | # Backported from Python 3.1 posixpath.py |
michael@0 | 16 | def _relpath(path, start=None): |
michael@0 | 17 | """Return a relative version of a path""" |
michael@0 | 18 | |
michael@0 | 19 | if not path: |
michael@0 | 20 | raise ValueError("no path specified") |
michael@0 | 21 | |
michael@0 | 22 | if start is None: |
michael@0 | 23 | start = os.curdir |
michael@0 | 24 | |
michael@0 | 25 | start_list = os.path.abspath(start).split(os.sep) |
michael@0 | 26 | path_list = os.path.abspath(path).split(os.sep) |
michael@0 | 27 | |
michael@0 | 28 | # Work out how much of the filepath is shared by start and path. |
michael@0 | 29 | i = len(os.path.commonprefix([start_list, path_list])) |
michael@0 | 30 | |
michael@0 | 31 | rel_list = [os.pardir] * (len(start_list)-i) + path_list[i:] |
michael@0 | 32 | if not rel_list: |
michael@0 | 33 | return os.curdir |
michael@0 | 34 | return os.path.join(*rel_list) |
michael@0 | 35 | |
michael@0 | 36 | os.path.relpath = _relpath |
michael@0 | 37 | |
michael@0 | 38 | # Characters that need to be escaped when used in shell words. |
michael@0 | 39 | shell_need_escapes = re.compile('[^\w\d%+,-./:=@\'"]', re.DOTALL) |
michael@0 | 40 | # Characters that need to be escaped within double-quoted strings. |
michael@0 | 41 | shell_dquote_escapes = re.compile('[^\w\d%+,-./:=@"]', re.DOTALL) |
michael@0 | 42 | def make_shell_cmd(l): |
michael@0 | 43 | def quote(s): |
michael@0 | 44 | if shell_need_escapes.search(s): |
michael@0 | 45 | if s.find("'") < 0: |
michael@0 | 46 | return "'" + s + "'" |
michael@0 | 47 | return '"' + shell_dquote_escapes.sub('\\g<0>', s) + '"' |
michael@0 | 48 | return s |
michael@0 | 49 | |
michael@0 | 50 | return ' '.join([quote(_) for _ in l]) |
michael@0 | 51 | |
michael@0 | 52 | # An instance of this class collects the lists of passing, failing, and |
michael@0 | 53 | # timing-out tests, runs the progress bar, and prints a summary at the end. |
michael@0 | 54 | class Summary(object): |
michael@0 | 55 | |
michael@0 | 56 | class SummaryBar(progressbar.ProgressBar): |
michael@0 | 57 | def __init__(self, limit): |
michael@0 | 58 | super(Summary.SummaryBar, self).__init__('', limit, 24) |
michael@0 | 59 | def start(self): |
michael@0 | 60 | self.label = '[starting ]' |
michael@0 | 61 | self.update(0) |
michael@0 | 62 | def counts(self, run, failures, timeouts): |
michael@0 | 63 | self.label = '[%4d|%4d|%4d|%4d]' % (run - failures, failures, timeouts, run) |
michael@0 | 64 | self.update(run) |
michael@0 | 65 | |
michael@0 | 66 | def __init__(self, num_tests): |
michael@0 | 67 | self.run = 0 |
michael@0 | 68 | self.failures = [] # kind of judgemental; "unexpecteds"? |
michael@0 | 69 | self.timeouts = [] |
michael@0 | 70 | if not OPTIONS.hide_progress: |
michael@0 | 71 | self.bar = Summary.SummaryBar(num_tests) |
michael@0 | 72 | |
michael@0 | 73 | # Progress bar control. |
michael@0 | 74 | def start(self): |
michael@0 | 75 | if not OPTIONS.hide_progress: |
michael@0 | 76 | self.bar.start() |
michael@0 | 77 | def update(self): |
michael@0 | 78 | if not OPTIONS.hide_progress: |
michael@0 | 79 | self.bar.counts(self.run, len(self.failures), len(self.timeouts)) |
michael@0 | 80 | # Call 'thunk' to show some output, while getting the progress bar out of the way. |
michael@0 | 81 | def interleave_output(self, thunk): |
michael@0 | 82 | if not OPTIONS.hide_progress: |
michael@0 | 83 | self.bar.clear() |
michael@0 | 84 | thunk() |
michael@0 | 85 | self.update() |
michael@0 | 86 | |
michael@0 | 87 | def passed(self, test): |
michael@0 | 88 | self.run += 1 |
michael@0 | 89 | self.update() |
michael@0 | 90 | |
michael@0 | 91 | def failed(self, test): |
michael@0 | 92 | self.run += 1 |
michael@0 | 93 | self.failures.append(test) |
michael@0 | 94 | self.update() |
michael@0 | 95 | |
michael@0 | 96 | def timeout(self, test): |
michael@0 | 97 | self.run += 1 |
michael@0 | 98 | self.timeouts.append(test) |
michael@0 | 99 | self.update() |
michael@0 | 100 | |
michael@0 | 101 | def finish(self): |
michael@0 | 102 | if not OPTIONS.hide_progress: |
michael@0 | 103 | self.bar.finish() |
michael@0 | 104 | |
michael@0 | 105 | if self.failures: |
michael@0 | 106 | |
michael@0 | 107 | print "tests failed:" |
michael@0 | 108 | for test in self.failures: |
michael@0 | 109 | test.show(sys.stdout) |
michael@0 | 110 | |
michael@0 | 111 | if OPTIONS.worklist: |
michael@0 | 112 | try: |
michael@0 | 113 | with open(OPTIONS.worklist) as out: |
michael@0 | 114 | for test in self.failures: |
michael@0 | 115 | out.write(test.name + '\n') |
michael@0 | 116 | except IOError as err: |
michael@0 | 117 | sys.stderr.write("Error writing worklist file '%s': %s" |
michael@0 | 118 | % (OPTIONS.worklist, err)) |
michael@0 | 119 | sys.exit(1) |
michael@0 | 120 | |
michael@0 | 121 | if OPTIONS.write_failures: |
michael@0 | 122 | try: |
michael@0 | 123 | with open(OPTIONS.write_failures) as out: |
michael@0 | 124 | for test in self.failures: |
michael@0 | 125 | test.show(out) |
michael@0 | 126 | except IOError as err: |
michael@0 | 127 | sys.stderr.write("Error writing worklist file '%s': %s" |
michael@0 | 128 | % (OPTIONS.write_failures, err)) |
michael@0 | 129 | sys.exit(1) |
michael@0 | 130 | |
michael@0 | 131 | if self.timeouts: |
michael@0 | 132 | print "tests timed out:" |
michael@0 | 133 | for test in self.timeouts: |
michael@0 | 134 | test.show(sys.stdout) |
michael@0 | 135 | |
michael@0 | 136 | if self.failures or self.timeouts: |
michael@0 | 137 | sys.exit(2) |
michael@0 | 138 | |
michael@0 | 139 | class Test(TaskPool.Task): |
michael@0 | 140 | def __init__(self, path, summary): |
michael@0 | 141 | super(Test, self).__init__() |
michael@0 | 142 | self.test_path = path # path to .py test file |
michael@0 | 143 | self.summary = summary |
michael@0 | 144 | |
michael@0 | 145 | # test.name is the name of the test relative to the top of the test |
michael@0 | 146 | # directory. This is what we use to report failures and timeouts, |
michael@0 | 147 | # and when writing test lists. |
michael@0 | 148 | self.name = os.path.relpath(self.test_path, OPTIONS.testdir) |
michael@0 | 149 | |
michael@0 | 150 | self.stdout = '' |
michael@0 | 151 | self.stderr = '' |
michael@0 | 152 | self.returncode = None |
michael@0 | 153 | |
michael@0 | 154 | def cmd(self): |
michael@0 | 155 | testlibdir = os.path.normpath(os.path.join(OPTIONS.testdir, '..', 'lib-for-tests')) |
michael@0 | 156 | return [OPTIONS.gdb_executable, |
michael@0 | 157 | '-nw', # Don't create a window (unnecessary?) |
michael@0 | 158 | '-nx', # Don't read .gdbinit. |
michael@0 | 159 | '--ex', 'add-auto-load-safe-path %s' % (OPTIONS.builddir,), |
michael@0 | 160 | '--ex', 'set env LD_LIBRARY_PATH %s' % (OPTIONS.libdir,), |
michael@0 | 161 | '--ex', 'file %s' % (os.path.join(OPTIONS.builddir, 'gdb-tests'),), |
michael@0 | 162 | '--eval-command', 'python testlibdir=%r' % (testlibdir,), |
michael@0 | 163 | '--eval-command', 'python testscript=%r' % (self.test_path,), |
michael@0 | 164 | '--eval-command', 'python execfile(%r)' % os.path.join(testlibdir, 'catcher.py')] |
michael@0 | 165 | |
michael@0 | 166 | def start(self, pipe, deadline): |
michael@0 | 167 | super(Test, self).start(pipe, deadline) |
michael@0 | 168 | if OPTIONS.show_cmd: |
michael@0 | 169 | self.summary.interleave_output(lambda: self.show_cmd(sys.stdout)) |
michael@0 | 170 | |
michael@0 | 171 | def onStdout(self, text): |
michael@0 | 172 | self.stdout += text |
michael@0 | 173 | |
michael@0 | 174 | def onStderr(self, text): |
michael@0 | 175 | self.stderr += text |
michael@0 | 176 | |
michael@0 | 177 | def onFinished(self, returncode): |
michael@0 | 178 | self.returncode = returncode |
michael@0 | 179 | if OPTIONS.show_output: |
michael@0 | 180 | self.summary.interleave_output(lambda: self.show_output(sys.stdout)) |
michael@0 | 181 | if returncode != 0: |
michael@0 | 182 | self.summary.failed(self) |
michael@0 | 183 | else: |
michael@0 | 184 | self.summary.passed(self) |
michael@0 | 185 | |
michael@0 | 186 | def onTimeout(self): |
michael@0 | 187 | self.summary.timeout(self) |
michael@0 | 188 | |
michael@0 | 189 | def show_cmd(self, out): |
michael@0 | 190 | print "Command: ", make_shell_cmd(self.cmd()) |
michael@0 | 191 | |
michael@0 | 192 | def show_output(self, out): |
michael@0 | 193 | if self.stdout: |
michael@0 | 194 | out.write('Standard output:') |
michael@0 | 195 | out.write('\n' + self.stdout + '\n') |
michael@0 | 196 | if self.stderr: |
michael@0 | 197 | out.write('Standard error:') |
michael@0 | 198 | out.write('\n' + self.stderr + '\n') |
michael@0 | 199 | |
michael@0 | 200 | def show(self, out): |
michael@0 | 201 | out.write(self.name + '\n') |
michael@0 | 202 | if OPTIONS.write_failure_output: |
michael@0 | 203 | out.write('Command: %s\n' % (make_shell_cmd(self.cmd()),)) |
michael@0 | 204 | self.show_output(out) |
michael@0 | 205 | out.write('GDB exit code: %r\n' % (self.returncode,)) |
michael@0 | 206 | |
michael@0 | 207 | def find_tests(dir, substring = None): |
michael@0 | 208 | ans = [] |
michael@0 | 209 | for dirpath, dirnames, filenames in os.walk(dir): |
michael@0 | 210 | if dirpath == '.': |
michael@0 | 211 | continue |
michael@0 | 212 | for filename in filenames: |
michael@0 | 213 | if not filename.endswith('.py'): |
michael@0 | 214 | continue |
michael@0 | 215 | test = os.path.join(dirpath, filename) |
michael@0 | 216 | if substring is None or substring in os.path.relpath(test, dir): |
michael@0 | 217 | ans.append(test) |
michael@0 | 218 | return ans |
michael@0 | 219 | |
michael@0 | 220 | def build_test_exec(builddir): |
michael@0 | 221 | p = subprocess.check_call(['make', 'gdb-tests'], cwd=builddir) |
michael@0 | 222 | |
michael@0 | 223 | def run_tests(tests, summary): |
michael@0 | 224 | pool = TaskPool(tests, job_limit=OPTIONS.workercount, timeout=OPTIONS.timeout) |
michael@0 | 225 | pool.run_all() |
michael@0 | 226 | |
michael@0 | 227 | OPTIONS = None |
michael@0 | 228 | def main(argv): |
michael@0 | 229 | global OPTIONS |
michael@0 | 230 | script_path = os.path.abspath(__file__) |
michael@0 | 231 | script_dir = os.path.dirname(script_path) |
michael@0 | 232 | |
michael@0 | 233 | # LIBDIR is the directory in which we find the SpiderMonkey shared |
michael@0 | 234 | # library, to link against. |
michael@0 | 235 | # |
michael@0 | 236 | # The [TESTS] optional arguments are paths of test files relative |
michael@0 | 237 | # to the jit-test/tests directory. |
michael@0 | 238 | from optparse import OptionParser |
michael@0 | 239 | op = OptionParser(usage='%prog [options] LIBDIR [TESTS...]') |
michael@0 | 240 | op.add_option('-s', '--show-cmd', dest='show_cmd', action='store_true', |
michael@0 | 241 | help='show GDB shell command run') |
michael@0 | 242 | op.add_option('-o', '--show-output', dest='show_output', action='store_true', |
michael@0 | 243 | help='show output from GDB') |
michael@0 | 244 | op.add_option('-x', '--exclude', dest='exclude', action='append', |
michael@0 | 245 | help='exclude given test dir or path') |
michael@0 | 246 | op.add_option('-t', '--timeout', dest='timeout', type=float, default=150.0, |
michael@0 | 247 | help='set test timeout in seconds') |
michael@0 | 248 | op.add_option('-j', '--worker-count', dest='workercount', type=int, |
michael@0 | 249 | help='Run [WORKERCOUNT] tests at a time') |
michael@0 | 250 | op.add_option('--no-progress', dest='hide_progress', action='store_true', |
michael@0 | 251 | help='hide progress bar') |
michael@0 | 252 | op.add_option('--worklist', dest='worklist', metavar='FILE', |
michael@0 | 253 | help='Read tests to run from [FILE] (or run all if [FILE] not found);\n' |
michael@0 | 254 | 'write failures back to [FILE]') |
michael@0 | 255 | op.add_option('-r', '--read-tests', dest='read_tests', metavar='FILE', |
michael@0 | 256 | help='Run test files listed in [FILE]') |
michael@0 | 257 | op.add_option('-w', '--write-failures', dest='write_failures', metavar='FILE', |
michael@0 | 258 | help='Write failing tests to [FILE]') |
michael@0 | 259 | op.add_option('--write-failure-output', dest='write_failure_output', action='store_true', |
michael@0 | 260 | help='With --write-failures=FILE, additionally write the output of failed tests to [FILE]') |
michael@0 | 261 | op.add_option('--gdb', dest='gdb_executable', metavar='EXECUTABLE', default='gdb', |
michael@0 | 262 | help='Run tests with [EXECUTABLE], rather than plain \'gdb\'.') |
michael@0 | 263 | op.add_option('--srcdir', dest='srcdir', |
michael@0 | 264 | default=os.path.abspath(os.path.join(script_dir, '..')), |
michael@0 | 265 | help='Use SpiderMonkey sources in [SRCDIR].') |
michael@0 | 266 | op.add_option('--testdir', dest='testdir', default=os.path.join(script_dir, 'tests'), |
michael@0 | 267 | help='Find tests in [TESTDIR].') |
michael@0 | 268 | op.add_option('--builddir', dest='builddir', |
michael@0 | 269 | help='Build test executable in [BUILDDIR].') |
michael@0 | 270 | (OPTIONS, args) = op.parse_args(argv) |
michael@0 | 271 | if len(args) < 1: |
michael@0 | 272 | op.error('missing LIBDIR argument') |
michael@0 | 273 | OPTIONS.libdir = os.path.abspath(args[0]) |
michael@0 | 274 | test_args = args[1:] |
michael@0 | 275 | |
michael@0 | 276 | if not OPTIONS.workercount: |
michael@0 | 277 | OPTIONS.workercount = get_cpu_count() |
michael@0 | 278 | |
michael@0 | 279 | # Compute default for OPTIONS.builddir now, since we've computed OPTIONS.libdir. |
michael@0 | 280 | if not OPTIONS.builddir: |
michael@0 | 281 | OPTIONS.builddir = os.path.join(OPTIONS.libdir, 'gdb') |
michael@0 | 282 | |
michael@0 | 283 | test_set = set() |
michael@0 | 284 | |
michael@0 | 285 | # All the various sources of test names accumulate. |
michael@0 | 286 | if test_args: |
michael@0 | 287 | for arg in test_args: |
michael@0 | 288 | test_set.update(find_tests(OPTIONS.testdir, arg)) |
michael@0 | 289 | if OPTIONS.worklist: |
michael@0 | 290 | try: |
michael@0 | 291 | with open(OPTIONS.worklist) as f: |
michael@0 | 292 | for line in f: |
michael@0 | 293 | test_set.update(os.path.join(test_dir, line.strip('\n'))) |
michael@0 | 294 | except IOError: |
michael@0 | 295 | # With worklist, a missing file means to start the process with |
michael@0 | 296 | # the complete list of tests. |
michael@0 | 297 | sys.stderr.write("Couldn't read worklist file '%s'; running all tests\n" |
michael@0 | 298 | % (OPTIONS.worklist,)) |
michael@0 | 299 | test_set = set(find_tests(OPTIONS.testdir)) |
michael@0 | 300 | if OPTIONS.read_tests: |
michael@0 | 301 | try: |
michael@0 | 302 | with open(OPTIONS.read_tests) as f: |
michael@0 | 303 | for line in f: |
michael@0 | 304 | test_set.update(os.path.join(test_dir, line.strip('\n'))) |
michael@0 | 305 | except IOError as err: |
michael@0 | 306 | sys.stderr.write("Error trying to read test file '%s': %s\n" |
michael@0 | 307 | % (OPTIONS.read_tests, err)) |
michael@0 | 308 | sys.exit(1) |
michael@0 | 309 | |
michael@0 | 310 | # If none of the above options were passed, and no tests were listed |
michael@0 | 311 | # explicitly, use the complete set. |
michael@0 | 312 | if not test_args and not OPTIONS.worklist and not OPTIONS.read_tests: |
michael@0 | 313 | test_set = set(find_tests(OPTIONS.testdir)) |
michael@0 | 314 | |
michael@0 | 315 | if OPTIONS.exclude: |
michael@0 | 316 | exclude_set = set() |
michael@0 | 317 | for exclude in OPTIONS.exclude: |
michael@0 | 318 | exclude_set.update(find_tests(test_dir, exclude)) |
michael@0 | 319 | test_set -= exclude_set |
michael@0 | 320 | |
michael@0 | 321 | if not test_set: |
michael@0 | 322 | sys.stderr.write("No tests found matching command line arguments.\n") |
michael@0 | 323 | sys.exit(1) |
michael@0 | 324 | |
michael@0 | 325 | summary = Summary(len(test_set)) |
michael@0 | 326 | test_list = [ Test(_, summary) for _ in sorted(test_set) ] |
michael@0 | 327 | |
michael@0 | 328 | # Build the test executable from all the .cpp files found in the test |
michael@0 | 329 | # directory tree. |
michael@0 | 330 | try: |
michael@0 | 331 | build_test_exec(OPTIONS.builddir) |
michael@0 | 332 | except subprocess.CalledProcessError as err: |
michael@0 | 333 | sys.stderr.write("Error building test executable: %s\n" % (err,)) |
michael@0 | 334 | sys.exit(1) |
michael@0 | 335 | |
michael@0 | 336 | # Run the tests. |
michael@0 | 337 | try: |
michael@0 | 338 | summary.start() |
michael@0 | 339 | run_tests(test_list, summary) |
michael@0 | 340 | summary.finish() |
michael@0 | 341 | except OSError as err: |
michael@0 | 342 | sys.stderr.write("Error running tests: %s\n" % (err,)) |
michael@0 | 343 | sys.exit(1) |
michael@0 | 344 | |
michael@0 | 345 | sys.exit(0) |
michael@0 | 346 | |
michael@0 | 347 | if __name__ == '__main__': |
michael@0 | 348 | main(sys.argv[1:]) |