14 # - tests are a mix of shell scripts and Python scripts |
14 # - tests are a mix of shell scripts and Python scripts |
15 # |
15 # |
16 # If you change this script, it is recommended that you ensure you |
16 # If you change this script, it is recommended that you ensure you |
17 # haven't broken it by running it in various modes with a representative |
17 # haven't broken it by running it in various modes with a representative |
18 # sample of test scripts. For example: |
18 # sample of test scripts. For example: |
19 # |
19 # |
20 # 1) serial, no coverage, temp install: |
20 # 1) serial, no coverage, temp install: |
21 # ./run-tests.py test-s* |
21 # ./run-tests.py test-s* |
22 # 2) serial, no coverage, local hg: |
22 # 2) serial, no coverage, local hg: |
23 # ./run-tests.py --local test-s* |
23 # ./run-tests.py --local test-s* |
24 # 3) serial, coverage, temp install: |
24 # 3) serial, coverage, temp install: |
29 # ./run-tests.py -j2 test-s* |
29 # ./run-tests.py -j2 test-s* |
30 # 6) parallel, no coverage, local hg: |
30 # 6) parallel, no coverage, local hg: |
31 # ./run-tests.py -j2 --local test-s* |
31 # ./run-tests.py -j2 --local test-s* |
32 # 7) parallel, coverage, temp install: |
32 # 7) parallel, coverage, temp install: |
33 # ./run-tests.py -j2 -c test-s* # currently broken |
33 # ./run-tests.py -j2 -c test-s* # currently broken |
34 # 8) parallel, coverage, local install |
34 # 8) parallel, coverage, local install: |
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) |
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) |
|
36 # 9) parallel, custom tmp dir: |
|
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests |
36 # |
38 # |
37 # (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 |
38 # 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 |
39 # completes fairly quickly, includes both shell and Python scripts, and |
41 # completes fairly quickly, includes both shell and Python scripts, and |
40 # includes some scripts that run daemon processes.) |
42 # includes some scripts that run daemon processes.) |
41 |
43 |
|
44 from ConfigParser import ConfigParser |
42 import difflib |
45 import difflib |
43 import errno |
46 import errno |
44 import optparse |
47 import optparse |
45 import os |
48 import os |
46 import subprocess |
49 import subprocess |
88 parser.add_option("-i", "--interactive", action="store_true", |
91 parser.add_option("-i", "--interactive", action="store_true", |
89 help="prompt to accept changed output") |
92 help="prompt to accept changed output") |
90 parser.add_option("-j", "--jobs", type="int", |
93 parser.add_option("-j", "--jobs", type="int", |
91 help="number of jobs to run in parallel" |
94 help="number of jobs to run in parallel" |
92 " (default: $%s or %d)" % defaults['jobs']) |
95 " (default: $%s or %d)" % defaults['jobs']) |
|
96 parser.add_option("-k", "--keywords", |
|
97 help="run tests matching keywords") |
93 parser.add_option("--keep-tmpdir", action="store_true", |
98 parser.add_option("--keep-tmpdir", action="store_true", |
94 help="keep temporary directory after running tests" |
99 help="keep temporary directory after running tests") |
95 " (best used with --tmpdir)") |
100 parser.add_option("--tmpdir", type="string", |
|
101 help="run tests in the given temporary directory" |
|
102 " (implies --keep-tmpdir)") |
|
103 parser.add_option("-d", "--debug", action="store_true", |
|
104 help="debug mode: write output of test scripts to console" |
|
105 " rather than capturing and diff'ing it (disables timeout)") |
96 parser.add_option("-R", "--restart", action="store_true", |
106 parser.add_option("-R", "--restart", action="store_true", |
97 help="restart at last error") |
107 help="restart at last error") |
98 parser.add_option("-p", "--port", type="int", |
108 parser.add_option("-p", "--port", type="int", |
99 help="port on which servers should listen" |
109 help="port on which servers should listen" |
100 " (default: $%s or %d)" % defaults['port']) |
110 " (default: $%s or %d)" % defaults['port']) |
101 parser.add_option("-r", "--retest", action="store_true", |
111 parser.add_option("-r", "--retest", action="store_true", |
102 help="retest failed tests") |
112 help="retest failed tests") |
103 parser.add_option("-s", "--cover_stdlib", action="store_true", |
113 parser.add_option("-s", "--cover_stdlib", action="store_true", |
104 help="print a test coverage report inc. standard libraries") |
114 help="print a test coverage report inc. standard libraries") |
|
115 parser.add_option("-S", "--noskips", action="store_true", |
|
116 help="don't report skip tests verbosely") |
105 parser.add_option("-t", "--timeout", type="int", |
117 parser.add_option("-t", "--timeout", type="int", |
106 help="kill errant tests after TIMEOUT seconds" |
118 help="kill errant tests after TIMEOUT seconds" |
107 " (default: $%s or %d)" % defaults['timeout']) |
119 " (default: $%s or %d)" % defaults['timeout']) |
108 parser.add_option("--tmpdir", type="string", |
|
109 help="run tests in the given temporary directory") |
|
110 parser.add_option("-v", "--verbose", action="store_true", |
120 parser.add_option("-v", "--verbose", action="store_true", |
111 help="output verbose messages") |
121 help="output verbose messages") |
112 parser.add_option("-n", "--nodiff", action="store_true", |
122 parser.add_option("-n", "--nodiff", action="store_true", |
113 help="skip showing test changes") |
123 help="skip showing test changes") |
114 parser.add_option("--with-hg", type="string", |
124 parser.add_option("--with-hg", type="string", |
117 "temporary installation") |
127 "temporary installation") |
118 parser.add_option("--local", action="store_true", |
128 parser.add_option("--local", action="store_true", |
119 help="shortcut for --with-hg=<testdir>/../hg") |
129 help="shortcut for --with-hg=<testdir>/../hg") |
120 parser.add_option("--pure", action="store_true", |
130 parser.add_option("--pure", action="store_true", |
121 help="use pure Python code instead of C extensions") |
131 help="use pure Python code instead of C extensions") |
|
132 parser.add_option("-3", "--py3k-warnings", action="store_true", |
|
133 help="enable Py3k warnings on Python 2.6+") |
|
134 parser.add_option("--inotify", action="store_true", |
|
135 help="enable inotify extension when running tests") |
|
136 parser.add_option("--blacklist", action="append", |
|
137 help="skip tests listed in the specified section of " |
|
138 "the blacklist file") |
122 |
139 |
123 for option, default in defaults.items(): |
140 for option, default in defaults.items(): |
124 defaults[option] = int(os.environ.get(*default)) |
141 defaults[option] = int(os.environ.get(*default)) |
125 parser.set_defaults(**defaults) |
142 parser.set_defaults(**defaults) |
126 (options, args) = parser.parse_args() |
143 (options, args) = parser.parse_args() |
160 if pid: |
177 if pid: |
161 print pid, |
178 print pid, |
162 for m in msg: |
179 for m in msg: |
163 print m, |
180 print m, |
164 print |
181 print |
|
182 sys.stdout.flush() |
165 else: |
183 else: |
166 vlog = lambda *msg: None |
184 vlog = lambda *msg: None |
167 |
185 |
|
186 if options.tmpdir: |
|
187 options.tmpdir = os.path.expanduser(options.tmpdir) |
|
188 |
168 if options.jobs < 1: |
189 if options.jobs < 1: |
169 print >> sys.stderr, 'ERROR: -j/--jobs must be positive' |
190 parser.error('--jobs must be positive') |
170 sys.exit(1) |
|
171 if options.interactive and options.jobs > 1: |
191 if options.interactive and options.jobs > 1: |
172 print '(--interactive overrides --jobs)' |
192 print '(--interactive overrides --jobs)' |
173 options.jobs = 1 |
193 options.jobs = 1 |
|
194 if options.interactive and options.debug: |
|
195 parser.error("-i/--interactive and -d/--debug are incompatible") |
|
196 if options.debug: |
|
197 if options.timeout != defaults['timeout']: |
|
198 sys.stderr.write( |
|
199 'warning: --timeout option ignored with --debug\n') |
|
200 options.timeout = 0 |
|
201 if options.py3k_warnings: |
|
202 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+') |
|
204 if options.blacklist: |
|
205 configparser = ConfigParser() |
|
206 configparser.read("blacklist") |
|
207 blacklist = dict() |
|
208 for section in options.blacklist: |
|
209 for (item, value) in configparser.items(section): |
|
210 blacklist["test-" + item] = section |
|
211 options.blacklist = blacklist |
174 |
212 |
175 return (options, args) |
213 return (options, args) |
176 |
214 |
177 def rename(src, dst): |
215 def rename(src, dst): |
178 """Like os.rename(), trade atomicity and opened files friendliness |
216 """Like os.rename(), trade atomicity and opened files friendliness |
211 line = line.splitlines()[0] |
249 line = line.splitlines()[0] |
212 failed.append(line[len(FAILED_PREFIX):]) |
250 failed.append(line[len(FAILED_PREFIX):]) |
213 |
251 |
214 return missing, failed |
252 return missing, failed |
215 |
253 |
216 def showdiff(expected, output): |
254 def showdiff(expected, output, ref, err): |
217 for line in difflib.unified_diff(expected, output, |
255 for line in difflib.unified_diff(expected, output, ref, err): |
218 "Expected output", "Test output"): |
|
219 sys.stdout.write(line) |
256 sys.stdout.write(line) |
220 |
257 |
221 def findprogram(program): |
258 def findprogram(program): |
222 """Search PATH for a executable program""" |
259 """Search PATH for a executable program""" |
223 for p in os.environ.get('PATH', os.defpath).split(os.pathsep): |
260 for p in os.environ.get('PATH', os.defpath).split(os.pathsep): |
264 vlog("# Performing temporary installation of HG") |
301 vlog("# Performing temporary installation of HG") |
265 installerrs = os.path.join("tests", "install.err") |
302 installerrs = os.path.join("tests", "install.err") |
266 pure = options.pure and "--pure" or "" |
303 pure = options.pure and "--pure" or "" |
267 |
304 |
268 # Run installer in hg root |
305 # Run installer in hg root |
269 os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..')) |
306 script = os.path.realpath(sys.argv[0]) |
|
307 hgroot = os.path.dirname(os.path.dirname(script)) |
|
308 os.chdir(hgroot) |
|
309 nohome = '--home=""' |
|
310 if os.name == 'nt': |
|
311 # The --home="" trick works only on OS where os.sep == '/' |
|
312 # because of a distutils convert_path() fast-path. Avoid it at |
|
313 # least on Windows for now, deal with .pydistutils.cfg bugs |
|
314 # when they happen. |
|
315 nohome = '' |
270 cmd = ('%s setup.py %s clean --all' |
316 cmd = ('%s setup.py %s clean --all' |
271 ' install --force --prefix="%s" --install-lib="%s"' |
317 ' install --force --prefix="%s" --install-lib="%s"' |
272 ' --install-scripts="%s" >%s 2>&1' |
318 ' --install-scripts="%s" %s >%s 2>&1' |
273 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, installerrs)) |
319 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, nohome, |
|
320 installerrs)) |
274 vlog("# Running", cmd) |
321 vlog("# Running", cmd) |
275 if os.system(cmd) == 0: |
322 if os.system(cmd) == 0: |
276 if not options.verbose: |
323 if not options.verbose: |
277 os.remove(installerrs) |
324 os.remove(installerrs) |
278 else: |
325 else: |
294 ' if line.startswith("diff "):\n' |
341 ' if line.startswith("diff "):\n' |
295 ' files += 1\n' |
342 ' files += 1\n' |
296 'sys.stdout.write("files patched: %d\\n" % files)\n') |
343 'sys.stdout.write("files patched: %d\\n" % files)\n') |
297 f.close() |
344 f.close() |
298 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) |
345 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) |
|
346 |
|
347 if options.py3k_warnings and not options.anycoverage: |
|
348 vlog("# Updating hg command to enable Py3k Warnings switch") |
|
349 f = open(os.path.join(BINDIR, 'hg'), 'r') |
|
350 lines = [line.rstrip() for line in f] |
|
351 lines[0] += ' -3' |
|
352 f.close() |
|
353 f = open(os.path.join(BINDIR, 'hg'), 'w') |
|
354 for line in lines: |
|
355 f.write(line + '\n') |
|
356 f.close() |
299 |
357 |
300 if options.anycoverage: |
358 if options.anycoverage: |
301 vlog("# Installing coverage wrapper") |
359 vlog("# Installing coverage wrapper") |
302 os.environ['COVERAGE_FILE'] = COVERAGE_FILE |
360 os.environ['COVERAGE_FILE'] = COVERAGE_FILE |
303 if os.path.exists(COVERAGE_FILE): |
361 if os.path.exists(COVERAGE_FILE): |
348 def alarmed(signum, frame): |
406 def alarmed(signum, frame): |
349 raise Timeout |
407 raise Timeout |
350 |
408 |
351 def run(cmd, options): |
409 def run(cmd, options): |
352 """Run command in a sub-process, capturing the output (stdout and stderr). |
410 """Run command in a sub-process, capturing the output (stdout and stderr). |
353 Return the exist code, and output.""" |
411 Return a tuple (exitcode, output). output is None in debug mode.""" |
354 # TODO: Use subprocess.Popen if we're running on Python 2.4 |
412 # TODO: Use subprocess.Popen if we're running on Python 2.4 |
|
413 if options.debug: |
|
414 proc = subprocess.Popen(cmd, shell=True) |
|
415 ret = proc.wait() |
|
416 return (ret, None) |
|
417 |
355 if os.name == 'nt' or sys.platform.startswith('java'): |
418 if os.name == 'nt' or sys.platform.startswith('java'): |
356 tochild, fromchild = os.popen4(cmd) |
419 tochild, fromchild = os.popen4(cmd) |
357 tochild.close() |
420 tochild.close() |
358 output = fromchild.read() |
421 output = fromchild.read() |
359 ret = fromchild.close() |
422 ret = fromchild.close() |
386 |
449 |
387 def skip(msg): |
450 def skip(msg): |
388 if not options.verbose: |
451 if not options.verbose: |
389 skips.append((test, msg)) |
452 skips.append((test, msg)) |
390 else: |
453 else: |
391 print "\nSkipping %s: %s" % (test, msg) |
454 print "\nSkipping %s: %s" % (testpath, msg) |
392 return None |
455 return None |
393 |
456 |
394 def fail(msg): |
457 def fail(msg): |
395 fails.append((test, msg)) |
458 fails.append((test, msg)) |
396 if not options.nodiff: |
459 if not options.nodiff: |
397 print "\nERROR: %s %s" % (test, msg) |
460 print "\nERROR: %s %s" % (testpath, msg) |
398 return None |
461 return None |
399 |
462 |
400 vlog("# Test", test) |
463 vlog("# Test", test) |
401 |
464 |
402 # create a fresh hgrc |
465 # create a fresh hgrc |
403 hgrc = file(HGRCPATH, 'w+') |
466 hgrc = open(HGRCPATH, 'w+') |
404 hgrc.write('[ui]\n') |
467 hgrc.write('[ui]\n') |
405 hgrc.write('slash = True\n') |
468 hgrc.write('slash = True\n') |
406 hgrc.write('[defaults]\n') |
469 hgrc.write('[defaults]\n') |
407 hgrc.write('backout = -d "0 0"\n') |
470 hgrc.write('backout = -d "0 0"\n') |
408 hgrc.write('commit = -d "0 0"\n') |
471 hgrc.write('commit = -d "0 0"\n') |
409 hgrc.write('tag = -d "0 0"\n') |
472 hgrc.write('tag = -d "0 0"\n') |
|
473 if options.inotify: |
|
474 hgrc.write('[extensions]\n') |
|
475 hgrc.write('inotify=\n') |
|
476 hgrc.write('[inotify]\n') |
|
477 hgrc.write('pidfile=%s\n' % DAEMON_PIDS) |
|
478 hgrc.write('appendpid=True\n') |
410 hgrc.close() |
479 hgrc.close() |
411 |
480 |
412 err = os.path.join(TESTDIR, test+".err") |
481 err = os.path.join(TESTDIR, test+".err") |
413 ref = os.path.join(TESTDIR, test+".out") |
482 ref = os.path.join(TESTDIR, test+".out") |
414 testpath = os.path.join(TESTDIR, test) |
483 testpath = os.path.join(TESTDIR, test) |
484 if ret: |
562 if ret: |
485 fail("output changed and returned error code %d" % ret) |
563 fail("output changed and returned error code %d" % ret) |
486 else: |
564 else: |
487 fail("output changed") |
565 fail("output changed") |
488 if not options.nodiff: |
566 if not options.nodiff: |
489 showdiff(refout, out) |
567 showdiff(refout, out, ref, err) |
490 ret = 1 |
568 ret = 1 |
491 elif ret: |
569 elif ret: |
492 mark = '!' |
570 mark = '!' |
493 fail("returned error code %d" % ret) |
571 fail("returned error code %d" % ret) |
494 |
572 |
495 if not options.verbose: |
573 if not options.verbose: |
496 sys.stdout.write(mark) |
574 sys.stdout.write(mark) |
497 sys.stdout.flush() |
575 sys.stdout.flush() |
498 |
576 |
499 if ret != 0 and not skipped: |
577 if ret != 0 and not skipped and not options.debug: |
500 # Save errors to a file for diagnosis |
578 # Save errors to a file for diagnosis |
501 f = open(err, "wb") |
579 f = open(err, "wb") |
502 for line in out: |
580 for line in out: |
503 f.write(line) |
581 f.write(line) |
504 f.close() |
582 f.close() |
505 |
583 |
506 # Kill off any leftover daemon processes |
584 # Kill off any leftover daemon processes |
507 try: |
585 try: |
508 fp = file(DAEMON_PIDS) |
586 fp = open(DAEMON_PIDS) |
509 for line in fp: |
587 for line in fp: |
510 try: |
588 try: |
511 pid = int(line) |
589 pid = int(line) |
512 except ValueError: |
590 except ValueError: |
513 continue |
591 continue |
714 |
813 |
715 checktools() |
814 checktools() |
716 |
815 |
717 # Reset some environment variables to well-known values so that |
816 # Reset some environment variables to well-known values so that |
718 # the tests produce repeatable output. |
817 # the tests produce repeatable output. |
719 os.environ['LANG'] = os.environ['LC_ALL'] = 'C' |
818 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C' |
720 os.environ['TZ'] = 'GMT' |
819 os.environ['TZ'] = 'GMT' |
721 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>" |
820 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>" |
722 os.environ['CDPATH'] = '' |
821 os.environ['CDPATH'] = '' |
|
822 os.environ['COLUMNS'] = '80' |
723 |
823 |
724 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE |
824 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE |
725 TESTDIR = os.environ["TESTDIR"] = os.getcwd() |
825 TESTDIR = os.environ["TESTDIR"] = os.getcwd() |
726 HGTMP = os.environ['HGTMP'] = os.path.realpath(tempfile.mkdtemp('', 'hgtests.', |
826 if options.tmpdir: |
727 options.tmpdir)) |
827 options.keep_tmpdir = True |
|
828 tmpdir = options.tmpdir |
|
829 if os.path.exists(tmpdir): |
|
830 # Meaning of tmpdir has changed since 1.3: we used to create |
|
831 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if |
|
832 # tmpdir already exists. |
|
833 sys.exit("error: temp dir %r already exists" % tmpdir) |
|
834 |
|
835 # Automatically removing tmpdir sounds convenient, but could |
|
836 # really annoy anyone in the habit of using "--tmpdir=/tmp" |
|
837 # or "--tmpdir=$HOME". |
|
838 #vlog("# Removing temp dir", tmpdir) |
|
839 #shutil.rmtree(tmpdir) |
|
840 os.makedirs(tmpdir) |
|
841 else: |
|
842 tmpdir = tempfile.mkdtemp('', 'hgtests.') |
|
843 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir) |
728 DAEMON_PIDS = None |
844 DAEMON_PIDS = None |
729 HGRCPATH = None |
845 HGRCPATH = None |
730 |
846 |
731 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' |
847 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' |
732 os.environ["HGMERGE"] = "internal:merge" |
848 os.environ["HGMERGE"] = "internal:merge" |