comparison tests/run-tests.py @ 5384:e3a0c092b4e2

Allow tests to run in parallel.
author Bryan O'Sullivan <bos@serpentine.com>
date Fri, 05 Oct 2007 12:17:01 -0700
parents 7cdc896fdcd5
children 557e4a916e12
comparison
equal deleted inserted replaced
5383:7cdc896fdcd5 5384:e3a0c092b4e2
25 required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"] 25 required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
26 26
27 parser = optparse.OptionParser("%prog [options] [tests]") 27 parser = optparse.OptionParser("%prog [options] [tests]")
28 parser.add_option("-C", "--annotate", action="store_true", 28 parser.add_option("-C", "--annotate", action="store_true",
29 help="output files annotated with coverage") 29 help="output files annotated with coverage")
30 parser.add_option("--child", type="int",
31 help="run as child process, summary to given fd")
30 parser.add_option("-c", "--cover", action="store_true", 32 parser.add_option("-c", "--cover", action="store_true",
31 help="print a test coverage report") 33 help="print a test coverage report")
32 parser.add_option("-f", "--first", action="store_true", 34 parser.add_option("-f", "--first", action="store_true",
33 help="exit on the first test failure") 35 help="exit on the first test failure")
34 parser.add_option("-i", "--interactive", action="store_true", 36 parser.add_option("-i", "--interactive", action="store_true",
35 help="prompt to accept changed output") 37 help="prompt to accept changed output")
38 parser.add_option("-j", "--jobs", type="int",
39 help="number of jobs to run in parallel")
36 parser.add_option("-R", "--restart", action="store_true", 40 parser.add_option("-R", "--restart", action="store_true",
37 help="restart at last error") 41 help="restart at last error")
42 parser.add_option("-p", "--port", type="int",
43 help="port on which servers should listen")
38 parser.add_option("-r", "--retest", action="store_true", 44 parser.add_option("-r", "--retest", action="store_true",
39 help="retest failed tests") 45 help="retest failed tests")
40 parser.add_option("-s", "--cover_stdlib", action="store_true", 46 parser.add_option("-s", "--cover_stdlib", action="store_true",
41 help="print a test coverage report inc. standard libraries") 47 help="print a test coverage report inc. standard libraries")
42 parser.add_option("-t", "--timeout", type="int", 48 parser.add_option("-t", "--timeout", type="int",
43 help="kill errant tests after TIMEOUT seconds") 49 help="kill errant tests after TIMEOUT seconds")
44 parser.add_option("-v", "--verbose", action="store_true", 50 parser.add_option("-v", "--verbose", action="store_true",
45 help="output verbose messages") 51 help="output verbose messages")
46 52 parser.add_option("--with-hg", type="string",
47 parser.set_defaults(timeout=180) 53 help="test existing install at given location")
54
55 parser.set_defaults(jobs=1, port=20059, timeout=180)
48 (options, args) = parser.parse_args() 56 (options, args) = parser.parse_args()
49 verbose = options.verbose 57 verbose = options.verbose
50 coverage = options.cover or options.cover_stdlib or options.annotate 58 coverage = options.cover or options.cover_stdlib or options.annotate
51 python = sys.executable 59 python = sys.executable
60
61 if options.jobs < 1:
62 print >> sys.stderr, 'ERROR: -j/--jobs must be positive'
63 sys.exit(1)
64 if options.interactive and options.jobs > 1:
65 print >> sys.stderr, 'ERROR: cannot mix -interactive and --jobs > 1'
66 sys.exit(1)
52 67
53 def vlog(*msg): 68 def vlog(*msg):
54 if verbose: 69 if verbose:
55 for m in msg: 70 for m in msg:
56 print m, 71 print m,
366 shutil.rmtree(tmpd, True) 381 shutil.rmtree(tmpd, True)
367 if skipped: 382 if skipped:
368 return None 383 return None
369 return ret == 0 384 return ret == 0
370 385
371 386 if not options.child:
372 os.umask(022) 387 os.umask(022)
373 388
374 check_required_tools() 389 check_required_tools()
375 390
376 # Reset some environment variables to well-known values so that 391 # Reset some environment variables to well-known values so that
377 # the tests produce repeatable output. 392 # the tests produce repeatable output.
378 os.environ['LANG'] = os.environ['LC_ALL'] = 'C' 393 os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
379 os.environ['TZ'] = 'GMT' 394 os.environ['TZ'] = 'GMT'
380 395
381 TESTDIR = os.environ["TESTDIR"] = os.getcwd() 396 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
382 HGTMP = os.environ["HGTMP"] = tempfile.mkdtemp("", "hgtests.") 397 HGTMP = os.environ["HGTMP"] = tempfile.mkdtemp("", "hgtests.")
383 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') 398 DAEMON_PIDS = None
384 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') 399 HGRCPATH = None
385 400
386 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' 401 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
387 os.environ["HGMERGE"] = ('python "%s" -L my -L other' 402 os.environ["HGMERGE"] = ('python "%s" -L my -L other'
388 % os.path.join(TESTDIR, os.path.pardir, 'contrib', 403 % os.path.join(TESTDIR, os.path.pardir,
389 'simplemerge')) 404 'contrib', 'simplemerge'))
390 os.environ["HGUSER"] = "test" 405 os.environ["HGUSER"] = "test"
391 os.environ["HGENCODING"] = "ascii" 406 os.environ["HGENCODING"] = "ascii"
392 os.environ["HGENCODINGMODE"] = "strict" 407 os.environ["HGENCODINGMODE"] = "strict"
393 408 os.environ["HGPORT"] = str(options.port)
394 vlog("# Using TESTDIR", TESTDIR) 409 os.environ["HGPORT1"] = str(options.port + 1)
395 vlog("# Using HGTMP", HGTMP) 410 os.environ["HGPORT2"] = str(options.port + 2)
396 411
397 INST = os.path.join(HGTMP, "install") 412 if options.with_hg:
413 INST = options.with_hg
414 else:
415 INST = os.path.join(HGTMP, "install")
398 BINDIR = os.path.join(INST, "bin") 416 BINDIR = os.path.join(INST, "bin")
399 PYTHONDIR = os.path.join(INST, "lib", "python") 417 PYTHONDIR = os.path.join(INST, "lib", "python")
400 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage") 418 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
401 419
402 try: 420 def run_children(tests):
421 if not options.with_hg:
422 install_hg()
423
424 optcopy = dict(options.__dict__)
425 optcopy['jobs'] = 1
426 optcopy['with_hg'] = INST
427 opts = []
428 for opt, value in optcopy.iteritems():
429 name = '--' + opt.replace('_', '-')
430 if value is True:
431 opts.append(name)
432 elif value is not None:
433 opts.append(name + '=' + str(value))
434
435 tests.reverse()
436 jobs = [[] for j in xrange(options.jobs)]
437 while tests:
438 for j in xrange(options.jobs):
439 if not tests: break
440 jobs[j].append(tests.pop())
441 fps = {}
442 for j in xrange(len(jobs)):
443 job = jobs[j]
444 if not job:
445 continue
446 rfd, wfd = os.pipe()
447 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
448 cmdline = [python, sys.argv[0]] + opts + childopts + job
449 vlog(' '.join(cmdline))
450 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
451 os.close(wfd)
452 failures = 0
453 tested, skipped, failed = 0, 0, 0
454 while fps:
455 pid, status = os.wait()
456 fp = fps.pop(pid)
457 test, skip, fail = map(int, fp.read().splitlines())
458 tested += test
459 skipped += skip
460 failed += fail
461 vlog('pid %d exited, status %d' % (pid, status))
462 failures |= status
463 print "\n# Ran %d tests, %d skipped, %d failed." % (
464 tested, skipped, failed)
465 sys.exit(failures != 0)
466
467 def run_tests(tests):
468 global DAEMON_PIDS, HGRCPATH
469 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
470 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
471
403 try: 472 try:
404 install_hg() 473 if not options.with_hg:
474 install_hg()
405 475
406 if options.timeout > 0: 476 if options.timeout > 0:
407 try: 477 try:
408 signal.signal(signal.SIGALRM, alarmed) 478 signal.signal(signal.SIGALRM, alarmed)
409 vlog('# Running tests with %d-second timeout' % 479 vlog('# Running tests with %d-second timeout' %
413 options.timeout = 0 483 options.timeout = 0
414 484
415 tested = 0 485 tested = 0
416 failed = 0 486 failed = 0
417 skipped = 0 487 skipped = 0
418
419 if len(args) == 0:
420 args = os.listdir(".")
421 args.sort()
422
423
424 tests = []
425 for test in args:
426 if (test.startswith("test-") and '~' not in test and
427 ('.' not in test or test.endswith('.py') or
428 test.endswith('.bat'))):
429 tests.append(test)
430 488
431 if options.restart: 489 if options.restart:
432 orig = list(tests) 490 orig = list(tests)
433 while tests: 491 while tests:
434 if os.path.exists(tests[0] + ".err"): 492 if os.path.exists(tests[0] + ".err"):
456 failed += 1 514 failed += 1
457 if options.first: 515 if options.first:
458 break 516 break
459 tested += 1 517 tested += 1
460 518
461 print "\n# Ran %d tests, %d skipped, %d failed." % (tested, skipped, 519 if options.child:
462 failed) 520 fp = os.fdopen(options.child, 'w')
521 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
522 fp.close()
523 else:
524 print "\n# Ran %d tests, %d skipped, %d failed." % (
525 tested, skipped, failed)
526
463 if coverage: 527 if coverage:
464 output_coverage() 528 output_coverage()
465 except KeyboardInterrupt: 529 except KeyboardInterrupt:
466 failed = True 530 failed = True
467 print "\ninterrupted!" 531 print "\ninterrupted!"
532
533 if failed:
534 sys.exit(1)
535
536 if len(args) == 0:
537 args = os.listdir(".")
538 args.sort()
539
540 tests = []
541 for test in args:
542 if (test.startswith("test-") and '~' not in test and
543 ('.' not in test or test.endswith('.py') or
544 test.endswith('.bat'))):
545 tests.append(test)
546
547 vlog("# Using TESTDIR", TESTDIR)
548 vlog("# Using HGTMP", HGTMP)
549
550 try:
551 if len(tests) > 1 and options.jobs > 1:
552 run_children(tests)
553 else:
554 run_tests(tests)
468 finally: 555 finally:
469 cleanup_exit() 556 cleanup_exit()
470
471 if failed:
472 sys.exit(1)