##// END OF EJS Templates
run-tests: kill daemons during tearDown()
Gregory Szorc -
r21456:a06a4142 default
parent child Browse files
Show More
@@ -1,1703 +1,1705 b''
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'])
460
461 def describe(ret):
459 def describe(ret):
462 if ret < 0:
460 if ret < 0:
463 return 'killed by signal: %d' % -ret
461 return 'killed by signal: %d' % -ret
464 return 'returned error code %d' % ret
462 return 'returned error code %d' % ret
465
463
466 self._skipped = False
464 self._skipped = False
467
465
468 if ret == self.SKIPPED_STATUS:
466 if ret == self.SKIPPED_STATUS:
469 if out is None: # Debug mode, nothing to parse.
467 if out is None: # Debug mode, nothing to parse.
470 missing = ['unknown']
468 missing = ['unknown']
471 failed = None
469 failed = None
472 else:
470 else:
473 missing, failed = TTest.parsehghaveoutput(out)
471 missing, failed = TTest.parsehghaveoutput(out)
474
472
475 if not missing:
473 if not missing:
476 missing = ['irrelevant']
474 missing = ['irrelevant']
477
475
478 if failed:
476 if failed:
479 self._result = self.fail('hg have failed checking for %s' %
477 self._result = self.fail('hg have failed checking for %s' %
480 failed[-1], ret)
478 failed[-1], ret)
481 else:
479 else:
482 self._skipped = True
480 self._skipped = True
483 self._result = self.skip(missing[-1])
481 self._result = self.skip(missing[-1])
484 elif ret == 'timeout':
482 elif ret == 'timeout':
485 self._result = self.fail('timed out', ret)
483 self._result = self.fail('timed out', ret)
486 elif out != self._refout:
484 elif out != self._refout:
487 info = {}
485 info = {}
488 if not options.nodiff:
486 if not options.nodiff:
489 iolock.acquire()
487 iolock.acquire()
490 if options.view:
488 if options.view:
491 os.system("%s %s %s" % (options.view, self._refpath,
489 os.system("%s %s %s" % (options.view, self._refpath,
492 self._errpath))
490 self._errpath))
493 else:
491 else:
494 info = showdiff(self._refout, out, self._refpath,
492 info = showdiff(self._refout, out, self._refpath,
495 self._errpath)
493 self._errpath)
496 iolock.release()
494 iolock.release()
497 msg = ''
495 msg = ''
498 if info.get('servefail'):
496 if info.get('servefail'):
499 msg += 'serve failed and '
497 msg += 'serve failed and '
500 if ret:
498 if ret:
501 msg += 'output changed and ' + describe(ret)
499 msg += 'output changed and ' + describe(ret)
502 else:
500 else:
503 msg += 'output changed'
501 msg += 'output changed'
504
502
505 if (ret != 0 or out != self._refout) and not self._skipped \
503 if (ret != 0 or out != self._refout) and not self._skipped \
506 and not options.debug:
504 and not options.debug:
507 f = open(self._errpath, 'wb')
505 f = open(self._errpath, 'wb')
508 for line in out:
506 for line in out:
509 f.write(line)
507 f.write(line)
510 f.close()
508 f.close()
511
509
512 self._result = self.fail(msg, ret)
510 self._result = self.fail(msg, ret)
513 elif ret:
511 elif ret:
514 self._result = self.fail(describe(ret), ret)
512 self._result = self.fail(describe(ret), ret)
515 else:
513 else:
516 self._result = self.success()
514 self._result = self.success()
517
515
518 if not self._unittest:
516 if not self._unittest:
519 self.tearDown()
517 self.tearDown()
520
518
521 return self._result
519 return self._result
522
520
523 def tearDown(self):
521 def tearDown(self):
524 """Tasks to perform after run()."""
522 """Tasks to perform after run()."""
523 for entry in self._daemonpids:
524 killdaemons(entry)
525 self._daemonpids = []
526
525 if not self._options.keep_tmpdir:
527 if not self._options.keep_tmpdir:
526 shutil.rmtree(self._testtmp)
528 shutil.rmtree(self._testtmp)
527
529
528 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
530 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
529 and not self._options.debug and self._out:
531 and not self._options.debug and self._out:
530 f = open(self._errpath, 'wb')
532 f = open(self._errpath, 'wb')
531 for line in self._out:
533 for line in self._out:
532 f.write(line)
534 f.write(line)
533 f.close()
535 f.close()
534
536
535 vlog("# Ret was:", self._ret)
537 vlog("# Ret was:", self._ret)
536
538
537 # Don't print progress in unittest mode because that is handled
539 # Don't print progress in unittest mode because that is handled
538 # by TestResult.
540 # by TestResult.
539 if not self._options.verbose and not self._unittest:
541 if not self._options.verbose and not self._unittest:
540 iolock.acquire()
542 iolock.acquire()
541 sys.stdout.write(self._result[0])
543 sys.stdout.write(self._result[0])
542 sys.stdout.flush()
544 sys.stdout.flush()
543 iolock.release()
545 iolock.release()
544
546
545 self._runner.times.append((self.name, self._duration))
547 self._runner.times.append((self.name, self._duration))
546
548
547 def _run(self, replacements, env):
549 def _run(self, replacements, env):
548 # This should be implemented in child classes to run tests.
550 # This should be implemented in child classes to run tests.
549 return self._skip('unknown test type')
551 return self._skip('unknown test type')
550
552
551 def _getreplacements(self):
553 def _getreplacements(self):
552 port = self._options.port + self._count * 3
554 port = self._options.port + self._count * 3
553 r = [
555 r = [
554 (r':%s\b' % port, ':$HGPORT'),
556 (r':%s\b' % port, ':$HGPORT'),
555 (r':%s\b' % (port + 1), ':$HGPORT1'),
557 (r':%s\b' % (port + 1), ':$HGPORT1'),
556 (r':%s\b' % (port + 2), ':$HGPORT2'),
558 (r':%s\b' % (port + 2), ':$HGPORT2'),
557 ]
559 ]
558
560
559 if os.name == 'nt':
561 if os.name == 'nt':
560 r.append(
562 r.append(
561 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
563 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
562 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
564 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
563 for c in self._testtmp), '$TESTTMP'))
565 for c in self._testtmp), '$TESTTMP'))
564 else:
566 else:
565 r.append((re.escape(self._testtmp), '$TESTTMP'))
567 r.append((re.escape(self._testtmp), '$TESTTMP'))
566
568
567 return r, port
569 return r, port
568
570
569 def _getenv(self, port):
571 def _getenv(self, port):
570 env = os.environ.copy()
572 env = os.environ.copy()
571 env['TESTTMP'] = self._testtmp
573 env['TESTTMP'] = self._testtmp
572 env['HOME'] = self._testtmp
574 env['HOME'] = self._testtmp
573 env["HGPORT"] = str(port)
575 env["HGPORT"] = str(port)
574 env["HGPORT1"] = str(port + 1)
576 env["HGPORT1"] = str(port + 1)
575 env["HGPORT2"] = str(port + 2)
577 env["HGPORT2"] = str(port + 2)
576 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
578 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
577 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
579 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
578 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
580 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
579 env["HGMERGE"] = "internal:merge"
581 env["HGMERGE"] = "internal:merge"
580 env["HGUSER"] = "test"
582 env["HGUSER"] = "test"
581 env["HGENCODING"] = "ascii"
583 env["HGENCODING"] = "ascii"
582 env["HGENCODINGMODE"] = "strict"
584 env["HGENCODINGMODE"] = "strict"
583
585
584 # Reset some environment variables to well-known values so that
586 # Reset some environment variables to well-known values so that
585 # the tests produce repeatable output.
587 # the tests produce repeatable output.
586 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
588 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
587 env['TZ'] = 'GMT'
589 env['TZ'] = 'GMT'
588 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
590 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
589 env['COLUMNS'] = '80'
591 env['COLUMNS'] = '80'
590 env['TERM'] = 'xterm'
592 env['TERM'] = 'xterm'
591
593
592 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
594 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
593 'NO_PROXY').split():
595 'NO_PROXY').split():
594 if k in env:
596 if k in env:
595 del env[k]
597 del env[k]
596
598
597 # unset env related to hooks
599 # unset env related to hooks
598 for k in env.keys():
600 for k in env.keys():
599 if k.startswith('HG_'):
601 if k.startswith('HG_'):
600 del env[k]
602 del env[k]
601
603
602 return env
604 return env
603
605
604 def _createhgrc(self, path):
606 def _createhgrc(self, path):
605 # create a fresh hgrc
607 # create a fresh hgrc
606 hgrc = open(path, 'w')
608 hgrc = open(path, 'w')
607 hgrc.write('[ui]\n')
609 hgrc.write('[ui]\n')
608 hgrc.write('slash = True\n')
610 hgrc.write('slash = True\n')
609 hgrc.write('interactive = False\n')
611 hgrc.write('interactive = False\n')
610 hgrc.write('[defaults]\n')
612 hgrc.write('[defaults]\n')
611 hgrc.write('backout = -d "0 0"\n')
613 hgrc.write('backout = -d "0 0"\n')
612 hgrc.write('commit = -d "0 0"\n')
614 hgrc.write('commit = -d "0 0"\n')
613 hgrc.write('shelve = --date "0 0"\n')
615 hgrc.write('shelve = --date "0 0"\n')
614 hgrc.write('tag = -d "0 0"\n')
616 hgrc.write('tag = -d "0 0"\n')
615 if self._options.extra_config_opt:
617 if self._options.extra_config_opt:
616 for opt in self._options.extra_config_opt:
618 for opt in self._options.extra_config_opt:
617 section, key = opt.split('.', 1)
619 section, key = opt.split('.', 1)
618 assert '=' in key, ('extra config opt %s must '
620 assert '=' in key, ('extra config opt %s must '
619 'have an = for assignment' % opt)
621 'have an = for assignment' % opt)
620 hgrc.write('[%s]\n%s\n' % (section, key))
622 hgrc.write('[%s]\n%s\n' % (section, key))
621 hgrc.close()
623 hgrc.close()
622
624
623 def success(self):
625 def success(self):
624 return '.', self.name, ''
626 return '.', self.name, ''
625
627
626 def fail(self, msg, ret):
628 def fail(self, msg, ret):
627 warned = ret is False
629 warned = ret is False
628 if not self._options.nodiff:
630 if not self._options.nodiff:
629 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', self.name,
631 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', self.name,
630 msg))
632 msg))
631 if (not ret and self._options.interactive and
633 if (not ret and self._options.interactive and
632 os.path.exists(self._errpath)):
634 os.path.exists(self._errpath)):
633 iolock.acquire()
635 iolock.acquire()
634 print 'Accept this change? [n] ',
636 print 'Accept this change? [n] ',
635 answer = sys.stdin.readline().strip()
637 answer = sys.stdin.readline().strip()
636 iolock.release()
638 iolock.release()
637 if answer.lower() in ('y', 'yes'):
639 if answer.lower() in ('y', 'yes'):
638 if self.name.endswith('.t'):
640 if self.name.endswith('.t'):
639 rename(self._errpath, self._path)
641 rename(self._errpath, self._path)
640 else:
642 else:
641 rename(self._errpath, '%s.out' % self._path)
643 rename(self._errpath, '%s.out' % self._path)
642
644
643 return '.', self.name, ''
645 return '.', self.name, ''
644
646
645 if self._unittest:
647 if self._unittest:
646 if warned:
648 if warned:
647 raise WarnTest(msg)
649 raise WarnTest(msg)
648 else:
650 else:
649 # unittest differentiates between errored and failed.
651 # unittest differentiates between errored and failed.
650 # Failed is denoted by AssertionError (by default at least).
652 # Failed is denoted by AssertionError (by default at least).
651 raise AssertionError(msg)
653 raise AssertionError(msg)
652
654
653 return warned and '~' or '!', self.name, msg
655 return warned and '~' or '!', self.name, msg
654
656
655 def skip(self, msg):
657 def skip(self, msg):
656 if self._unittest:
658 if self._unittest:
657 raise SkipTest(msg)
659 raise SkipTest(msg)
658
660
659 if self._options.verbose:
661 if self._options.verbose:
660 log("\nSkipping %s: %s" % (self._path, msg))
662 log("\nSkipping %s: %s" % (self._path, msg))
661
663
662 return 's', self.name, msg
664 return 's', self.name, msg
663
665
664 def ignore(self, msg):
666 def ignore(self, msg):
665 if self._unittest:
667 if self._unittest:
666 raise IgnoreTest(msg)
668 raise IgnoreTest(msg)
667
669
668 return 'i', self.name, msg
670 return 'i', self.name, msg
669
671
670 class PythonTest(Test):
672 class PythonTest(Test):
671 """A Python-based test."""
673 """A Python-based test."""
672 def _run(self, replacements, env):
674 def _run(self, replacements, env):
673 py3kswitch = self._options.py3k_warnings and ' -3' or ''
675 py3kswitch = self._options.py3k_warnings and ' -3' or ''
674 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
676 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
675 vlog("# Running", cmd)
677 vlog("# Running", cmd)
676 if os.name == 'nt':
678 if os.name == 'nt':
677 replacements.append((r'\r\n', '\n'))
679 replacements.append((r'\r\n', '\n'))
678 return run(cmd, self._testtmp, self._options, replacements, env,
680 return run(cmd, self._testtmp, self._options, replacements, env,
679 self._runner.abort)
681 self._runner.abort)
680
682
681 class TTest(Test):
683 class TTest(Test):
682 """A "t test" is a test backed by a .t file."""
684 """A "t test" is a test backed by a .t file."""
683
685
684 SKIPPED_PREFIX = 'skipped: '
686 SKIPPED_PREFIX = 'skipped: '
685 FAILED_PREFIX = 'hghave check failed: '
687 FAILED_PREFIX = 'hghave check failed: '
686 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
688 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
687
689
688 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
690 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(
691 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256)).update(
690 {'\\': '\\\\', '\r': r'\r'})
692 {'\\': '\\\\', '\r': r'\r'})
691
693
692 def _run(self, replacements, env):
694 def _run(self, replacements, env):
693 f = open(self._path)
695 f = open(self._path)
694 lines = f.readlines()
696 lines = f.readlines()
695 f.close()
697 f.close()
696
698
697 salt, script, after, expected = self._parsetest(lines)
699 salt, script, after, expected = self._parsetest(lines)
698
700
699 # Write out the generated script.
701 # Write out the generated script.
700 fname = '%s.sh' % self._testtmp
702 fname = '%s.sh' % self._testtmp
701 f = open(fname, 'w')
703 f = open(fname, 'w')
702 for l in script:
704 for l in script:
703 f.write(l)
705 f.write(l)
704 f.close()
706 f.close()
705
707
706 cmd = '%s "%s"' % (self._options.shell, fname)
708 cmd = '%s "%s"' % (self._options.shell, fname)
707 vlog("# Running", cmd)
709 vlog("# Running", cmd)
708
710
709 exitcode, output = run(cmd, self._testtmp, self._options, replacements,
711 exitcode, output = run(cmd, self._testtmp, self._options, replacements,
710 env, self._runner.abort)
712 env, self._runner.abort)
711 # Do not merge output if skipped. Return hghave message instead.
713 # Do not merge output if skipped. Return hghave message instead.
712 # Similarly, with --debug, output is None.
714 # Similarly, with --debug, output is None.
713 if exitcode == self.SKIPPED_STATUS or output is None:
715 if exitcode == self.SKIPPED_STATUS or output is None:
714 return exitcode, output
716 return exitcode, output
715
717
716 return self._processoutput(exitcode, output, salt, after, expected)
718 return self._processoutput(exitcode, output, salt, after, expected)
717
719
718 def _hghave(self, reqs):
720 def _hghave(self, reqs):
719 # TODO do something smarter when all other uses of hghave are gone.
721 # TODO do something smarter when all other uses of hghave are gone.
720 tdir = self._testdir.replace('\\', '/')
722 tdir = self._testdir.replace('\\', '/')
721 proc = Popen4('%s -c "%s/hghave %s"' %
723 proc = Popen4('%s -c "%s/hghave %s"' %
722 (self._options.shell, tdir, ' '.join(reqs)),
724 (self._options.shell, tdir, ' '.join(reqs)),
723 self._testtmp, 0)
725 self._testtmp, 0)
724 stdout, stderr = proc.communicate()
726 stdout, stderr = proc.communicate()
725 ret = proc.wait()
727 ret = proc.wait()
726 if wifexited(ret):
728 if wifexited(ret):
727 ret = os.WEXITSTATUS(ret)
729 ret = os.WEXITSTATUS(ret)
728 if ret == 2:
730 if ret == 2:
729 print stdout
731 print stdout
730 sys.exit(1)
732 sys.exit(1)
731
733
732 return ret == 0
734 return ret == 0
733
735
734 def _parsetest(self, lines):
736 def _parsetest(self, lines):
735 # We generate a shell script which outputs unique markers to line
737 # We generate a shell script which outputs unique markers to line
736 # up script results with our source. These markers include input
738 # up script results with our source. These markers include input
737 # line number and the last return code.
739 # line number and the last return code.
738 salt = "SALT" + str(time.time())
740 salt = "SALT" + str(time.time())
739 def addsalt(line, inpython):
741 def addsalt(line, inpython):
740 if inpython:
742 if inpython:
741 script.append('%s %d 0\n' % (salt, line))
743 script.append('%s %d 0\n' % (salt, line))
742 else:
744 else:
743 script.append('echo %s %s $?\n' % (salt, line))
745 script.append('echo %s %s $?\n' % (salt, line))
744
746
745 script = []
747 script = []
746
748
747 # After we run the shell script, we re-unify the script output
749 # After we run the shell script, we re-unify the script output
748 # with non-active parts of the source, with synchronization by our
750 # with non-active parts of the source, with synchronization by our
749 # SALT line number markers. The after table contains the non-active
751 # SALT line number markers. The after table contains the non-active
750 # components, ordered by line number.
752 # components, ordered by line number.
751 after = {}
753 after = {}
752
754
753 # Expected shell script output.
755 # Expected shell script output.
754 expected = {}
756 expected = {}
755
757
756 pos = prepos = -1
758 pos = prepos = -1
757
759
758 # True or False when in a true or false conditional section
760 # True or False when in a true or false conditional section
759 skipping = None
761 skipping = None
760
762
761 # We keep track of whether or not we're in a Python block so we
763 # We keep track of whether or not we're in a Python block so we
762 # can generate the surrounding doctest magic.
764 # can generate the surrounding doctest magic.
763 inpython = False
765 inpython = False
764
766
765 if self._options.debug:
767 if self._options.debug:
766 script.append('set -x\n')
768 script.append('set -x\n')
767 if os.getenv('MSYSTEM'):
769 if os.getenv('MSYSTEM'):
768 script.append('alias pwd="pwd -W"\n')
770 script.append('alias pwd="pwd -W"\n')
769
771
770 for n, l in enumerate(lines):
772 for n, l in enumerate(lines):
771 if not l.endswith('\n'):
773 if not l.endswith('\n'):
772 l += '\n'
774 l += '\n'
773 if l.startswith('#if'):
775 if l.startswith('#if'):
774 lsplit = l.split()
776 lsplit = l.split()
775 if len(lsplit) < 2 or lsplit[0] != '#if':
777 if len(lsplit) < 2 or lsplit[0] != '#if':
776 after.setdefault(pos, []).append(' !!! invalid #if\n')
778 after.setdefault(pos, []).append(' !!! invalid #if\n')
777 if skipping is not None:
779 if skipping is not None:
778 after.setdefault(pos, []).append(' !!! nested #if\n')
780 after.setdefault(pos, []).append(' !!! nested #if\n')
779 skipping = not self._hghave(lsplit[1:])
781 skipping = not self._hghave(lsplit[1:])
780 after.setdefault(pos, []).append(l)
782 after.setdefault(pos, []).append(l)
781 elif l.startswith('#else'):
783 elif l.startswith('#else'):
782 if skipping is None:
784 if skipping is None:
783 after.setdefault(pos, []).append(' !!! missing #if\n')
785 after.setdefault(pos, []).append(' !!! missing #if\n')
784 skipping = not skipping
786 skipping = not skipping
785 after.setdefault(pos, []).append(l)
787 after.setdefault(pos, []).append(l)
786 elif l.startswith('#endif'):
788 elif l.startswith('#endif'):
787 if skipping is None:
789 if skipping is None:
788 after.setdefault(pos, []).append(' !!! missing #if\n')
790 after.setdefault(pos, []).append(' !!! missing #if\n')
789 skipping = None
791 skipping = None
790 after.setdefault(pos, []).append(l)
792 after.setdefault(pos, []).append(l)
791 elif skipping:
793 elif skipping:
792 after.setdefault(pos, []).append(l)
794 after.setdefault(pos, []).append(l)
793 elif l.startswith(' >>> '): # python inlines
795 elif l.startswith(' >>> '): # python inlines
794 after.setdefault(pos, []).append(l)
796 after.setdefault(pos, []).append(l)
795 prepos = pos
797 prepos = pos
796 pos = n
798 pos = n
797 if not inpython:
799 if not inpython:
798 # We've just entered a Python block. Add the header.
800 # We've just entered a Python block. Add the header.
799 inpython = True
801 inpython = True
800 addsalt(prepos, False) # Make sure we report the exit code.
802 addsalt(prepos, False) # Make sure we report the exit code.
801 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
803 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
802 addsalt(n, True)
804 addsalt(n, True)
803 script.append(l[2:])
805 script.append(l[2:])
804 elif l.startswith(' ... '): # python inlines
806 elif l.startswith(' ... '): # python inlines
805 after.setdefault(prepos, []).append(l)
807 after.setdefault(prepos, []).append(l)
806 script.append(l[2:])
808 script.append(l[2:])
807 elif l.startswith(' $ '): # commands
809 elif l.startswith(' $ '): # commands
808 if inpython:
810 if inpython:
809 script.append('EOF\n')
811 script.append('EOF\n')
810 inpython = False
812 inpython = False
811 after.setdefault(pos, []).append(l)
813 after.setdefault(pos, []).append(l)
812 prepos = pos
814 prepos = pos
813 pos = n
815 pos = n
814 addsalt(n, False)
816 addsalt(n, False)
815 cmd = l[4:].split()
817 cmd = l[4:].split()
816 if len(cmd) == 2 and cmd[0] == 'cd':
818 if len(cmd) == 2 and cmd[0] == 'cd':
817 l = ' $ cd %s || exit 1\n' % cmd[1]
819 l = ' $ cd %s || exit 1\n' % cmd[1]
818 script.append(l[4:])
820 script.append(l[4:])
819 elif l.startswith(' > '): # continuations
821 elif l.startswith(' > '): # continuations
820 after.setdefault(prepos, []).append(l)
822 after.setdefault(prepos, []).append(l)
821 script.append(l[4:])
823 script.append(l[4:])
822 elif l.startswith(' '): # results
824 elif l.startswith(' '): # results
823 # Queue up a list of expected results.
825 # Queue up a list of expected results.
824 expected.setdefault(pos, []).append(l[2:])
826 expected.setdefault(pos, []).append(l[2:])
825 else:
827 else:
826 if inpython:
828 if inpython:
827 script.append('EOF\n')
829 script.append('EOF\n')
828 inpython = False
830 inpython = False
829 # Non-command/result. Queue up for merged output.
831 # Non-command/result. Queue up for merged output.
830 after.setdefault(pos, []).append(l)
832 after.setdefault(pos, []).append(l)
831
833
832 if inpython:
834 if inpython:
833 script.append('EOF\n')
835 script.append('EOF\n')
834 if skipping is not None:
836 if skipping is not None:
835 after.setdefault(pos, []).append(' !!! missing #endif\n')
837 after.setdefault(pos, []).append(' !!! missing #endif\n')
836 addsalt(n + 1, False)
838 addsalt(n + 1, False)
837
839
838 return salt, script, after, expected
840 return salt, script, after, expected
839
841
840 def _processoutput(self, exitcode, output, salt, after, expected):
842 def _processoutput(self, exitcode, output, salt, after, expected):
841 # Merge the script output back into a unified test.
843 # Merge the script output back into a unified test.
842 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
844 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
843 if exitcode != 0:
845 if exitcode != 0:
844 warnonly = 3
846 warnonly = 3
845
847
846 pos = -1
848 pos = -1
847 postout = []
849 postout = []
848 for l in output:
850 for l in output:
849 lout, lcmd = l, None
851 lout, lcmd = l, None
850 if salt in l:
852 if salt in l:
851 lout, lcmd = l.split(salt, 1)
853 lout, lcmd = l.split(salt, 1)
852
854
853 if lout:
855 if lout:
854 if not lout.endswith('\n'):
856 if not lout.endswith('\n'):
855 lout += ' (no-eol)\n'
857 lout += ' (no-eol)\n'
856
858
857 # Find the expected output at the current position.
859 # Find the expected output at the current position.
858 el = None
860 el = None
859 if expected.get(pos, None):
861 if expected.get(pos, None):
860 el = expected[pos].pop(0)
862 el = expected[pos].pop(0)
861
863
862 r = TTest.linematch(el, lout)
864 r = TTest.linematch(el, lout)
863 if isinstance(r, str):
865 if isinstance(r, str):
864 if r == '+glob':
866 if r == '+glob':
865 lout = el[:-1] + ' (glob)\n'
867 lout = el[:-1] + ' (glob)\n'
866 r = '' # Warn only this line.
868 r = '' # Warn only this line.
867 elif r == '-glob':
869 elif r == '-glob':
868 lout = ''.join(el.rsplit(' (glob)', 1))
870 lout = ''.join(el.rsplit(' (glob)', 1))
869 r = '' # Warn only this line.
871 r = '' # Warn only this line.
870 else:
872 else:
871 log('\ninfo, unknown linematch result: %r\n' % r)
873 log('\ninfo, unknown linematch result: %r\n' % r)
872 r = False
874 r = False
873 if r:
875 if r:
874 postout.append(' ' + el)
876 postout.append(' ' + el)
875 else:
877 else:
876 if self.NEEDESCAPE(lout):
878 if self.NEEDESCAPE(lout):
877 lout = TTest.stringescape('%s (esc)\n' %
879 lout = TTest.stringescape('%s (esc)\n' %
878 lout.rstrip('\n'))
880 lout.rstrip('\n'))
879 postout.append(' ' + lout) # Let diff deal with it.
881 postout.append(' ' + lout) # Let diff deal with it.
880 if r != '': # If line failed.
882 if r != '': # If line failed.
881 warnonly = 3 # for sure not
883 warnonly = 3 # for sure not
882 elif warnonly == 1: # Is "not yet" and line is warn only.
884 elif warnonly == 1: # Is "not yet" and line is warn only.
883 warnonly = 2 # Yes do warn.
885 warnonly = 2 # Yes do warn.
884
886
885 if lcmd:
887 if lcmd:
886 # Add on last return code.
888 # Add on last return code.
887 ret = int(lcmd.split()[1])
889 ret = int(lcmd.split()[1])
888 if ret != 0:
890 if ret != 0:
889 postout.append(' [%s]\n' % ret)
891 postout.append(' [%s]\n' % ret)
890 if pos in after:
892 if pos in after:
891 # Merge in non-active test bits.
893 # Merge in non-active test bits.
892 postout += after.pop(pos)
894 postout += after.pop(pos)
893 pos = int(lcmd.split()[0])
895 pos = int(lcmd.split()[0])
894
896
895 if pos in after:
897 if pos in after:
896 postout += after.pop(pos)
898 postout += after.pop(pos)
897
899
898 if warnonly == 2:
900 if warnonly == 2:
899 exitcode = False # Set exitcode to warned.
901 exitcode = False # Set exitcode to warned.
900
902
901 return exitcode, postout
903 return exitcode, postout
902
904
903 @staticmethod
905 @staticmethod
904 def rematch(el, l):
906 def rematch(el, l):
905 try:
907 try:
906 # use \Z to ensure that the regex matches to the end of the string
908 # use \Z to ensure that the regex matches to the end of the string
907 if os.name == 'nt':
909 if os.name == 'nt':
908 return re.match(el + r'\r?\n\Z', l)
910 return re.match(el + r'\r?\n\Z', l)
909 return re.match(el + r'\n\Z', l)
911 return re.match(el + r'\n\Z', l)
910 except re.error:
912 except re.error:
911 # el is an invalid regex
913 # el is an invalid regex
912 return False
914 return False
913
915
914 @staticmethod
916 @staticmethod
915 def globmatch(el, l):
917 def globmatch(el, l):
916 # The only supported special characters are * and ? plus / which also
918 # The only supported special characters are * and ? plus / which also
917 # matches \ on windows. Escaping of these characters is supported.
919 # matches \ on windows. Escaping of these characters is supported.
918 if el + '\n' == l:
920 if el + '\n' == l:
919 if os.altsep:
921 if os.altsep:
920 # matching on "/" is not needed for this line
922 # matching on "/" is not needed for this line
921 return '-glob'
923 return '-glob'
922 return True
924 return True
923 i, n = 0, len(el)
925 i, n = 0, len(el)
924 res = ''
926 res = ''
925 while i < n:
927 while i < n:
926 c = el[i]
928 c = el[i]
927 i += 1
929 i += 1
928 if c == '\\' and el[i] in '*?\\/':
930 if c == '\\' and el[i] in '*?\\/':
929 res += el[i - 1:i + 1]
931 res += el[i - 1:i + 1]
930 i += 1
932 i += 1
931 elif c == '*':
933 elif c == '*':
932 res += '.*'
934 res += '.*'
933 elif c == '?':
935 elif c == '?':
934 res += '.'
936 res += '.'
935 elif c == '/' and os.altsep:
937 elif c == '/' and os.altsep:
936 res += '[/\\\\]'
938 res += '[/\\\\]'
937 else:
939 else:
938 res += re.escape(c)
940 res += re.escape(c)
939 return TTest.rematch(res, l)
941 return TTest.rematch(res, l)
940
942
941 @staticmethod
943 @staticmethod
942 def linematch(el, l):
944 def linematch(el, l):
943 if el == l: # perfect match (fast)
945 if el == l: # perfect match (fast)
944 return True
946 return True
945 if el:
947 if el:
946 if el.endswith(" (esc)\n"):
948 if el.endswith(" (esc)\n"):
947 el = el[:-7].decode('string-escape') + '\n'
949 el = el[:-7].decode('string-escape') + '\n'
948 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
950 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
949 return True
951 return True
950 if el.endswith(" (re)\n"):
952 if el.endswith(" (re)\n"):
951 return TTest.rematch(el[:-6], l)
953 return TTest.rematch(el[:-6], l)
952 if el.endswith(" (glob)\n"):
954 if el.endswith(" (glob)\n"):
953 return TTest.globmatch(el[:-8], l)
955 return TTest.globmatch(el[:-8], l)
954 if os.altsep and l.replace('\\', '/') == el:
956 if os.altsep and l.replace('\\', '/') == el:
955 return '+glob'
957 return '+glob'
956 return False
958 return False
957
959
958 @staticmethod
960 @staticmethod
959 def parsehghaveoutput(lines):
961 def parsehghaveoutput(lines):
960 '''Parse hghave log lines.
962 '''Parse hghave log lines.
961
963
962 Return tuple of lists (missing, failed):
964 Return tuple of lists (missing, failed):
963 * the missing/unknown features
965 * the missing/unknown features
964 * the features for which existence check failed'''
966 * the features for which existence check failed'''
965 missing = []
967 missing = []
966 failed = []
968 failed = []
967 for line in lines:
969 for line in lines:
968 if line.startswith(TTest.SKIPPED_PREFIX):
970 if line.startswith(TTest.SKIPPED_PREFIX):
969 line = line.splitlines()[0]
971 line = line.splitlines()[0]
970 missing.append(line[len(TTest.SKIPPED_PREFIX):])
972 missing.append(line[len(TTest.SKIPPED_PREFIX):])
971 elif line.startswith(TTest.FAILED_PREFIX):
973 elif line.startswith(TTest.FAILED_PREFIX):
972 line = line.splitlines()[0]
974 line = line.splitlines()[0]
973 failed.append(line[len(TTest.FAILED_PREFIX):])
975 failed.append(line[len(TTest.FAILED_PREFIX):])
974
976
975 return missing, failed
977 return missing, failed
976
978
977 @staticmethod
979 @staticmethod
978 def _escapef(m):
980 def _escapef(m):
979 return TTest.ESCAPEMAP[m.group(0)]
981 return TTest.ESCAPEMAP[m.group(0)]
980
982
981 @staticmethod
983 @staticmethod
982 def _stringescape(s):
984 def _stringescape(s):
983 return TTest.ESCAPESUB(TTest._escapef, s)
985 return TTest.ESCAPESUB(TTest._escapef, s)
984
986
985
987
986 wifexited = getattr(os, "WIFEXITED", lambda x: False)
988 wifexited = getattr(os, "WIFEXITED", lambda x: False)
987 def run(cmd, wd, options, replacements, env, abort):
989 def run(cmd, wd, options, replacements, env, abort):
988 """Run command in a sub-process, capturing the output (stdout and stderr).
990 """Run command in a sub-process, capturing the output (stdout and stderr).
989 Return a tuple (exitcode, output). output is None in debug mode."""
991 Return a tuple (exitcode, output). output is None in debug mode."""
990 # TODO: Use subprocess.Popen if we're running on Python 2.4
992 # TODO: Use subprocess.Popen if we're running on Python 2.4
991 if options.debug:
993 if options.debug:
992 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
994 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
993 ret = proc.wait()
995 ret = proc.wait()
994 return (ret, None)
996 return (ret, None)
995
997
996 proc = Popen4(cmd, wd, options.timeout, env)
998 proc = Popen4(cmd, wd, options.timeout, env)
997 def cleanup():
999 def cleanup():
998 terminate(proc)
1000 terminate(proc)
999 ret = proc.wait()
1001 ret = proc.wait()
1000 if ret == 0:
1002 if ret == 0:
1001 ret = signal.SIGTERM << 8
1003 ret = signal.SIGTERM << 8
1002 killdaemons(env['DAEMON_PIDS'])
1004 killdaemons(env['DAEMON_PIDS'])
1003 return ret
1005 return ret
1004
1006
1005 output = ''
1007 output = ''
1006 proc.tochild.close()
1008 proc.tochild.close()
1007
1009
1008 try:
1010 try:
1009 output = proc.fromchild.read()
1011 output = proc.fromchild.read()
1010 except KeyboardInterrupt:
1012 except KeyboardInterrupt:
1011 vlog('# Handling keyboard interrupt')
1013 vlog('# Handling keyboard interrupt')
1012 cleanup()
1014 cleanup()
1013 raise
1015 raise
1014
1016
1015 ret = proc.wait()
1017 ret = proc.wait()
1016 if wifexited(ret):
1018 if wifexited(ret):
1017 ret = os.WEXITSTATUS(ret)
1019 ret = os.WEXITSTATUS(ret)
1018
1020
1019 if proc.timeout:
1021 if proc.timeout:
1020 ret = 'timeout'
1022 ret = 'timeout'
1021
1023
1022 if ret:
1024 if ret:
1023 killdaemons(env['DAEMON_PIDS'])
1025 killdaemons(env['DAEMON_PIDS'])
1024
1026
1025 if abort[0]:
1027 if abort[0]:
1026 raise KeyboardInterrupt()
1028 raise KeyboardInterrupt()
1027
1029
1028 for s, r in replacements:
1030 for s, r in replacements:
1029 output = re.sub(s, r, output)
1031 output = re.sub(s, r, output)
1030 return ret, output.splitlines(True)
1032 return ret, output.splitlines(True)
1031
1033
1032 iolock = threading.Lock()
1034 iolock = threading.Lock()
1033
1035
1034 class SkipTest(Exception):
1036 class SkipTest(Exception):
1035 """Raised to indicate that a test is to be skipped."""
1037 """Raised to indicate that a test is to be skipped."""
1036
1038
1037 class IgnoreTest(Exception):
1039 class IgnoreTest(Exception):
1038 """Raised to indicate that a test is to be ignored."""
1040 """Raised to indicate that a test is to be ignored."""
1039
1041
1040 class WarnTest(Exception):
1042 class WarnTest(Exception):
1041 """Raised to indicate that a test warned."""
1043 """Raised to indicate that a test warned."""
1042
1044
1043 class TestResult(unittest._TextTestResult):
1045 class TestResult(unittest._TextTestResult):
1044 """Holds results when executing via unittest."""
1046 """Holds results when executing via unittest."""
1045 # Don't worry too much about accessing the non-public _TextTestResult.
1047 # Don't worry too much about accessing the non-public _TextTestResult.
1046 # It is relatively common in Python testing tools.
1048 # It is relatively common in Python testing tools.
1047 def __init__(self, *args, **kwargs):
1049 def __init__(self, *args, **kwargs):
1048 super(TestResult, self).__init__(*args, **kwargs)
1050 super(TestResult, self).__init__(*args, **kwargs)
1049
1051
1050 # unittest.TestResult didn't have skipped until 2.7. We need to
1052 # unittest.TestResult didn't have skipped until 2.7. We need to
1051 # polyfill it.
1053 # polyfill it.
1052 self.skipped = []
1054 self.skipped = []
1053
1055
1054 # We have a custom "ignored" result that isn't present in any Python
1056 # 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
1057 # unittest implementation. It is very similar to skipped. It may make
1056 # sense to map it into skip some day.
1058 # sense to map it into skip some day.
1057 self.ignored = []
1059 self.ignored = []
1058
1060
1059 # We have a custom "warned" result that isn't present in any Python
1061 # 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
1062 # unittest implementation. It is very similar to failed. It may make
1061 # sense to map it into fail some day.
1063 # sense to map it into fail some day.
1062 self.warned = []
1064 self.warned = []
1063
1065
1064 # Polyfill.
1066 # Polyfill.
1065 def addSkip(self, test, reason):
1067 def addSkip(self, test, reason):
1066 self.skipped.append((test, reason))
1068 self.skipped.append((test, reason))
1067
1069
1068 if self.showAll:
1070 if self.showAll:
1069 self.stream.writeln('skipped %s' % reason)
1071 self.stream.writeln('skipped %s' % reason)
1070 else:
1072 else:
1071 self.stream.write('s')
1073 self.stream.write('s')
1072 self.stream.flush()
1074 self.stream.flush()
1073
1075
1074 def addIgnore(self, test, reason):
1076 def addIgnore(self, test, reason):
1075 self.ignored.append((test, reason))
1077 self.ignored.append((test, reason))
1076
1078
1077 if self.showAll:
1079 if self.showAll:
1078 self.stream.writeln('ignored %s' % reason)
1080 self.stream.writeln('ignored %s' % reason)
1079 else:
1081 else:
1080 self.stream.write('i')
1082 self.stream.write('i')
1081 self.stream.flush()
1083 self.stream.flush()
1082
1084
1083 def addWarn(self, test, reason):
1085 def addWarn(self, test, reason):
1084 self.warned.append((test, reason))
1086 self.warned.append((test, reason))
1085
1087
1086 if self.showAll:
1088 if self.showAll:
1087 self.stream.writeln('warned %s' % reason)
1089 self.stream.writeln('warned %s' % reason)
1088 else:
1090 else:
1089 self.stream.write('~')
1091 self.stream.write('~')
1090 self.stream.flush()
1092 self.stream.flush()
1091
1093
1092 class TestSuite(unittest.TestSuite):
1094 class TestSuite(unittest.TestSuite):
1093 """Custom unitest TestSuite that knows how to execute concurrently."""
1095 """Custom unitest TestSuite that knows how to execute concurrently."""
1094
1096
1095 def __init__(self, runner, *args, **kwargs):
1097 def __init__(self, runner, *args, **kwargs):
1096 super(TestSuite, self).__init__(*args, **kwargs)
1098 super(TestSuite, self).__init__(*args, **kwargs)
1097
1099
1098 self._runner = runner
1100 self._runner = runner
1099
1101
1100 def run(self, result):
1102 def run(self, result):
1101 self._runner._executetests(self._tests, result=result)
1103 self._runner._executetests(self._tests, result=result)
1102
1104
1103 return result
1105 return result
1104
1106
1105 class TextTestRunner(unittest.TextTestRunner):
1107 class TextTestRunner(unittest.TextTestRunner):
1106 """Custom unittest test runner that uses appropriate settings."""
1108 """Custom unittest test runner that uses appropriate settings."""
1107
1109
1108 def _makeResult(self):
1110 def _makeResult(self):
1109 return TestResult(self.stream, self.descriptions, self.verbosity)
1111 return TestResult(self.stream, self.descriptions, self.verbosity)
1110
1112
1111 class TestRunner(object):
1113 class TestRunner(object):
1112 """Holds context for executing tests.
1114 """Holds context for executing tests.
1113
1115
1114 Tests rely on a lot of state. This object holds it for them.
1116 Tests rely on a lot of state. This object holds it for them.
1115 """
1117 """
1116
1118
1117 REQUIREDTOOLS = [
1119 REQUIREDTOOLS = [
1118 os.path.basename(sys.executable),
1120 os.path.basename(sys.executable),
1119 'diff',
1121 'diff',
1120 'grep',
1122 'grep',
1121 'unzip',
1123 'unzip',
1122 'gunzip',
1124 'gunzip',
1123 'bunzip2',
1125 'bunzip2',
1124 'sed',
1126 'sed',
1125 ]
1127 ]
1126
1128
1127 TESTTYPES = [
1129 TESTTYPES = [
1128 ('.py', PythonTest, '.out'),
1130 ('.py', PythonTest, '.out'),
1129 ('.t', TTest, ''),
1131 ('.t', TTest, ''),
1130 ]
1132 ]
1131
1133
1132 def __init__(self):
1134 def __init__(self):
1133 self.options = None
1135 self.options = None
1134 self.testdir = None
1136 self.testdir = None
1135 self.hgtmp = None
1137 self.hgtmp = None
1136 self.inst = None
1138 self.inst = None
1137 self.bindir = None
1139 self.bindir = None
1138 self.tmpbinddir = None
1140 self.tmpbinddir = None
1139 self.pythondir = None
1141 self.pythondir = None
1140 self.coveragefile = None
1142 self.coveragefile = None
1141 self.times = [] # Holds execution times of tests.
1143 self.times = [] # Holds execution times of tests.
1142 self.results = {
1144 self.results = {
1143 '.': [],
1145 '.': [],
1144 '!': [],
1146 '!': [],
1145 '~': [],
1147 '~': [],
1146 's': [],
1148 's': [],
1147 'i': [],
1149 'i': [],
1148 'u': [],
1150 'u': [],
1149 }
1151 }
1150 self.abort = [False]
1152 self.abort = [False]
1151 self._createdfiles = []
1153 self._createdfiles = []
1152 self._hgpath = None
1154 self._hgpath = None
1153
1155
1154 def run(self, args, parser=None):
1156 def run(self, args, parser=None):
1155 """Run the test suite."""
1157 """Run the test suite."""
1156 oldmask = os.umask(022)
1158 oldmask = os.umask(022)
1157 try:
1159 try:
1158 parser = parser or getparser()
1160 parser = parser or getparser()
1159 options, args = parseargs(args, parser)
1161 options, args = parseargs(args, parser)
1160 self.options = options
1162 self.options = options
1161
1163
1162 self._checktools()
1164 self._checktools()
1163 tests = self.findtests(args)
1165 tests = self.findtests(args)
1164 return self._run(tests)
1166 return self._run(tests)
1165 finally:
1167 finally:
1166 os.umask(oldmask)
1168 os.umask(oldmask)
1167
1169
1168 def _run(self, tests):
1170 def _run(self, tests):
1169 if self.options.random:
1171 if self.options.random:
1170 random.shuffle(tests)
1172 random.shuffle(tests)
1171 else:
1173 else:
1172 # keywords for slow tests
1174 # keywords for slow tests
1173 slow = 'svn gendoc check-code-hg'.split()
1175 slow = 'svn gendoc check-code-hg'.split()
1174 def sortkey(f):
1176 def sortkey(f):
1175 # run largest tests first, as they tend to take the longest
1177 # run largest tests first, as they tend to take the longest
1176 try:
1178 try:
1177 val = -os.stat(f).st_size
1179 val = -os.stat(f).st_size
1178 except OSError, e:
1180 except OSError, e:
1179 if e.errno != errno.ENOENT:
1181 if e.errno != errno.ENOENT:
1180 raise
1182 raise
1181 return -1e9 # file does not exist, tell early
1183 return -1e9 # file does not exist, tell early
1182 for kw in slow:
1184 for kw in slow:
1183 if kw in f:
1185 if kw in f:
1184 val *= 10
1186 val *= 10
1185 return val
1187 return val
1186 tests.sort(key=sortkey)
1188 tests.sort(key=sortkey)
1187
1189
1188 self.testdir = os.environ['TESTDIR'] = os.getcwd()
1190 self.testdir = os.environ['TESTDIR'] = os.getcwd()
1189
1191
1190 if 'PYTHONHASHSEED' not in os.environ:
1192 if 'PYTHONHASHSEED' not in os.environ:
1191 # use a random python hash seed all the time
1193 # use a random python hash seed all the time
1192 # we do the randomness ourself to know what seed is used
1194 # we do the randomness ourself to know what seed is used
1193 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1195 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1194
1196
1195 if self.options.tmpdir:
1197 if self.options.tmpdir:
1196 self.options.keep_tmpdir = True
1198 self.options.keep_tmpdir = True
1197 tmpdir = self.options.tmpdir
1199 tmpdir = self.options.tmpdir
1198 if os.path.exists(tmpdir):
1200 if os.path.exists(tmpdir):
1199 # Meaning of tmpdir has changed since 1.3: we used to create
1201 # Meaning of tmpdir has changed since 1.3: we used to create
1200 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1202 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1201 # tmpdir already exists.
1203 # tmpdir already exists.
1202 print "error: temp dir %r already exists" % tmpdir
1204 print "error: temp dir %r already exists" % tmpdir
1203 return 1
1205 return 1
1204
1206
1205 # Automatically removing tmpdir sounds convenient, but could
1207 # Automatically removing tmpdir sounds convenient, but could
1206 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1208 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1207 # or "--tmpdir=$HOME".
1209 # or "--tmpdir=$HOME".
1208 #vlog("# Removing temp dir", tmpdir)
1210 #vlog("# Removing temp dir", tmpdir)
1209 #shutil.rmtree(tmpdir)
1211 #shutil.rmtree(tmpdir)
1210 os.makedirs(tmpdir)
1212 os.makedirs(tmpdir)
1211 else:
1213 else:
1212 d = None
1214 d = None
1213 if os.name == 'nt':
1215 if os.name == 'nt':
1214 # without this, we get the default temp dir location, but
1216 # without this, we get the default temp dir location, but
1215 # in all lowercase, which causes troubles with paths (issue3490)
1217 # in all lowercase, which causes troubles with paths (issue3490)
1216 d = os.getenv('TMP')
1218 d = os.getenv('TMP')
1217 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1219 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1218 self.hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1220 self.hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1219
1221
1220 if self.options.with_hg:
1222 if self.options.with_hg:
1221 self.inst = None
1223 self.inst = None
1222 self.bindir = os.path.dirname(os.path.realpath(
1224 self.bindir = os.path.dirname(os.path.realpath(
1223 self.options.with_hg))
1225 self.options.with_hg))
1224 self.tmpbindir = os.path.join(self.hgtmp, 'install', 'bin')
1226 self.tmpbindir = os.path.join(self.hgtmp, 'install', 'bin')
1225 os.makedirs(self.tmpbindir)
1227 os.makedirs(self.tmpbindir)
1226
1228
1227 # This looks redundant with how Python initializes sys.path from
1229 # This looks redundant with how Python initializes sys.path from
1228 # the location of the script being executed. Needed because the
1230 # the location of the script being executed. Needed because the
1229 # "hg" specified by --with-hg is not the only Python script
1231 # "hg" specified by --with-hg is not the only Python script
1230 # executed in the test suite that needs to import 'mercurial'
1232 # executed in the test suite that needs to import 'mercurial'
1231 # ... which means it's not really redundant at all.
1233 # ... which means it's not really redundant at all.
1232 self.pythondir = self.bindir
1234 self.pythondir = self.bindir
1233 else:
1235 else:
1234 self.inst = os.path.join(self.hgtmp, "install")
1236 self.inst = os.path.join(self.hgtmp, "install")
1235 self.bindir = os.environ["BINDIR"] = os.path.join(self.inst,
1237 self.bindir = os.environ["BINDIR"] = os.path.join(self.inst,
1236 "bin")
1238 "bin")
1237 self.tmpbindir = self.bindir
1239 self.tmpbindir = self.bindir
1238 self.pythondir = os.path.join(self.inst, "lib", "python")
1240 self.pythondir = os.path.join(self.inst, "lib", "python")
1239
1241
1240 os.environ["BINDIR"] = self.bindir
1242 os.environ["BINDIR"] = self.bindir
1241 os.environ["PYTHON"] = PYTHON
1243 os.environ["PYTHON"] = PYTHON
1242
1244
1243 path = [self.bindir] + os.environ["PATH"].split(os.pathsep)
1245 path = [self.bindir] + os.environ["PATH"].split(os.pathsep)
1244 if self.tmpbindir != self.bindir:
1246 if self.tmpbindir != self.bindir:
1245 path = [self.tmpbindir] + path
1247 path = [self.tmpbindir] + path
1246 os.environ["PATH"] = os.pathsep.join(path)
1248 os.environ["PATH"] = os.pathsep.join(path)
1247
1249
1248 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1250 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1249 # can run .../tests/run-tests.py test-foo where test-foo
1251 # can run .../tests/run-tests.py test-foo where test-foo
1250 # adds an extension to HGRC. Also include run-test.py directory to
1252 # adds an extension to HGRC. Also include run-test.py directory to
1251 # import modules like heredoctest.
1253 # import modules like heredoctest.
1252 pypath = [self.pythondir, self.testdir,
1254 pypath = [self.pythondir, self.testdir,
1253 os.path.abspath(os.path.dirname(__file__))]
1255 os.path.abspath(os.path.dirname(__file__))]
1254 # We have to augment PYTHONPATH, rather than simply replacing
1256 # We have to augment PYTHONPATH, rather than simply replacing
1255 # it, in case external libraries are only available via current
1257 # it, in case external libraries are only available via current
1256 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1258 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1257 # are in /opt/subversion.)
1259 # are in /opt/subversion.)
1258 oldpypath = os.environ.get(IMPL_PATH)
1260 oldpypath = os.environ.get(IMPL_PATH)
1259 if oldpypath:
1261 if oldpypath:
1260 pypath.append(oldpypath)
1262 pypath.append(oldpypath)
1261 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1263 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1262
1264
1263 self.coveragefile = os.path.join(self.testdir, '.coverage')
1265 self.coveragefile = os.path.join(self.testdir, '.coverage')
1264
1266
1265 vlog("# Using TESTDIR", self.testdir)
1267 vlog("# Using TESTDIR", self.testdir)
1266 vlog("# Using HGTMP", self.hgtmp)
1268 vlog("# Using HGTMP", self.hgtmp)
1267 vlog("# Using PATH", os.environ["PATH"])
1269 vlog("# Using PATH", os.environ["PATH"])
1268 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1270 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1269
1271
1270 try:
1272 try:
1271 return self._runtests(tests) or 0
1273 return self._runtests(tests) or 0
1272 finally:
1274 finally:
1273 time.sleep(.1)
1275 time.sleep(.1)
1274 self._cleanup()
1276 self._cleanup()
1275
1277
1276 def findtests(self, args):
1278 def findtests(self, args):
1277 """Finds possible test files from arguments.
1279 """Finds possible test files from arguments.
1278
1280
1279 If you wish to inject custom tests into the test harness, this would
1281 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.
1282 be a good function to monkeypatch or override in a derived class.
1281 """
1283 """
1282 if not args:
1284 if not args:
1283 if self.options.changed:
1285 if self.options.changed:
1284 proc = Popen4('hg st --rev "%s" -man0 .' %
1286 proc = Popen4('hg st --rev "%s" -man0 .' %
1285 self.options.changed, None, 0)
1287 self.options.changed, None, 0)
1286 stdout, stderr = proc.communicate()
1288 stdout, stderr = proc.communicate()
1287 args = stdout.strip('\0').split('\0')
1289 args = stdout.strip('\0').split('\0')
1288 else:
1290 else:
1289 args = os.listdir('.')
1291 args = os.listdir('.')
1290
1292
1291 return [t for t in args
1293 return [t for t in args
1292 if os.path.basename(t).startswith('test-')
1294 if os.path.basename(t).startswith('test-')
1293 and (t.endswith('.py') or t.endswith('.t'))]
1295 and (t.endswith('.py') or t.endswith('.t'))]
1294
1296
1295 def _runtests(self, tests):
1297 def _runtests(self, tests):
1296 try:
1298 try:
1297 if self.inst:
1299 if self.inst:
1298 self._installhg()
1300 self._installhg()
1299 self._checkhglib("Testing")
1301 self._checkhglib("Testing")
1300 else:
1302 else:
1301 self._usecorrectpython()
1303 self._usecorrectpython()
1302
1304
1303 if self.options.restart:
1305 if self.options.restart:
1304 orig = list(tests)
1306 orig = list(tests)
1305 while tests:
1307 while tests:
1306 if os.path.exists(tests[0] + ".err"):
1308 if os.path.exists(tests[0] + ".err"):
1307 break
1309 break
1308 tests.pop(0)
1310 tests.pop(0)
1309 if not tests:
1311 if not tests:
1310 print "running all tests"
1312 print "running all tests"
1311 tests = orig
1313 tests = orig
1312
1314
1313 tests = [self._gettest(t, i, asunit=self.options.unittest)
1315 tests = [self._gettest(t, i, asunit=self.options.unittest)
1314 for i, t in enumerate(tests)]
1316 for i, t in enumerate(tests)]
1315
1317
1316 if self.options.unittest:
1318 if self.options.unittest:
1317 suite = TestSuite(self, tests=tests)
1319 suite = TestSuite(self, tests=tests)
1318 verbosity = 1
1320 verbosity = 1
1319 if self.options.verbose:
1321 if self.options.verbose:
1320 verbosity = 2
1322 verbosity = 2
1321 runner = TextTestRunner(verbosity=verbosity)
1323 runner = TextTestRunner(verbosity=verbosity)
1322 runner.run(suite)
1324 runner.run(suite)
1323 else:
1325 else:
1324 self._executetests(tests)
1326 self._executetests(tests)
1325
1327
1326 failed = len(self.results['!'])
1328 failed = len(self.results['!'])
1327 warned = len(self.results['~'])
1329 warned = len(self.results['~'])
1328 tested = len(self.results['.']) + failed + warned
1330 tested = len(self.results['.']) + failed + warned
1329 skipped = len(self.results['s'])
1331 skipped = len(self.results['s'])
1330 ignored = len(self.results['i'])
1332 ignored = len(self.results['i'])
1331
1333
1332 print
1334 print
1333 if not self.options.noskips:
1335 if not self.options.noskips:
1334 for s in self.results['s']:
1336 for s in self.results['s']:
1335 print "Skipped %s: %s" % s
1337 print "Skipped %s: %s" % s
1336 for s in self.results['~']:
1338 for s in self.results['~']:
1337 print "Warned %s: %s" % s
1339 print "Warned %s: %s" % s
1338 for s in self.results['!']:
1340 for s in self.results['!']:
1339 print "Failed %s: %s" % s
1341 print "Failed %s: %s" % s
1340 self._checkhglib("Tested")
1342 self._checkhglib("Tested")
1341 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1343 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1342 tested, skipped + ignored, warned, failed)
1344 tested, skipped + ignored, warned, failed)
1343 if self.results['!']:
1345 if self.results['!']:
1344 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1346 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1345 if self.options.time:
1347 if self.options.time:
1346 self._outputtimes()
1348 self._outputtimes()
1347
1349
1348 if self.options.anycoverage:
1350 if self.options.anycoverage:
1349 self._outputcoverage()
1351 self._outputcoverage()
1350 except KeyboardInterrupt:
1352 except KeyboardInterrupt:
1351 failed = True
1353 failed = True
1352 print "\ninterrupted!"
1354 print "\ninterrupted!"
1353
1355
1354 if failed:
1356 if failed:
1355 return 1
1357 return 1
1356 if warned:
1358 if warned:
1357 return 80
1359 return 80
1358
1360
1359 def _gettest(self, test, count, asunit=False):
1361 def _gettest(self, test, count, asunit=False):
1360 """Obtain a Test by looking at its filename.
1362 """Obtain a Test by looking at its filename.
1361
1363
1362 Returns a Test instance. The Test may not be runnable if it doesn't
1364 Returns a Test instance. The Test may not be runnable if it doesn't
1363 map to a known type.
1365 map to a known type.
1364 """
1366 """
1365 lctest = test.lower()
1367 lctest = test.lower()
1366 refpath = os.path.join(self.testdir, test)
1368 refpath = os.path.join(self.testdir, test)
1367
1369
1368 testcls = Test
1370 testcls = Test
1369
1371
1370 for ext, cls, out in self.TESTTYPES:
1372 for ext, cls, out in self.TESTTYPES:
1371 if lctest.endswith(ext):
1373 if lctest.endswith(ext):
1372 testcls = cls
1374 testcls = cls
1373 refpath = os.path.join(self.testdir, test + out)
1375 refpath = os.path.join(self.testdir, test + out)
1374 break
1376 break
1375
1377
1376 t = testcls(self, test, count, refpath, unittest=asunit)
1378 t = testcls(self, test, count, refpath, unittest=asunit)
1377
1379
1378 if not asunit:
1380 if not asunit:
1379 return t
1381 return t
1380
1382
1381 class MercurialTest(unittest.TestCase):
1383 class MercurialTest(unittest.TestCase):
1382 def __init__(self, name, *args, **kwargs):
1384 def __init__(self, name, *args, **kwargs):
1383 super(MercurialTest, self).__init__(*args, **kwargs)
1385 super(MercurialTest, self).__init__(*args, **kwargs)
1384 self.name = name
1386 self.name = name
1385
1387
1386 def shortDescription(self):
1388 def shortDescription(self):
1387 return self.name
1389 return self.name
1388
1390
1389 # Need to stash away the TestResult since we do custom things
1391 # Need to stash away the TestResult since we do custom things
1390 # with it.
1392 # with it.
1391 def run(self, result):
1393 def run(self, result):
1392 result.startTest(self)
1394 result.startTest(self)
1393 try:
1395 try:
1394 try:
1396 try:
1395 t.setUp()
1397 t.setUp()
1396 except (KeyboardInterrupt, SystemExit):
1398 except (KeyboardInterrupt, SystemExit):
1397 raise
1399 raise
1398 except Exception:
1400 except Exception:
1399 result.addError(self, sys.exc_info())
1401 result.addError(self, sys.exc_info())
1400 return
1402 return
1401
1403
1402 success = False
1404 success = False
1403 try:
1405 try:
1404 self.runTest()
1406 self.runTest()
1405 except KeyboardInterrupt:
1407 except KeyboardInterrupt:
1406 raise
1408 raise
1407 except SkipTest, e:
1409 except SkipTest, e:
1408 result.addSkip(self, str(e))
1410 result.addSkip(self, str(e))
1409 except IgnoreTest, e:
1411 except IgnoreTest, e:
1410 result.addIgnore(self, str(e))
1412 result.addIgnore(self, str(e))
1411 except WarnTest, e:
1413 except WarnTest, e:
1412 result.addWarn(self, str(e))
1414 result.addWarn(self, str(e))
1413 except self.failureException:
1415 except self.failureException:
1414 result.addFailure(self, sys.exc_info())
1416 result.addFailure(self, sys.exc_info())
1415 except Exception:
1417 except Exception:
1416 result.addError(self, sys.exc_info())
1418 result.addError(self, sys.exc_info())
1417 else:
1419 else:
1418 success = True
1420 success = True
1419
1421
1420 try:
1422 try:
1421 t.tearDown()
1423 t.tearDown()
1422 except (KeyboardInterrupt, SystemExit):
1424 except (KeyboardInterrupt, SystemExit):
1423 raise
1425 raise
1424 except Exception:
1426 except Exception:
1425 result.addError(self, sys.exc_info())
1427 result.addError(self, sys.exc_info())
1426 success = False
1428 success = False
1427
1429
1428 if success:
1430 if success:
1429 result.addSuccess(self)
1431 result.addSuccess(self)
1430 finally:
1432 finally:
1431 result.stopTest(self)
1433 result.stopTest(self)
1432
1434
1433 def runTest(self):
1435 def runTest(self):
1434 code, tname, msg = t.run()
1436 code, tname, msg = t.run()
1435
1437
1436 # All non-success conditions should be exceptions and should
1438 # All non-success conditions should be exceptions and should
1437 # be caught in run().
1439 # be caught in run().
1438 assert code == '.'
1440 assert code == '.'
1439
1441
1440 # We need this proxy until tearDown() is implemented.
1442 # We need this proxy until tearDown() is implemented.
1441 def cleanup(self):
1443 def cleanup(self):
1442 return t.cleanup()
1444 return t.cleanup()
1443
1445
1444 return MercurialTest(test)
1446 return MercurialTest(test)
1445
1447
1446 def _cleanup(self):
1448 def _cleanup(self):
1447 """Clean up state from this test invocation."""
1449 """Clean up state from this test invocation."""
1448
1450
1449 if self.options.keep_tmpdir:
1451 if self.options.keep_tmpdir:
1450 return
1452 return
1451
1453
1452 vlog("# Cleaning up HGTMP", self.hgtmp)
1454 vlog("# Cleaning up HGTMP", self.hgtmp)
1453 shutil.rmtree(self.hgtmp, True)
1455 shutil.rmtree(self.hgtmp, True)
1454 for f in self._createdfiles:
1456 for f in self._createdfiles:
1455 try:
1457 try:
1456 os.remove(f)
1458 os.remove(f)
1457 except OSError:
1459 except OSError:
1458 pass
1460 pass
1459
1461
1460 def _usecorrectpython(self):
1462 def _usecorrectpython(self):
1461 # Some tests run the Python interpreter. They must use the
1463 # Some tests run the Python interpreter. They must use the
1462 # same interpreter or bad things will happen.
1464 # same interpreter or bad things will happen.
1463 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1465 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1464 if getattr(os, 'symlink', None):
1466 if getattr(os, 'symlink', None):
1465 vlog("# Making python executable in test path a symlink to '%s'" %
1467 vlog("# Making python executable in test path a symlink to '%s'" %
1466 sys.executable)
1468 sys.executable)
1467 mypython = os.path.join(self.tmpbindir, pyexename)
1469 mypython = os.path.join(self.tmpbindir, pyexename)
1468 try:
1470 try:
1469 if os.readlink(mypython) == sys.executable:
1471 if os.readlink(mypython) == sys.executable:
1470 return
1472 return
1471 os.unlink(mypython)
1473 os.unlink(mypython)
1472 except OSError, err:
1474 except OSError, err:
1473 if err.errno != errno.ENOENT:
1475 if err.errno != errno.ENOENT:
1474 raise
1476 raise
1475 if self._findprogram(pyexename) != sys.executable:
1477 if self._findprogram(pyexename) != sys.executable:
1476 try:
1478 try:
1477 os.symlink(sys.executable, mypython)
1479 os.symlink(sys.executable, mypython)
1478 self._createdfiles.append(mypython)
1480 self._createdfiles.append(mypython)
1479 except OSError, err:
1481 except OSError, err:
1480 # child processes may race, which is harmless
1482 # child processes may race, which is harmless
1481 if err.errno != errno.EEXIST:
1483 if err.errno != errno.EEXIST:
1482 raise
1484 raise
1483 else:
1485 else:
1484 exedir, exename = os.path.split(sys.executable)
1486 exedir, exename = os.path.split(sys.executable)
1485 vlog("# Modifying search path to find %s as %s in '%s'" %
1487 vlog("# Modifying search path to find %s as %s in '%s'" %
1486 (exename, pyexename, exedir))
1488 (exename, pyexename, exedir))
1487 path = os.environ['PATH'].split(os.pathsep)
1489 path = os.environ['PATH'].split(os.pathsep)
1488 while exedir in path:
1490 while exedir in path:
1489 path.remove(exedir)
1491 path.remove(exedir)
1490 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1492 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1491 if not self._findprogram(pyexename):
1493 if not self._findprogram(pyexename):
1492 print "WARNING: Cannot find %s in search path" % pyexename
1494 print "WARNING: Cannot find %s in search path" % pyexename
1493
1495
1494 def _installhg(self):
1496 def _installhg(self):
1495 vlog("# Performing temporary installation of HG")
1497 vlog("# Performing temporary installation of HG")
1496 installerrs = os.path.join("tests", "install.err")
1498 installerrs = os.path.join("tests", "install.err")
1497 compiler = ''
1499 compiler = ''
1498 if self.options.compiler:
1500 if self.options.compiler:
1499 compiler = '--compiler ' + self.options.compiler
1501 compiler = '--compiler ' + self.options.compiler
1500 pure = self.options.pure and "--pure" or ""
1502 pure = self.options.pure and "--pure" or ""
1501 py3 = ''
1503 py3 = ''
1502 if sys.version_info[0] == 3:
1504 if sys.version_info[0] == 3:
1503 py3 = '--c2to3'
1505 py3 = '--c2to3'
1504
1506
1505 # Run installer in hg root
1507 # Run installer in hg root
1506 script = os.path.realpath(sys.argv[0])
1508 script = os.path.realpath(sys.argv[0])
1507 hgroot = os.path.dirname(os.path.dirname(script))
1509 hgroot = os.path.dirname(os.path.dirname(script))
1508 os.chdir(hgroot)
1510 os.chdir(hgroot)
1509 nohome = '--home=""'
1511 nohome = '--home=""'
1510 if os.name == 'nt':
1512 if os.name == 'nt':
1511 # The --home="" trick works only on OS where os.sep == '/'
1513 # The --home="" trick works only on OS where os.sep == '/'
1512 # because of a distutils convert_path() fast-path. Avoid it at
1514 # because of a distutils convert_path() fast-path. Avoid it at
1513 # least on Windows for now, deal with .pydistutils.cfg bugs
1515 # least on Windows for now, deal with .pydistutils.cfg bugs
1514 # when they happen.
1516 # when they happen.
1515 nohome = ''
1517 nohome = ''
1516 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1518 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1517 ' build %(compiler)s --build-base="%(base)s"'
1519 ' build %(compiler)s --build-base="%(base)s"'
1518 ' install --force --prefix="%(prefix)s"'
1520 ' install --force --prefix="%(prefix)s"'
1519 ' --install-lib="%(libdir)s"'
1521 ' --install-lib="%(libdir)s"'
1520 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1522 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1521 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1523 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1522 'compiler': compiler,
1524 'compiler': compiler,
1523 'base': os.path.join(self.hgtmp, "build"),
1525 'base': os.path.join(self.hgtmp, "build"),
1524 'prefix': self.inst, 'libdir': self.pythondir,
1526 'prefix': self.inst, 'libdir': self.pythondir,
1525 'bindir': self.bindir,
1527 'bindir': self.bindir,
1526 'nohome': nohome, 'logfile': installerrs})
1528 'nohome': nohome, 'logfile': installerrs})
1527 vlog("# Running", cmd)
1529 vlog("# Running", cmd)
1528 if os.system(cmd) == 0:
1530 if os.system(cmd) == 0:
1529 if not self.options.verbose:
1531 if not self.options.verbose:
1530 os.remove(installerrs)
1532 os.remove(installerrs)
1531 else:
1533 else:
1532 f = open(installerrs)
1534 f = open(installerrs)
1533 for line in f:
1535 for line in f:
1534 print line,
1536 print line,
1535 f.close()
1537 f.close()
1536 sys.exit(1)
1538 sys.exit(1)
1537 os.chdir(self.testdir)
1539 os.chdir(self.testdir)
1538
1540
1539 self._usecorrectpython()
1541 self._usecorrectpython()
1540
1542
1541 if self.options.py3k_warnings and not self.options.anycoverage:
1543 if self.options.py3k_warnings and not self.options.anycoverage:
1542 vlog("# Updating hg command to enable Py3k Warnings switch")
1544 vlog("# Updating hg command to enable Py3k Warnings switch")
1543 f = open(os.path.join(self.bindir, 'hg'), 'r')
1545 f = open(os.path.join(self.bindir, 'hg'), 'r')
1544 lines = [line.rstrip() for line in f]
1546 lines = [line.rstrip() for line in f]
1545 lines[0] += ' -3'
1547 lines[0] += ' -3'
1546 f.close()
1548 f.close()
1547 f = open(os.path.join(self.bindir, 'hg'), 'w')
1549 f = open(os.path.join(self.bindir, 'hg'), 'w')
1548 for line in lines:
1550 for line in lines:
1549 f.write(line + '\n')
1551 f.write(line + '\n')
1550 f.close()
1552 f.close()
1551
1553
1552 hgbat = os.path.join(self.bindir, 'hg.bat')
1554 hgbat = os.path.join(self.bindir, 'hg.bat')
1553 if os.path.isfile(hgbat):
1555 if os.path.isfile(hgbat):
1554 # hg.bat expects to be put in bin/scripts while run-tests.py
1556 # hg.bat expects to be put in bin/scripts while run-tests.py
1555 # installation layout put it in bin/ directly. Fix it
1557 # installation layout put it in bin/ directly. Fix it
1556 f = open(hgbat, 'rb')
1558 f = open(hgbat, 'rb')
1557 data = f.read()
1559 data = f.read()
1558 f.close()
1560 f.close()
1559 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1561 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1560 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1562 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1561 '"%~dp0python" "%~dp0hg" %*')
1563 '"%~dp0python" "%~dp0hg" %*')
1562 f = open(hgbat, 'wb')
1564 f = open(hgbat, 'wb')
1563 f.write(data)
1565 f.write(data)
1564 f.close()
1566 f.close()
1565 else:
1567 else:
1566 print 'WARNING: cannot fix hg.bat reference to python.exe'
1568 print 'WARNING: cannot fix hg.bat reference to python.exe'
1567
1569
1568 if self.options.anycoverage:
1570 if self.options.anycoverage:
1569 custom = os.path.join(self.testdir, 'sitecustomize.py')
1571 custom = os.path.join(self.testdir, 'sitecustomize.py')
1570 target = os.path.join(self.pythondir, 'sitecustomize.py')
1572 target = os.path.join(self.pythondir, 'sitecustomize.py')
1571 vlog('# Installing coverage trigger to %s' % target)
1573 vlog('# Installing coverage trigger to %s' % target)
1572 shutil.copyfile(custom, target)
1574 shutil.copyfile(custom, target)
1573 rc = os.path.join(self.testdir, '.coveragerc')
1575 rc = os.path.join(self.testdir, '.coveragerc')
1574 vlog('# Installing coverage rc to %s' % rc)
1576 vlog('# Installing coverage rc to %s' % rc)
1575 os.environ['COVERAGE_PROCESS_START'] = rc
1577 os.environ['COVERAGE_PROCESS_START'] = rc
1576 fn = os.path.join(self.inst, '..', '.coverage')
1578 fn = os.path.join(self.inst, '..', '.coverage')
1577 os.environ['COVERAGE_FILE'] = fn
1579 os.environ['COVERAGE_FILE'] = fn
1578
1580
1579 def _checkhglib(self, verb):
1581 def _checkhglib(self, verb):
1580 """Ensure that the 'mercurial' package imported by python is
1582 """Ensure that the 'mercurial' package imported by python is
1581 the one we expect it to be. If not, print a warning to stderr."""
1583 the one we expect it to be. If not, print a warning to stderr."""
1582 expecthg = os.path.join(self.pythondir, 'mercurial')
1584 expecthg = os.path.join(self.pythondir, 'mercurial')
1583 actualhg = self._gethgpath()
1585 actualhg = self._gethgpath()
1584 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1586 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1585 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1587 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1586 ' (expected %s)\n'
1588 ' (expected %s)\n'
1587 % (verb, actualhg, expecthg))
1589 % (verb, actualhg, expecthg))
1588 def _gethgpath(self):
1590 def _gethgpath(self):
1589 """Return the path to the mercurial package that is actually found by
1591 """Return the path to the mercurial package that is actually found by
1590 the current Python interpreter."""
1592 the current Python interpreter."""
1591 if self._hgpath is not None:
1593 if self._hgpath is not None:
1592 return self._hgpath
1594 return self._hgpath
1593
1595
1594 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1596 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1595 pipe = os.popen(cmd % PYTHON)
1597 pipe = os.popen(cmd % PYTHON)
1596 try:
1598 try:
1597 self._hgpath = pipe.read().strip()
1599 self._hgpath = pipe.read().strip()
1598 finally:
1600 finally:
1599 pipe.close()
1601 pipe.close()
1600
1602
1601 return self._hgpath
1603 return self._hgpath
1602
1604
1603 def _outputtimes(self):
1605 def _outputtimes(self):
1604 vlog('# Producing time report')
1606 vlog('# Producing time report')
1605 self.times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1607 self.times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1606 cols = '%7.3f %s'
1608 cols = '%7.3f %s'
1607 print '\n%-7s %s' % ('Time', 'Test')
1609 print '\n%-7s %s' % ('Time', 'Test')
1608 for test, timetaken in self.times:
1610 for test, timetaken in self.times:
1609 print cols % (timetaken, test)
1611 print cols % (timetaken, test)
1610
1612
1611 def _outputcoverage(self):
1613 def _outputcoverage(self):
1612 vlog('# Producing coverage report')
1614 vlog('# Producing coverage report')
1613 os.chdir(self.pythondir)
1615 os.chdir(self.pythondir)
1614
1616
1615 def covrun(*args):
1617 def covrun(*args):
1616 cmd = 'coverage %s' % ' '.join(args)
1618 cmd = 'coverage %s' % ' '.join(args)
1617 vlog('# Running: %s' % cmd)
1619 vlog('# Running: %s' % cmd)
1618 os.system(cmd)
1620 os.system(cmd)
1619
1621
1620 covrun('-c')
1622 covrun('-c')
1621 omit = ','.join(os.path.join(x, '*') for x in
1623 omit = ','.join(os.path.join(x, '*') for x in
1622 [self.bindir, self.testdir])
1624 [self.bindir, self.testdir])
1623 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1625 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1624 if self.options.htmlcov:
1626 if self.options.htmlcov:
1625 htmldir = os.path.join(self.testdir, 'htmlcov')
1627 htmldir = os.path.join(self.testdir, 'htmlcov')
1626 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1628 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1627 '"--omit=%s"' % omit)
1629 '"--omit=%s"' % omit)
1628 if self.options.annotate:
1630 if self.options.annotate:
1629 adir = os.path.join(self.testdir, 'annotated')
1631 adir = os.path.join(self.testdir, 'annotated')
1630 if not os.path.isdir(adir):
1632 if not os.path.isdir(adir):
1631 os.mkdir(adir)
1633 os.mkdir(adir)
1632 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1634 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1633
1635
1634 def _executetests(self, tests, result=None):
1636 def _executetests(self, tests, result=None):
1635 # We copy because we modify the list.
1637 # We copy because we modify the list.
1636 tests = list(tests)
1638 tests = list(tests)
1637
1639
1638 jobs = self.options.jobs
1640 jobs = self.options.jobs
1639 done = queue.Queue()
1641 done = queue.Queue()
1640 running = 0
1642 running = 0
1641
1643
1642 def job(test, result):
1644 def job(test, result):
1643 try:
1645 try:
1644 # If in unittest mode.
1646 # If in unittest mode.
1645 if result:
1647 if result:
1646 test(result)
1648 test(result)
1647 # We need to put something here to make the logic happy.
1649 # We need to put something here to make the logic happy.
1648 # This will get cleaned up later.
1650 # This will get cleaned up later.
1649 done.put(('u', None, None))
1651 done.put(('u', None, None))
1650 else:
1652 else:
1651 done.put(test.run())
1653 done.put(test.run())
1652 test.cleanup()
1654 test.cleanup()
1653 except KeyboardInterrupt:
1655 except KeyboardInterrupt:
1654 pass
1656 pass
1655 except: # re-raises
1657 except: # re-raises
1656 done.put(('!', test, 'run-test raised an error, see traceback'))
1658 done.put(('!', test, 'run-test raised an error, see traceback'))
1657 raise
1659 raise
1658
1660
1659 try:
1661 try:
1660 while tests or running:
1662 while tests or running:
1661 if not done.empty() or running == jobs or not tests:
1663 if not done.empty() or running == jobs or not tests:
1662 try:
1664 try:
1663 code, test, msg = done.get(True, 1)
1665 code, test, msg = done.get(True, 1)
1664 self.results[code].append((test, msg))
1666 self.results[code].append((test, msg))
1665 if self.options.first and code not in '.si':
1667 if self.options.first and code not in '.si':
1666 break
1668 break
1667 except queue.Empty:
1669 except queue.Empty:
1668 continue
1670 continue
1669 running -= 1
1671 running -= 1
1670 if tests and not running == jobs:
1672 if tests and not running == jobs:
1671 test = tests.pop(0)
1673 test = tests.pop(0)
1672 if self.options.loop:
1674 if self.options.loop:
1673 tests.append(test)
1675 tests.append(test)
1674 t = threading.Thread(target=job, name=test.name,
1676 t = threading.Thread(target=job, name=test.name,
1675 args=(test, result))
1677 args=(test, result))
1676 t.start()
1678 t.start()
1677 running += 1
1679 running += 1
1678 except KeyboardInterrupt:
1680 except KeyboardInterrupt:
1679 self.abort[0] = True
1681 self.abort[0] = True
1680
1682
1681 def _findprogram(self, program):
1683 def _findprogram(self, program):
1682 """Search PATH for a executable program"""
1684 """Search PATH for a executable program"""
1683 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1685 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1684 name = os.path.join(p, program)
1686 name = os.path.join(p, program)
1685 if os.name == 'nt' or os.access(name, os.X_OK):
1687 if os.name == 'nt' or os.access(name, os.X_OK):
1686 return name
1688 return name
1687 return None
1689 return None
1688
1690
1689 def _checktools(self):
1691 def _checktools(self):
1690 # Before we go any further, check for pre-requisite tools
1692 # Before we go any further, check for pre-requisite tools
1691 # stuff from coreutils (cat, rm, etc) are not tested
1693 # stuff from coreutils (cat, rm, etc) are not tested
1692 for p in self.REQUIREDTOOLS:
1694 for p in self.REQUIREDTOOLS:
1693 if os.name == 'nt' and not p.endswith('.exe'):
1695 if os.name == 'nt' and not p.endswith('.exe'):
1694 p += '.exe'
1696 p += '.exe'
1695 found = self._findprogram(p)
1697 found = self._findprogram(p)
1696 if found:
1698 if found:
1697 vlog("# Found prerequisite", p, "at", found)
1699 vlog("# Found prerequisite", p, "at", found)
1698 else:
1700 else:
1699 print "WARNING: Did not find prerequisite tool: %s " % p
1701 print "WARNING: Did not find prerequisite tool: %s " % p
1700
1702
1701 if __name__ == '__main__':
1703 if __name__ == '__main__':
1702 runner = TestRunner()
1704 runner = TestRunner()
1703 sys.exit(runner.run(sys.argv[1:]))
1705 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now