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