39 # (You could use any subset of the tests: test-s* happens to match |
39 # (You could use any subset of the tests: test-s* happens to match |
40 # enough that it's worth doing parallel runs, few enough that it |
40 # enough that it's worth doing parallel runs, few enough that it |
41 # completes fairly quickly, includes both shell and Python scripts, and |
41 # completes fairly quickly, includes both shell and Python scripts, and |
42 # includes some scripts that run daemon processes.) |
42 # includes some scripts that run daemon processes.) |
43 |
43 |
44 from ConfigParser import ConfigParser |
|
45 import difflib |
44 import difflib |
46 import errno |
45 import errno |
47 import optparse |
46 import optparse |
48 import os |
47 import os |
|
48 import signal |
49 import subprocess |
49 import subprocess |
50 import shutil |
50 import shutil |
51 import signal |
51 import signal |
52 import sys |
52 import sys |
53 import tempfile |
53 import tempfile |
132 parser.add_option("-3", "--py3k-warnings", action="store_true", |
132 parser.add_option("-3", "--py3k-warnings", action="store_true", |
133 help="enable Py3k warnings on Python 2.6+") |
133 help="enable Py3k warnings on Python 2.6+") |
134 parser.add_option("--inotify", action="store_true", |
134 parser.add_option("--inotify", action="store_true", |
135 help="enable inotify extension when running tests") |
135 help="enable inotify extension when running tests") |
136 parser.add_option("--blacklist", action="append", |
136 parser.add_option("--blacklist", action="append", |
137 help="skip tests listed in the specified section of " |
137 help="skip tests listed in the specified blacklist file") |
138 "the blacklist file") |
|
139 |
138 |
140 for option, default in defaults.items(): |
139 for option, default in defaults.items(): |
141 defaults[option] = int(os.environ.get(*default)) |
140 defaults[option] = int(os.environ.get(*default)) |
142 parser.set_defaults(**defaults) |
141 parser.set_defaults(**defaults) |
143 (options, args) = parser.parse_args() |
142 (options, args) = parser.parse_args() |
200 options.timeout = 0 |
199 options.timeout = 0 |
201 if options.py3k_warnings: |
200 if options.py3k_warnings: |
202 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): |
201 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): |
203 parser.error('--py3k-warnings can only be used on Python 2.6+') |
202 parser.error('--py3k-warnings can only be used on Python 2.6+') |
204 if options.blacklist: |
203 if options.blacklist: |
205 configparser = ConfigParser() |
|
206 configparser.read("blacklist") |
|
207 blacklist = dict() |
204 blacklist = dict() |
208 for section in options.blacklist: |
205 for filename in options.blacklist: |
209 for (item, value) in configparser.items(section): |
206 try: |
210 blacklist["test-" + item] = section |
207 path = os.path.expanduser(os.path.expandvars(filename)) |
|
208 f = open(path, "r") |
|
209 except IOError, err: |
|
210 if err.errno != errno.ENOENT: |
|
211 raise |
|
212 print "warning: no such blacklist file: %s" % filename |
|
213 continue |
|
214 |
|
215 for line in f.readlines(): |
|
216 line = line.strip() |
|
217 if line and not line.startswith('#'): |
|
218 blacklist[line] = filename |
|
219 |
211 options.blacklist = blacklist |
220 options.blacklist = blacklist |
212 |
221 |
213 return (options, args) |
222 return (options, args) |
214 |
223 |
215 def rename(src, dst): |
224 def rename(src, dst): |
272 found = findprogram(p) |
281 found = findprogram(p) |
273 if found: |
282 if found: |
274 vlog("# Found prerequisite", p, "at", found) |
283 vlog("# Found prerequisite", p, "at", found) |
275 else: |
284 else: |
276 print "WARNING: Did not find prerequisite tool: "+p |
285 print "WARNING: Did not find prerequisite tool: "+p |
|
286 |
|
287 def killdaemons(): |
|
288 # Kill off any leftover daemon processes |
|
289 try: |
|
290 fp = open(DAEMON_PIDS) |
|
291 for line in fp: |
|
292 try: |
|
293 pid = int(line) |
|
294 except ValueError: |
|
295 continue |
|
296 try: |
|
297 os.kill(pid, 0) |
|
298 vlog('# Killing daemon process %d' % pid) |
|
299 os.kill(pid, signal.SIGTERM) |
|
300 time.sleep(0.25) |
|
301 os.kill(pid, 0) |
|
302 vlog('# Daemon process %d is stuck - really killing it' % pid) |
|
303 os.kill(pid, signal.SIGKILL) |
|
304 except OSError, err: |
|
305 if err.errno != errno.ESRCH: |
|
306 raise |
|
307 fp.close() |
|
308 os.unlink(DAEMON_PIDS) |
|
309 except IOError: |
|
310 pass |
277 |
311 |
278 def cleanup(options): |
312 def cleanup(options): |
279 if not options.keep_tmpdir: |
313 if not options.keep_tmpdir: |
280 vlog("# Cleaning up HGTMP", HGTMP) |
314 vlog("# Cleaning up HGTMP", HGTMP) |
281 shutil.rmtree(HGTMP, True) |
315 shutil.rmtree(HGTMP, True) |
422 ret = fromchild.close() |
456 ret = fromchild.close() |
423 if ret == None: |
457 if ret == None: |
424 ret = 0 |
458 ret = 0 |
425 else: |
459 else: |
426 proc = Popen4(cmd) |
460 proc = Popen4(cmd) |
|
461 def cleanup(): |
|
462 os.kill(proc.pid, signal.SIGTERM) |
|
463 ret = proc.wait() |
|
464 if ret == 0: |
|
465 ret = signal.SIGTERM << 8 |
|
466 killdaemons() |
|
467 return ret |
|
468 |
427 try: |
469 try: |
428 output = '' |
470 output = '' |
429 proc.tochild.close() |
471 proc.tochild.close() |
430 output = proc.fromchild.read() |
472 output = proc.fromchild.read() |
431 ret = proc.wait() |
473 ret = proc.wait() |
432 if os.WIFEXITED(ret): |
474 if os.WIFEXITED(ret): |
433 ret = os.WEXITSTATUS(ret) |
475 ret = os.WEXITSTATUS(ret) |
434 except Timeout: |
476 except Timeout: |
435 vlog('# Process %d timed out - killing it' % proc.pid) |
477 vlog('# Process %d timed out - killing it' % proc.pid) |
436 os.kill(proc.pid, signal.SIGTERM) |
478 ret = cleanup() |
437 ret = proc.wait() |
|
438 if ret == 0: |
|
439 ret = signal.SIGTERM << 8 |
|
440 output += ("\n### Abort: timeout after %d seconds.\n" |
479 output += ("\n### Abort: timeout after %d seconds.\n" |
441 % options.timeout) |
480 % options.timeout) |
|
481 except KeyboardInterrupt: |
|
482 vlog('# Handling keyboard interrupt') |
|
483 cleanup() |
|
484 raise |
|
485 |
442 return ret, splitnewlines(output) |
486 return ret, splitnewlines(output) |
443 |
487 |
444 def runone(options, test, skips, fails): |
488 def runone(options, test, skips, fails): |
445 '''tristate output: |
489 '''tristate output: |
446 None -> skipped |
490 None -> skipped |
579 f = open(err, "wb") |
623 f = open(err, "wb") |
580 for line in out: |
624 for line in out: |
581 f.write(line) |
625 f.write(line) |
582 f.close() |
626 f.close() |
583 |
627 |
584 # Kill off any leftover daemon processes |
628 killdaemons() |
585 try: |
|
586 fp = open(DAEMON_PIDS) |
|
587 for line in fp: |
|
588 try: |
|
589 pid = int(line) |
|
590 except ValueError: |
|
591 continue |
|
592 try: |
|
593 os.kill(pid, 0) |
|
594 vlog('# Killing daemon process %d' % pid) |
|
595 os.kill(pid, signal.SIGTERM) |
|
596 time.sleep(0.25) |
|
597 os.kill(pid, 0) |
|
598 vlog('# Daemon process %d is stuck - really killing it' % pid) |
|
599 os.kill(pid, signal.SIGKILL) |
|
600 except OSError, err: |
|
601 if err.errno != errno.ESRCH: |
|
602 raise |
|
603 fp.close() |
|
604 os.unlink(DAEMON_PIDS) |
|
605 except IOError: |
|
606 pass |
|
607 |
629 |
608 os.chdir(TESTDIR) |
630 os.chdir(TESTDIR) |
609 if not options.keep_tmpdir: |
631 if not options.keep_tmpdir: |
610 shutil.rmtree(tmpd, True) |
632 shutil.rmtree(tmpd, True) |
611 if skipped: |
633 if skipped: |
658 |
680 |
659 tests.reverse() |
681 tests.reverse() |
660 jobs = [[] for j in xrange(options.jobs)] |
682 jobs = [[] for j in xrange(options.jobs)] |
661 while tests: |
683 while tests: |
662 for job in jobs: |
684 for job in jobs: |
663 if not tests: break |
685 if not tests: |
|
686 break |
664 job.append(tests.pop()) |
687 job.append(tests.pop()) |
665 fps = {} |
688 fps = {} |
|
689 |
666 for j, job in enumerate(jobs): |
690 for j, job in enumerate(jobs): |
667 if not job: |
691 if not job: |
668 continue |
692 continue |
669 rfd, wfd = os.pipe() |
693 rfd, wfd = os.pipe() |
670 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] |
694 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] |
672 childopts += ['--tmpdir', childtmp] |
696 childopts += ['--tmpdir', childtmp] |
673 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job |
697 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job |
674 vlog(' '.join(cmdline)) |
698 vlog(' '.join(cmdline)) |
675 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') |
699 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') |
676 os.close(wfd) |
700 os.close(wfd) |
|
701 signal.signal(signal.SIGINT, signal.SIG_IGN) |
677 failures = 0 |
702 failures = 0 |
678 tested, skipped, failed = 0, 0, 0 |
703 tested, skipped, failed = 0, 0, 0 |
679 skips = [] |
704 skips = [] |
680 fails = [] |
705 fails = [] |
681 while fps: |
706 while fps: |
682 pid, status = os.wait() |
707 pid, status = os.wait() |
683 fp = fps.pop(pid) |
708 fp = fps.pop(pid) |
684 l = fp.read().splitlines() |
709 l = fp.read().splitlines() |
685 test, skip, fail = map(int, l[:3]) |
710 try: |
|
711 test, skip, fail = map(int, l[:3]) |
|
712 except ValueError: |
|
713 test, skip, fail = 0, 0, 0 |
686 split = -fail or len(l) |
714 split = -fail or len(l) |
687 for s in l[3:split]: |
715 for s in l[3:split]: |
688 skips.append(s.split(" ", 1)) |
716 skips.append(s.split(" ", 1)) |
689 for s in l[split:]: |
717 for s in l[split:]: |
690 fails.append(s.split(" ", 1)) |
718 fails.append(s.split(" ", 1)) |
741 skips = [] |
769 skips = [] |
742 fails = [] |
770 fails = [] |
743 |
771 |
744 for test in tests: |
772 for test in tests: |
745 if options.blacklist: |
773 if options.blacklist: |
746 section = options.blacklist.get(test) |
774 filename = options.blacklist.get(test) |
747 if section is not None: |
775 if filename is not None: |
748 skips.append((test, "blacklisted (%s section)" % section)) |
776 skips.append((test, "blacklisted (%s)" % filename)) |
749 skipped += 1 |
777 skipped += 1 |
750 continue |
778 continue |
751 |
779 |
752 if options.retest and not os.path.exists(test + ".err"): |
780 if options.retest and not os.path.exists(test + ".err"): |
753 skipped += 1 |
781 skipped += 1 |