##// END OF EJS Templates
run-tests: move err file writing to tearDown()
Gregory Szorc -
r21455:0f0bace8 default
parent child Browse files
Show More
@@ -1,1703 +1,1703
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # run-tests.py - Run a set of tests on Mercurial
3 # run-tests.py - Run a set of tests on Mercurial
4 #
4 #
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
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.
8 # GNU General Public License version 2 or any later version.
9
9
10 # Modifying this script is tricky because it has many modes:
10 # Modifying this script is tricky because it has many modes:
11 # - serial (default) vs parallel (-jN, N > 1)
11 # - serial (default) vs parallel (-jN, N > 1)
12 # - no coverage (default) vs coverage (-c, -C, -s)
12 # - no coverage (default) vs coverage (-c, -C, -s)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
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:
25 # ./run-tests.py -c test-s*
25 # ./run-tests.py -c test-s*
26 # 4) serial, coverage, local hg:
26 # 4) serial, coverage, local hg:
27 # ./run-tests.py -c --local test-s* # unsupported
27 # ./run-tests.py -c --local test-s* # unsupported
28 # 5) parallel, no coverage, temp install:
28 # 5) parallel, no 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:
36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 #
38 #
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 distutils import version
44 from distutils import version
45 import difflib
45 import difflib
46 import errno
46 import errno
47 import optparse
47 import optparse
48 import os
48 import os
49 import shutil
49 import shutil
50 import subprocess
50 import subprocess
51 import signal
51 import signal
52 import sys
52 import sys
53 import tempfile
53 import tempfile
54 import time
54 import time
55 import random
55 import random
56 import re
56 import re
57 import threading
57 import threading
58 import killdaemons as killmod
58 import killdaemons as killmod
59 import Queue as queue
59 import Queue as queue
60 import unittest
60 import unittest
61
61
62 processlock = threading.Lock()
62 processlock = threading.Lock()
63
63
64 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
64 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
65 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
65 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
66 # zombies but it's pretty harmless even if we do.
66 # zombies but it's pretty harmless even if we do.
67 if sys.version_info < (2, 5):
67 if sys.version_info < (2, 5):
68 subprocess._cleanup = lambda: None
68 subprocess._cleanup = lambda: None
69
69
70 closefds = os.name == 'posix'
70 closefds = os.name == 'posix'
71 def Popen4(cmd, wd, timeout, env=None):
71 def Popen4(cmd, wd, timeout, env=None):
72 processlock.acquire()
72 processlock.acquire()
73 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
73 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
74 close_fds=closefds,
74 close_fds=closefds,
75 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
75 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
76 stderr=subprocess.STDOUT)
76 stderr=subprocess.STDOUT)
77 processlock.release()
77 processlock.release()
78
78
79 p.fromchild = p.stdout
79 p.fromchild = p.stdout
80 p.tochild = p.stdin
80 p.tochild = p.stdin
81 p.childerr = p.stderr
81 p.childerr = p.stderr
82
82
83 p.timeout = False
83 p.timeout = False
84 if timeout:
84 if timeout:
85 def t():
85 def t():
86 start = time.time()
86 start = time.time()
87 while time.time() - start < timeout and p.returncode is None:
87 while time.time() - start < timeout and p.returncode is None:
88 time.sleep(.1)
88 time.sleep(.1)
89 p.timeout = True
89 p.timeout = True
90 if p.returncode is None:
90 if p.returncode is None:
91 terminate(p)
91 terminate(p)
92 threading.Thread(target=t).start()
92 threading.Thread(target=t).start()
93
93
94 return p
94 return p
95
95
96 PYTHON = sys.executable.replace('\\', '/')
96 PYTHON = sys.executable.replace('\\', '/')
97 IMPL_PATH = 'PYTHONPATH'
97 IMPL_PATH = 'PYTHONPATH'
98 if 'java' in sys.platform:
98 if 'java' in sys.platform:
99 IMPL_PATH = 'JYTHONPATH'
99 IMPL_PATH = 'JYTHONPATH'
100
100
101 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
101 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
102
102
103 defaults = {
103 defaults = {
104 'jobs': ('HGTEST_JOBS', 1),
104 'jobs': ('HGTEST_JOBS', 1),
105 'timeout': ('HGTEST_TIMEOUT', 180),
105 'timeout': ('HGTEST_TIMEOUT', 180),
106 'port': ('HGTEST_PORT', 20059),
106 'port': ('HGTEST_PORT', 20059),
107 'shell': ('HGTEST_SHELL', 'sh'),
107 'shell': ('HGTEST_SHELL', 'sh'),
108 }
108 }
109
109
110 def parselistfiles(files, listtype, warn=True):
110 def parselistfiles(files, listtype, warn=True):
111 entries = dict()
111 entries = dict()
112 for filename in files:
112 for filename in files:
113 try:
113 try:
114 path = os.path.expanduser(os.path.expandvars(filename))
114 path = os.path.expanduser(os.path.expandvars(filename))
115 f = open(path, "r")
115 f = open(path, "r")
116 except IOError, err:
116 except IOError, err:
117 if err.errno != errno.ENOENT:
117 if err.errno != errno.ENOENT:
118 raise
118 raise
119 if warn:
119 if warn:
120 print "warning: no such %s file: %s" % (listtype, filename)
120 print "warning: no such %s file: %s" % (listtype, filename)
121 continue
121 continue
122
122
123 for line in f.readlines():
123 for line in f.readlines():
124 line = line.split('#', 1)[0].strip()
124 line = line.split('#', 1)[0].strip()
125 if line:
125 if line:
126 entries[line] = filename
126 entries[line] = filename
127
127
128 f.close()
128 f.close()
129 return entries
129 return entries
130
130
131 def getparser():
131 def getparser():
132 """Obtain the OptionParser used by the CLI."""
132 """Obtain the OptionParser used by the CLI."""
133 parser = optparse.OptionParser("%prog [options] [tests]")
133 parser = optparse.OptionParser("%prog [options] [tests]")
134
134
135 # keep these sorted
135 # keep these sorted
136 parser.add_option("--blacklist", action="append",
136 parser.add_option("--blacklist", action="append",
137 help="skip tests listed in the specified blacklist file")
137 help="skip tests listed in the specified blacklist file")
138 parser.add_option("--whitelist", action="append",
138 parser.add_option("--whitelist", action="append",
139 help="always run tests listed in the specified whitelist file")
139 help="always run tests listed in the specified whitelist file")
140 parser.add_option("--changed", type="string",
140 parser.add_option("--changed", type="string",
141 help="run tests that are changed in parent rev or working directory")
141 help="run tests that are changed in parent rev or working directory")
142 parser.add_option("-C", "--annotate", action="store_true",
142 parser.add_option("-C", "--annotate", action="store_true",
143 help="output files annotated with coverage")
143 help="output files annotated with coverage")
144 parser.add_option("-c", "--cover", action="store_true",
144 parser.add_option("-c", "--cover", action="store_true",
145 help="print a test coverage report")
145 help="print a test coverage report")
146 parser.add_option("-d", "--debug", action="store_true",
146 parser.add_option("-d", "--debug", action="store_true",
147 help="debug mode: write output of test scripts to console"
147 help="debug mode: write output of test scripts to console"
148 " rather than capturing and diffing it (disables timeout)")
148 " rather than capturing and diffing it (disables timeout)")
149 parser.add_option("-f", "--first", action="store_true",
149 parser.add_option("-f", "--first", action="store_true",
150 help="exit on the first test failure")
150 help="exit on the first test failure")
151 parser.add_option("-H", "--htmlcov", action="store_true",
151 parser.add_option("-H", "--htmlcov", action="store_true",
152 help="create an HTML report of the coverage of the files")
152 help="create an HTML report of the coverage of the files")
153 parser.add_option("-i", "--interactive", action="store_true",
153 parser.add_option("-i", "--interactive", action="store_true",
154 help="prompt to accept changed output")
154 help="prompt to accept changed output")
155 parser.add_option("-j", "--jobs", type="int",
155 parser.add_option("-j", "--jobs", type="int",
156 help="number of jobs to run in parallel"
156 help="number of jobs to run in parallel"
157 " (default: $%s or %d)" % defaults['jobs'])
157 " (default: $%s or %d)" % defaults['jobs'])
158 parser.add_option("--keep-tmpdir", action="store_true",
158 parser.add_option("--keep-tmpdir", action="store_true",
159 help="keep temporary directory after running tests")
159 help="keep temporary directory after running tests")
160 parser.add_option("-k", "--keywords",
160 parser.add_option("-k", "--keywords",
161 help="run tests matching keywords")
161 help="run tests matching keywords")
162 parser.add_option("-l", "--local", action="store_true",
162 parser.add_option("-l", "--local", action="store_true",
163 help="shortcut for --with-hg=<testdir>/../hg")
163 help="shortcut for --with-hg=<testdir>/../hg")
164 parser.add_option("--loop", action="store_true",
164 parser.add_option("--loop", action="store_true",
165 help="loop tests repeatedly")
165 help="loop tests repeatedly")
166 parser.add_option("-n", "--nodiff", action="store_true",
166 parser.add_option("-n", "--nodiff", action="store_true",
167 help="skip showing test changes")
167 help="skip showing test changes")
168 parser.add_option("-p", "--port", type="int",
168 parser.add_option("-p", "--port", type="int",
169 help="port on which servers should listen"
169 help="port on which servers should listen"
170 " (default: $%s or %d)" % defaults['port'])
170 " (default: $%s or %d)" % defaults['port'])
171 parser.add_option("--compiler", type="string",
171 parser.add_option("--compiler", type="string",
172 help="compiler to build with")
172 help="compiler to build with")
173 parser.add_option("--pure", action="store_true",
173 parser.add_option("--pure", action="store_true",
174 help="use pure Python code instead of C extensions")
174 help="use pure Python code instead of C extensions")
175 parser.add_option("-R", "--restart", action="store_true",
175 parser.add_option("-R", "--restart", action="store_true",
176 help="restart at last error")
176 help="restart at last error")
177 parser.add_option("-r", "--retest", action="store_true",
177 parser.add_option("-r", "--retest", action="store_true",
178 help="retest failed tests")
178 help="retest failed tests")
179 parser.add_option("-S", "--noskips", action="store_true",
179 parser.add_option("-S", "--noskips", action="store_true",
180 help="don't report skip tests verbosely")
180 help="don't report skip tests verbosely")
181 parser.add_option("--shell", type="string",
181 parser.add_option("--shell", type="string",
182 help="shell to use (default: $%s or %s)" % defaults['shell'])
182 help="shell to use (default: $%s or %s)" % defaults['shell'])
183 parser.add_option("-t", "--timeout", type="int",
183 parser.add_option("-t", "--timeout", type="int",
184 help="kill errant tests after TIMEOUT seconds"
184 help="kill errant tests after TIMEOUT seconds"
185 " (default: $%s or %d)" % defaults['timeout'])
185 " (default: $%s or %d)" % defaults['timeout'])
186 parser.add_option("--time", action="store_true",
186 parser.add_option("--time", action="store_true",
187 help="time how long each test takes")
187 help="time how long each test takes")
188 parser.add_option("--tmpdir", type="string",
188 parser.add_option("--tmpdir", type="string",
189 help="run tests in the given temporary directory"
189 help="run tests in the given temporary directory"
190 " (implies --keep-tmpdir)")
190 " (implies --keep-tmpdir)")
191 parser.add_option("--unittest", action="store_true",
191 parser.add_option("--unittest", action="store_true",
192 help="run tests with Python's unittest package"
192 help="run tests with Python's unittest package"
193 " (this is an experimental feature)")
193 " (this is an experimental feature)")
194 parser.add_option("-v", "--verbose", action="store_true",
194 parser.add_option("-v", "--verbose", action="store_true",
195 help="output verbose messages")
195 help="output verbose messages")
196 parser.add_option("--view", type="string",
196 parser.add_option("--view", type="string",
197 help="external diff viewer")
197 help="external diff viewer")
198 parser.add_option("--with-hg", type="string",
198 parser.add_option("--with-hg", type="string",
199 metavar="HG",
199 metavar="HG",
200 help="test using specified hg script rather than a "
200 help="test using specified hg script rather than a "
201 "temporary installation")
201 "temporary installation")
202 parser.add_option("-3", "--py3k-warnings", action="store_true",
202 parser.add_option("-3", "--py3k-warnings", action="store_true",
203 help="enable Py3k warnings on Python 2.6+")
203 help="enable Py3k warnings on Python 2.6+")
204 parser.add_option('--extra-config-opt', action="append",
204 parser.add_option('--extra-config-opt', action="append",
205 help='set the given config opt in the test hgrc')
205 help='set the given config opt in the test hgrc')
206 parser.add_option('--random', action="store_true",
206 parser.add_option('--random', action="store_true",
207 help='run tests in random order')
207 help='run tests in random order')
208
208
209 for option, (envvar, default) in defaults.items():
209 for option, (envvar, default) in defaults.items():
210 defaults[option] = type(default)(os.environ.get(envvar, default))
210 defaults[option] = type(default)(os.environ.get(envvar, default))
211 parser.set_defaults(**defaults)
211 parser.set_defaults(**defaults)
212
212
213 return parser
213 return parser
214
214
215 def parseargs(args, parser):
215 def parseargs(args, parser):
216 """Parse arguments with our OptionParser and validate results."""
216 """Parse arguments with our OptionParser and validate results."""
217 (options, args) = parser.parse_args(args)
217 (options, args) = parser.parse_args(args)
218
218
219 # jython is always pure
219 # jython is always pure
220 if 'java' in sys.platform or '__pypy__' in sys.modules:
220 if 'java' in sys.platform or '__pypy__' in sys.modules:
221 options.pure = True
221 options.pure = True
222
222
223 if options.with_hg:
223 if options.with_hg:
224 options.with_hg = os.path.expanduser(options.with_hg)
224 options.with_hg = os.path.expanduser(options.with_hg)
225 if not (os.path.isfile(options.with_hg) and
225 if not (os.path.isfile(options.with_hg) and
226 os.access(options.with_hg, os.X_OK)):
226 os.access(options.with_hg, os.X_OK)):
227 parser.error('--with-hg must specify an executable hg script')
227 parser.error('--with-hg must specify an executable hg script')
228 if not os.path.basename(options.with_hg) == 'hg':
228 if not os.path.basename(options.with_hg) == 'hg':
229 sys.stderr.write('warning: --with-hg should specify an hg script\n')
229 sys.stderr.write('warning: --with-hg should specify an hg script\n')
230 if options.local:
230 if options.local:
231 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
231 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
232 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
232 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
233 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
233 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
234 parser.error('--local specified, but %r not found or not executable'
234 parser.error('--local specified, but %r not found or not executable'
235 % hgbin)
235 % hgbin)
236 options.with_hg = hgbin
236 options.with_hg = hgbin
237
237
238 options.anycoverage = options.cover or options.annotate or options.htmlcov
238 options.anycoverage = options.cover or options.annotate or options.htmlcov
239 if options.anycoverage:
239 if options.anycoverage:
240 try:
240 try:
241 import coverage
241 import coverage
242 covver = version.StrictVersion(coverage.__version__).version
242 covver = version.StrictVersion(coverage.__version__).version
243 if covver < (3, 3):
243 if covver < (3, 3):
244 parser.error('coverage options require coverage 3.3 or later')
244 parser.error('coverage options require coverage 3.3 or later')
245 except ImportError:
245 except ImportError:
246 parser.error('coverage options now require the coverage package')
246 parser.error('coverage options now require the coverage package')
247
247
248 if options.anycoverage and options.local:
248 if options.anycoverage and options.local:
249 # this needs some path mangling somewhere, I guess
249 # this needs some path mangling somewhere, I guess
250 parser.error("sorry, coverage options do not work when --local "
250 parser.error("sorry, coverage options do not work when --local "
251 "is specified")
251 "is specified")
252
252
253 global verbose
253 global verbose
254 if options.verbose:
254 if options.verbose:
255 verbose = ''
255 verbose = ''
256
256
257 if options.tmpdir:
257 if options.tmpdir:
258 options.tmpdir = os.path.expanduser(options.tmpdir)
258 options.tmpdir = os.path.expanduser(options.tmpdir)
259
259
260 if options.jobs < 1:
260 if options.jobs < 1:
261 parser.error('--jobs must be positive')
261 parser.error('--jobs must be positive')
262 if options.interactive and options.debug:
262 if options.interactive and options.debug:
263 parser.error("-i/--interactive and -d/--debug are incompatible")
263 parser.error("-i/--interactive and -d/--debug are incompatible")
264 if options.debug:
264 if options.debug:
265 if options.timeout != defaults['timeout']:
265 if options.timeout != defaults['timeout']:
266 sys.stderr.write(
266 sys.stderr.write(
267 'warning: --timeout option ignored with --debug\n')
267 'warning: --timeout option ignored with --debug\n')
268 options.timeout = 0
268 options.timeout = 0
269 if options.py3k_warnings:
269 if options.py3k_warnings:
270 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
270 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
271 parser.error('--py3k-warnings can only be used on Python 2.6+')
271 parser.error('--py3k-warnings can only be used on Python 2.6+')
272 if options.blacklist:
272 if options.blacklist:
273 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
273 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
274 if options.whitelist:
274 if options.whitelist:
275 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
275 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
276 else:
276 else:
277 options.whitelisted = {}
277 options.whitelisted = {}
278
278
279 return (options, args)
279 return (options, args)
280
280
281 def rename(src, dst):
281 def rename(src, dst):
282 """Like os.rename(), trade atomicity and opened files friendliness
282 """Like os.rename(), trade atomicity and opened files friendliness
283 for existing destination support.
283 for existing destination support.
284 """
284 """
285 shutil.copy(src, dst)
285 shutil.copy(src, dst)
286 os.remove(src)
286 os.remove(src)
287
287
288 def showdiff(expected, output, ref, err):
288 def showdiff(expected, output, ref, err):
289 print
289 print
290 servefail = False
290 servefail = False
291 for line in difflib.unified_diff(expected, output, ref, err):
291 for line in difflib.unified_diff(expected, output, ref, err):
292 sys.stdout.write(line)
292 sys.stdout.write(line)
293 if not servefail and line.startswith(
293 if not servefail and line.startswith(
294 '+ abort: child process failed to start'):
294 '+ abort: child process failed to start'):
295 servefail = True
295 servefail = True
296 return {'servefail': servefail}
296 return {'servefail': servefail}
297
297
298
298
299 verbose = False
299 verbose = False
300 def vlog(*msg):
300 def vlog(*msg):
301 if verbose is not False:
301 if verbose is not False:
302 iolock.acquire()
302 iolock.acquire()
303 if verbose:
303 if verbose:
304 print verbose,
304 print verbose,
305 for m in msg:
305 for m in msg:
306 print m,
306 print m,
307 print
307 print
308 sys.stdout.flush()
308 sys.stdout.flush()
309 iolock.release()
309 iolock.release()
310
310
311 def log(*msg):
311 def log(*msg):
312 iolock.acquire()
312 iolock.acquire()
313 if verbose:
313 if verbose:
314 print verbose,
314 print verbose,
315 for m in msg:
315 for m in msg:
316 print m,
316 print m,
317 print
317 print
318 sys.stdout.flush()
318 sys.stdout.flush()
319 iolock.release()
319 iolock.release()
320
320
321 def terminate(proc):
321 def terminate(proc):
322 """Terminate subprocess (with fallback for Python versions < 2.6)"""
322 """Terminate subprocess (with fallback for Python versions < 2.6)"""
323 vlog('# Terminating process %d' % proc.pid)
323 vlog('# Terminating process %d' % proc.pid)
324 try:
324 try:
325 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
325 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
326 except OSError:
326 except OSError:
327 pass
327 pass
328
328
329 def killdaemons(pidfile):
329 def killdaemons(pidfile):
330 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
330 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
331 logfn=vlog)
331 logfn=vlog)
332
332
333 class Test(object):
333 class Test(object):
334 """Encapsulates a single, runnable test.
334 """Encapsulates a single, runnable test.
335
335
336 Test instances can be run multiple times via run(). However, multiple
336 Test instances can be run multiple times via run(). However, multiple
337 runs cannot be run concurrently.
337 runs cannot be run concurrently.
338 """
338 """
339
339
340 # Status code reserved for skipped tests (used by hghave).
340 # Status code reserved for skipped tests (used by hghave).
341 SKIPPED_STATUS = 80
341 SKIPPED_STATUS = 80
342
342
343 def __init__(self, runner, test, count, refpath, unittest=False):
343 def __init__(self, runner, test, count, refpath, unittest=False):
344 path = os.path.join(runner.testdir, test)
344 path = os.path.join(runner.testdir, test)
345 errpath = os.path.join(runner.testdir, '%s.err' % test)
345 errpath = os.path.join(runner.testdir, '%s.err' % test)
346
346
347 self.name = test
347 self.name = test
348
348
349 self._runner = runner
349 self._runner = runner
350 self._testdir = runner.testdir
350 self._testdir = runner.testdir
351 self._path = path
351 self._path = path
352 self._options = runner.options
352 self._options = runner.options
353 self._count = count
353 self._count = count
354 self._daemonpids = []
354 self._daemonpids = []
355 self._refpath = refpath
355 self._refpath = refpath
356 self._errpath = errpath
356 self._errpath = errpath
357 self._unittest = unittest
357 self._unittest = unittest
358
358
359 self._finished = None
359 self._finished = None
360 self._ret = None
360 self._ret = None
361 self._out = None
361 self._out = None
362 self._duration = None
362 self._duration = None
363 self._result = None
363 self._result = None
364 self._skipped = None
364 self._skipped = None
365 self._testtmp = None
365 self._testtmp = None
366
366
367 # If we're not in --debug mode and reference output file exists,
367 # If we're not in --debug mode and reference output file exists,
368 # check test output against it.
368 # check test output against it.
369 if runner.options.debug:
369 if runner.options.debug:
370 self._refout = None # to match "out is None"
370 self._refout = None # to match "out is None"
371 elif os.path.exists(refpath):
371 elif os.path.exists(refpath):
372 f = open(refpath, 'r')
372 f = open(refpath, 'r')
373 self._refout = f.read().splitlines(True)
373 self._refout = f.read().splitlines(True)
374 f.close()
374 f.close()
375 else:
375 else:
376 self._refout = []
376 self._refout = []
377
377
378 self._threadtmp = os.path.join(runner.hgtmp, 'child%d' % count)
378 self._threadtmp = os.path.join(runner.hgtmp, 'child%d' % count)
379 os.mkdir(self._threadtmp)
379 os.mkdir(self._threadtmp)
380
380
381 def cleanup(self):
381 def cleanup(self):
382 for entry in self._daemonpids:
382 for entry in self._daemonpids:
383 killdaemons(entry)
383 killdaemons(entry)
384
384
385 if self._threadtmp and not self._options.keep_tmpdir:
385 if self._threadtmp and not self._options.keep_tmpdir:
386 shutil.rmtree(self._threadtmp, True)
386 shutil.rmtree(self._threadtmp, True)
387
387
388 def setUp(self):
388 def setUp(self):
389 """Tasks to perform before run()."""
389 """Tasks to perform before run()."""
390 self._finished = False
390 self._finished = False
391 self._ret = None
391 self._ret = None
392 self._out = None
392 self._out = None
393 self._duration = None
393 self._duration = None
394 self._result = None
394 self._result = None
395 self._skipped = None
395 self._skipped = None
396
396
397 self._testtmp = os.path.join(self._threadtmp,
397 self._testtmp = os.path.join(self._threadtmp,
398 os.path.basename(self._path))
398 os.path.basename(self._path))
399 os.mkdir(self._testtmp)
399 os.mkdir(self._testtmp)
400
400
401 def run(self):
401 def run(self):
402 """Run this test instance.
402 """Run this test instance.
403
403
404 This will return a tuple describing the result of the test.
404 This will return a tuple describing the result of the test.
405 """
405 """
406 if not self._unittest:
406 if not self._unittest:
407 self.setUp()
407 self.setUp()
408
408
409 if not os.path.exists(self._path):
409 if not os.path.exists(self._path):
410 return self.skip("Doesn't exist")
410 return self.skip("Doesn't exist")
411
411
412 options = self._options
412 options = self._options
413 if not (options.whitelisted and self.name in options.whitelisted):
413 if not (options.whitelisted and self.name in options.whitelisted):
414 if options.blacklist and self.name in options.blacklist:
414 if options.blacklist and self.name in options.blacklist:
415 return self.skip('blacklisted')
415 return self.skip('blacklisted')
416
416
417 if options.retest and not os.path.exists('%s.err' % self.name):
417 if options.retest and not os.path.exists('%s.err' % self.name):
418 return self.ignore('not retesting')
418 return self.ignore('not retesting')
419
419
420 if options.keywords:
420 if options.keywords:
421 f = open(self.name)
421 f = open(self.name)
422 t = f.read().lower() + self.name.lower()
422 t = f.read().lower() + self.name.lower()
423 f.close()
423 f.close()
424 for k in options.keywords.lower().split():
424 for k in options.keywords.lower().split():
425 if k in t:
425 if k in t:
426 break
426 break
427 else:
427 else:
428 return self.ignore("doesn't match keyword")
428 return self.ignore("doesn't match keyword")
429
429
430 if not os.path.basename(self.name.lower()).startswith('test-'):
430 if not os.path.basename(self.name.lower()).startswith('test-'):
431 return self.skip('not a test file')
431 return self.skip('not a test file')
432
432
433 # Remove any previous output files.
433 # Remove any previous output files.
434 if os.path.exists(self._errpath):
434 if os.path.exists(self._errpath):
435 os.remove(self._errpath)
435 os.remove(self._errpath)
436
436
437 replacements, port = self._getreplacements()
437 replacements, port = self._getreplacements()
438 env = self._getenv(port)
438 env = self._getenv(port)
439 self._daemonpids.append(env['DAEMON_PIDS'])
439 self._daemonpids.append(env['DAEMON_PIDS'])
440 self._createhgrc(env['HGRCPATH'])
440 self._createhgrc(env['HGRCPATH'])
441
441
442 vlog('# Test', self.name)
442 vlog('# Test', self.name)
443
443
444 starttime = time.time()
444 starttime = time.time()
445 try:
445 try:
446 ret, out = self._run(replacements, env)
446 ret, out = self._run(replacements, env)
447 self._duration = time.time() - starttime
447 self._duration = time.time() - starttime
448 self._finished = True
448 self._finished = True
449 self._ret = ret
449 self._ret = ret
450 self._out = out
450 self._out = out
451 except KeyboardInterrupt:
451 except KeyboardInterrupt:
452 self._duration = time.time() - starttime
452 self._duration = time.time() - starttime
453 log('INTERRUPTED: %s (after %d seconds)' % (self.name,
453 log('INTERRUPTED: %s (after %d seconds)' % (self.name,
454 self._duration))
454 self._duration))
455 raise
455 raise
456 except Exception, e:
456 except Exception, e:
457 return self.fail('Exception during execution: %s' % e, 255)
457 return self.fail('Exception during execution: %s' % e, 255)
458
458
459 killdaemons(env['DAEMON_PIDS'])
459 killdaemons(env['DAEMON_PIDS'])
460
460
461 def describe(ret):
461 def describe(ret):
462 if ret < 0:
462 if ret < 0:
463 return 'killed by signal: %d' % -ret
463 return 'killed by signal: %d' % -ret
464 return 'returned error code %d' % ret
464 return 'returned error code %d' % ret
465
465
466 self._skipped = False
466 self._skipped = False
467
467
468 if ret == self.SKIPPED_STATUS:
468 if ret == self.SKIPPED_STATUS:
469 if out is None: # Debug mode, nothing to parse.
469 if out is None: # Debug mode, nothing to parse.
470 missing = ['unknown']
470 missing = ['unknown']
471 failed = None
471 failed = None
472 else:
472 else:
473 missing, failed = TTest.parsehghaveoutput(out)
473 missing, failed = TTest.parsehghaveoutput(out)
474
474
475 if not missing:
475 if not missing:
476 missing = ['irrelevant']
476 missing = ['irrelevant']
477
477
478 if failed:
478 if failed:
479 self._result = self.fail('hg have failed checking for %s' %
479 self._result = self.fail('hg have failed checking for %s' %
480 failed[-1], ret)
480 failed[-1], ret)
481 else:
481 else:
482 self._skipped = True
482 self._skipped = True
483 self._result = self.skip(missing[-1])
483 self._result = self.skip(missing[-1])
484 elif ret == 'timeout':
484 elif ret == 'timeout':
485 self._result = self.fail('timed out', ret)
485 self._result = self.fail('timed out', ret)
486 elif out != self._refout:
486 elif out != self._refout:
487 info = {}
487 info = {}
488 if not options.nodiff:
488 if not options.nodiff:
489 iolock.acquire()
489 iolock.acquire()
490 if options.view:
490 if options.view:
491 os.system("%s %s %s" % (options.view, self._refpath,
491 os.system("%s %s %s" % (options.view, self._refpath,
492 self._errpath))
492 self._errpath))
493 else:
493 else:
494 info = showdiff(self._refout, out, self._refpath,
494 info = showdiff(self._refout, out, self._refpath,
495 self._errpath)
495 self._errpath)
496 iolock.release()
496 iolock.release()
497 msg = ''
497 msg = ''
498 if info.get('servefail'):
498 if info.get('servefail'):
499 msg += 'serve failed and '
499 msg += 'serve failed and '
500 if ret:
500 if ret:
501 msg += 'output changed and ' + describe(ret)
501 msg += 'output changed and ' + describe(ret)
502 else:
502 else:
503 msg += 'output changed'
503 msg += 'output changed'
504
504
505 if (ret != 0 or out != self._refout) and not self._skipped \
505 if (ret != 0 or out != self._refout) and not self._skipped \
506 and not options.debug:
506 and not options.debug:
507 f = open(self._errpath, 'wb')
507 f = open(self._errpath, 'wb')
508 for line in out:
508 for line in out:
509 f.write(line)
509 f.write(line)
510 f.close()
510 f.close()
511
511
512 self._result = self.fail(msg, ret)
512 self._result = self.fail(msg, ret)
513 elif ret:
513 elif ret:
514 self._result = self.fail(describe(ret), ret)
514 self._result = self.fail(describe(ret), ret)
515 else:
515 else:
516 self._result = self.success()
516 self._result = self.success()
517
517
518 if (ret != 0 or out != self._refout) and not self._skipped \
519 and not options.debug:
520 f = open(self._errpath, 'wb')
521 for line in out:
522 f.write(line)
523 f.close()
524
525 if not self._unittest:
518 if not self._unittest:
526 self.tearDown()
519 self.tearDown()
527
520
528 return self._result
521 return self._result
529
522
530 def tearDown(self):
523 def tearDown(self):
531 """Tasks to perform after run()."""
524 """Tasks to perform after run()."""
532 if not self._options.keep_tmpdir:
525 if not self._options.keep_tmpdir:
533 shutil.rmtree(self._testtmp)
526 shutil.rmtree(self._testtmp)
534
527
528 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
529 and not self._options.debug and self._out:
530 f = open(self._errpath, 'wb')
531 for line in self._out:
532 f.write(line)
533 f.close()
534
535 vlog("# Ret was:", self._ret)
535 vlog("# Ret was:", self._ret)
536
536
537 # Don't print progress in unittest mode because that is handled
537 # Don't print progress in unittest mode because that is handled
538 # by TestResult.
538 # by TestResult.
539 if not self._options.verbose and not self._unittest:
539 if not self._options.verbose and not self._unittest:
540 iolock.acquire()
540 iolock.acquire()
541 sys.stdout.write(self._result[0])
541 sys.stdout.write(self._result[0])
542 sys.stdout.flush()
542 sys.stdout.flush()
543 iolock.release()
543 iolock.release()
544
544
545 self._runner.times.append((self.name, self._duration))
545 self._runner.times.append((self.name, self._duration))
546
546
547 def _run(self, replacements, env):
547 def _run(self, replacements, env):
548 # This should be implemented in child classes to run tests.
548 # This should be implemented in child classes to run tests.
549 return self._skip('unknown test type')
549 return self._skip('unknown test type')
550
550
551 def _getreplacements(self):
551 def _getreplacements(self):
552 port = self._options.port + self._count * 3
552 port = self._options.port + self._count * 3
553 r = [
553 r = [
554 (r':%s\b' % port, ':$HGPORT'),
554 (r':%s\b' % port, ':$HGPORT'),
555 (r':%s\b' % (port + 1), ':$HGPORT1'),
555 (r':%s\b' % (port + 1), ':$HGPORT1'),
556 (r':%s\b' % (port + 2), ':$HGPORT2'),
556 (r':%s\b' % (port + 2), ':$HGPORT2'),
557 ]
557 ]
558
558
559 if os.name == 'nt':
559 if os.name == 'nt':
560 r.append(
560 r.append(
561 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
561 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
562 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
562 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
563 for c in self._testtmp), '$TESTTMP'))
563 for c in self._testtmp), '$TESTTMP'))
564 else:
564 else:
565 r.append((re.escape(self._testtmp), '$TESTTMP'))
565 r.append((re.escape(self._testtmp), '$TESTTMP'))
566
566
567 return r, port
567 return r, port
568
568
569 def _getenv(self, port):
569 def _getenv(self, port):
570 env = os.environ.copy()
570 env = os.environ.copy()
571 env['TESTTMP'] = self._testtmp
571 env['TESTTMP'] = self._testtmp
572 env['HOME'] = self._testtmp
572 env['HOME'] = self._testtmp
573 env["HGPORT"] = str(port)
573 env["HGPORT"] = str(port)
574 env["HGPORT1"] = str(port + 1)
574 env["HGPORT1"] = str(port + 1)
575 env["HGPORT2"] = str(port + 2)
575 env["HGPORT2"] = str(port + 2)
576 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
576 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
577 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
577 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
578 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
578 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
579 env["HGMERGE"] = "internal:merge"
579 env["HGMERGE"] = "internal:merge"
580 env["HGUSER"] = "test"
580 env["HGUSER"] = "test"
581 env["HGENCODING"] = "ascii"
581 env["HGENCODING"] = "ascii"
582 env["HGENCODINGMODE"] = "strict"
582 env["HGENCODINGMODE"] = "strict"
583
583
584 # Reset some environment variables to well-known values so that
584 # Reset some environment variables to well-known values so that
585 # the tests produce repeatable output.
585 # the tests produce repeatable output.
586 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
586 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
587 env['TZ'] = 'GMT'
587 env['TZ'] = 'GMT'
588 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
588 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
589 env['COLUMNS'] = '80'
589 env['COLUMNS'] = '80'
590 env['TERM'] = 'xterm'
590 env['TERM'] = 'xterm'
591
591
592 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
592 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
593 'NO_PROXY').split():
593 'NO_PROXY').split():
594 if k in env:
594 if k in env:
595 del env[k]
595 del env[k]
596
596
597 # unset env related to hooks
597 # unset env related to hooks
598 for k in env.keys():
598 for k in env.keys():
599 if k.startswith('HG_'):
599 if k.startswith('HG_'):
600 del env[k]
600 del env[k]
601
601
602 return env
602 return env
603
603
604 def _createhgrc(self, path):
604 def _createhgrc(self, path):
605 # create a fresh hgrc
605 # create a fresh hgrc
606 hgrc = open(path, 'w')
606 hgrc = open(path, 'w')
607 hgrc.write('[ui]\n')
607 hgrc.write('[ui]\n')
608 hgrc.write('slash = True\n')
608 hgrc.write('slash = True\n')
609 hgrc.write('interactive = False\n')
609 hgrc.write('interactive = False\n')
610 hgrc.write('[defaults]\n')
610 hgrc.write('[defaults]\n')
611 hgrc.write('backout = -d "0 0"\n')
611 hgrc.write('backout = -d "0 0"\n')
612 hgrc.write('commit = -d "0 0"\n')
612 hgrc.write('commit = -d "0 0"\n')
613 hgrc.write('shelve = --date "0 0"\n')
613 hgrc.write('shelve = --date "0 0"\n')
614 hgrc.write('tag = -d "0 0"\n')
614 hgrc.write('tag = -d "0 0"\n')
615 if self._options.extra_config_opt:
615 if self._options.extra_config_opt:
616 for opt in self._options.extra_config_opt:
616 for opt in self._options.extra_config_opt:
617 section, key = opt.split('.', 1)
617 section, key = opt.split('.', 1)
618 assert '=' in key, ('extra config opt %s must '
618 assert '=' in key, ('extra config opt %s must '
619 'have an = for assignment' % opt)
619 'have an = for assignment' % opt)
620 hgrc.write('[%s]\n%s\n' % (section, key))
620 hgrc.write('[%s]\n%s\n' % (section, key))
621 hgrc.close()
621 hgrc.close()
622
622
623 def success(self):
623 def success(self):
624 return '.', self.name, ''
624 return '.', self.name, ''
625
625
626 def fail(self, msg, ret):
626 def fail(self, msg, ret):
627 warned = ret is False
627 warned = ret is False
628 if not self._options.nodiff:
628 if not self._options.nodiff:
629 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', self.name,
629 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', self.name,
630 msg))
630 msg))
631 if (not ret and self._options.interactive and
631 if (not ret and self._options.interactive and
632 os.path.exists(self._errpath)):
632 os.path.exists(self._errpath)):
633 iolock.acquire()
633 iolock.acquire()
634 print 'Accept this change? [n] ',
634 print 'Accept this change? [n] ',
635 answer = sys.stdin.readline().strip()
635 answer = sys.stdin.readline().strip()
636 iolock.release()
636 iolock.release()
637 if answer.lower() in ('y', 'yes'):
637 if answer.lower() in ('y', 'yes'):
638 if self.name.endswith('.t'):
638 if self.name.endswith('.t'):
639 rename(self._errpath, self._path)
639 rename(self._errpath, self._path)
640 else:
640 else:
641 rename(self._errpath, '%s.out' % self._path)
641 rename(self._errpath, '%s.out' % self._path)
642
642
643 return '.', self.name, ''
643 return '.', self.name, ''
644
644
645 if self._unittest:
645 if self._unittest:
646 if warned:
646 if warned:
647 raise WarnTest(msg)
647 raise WarnTest(msg)
648 else:
648 else:
649 # unittest differentiates between errored and failed.
649 # unittest differentiates between errored and failed.
650 # Failed is denoted by AssertionError (by default at least).
650 # Failed is denoted by AssertionError (by default at least).
651 raise AssertionError(msg)
651 raise AssertionError(msg)
652
652
653 return warned and '~' or '!', self.name, msg
653 return warned and '~' or '!', self.name, msg
654
654
655 def skip(self, msg):
655 def skip(self, msg):
656 if self._unittest:
656 if self._unittest:
657 raise SkipTest(msg)
657 raise SkipTest(msg)
658
658
659 if self._options.verbose:
659 if self._options.verbose:
660 log("\nSkipping %s: %s" % (self._path, msg))
660 log("\nSkipping %s: %s" % (self._path, msg))
661
661
662 return 's', self.name, msg
662 return 's', self.name, msg
663
663
664 def ignore(self, msg):
664 def ignore(self, msg):
665 if self._unittest:
665 if self._unittest:
666 raise IgnoreTest(msg)
666 raise IgnoreTest(msg)
667
667
668 return 'i', self.name, msg
668 return 'i', self.name, msg
669
669
670 class PythonTest(Test):
670 class PythonTest(Test):
671 """A Python-based test."""
671 """A Python-based test."""
672 def _run(self, replacements, env):
672 def _run(self, replacements, env):
673 py3kswitch = self._options.py3k_warnings and ' -3' or ''
673 py3kswitch = self._options.py3k_warnings and ' -3' or ''
674 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
674 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
675 vlog("# Running", cmd)
675 vlog("# Running", cmd)
676 if os.name == 'nt':
676 if os.name == 'nt':
677 replacements.append((r'\r\n', '\n'))
677 replacements.append((r'\r\n', '\n'))
678 return run(cmd, self._testtmp, self._options, replacements, env,
678 return run(cmd, self._testtmp, self._options, replacements, env,
679 self._runner.abort)
679 self._runner.abort)
680
680
681 class TTest(Test):
681 class TTest(Test):
682 """A "t test" is a test backed by a .t file."""
682 """A "t test" is a test backed by a .t file."""
683
683
684 SKIPPED_PREFIX = 'skipped: '
684 SKIPPED_PREFIX = 'skipped: '
685 FAILED_PREFIX = 'hghave check failed: '
685 FAILED_PREFIX = 'hghave check failed: '
686 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
686 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
687
687
688 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
688 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
689 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256)).update(
689 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256)).update(
690 {'\\': '\\\\', '\r': r'\r'})
690 {'\\': '\\\\', '\r': r'\r'})
691
691
692 def _run(self, replacements, env):
692 def _run(self, replacements, env):
693 f = open(self._path)
693 f = open(self._path)
694 lines = f.readlines()
694 lines = f.readlines()
695 f.close()
695 f.close()
696
696
697 salt, script, after, expected = self._parsetest(lines)
697 salt, script, after, expected = self._parsetest(lines)
698
698
699 # Write out the generated script.
699 # Write out the generated script.
700 fname = '%s.sh' % self._testtmp
700 fname = '%s.sh' % self._testtmp
701 f = open(fname, 'w')
701 f = open(fname, 'w')
702 for l in script:
702 for l in script:
703 f.write(l)
703 f.write(l)
704 f.close()
704 f.close()
705
705
706 cmd = '%s "%s"' % (self._options.shell, fname)
706 cmd = '%s "%s"' % (self._options.shell, fname)
707 vlog("# Running", cmd)
707 vlog("# Running", cmd)
708
708
709 exitcode, output = run(cmd, self._testtmp, self._options, replacements,
709 exitcode, output = run(cmd, self._testtmp, self._options, replacements,
710 env, self._runner.abort)
710 env, self._runner.abort)
711 # Do not merge output if skipped. Return hghave message instead.
711 # Do not merge output if skipped. Return hghave message instead.
712 # Similarly, with --debug, output is None.
712 # Similarly, with --debug, output is None.
713 if exitcode == self.SKIPPED_STATUS or output is None:
713 if exitcode == self.SKIPPED_STATUS or output is None:
714 return exitcode, output
714 return exitcode, output
715
715
716 return self._processoutput(exitcode, output, salt, after, expected)
716 return self._processoutput(exitcode, output, salt, after, expected)
717
717
718 def _hghave(self, reqs):
718 def _hghave(self, reqs):
719 # TODO do something smarter when all other uses of hghave are gone.
719 # TODO do something smarter when all other uses of hghave are gone.
720 tdir = self._testdir.replace('\\', '/')
720 tdir = self._testdir.replace('\\', '/')
721 proc = Popen4('%s -c "%s/hghave %s"' %
721 proc = Popen4('%s -c "%s/hghave %s"' %
722 (self._options.shell, tdir, ' '.join(reqs)),
722 (self._options.shell, tdir, ' '.join(reqs)),
723 self._testtmp, 0)
723 self._testtmp, 0)
724 stdout, stderr = proc.communicate()
724 stdout, stderr = proc.communicate()
725 ret = proc.wait()
725 ret = proc.wait()
726 if wifexited(ret):
726 if wifexited(ret):
727 ret = os.WEXITSTATUS(ret)
727 ret = os.WEXITSTATUS(ret)
728 if ret == 2:
728 if ret == 2:
729 print stdout
729 print stdout
730 sys.exit(1)
730 sys.exit(1)
731
731
732 return ret == 0
732 return ret == 0
733
733
734 def _parsetest(self, lines):
734 def _parsetest(self, lines):
735 # We generate a shell script which outputs unique markers to line
735 # We generate a shell script which outputs unique markers to line
736 # up script results with our source. These markers include input
736 # up script results with our source. These markers include input
737 # line number and the last return code.
737 # line number and the last return code.
738 salt = "SALT" + str(time.time())
738 salt = "SALT" + str(time.time())
739 def addsalt(line, inpython):
739 def addsalt(line, inpython):
740 if inpython:
740 if inpython:
741 script.append('%s %d 0\n' % (salt, line))
741 script.append('%s %d 0\n' % (salt, line))
742 else:
742 else:
743 script.append('echo %s %s $?\n' % (salt, line))
743 script.append('echo %s %s $?\n' % (salt, line))
744
744
745 script = []
745 script = []
746
746
747 # After we run the shell script, we re-unify the script output
747 # After we run the shell script, we re-unify the script output
748 # with non-active parts of the source, with synchronization by our
748 # with non-active parts of the source, with synchronization by our
749 # SALT line number markers. The after table contains the non-active
749 # SALT line number markers. The after table contains the non-active
750 # components, ordered by line number.
750 # components, ordered by line number.
751 after = {}
751 after = {}
752
752
753 # Expected shell script output.
753 # Expected shell script output.
754 expected = {}
754 expected = {}
755
755
756 pos = prepos = -1
756 pos = prepos = -1
757
757
758 # True or False when in a true or false conditional section
758 # True or False when in a true or false conditional section
759 skipping = None
759 skipping = None
760
760
761 # We keep track of whether or not we're in a Python block so we
761 # We keep track of whether or not we're in a Python block so we
762 # can generate the surrounding doctest magic.
762 # can generate the surrounding doctest magic.
763 inpython = False
763 inpython = False
764
764
765 if self._options.debug:
765 if self._options.debug:
766 script.append('set -x\n')
766 script.append('set -x\n')
767 if os.getenv('MSYSTEM'):
767 if os.getenv('MSYSTEM'):
768 script.append('alias pwd="pwd -W"\n')
768 script.append('alias pwd="pwd -W"\n')
769
769
770 for n, l in enumerate(lines):
770 for n, l in enumerate(lines):
771 if not l.endswith('\n'):
771 if not l.endswith('\n'):
772 l += '\n'
772 l += '\n'
773 if l.startswith('#if'):
773 if l.startswith('#if'):
774 lsplit = l.split()
774 lsplit = l.split()
775 if len(lsplit) < 2 or lsplit[0] != '#if':
775 if len(lsplit) < 2 or lsplit[0] != '#if':
776 after.setdefault(pos, []).append(' !!! invalid #if\n')
776 after.setdefault(pos, []).append(' !!! invalid #if\n')
777 if skipping is not None:
777 if skipping is not None:
778 after.setdefault(pos, []).append(' !!! nested #if\n')
778 after.setdefault(pos, []).append(' !!! nested #if\n')
779 skipping = not self._hghave(lsplit[1:])
779 skipping = not self._hghave(lsplit[1:])
780 after.setdefault(pos, []).append(l)
780 after.setdefault(pos, []).append(l)
781 elif l.startswith('#else'):
781 elif l.startswith('#else'):
782 if skipping is None:
782 if skipping is None:
783 after.setdefault(pos, []).append(' !!! missing #if\n')
783 after.setdefault(pos, []).append(' !!! missing #if\n')
784 skipping = not skipping
784 skipping = not skipping
785 after.setdefault(pos, []).append(l)
785 after.setdefault(pos, []).append(l)
786 elif l.startswith('#endif'):
786 elif l.startswith('#endif'):
787 if skipping is None:
787 if skipping is None:
788 after.setdefault(pos, []).append(' !!! missing #if\n')
788 after.setdefault(pos, []).append(' !!! missing #if\n')
789 skipping = None
789 skipping = None
790 after.setdefault(pos, []).append(l)
790 after.setdefault(pos, []).append(l)
791 elif skipping:
791 elif skipping:
792 after.setdefault(pos, []).append(l)
792 after.setdefault(pos, []).append(l)
793 elif l.startswith(' >>> '): # python inlines
793 elif l.startswith(' >>> '): # python inlines
794 after.setdefault(pos, []).append(l)
794 after.setdefault(pos, []).append(l)
795 prepos = pos
795 prepos = pos
796 pos = n
796 pos = n
797 if not inpython:
797 if not inpython:
798 # We've just entered a Python block. Add the header.
798 # We've just entered a Python block. Add the header.
799 inpython = True
799 inpython = True
800 addsalt(prepos, False) # Make sure we report the exit code.
800 addsalt(prepos, False) # Make sure we report the exit code.
801 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
801 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
802 addsalt(n, True)
802 addsalt(n, True)
803 script.append(l[2:])
803 script.append(l[2:])
804 elif l.startswith(' ... '): # python inlines
804 elif l.startswith(' ... '): # python inlines
805 after.setdefault(prepos, []).append(l)
805 after.setdefault(prepos, []).append(l)
806 script.append(l[2:])
806 script.append(l[2:])
807 elif l.startswith(' $ '): # commands
807 elif l.startswith(' $ '): # commands
808 if inpython:
808 if inpython:
809 script.append('EOF\n')
809 script.append('EOF\n')
810 inpython = False
810 inpython = False
811 after.setdefault(pos, []).append(l)
811 after.setdefault(pos, []).append(l)
812 prepos = pos
812 prepos = pos
813 pos = n
813 pos = n
814 addsalt(n, False)
814 addsalt(n, False)
815 cmd = l[4:].split()
815 cmd = l[4:].split()
816 if len(cmd) == 2 and cmd[0] == 'cd':
816 if len(cmd) == 2 and cmd[0] == 'cd':
817 l = ' $ cd %s || exit 1\n' % cmd[1]
817 l = ' $ cd %s || exit 1\n' % cmd[1]
818 script.append(l[4:])
818 script.append(l[4:])
819 elif l.startswith(' > '): # continuations
819 elif l.startswith(' > '): # continuations
820 after.setdefault(prepos, []).append(l)
820 after.setdefault(prepos, []).append(l)
821 script.append(l[4:])
821 script.append(l[4:])
822 elif l.startswith(' '): # results
822 elif l.startswith(' '): # results
823 # Queue up a list of expected results.
823 # Queue up a list of expected results.
824 expected.setdefault(pos, []).append(l[2:])
824 expected.setdefault(pos, []).append(l[2:])
825 else:
825 else:
826 if inpython:
826 if inpython:
827 script.append('EOF\n')
827 script.append('EOF\n')
828 inpython = False
828 inpython = False
829 # Non-command/result. Queue up for merged output.
829 # Non-command/result. Queue up for merged output.
830 after.setdefault(pos, []).append(l)
830 after.setdefault(pos, []).append(l)
831
831
832 if inpython:
832 if inpython:
833 script.append('EOF\n')
833 script.append('EOF\n')
834 if skipping is not None:
834 if skipping is not None:
835 after.setdefault(pos, []).append(' !!! missing #endif\n')
835 after.setdefault(pos, []).append(' !!! missing #endif\n')
836 addsalt(n + 1, False)
836 addsalt(n + 1, False)
837
837
838 return salt, script, after, expected
838 return salt, script, after, expected
839
839
840 def _processoutput(self, exitcode, output, salt, after, expected):
840 def _processoutput(self, exitcode, output, salt, after, expected):
841 # Merge the script output back into a unified test.
841 # Merge the script output back into a unified test.
842 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
842 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
843 if exitcode != 0:
843 if exitcode != 0:
844 warnonly = 3
844 warnonly = 3
845
845
846 pos = -1
846 pos = -1
847 postout = []
847 postout = []
848 for l in output:
848 for l in output:
849 lout, lcmd = l, None
849 lout, lcmd = l, None
850 if salt in l:
850 if salt in l:
851 lout, lcmd = l.split(salt, 1)
851 lout, lcmd = l.split(salt, 1)
852
852
853 if lout:
853 if lout:
854 if not lout.endswith('\n'):
854 if not lout.endswith('\n'):
855 lout += ' (no-eol)\n'
855 lout += ' (no-eol)\n'
856
856
857 # Find the expected output at the current position.
857 # Find the expected output at the current position.
858 el = None
858 el = None
859 if expected.get(pos, None):
859 if expected.get(pos, None):
860 el = expected[pos].pop(0)
860 el = expected[pos].pop(0)
861
861
862 r = TTest.linematch(el, lout)
862 r = TTest.linematch(el, lout)
863 if isinstance(r, str):
863 if isinstance(r, str):
864 if r == '+glob':
864 if r == '+glob':
865 lout = el[:-1] + ' (glob)\n'
865 lout = el[:-1] + ' (glob)\n'
866 r = '' # Warn only this line.
866 r = '' # Warn only this line.
867 elif r == '-glob':
867 elif r == '-glob':
868 lout = ''.join(el.rsplit(' (glob)', 1))
868 lout = ''.join(el.rsplit(' (glob)', 1))
869 r = '' # Warn only this line.
869 r = '' # Warn only this line.
870 else:
870 else:
871 log('\ninfo, unknown linematch result: %r\n' % r)
871 log('\ninfo, unknown linematch result: %r\n' % r)
872 r = False
872 r = False
873 if r:
873 if r:
874 postout.append(' ' + el)
874 postout.append(' ' + el)
875 else:
875 else:
876 if self.NEEDESCAPE(lout):
876 if self.NEEDESCAPE(lout):
877 lout = TTest.stringescape('%s (esc)\n' %
877 lout = TTest.stringescape('%s (esc)\n' %
878 lout.rstrip('\n'))
878 lout.rstrip('\n'))
879 postout.append(' ' + lout) # Let diff deal with it.
879 postout.append(' ' + lout) # Let diff deal with it.
880 if r != '': # If line failed.
880 if r != '': # If line failed.
881 warnonly = 3 # for sure not
881 warnonly = 3 # for sure not
882 elif warnonly == 1: # Is "not yet" and line is warn only.
882 elif warnonly == 1: # Is "not yet" and line is warn only.
883 warnonly = 2 # Yes do warn.
883 warnonly = 2 # Yes do warn.
884
884
885 if lcmd:
885 if lcmd:
886 # Add on last return code.
886 # Add on last return code.
887 ret = int(lcmd.split()[1])
887 ret = int(lcmd.split()[1])
888 if ret != 0:
888 if ret != 0:
889 postout.append(' [%s]\n' % ret)
889 postout.append(' [%s]\n' % ret)
890 if pos in after:
890 if pos in after:
891 # Merge in non-active test bits.
891 # Merge in non-active test bits.
892 postout += after.pop(pos)
892 postout += after.pop(pos)
893 pos = int(lcmd.split()[0])
893 pos = int(lcmd.split()[0])
894
894
895 if pos in after:
895 if pos in after:
896 postout += after.pop(pos)
896 postout += after.pop(pos)
897
897
898 if warnonly == 2:
898 if warnonly == 2:
899 exitcode = False # Set exitcode to warned.
899 exitcode = False # Set exitcode to warned.
900
900
901 return exitcode, postout
901 return exitcode, postout
902
902
903 @staticmethod
903 @staticmethod
904 def rematch(el, l):
904 def rematch(el, l):
905 try:
905 try:
906 # use \Z to ensure that the regex matches to the end of the string
906 # use \Z to ensure that the regex matches to the end of the string
907 if os.name == 'nt':
907 if os.name == 'nt':
908 return re.match(el + r'\r?\n\Z', l)
908 return re.match(el + r'\r?\n\Z', l)
909 return re.match(el + r'\n\Z', l)
909 return re.match(el + r'\n\Z', l)
910 except re.error:
910 except re.error:
911 # el is an invalid regex
911 # el is an invalid regex
912 return False
912 return False
913
913
914 @staticmethod
914 @staticmethod
915 def globmatch(el, l):
915 def globmatch(el, l):
916 # The only supported special characters are * and ? plus / which also
916 # The only supported special characters are * and ? plus / which also
917 # matches \ on windows. Escaping of these characters is supported.
917 # matches \ on windows. Escaping of these characters is supported.
918 if el + '\n' == l:
918 if el + '\n' == l:
919 if os.altsep:
919 if os.altsep:
920 # matching on "/" is not needed for this line
920 # matching on "/" is not needed for this line
921 return '-glob'
921 return '-glob'
922 return True
922 return True
923 i, n = 0, len(el)
923 i, n = 0, len(el)
924 res = ''
924 res = ''
925 while i < n:
925 while i < n:
926 c = el[i]
926 c = el[i]
927 i += 1
927 i += 1
928 if c == '\\' and el[i] in '*?\\/':
928 if c == '\\' and el[i] in '*?\\/':
929 res += el[i - 1:i + 1]
929 res += el[i - 1:i + 1]
930 i += 1
930 i += 1
931 elif c == '*':
931 elif c == '*':
932 res += '.*'
932 res += '.*'
933 elif c == '?':
933 elif c == '?':
934 res += '.'
934 res += '.'
935 elif c == '/' and os.altsep:
935 elif c == '/' and os.altsep:
936 res += '[/\\\\]'
936 res += '[/\\\\]'
937 else:
937 else:
938 res += re.escape(c)
938 res += re.escape(c)
939 return TTest.rematch(res, l)
939 return TTest.rematch(res, l)
940
940
941 @staticmethod
941 @staticmethod
942 def linematch(el, l):
942 def linematch(el, l):
943 if el == l: # perfect match (fast)
943 if el == l: # perfect match (fast)
944 return True
944 return True
945 if el:
945 if el:
946 if el.endswith(" (esc)\n"):
946 if el.endswith(" (esc)\n"):
947 el = el[:-7].decode('string-escape') + '\n'
947 el = el[:-7].decode('string-escape') + '\n'
948 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
948 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
949 return True
949 return True
950 if el.endswith(" (re)\n"):
950 if el.endswith(" (re)\n"):
951 return TTest.rematch(el[:-6], l)
951 return TTest.rematch(el[:-6], l)
952 if el.endswith(" (glob)\n"):
952 if el.endswith(" (glob)\n"):
953 return TTest.globmatch(el[:-8], l)
953 return TTest.globmatch(el[:-8], l)
954 if os.altsep and l.replace('\\', '/') == el:
954 if os.altsep and l.replace('\\', '/') == el:
955 return '+glob'
955 return '+glob'
956 return False
956 return False
957
957
958 @staticmethod
958 @staticmethod
959 def parsehghaveoutput(lines):
959 def parsehghaveoutput(lines):
960 '''Parse hghave log lines.
960 '''Parse hghave log lines.
961
961
962 Return tuple of lists (missing, failed):
962 Return tuple of lists (missing, failed):
963 * the missing/unknown features
963 * the missing/unknown features
964 * the features for which existence check failed'''
964 * the features for which existence check failed'''
965 missing = []
965 missing = []
966 failed = []
966 failed = []
967 for line in lines:
967 for line in lines:
968 if line.startswith(TTest.SKIPPED_PREFIX):
968 if line.startswith(TTest.SKIPPED_PREFIX):
969 line = line.splitlines()[0]
969 line = line.splitlines()[0]
970 missing.append(line[len(TTest.SKIPPED_PREFIX):])
970 missing.append(line[len(TTest.SKIPPED_PREFIX):])
971 elif line.startswith(TTest.FAILED_PREFIX):
971 elif line.startswith(TTest.FAILED_PREFIX):
972 line = line.splitlines()[0]
972 line = line.splitlines()[0]
973 failed.append(line[len(TTest.FAILED_PREFIX):])
973 failed.append(line[len(TTest.FAILED_PREFIX):])
974
974
975 return missing, failed
975 return missing, failed
976
976
977 @staticmethod
977 @staticmethod
978 def _escapef(m):
978 def _escapef(m):
979 return TTest.ESCAPEMAP[m.group(0)]
979 return TTest.ESCAPEMAP[m.group(0)]
980
980
981 @staticmethod
981 @staticmethod
982 def _stringescape(s):
982 def _stringescape(s):
983 return TTest.ESCAPESUB(TTest._escapef, s)
983 return TTest.ESCAPESUB(TTest._escapef, s)
984
984
985
985
986 wifexited = getattr(os, "WIFEXITED", lambda x: False)
986 wifexited = getattr(os, "WIFEXITED", lambda x: False)
987 def run(cmd, wd, options, replacements, env, abort):
987 def run(cmd, wd, options, replacements, env, abort):
988 """Run command in a sub-process, capturing the output (stdout and stderr).
988 """Run command in a sub-process, capturing the output (stdout and stderr).
989 Return a tuple (exitcode, output). output is None in debug mode."""
989 Return a tuple (exitcode, output). output is None in debug mode."""
990 # TODO: Use subprocess.Popen if we're running on Python 2.4
990 # TODO: Use subprocess.Popen if we're running on Python 2.4
991 if options.debug:
991 if options.debug:
992 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
992 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
993 ret = proc.wait()
993 ret = proc.wait()
994 return (ret, None)
994 return (ret, None)
995
995
996 proc = Popen4(cmd, wd, options.timeout, env)
996 proc = Popen4(cmd, wd, options.timeout, env)
997 def cleanup():
997 def cleanup():
998 terminate(proc)
998 terminate(proc)
999 ret = proc.wait()
999 ret = proc.wait()
1000 if ret == 0:
1000 if ret == 0:
1001 ret = signal.SIGTERM << 8
1001 ret = signal.SIGTERM << 8
1002 killdaemons(env['DAEMON_PIDS'])
1002 killdaemons(env['DAEMON_PIDS'])
1003 return ret
1003 return ret
1004
1004
1005 output = ''
1005 output = ''
1006 proc.tochild.close()
1006 proc.tochild.close()
1007
1007
1008 try:
1008 try:
1009 output = proc.fromchild.read()
1009 output = proc.fromchild.read()
1010 except KeyboardInterrupt:
1010 except KeyboardInterrupt:
1011 vlog('# Handling keyboard interrupt')
1011 vlog('# Handling keyboard interrupt')
1012 cleanup()
1012 cleanup()
1013 raise
1013 raise
1014
1014
1015 ret = proc.wait()
1015 ret = proc.wait()
1016 if wifexited(ret):
1016 if wifexited(ret):
1017 ret = os.WEXITSTATUS(ret)
1017 ret = os.WEXITSTATUS(ret)
1018
1018
1019 if proc.timeout:
1019 if proc.timeout:
1020 ret = 'timeout'
1020 ret = 'timeout'
1021
1021
1022 if ret:
1022 if ret:
1023 killdaemons(env['DAEMON_PIDS'])
1023 killdaemons(env['DAEMON_PIDS'])
1024
1024
1025 if abort[0]:
1025 if abort[0]:
1026 raise KeyboardInterrupt()
1026 raise KeyboardInterrupt()
1027
1027
1028 for s, r in replacements:
1028 for s, r in replacements:
1029 output = re.sub(s, r, output)
1029 output = re.sub(s, r, output)
1030 return ret, output.splitlines(True)
1030 return ret, output.splitlines(True)
1031
1031
1032 iolock = threading.Lock()
1032 iolock = threading.Lock()
1033
1033
1034 class SkipTest(Exception):
1034 class SkipTest(Exception):
1035 """Raised to indicate that a test is to be skipped."""
1035 """Raised to indicate that a test is to be skipped."""
1036
1036
1037 class IgnoreTest(Exception):
1037 class IgnoreTest(Exception):
1038 """Raised to indicate that a test is to be ignored."""
1038 """Raised to indicate that a test is to be ignored."""
1039
1039
1040 class WarnTest(Exception):
1040 class WarnTest(Exception):
1041 """Raised to indicate that a test warned."""
1041 """Raised to indicate that a test warned."""
1042
1042
1043 class TestResult(unittest._TextTestResult):
1043 class TestResult(unittest._TextTestResult):
1044 """Holds results when executing via unittest."""
1044 """Holds results when executing via unittest."""
1045 # Don't worry too much about accessing the non-public _TextTestResult.
1045 # Don't worry too much about accessing the non-public _TextTestResult.
1046 # It is relatively common in Python testing tools.
1046 # It is relatively common in Python testing tools.
1047 def __init__(self, *args, **kwargs):
1047 def __init__(self, *args, **kwargs):
1048 super(TestResult, self).__init__(*args, **kwargs)
1048 super(TestResult, self).__init__(*args, **kwargs)
1049
1049
1050 # unittest.TestResult didn't have skipped until 2.7. We need to
1050 # unittest.TestResult didn't have skipped until 2.7. We need to
1051 # polyfill it.
1051 # polyfill it.
1052 self.skipped = []
1052 self.skipped = []
1053
1053
1054 # We have a custom "ignored" result that isn't present in any Python
1054 # We have a custom "ignored" result that isn't present in any Python
1055 # unittest implementation. It is very similar to skipped. It may make
1055 # unittest implementation. It is very similar to skipped. It may make
1056 # sense to map it into skip some day.
1056 # sense to map it into skip some day.
1057 self.ignored = []
1057 self.ignored = []
1058
1058
1059 # We have a custom "warned" result that isn't present in any Python
1059 # We have a custom "warned" result that isn't present in any Python
1060 # unittest implementation. It is very similar to failed. It may make
1060 # unittest implementation. It is very similar to failed. It may make
1061 # sense to map it into fail some day.
1061 # sense to map it into fail some day.
1062 self.warned = []
1062 self.warned = []
1063
1063
1064 # Polyfill.
1064 # Polyfill.
1065 def addSkip(self, test, reason):
1065 def addSkip(self, test, reason):
1066 self.skipped.append((test, reason))
1066 self.skipped.append((test, reason))
1067
1067
1068 if self.showAll:
1068 if self.showAll:
1069 self.stream.writeln('skipped %s' % reason)
1069 self.stream.writeln('skipped %s' % reason)
1070 else:
1070 else:
1071 self.stream.write('s')
1071 self.stream.write('s')
1072 self.stream.flush()
1072 self.stream.flush()
1073
1073
1074 def addIgnore(self, test, reason):
1074 def addIgnore(self, test, reason):
1075 self.ignored.append((test, reason))
1075 self.ignored.append((test, reason))
1076
1076
1077 if self.showAll:
1077 if self.showAll:
1078 self.stream.writeln('ignored %s' % reason)
1078 self.stream.writeln('ignored %s' % reason)
1079 else:
1079 else:
1080 self.stream.write('i')
1080 self.stream.write('i')
1081 self.stream.flush()
1081 self.stream.flush()
1082
1082
1083 def addWarn(self, test, reason):
1083 def addWarn(self, test, reason):
1084 self.warned.append((test, reason))
1084 self.warned.append((test, reason))
1085
1085
1086 if self.showAll:
1086 if self.showAll:
1087 self.stream.writeln('warned %s' % reason)
1087 self.stream.writeln('warned %s' % reason)
1088 else:
1088 else:
1089 self.stream.write('~')
1089 self.stream.write('~')
1090 self.stream.flush()
1090 self.stream.flush()
1091
1091
1092 class TestSuite(unittest.TestSuite):
1092 class TestSuite(unittest.TestSuite):
1093 """Custom unitest TestSuite that knows how to execute concurrently."""
1093 """Custom unitest TestSuite that knows how to execute concurrently."""
1094
1094
1095 def __init__(self, runner, *args, **kwargs):
1095 def __init__(self, runner, *args, **kwargs):
1096 super(TestSuite, self).__init__(*args, **kwargs)
1096 super(TestSuite, self).__init__(*args, **kwargs)
1097
1097
1098 self._runner = runner
1098 self._runner = runner
1099
1099
1100 def run(self, result):
1100 def run(self, result):
1101 self._runner._executetests(self._tests, result=result)
1101 self._runner._executetests(self._tests, result=result)
1102
1102
1103 return result
1103 return result
1104
1104
1105 class TextTestRunner(unittest.TextTestRunner):
1105 class TextTestRunner(unittest.TextTestRunner):
1106 """Custom unittest test runner that uses appropriate settings."""
1106 """Custom unittest test runner that uses appropriate settings."""
1107
1107
1108 def _makeResult(self):
1108 def _makeResult(self):
1109 return TestResult(self.stream, self.descriptions, self.verbosity)
1109 return TestResult(self.stream, self.descriptions, self.verbosity)
1110
1110
1111 class TestRunner(object):
1111 class TestRunner(object):
1112 """Holds context for executing tests.
1112 """Holds context for executing tests.
1113
1113
1114 Tests rely on a lot of state. This object holds it for them.
1114 Tests rely on a lot of state. This object holds it for them.
1115 """
1115 """
1116
1116
1117 REQUIREDTOOLS = [
1117 REQUIREDTOOLS = [
1118 os.path.basename(sys.executable),
1118 os.path.basename(sys.executable),
1119 'diff',
1119 'diff',
1120 'grep',
1120 'grep',
1121 'unzip',
1121 'unzip',
1122 'gunzip',
1122 'gunzip',
1123 'bunzip2',
1123 'bunzip2',
1124 'sed',
1124 'sed',
1125 ]
1125 ]
1126
1126
1127 TESTTYPES = [
1127 TESTTYPES = [
1128 ('.py', PythonTest, '.out'),
1128 ('.py', PythonTest, '.out'),
1129 ('.t', TTest, ''),
1129 ('.t', TTest, ''),
1130 ]
1130 ]
1131
1131
1132 def __init__(self):
1132 def __init__(self):
1133 self.options = None
1133 self.options = None
1134 self.testdir = None
1134 self.testdir = None
1135 self.hgtmp = None
1135 self.hgtmp = None
1136 self.inst = None
1136 self.inst = None
1137 self.bindir = None
1137 self.bindir = None
1138 self.tmpbinddir = None
1138 self.tmpbinddir = None
1139 self.pythondir = None
1139 self.pythondir = None
1140 self.coveragefile = None
1140 self.coveragefile = None
1141 self.times = [] # Holds execution times of tests.
1141 self.times = [] # Holds execution times of tests.
1142 self.results = {
1142 self.results = {
1143 '.': [],
1143 '.': [],
1144 '!': [],
1144 '!': [],
1145 '~': [],
1145 '~': [],
1146 's': [],
1146 's': [],
1147 'i': [],
1147 'i': [],
1148 'u': [],
1148 'u': [],
1149 }
1149 }
1150 self.abort = [False]
1150 self.abort = [False]
1151 self._createdfiles = []
1151 self._createdfiles = []
1152 self._hgpath = None
1152 self._hgpath = None
1153
1153
1154 def run(self, args, parser=None):
1154 def run(self, args, parser=None):
1155 """Run the test suite."""
1155 """Run the test suite."""
1156 oldmask = os.umask(022)
1156 oldmask = os.umask(022)
1157 try:
1157 try:
1158 parser = parser or getparser()
1158 parser = parser or getparser()
1159 options, args = parseargs(args, parser)
1159 options, args = parseargs(args, parser)
1160 self.options = options
1160 self.options = options
1161
1161
1162 self._checktools()
1162 self._checktools()
1163 tests = self.findtests(args)
1163 tests = self.findtests(args)
1164 return self._run(tests)
1164 return self._run(tests)
1165 finally:
1165 finally:
1166 os.umask(oldmask)
1166 os.umask(oldmask)
1167
1167
1168 def _run(self, tests):
1168 def _run(self, tests):
1169 if self.options.random:
1169 if self.options.random:
1170 random.shuffle(tests)
1170 random.shuffle(tests)
1171 else:
1171 else:
1172 # keywords for slow tests
1172 # keywords for slow tests
1173 slow = 'svn gendoc check-code-hg'.split()
1173 slow = 'svn gendoc check-code-hg'.split()
1174 def sortkey(f):
1174 def sortkey(f):
1175 # run largest tests first, as they tend to take the longest
1175 # run largest tests first, as they tend to take the longest
1176 try:
1176 try:
1177 val = -os.stat(f).st_size
1177 val = -os.stat(f).st_size
1178 except OSError, e:
1178 except OSError, e:
1179 if e.errno != errno.ENOENT:
1179 if e.errno != errno.ENOENT:
1180 raise
1180 raise
1181 return -1e9 # file does not exist, tell early
1181 return -1e9 # file does not exist, tell early
1182 for kw in slow:
1182 for kw in slow:
1183 if kw in f:
1183 if kw in f:
1184 val *= 10
1184 val *= 10
1185 return val
1185 return val
1186 tests.sort(key=sortkey)
1186 tests.sort(key=sortkey)
1187
1187
1188 self.testdir = os.environ['TESTDIR'] = os.getcwd()
1188 self.testdir = os.environ['TESTDIR'] = os.getcwd()
1189
1189
1190 if 'PYTHONHASHSEED' not in os.environ:
1190 if 'PYTHONHASHSEED' not in os.environ:
1191 # use a random python hash seed all the time
1191 # use a random python hash seed all the time
1192 # we do the randomness ourself to know what seed is used
1192 # we do the randomness ourself to know what seed is used
1193 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1193 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1194
1194
1195 if self.options.tmpdir:
1195 if self.options.tmpdir:
1196 self.options.keep_tmpdir = True
1196 self.options.keep_tmpdir = True
1197 tmpdir = self.options.tmpdir
1197 tmpdir = self.options.tmpdir
1198 if os.path.exists(tmpdir):
1198 if os.path.exists(tmpdir):
1199 # Meaning of tmpdir has changed since 1.3: we used to create
1199 # Meaning of tmpdir has changed since 1.3: we used to create
1200 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1200 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1201 # tmpdir already exists.
1201 # tmpdir already exists.
1202 print "error: temp dir %r already exists" % tmpdir
1202 print "error: temp dir %r already exists" % tmpdir
1203 return 1
1203 return 1
1204
1204
1205 # Automatically removing tmpdir sounds convenient, but could
1205 # Automatically removing tmpdir sounds convenient, but could
1206 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1206 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1207 # or "--tmpdir=$HOME".
1207 # or "--tmpdir=$HOME".
1208 #vlog("# Removing temp dir", tmpdir)
1208 #vlog("# Removing temp dir", tmpdir)
1209 #shutil.rmtree(tmpdir)
1209 #shutil.rmtree(tmpdir)
1210 os.makedirs(tmpdir)
1210 os.makedirs(tmpdir)
1211 else:
1211 else:
1212 d = None
1212 d = None
1213 if os.name == 'nt':
1213 if os.name == 'nt':
1214 # without this, we get the default temp dir location, but
1214 # without this, we get the default temp dir location, but
1215 # in all lowercase, which causes troubles with paths (issue3490)
1215 # in all lowercase, which causes troubles with paths (issue3490)
1216 d = os.getenv('TMP')
1216 d = os.getenv('TMP')
1217 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1217 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1218 self.hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1218 self.hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1219
1219
1220 if self.options.with_hg:
1220 if self.options.with_hg:
1221 self.inst = None
1221 self.inst = None
1222 self.bindir = os.path.dirname(os.path.realpath(
1222 self.bindir = os.path.dirname(os.path.realpath(
1223 self.options.with_hg))
1223 self.options.with_hg))
1224 self.tmpbindir = os.path.join(self.hgtmp, 'install', 'bin')
1224 self.tmpbindir = os.path.join(self.hgtmp, 'install', 'bin')
1225 os.makedirs(self.tmpbindir)
1225 os.makedirs(self.tmpbindir)
1226
1226
1227 # This looks redundant with how Python initializes sys.path from
1227 # This looks redundant with how Python initializes sys.path from
1228 # the location of the script being executed. Needed because the
1228 # the location of the script being executed. Needed because the
1229 # "hg" specified by --with-hg is not the only Python script
1229 # "hg" specified by --with-hg is not the only Python script
1230 # executed in the test suite that needs to import 'mercurial'
1230 # executed in the test suite that needs to import 'mercurial'
1231 # ... which means it's not really redundant at all.
1231 # ... which means it's not really redundant at all.
1232 self.pythondir = self.bindir
1232 self.pythondir = self.bindir
1233 else:
1233 else:
1234 self.inst = os.path.join(self.hgtmp, "install")
1234 self.inst = os.path.join(self.hgtmp, "install")
1235 self.bindir = os.environ["BINDIR"] = os.path.join(self.inst,
1235 self.bindir = os.environ["BINDIR"] = os.path.join(self.inst,
1236 "bin")
1236 "bin")
1237 self.tmpbindir = self.bindir
1237 self.tmpbindir = self.bindir
1238 self.pythondir = os.path.join(self.inst, "lib", "python")
1238 self.pythondir = os.path.join(self.inst, "lib", "python")
1239
1239
1240 os.environ["BINDIR"] = self.bindir
1240 os.environ["BINDIR"] = self.bindir
1241 os.environ["PYTHON"] = PYTHON
1241 os.environ["PYTHON"] = PYTHON
1242
1242
1243 path = [self.bindir] + os.environ["PATH"].split(os.pathsep)
1243 path = [self.bindir] + os.environ["PATH"].split(os.pathsep)
1244 if self.tmpbindir != self.bindir:
1244 if self.tmpbindir != self.bindir:
1245 path = [self.tmpbindir] + path
1245 path = [self.tmpbindir] + path
1246 os.environ["PATH"] = os.pathsep.join(path)
1246 os.environ["PATH"] = os.pathsep.join(path)
1247
1247
1248 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1248 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1249 # can run .../tests/run-tests.py test-foo where test-foo
1249 # can run .../tests/run-tests.py test-foo where test-foo
1250 # adds an extension to HGRC. Also include run-test.py directory to
1250 # adds an extension to HGRC. Also include run-test.py directory to
1251 # import modules like heredoctest.
1251 # import modules like heredoctest.
1252 pypath = [self.pythondir, self.testdir,
1252 pypath = [self.pythondir, self.testdir,
1253 os.path.abspath(os.path.dirname(__file__))]
1253 os.path.abspath(os.path.dirname(__file__))]
1254 # We have to augment PYTHONPATH, rather than simply replacing
1254 # We have to augment PYTHONPATH, rather than simply replacing
1255 # it, in case external libraries are only available via current
1255 # it, in case external libraries are only available via current
1256 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1256 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1257 # are in /opt/subversion.)
1257 # are in /opt/subversion.)
1258 oldpypath = os.environ.get(IMPL_PATH)
1258 oldpypath = os.environ.get(IMPL_PATH)
1259 if oldpypath:
1259 if oldpypath:
1260 pypath.append(oldpypath)
1260 pypath.append(oldpypath)
1261 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1261 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1262
1262
1263 self.coveragefile = os.path.join(self.testdir, '.coverage')
1263 self.coveragefile = os.path.join(self.testdir, '.coverage')
1264
1264
1265 vlog("# Using TESTDIR", self.testdir)
1265 vlog("# Using TESTDIR", self.testdir)
1266 vlog("# Using HGTMP", self.hgtmp)
1266 vlog("# Using HGTMP", self.hgtmp)
1267 vlog("# Using PATH", os.environ["PATH"])
1267 vlog("# Using PATH", os.environ["PATH"])
1268 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1268 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1269
1269
1270 try:
1270 try:
1271 return self._runtests(tests) or 0
1271 return self._runtests(tests) or 0
1272 finally:
1272 finally:
1273 time.sleep(.1)
1273 time.sleep(.1)
1274 self._cleanup()
1274 self._cleanup()
1275
1275
1276 def findtests(self, args):
1276 def findtests(self, args):
1277 """Finds possible test files from arguments.
1277 """Finds possible test files from arguments.
1278
1278
1279 If you wish to inject custom tests into the test harness, this would
1279 If you wish to inject custom tests into the test harness, this would
1280 be a good function to monkeypatch or override in a derived class.
1280 be a good function to monkeypatch or override in a derived class.
1281 """
1281 """
1282 if not args:
1282 if not args:
1283 if self.options.changed:
1283 if self.options.changed:
1284 proc = Popen4('hg st --rev "%s" -man0 .' %
1284 proc = Popen4('hg st --rev "%s" -man0 .' %
1285 self.options.changed, None, 0)
1285 self.options.changed, None, 0)
1286 stdout, stderr = proc.communicate()
1286 stdout, stderr = proc.communicate()
1287 args = stdout.strip('\0').split('\0')
1287 args = stdout.strip('\0').split('\0')
1288 else:
1288 else:
1289 args = os.listdir('.')
1289 args = os.listdir('.')
1290
1290
1291 return [t for t in args
1291 return [t for t in args
1292 if os.path.basename(t).startswith('test-')
1292 if os.path.basename(t).startswith('test-')
1293 and (t.endswith('.py') or t.endswith('.t'))]
1293 and (t.endswith('.py') or t.endswith('.t'))]
1294
1294
1295 def _runtests(self, tests):
1295 def _runtests(self, tests):
1296 try:
1296 try:
1297 if self.inst:
1297 if self.inst:
1298 self._installhg()
1298 self._installhg()
1299 self._checkhglib("Testing")
1299 self._checkhglib("Testing")
1300 else:
1300 else:
1301 self._usecorrectpython()
1301 self._usecorrectpython()
1302
1302
1303 if self.options.restart:
1303 if self.options.restart:
1304 orig = list(tests)
1304 orig = list(tests)
1305 while tests:
1305 while tests:
1306 if os.path.exists(tests[0] + ".err"):
1306 if os.path.exists(tests[0] + ".err"):
1307 break
1307 break
1308 tests.pop(0)
1308 tests.pop(0)
1309 if not tests:
1309 if not tests:
1310 print "running all tests"
1310 print "running all tests"
1311 tests = orig
1311 tests = orig
1312
1312
1313 tests = [self._gettest(t, i, asunit=self.options.unittest)
1313 tests = [self._gettest(t, i, asunit=self.options.unittest)
1314 for i, t in enumerate(tests)]
1314 for i, t in enumerate(tests)]
1315
1315
1316 if self.options.unittest:
1316 if self.options.unittest:
1317 suite = TestSuite(self, tests=tests)
1317 suite = TestSuite(self, tests=tests)
1318 verbosity = 1
1318 verbosity = 1
1319 if self.options.verbose:
1319 if self.options.verbose:
1320 verbosity = 2
1320 verbosity = 2
1321 runner = TextTestRunner(verbosity=verbosity)
1321 runner = TextTestRunner(verbosity=verbosity)
1322 runner.run(suite)
1322 runner.run(suite)
1323 else:
1323 else:
1324 self._executetests(tests)
1324 self._executetests(tests)
1325
1325
1326 failed = len(self.results['!'])
1326 failed = len(self.results['!'])
1327 warned = len(self.results['~'])
1327 warned = len(self.results['~'])
1328 tested = len(self.results['.']) + failed + warned
1328 tested = len(self.results['.']) + failed + warned
1329 skipped = len(self.results['s'])
1329 skipped = len(self.results['s'])
1330 ignored = len(self.results['i'])
1330 ignored = len(self.results['i'])
1331
1331
1332 print
1332 print
1333 if not self.options.noskips:
1333 if not self.options.noskips:
1334 for s in self.results['s']:
1334 for s in self.results['s']:
1335 print "Skipped %s: %s" % s
1335 print "Skipped %s: %s" % s
1336 for s in self.results['~']:
1336 for s in self.results['~']:
1337 print "Warned %s: %s" % s
1337 print "Warned %s: %s" % s
1338 for s in self.results['!']:
1338 for s in self.results['!']:
1339 print "Failed %s: %s" % s
1339 print "Failed %s: %s" % s
1340 self._checkhglib("Tested")
1340 self._checkhglib("Tested")
1341 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1341 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1342 tested, skipped + ignored, warned, failed)
1342 tested, skipped + ignored, warned, failed)
1343 if self.results['!']:
1343 if self.results['!']:
1344 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1344 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1345 if self.options.time:
1345 if self.options.time:
1346 self._outputtimes()
1346 self._outputtimes()
1347
1347
1348 if self.options.anycoverage:
1348 if self.options.anycoverage:
1349 self._outputcoverage()
1349 self._outputcoverage()
1350 except KeyboardInterrupt:
1350 except KeyboardInterrupt:
1351 failed = True
1351 failed = True
1352 print "\ninterrupted!"
1352 print "\ninterrupted!"
1353
1353
1354 if failed:
1354 if failed:
1355 return 1
1355 return 1
1356 if warned:
1356 if warned:
1357 return 80
1357 return 80
1358
1358
1359 def _gettest(self, test, count, asunit=False):
1359 def _gettest(self, test, count, asunit=False):
1360 """Obtain a Test by looking at its filename.
1360 """Obtain a Test by looking at its filename.
1361
1361
1362 Returns a Test instance. The Test may not be runnable if it doesn't
1362 Returns a Test instance. The Test may not be runnable if it doesn't
1363 map to a known type.
1363 map to a known type.
1364 """
1364 """
1365 lctest = test.lower()
1365 lctest = test.lower()
1366 refpath = os.path.join(self.testdir, test)
1366 refpath = os.path.join(self.testdir, test)
1367
1367
1368 testcls = Test
1368 testcls = Test
1369
1369
1370 for ext, cls, out in self.TESTTYPES:
1370 for ext, cls, out in self.TESTTYPES:
1371 if lctest.endswith(ext):
1371 if lctest.endswith(ext):
1372 testcls = cls
1372 testcls = cls
1373 refpath = os.path.join(self.testdir, test + out)
1373 refpath = os.path.join(self.testdir, test + out)
1374 break
1374 break
1375
1375
1376 t = testcls(self, test, count, refpath, unittest=asunit)
1376 t = testcls(self, test, count, refpath, unittest=asunit)
1377
1377
1378 if not asunit:
1378 if not asunit:
1379 return t
1379 return t
1380
1380
1381 class MercurialTest(unittest.TestCase):
1381 class MercurialTest(unittest.TestCase):
1382 def __init__(self, name, *args, **kwargs):
1382 def __init__(self, name, *args, **kwargs):
1383 super(MercurialTest, self).__init__(*args, **kwargs)
1383 super(MercurialTest, self).__init__(*args, **kwargs)
1384 self.name = name
1384 self.name = name
1385
1385
1386 def shortDescription(self):
1386 def shortDescription(self):
1387 return self.name
1387 return self.name
1388
1388
1389 # Need to stash away the TestResult since we do custom things
1389 # Need to stash away the TestResult since we do custom things
1390 # with it.
1390 # with it.
1391 def run(self, result):
1391 def run(self, result):
1392 result.startTest(self)
1392 result.startTest(self)
1393 try:
1393 try:
1394 try:
1394 try:
1395 t.setUp()
1395 t.setUp()
1396 except (KeyboardInterrupt, SystemExit):
1396 except (KeyboardInterrupt, SystemExit):
1397 raise
1397 raise
1398 except Exception:
1398 except Exception:
1399 result.addError(self, sys.exc_info())
1399 result.addError(self, sys.exc_info())
1400 return
1400 return
1401
1401
1402 success = False
1402 success = False
1403 try:
1403 try:
1404 self.runTest()
1404 self.runTest()
1405 except KeyboardInterrupt:
1405 except KeyboardInterrupt:
1406 raise
1406 raise
1407 except SkipTest, e:
1407 except SkipTest, e:
1408 result.addSkip(self, str(e))
1408 result.addSkip(self, str(e))
1409 except IgnoreTest, e:
1409 except IgnoreTest, e:
1410 result.addIgnore(self, str(e))
1410 result.addIgnore(self, str(e))
1411 except WarnTest, e:
1411 except WarnTest, e:
1412 result.addWarn(self, str(e))
1412 result.addWarn(self, str(e))
1413 except self.failureException:
1413 except self.failureException:
1414 result.addFailure(self, sys.exc_info())
1414 result.addFailure(self, sys.exc_info())
1415 except Exception:
1415 except Exception:
1416 result.addError(self, sys.exc_info())
1416 result.addError(self, sys.exc_info())
1417 else:
1417 else:
1418 success = True
1418 success = True
1419
1419
1420 try:
1420 try:
1421 t.tearDown()
1421 t.tearDown()
1422 except (KeyboardInterrupt, SystemExit):
1422 except (KeyboardInterrupt, SystemExit):
1423 raise
1423 raise
1424 except Exception:
1424 except Exception:
1425 result.addError(self, sys.exc_info())
1425 result.addError(self, sys.exc_info())
1426 success = False
1426 success = False
1427
1427
1428 if success:
1428 if success:
1429 result.addSuccess(self)
1429 result.addSuccess(self)
1430 finally:
1430 finally:
1431 result.stopTest(self)
1431 result.stopTest(self)
1432
1432
1433 def runTest(self):
1433 def runTest(self):
1434 code, tname, msg = t.run()
1434 code, tname, msg = t.run()
1435
1435
1436 # All non-success conditions should be exceptions and should
1436 # All non-success conditions should be exceptions and should
1437 # be caught in run().
1437 # be caught in run().
1438 assert code == '.'
1438 assert code == '.'
1439
1439
1440 # We need this proxy until tearDown() is implemented.
1440 # We need this proxy until tearDown() is implemented.
1441 def cleanup(self):
1441 def cleanup(self):
1442 return t.cleanup()
1442 return t.cleanup()
1443
1443
1444 return MercurialTest(test)
1444 return MercurialTest(test)
1445
1445
1446 def _cleanup(self):
1446 def _cleanup(self):
1447 """Clean up state from this test invocation."""
1447 """Clean up state from this test invocation."""
1448
1448
1449 if self.options.keep_tmpdir:
1449 if self.options.keep_tmpdir:
1450 return
1450 return
1451
1451
1452 vlog("# Cleaning up HGTMP", self.hgtmp)
1452 vlog("# Cleaning up HGTMP", self.hgtmp)
1453 shutil.rmtree(self.hgtmp, True)
1453 shutil.rmtree(self.hgtmp, True)
1454 for f in self._createdfiles:
1454 for f in self._createdfiles:
1455 try:
1455 try:
1456 os.remove(f)
1456 os.remove(f)
1457 except OSError:
1457 except OSError:
1458 pass
1458 pass
1459
1459
1460 def _usecorrectpython(self):
1460 def _usecorrectpython(self):
1461 # Some tests run the Python interpreter. They must use the
1461 # Some tests run the Python interpreter. They must use the
1462 # same interpreter or bad things will happen.
1462 # same interpreter or bad things will happen.
1463 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1463 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1464 if getattr(os, 'symlink', None):
1464 if getattr(os, 'symlink', None):
1465 vlog("# Making python executable in test path a symlink to '%s'" %
1465 vlog("# Making python executable in test path a symlink to '%s'" %
1466 sys.executable)
1466 sys.executable)
1467 mypython = os.path.join(self.tmpbindir, pyexename)
1467 mypython = os.path.join(self.tmpbindir, pyexename)
1468 try:
1468 try:
1469 if os.readlink(mypython) == sys.executable:
1469 if os.readlink(mypython) == sys.executable:
1470 return
1470 return
1471 os.unlink(mypython)
1471 os.unlink(mypython)
1472 except OSError, err:
1472 except OSError, err:
1473 if err.errno != errno.ENOENT:
1473 if err.errno != errno.ENOENT:
1474 raise
1474 raise
1475 if self._findprogram(pyexename) != sys.executable:
1475 if self._findprogram(pyexename) != sys.executable:
1476 try:
1476 try:
1477 os.symlink(sys.executable, mypython)
1477 os.symlink(sys.executable, mypython)
1478 self._createdfiles.append(mypython)
1478 self._createdfiles.append(mypython)
1479 except OSError, err:
1479 except OSError, err:
1480 # child processes may race, which is harmless
1480 # child processes may race, which is harmless
1481 if err.errno != errno.EEXIST:
1481 if err.errno != errno.EEXIST:
1482 raise
1482 raise
1483 else:
1483 else:
1484 exedir, exename = os.path.split(sys.executable)
1484 exedir, exename = os.path.split(sys.executable)
1485 vlog("# Modifying search path to find %s as %s in '%s'" %
1485 vlog("# Modifying search path to find %s as %s in '%s'" %
1486 (exename, pyexename, exedir))
1486 (exename, pyexename, exedir))
1487 path = os.environ['PATH'].split(os.pathsep)
1487 path = os.environ['PATH'].split(os.pathsep)
1488 while exedir in path:
1488 while exedir in path:
1489 path.remove(exedir)
1489 path.remove(exedir)
1490 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1490 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1491 if not self._findprogram(pyexename):
1491 if not self._findprogram(pyexename):
1492 print "WARNING: Cannot find %s in search path" % pyexename
1492 print "WARNING: Cannot find %s in search path" % pyexename
1493
1493
1494 def _installhg(self):
1494 def _installhg(self):
1495 vlog("# Performing temporary installation of HG")
1495 vlog("# Performing temporary installation of HG")
1496 installerrs = os.path.join("tests", "install.err")
1496 installerrs = os.path.join("tests", "install.err")
1497 compiler = ''
1497 compiler = ''
1498 if self.options.compiler:
1498 if self.options.compiler:
1499 compiler = '--compiler ' + self.options.compiler
1499 compiler = '--compiler ' + self.options.compiler
1500 pure = self.options.pure and "--pure" or ""
1500 pure = self.options.pure and "--pure" or ""
1501 py3 = ''
1501 py3 = ''
1502 if sys.version_info[0] == 3:
1502 if sys.version_info[0] == 3:
1503 py3 = '--c2to3'
1503 py3 = '--c2to3'
1504
1504
1505 # Run installer in hg root
1505 # Run installer in hg root
1506 script = os.path.realpath(sys.argv[0])
1506 script = os.path.realpath(sys.argv[0])
1507 hgroot = os.path.dirname(os.path.dirname(script))
1507 hgroot = os.path.dirname(os.path.dirname(script))
1508 os.chdir(hgroot)
1508 os.chdir(hgroot)
1509 nohome = '--home=""'
1509 nohome = '--home=""'
1510 if os.name == 'nt':
1510 if os.name == 'nt':
1511 # The --home="" trick works only on OS where os.sep == '/'
1511 # The --home="" trick works only on OS where os.sep == '/'
1512 # because of a distutils convert_path() fast-path. Avoid it at
1512 # because of a distutils convert_path() fast-path. Avoid it at
1513 # least on Windows for now, deal with .pydistutils.cfg bugs
1513 # least on Windows for now, deal with .pydistutils.cfg bugs
1514 # when they happen.
1514 # when they happen.
1515 nohome = ''
1515 nohome = ''
1516 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1516 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1517 ' build %(compiler)s --build-base="%(base)s"'
1517 ' build %(compiler)s --build-base="%(base)s"'
1518 ' install --force --prefix="%(prefix)s"'
1518 ' install --force --prefix="%(prefix)s"'
1519 ' --install-lib="%(libdir)s"'
1519 ' --install-lib="%(libdir)s"'
1520 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1520 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1521 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1521 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1522 'compiler': compiler,
1522 'compiler': compiler,
1523 'base': os.path.join(self.hgtmp, "build"),
1523 'base': os.path.join(self.hgtmp, "build"),
1524 'prefix': self.inst, 'libdir': self.pythondir,
1524 'prefix': self.inst, 'libdir': self.pythondir,
1525 'bindir': self.bindir,
1525 'bindir': self.bindir,
1526 'nohome': nohome, 'logfile': installerrs})
1526 'nohome': nohome, 'logfile': installerrs})
1527 vlog("# Running", cmd)
1527 vlog("# Running", cmd)
1528 if os.system(cmd) == 0:
1528 if os.system(cmd) == 0:
1529 if not self.options.verbose:
1529 if not self.options.verbose:
1530 os.remove(installerrs)
1530 os.remove(installerrs)
1531 else:
1531 else:
1532 f = open(installerrs)
1532 f = open(installerrs)
1533 for line in f:
1533 for line in f:
1534 print line,
1534 print line,
1535 f.close()
1535 f.close()
1536 sys.exit(1)
1536 sys.exit(1)
1537 os.chdir(self.testdir)
1537 os.chdir(self.testdir)
1538
1538
1539 self._usecorrectpython()
1539 self._usecorrectpython()
1540
1540
1541 if self.options.py3k_warnings and not self.options.anycoverage:
1541 if self.options.py3k_warnings and not self.options.anycoverage:
1542 vlog("# Updating hg command to enable Py3k Warnings switch")
1542 vlog("# Updating hg command to enable Py3k Warnings switch")
1543 f = open(os.path.join(self.bindir, 'hg'), 'r')
1543 f = open(os.path.join(self.bindir, 'hg'), 'r')
1544 lines = [line.rstrip() for line in f]
1544 lines = [line.rstrip() for line in f]
1545 lines[0] += ' -3'
1545 lines[0] += ' -3'
1546 f.close()
1546 f.close()
1547 f = open(os.path.join(self.bindir, 'hg'), 'w')
1547 f = open(os.path.join(self.bindir, 'hg'), 'w')
1548 for line in lines:
1548 for line in lines:
1549 f.write(line + '\n')
1549 f.write(line + '\n')
1550 f.close()
1550 f.close()
1551
1551
1552 hgbat = os.path.join(self.bindir, 'hg.bat')
1552 hgbat = os.path.join(self.bindir, 'hg.bat')
1553 if os.path.isfile(hgbat):
1553 if os.path.isfile(hgbat):
1554 # hg.bat expects to be put in bin/scripts while run-tests.py
1554 # hg.bat expects to be put in bin/scripts while run-tests.py
1555 # installation layout put it in bin/ directly. Fix it
1555 # installation layout put it in bin/ directly. Fix it
1556 f = open(hgbat, 'rb')
1556 f = open(hgbat, 'rb')
1557 data = f.read()
1557 data = f.read()
1558 f.close()
1558 f.close()
1559 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1559 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1560 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1560 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1561 '"%~dp0python" "%~dp0hg" %*')
1561 '"%~dp0python" "%~dp0hg" %*')
1562 f = open(hgbat, 'wb')
1562 f = open(hgbat, 'wb')
1563 f.write(data)
1563 f.write(data)
1564 f.close()
1564 f.close()
1565 else:
1565 else:
1566 print 'WARNING: cannot fix hg.bat reference to python.exe'
1566 print 'WARNING: cannot fix hg.bat reference to python.exe'
1567
1567
1568 if self.options.anycoverage:
1568 if self.options.anycoverage:
1569 custom = os.path.join(self.testdir, 'sitecustomize.py')
1569 custom = os.path.join(self.testdir, 'sitecustomize.py')
1570 target = os.path.join(self.pythondir, 'sitecustomize.py')
1570 target = os.path.join(self.pythondir, 'sitecustomize.py')
1571 vlog('# Installing coverage trigger to %s' % target)
1571 vlog('# Installing coverage trigger to %s' % target)
1572 shutil.copyfile(custom, target)
1572 shutil.copyfile(custom, target)
1573 rc = os.path.join(self.testdir, '.coveragerc')
1573 rc = os.path.join(self.testdir, '.coveragerc')
1574 vlog('# Installing coverage rc to %s' % rc)
1574 vlog('# Installing coverage rc to %s' % rc)
1575 os.environ['COVERAGE_PROCESS_START'] = rc
1575 os.environ['COVERAGE_PROCESS_START'] = rc
1576 fn = os.path.join(self.inst, '..', '.coverage')
1576 fn = os.path.join(self.inst, '..', '.coverage')
1577 os.environ['COVERAGE_FILE'] = fn
1577 os.environ['COVERAGE_FILE'] = fn
1578
1578
1579 def _checkhglib(self, verb):
1579 def _checkhglib(self, verb):
1580 """Ensure that the 'mercurial' package imported by python is
1580 """Ensure that the 'mercurial' package imported by python is
1581 the one we expect it to be. If not, print a warning to stderr."""
1581 the one we expect it to be. If not, print a warning to stderr."""
1582 expecthg = os.path.join(self.pythondir, 'mercurial')
1582 expecthg = os.path.join(self.pythondir, 'mercurial')
1583 actualhg = self._gethgpath()
1583 actualhg = self._gethgpath()
1584 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1584 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1585 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1585 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1586 ' (expected %s)\n'
1586 ' (expected %s)\n'
1587 % (verb, actualhg, expecthg))
1587 % (verb, actualhg, expecthg))
1588 def _gethgpath(self):
1588 def _gethgpath(self):
1589 """Return the path to the mercurial package that is actually found by
1589 """Return the path to the mercurial package that is actually found by
1590 the current Python interpreter."""
1590 the current Python interpreter."""
1591 if self._hgpath is not None:
1591 if self._hgpath is not None:
1592 return self._hgpath
1592 return self._hgpath
1593
1593
1594 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1594 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1595 pipe = os.popen(cmd % PYTHON)
1595 pipe = os.popen(cmd % PYTHON)
1596 try:
1596 try:
1597 self._hgpath = pipe.read().strip()
1597 self._hgpath = pipe.read().strip()
1598 finally:
1598 finally:
1599 pipe.close()
1599 pipe.close()
1600
1600
1601 return self._hgpath
1601 return self._hgpath
1602
1602
1603 def _outputtimes(self):
1603 def _outputtimes(self):
1604 vlog('# Producing time report')
1604 vlog('# Producing time report')
1605 self.times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1605 self.times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1606 cols = '%7.3f %s'
1606 cols = '%7.3f %s'
1607 print '\n%-7s %s' % ('Time', 'Test')
1607 print '\n%-7s %s' % ('Time', 'Test')
1608 for test, timetaken in self.times:
1608 for test, timetaken in self.times:
1609 print cols % (timetaken, test)
1609 print cols % (timetaken, test)
1610
1610
1611 def _outputcoverage(self):
1611 def _outputcoverage(self):
1612 vlog('# Producing coverage report')
1612 vlog('# Producing coverage report')
1613 os.chdir(self.pythondir)
1613 os.chdir(self.pythondir)
1614
1614
1615 def covrun(*args):
1615 def covrun(*args):
1616 cmd = 'coverage %s' % ' '.join(args)
1616 cmd = 'coverage %s' % ' '.join(args)
1617 vlog('# Running: %s' % cmd)
1617 vlog('# Running: %s' % cmd)
1618 os.system(cmd)
1618 os.system(cmd)
1619
1619
1620 covrun('-c')
1620 covrun('-c')
1621 omit = ','.join(os.path.join(x, '*') for x in
1621 omit = ','.join(os.path.join(x, '*') for x in
1622 [self.bindir, self.testdir])
1622 [self.bindir, self.testdir])
1623 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1623 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1624 if self.options.htmlcov:
1624 if self.options.htmlcov:
1625 htmldir = os.path.join(self.testdir, 'htmlcov')
1625 htmldir = os.path.join(self.testdir, 'htmlcov')
1626 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1626 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1627 '"--omit=%s"' % omit)
1627 '"--omit=%s"' % omit)
1628 if self.options.annotate:
1628 if self.options.annotate:
1629 adir = os.path.join(self.testdir, 'annotated')
1629 adir = os.path.join(self.testdir, 'annotated')
1630 if not os.path.isdir(adir):
1630 if not os.path.isdir(adir):
1631 os.mkdir(adir)
1631 os.mkdir(adir)
1632 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1632 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1633
1633
1634 def _executetests(self, tests, result=None):
1634 def _executetests(self, tests, result=None):
1635 # We copy because we modify the list.
1635 # We copy because we modify the list.
1636 tests = list(tests)
1636 tests = list(tests)
1637
1637
1638 jobs = self.options.jobs
1638 jobs = self.options.jobs
1639 done = queue.Queue()
1639 done = queue.Queue()
1640 running = 0
1640 running = 0
1641
1641
1642 def job(test, result):
1642 def job(test, result):
1643 try:
1643 try:
1644 # If in unittest mode.
1644 # If in unittest mode.
1645 if result:
1645 if result:
1646 test(result)
1646 test(result)
1647 # We need to put something here to make the logic happy.
1647 # We need to put something here to make the logic happy.
1648 # This will get cleaned up later.
1648 # This will get cleaned up later.
1649 done.put(('u', None, None))
1649 done.put(('u', None, None))
1650 else:
1650 else:
1651 done.put(test.run())
1651 done.put(test.run())
1652 test.cleanup()
1652 test.cleanup()
1653 except KeyboardInterrupt:
1653 except KeyboardInterrupt:
1654 pass
1654 pass
1655 except: # re-raises
1655 except: # re-raises
1656 done.put(('!', test, 'run-test raised an error, see traceback'))
1656 done.put(('!', test, 'run-test raised an error, see traceback'))
1657 raise
1657 raise
1658
1658
1659 try:
1659 try:
1660 while tests or running:
1660 while tests or running:
1661 if not done.empty() or running == jobs or not tests:
1661 if not done.empty() or running == jobs or not tests:
1662 try:
1662 try:
1663 code, test, msg = done.get(True, 1)
1663 code, test, msg = done.get(True, 1)
1664 self.results[code].append((test, msg))
1664 self.results[code].append((test, msg))
1665 if self.options.first and code not in '.si':
1665 if self.options.first and code not in '.si':
1666 break
1666 break
1667 except queue.Empty:
1667 except queue.Empty:
1668 continue
1668 continue
1669 running -= 1
1669 running -= 1
1670 if tests and not running == jobs:
1670 if tests and not running == jobs:
1671 test = tests.pop(0)
1671 test = tests.pop(0)
1672 if self.options.loop:
1672 if self.options.loop:
1673 tests.append(test)
1673 tests.append(test)
1674 t = threading.Thread(target=job, name=test.name,
1674 t = threading.Thread(target=job, name=test.name,
1675 args=(test, result))
1675 args=(test, result))
1676 t.start()
1676 t.start()
1677 running += 1
1677 running += 1
1678 except KeyboardInterrupt:
1678 except KeyboardInterrupt:
1679 self.abort[0] = True
1679 self.abort[0] = True
1680
1680
1681 def _findprogram(self, program):
1681 def _findprogram(self, program):
1682 """Search PATH for a executable program"""
1682 """Search PATH for a executable program"""
1683 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1683 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1684 name = os.path.join(p, program)
1684 name = os.path.join(p, program)
1685 if os.name == 'nt' or os.access(name, os.X_OK):
1685 if os.name == 'nt' or os.access(name, os.X_OK):
1686 return name
1686 return name
1687 return None
1687 return None
1688
1688
1689 def _checktools(self):
1689 def _checktools(self):
1690 # Before we go any further, check for pre-requisite tools
1690 # Before we go any further, check for pre-requisite tools
1691 # stuff from coreutils (cat, rm, etc) are not tested
1691 # stuff from coreutils (cat, rm, etc) are not tested
1692 for p in self.REQUIREDTOOLS:
1692 for p in self.REQUIREDTOOLS:
1693 if os.name == 'nt' and not p.endswith('.exe'):
1693 if os.name == 'nt' and not p.endswith('.exe'):
1694 p += '.exe'
1694 p += '.exe'
1695 found = self._findprogram(p)
1695 found = self._findprogram(p)
1696 if found:
1696 if found:
1697 vlog("# Found prerequisite", p, "at", found)
1697 vlog("# Found prerequisite", p, "at", found)
1698 else:
1698 else:
1699 print "WARNING: Did not find prerequisite tool: %s " % p
1699 print "WARNING: Did not find prerequisite tool: %s " % p
1700
1700
1701 if __name__ == '__main__':
1701 if __name__ == '__main__':
1702 runner = TestRunner()
1702 runner = TestRunner()
1703 sys.exit(runner.run(sys.argv[1:]))
1703 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now