Mercurial > evolve
comparison tests/run-tests.py @ 0:bbeef801409c
minimalistic state concept.
author | Pierre-Yves David <pierre-yves.david@logilab.fr> |
---|---|
date | Fri, 20 May 2011 16:16:34 +0200 |
parents | |
children | aa0870d093b8 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:bbeef801409c |
---|---|
1 #!/usr/bin/env python | |
2 # | |
3 # run-tests.py - Run a set of tests on Mercurial | |
4 # | |
5 # Copyright 2006 Matt Mackall <mpm@selenic.com> | |
6 # | |
7 # This software may be used and distributed according to the terms of the | |
8 # GNU General Public License version 2 or any later version. | |
9 | |
10 # Modifying this script is tricky because it has many modes: | |
11 # - serial (default) vs parallel (-jN, N > 1) | |
12 # - no coverage (default) vs coverage (-c, -C, -s) | |
13 # - temp install (default) vs specific hg script (--with-hg, --local) | |
14 # - tests are a mix of shell scripts and Python scripts | |
15 # | |
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 | |
18 # sample of test scripts. For example: | |
19 # | |
20 # 1) serial, no coverage, temp install: | |
21 # ./run-tests.py test-s* | |
22 # 2) serial, no coverage, local hg: | |
23 # ./run-tests.py --local test-s* | |
24 # 3) serial, coverage, temp install: | |
25 # ./run-tests.py -c test-s* | |
26 # 4) serial, coverage, local hg: | |
27 # ./run-tests.py -c --local test-s* # unsupported | |
28 # 5) parallel, no coverage, temp install: | |
29 # ./run-tests.py -j2 test-s* | |
30 # 6) parallel, no coverage, local hg: | |
31 # ./run-tests.py -j2 --local test-s* | |
32 # 7) parallel, coverage, temp install: | |
33 # ./run-tests.py -j2 -c test-s* # currently broken | |
34 # 8) parallel, coverage, local install: | |
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 | |
38 # | |
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 | |
41 # completes fairly quickly, includes both shell and Python scripts, and | |
42 # includes some scripts that run daemon processes.) | |
43 | |
44 from distutils import version | |
45 import difflib | |
46 import errno | |
47 import optparse | |
48 import os | |
49 import shutil | |
50 import subprocess | |
51 import signal | |
52 import sys | |
53 import tempfile | |
54 import time | |
55 import re | |
56 | |
57 closefds = os.name == 'posix' | |
58 def Popen4(cmd, bufsize=-1): | |
59 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, | |
60 close_fds=closefds, | |
61 stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
62 stderr=subprocess.STDOUT) | |
63 p.fromchild = p.stdout | |
64 p.tochild = p.stdin | |
65 p.childerr = p.stderr | |
66 return p | |
67 | |
68 # reserved exit code to skip test (used by hghave) | |
69 SKIPPED_STATUS = 80 | |
70 SKIPPED_PREFIX = 'skipped: ' | |
71 FAILED_PREFIX = 'hghave check failed: ' | |
72 PYTHON = sys.executable | |
73 IMPL_PATH = 'PYTHONPATH' | |
74 if 'java' in sys.platform: | |
75 IMPL_PATH = 'JYTHONPATH' | |
76 | |
77 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"] | |
78 | |
79 defaults = { | |
80 'jobs': ('HGTEST_JOBS', 1), | |
81 'timeout': ('HGTEST_TIMEOUT', 180), | |
82 'port': ('HGTEST_PORT', 20059), | |
83 } | |
84 | |
85 def parseargs(): | |
86 parser = optparse.OptionParser("%prog [options] [tests]") | |
87 | |
88 # keep these sorted | |
89 parser.add_option("--blacklist", action="append", | |
90 help="skip tests listed in the specified blacklist file") | |
91 parser.add_option("-C", "--annotate", action="store_true", | |
92 help="output files annotated with coverage") | |
93 parser.add_option("--child", type="int", | |
94 help="run as child process, summary to given fd") | |
95 parser.add_option("-c", "--cover", action="store_true", | |
96 help="print a test coverage report") | |
97 parser.add_option("-d", "--debug", action="store_true", | |
98 help="debug mode: write output of test scripts to console" | |
99 " rather than capturing and diff'ing it (disables timeout)") | |
100 parser.add_option("-f", "--first", action="store_true", | |
101 help="exit on the first test failure") | |
102 parser.add_option("--inotify", action="store_true", | |
103 help="enable inotify extension when running tests") | |
104 parser.add_option("-i", "--interactive", action="store_true", | |
105 help="prompt to accept changed output") | |
106 parser.add_option("-j", "--jobs", type="int", | |
107 help="number of jobs to run in parallel" | |
108 " (default: $%s or %d)" % defaults['jobs']) | |
109 parser.add_option("--keep-tmpdir", action="store_true", | |
110 help="keep temporary directory after running tests") | |
111 parser.add_option("-k", "--keywords", | |
112 help="run tests matching keywords") | |
113 parser.add_option("-l", "--local", action="store_true", | |
114 help="shortcut for --with-hg=<testdir>/../hg") | |
115 parser.add_option("-n", "--nodiff", action="store_true", | |
116 help="skip showing test changes") | |
117 parser.add_option("-p", "--port", type="int", | |
118 help="port on which servers should listen" | |
119 " (default: $%s or %d)" % defaults['port']) | |
120 parser.add_option("--pure", action="store_true", | |
121 help="use pure Python code instead of C extensions") | |
122 parser.add_option("-R", "--restart", action="store_true", | |
123 help="restart at last error") | |
124 parser.add_option("-r", "--retest", action="store_true", | |
125 help="retest failed tests") | |
126 parser.add_option("-S", "--noskips", action="store_true", | |
127 help="don't report skip tests verbosely") | |
128 parser.add_option("-t", "--timeout", type="int", | |
129 help="kill errant tests after TIMEOUT seconds" | |
130 " (default: $%s or %d)" % defaults['timeout']) | |
131 parser.add_option("--tmpdir", type="string", | |
132 help="run tests in the given temporary directory" | |
133 " (implies --keep-tmpdir)") | |
134 parser.add_option("-v", "--verbose", action="store_true", | |
135 help="output verbose messages") | |
136 parser.add_option("--view", type="string", | |
137 help="external diff viewer") | |
138 parser.add_option("--with-hg", type="string", | |
139 metavar="HG", | |
140 help="test using specified hg script rather than a " | |
141 "temporary installation") | |
142 parser.add_option("-3", "--py3k-warnings", action="store_true", | |
143 help="enable Py3k warnings on Python 2.6+") | |
144 | |
145 for option, default in defaults.items(): | |
146 defaults[option] = int(os.environ.get(*default)) | |
147 parser.set_defaults(**defaults) | |
148 (options, args) = parser.parse_args() | |
149 | |
150 # jython is always pure | |
151 if 'java' in sys.platform or '__pypy__' in sys.modules: | |
152 options.pure = True | |
153 | |
154 if options.with_hg: | |
155 if not (os.path.isfile(options.with_hg) and | |
156 os.access(options.with_hg, os.X_OK)): | |
157 parser.error('--with-hg must specify an executable hg script') | |
158 if not os.path.basename(options.with_hg) == 'hg': | |
159 sys.stderr.write('warning: --with-hg should specify an hg script') | |
160 if options.local: | |
161 testdir = os.path.dirname(os.path.realpath(sys.argv[0])) | |
162 hgbin = os.path.join(os.path.dirname(testdir), 'hg') | |
163 if not os.access(hgbin, os.X_OK): | |
164 parser.error('--local specified, but %r not found or not executable' | |
165 % hgbin) | |
166 options.with_hg = hgbin | |
167 | |
168 options.anycoverage = options.cover or options.annotate | |
169 if options.anycoverage: | |
170 try: | |
171 import coverage | |
172 covver = version.StrictVersion(coverage.__version__).version | |
173 if covver < (3, 3): | |
174 parser.error('coverage options require coverage 3.3 or later') | |
175 except ImportError: | |
176 parser.error('coverage options now require the coverage package') | |
177 | |
178 if options.anycoverage and options.local: | |
179 # this needs some path mangling somewhere, I guess | |
180 parser.error("sorry, coverage options do not work when --local " | |
181 "is specified") | |
182 | |
183 global vlog | |
184 if options.verbose: | |
185 if options.jobs > 1 or options.child is not None: | |
186 pid = "[%d]" % os.getpid() | |
187 else: | |
188 pid = None | |
189 def vlog(*msg): | |
190 if pid: | |
191 print pid, | |
192 for m in msg: | |
193 print m, | |
194 print | |
195 sys.stdout.flush() | |
196 else: | |
197 vlog = lambda *msg: None | |
198 | |
199 if options.tmpdir: | |
200 options.tmpdir = os.path.expanduser(options.tmpdir) | |
201 | |
202 if options.jobs < 1: | |
203 parser.error('--jobs must be positive') | |
204 if options.interactive and options.jobs > 1: | |
205 print '(--interactive overrides --jobs)' | |
206 options.jobs = 1 | |
207 if options.interactive and options.debug: | |
208 parser.error("-i/--interactive and -d/--debug are incompatible") | |
209 if options.debug: | |
210 if options.timeout != defaults['timeout']: | |
211 sys.stderr.write( | |
212 'warning: --timeout option ignored with --debug\n') | |
213 options.timeout = 0 | |
214 if options.py3k_warnings: | |
215 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): | |
216 parser.error('--py3k-warnings can only be used on Python 2.6+') | |
217 if options.blacklist: | |
218 blacklist = dict() | |
219 for filename in options.blacklist: | |
220 try: | |
221 path = os.path.expanduser(os.path.expandvars(filename)) | |
222 f = open(path, "r") | |
223 except IOError, err: | |
224 if err.errno != errno.ENOENT: | |
225 raise | |
226 print "warning: no such blacklist file: %s" % filename | |
227 continue | |
228 | |
229 for line in f.readlines(): | |
230 line = line.strip() | |
231 if line and not line.startswith('#'): | |
232 blacklist[line] = filename | |
233 | |
234 f.close() | |
235 | |
236 options.blacklist = blacklist | |
237 | |
238 return (options, args) | |
239 | |
240 def rename(src, dst): | |
241 """Like os.rename(), trade atomicity and opened files friendliness | |
242 for existing destination support. | |
243 """ | |
244 shutil.copy(src, dst) | |
245 os.remove(src) | |
246 | |
247 def splitnewlines(text): | |
248 '''like str.splitlines, but only split on newlines. | |
249 keep line endings.''' | |
250 i = 0 | |
251 lines = [] | |
252 while True: | |
253 n = text.find('\n', i) | |
254 if n == -1: | |
255 last = text[i:] | |
256 if last: | |
257 lines.append(last) | |
258 return lines | |
259 lines.append(text[i:n + 1]) | |
260 i = n + 1 | |
261 | |
262 def parsehghaveoutput(lines): | |
263 '''Parse hghave log lines. | |
264 Return tuple of lists (missing, failed): | |
265 * the missing/unknown features | |
266 * the features for which existence check failed''' | |
267 missing = [] | |
268 failed = [] | |
269 for line in lines: | |
270 if line.startswith(SKIPPED_PREFIX): | |
271 line = line.splitlines()[0] | |
272 missing.append(line[len(SKIPPED_PREFIX):]) | |
273 elif line.startswith(FAILED_PREFIX): | |
274 line = line.splitlines()[0] | |
275 failed.append(line[len(FAILED_PREFIX):]) | |
276 | |
277 return missing, failed | |
278 | |
279 def showdiff(expected, output, ref, err): | |
280 for line in difflib.unified_diff(expected, output, ref, err): | |
281 sys.stdout.write(line) | |
282 | |
283 def findprogram(program): | |
284 """Search PATH for a executable program""" | |
285 for p in os.environ.get('PATH', os.defpath).split(os.pathsep): | |
286 name = os.path.join(p, program) | |
287 if os.access(name, os.X_OK): | |
288 return name | |
289 return None | |
290 | |
291 def checktools(): | |
292 # Before we go any further, check for pre-requisite tools | |
293 # stuff from coreutils (cat, rm, etc) are not tested | |
294 for p in requiredtools: | |
295 if os.name == 'nt': | |
296 p += '.exe' | |
297 found = findprogram(p) | |
298 if found: | |
299 vlog("# Found prerequisite", p, "at", found) | |
300 else: | |
301 print "WARNING: Did not find prerequisite tool: "+p | |
302 | |
303 def killdaemons(): | |
304 # Kill off any leftover daemon processes | |
305 try: | |
306 fp = open(DAEMON_PIDS) | |
307 for line in fp: | |
308 try: | |
309 pid = int(line) | |
310 except ValueError: | |
311 continue | |
312 try: | |
313 os.kill(pid, 0) | |
314 vlog('# Killing daemon process %d' % pid) | |
315 os.kill(pid, signal.SIGTERM) | |
316 time.sleep(0.25) | |
317 os.kill(pid, 0) | |
318 vlog('# Daemon process %d is stuck - really killing it' % pid) | |
319 os.kill(pid, signal.SIGKILL) | |
320 except OSError, err: | |
321 if err.errno != errno.ESRCH: | |
322 raise | |
323 fp.close() | |
324 os.unlink(DAEMON_PIDS) | |
325 except IOError: | |
326 pass | |
327 | |
328 def cleanup(options): | |
329 if not options.keep_tmpdir: | |
330 vlog("# Cleaning up HGTMP", HGTMP) | |
331 shutil.rmtree(HGTMP, True) | |
332 | |
333 def usecorrectpython(): | |
334 # some tests run python interpreter. they must use same | |
335 # interpreter we use or bad things will happen. | |
336 exedir, exename = os.path.split(sys.executable) | |
337 if exename == 'python': | |
338 path = findprogram('python') | |
339 if os.path.dirname(path) == exedir: | |
340 return | |
341 vlog('# Making python executable in test path use correct Python') | |
342 mypython = os.path.join(BINDIR, 'python') | |
343 try: | |
344 os.symlink(sys.executable, mypython) | |
345 except AttributeError: | |
346 # windows fallback | |
347 shutil.copyfile(sys.executable, mypython) | |
348 shutil.copymode(sys.executable, mypython) | |
349 | |
350 def installhg(options): | |
351 vlog("# Performing temporary installation of HG") | |
352 installerrs = os.path.join("tests", "install.err") | |
353 pure = options.pure and "--pure" or "" | |
354 | |
355 # Run installer in hg root | |
356 script = os.path.realpath(sys.argv[0]) | |
357 hgroot = os.path.dirname(os.path.dirname(script)) | |
358 os.chdir(hgroot) | |
359 nohome = '--home=""' | |
360 if os.name == 'nt': | |
361 # The --home="" trick works only on OS where os.sep == '/' | |
362 # because of a distutils convert_path() fast-path. Avoid it at | |
363 # least on Windows for now, deal with .pydistutils.cfg bugs | |
364 # when they happen. | |
365 nohome = '' | |
366 cmd = ('%s setup.py %s clean --all' | |
367 ' build --build-base="%s"' | |
368 ' install --force --prefix="%s" --install-lib="%s"' | |
369 ' --install-scripts="%s" %s >%s 2>&1' | |
370 % (sys.executable, pure, os.path.join(HGTMP, "build"), | |
371 INST, PYTHONDIR, BINDIR, nohome, installerrs)) | |
372 vlog("# Running", cmd) | |
373 if os.system(cmd) == 0: | |
374 if not options.verbose: | |
375 os.remove(installerrs) | |
376 else: | |
377 f = open(installerrs) | |
378 for line in f: | |
379 print line, | |
380 f.close() | |
381 sys.exit(1) | |
382 os.chdir(TESTDIR) | |
383 | |
384 usecorrectpython() | |
385 | |
386 vlog("# Installing dummy diffstat") | |
387 f = open(os.path.join(BINDIR, 'diffstat'), 'w') | |
388 f.write('#!' + sys.executable + '\n' | |
389 'import sys\n' | |
390 'files = 0\n' | |
391 'for line in sys.stdin:\n' | |
392 ' if line.startswith("diff "):\n' | |
393 ' files += 1\n' | |
394 'sys.stdout.write("files patched: %d\\n" % files)\n') | |
395 f.close() | |
396 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) | |
397 | |
398 if options.py3k_warnings and not options.anycoverage: | |
399 vlog("# Updating hg command to enable Py3k Warnings switch") | |
400 f = open(os.path.join(BINDIR, 'hg'), 'r') | |
401 lines = [line.rstrip() for line in f] | |
402 lines[0] += ' -3' | |
403 f.close() | |
404 f = open(os.path.join(BINDIR, 'hg'), 'w') | |
405 for line in lines: | |
406 f.write(line + '\n') | |
407 f.close() | |
408 | |
409 if options.anycoverage: | |
410 custom = os.path.join(TESTDIR, 'sitecustomize.py') | |
411 target = os.path.join(PYTHONDIR, 'sitecustomize.py') | |
412 vlog('# Installing coverage trigger to %s' % target) | |
413 shutil.copyfile(custom, target) | |
414 rc = os.path.join(TESTDIR, '.coveragerc') | |
415 vlog('# Installing coverage rc to %s' % rc) | |
416 os.environ['COVERAGE_PROCESS_START'] = rc | |
417 fn = os.path.join(INST, '..', '.coverage') | |
418 os.environ['COVERAGE_FILE'] = fn | |
419 | |
420 def outputcoverage(options): | |
421 | |
422 vlog('# Producing coverage report') | |
423 os.chdir(PYTHONDIR) | |
424 | |
425 def covrun(*args): | |
426 cmd = 'coverage %s' % ' '.join(args) | |
427 vlog('# Running: %s' % cmd) | |
428 os.system(cmd) | |
429 | |
430 if options.child: | |
431 return | |
432 | |
433 covrun('-c') | |
434 omit = ','.join([BINDIR, TESTDIR]) | |
435 covrun('-i', '-r', '"--omit=%s"' % omit) # report | |
436 if options.annotate: | |
437 adir = os.path.join(TESTDIR, 'annotated') | |
438 if not os.path.isdir(adir): | |
439 os.mkdir(adir) | |
440 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit) | |
441 | |
442 class Timeout(Exception): | |
443 pass | |
444 | |
445 def alarmed(signum, frame): | |
446 raise Timeout | |
447 | |
448 def pytest(test, options, replacements): | |
449 py3kswitch = options.py3k_warnings and ' -3' or '' | |
450 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test) | |
451 vlog("# Running", cmd) | |
452 return run(cmd, options, replacements) | |
453 | |
454 def shtest(test, options, replacements): | |
455 cmd = '"%s"' % test | |
456 vlog("# Running", cmd) | |
457 return run(cmd, options, replacements) | |
458 | |
459 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search | |
460 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub | |
461 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256)) | |
462 escapemap.update({'\\': '\\\\', '\r': r'\r'}) | |
463 def escapef(m): | |
464 return escapemap[m.group(0)] | |
465 def stringescape(s): | |
466 return escapesub(escapef, s) | |
467 | |
468 def tsttest(test, options, replacements): | |
469 t = open(test) | |
470 out = [] | |
471 script = [] | |
472 salt = "SALT" + str(time.time()) | |
473 | |
474 pos = prepos = -1 | |
475 after = {} | |
476 expected = {} | |
477 for n, l in enumerate(t): | |
478 if not l.endswith('\n'): | |
479 l += '\n' | |
480 if l.startswith(' $ '): # commands | |
481 after.setdefault(pos, []).append(l) | |
482 prepos = pos | |
483 pos = n | |
484 script.append('echo %s %s $?\n' % (salt, n)) | |
485 script.append(l[4:]) | |
486 elif l.startswith(' > '): # continuations | |
487 after.setdefault(prepos, []).append(l) | |
488 script.append(l[4:]) | |
489 elif l.startswith(' '): # results | |
490 # queue up a list of expected results | |
491 expected.setdefault(pos, []).append(l[2:]) | |
492 else: | |
493 # non-command/result - queue up for merged output | |
494 after.setdefault(pos, []).append(l) | |
495 | |
496 t.close() | |
497 | |
498 script.append('echo %s %s $?\n' % (salt, n + 1)) | |
499 | |
500 fd, name = tempfile.mkstemp(suffix='hg-tst') | |
501 | |
502 try: | |
503 for l in script: | |
504 os.write(fd, l) | |
505 os.close(fd) | |
506 | |
507 cmd = '/bin/sh "%s"' % name | |
508 vlog("# Running", cmd) | |
509 exitcode, output = run(cmd, options, replacements) | |
510 # do not merge output if skipped, return hghave message instead | |
511 # similarly, with --debug, output is None | |
512 if exitcode == SKIPPED_STATUS or output is None: | |
513 return exitcode, output | |
514 finally: | |
515 os.remove(name) | |
516 | |
517 def rematch(el, l): | |
518 try: | |
519 # ensure that the regex matches to the end of the string | |
520 return re.match(el + r'\Z', l) | |
521 except re.error: | |
522 # el is an invalid regex | |
523 return False | |
524 | |
525 def globmatch(el, l): | |
526 # The only supported special characters are * and ?. Escaping is | |
527 # supported. | |
528 i, n = 0, len(el) | |
529 res = '' | |
530 while i < n: | |
531 c = el[i] | |
532 i += 1 | |
533 if c == '\\' and el[i] in '*?\\': | |
534 res += el[i - 1:i + 1] | |
535 i += 1 | |
536 elif c == '*': | |
537 res += '.*' | |
538 elif c == '?': | |
539 res += '.' | |
540 else: | |
541 res += re.escape(c) | |
542 return rematch(res, l) | |
543 | |
544 pos = -1 | |
545 postout = [] | |
546 ret = 0 | |
547 for n, l in enumerate(output): | |
548 lout, lcmd = l, None | |
549 if salt in l: | |
550 lout, lcmd = l.split(salt, 1) | |
551 | |
552 if lout: | |
553 if lcmd: | |
554 lout += ' (no-eol)\n' | |
555 | |
556 el = None | |
557 if pos in expected and expected[pos]: | |
558 el = expected[pos].pop(0) | |
559 | |
560 if el == lout: # perfect match (fast) | |
561 postout.append(" " + lout) | |
562 elif (el and | |
563 (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or | |
564 el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout) | |
565 or el.endswith(" (esc)\n") and | |
566 el.decode('string-escape') == l)): | |
567 postout.append(" " + el) # fallback regex/glob/esc match | |
568 else: | |
569 if needescape(lout): | |
570 lout = stringescape(lout.rstrip('\n')) + " (esc)\n" | |
571 postout.append(" " + lout) # let diff deal with it | |
572 | |
573 if lcmd: | |
574 # add on last return code | |
575 ret = int(lcmd.split()[1]) | |
576 if ret != 0: | |
577 postout.append(" [%s]\n" % ret) | |
578 if pos in after: | |
579 postout += after.pop(pos) | |
580 pos = int(lcmd.split()[0]) | |
581 | |
582 if pos in after: | |
583 postout += after.pop(pos) | |
584 | |
585 return exitcode, postout | |
586 | |
587 wifexited = getattr(os, "WIFEXITED", lambda x: False) | |
588 def run(cmd, options, replacements): | |
589 """Run command in a sub-process, capturing the output (stdout and stderr). | |
590 Return a tuple (exitcode, output). output is None in debug mode.""" | |
591 # TODO: Use subprocess.Popen if we're running on Python 2.4 | |
592 if options.debug: | |
593 proc = subprocess.Popen(cmd, shell=True) | |
594 ret = proc.wait() | |
595 return (ret, None) | |
596 | |
597 if os.name == 'nt' or sys.platform.startswith('java'): | |
598 tochild, fromchild = os.popen4(cmd) | |
599 tochild.close() | |
600 output = fromchild.read() | |
601 ret = fromchild.close() | |
602 if ret is None: | |
603 ret = 0 | |
604 else: | |
605 proc = Popen4(cmd) | |
606 def cleanup(): | |
607 os.kill(proc.pid, signal.SIGTERM) | |
608 ret = proc.wait() | |
609 if ret == 0: | |
610 ret = signal.SIGTERM << 8 | |
611 killdaemons() | |
612 return ret | |
613 | |
614 try: | |
615 output = '' | |
616 proc.tochild.close() | |
617 output = proc.fromchild.read() | |
618 ret = proc.wait() | |
619 if wifexited(ret): | |
620 ret = os.WEXITSTATUS(ret) | |
621 except Timeout: | |
622 vlog('# Process %d timed out - killing it' % proc.pid) | |
623 ret = cleanup() | |
624 output += ("\n### Abort: timeout after %d seconds.\n" | |
625 % options.timeout) | |
626 except KeyboardInterrupt: | |
627 vlog('# Handling keyboard interrupt') | |
628 cleanup() | |
629 raise | |
630 | |
631 for s, r in replacements: | |
632 output = re.sub(s, r, output) | |
633 return ret, splitnewlines(output) | |
634 | |
635 def runone(options, test, skips, fails): | |
636 '''tristate output: | |
637 None -> skipped | |
638 True -> passed | |
639 False -> failed''' | |
640 | |
641 def skip(msg): | |
642 if not options.verbose: | |
643 skips.append((test, msg)) | |
644 else: | |
645 print "\nSkipping %s: %s" % (testpath, msg) | |
646 return None | |
647 | |
648 def fail(msg): | |
649 fails.append((test, msg)) | |
650 if not options.nodiff: | |
651 print "\nERROR: %s %s" % (testpath, msg) | |
652 return None | |
653 | |
654 vlog("# Test", test) | |
655 | |
656 # create a fresh hgrc | |
657 hgrc = open(HGRCPATH, 'w+') | |
658 hgrc.write('[ui]\n') | |
659 hgrc.write('slash = True\n') | |
660 hgrc.write('[defaults]\n') | |
661 hgrc.write('backout = -d "0 0"\n') | |
662 hgrc.write('commit = -d "0 0"\n') | |
663 hgrc.write('tag = -d "0 0"\n') | |
664 if options.inotify: | |
665 hgrc.write('[extensions]\n') | |
666 hgrc.write('inotify=\n') | |
667 hgrc.write('[inotify]\n') | |
668 hgrc.write('pidfile=%s\n' % DAEMON_PIDS) | |
669 hgrc.write('appendpid=True\n') | |
670 hgrc.close() | |
671 | |
672 testpath = os.path.join(TESTDIR, test) | |
673 ref = os.path.join(TESTDIR, test+".out") | |
674 err = os.path.join(TESTDIR, test+".err") | |
675 if os.path.exists(err): | |
676 os.remove(err) # Remove any previous output files | |
677 try: | |
678 tf = open(testpath) | |
679 firstline = tf.readline().rstrip() | |
680 tf.close() | |
681 except: | |
682 firstline = '' | |
683 lctest = test.lower() | |
684 | |
685 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python': | |
686 runner = pytest | |
687 elif lctest.endswith('.t'): | |
688 runner = tsttest | |
689 ref = testpath | |
690 else: | |
691 # do not try to run non-executable programs | |
692 if not os.access(testpath, os.X_OK): | |
693 return skip("not executable") | |
694 runner = shtest | |
695 | |
696 # Make a tmp subdirectory to work in | |
697 testtmp = os.environ["TESTTMP"] = os.path.join(HGTMP, test) | |
698 os.mkdir(testtmp) | |
699 os.chdir(testtmp) | |
700 | |
701 if options.timeout > 0: | |
702 signal.alarm(options.timeout) | |
703 | |
704 ret, out = runner(testpath, options, [ | |
705 (re.escape(testtmp), '$TESTTMP'), | |
706 (r':%s\b' % options.port, ':$HGPORT'), | |
707 (r':%s\b' % (options.port + 1), ':$HGPORT1'), | |
708 (r':%s\b' % (options.port + 2), ':$HGPORT2'), | |
709 ]) | |
710 vlog("# Ret was:", ret) | |
711 | |
712 if options.timeout > 0: | |
713 signal.alarm(0) | |
714 | |
715 mark = '.' | |
716 | |
717 skipped = (ret == SKIPPED_STATUS) | |
718 | |
719 # If we're not in --debug mode and reference output file exists, | |
720 # check test output against it. | |
721 if options.debug: | |
722 refout = None # to match "out is None" | |
723 elif os.path.exists(ref): | |
724 f = open(ref, "r") | |
725 refout = splitnewlines(f.read()) | |
726 f.close() | |
727 else: | |
728 refout = [] | |
729 | |
730 if (ret != 0 or out != refout) and not skipped and not options.debug: | |
731 # Save errors to a file for diagnosis | |
732 f = open(err, "wb") | |
733 for line in out: | |
734 f.write(line) | |
735 f.close() | |
736 | |
737 if skipped: | |
738 mark = 's' | |
739 if out is None: # debug mode: nothing to parse | |
740 missing = ['unknown'] | |
741 failed = None | |
742 else: | |
743 missing, failed = parsehghaveoutput(out) | |
744 if not missing: | |
745 missing = ['irrelevant'] | |
746 if failed: | |
747 fail("hghave failed checking for %s" % failed[-1]) | |
748 skipped = False | |
749 else: | |
750 skip(missing[-1]) | |
751 elif out != refout: | |
752 mark = '!' | |
753 if ret: | |
754 fail("output changed and returned error code %d" % ret) | |
755 else: | |
756 fail("output changed") | |
757 if not options.nodiff: | |
758 if options.view: | |
759 os.system("%s %s %s" % (options.view, ref, err)) | |
760 else: | |
761 showdiff(refout, out, ref, err) | |
762 ret = 1 | |
763 elif ret: | |
764 mark = '!' | |
765 fail("returned error code %d" % ret) | |
766 | |
767 if not options.verbose: | |
768 sys.stdout.write(mark) | |
769 sys.stdout.flush() | |
770 | |
771 killdaemons() | |
772 | |
773 os.chdir(TESTDIR) | |
774 if not options.keep_tmpdir: | |
775 shutil.rmtree(testtmp, True) | |
776 if skipped: | |
777 return None | |
778 return ret == 0 | |
779 | |
780 _hgpath = None | |
781 | |
782 def _gethgpath(): | |
783 """Return the path to the mercurial package that is actually found by | |
784 the current Python interpreter.""" | |
785 global _hgpath | |
786 if _hgpath is not None: | |
787 return _hgpath | |
788 | |
789 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"' | |
790 pipe = os.popen(cmd % PYTHON) | |
791 try: | |
792 _hgpath = pipe.read().strip() | |
793 finally: | |
794 pipe.close() | |
795 return _hgpath | |
796 | |
797 def _checkhglib(verb): | |
798 """Ensure that the 'mercurial' package imported by python is | |
799 the one we expect it to be. If not, print a warning to stderr.""" | |
800 expecthg = os.path.join(PYTHONDIR, 'mercurial') | |
801 actualhg = _gethgpath() | |
802 if actualhg != expecthg: | |
803 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n' | |
804 ' (expected %s)\n' | |
805 % (verb, actualhg, expecthg)) | |
806 | |
807 def runchildren(options, tests): | |
808 if INST: | |
809 installhg(options) | |
810 _checkhglib("Testing") | |
811 | |
812 optcopy = dict(options.__dict__) | |
813 optcopy['jobs'] = 1 | |
814 del optcopy['blacklist'] | |
815 if optcopy['with_hg'] is None: | |
816 optcopy['with_hg'] = os.path.join(BINDIR, "hg") | |
817 optcopy.pop('anycoverage', None) | |
818 | |
819 opts = [] | |
820 for opt, value in optcopy.iteritems(): | |
821 name = '--' + opt.replace('_', '-') | |
822 if value is True: | |
823 opts.append(name) | |
824 elif value is not None: | |
825 opts.append(name + '=' + str(value)) | |
826 | |
827 tests.reverse() | |
828 jobs = [[] for j in xrange(options.jobs)] | |
829 while tests: | |
830 for job in jobs: | |
831 if not tests: | |
832 break | |
833 job.append(tests.pop()) | |
834 fps = {} | |
835 | |
836 for j, job in enumerate(jobs): | |
837 if not job: | |
838 continue | |
839 rfd, wfd = os.pipe() | |
840 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] | |
841 childtmp = os.path.join(HGTMP, 'child%d' % j) | |
842 childopts += ['--tmpdir', childtmp] | |
843 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job | |
844 vlog(' '.join(cmdline)) | |
845 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') | |
846 os.close(wfd) | |
847 signal.signal(signal.SIGINT, signal.SIG_IGN) | |
848 failures = 0 | |
849 tested, skipped, failed = 0, 0, 0 | |
850 skips = [] | |
851 fails = [] | |
852 while fps: | |
853 pid, status = os.wait() | |
854 fp = fps.pop(pid) | |
855 l = fp.read().splitlines() | |
856 try: | |
857 test, skip, fail = map(int, l[:3]) | |
858 except ValueError: | |
859 test, skip, fail = 0, 0, 0 | |
860 split = -fail or len(l) | |
861 for s in l[3:split]: | |
862 skips.append(s.split(" ", 1)) | |
863 for s in l[split:]: | |
864 fails.append(s.split(" ", 1)) | |
865 tested += test | |
866 skipped += skip | |
867 failed += fail | |
868 vlog('pid %d exited, status %d' % (pid, status)) | |
869 failures |= status | |
870 print | |
871 if not options.noskips: | |
872 for s in skips: | |
873 print "Skipped %s: %s" % (s[0], s[1]) | |
874 for s in fails: | |
875 print "Failed %s: %s" % (s[0], s[1]) | |
876 | |
877 _checkhglib("Tested") | |
878 print "# Ran %d tests, %d skipped, %d failed." % ( | |
879 tested, skipped, failed) | |
880 | |
881 if options.anycoverage: | |
882 outputcoverage(options) | |
883 sys.exit(failures != 0) | |
884 | |
885 def runtests(options, tests): | |
886 global DAEMON_PIDS, HGRCPATH | |
887 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') | |
888 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') | |
889 | |
890 try: | |
891 if INST: | |
892 installhg(options) | |
893 _checkhglib("Testing") | |
894 | |
895 if options.timeout > 0: | |
896 try: | |
897 signal.signal(signal.SIGALRM, alarmed) | |
898 vlog('# Running each test with %d second timeout' % | |
899 options.timeout) | |
900 except AttributeError: | |
901 print 'WARNING: cannot run tests with timeouts' | |
902 options.timeout = 0 | |
903 | |
904 tested = 0 | |
905 failed = 0 | |
906 skipped = 0 | |
907 | |
908 if options.restart: | |
909 orig = list(tests) | |
910 while tests: | |
911 if os.path.exists(tests[0] + ".err"): | |
912 break | |
913 tests.pop(0) | |
914 if not tests: | |
915 print "running all tests" | |
916 tests = orig | |
917 | |
918 skips = [] | |
919 fails = [] | |
920 | |
921 for test in tests: | |
922 if options.blacklist: | |
923 filename = options.blacklist.get(test) | |
924 if filename is not None: | |
925 skips.append((test, "blacklisted (%s)" % filename)) | |
926 skipped += 1 | |
927 continue | |
928 | |
929 if options.retest and not os.path.exists(test + ".err"): | |
930 skipped += 1 | |
931 continue | |
932 | |
933 if options.keywords: | |
934 fp = open(test) | |
935 t = fp.read().lower() + test.lower() | |
936 fp.close() | |
937 for k in options.keywords.lower().split(): | |
938 if k in t: | |
939 break | |
940 else: | |
941 skipped += 1 | |
942 continue | |
943 | |
944 ret = runone(options, test, skips, fails) | |
945 if ret is None: | |
946 skipped += 1 | |
947 elif not ret: | |
948 if options.interactive: | |
949 print "Accept this change? [n] ", | |
950 answer = sys.stdin.readline().strip() | |
951 if answer.lower() in "y yes".split(): | |
952 if test.endswith(".t"): | |
953 rename(test + ".err", test) | |
954 else: | |
955 rename(test + ".err", test + ".out") | |
956 tested += 1 | |
957 fails.pop() | |
958 continue | |
959 failed += 1 | |
960 if options.first: | |
961 break | |
962 tested += 1 | |
963 | |
964 if options.child: | |
965 fp = os.fdopen(options.child, 'w') | |
966 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed)) | |
967 for s in skips: | |
968 fp.write("%s %s\n" % s) | |
969 for s in fails: | |
970 fp.write("%s %s\n" % s) | |
971 fp.close() | |
972 else: | |
973 print | |
974 for s in skips: | |
975 print "Skipped %s: %s" % s | |
976 for s in fails: | |
977 print "Failed %s: %s" % s | |
978 _checkhglib("Tested") | |
979 print "# Ran %d tests, %d skipped, %d failed." % ( | |
980 tested, skipped, failed) | |
981 | |
982 if options.anycoverage: | |
983 outputcoverage(options) | |
984 except KeyboardInterrupt: | |
985 failed = True | |
986 print "\ninterrupted!" | |
987 | |
988 if failed: | |
989 sys.exit(1) | |
990 | |
991 def main(): | |
992 (options, args) = parseargs() | |
993 if not options.child: | |
994 os.umask(022) | |
995 | |
996 checktools() | |
997 | |
998 if len(args) == 0: | |
999 args = os.listdir(".") | |
1000 args.sort() | |
1001 | |
1002 tests = [] | |
1003 skipped = [] | |
1004 for test in args: | |
1005 if (test.startswith("test-") and '~' not in test and | |
1006 ('.' not in test or test.endswith('.py') or | |
1007 test.endswith('.bat') or test.endswith('.t'))): | |
1008 if not os.path.exists(test): | |
1009 skipped.append(test) | |
1010 else: | |
1011 tests.append(test) | |
1012 if not tests: | |
1013 for test in skipped: | |
1014 print 'Skipped %s: does not exist' % test | |
1015 print "# Ran 0 tests, %d skipped, 0 failed." % len(skipped) | |
1016 return | |
1017 tests = tests + skipped | |
1018 | |
1019 # Reset some environment variables to well-known values so that | |
1020 # the tests produce repeatable output. | |
1021 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C' | |
1022 os.environ['TZ'] = 'GMT' | |
1023 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>" | |
1024 os.environ['CDPATH'] = '' | |
1025 os.environ['COLUMNS'] = '80' | |
1026 os.environ['GREP_OPTIONS'] = '' | |
1027 os.environ['http_proxy'] = '' | |
1028 | |
1029 # unset env related to hooks | |
1030 for k in os.environ.keys(): | |
1031 if k.startswith('HG_'): | |
1032 # can't remove on solaris | |
1033 os.environ[k] = '' | |
1034 del os.environ[k] | |
1035 | |
1036 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE | |
1037 TESTDIR = os.environ["TESTDIR"] = os.getcwd() | |
1038 if options.tmpdir: | |
1039 options.keep_tmpdir = True | |
1040 tmpdir = options.tmpdir | |
1041 if os.path.exists(tmpdir): | |
1042 # Meaning of tmpdir has changed since 1.3: we used to create | |
1043 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if | |
1044 # tmpdir already exists. | |
1045 sys.exit("error: temp dir %r already exists" % tmpdir) | |
1046 | |
1047 # Automatically removing tmpdir sounds convenient, but could | |
1048 # really annoy anyone in the habit of using "--tmpdir=/tmp" | |
1049 # or "--tmpdir=$HOME". | |
1050 #vlog("# Removing temp dir", tmpdir) | |
1051 #shutil.rmtree(tmpdir) | |
1052 os.makedirs(tmpdir) | |
1053 else: | |
1054 tmpdir = tempfile.mkdtemp('', 'hgtests.') | |
1055 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir) | |
1056 DAEMON_PIDS = None | |
1057 HGRCPATH = None | |
1058 | |
1059 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' | |
1060 os.environ["HGMERGE"] = "internal:merge" | |
1061 os.environ["HGUSER"] = "test" | |
1062 os.environ["HGENCODING"] = "ascii" | |
1063 os.environ["HGENCODINGMODE"] = "strict" | |
1064 os.environ["HGPORT"] = str(options.port) | |
1065 os.environ["HGPORT1"] = str(options.port + 1) | |
1066 os.environ["HGPORT2"] = str(options.port + 2) | |
1067 | |
1068 if options.with_hg: | |
1069 INST = None | |
1070 BINDIR = os.path.dirname(os.path.realpath(options.with_hg)) | |
1071 | |
1072 # This looks redundant with how Python initializes sys.path from | |
1073 # the location of the script being executed. Needed because the | |
1074 # "hg" specified by --with-hg is not the only Python script | |
1075 # executed in the test suite that needs to import 'mercurial' | |
1076 # ... which means it's not really redundant at all. | |
1077 PYTHONDIR = BINDIR | |
1078 else: | |
1079 INST = os.path.join(HGTMP, "install") | |
1080 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin") | |
1081 PYTHONDIR = os.path.join(INST, "lib", "python") | |
1082 | |
1083 os.environ["BINDIR"] = BINDIR | |
1084 os.environ["PYTHON"] = PYTHON | |
1085 | |
1086 if not options.child: | |
1087 path = [BINDIR] + os.environ["PATH"].split(os.pathsep) | |
1088 os.environ["PATH"] = os.pathsep.join(path) | |
1089 | |
1090 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions | |
1091 # can run .../tests/run-tests.py test-foo where test-foo | |
1092 # adds an extension to HGRC | |
1093 pypath = [PYTHONDIR, TESTDIR] | |
1094 # We have to augment PYTHONPATH, rather than simply replacing | |
1095 # it, in case external libraries are only available via current | |
1096 # PYTHONPATH. (In particular, the Subversion bindings on OS X | |
1097 # are in /opt/subversion.) | |
1098 oldpypath = os.environ.get(IMPL_PATH) | |
1099 if oldpypath: | |
1100 pypath.append(oldpypath) | |
1101 os.environ[IMPL_PATH] = os.pathsep.join(pypath) | |
1102 | |
1103 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage") | |
1104 | |
1105 vlog("# Using TESTDIR", TESTDIR) | |
1106 vlog("# Using HGTMP", HGTMP) | |
1107 vlog("# Using PATH", os.environ["PATH"]) | |
1108 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH]) | |
1109 | |
1110 try: | |
1111 if len(tests) > 1 and options.jobs > 1: | |
1112 runchildren(options, tests) | |
1113 else: | |
1114 runtests(options, tests) | |
1115 finally: | |
1116 time.sleep(1) | |
1117 cleanup(options) | |
1118 | |
1119 if __name__ == '__main__': | |
1120 main() |