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()