##// END OF EJS Templates
run-tests: add iolock to failure output
Matt Mackall -
r21993:284a8c9f default
parent child Browse files
Show More
@@ -1,1836 +1,1838 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, "rb")
115 f = open(path, "rb")
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("-v", "--verbose", action="store_true",
191 parser.add_option("-v", "--verbose", action="store_true",
192 help="output verbose messages")
192 help="output verbose messages")
193 parser.add_option("--view", type="string",
193 parser.add_option("--view", type="string",
194 help="external diff viewer")
194 help="external diff viewer")
195 parser.add_option("--with-hg", type="string",
195 parser.add_option("--with-hg", type="string",
196 metavar="HG",
196 metavar="HG",
197 help="test using specified hg script rather than a "
197 help="test using specified hg script rather than a "
198 "temporary installation")
198 "temporary installation")
199 parser.add_option("-3", "--py3k-warnings", action="store_true",
199 parser.add_option("-3", "--py3k-warnings", action="store_true",
200 help="enable Py3k warnings on Python 2.6+")
200 help="enable Py3k warnings on Python 2.6+")
201 parser.add_option('--extra-config-opt', action="append",
201 parser.add_option('--extra-config-opt', action="append",
202 help='set the given config opt in the test hgrc')
202 help='set the given config opt in the test hgrc')
203 parser.add_option('--random', action="store_true",
203 parser.add_option('--random', action="store_true",
204 help='run tests in random order')
204 help='run tests in random order')
205
205
206 for option, (envvar, default) in defaults.items():
206 for option, (envvar, default) in defaults.items():
207 defaults[option] = type(default)(os.environ.get(envvar, default))
207 defaults[option] = type(default)(os.environ.get(envvar, default))
208 parser.set_defaults(**defaults)
208 parser.set_defaults(**defaults)
209
209
210 return parser
210 return parser
211
211
212 def parseargs(args, parser):
212 def parseargs(args, parser):
213 """Parse arguments with our OptionParser and validate results."""
213 """Parse arguments with our OptionParser and validate results."""
214 (options, args) = parser.parse_args(args)
214 (options, args) = parser.parse_args(args)
215
215
216 # jython is always pure
216 # jython is always pure
217 if 'java' in sys.platform or '__pypy__' in sys.modules:
217 if 'java' in sys.platform or '__pypy__' in sys.modules:
218 options.pure = True
218 options.pure = True
219
219
220 if options.with_hg:
220 if options.with_hg:
221 options.with_hg = os.path.expanduser(options.with_hg)
221 options.with_hg = os.path.expanduser(options.with_hg)
222 if not (os.path.isfile(options.with_hg) and
222 if not (os.path.isfile(options.with_hg) and
223 os.access(options.with_hg, os.X_OK)):
223 os.access(options.with_hg, os.X_OK)):
224 parser.error('--with-hg must specify an executable hg script')
224 parser.error('--with-hg must specify an executable hg script')
225 if not os.path.basename(options.with_hg) == 'hg':
225 if not os.path.basename(options.with_hg) == 'hg':
226 sys.stderr.write('warning: --with-hg should specify an hg script\n')
226 sys.stderr.write('warning: --with-hg should specify an hg script\n')
227 if options.local:
227 if options.local:
228 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
228 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
229 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
229 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
230 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
230 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
231 parser.error('--local specified, but %r not found or not executable'
231 parser.error('--local specified, but %r not found or not executable'
232 % hgbin)
232 % hgbin)
233 options.with_hg = hgbin
233 options.with_hg = hgbin
234
234
235 options.anycoverage = options.cover or options.annotate or options.htmlcov
235 options.anycoverage = options.cover or options.annotate or options.htmlcov
236 if options.anycoverage:
236 if options.anycoverage:
237 try:
237 try:
238 import coverage
238 import coverage
239 covver = version.StrictVersion(coverage.__version__).version
239 covver = version.StrictVersion(coverage.__version__).version
240 if covver < (3, 3):
240 if covver < (3, 3):
241 parser.error('coverage options require coverage 3.3 or later')
241 parser.error('coverage options require coverage 3.3 or later')
242 except ImportError:
242 except ImportError:
243 parser.error('coverage options now require the coverage package')
243 parser.error('coverage options now require the coverage package')
244
244
245 if options.anycoverage and options.local:
245 if options.anycoverage and options.local:
246 # this needs some path mangling somewhere, I guess
246 # this needs some path mangling somewhere, I guess
247 parser.error("sorry, coverage options do not work when --local "
247 parser.error("sorry, coverage options do not work when --local "
248 "is specified")
248 "is specified")
249
249
250 global verbose
250 global verbose
251 if options.verbose:
251 if options.verbose:
252 verbose = ''
252 verbose = ''
253
253
254 if options.tmpdir:
254 if options.tmpdir:
255 options.tmpdir = os.path.expanduser(options.tmpdir)
255 options.tmpdir = os.path.expanduser(options.tmpdir)
256
256
257 if options.jobs < 1:
257 if options.jobs < 1:
258 parser.error('--jobs must be positive')
258 parser.error('--jobs must be positive')
259 if options.interactive and options.debug:
259 if options.interactive and options.debug:
260 parser.error("-i/--interactive and -d/--debug are incompatible")
260 parser.error("-i/--interactive and -d/--debug are incompatible")
261 if options.debug:
261 if options.debug:
262 if options.timeout != defaults['timeout']:
262 if options.timeout != defaults['timeout']:
263 sys.stderr.write(
263 sys.stderr.write(
264 'warning: --timeout option ignored with --debug\n')
264 'warning: --timeout option ignored with --debug\n')
265 options.timeout = 0
265 options.timeout = 0
266 if options.py3k_warnings:
266 if options.py3k_warnings:
267 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
267 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
268 parser.error('--py3k-warnings can only be used on Python 2.6+')
268 parser.error('--py3k-warnings can only be used on Python 2.6+')
269 if options.blacklist:
269 if options.blacklist:
270 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
270 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
271 if options.whitelist:
271 if options.whitelist:
272 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
272 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
273 else:
273 else:
274 options.whitelisted = {}
274 options.whitelisted = {}
275
275
276 return (options, args)
276 return (options, args)
277
277
278 def rename(src, dst):
278 def rename(src, dst):
279 """Like os.rename(), trade atomicity and opened files friendliness
279 """Like os.rename(), trade atomicity and opened files friendliness
280 for existing destination support.
280 for existing destination support.
281 """
281 """
282 shutil.copy(src, dst)
282 shutil.copy(src, dst)
283 os.remove(src)
283 os.remove(src)
284
284
285 def getdiff(expected, output, ref, err):
285 def getdiff(expected, output, ref, err):
286 servefail = False
286 servefail = False
287 lines = []
287 lines = []
288 for line in difflib.unified_diff(expected, output, ref, err):
288 for line in difflib.unified_diff(expected, output, ref, err):
289 if line.startswith('+++') or line.startswith('---'):
289 if line.startswith('+++') or line.startswith('---'):
290 if line.endswith(' \n'):
290 if line.endswith(' \n'):
291 line = line[:-2] + '\n'
291 line = line[:-2] + '\n'
292 lines.append(line)
292 lines.append(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
296
297 return servefail, lines
297 return servefail, lines
298
298
299 verbose = False
299 verbose = False
300 def vlog(*msg):
300 def vlog(*msg):
301 """Log only when in verbose mode."""
301 """Log only when in verbose mode."""
302 if verbose is False:
302 if verbose is False:
303 return
303 return
304
304
305 return log(*msg)
305 return log(*msg)
306
306
307 def log(*msg):
307 def log(*msg):
308 """Log something to stdout.
308 """Log something to stdout.
309
309
310 Arguments are strings to print.
310 Arguments are strings to print.
311 """
311 """
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(unittest.TestCase):
333 class Test(unittest.TestCase):
334 """Encapsulates a single, runnable test.
334 """Encapsulates a single, runnable test.
335
335
336 While this class conforms to the unittest.TestCase API, it differs in that
336 While this class conforms to the unittest.TestCase API, it differs in that
337 instances need to be instantiated manually. (Typically, unittest.TestCase
337 instances need to be instantiated manually. (Typically, unittest.TestCase
338 classes are instantiated automatically by scanning modules.)
338 classes are instantiated automatically by scanning modules.)
339 """
339 """
340
340
341 # Status code reserved for skipped tests (used by hghave).
341 # Status code reserved for skipped tests (used by hghave).
342 SKIPPED_STATUS = 80
342 SKIPPED_STATUS = 80
343
343
344 def __init__(self, path, tmpdir, keeptmpdir=False,
344 def __init__(self, path, tmpdir, keeptmpdir=False,
345 debug=False,
345 debug=False,
346 timeout=defaults['timeout'],
346 timeout=defaults['timeout'],
347 startport=defaults['port'], extraconfigopts=None,
347 startport=defaults['port'], extraconfigopts=None,
348 py3kwarnings=False, shell=None):
348 py3kwarnings=False, shell=None):
349 """Create a test from parameters.
349 """Create a test from parameters.
350
350
351 path is the full path to the file defining the test.
351 path is the full path to the file defining the test.
352
352
353 tmpdir is the main temporary directory to use for this test.
353 tmpdir is the main temporary directory to use for this test.
354
354
355 keeptmpdir determines whether to keep the test's temporary directory
355 keeptmpdir determines whether to keep the test's temporary directory
356 after execution. It defaults to removal (False).
356 after execution. It defaults to removal (False).
357
357
358 debug mode will make the test execute verbosely, with unfiltered
358 debug mode will make the test execute verbosely, with unfiltered
359 output.
359 output.
360
360
361 timeout controls the maximum run time of the test. It is ignored when
361 timeout controls the maximum run time of the test. It is ignored when
362 debug is True.
362 debug is True.
363
363
364 startport controls the starting port number to use for this test. Each
364 startport controls the starting port number to use for this test. Each
365 test will reserve 3 port numbers for execution. It is the caller's
365 test will reserve 3 port numbers for execution. It is the caller's
366 responsibility to allocate a non-overlapping port range to Test
366 responsibility to allocate a non-overlapping port range to Test
367 instances.
367 instances.
368
368
369 extraconfigopts is an iterable of extra hgrc config options. Values
369 extraconfigopts is an iterable of extra hgrc config options. Values
370 must have the form "key=value" (something understood by hgrc). Values
370 must have the form "key=value" (something understood by hgrc). Values
371 of the form "foo.key=value" will result in "[foo] key=value".
371 of the form "foo.key=value" will result in "[foo] key=value".
372
372
373 py3kwarnings enables Py3k warnings.
373 py3kwarnings enables Py3k warnings.
374
374
375 shell is the shell to execute tests in.
375 shell is the shell to execute tests in.
376 """
376 """
377
377
378 self.path = path
378 self.path = path
379 self.name = os.path.basename(path)
379 self.name = os.path.basename(path)
380 self._testdir = os.path.dirname(path)
380 self._testdir = os.path.dirname(path)
381 self.errpath = os.path.join(self._testdir, '%s.err' % self.name)
381 self.errpath = os.path.join(self._testdir, '%s.err' % self.name)
382
382
383 self._threadtmp = tmpdir
383 self._threadtmp = tmpdir
384 self._keeptmpdir = keeptmpdir
384 self._keeptmpdir = keeptmpdir
385 self._debug = debug
385 self._debug = debug
386 self._timeout = timeout
386 self._timeout = timeout
387 self._startport = startport
387 self._startport = startport
388 self._extraconfigopts = extraconfigopts or []
388 self._extraconfigopts = extraconfigopts or []
389 self._py3kwarnings = py3kwarnings
389 self._py3kwarnings = py3kwarnings
390 self._shell = shell
390 self._shell = shell
391
391
392 self._aborted = False
392 self._aborted = False
393 self._daemonpids = []
393 self._daemonpids = []
394 self._finished = None
394 self._finished = None
395 self._ret = None
395 self._ret = None
396 self._out = None
396 self._out = None
397 self._skipped = None
397 self._skipped = None
398 self._testtmp = None
398 self._testtmp = None
399
399
400 # If we're not in --debug mode and reference output file exists,
400 # If we're not in --debug mode and reference output file exists,
401 # check test output against it.
401 # check test output against it.
402 if debug:
402 if debug:
403 self._refout = None # to match "out is None"
403 self._refout = None # to match "out is None"
404 elif os.path.exists(self.refpath):
404 elif os.path.exists(self.refpath):
405 f = open(self.refpath, 'rb')
405 f = open(self.refpath, 'rb')
406 self._refout = f.read().splitlines(True)
406 self._refout = f.read().splitlines(True)
407 f.close()
407 f.close()
408 else:
408 else:
409 self._refout = []
409 self._refout = []
410
410
411 def __str__(self):
411 def __str__(self):
412 return self.name
412 return self.name
413
413
414 def shortDescription(self):
414 def shortDescription(self):
415 return self.name
415 return self.name
416
416
417 def setUp(self):
417 def setUp(self):
418 """Tasks to perform before run()."""
418 """Tasks to perform before run()."""
419 self._finished = False
419 self._finished = False
420 self._ret = None
420 self._ret = None
421 self._out = None
421 self._out = None
422 self._skipped = None
422 self._skipped = None
423
423
424 try:
424 try:
425 os.mkdir(self._threadtmp)
425 os.mkdir(self._threadtmp)
426 except OSError, e:
426 except OSError, e:
427 if e.errno != errno.EEXIST:
427 if e.errno != errno.EEXIST:
428 raise
428 raise
429
429
430 self._testtmp = os.path.join(self._threadtmp,
430 self._testtmp = os.path.join(self._threadtmp,
431 os.path.basename(self.path))
431 os.path.basename(self.path))
432 os.mkdir(self._testtmp)
432 os.mkdir(self._testtmp)
433
433
434 # Remove any previous output files.
434 # Remove any previous output files.
435 if os.path.exists(self.errpath):
435 if os.path.exists(self.errpath):
436 os.remove(self.errpath)
436 os.remove(self.errpath)
437
437
438 def run(self, result):
438 def run(self, result):
439 """Run this test and report results against a TestResult instance."""
439 """Run this test and report results against a TestResult instance."""
440 # This function is extremely similar to unittest.TestCase.run(). Once
440 # This function is extremely similar to unittest.TestCase.run(). Once
441 # we require Python 2.7 (or at least its version of unittest), this
441 # we require Python 2.7 (or at least its version of unittest), this
442 # function can largely go away.
442 # function can largely go away.
443 self._result = result
443 self._result = result
444 result.startTest(self)
444 result.startTest(self)
445 try:
445 try:
446 try:
446 try:
447 self.setUp()
447 self.setUp()
448 except (KeyboardInterrupt, SystemExit):
448 except (KeyboardInterrupt, SystemExit):
449 self._aborted = True
449 self._aborted = True
450 raise
450 raise
451 except Exception:
451 except Exception:
452 result.addError(self, sys.exc_info())
452 result.addError(self, sys.exc_info())
453 return
453 return
454
454
455 success = False
455 success = False
456 try:
456 try:
457 self.runTest()
457 self.runTest()
458 except KeyboardInterrupt:
458 except KeyboardInterrupt:
459 self._aborted = True
459 self._aborted = True
460 raise
460 raise
461 except SkipTest, e:
461 except SkipTest, e:
462 result.addSkip(self, str(e))
462 result.addSkip(self, str(e))
463 except IgnoreTest, e:
463 except IgnoreTest, e:
464 result.addIgnore(self, str(e))
464 result.addIgnore(self, str(e))
465 except WarnTest, e:
465 except WarnTest, e:
466 result.addWarn(self, str(e))
466 result.addWarn(self, str(e))
467 except self.failureException, e:
467 except self.failureException, e:
468 # This differs from unittest in that we don't capture
468 # This differs from unittest in that we don't capture
469 # the stack trace. This is for historical reasons and
469 # the stack trace. This is for historical reasons and
470 # this decision could be revisted in the future,
470 # this decision could be revisted in the future,
471 # especially for PythonTest instances.
471 # especially for PythonTest instances.
472 if result.addFailure(self, str(e)):
472 if result.addFailure(self, str(e)):
473 success = True
473 success = True
474 except Exception:
474 except Exception:
475 result.addError(self, sys.exc_info())
475 result.addError(self, sys.exc_info())
476 else:
476 else:
477 success = True
477 success = True
478
478
479 try:
479 try:
480 self.tearDown()
480 self.tearDown()
481 except (KeyboardInterrupt, SystemExit):
481 except (KeyboardInterrupt, SystemExit):
482 self._aborted = True
482 self._aborted = True
483 raise
483 raise
484 except Exception:
484 except Exception:
485 result.addError(self, sys.exc_info())
485 result.addError(self, sys.exc_info())
486 success = False
486 success = False
487
487
488 if success:
488 if success:
489 result.addSuccess(self)
489 result.addSuccess(self)
490 finally:
490 finally:
491 result.stopTest(self, interrupted=self._aborted)
491 result.stopTest(self, interrupted=self._aborted)
492
492
493 def runTest(self):
493 def runTest(self):
494 """Run this test instance.
494 """Run this test instance.
495
495
496 This will return a tuple describing the result of the test.
496 This will return a tuple describing the result of the test.
497 """
497 """
498 replacements = self._getreplacements()
498 replacements = self._getreplacements()
499 env = self._getenv()
499 env = self._getenv()
500 self._daemonpids.append(env['DAEMON_PIDS'])
500 self._daemonpids.append(env['DAEMON_PIDS'])
501 self._createhgrc(env['HGRCPATH'])
501 self._createhgrc(env['HGRCPATH'])
502
502
503 vlog('# Test', self.name)
503 vlog('# Test', self.name)
504
504
505 ret, out = self._run(replacements, env)
505 ret, out = self._run(replacements, env)
506 self._finished = True
506 self._finished = True
507 self._ret = ret
507 self._ret = ret
508 self._out = out
508 self._out = out
509
509
510 def describe(ret):
510 def describe(ret):
511 if ret < 0:
511 if ret < 0:
512 return 'killed by signal: %d' % -ret
512 return 'killed by signal: %d' % -ret
513 return 'returned error code %d' % ret
513 return 'returned error code %d' % ret
514
514
515 self._skipped = False
515 self._skipped = False
516
516
517 if ret == self.SKIPPED_STATUS:
517 if ret == self.SKIPPED_STATUS:
518 if out is None: # Debug mode, nothing to parse.
518 if out is None: # Debug mode, nothing to parse.
519 missing = ['unknown']
519 missing = ['unknown']
520 failed = None
520 failed = None
521 else:
521 else:
522 missing, failed = TTest.parsehghaveoutput(out)
522 missing, failed = TTest.parsehghaveoutput(out)
523
523
524 if not missing:
524 if not missing:
525 missing = ['irrelevant']
525 missing = ['irrelevant']
526
526
527 if failed:
527 if failed:
528 self.fail('hg have failed checking for %s' % failed[-1])
528 self.fail('hg have failed checking for %s' % failed[-1])
529 else:
529 else:
530 self._skipped = True
530 self._skipped = True
531 raise SkipTest(missing[-1])
531 raise SkipTest(missing[-1])
532 elif ret == 'timeout':
532 elif ret == 'timeout':
533 self.fail('timed out')
533 self.fail('timed out')
534 elif ret is False:
534 elif ret is False:
535 raise WarnTest('no result code from test')
535 raise WarnTest('no result code from test')
536 elif out != self._refout:
536 elif out != self._refout:
537 # Diff generation may rely on written .err file.
537 # Diff generation may rely on written .err file.
538 if (ret != 0 or out != self._refout) and not self._skipped \
538 if (ret != 0 or out != self._refout) and not self._skipped \
539 and not self._debug:
539 and not self._debug:
540 f = open(self.errpath, 'wb')
540 f = open(self.errpath, 'wb')
541 for line in out:
541 for line in out:
542 f.write(line)
542 f.write(line)
543 f.close()
543 f.close()
544
544
545 # The result object handles diff calculation for us.
545 # The result object handles diff calculation for us.
546 if self._result.addOutputMismatch(self, ret, out, self._refout):
546 if self._result.addOutputMismatch(self, ret, out, self._refout):
547 # change was accepted, skip failing
547 # change was accepted, skip failing
548 return
548 return
549
549
550 if ret:
550 if ret:
551 msg = 'output changed and ' + describe(ret)
551 msg = 'output changed and ' + describe(ret)
552 else:
552 else:
553 msg = 'output changed'
553 msg = 'output changed'
554
554
555 self.fail(msg)
555 self.fail(msg)
556 elif ret:
556 elif ret:
557 self.fail(describe(ret))
557 self.fail(describe(ret))
558
558
559 def tearDown(self):
559 def tearDown(self):
560 """Tasks to perform after run()."""
560 """Tasks to perform after run()."""
561 for entry in self._daemonpids:
561 for entry in self._daemonpids:
562 killdaemons(entry)
562 killdaemons(entry)
563 self._daemonpids = []
563 self._daemonpids = []
564
564
565 if not self._keeptmpdir:
565 if not self._keeptmpdir:
566 shutil.rmtree(self._testtmp, True)
566 shutil.rmtree(self._testtmp, True)
567 shutil.rmtree(self._threadtmp, True)
567 shutil.rmtree(self._threadtmp, True)
568
568
569 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
569 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
570 and not self._debug and self._out:
570 and not self._debug and self._out:
571 f = open(self.errpath, 'wb')
571 f = open(self.errpath, 'wb')
572 for line in self._out:
572 for line in self._out:
573 f.write(line)
573 f.write(line)
574 f.close()
574 f.close()
575
575
576 vlog("# Ret was:", self._ret)
576 vlog("# Ret was:", self._ret)
577
577
578 def _run(self, replacements, env):
578 def _run(self, replacements, env):
579 # This should be implemented in child classes to run tests.
579 # This should be implemented in child classes to run tests.
580 raise SkipTest('unknown test type')
580 raise SkipTest('unknown test type')
581
581
582 def abort(self):
582 def abort(self):
583 """Terminate execution of this test."""
583 """Terminate execution of this test."""
584 self._aborted = True
584 self._aborted = True
585
585
586 def _getreplacements(self):
586 def _getreplacements(self):
587 """Obtain a mapping of text replacements to apply to test output.
587 """Obtain a mapping of text replacements to apply to test output.
588
588
589 Test output needs to be normalized so it can be compared to expected
589 Test output needs to be normalized so it can be compared to expected
590 output. This function defines how some of that normalization will
590 output. This function defines how some of that normalization will
591 occur.
591 occur.
592 """
592 """
593 r = [
593 r = [
594 (r':%s\b' % self._startport, ':$HGPORT'),
594 (r':%s\b' % self._startport, ':$HGPORT'),
595 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
595 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
596 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
596 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
597 ]
597 ]
598
598
599 if os.name == 'nt':
599 if os.name == 'nt':
600 r.append(
600 r.append(
601 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
601 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
602 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
602 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
603 for c in self._testtmp), '$TESTTMP'))
603 for c in self._testtmp), '$TESTTMP'))
604 else:
604 else:
605 r.append((re.escape(self._testtmp), '$TESTTMP'))
605 r.append((re.escape(self._testtmp), '$TESTTMP'))
606
606
607 return r
607 return r
608
608
609 def _getenv(self):
609 def _getenv(self):
610 """Obtain environment variables to use during test execution."""
610 """Obtain environment variables to use during test execution."""
611 env = os.environ.copy()
611 env = os.environ.copy()
612 env['TESTTMP'] = self._testtmp
612 env['TESTTMP'] = self._testtmp
613 env['HOME'] = self._testtmp
613 env['HOME'] = self._testtmp
614 env["HGPORT"] = str(self._startport)
614 env["HGPORT"] = str(self._startport)
615 env["HGPORT1"] = str(self._startport + 1)
615 env["HGPORT1"] = str(self._startport + 1)
616 env["HGPORT2"] = str(self._startport + 2)
616 env["HGPORT2"] = str(self._startport + 2)
617 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
617 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
618 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
618 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
619 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
619 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
620 env["HGMERGE"] = "internal:merge"
620 env["HGMERGE"] = "internal:merge"
621 env["HGUSER"] = "test"
621 env["HGUSER"] = "test"
622 env["HGENCODING"] = "ascii"
622 env["HGENCODING"] = "ascii"
623 env["HGENCODINGMODE"] = "strict"
623 env["HGENCODINGMODE"] = "strict"
624
624
625 # Reset some environment variables to well-known values so that
625 # Reset some environment variables to well-known values so that
626 # the tests produce repeatable output.
626 # the tests produce repeatable output.
627 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
627 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
628 env['TZ'] = 'GMT'
628 env['TZ'] = 'GMT'
629 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
629 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
630 env['COLUMNS'] = '80'
630 env['COLUMNS'] = '80'
631 env['TERM'] = 'xterm'
631 env['TERM'] = 'xterm'
632
632
633 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
633 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
634 'NO_PROXY').split():
634 'NO_PROXY').split():
635 if k in env:
635 if k in env:
636 del env[k]
636 del env[k]
637
637
638 # unset env related to hooks
638 # unset env related to hooks
639 for k in env.keys():
639 for k in env.keys():
640 if k.startswith('HG_'):
640 if k.startswith('HG_'):
641 del env[k]
641 del env[k]
642
642
643 return env
643 return env
644
644
645 def _createhgrc(self, path):
645 def _createhgrc(self, path):
646 """Create an hgrc file for this test."""
646 """Create an hgrc file for this test."""
647 hgrc = open(path, 'wb')
647 hgrc = open(path, 'wb')
648 hgrc.write('[ui]\n')
648 hgrc.write('[ui]\n')
649 hgrc.write('slash = True\n')
649 hgrc.write('slash = True\n')
650 hgrc.write('interactive = False\n')
650 hgrc.write('interactive = False\n')
651 hgrc.write('mergemarkers = detailed\n')
651 hgrc.write('mergemarkers = detailed\n')
652 hgrc.write('[defaults]\n')
652 hgrc.write('[defaults]\n')
653 hgrc.write('backout = -d "0 0"\n')
653 hgrc.write('backout = -d "0 0"\n')
654 hgrc.write('commit = -d "0 0"\n')
654 hgrc.write('commit = -d "0 0"\n')
655 hgrc.write('shelve = --date "0 0"\n')
655 hgrc.write('shelve = --date "0 0"\n')
656 hgrc.write('tag = -d "0 0"\n')
656 hgrc.write('tag = -d "0 0"\n')
657 for opt in self._extraconfigopts:
657 for opt in self._extraconfigopts:
658 section, key = opt.split('.', 1)
658 section, key = opt.split('.', 1)
659 assert '=' in key, ('extra config opt %s must '
659 assert '=' in key, ('extra config opt %s must '
660 'have an = for assignment' % opt)
660 'have an = for assignment' % opt)
661 hgrc.write('[%s]\n%s\n' % (section, key))
661 hgrc.write('[%s]\n%s\n' % (section, key))
662 hgrc.close()
662 hgrc.close()
663
663
664 def fail(self, msg):
664 def fail(self, msg):
665 # unittest differentiates between errored and failed.
665 # unittest differentiates between errored and failed.
666 # Failed is denoted by AssertionError (by default at least).
666 # Failed is denoted by AssertionError (by default at least).
667 raise AssertionError(msg)
667 raise AssertionError(msg)
668
668
669 class PythonTest(Test):
669 class PythonTest(Test):
670 """A Python-based test."""
670 """A Python-based test."""
671
671
672 @property
672 @property
673 def refpath(self):
673 def refpath(self):
674 return os.path.join(self._testdir, '%s.out' % self.name)
674 return os.path.join(self._testdir, '%s.out' % self.name)
675
675
676 def _run(self, replacements, env):
676 def _run(self, replacements, env):
677 py3kswitch = self._py3kwarnings and ' -3' or ''
677 py3kswitch = self._py3kwarnings and ' -3' or ''
678 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
678 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
679 vlog("# Running", cmd)
679 vlog("# Running", cmd)
680 if os.name == 'nt':
680 if os.name == 'nt':
681 replacements.append((r'\r\n', '\n'))
681 replacements.append((r'\r\n', '\n'))
682 result = run(cmd, self._testtmp, replacements, env,
682 result = run(cmd, self._testtmp, replacements, env,
683 debug=self._debug, timeout=self._timeout)
683 debug=self._debug, timeout=self._timeout)
684 if self._aborted:
684 if self._aborted:
685 raise KeyboardInterrupt()
685 raise KeyboardInterrupt()
686
686
687 return result
687 return result
688
688
689 class TTest(Test):
689 class TTest(Test):
690 """A "t test" is a test backed by a .t file."""
690 """A "t test" is a test backed by a .t file."""
691
691
692 SKIPPED_PREFIX = 'skipped: '
692 SKIPPED_PREFIX = 'skipped: '
693 FAILED_PREFIX = 'hghave check failed: '
693 FAILED_PREFIX = 'hghave check failed: '
694 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
694 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
695
695
696 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
696 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
697 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
697 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
698 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
698 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
699
699
700 @property
700 @property
701 def refpath(self):
701 def refpath(self):
702 return os.path.join(self._testdir, self.name)
702 return os.path.join(self._testdir, self.name)
703
703
704 def _run(self, replacements, env):
704 def _run(self, replacements, env):
705 f = open(self.path, 'rb')
705 f = open(self.path, 'rb')
706 lines = f.readlines()
706 lines = f.readlines()
707 f.close()
707 f.close()
708
708
709 salt, script, after, expected = self._parsetest(lines)
709 salt, script, after, expected = self._parsetest(lines)
710
710
711 # Write out the generated script.
711 # Write out the generated script.
712 fname = '%s.sh' % self._testtmp
712 fname = '%s.sh' % self._testtmp
713 f = open(fname, 'wb')
713 f = open(fname, 'wb')
714 for l in script:
714 for l in script:
715 f.write(l)
715 f.write(l)
716 f.close()
716 f.close()
717
717
718 cmd = '%s "%s"' % (self._shell, fname)
718 cmd = '%s "%s"' % (self._shell, fname)
719 vlog("# Running", cmd)
719 vlog("# Running", cmd)
720
720
721 exitcode, output = run(cmd, self._testtmp, replacements, env,
721 exitcode, output = run(cmd, self._testtmp, replacements, env,
722 debug=self._debug, timeout=self._timeout)
722 debug=self._debug, timeout=self._timeout)
723
723
724 if self._aborted:
724 if self._aborted:
725 raise KeyboardInterrupt()
725 raise KeyboardInterrupt()
726
726
727 # Do not merge output if skipped. Return hghave message instead.
727 # Do not merge output if skipped. Return hghave message instead.
728 # Similarly, with --debug, output is None.
728 # Similarly, with --debug, output is None.
729 if exitcode == self.SKIPPED_STATUS or output is None:
729 if exitcode == self.SKIPPED_STATUS or output is None:
730 return exitcode, output
730 return exitcode, output
731
731
732 return self._processoutput(exitcode, output, salt, after, expected)
732 return self._processoutput(exitcode, output, salt, after, expected)
733
733
734 def _hghave(self, reqs):
734 def _hghave(self, reqs):
735 # TODO do something smarter when all other uses of hghave are gone.
735 # TODO do something smarter when all other uses of hghave are gone.
736 tdir = self._testdir.replace('\\', '/')
736 tdir = self._testdir.replace('\\', '/')
737 proc = Popen4('%s -c "%s/hghave %s"' %
737 proc = Popen4('%s -c "%s/hghave %s"' %
738 (self._shell, tdir, ' '.join(reqs)),
738 (self._shell, tdir, ' '.join(reqs)),
739 self._testtmp, 0)
739 self._testtmp, 0)
740 stdout, stderr = proc.communicate()
740 stdout, stderr = proc.communicate()
741 ret = proc.wait()
741 ret = proc.wait()
742 if wifexited(ret):
742 if wifexited(ret):
743 ret = os.WEXITSTATUS(ret)
743 ret = os.WEXITSTATUS(ret)
744 if ret == 2:
744 if ret == 2:
745 print stdout
745 print stdout
746 sys.exit(1)
746 sys.exit(1)
747
747
748 return ret == 0
748 return ret == 0
749
749
750 def _parsetest(self, lines):
750 def _parsetest(self, lines):
751 # We generate a shell script which outputs unique markers to line
751 # We generate a shell script which outputs unique markers to line
752 # up script results with our source. These markers include input
752 # up script results with our source. These markers include input
753 # line number and the last return code.
753 # line number and the last return code.
754 salt = "SALT" + str(time.time())
754 salt = "SALT" + str(time.time())
755 def addsalt(line, inpython):
755 def addsalt(line, inpython):
756 if inpython:
756 if inpython:
757 script.append('%s %d 0\n' % (salt, line))
757 script.append('%s %d 0\n' % (salt, line))
758 else:
758 else:
759 script.append('echo %s %s $?\n' % (salt, line))
759 script.append('echo %s %s $?\n' % (salt, line))
760
760
761 script = []
761 script = []
762
762
763 # After we run the shell script, we re-unify the script output
763 # After we run the shell script, we re-unify the script output
764 # with non-active parts of the source, with synchronization by our
764 # with non-active parts of the source, with synchronization by our
765 # SALT line number markers. The after table contains the non-active
765 # SALT line number markers. The after table contains the non-active
766 # components, ordered by line number.
766 # components, ordered by line number.
767 after = {}
767 after = {}
768
768
769 # Expected shell script output.
769 # Expected shell script output.
770 expected = {}
770 expected = {}
771
771
772 pos = prepos = -1
772 pos = prepos = -1
773
773
774 # True or False when in a true or false conditional section
774 # True or False when in a true or false conditional section
775 skipping = None
775 skipping = None
776
776
777 # We keep track of whether or not we're in a Python block so we
777 # We keep track of whether or not we're in a Python block so we
778 # can generate the surrounding doctest magic.
778 # can generate the surrounding doctest magic.
779 inpython = False
779 inpython = False
780
780
781 if self._debug:
781 if self._debug:
782 script.append('set -x\n')
782 script.append('set -x\n')
783 if os.getenv('MSYSTEM'):
783 if os.getenv('MSYSTEM'):
784 script.append('alias pwd="pwd -W"\n')
784 script.append('alias pwd="pwd -W"\n')
785
785
786 for n, l in enumerate(lines):
786 for n, l in enumerate(lines):
787 if not l.endswith('\n'):
787 if not l.endswith('\n'):
788 l += '\n'
788 l += '\n'
789 if l.startswith('#if'):
789 if l.startswith('#if'):
790 lsplit = l.split()
790 lsplit = l.split()
791 if len(lsplit) < 2 or lsplit[0] != '#if':
791 if len(lsplit) < 2 or lsplit[0] != '#if':
792 after.setdefault(pos, []).append(' !!! invalid #if\n')
792 after.setdefault(pos, []).append(' !!! invalid #if\n')
793 if skipping is not None:
793 if skipping is not None:
794 after.setdefault(pos, []).append(' !!! nested #if\n')
794 after.setdefault(pos, []).append(' !!! nested #if\n')
795 skipping = not self._hghave(lsplit[1:])
795 skipping = not self._hghave(lsplit[1:])
796 after.setdefault(pos, []).append(l)
796 after.setdefault(pos, []).append(l)
797 elif l.startswith('#else'):
797 elif l.startswith('#else'):
798 if skipping is None:
798 if skipping is None:
799 after.setdefault(pos, []).append(' !!! missing #if\n')
799 after.setdefault(pos, []).append(' !!! missing #if\n')
800 skipping = not skipping
800 skipping = not skipping
801 after.setdefault(pos, []).append(l)
801 after.setdefault(pos, []).append(l)
802 elif l.startswith('#endif'):
802 elif l.startswith('#endif'):
803 if skipping is None:
803 if skipping is None:
804 after.setdefault(pos, []).append(' !!! missing #if\n')
804 after.setdefault(pos, []).append(' !!! missing #if\n')
805 skipping = None
805 skipping = None
806 after.setdefault(pos, []).append(l)
806 after.setdefault(pos, []).append(l)
807 elif skipping:
807 elif skipping:
808 after.setdefault(pos, []).append(l)
808 after.setdefault(pos, []).append(l)
809 elif l.startswith(' >>> '): # python inlines
809 elif l.startswith(' >>> '): # python inlines
810 after.setdefault(pos, []).append(l)
810 after.setdefault(pos, []).append(l)
811 prepos = pos
811 prepos = pos
812 pos = n
812 pos = n
813 if not inpython:
813 if not inpython:
814 # We've just entered a Python block. Add the header.
814 # We've just entered a Python block. Add the header.
815 inpython = True
815 inpython = True
816 addsalt(prepos, False) # Make sure we report the exit code.
816 addsalt(prepos, False) # Make sure we report the exit code.
817 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
817 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
818 addsalt(n, True)
818 addsalt(n, True)
819 script.append(l[2:])
819 script.append(l[2:])
820 elif l.startswith(' ... '): # python inlines
820 elif l.startswith(' ... '): # python inlines
821 after.setdefault(prepos, []).append(l)
821 after.setdefault(prepos, []).append(l)
822 script.append(l[2:])
822 script.append(l[2:])
823 elif l.startswith(' $ '): # commands
823 elif l.startswith(' $ '): # commands
824 if inpython:
824 if inpython:
825 script.append('EOF\n')
825 script.append('EOF\n')
826 inpython = False
826 inpython = False
827 after.setdefault(pos, []).append(l)
827 after.setdefault(pos, []).append(l)
828 prepos = pos
828 prepos = pos
829 pos = n
829 pos = n
830 addsalt(n, False)
830 addsalt(n, False)
831 cmd = l[4:].split()
831 cmd = l[4:].split()
832 if len(cmd) == 2 and cmd[0] == 'cd':
832 if len(cmd) == 2 and cmd[0] == 'cd':
833 l = ' $ cd %s || exit 1\n' % cmd[1]
833 l = ' $ cd %s || exit 1\n' % cmd[1]
834 script.append(l[4:])
834 script.append(l[4:])
835 elif l.startswith(' > '): # continuations
835 elif l.startswith(' > '): # continuations
836 after.setdefault(prepos, []).append(l)
836 after.setdefault(prepos, []).append(l)
837 script.append(l[4:])
837 script.append(l[4:])
838 elif l.startswith(' '): # results
838 elif l.startswith(' '): # results
839 # Queue up a list of expected results.
839 # Queue up a list of expected results.
840 expected.setdefault(pos, []).append(l[2:])
840 expected.setdefault(pos, []).append(l[2:])
841 else:
841 else:
842 if inpython:
842 if inpython:
843 script.append('EOF\n')
843 script.append('EOF\n')
844 inpython = False
844 inpython = False
845 # Non-command/result. Queue up for merged output.
845 # Non-command/result. Queue up for merged output.
846 after.setdefault(pos, []).append(l)
846 after.setdefault(pos, []).append(l)
847
847
848 if inpython:
848 if inpython:
849 script.append('EOF\n')
849 script.append('EOF\n')
850 if skipping is not None:
850 if skipping is not None:
851 after.setdefault(pos, []).append(' !!! missing #endif\n')
851 after.setdefault(pos, []).append(' !!! missing #endif\n')
852 addsalt(n + 1, False)
852 addsalt(n + 1, False)
853
853
854 return salt, script, after, expected
854 return salt, script, after, expected
855
855
856 def _processoutput(self, exitcode, output, salt, after, expected):
856 def _processoutput(self, exitcode, output, salt, after, expected):
857 # Merge the script output back into a unified test.
857 # Merge the script output back into a unified test.
858 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
858 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
859 if exitcode != 0:
859 if exitcode != 0:
860 warnonly = 3
860 warnonly = 3
861
861
862 pos = -1
862 pos = -1
863 postout = []
863 postout = []
864 for l in output:
864 for l in output:
865 lout, lcmd = l, None
865 lout, lcmd = l, None
866 if salt in l:
866 if salt in l:
867 lout, lcmd = l.split(salt, 1)
867 lout, lcmd = l.split(salt, 1)
868
868
869 if lout:
869 if lout:
870 if not lout.endswith('\n'):
870 if not lout.endswith('\n'):
871 lout += ' (no-eol)\n'
871 lout += ' (no-eol)\n'
872
872
873 # Find the expected output at the current position.
873 # Find the expected output at the current position.
874 el = None
874 el = None
875 if expected.get(pos, None):
875 if expected.get(pos, None):
876 el = expected[pos].pop(0)
876 el = expected[pos].pop(0)
877
877
878 r = TTest.linematch(el, lout)
878 r = TTest.linematch(el, lout)
879 if isinstance(r, str):
879 if isinstance(r, str):
880 if r == '+glob':
880 if r == '+glob':
881 lout = el[:-1] + ' (glob)\n'
881 lout = el[:-1] + ' (glob)\n'
882 r = '' # Warn only this line.
882 r = '' # Warn only this line.
883 elif r == '-glob':
883 elif r == '-glob':
884 lout = ''.join(el.rsplit(' (glob)', 1))
884 lout = ''.join(el.rsplit(' (glob)', 1))
885 r = '' # Warn only this line.
885 r = '' # Warn only this line.
886 else:
886 else:
887 log('\ninfo, unknown linematch result: %r\n' % r)
887 log('\ninfo, unknown linematch result: %r\n' % r)
888 r = False
888 r = False
889 if r:
889 if r:
890 postout.append(' ' + el)
890 postout.append(' ' + el)
891 else:
891 else:
892 if self.NEEDESCAPE(lout):
892 if self.NEEDESCAPE(lout):
893 lout = TTest._stringescape('%s (esc)\n' %
893 lout = TTest._stringescape('%s (esc)\n' %
894 lout.rstrip('\n'))
894 lout.rstrip('\n'))
895 postout.append(' ' + lout) # Let diff deal with it.
895 postout.append(' ' + lout) # Let diff deal with it.
896 if r != '': # If line failed.
896 if r != '': # If line failed.
897 warnonly = 3 # for sure not
897 warnonly = 3 # for sure not
898 elif warnonly == 1: # Is "not yet" and line is warn only.
898 elif warnonly == 1: # Is "not yet" and line is warn only.
899 warnonly = 2 # Yes do warn.
899 warnonly = 2 # Yes do warn.
900
900
901 if lcmd:
901 if lcmd:
902 # Add on last return code.
902 # Add on last return code.
903 ret = int(lcmd.split()[1])
903 ret = int(lcmd.split()[1])
904 if ret != 0:
904 if ret != 0:
905 postout.append(' [%s]\n' % ret)
905 postout.append(' [%s]\n' % ret)
906 if pos in after:
906 if pos in after:
907 # Merge in non-active test bits.
907 # Merge in non-active test bits.
908 postout += after.pop(pos)
908 postout += after.pop(pos)
909 pos = int(lcmd.split()[0])
909 pos = int(lcmd.split()[0])
910
910
911 if pos in after:
911 if pos in after:
912 postout += after.pop(pos)
912 postout += after.pop(pos)
913
913
914 if warnonly == 2:
914 if warnonly == 2:
915 exitcode = False # Set exitcode to warned.
915 exitcode = False # Set exitcode to warned.
916
916
917 return exitcode, postout
917 return exitcode, postout
918
918
919 @staticmethod
919 @staticmethod
920 def rematch(el, l):
920 def rematch(el, l):
921 try:
921 try:
922 # use \Z to ensure that the regex matches to the end of the string
922 # use \Z to ensure that the regex matches to the end of the string
923 if os.name == 'nt':
923 if os.name == 'nt':
924 return re.match(el + r'\r?\n\Z', l)
924 return re.match(el + r'\r?\n\Z', l)
925 return re.match(el + r'\n\Z', l)
925 return re.match(el + r'\n\Z', l)
926 except re.error:
926 except re.error:
927 # el is an invalid regex
927 # el is an invalid regex
928 return False
928 return False
929
929
930 @staticmethod
930 @staticmethod
931 def globmatch(el, l):
931 def globmatch(el, l):
932 # The only supported special characters are * and ? plus / which also
932 # The only supported special characters are * and ? plus / which also
933 # matches \ on windows. Escaping of these characters is supported.
933 # matches \ on windows. Escaping of these characters is supported.
934 if el + '\n' == l:
934 if el + '\n' == l:
935 if os.altsep:
935 if os.altsep:
936 # matching on "/" is not needed for this line
936 # matching on "/" is not needed for this line
937 return '-glob'
937 return '-glob'
938 return True
938 return True
939 i, n = 0, len(el)
939 i, n = 0, len(el)
940 res = ''
940 res = ''
941 while i < n:
941 while i < n:
942 c = el[i]
942 c = el[i]
943 i += 1
943 i += 1
944 if c == '\\' and el[i] in '*?\\/':
944 if c == '\\' and el[i] in '*?\\/':
945 res += el[i - 1:i + 1]
945 res += el[i - 1:i + 1]
946 i += 1
946 i += 1
947 elif c == '*':
947 elif c == '*':
948 res += '.*'
948 res += '.*'
949 elif c == '?':
949 elif c == '?':
950 res += '.'
950 res += '.'
951 elif c == '/' and os.altsep:
951 elif c == '/' and os.altsep:
952 res += '[/\\\\]'
952 res += '[/\\\\]'
953 else:
953 else:
954 res += re.escape(c)
954 res += re.escape(c)
955 return TTest.rematch(res, l)
955 return TTest.rematch(res, l)
956
956
957 @staticmethod
957 @staticmethod
958 def linematch(el, l):
958 def linematch(el, l):
959 if el == l: # perfect match (fast)
959 if el == l: # perfect match (fast)
960 return True
960 return True
961 if el:
961 if el:
962 if el.endswith(" (esc)\n"):
962 if el.endswith(" (esc)\n"):
963 el = el[:-7].decode('string-escape') + '\n'
963 el = el[:-7].decode('string-escape') + '\n'
964 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
964 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
965 return True
965 return True
966 if el.endswith(" (re)\n"):
966 if el.endswith(" (re)\n"):
967 return TTest.rematch(el[:-6], l)
967 return TTest.rematch(el[:-6], l)
968 if el.endswith(" (glob)\n"):
968 if el.endswith(" (glob)\n"):
969 return TTest.globmatch(el[:-8], l)
969 return TTest.globmatch(el[:-8], l)
970 if os.altsep and l.replace('\\', '/') == el:
970 if os.altsep and l.replace('\\', '/') == el:
971 return '+glob'
971 return '+glob'
972 return False
972 return False
973
973
974 @staticmethod
974 @staticmethod
975 def parsehghaveoutput(lines):
975 def parsehghaveoutput(lines):
976 '''Parse hghave log lines.
976 '''Parse hghave log lines.
977
977
978 Return tuple of lists (missing, failed):
978 Return tuple of lists (missing, failed):
979 * the missing/unknown features
979 * the missing/unknown features
980 * the features for which existence check failed'''
980 * the features for which existence check failed'''
981 missing = []
981 missing = []
982 failed = []
982 failed = []
983 for line in lines:
983 for line in lines:
984 if line.startswith(TTest.SKIPPED_PREFIX):
984 if line.startswith(TTest.SKIPPED_PREFIX):
985 line = line.splitlines()[0]
985 line = line.splitlines()[0]
986 missing.append(line[len(TTest.SKIPPED_PREFIX):])
986 missing.append(line[len(TTest.SKIPPED_PREFIX):])
987 elif line.startswith(TTest.FAILED_PREFIX):
987 elif line.startswith(TTest.FAILED_PREFIX):
988 line = line.splitlines()[0]
988 line = line.splitlines()[0]
989 failed.append(line[len(TTest.FAILED_PREFIX):])
989 failed.append(line[len(TTest.FAILED_PREFIX):])
990
990
991 return missing, failed
991 return missing, failed
992
992
993 @staticmethod
993 @staticmethod
994 def _escapef(m):
994 def _escapef(m):
995 return TTest.ESCAPEMAP[m.group(0)]
995 return TTest.ESCAPEMAP[m.group(0)]
996
996
997 @staticmethod
997 @staticmethod
998 def _stringescape(s):
998 def _stringescape(s):
999 return TTest.ESCAPESUB(TTest._escapef, s)
999 return TTest.ESCAPESUB(TTest._escapef, s)
1000
1000
1001
1001
1002 wifexited = getattr(os, "WIFEXITED", lambda x: False)
1002 wifexited = getattr(os, "WIFEXITED", lambda x: False)
1003 def run(cmd, wd, replacements, env, debug=False, timeout=None):
1003 def run(cmd, wd, replacements, env, debug=False, timeout=None):
1004 """Run command in a sub-process, capturing the output (stdout and stderr).
1004 """Run command in a sub-process, capturing the output (stdout and stderr).
1005 Return a tuple (exitcode, output). output is None in debug mode."""
1005 Return a tuple (exitcode, output). output is None in debug mode."""
1006 if debug:
1006 if debug:
1007 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1007 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1008 ret = proc.wait()
1008 ret = proc.wait()
1009 return (ret, None)
1009 return (ret, None)
1010
1010
1011 proc = Popen4(cmd, wd, timeout, env)
1011 proc = Popen4(cmd, wd, timeout, env)
1012 def cleanup():
1012 def cleanup():
1013 terminate(proc)
1013 terminate(proc)
1014 ret = proc.wait()
1014 ret = proc.wait()
1015 if ret == 0:
1015 if ret == 0:
1016 ret = signal.SIGTERM << 8
1016 ret = signal.SIGTERM << 8
1017 killdaemons(env['DAEMON_PIDS'])
1017 killdaemons(env['DAEMON_PIDS'])
1018 return ret
1018 return ret
1019
1019
1020 output = ''
1020 output = ''
1021 proc.tochild.close()
1021 proc.tochild.close()
1022
1022
1023 try:
1023 try:
1024 output = proc.fromchild.read()
1024 output = proc.fromchild.read()
1025 except KeyboardInterrupt:
1025 except KeyboardInterrupt:
1026 vlog('# Handling keyboard interrupt')
1026 vlog('# Handling keyboard interrupt')
1027 cleanup()
1027 cleanup()
1028 raise
1028 raise
1029
1029
1030 ret = proc.wait()
1030 ret = proc.wait()
1031 if wifexited(ret):
1031 if wifexited(ret):
1032 ret = os.WEXITSTATUS(ret)
1032 ret = os.WEXITSTATUS(ret)
1033
1033
1034 if proc.timeout:
1034 if proc.timeout:
1035 ret = 'timeout'
1035 ret = 'timeout'
1036
1036
1037 if ret:
1037 if ret:
1038 killdaemons(env['DAEMON_PIDS'])
1038 killdaemons(env['DAEMON_PIDS'])
1039
1039
1040 for s, r in replacements:
1040 for s, r in replacements:
1041 output = re.sub(s, r, output)
1041 output = re.sub(s, r, output)
1042 return ret, output.splitlines(True)
1042 return ret, output.splitlines(True)
1043
1043
1044 iolock = threading.Lock()
1044 iolock = threading.Lock()
1045
1045
1046 class SkipTest(Exception):
1046 class SkipTest(Exception):
1047 """Raised to indicate that a test is to be skipped."""
1047 """Raised to indicate that a test is to be skipped."""
1048
1048
1049 class IgnoreTest(Exception):
1049 class IgnoreTest(Exception):
1050 """Raised to indicate that a test is to be ignored."""
1050 """Raised to indicate that a test is to be ignored."""
1051
1051
1052 class WarnTest(Exception):
1052 class WarnTest(Exception):
1053 """Raised to indicate that a test warned."""
1053 """Raised to indicate that a test warned."""
1054
1054
1055 class TestResult(unittest._TextTestResult):
1055 class TestResult(unittest._TextTestResult):
1056 """Holds results when executing via unittest."""
1056 """Holds results when executing via unittest."""
1057 # Don't worry too much about accessing the non-public _TextTestResult.
1057 # Don't worry too much about accessing the non-public _TextTestResult.
1058 # It is relatively common in Python testing tools.
1058 # It is relatively common in Python testing tools.
1059 def __init__(self, options, *args, **kwargs):
1059 def __init__(self, options, *args, **kwargs):
1060 super(TestResult, self).__init__(*args, **kwargs)
1060 super(TestResult, self).__init__(*args, **kwargs)
1061
1061
1062 self._options = options
1062 self._options = options
1063
1063
1064 # unittest.TestResult didn't have skipped until 2.7. We need to
1064 # unittest.TestResult didn't have skipped until 2.7. We need to
1065 # polyfill it.
1065 # polyfill it.
1066 self.skipped = []
1066 self.skipped = []
1067
1067
1068 # We have a custom "ignored" result that isn't present in any Python
1068 # We have a custom "ignored" result that isn't present in any Python
1069 # unittest implementation. It is very similar to skipped. It may make
1069 # unittest implementation. It is very similar to skipped. It may make
1070 # sense to map it into skip some day.
1070 # sense to map it into skip some day.
1071 self.ignored = []
1071 self.ignored = []
1072
1072
1073 # We have a custom "warned" result that isn't present in any Python
1073 # We have a custom "warned" result that isn't present in any Python
1074 # unittest implementation. It is very similar to failed. It may make
1074 # unittest implementation. It is very similar to failed. It may make
1075 # sense to map it into fail some day.
1075 # sense to map it into fail some day.
1076 self.warned = []
1076 self.warned = []
1077
1077
1078 self.times = []
1078 self.times = []
1079 self._started = {}
1079 self._started = {}
1080 self._stopped = {}
1080 self._stopped = {}
1081
1081
1082 def addFailure(self, test, reason):
1082 def addFailure(self, test, reason):
1083 self.failures.append((test, reason))
1083 self.failures.append((test, reason))
1084
1084
1085 if self._options.first:
1085 if self._options.first:
1086 self.stop()
1086 self.stop()
1087 else:
1087 else:
1088 iolock.acquire()
1088 if not self._options.nodiff:
1089 if not self._options.nodiff:
1089 self.stream.write('\nERROR: %s output changed\n' % test)
1090 self.stream.write('\nERROR: %s output changed\n' % test)
1090
1091
1091 self.stream.write('!')
1092 self.stream.write('!')
1093 iolock.release()
1092
1094
1093 def addError(self, *args, **kwargs):
1095 def addError(self, *args, **kwargs):
1094 super(TestResult, self).addError(*args, **kwargs)
1096 super(TestResult, self).addError(*args, **kwargs)
1095
1097
1096 if self._options.first:
1098 if self._options.first:
1097 self.stop()
1099 self.stop()
1098
1100
1099 # Polyfill.
1101 # Polyfill.
1100 def addSkip(self, test, reason):
1102 def addSkip(self, test, reason):
1101 self.skipped.append((test, reason))
1103 self.skipped.append((test, reason))
1102
1104
1103 if self.showAll:
1105 if self.showAll:
1104 self.stream.writeln('skipped %s' % reason)
1106 self.stream.writeln('skipped %s' % reason)
1105 else:
1107 else:
1106 self.stream.write('s')
1108 self.stream.write('s')
1107 self.stream.flush()
1109 self.stream.flush()
1108
1110
1109 def addIgnore(self, test, reason):
1111 def addIgnore(self, test, reason):
1110 self.ignored.append((test, reason))
1112 self.ignored.append((test, reason))
1111
1113
1112 if self.showAll:
1114 if self.showAll:
1113 self.stream.writeln('ignored %s' % reason)
1115 self.stream.writeln('ignored %s' % reason)
1114 else:
1116 else:
1115 if reason != 'not retesting':
1117 if reason != 'not retesting':
1116 self.stream.write('i')
1118 self.stream.write('i')
1117 self.stream.flush()
1119 self.stream.flush()
1118
1120
1119 def addWarn(self, test, reason):
1121 def addWarn(self, test, reason):
1120 self.warned.append((test, reason))
1122 self.warned.append((test, reason))
1121
1123
1122 if self._options.first:
1124 if self._options.first:
1123 self.stop()
1125 self.stop()
1124
1126
1125 if self.showAll:
1127 if self.showAll:
1126 self.stream.writeln('warned %s' % reason)
1128 self.stream.writeln('warned %s' % reason)
1127 else:
1129 else:
1128 self.stream.write('~')
1130 self.stream.write('~')
1129 self.stream.flush()
1131 self.stream.flush()
1130
1132
1131 def addOutputMismatch(self, test, ret, got, expected):
1133 def addOutputMismatch(self, test, ret, got, expected):
1132 """Record a mismatch in test output for a particular test."""
1134 """Record a mismatch in test output for a particular test."""
1133
1135
1134 accepted = False
1136 accepted = False
1135
1137
1136 iolock.acquire()
1138 iolock.acquire()
1137 if self._options.nodiff:
1139 if self._options.nodiff:
1138 pass
1140 pass
1139 elif self._options.view:
1141 elif self._options.view:
1140 os.system("%s %s %s" %
1142 os.system("%s %s %s" %
1141 (self._options.view, test.refpath, test.errpath))
1143 (self._options.view, test.refpath, test.errpath))
1142 else:
1144 else:
1143 failed, lines = getdiff(expected, got,
1145 failed, lines = getdiff(expected, got,
1144 test.refpath, test.errpath)
1146 test.refpath, test.errpath)
1145 if failed:
1147 if failed:
1146 self.addFailure(test, 'diff generation failed')
1148 self.addFailure(test, 'diff generation failed')
1147 else:
1149 else:
1148 self.stream.write('\n')
1150 self.stream.write('\n')
1149 for line in lines:
1151 for line in lines:
1150 self.stream.write(line)
1152 self.stream.write(line)
1151 self.stream.flush()
1153 self.stream.flush()
1152
1154
1153 # handle interactive prompt without releasing iolock
1155 # handle interactive prompt without releasing iolock
1154 if self._options.interactive:
1156 if self._options.interactive:
1155 self.stream.write('Accept this change? [n] ')
1157 self.stream.write('Accept this change? [n] ')
1156 answer = sys.stdin.readline().strip()
1158 answer = sys.stdin.readline().strip()
1157 if answer.lower() in ('y', 'yes'):
1159 if answer.lower() in ('y', 'yes'):
1158 if test.name.endswith('.t'):
1160 if test.name.endswith('.t'):
1159 rename(test.errpath, test.path)
1161 rename(test.errpath, test.path)
1160 else:
1162 else:
1161 rename(test.errpath, '%s.out' % test.path)
1163 rename(test.errpath, '%s.out' % test.path)
1162 accepted = True
1164 accepted = True
1163
1165
1164 iolock.release()
1166 iolock.release()
1165
1167
1166 return accepted
1168 return accepted
1167
1169
1168 def startTest(self, test):
1170 def startTest(self, test):
1169 super(TestResult, self).startTest(test)
1171 super(TestResult, self).startTest(test)
1170
1172
1171 # os.times module computes the user time and system time spent by
1173 # os.times module computes the user time and system time spent by
1172 # child's processes along with real elapsed time taken by a process.
1174 # child's processes along with real elapsed time taken by a process.
1173 # This module has one limitation. It can only work for Linux user
1175 # This module has one limitation. It can only work for Linux user
1174 # and not for Windows.
1176 # and not for Windows.
1175 self._started[test.name] = os.times()
1177 self._started[test.name] = os.times()
1176
1178
1177 def stopTest(self, test, interrupted=False):
1179 def stopTest(self, test, interrupted=False):
1178 super(TestResult, self).stopTest(test)
1180 super(TestResult, self).stopTest(test)
1179
1181
1180 self._stopped[test.name] = os.times()
1182 self._stopped[test.name] = os.times()
1181
1183
1182 starttime = self._started[test.name]
1184 starttime = self._started[test.name]
1183 endtime = self._stopped[test.name]
1185 endtime = self._stopped[test.name]
1184 self.times.append((test.name, endtime[2] - starttime[2],
1186 self.times.append((test.name, endtime[2] - starttime[2],
1185 endtime[3] - starttime[3], endtime[4] - starttime[4]))
1187 endtime[3] - starttime[3], endtime[4] - starttime[4]))
1186
1188
1187 del self._started[test.name]
1189 del self._started[test.name]
1188 del self._stopped[test.name]
1190 del self._stopped[test.name]
1189
1191
1190 if interrupted:
1192 if interrupted:
1191 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1193 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1192 test.name, self.times[-1][3]))
1194 test.name, self.times[-1][3]))
1193
1195
1194 class TestSuite(unittest.TestSuite):
1196 class TestSuite(unittest.TestSuite):
1195 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1197 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1196
1198
1197 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1199 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1198 retest=False, keywords=None, loop=False,
1200 retest=False, keywords=None, loop=False,
1199 *args, **kwargs):
1201 *args, **kwargs):
1200 """Create a new instance that can run tests with a configuration.
1202 """Create a new instance that can run tests with a configuration.
1201
1203
1202 testdir specifies the directory where tests are executed from. This
1204 testdir specifies the directory where tests are executed from. This
1203 is typically the ``tests`` directory from Mercurial's source
1205 is typically the ``tests`` directory from Mercurial's source
1204 repository.
1206 repository.
1205
1207
1206 jobs specifies the number of jobs to run concurrently. Each test
1208 jobs specifies the number of jobs to run concurrently. Each test
1207 executes on its own thread. Tests actually spawn new processes, so
1209 executes on its own thread. Tests actually spawn new processes, so
1208 state mutation should not be an issue.
1210 state mutation should not be an issue.
1209
1211
1210 whitelist and blacklist denote tests that have been whitelisted and
1212 whitelist and blacklist denote tests that have been whitelisted and
1211 blacklisted, respectively. These arguments don't belong in TestSuite.
1213 blacklisted, respectively. These arguments don't belong in TestSuite.
1212 Instead, whitelist and blacklist should be handled by the thing that
1214 Instead, whitelist and blacklist should be handled by the thing that
1213 populates the TestSuite with tests. They are present to preserve
1215 populates the TestSuite with tests. They are present to preserve
1214 backwards compatible behavior which reports skipped tests as part
1216 backwards compatible behavior which reports skipped tests as part
1215 of the results.
1217 of the results.
1216
1218
1217 retest denotes whether to retest failed tests. This arguably belongs
1219 retest denotes whether to retest failed tests. This arguably belongs
1218 outside of TestSuite.
1220 outside of TestSuite.
1219
1221
1220 keywords denotes key words that will be used to filter which tests
1222 keywords denotes key words that will be used to filter which tests
1221 to execute. This arguably belongs outside of TestSuite.
1223 to execute. This arguably belongs outside of TestSuite.
1222
1224
1223 loop denotes whether to loop over tests forever.
1225 loop denotes whether to loop over tests forever.
1224 """
1226 """
1225 super(TestSuite, self).__init__(*args, **kwargs)
1227 super(TestSuite, self).__init__(*args, **kwargs)
1226
1228
1227 self._jobs = jobs
1229 self._jobs = jobs
1228 self._whitelist = whitelist
1230 self._whitelist = whitelist
1229 self._blacklist = blacklist
1231 self._blacklist = blacklist
1230 self._retest = retest
1232 self._retest = retest
1231 self._keywords = keywords
1233 self._keywords = keywords
1232 self._loop = loop
1234 self._loop = loop
1233
1235
1234 def run(self, result):
1236 def run(self, result):
1235 # We have a number of filters that need to be applied. We do this
1237 # We have a number of filters that need to be applied. We do this
1236 # here instead of inside Test because it makes the running logic for
1238 # here instead of inside Test because it makes the running logic for
1237 # Test simpler.
1239 # Test simpler.
1238 tests = []
1240 tests = []
1239 for test in self._tests:
1241 for test in self._tests:
1240 if not os.path.exists(test.path):
1242 if not os.path.exists(test.path):
1241 result.addSkip(test, "Doesn't exist")
1243 result.addSkip(test, "Doesn't exist")
1242 continue
1244 continue
1243
1245
1244 if not (self._whitelist and test.name in self._whitelist):
1246 if not (self._whitelist and test.name in self._whitelist):
1245 if self._blacklist and test.name in self._blacklist:
1247 if self._blacklist and test.name in self._blacklist:
1246 result.addSkip(test, 'blacklisted')
1248 result.addSkip(test, 'blacklisted')
1247 continue
1249 continue
1248
1250
1249 if self._retest and not os.path.exists(test.errpath):
1251 if self._retest and not os.path.exists(test.errpath):
1250 result.addIgnore(test, 'not retesting')
1252 result.addIgnore(test, 'not retesting')
1251 continue
1253 continue
1252
1254
1253 if self._keywords:
1255 if self._keywords:
1254 f = open(test.path, 'rb')
1256 f = open(test.path, 'rb')
1255 t = f.read().lower() + test.name.lower()
1257 t = f.read().lower() + test.name.lower()
1256 f.close()
1258 f.close()
1257 ignored = False
1259 ignored = False
1258 for k in self._keywords.lower().split():
1260 for k in self._keywords.lower().split():
1259 if k not in t:
1261 if k not in t:
1260 result.addIgnore(test, "doesn't match keyword")
1262 result.addIgnore(test, "doesn't match keyword")
1261 ignored = True
1263 ignored = True
1262 break
1264 break
1263
1265
1264 if ignored:
1266 if ignored:
1265 continue
1267 continue
1266
1268
1267 tests.append(test)
1269 tests.append(test)
1268
1270
1269 runtests = list(tests)
1271 runtests = list(tests)
1270 done = queue.Queue()
1272 done = queue.Queue()
1271 running = 0
1273 running = 0
1272
1274
1273 def job(test, result):
1275 def job(test, result):
1274 try:
1276 try:
1275 test(result)
1277 test(result)
1276 done.put(None)
1278 done.put(None)
1277 except KeyboardInterrupt:
1279 except KeyboardInterrupt:
1278 pass
1280 pass
1279 except: # re-raises
1281 except: # re-raises
1280 done.put(('!', test, 'run-test raised an error, see traceback'))
1282 done.put(('!', test, 'run-test raised an error, see traceback'))
1281 raise
1283 raise
1282
1284
1283 try:
1285 try:
1284 while tests or running:
1286 while tests or running:
1285 if not done.empty() or running == self._jobs or not tests:
1287 if not done.empty() or running == self._jobs or not tests:
1286 try:
1288 try:
1287 done.get(True, 1)
1289 done.get(True, 1)
1288 if result and result.shouldStop:
1290 if result and result.shouldStop:
1289 break
1291 break
1290 except queue.Empty:
1292 except queue.Empty:
1291 continue
1293 continue
1292 running -= 1
1294 running -= 1
1293 if tests and not running == self._jobs:
1295 if tests and not running == self._jobs:
1294 test = tests.pop(0)
1296 test = tests.pop(0)
1295 if self._loop:
1297 if self._loop:
1296 tests.append(test)
1298 tests.append(test)
1297 t = threading.Thread(target=job, name=test.name,
1299 t = threading.Thread(target=job, name=test.name,
1298 args=(test, result))
1300 args=(test, result))
1299 t.start()
1301 t.start()
1300 running += 1
1302 running += 1
1301 except KeyboardInterrupt:
1303 except KeyboardInterrupt:
1302 for test in runtests:
1304 for test in runtests:
1303 test.abort()
1305 test.abort()
1304
1306
1305 return result
1307 return result
1306
1308
1307 class TextTestRunner(unittest.TextTestRunner):
1309 class TextTestRunner(unittest.TextTestRunner):
1308 """Custom unittest test runner that uses appropriate settings."""
1310 """Custom unittest test runner that uses appropriate settings."""
1309
1311
1310 def __init__(self, runner, *args, **kwargs):
1312 def __init__(self, runner, *args, **kwargs):
1311 super(TextTestRunner, self).__init__(*args, **kwargs)
1313 super(TextTestRunner, self).__init__(*args, **kwargs)
1312
1314
1313 self._runner = runner
1315 self._runner = runner
1314
1316
1315 def run(self, test):
1317 def run(self, test):
1316 result = TestResult(self._runner.options, self.stream,
1318 result = TestResult(self._runner.options, self.stream,
1317 self.descriptions, self.verbosity)
1319 self.descriptions, self.verbosity)
1318
1320
1319 test(result)
1321 test(result)
1320
1322
1321 failed = len(result.failures)
1323 failed = len(result.failures)
1322 warned = len(result.warned)
1324 warned = len(result.warned)
1323 skipped = len(result.skipped)
1325 skipped = len(result.skipped)
1324 ignored = len(result.ignored)
1326 ignored = len(result.ignored)
1325
1327
1326 self.stream.writeln('')
1328 self.stream.writeln('')
1327
1329
1328 if not self._runner.options.noskips:
1330 if not self._runner.options.noskips:
1329 for test, msg in result.skipped:
1331 for test, msg in result.skipped:
1330 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1332 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1331 for test, msg in result.warned:
1333 for test, msg in result.warned:
1332 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1334 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1333 for test, msg in result.failures:
1335 for test, msg in result.failures:
1334 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1336 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1335 for test, msg in result.errors:
1337 for test, msg in result.errors:
1336 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1338 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1337
1339
1338 self._runner._checkhglib('Tested')
1340 self._runner._checkhglib('Tested')
1339
1341
1340 # When '--retest' is enabled, only failure tests run. At this point
1342 # When '--retest' is enabled, only failure tests run. At this point
1341 # "result.testsRun" holds the count of failure test that has run. But
1343 # "result.testsRun" holds the count of failure test that has run. But
1342 # as while printing output, we have subtracted the skipped and ignored
1344 # as while printing output, we have subtracted the skipped and ignored
1343 # count from "result.testsRun". Therefore, to make the count remain
1345 # count from "result.testsRun". Therefore, to make the count remain
1344 # the same, we need to add skipped and ignored count in here.
1346 # the same, we need to add skipped and ignored count in here.
1345 if self._runner.options.retest:
1347 if self._runner.options.retest:
1346 result.testsRun = result.testsRun + skipped + ignored
1348 result.testsRun = result.testsRun + skipped + ignored
1347
1349
1348 # This differs from unittest's default output in that we don't count
1350 # This differs from unittest's default output in that we don't count
1349 # skipped and ignored tests as part of the total test count.
1351 # skipped and ignored tests as part of the total test count.
1350 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1352 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1351 % (result.testsRun - skipped - ignored,
1353 % (result.testsRun - skipped - ignored,
1352 skipped + ignored, warned, failed))
1354 skipped + ignored, warned, failed))
1353 if failed:
1355 if failed:
1354 self.stream.writeln('python hash seed: %s' %
1356 self.stream.writeln('python hash seed: %s' %
1355 os.environ['PYTHONHASHSEED'])
1357 os.environ['PYTHONHASHSEED'])
1356 if self._runner.options.time:
1358 if self._runner.options.time:
1357 self.printtimes(result.times)
1359 self.printtimes(result.times)
1358
1360
1359 return result
1361 return result
1360
1362
1361 def printtimes(self, times):
1363 def printtimes(self, times):
1362 self.stream.writeln('# Producing time report')
1364 self.stream.writeln('# Producing time report')
1363 times.sort(key=lambda t: (t[3]))
1365 times.sort(key=lambda t: (t[3]))
1364 cols = '%7.3f %7.3f %7.3f %s'
1366 cols = '%7.3f %7.3f %7.3f %s'
1365 self.stream.writeln('%-7s %-7s %-7s %s' % ('cuser', 'csys', 'real',
1367 self.stream.writeln('%-7s %-7s %-7s %s' % ('cuser', 'csys', 'real',
1366 'Test'))
1368 'Test'))
1367 for test, cuser, csys, real in times:
1369 for test, cuser, csys, real in times:
1368 self.stream.writeln(cols % (cuser, csys, real, test))
1370 self.stream.writeln(cols % (cuser, csys, real, test))
1369
1371
1370 class TestRunner(object):
1372 class TestRunner(object):
1371 """Holds context for executing tests.
1373 """Holds context for executing tests.
1372
1374
1373 Tests rely on a lot of state. This object holds it for them.
1375 Tests rely on a lot of state. This object holds it for them.
1374 """
1376 """
1375
1377
1376 # Programs required to run tests.
1378 # Programs required to run tests.
1377 REQUIREDTOOLS = [
1379 REQUIREDTOOLS = [
1378 os.path.basename(sys.executable),
1380 os.path.basename(sys.executable),
1379 'diff',
1381 'diff',
1380 'grep',
1382 'grep',
1381 'unzip',
1383 'unzip',
1382 'gunzip',
1384 'gunzip',
1383 'bunzip2',
1385 'bunzip2',
1384 'sed',
1386 'sed',
1385 ]
1387 ]
1386
1388
1387 # Maps file extensions to test class.
1389 # Maps file extensions to test class.
1388 TESTTYPES = [
1390 TESTTYPES = [
1389 ('.py', PythonTest),
1391 ('.py', PythonTest),
1390 ('.t', TTest),
1392 ('.t', TTest),
1391 ]
1393 ]
1392
1394
1393 def __init__(self):
1395 def __init__(self):
1394 self.options = None
1396 self.options = None
1395 self._testdir = None
1397 self._testdir = None
1396 self._hgtmp = None
1398 self._hgtmp = None
1397 self._installdir = None
1399 self._installdir = None
1398 self._bindir = None
1400 self._bindir = None
1399 self._tmpbinddir = None
1401 self._tmpbinddir = None
1400 self._pythondir = None
1402 self._pythondir = None
1401 self._coveragefile = None
1403 self._coveragefile = None
1402 self._createdfiles = []
1404 self._createdfiles = []
1403 self._hgpath = None
1405 self._hgpath = None
1404
1406
1405 def run(self, args, parser=None):
1407 def run(self, args, parser=None):
1406 """Run the test suite."""
1408 """Run the test suite."""
1407 oldmask = os.umask(022)
1409 oldmask = os.umask(022)
1408 try:
1410 try:
1409 parser = parser or getparser()
1411 parser = parser or getparser()
1410 options, args = parseargs(args, parser)
1412 options, args = parseargs(args, parser)
1411 self.options = options
1413 self.options = options
1412
1414
1413 self._checktools()
1415 self._checktools()
1414 tests = self.findtests(args)
1416 tests = self.findtests(args)
1415 return self._run(tests)
1417 return self._run(tests)
1416 finally:
1418 finally:
1417 os.umask(oldmask)
1419 os.umask(oldmask)
1418
1420
1419 def _run(self, tests):
1421 def _run(self, tests):
1420 if self.options.random:
1422 if self.options.random:
1421 random.shuffle(tests)
1423 random.shuffle(tests)
1422 else:
1424 else:
1423 # keywords for slow tests
1425 # keywords for slow tests
1424 slow = 'svn gendoc check-code-hg'.split()
1426 slow = 'svn gendoc check-code-hg'.split()
1425 def sortkey(f):
1427 def sortkey(f):
1426 # run largest tests first, as they tend to take the longest
1428 # run largest tests first, as they tend to take the longest
1427 try:
1429 try:
1428 val = -os.stat(f).st_size
1430 val = -os.stat(f).st_size
1429 except OSError, e:
1431 except OSError, e:
1430 if e.errno != errno.ENOENT:
1432 if e.errno != errno.ENOENT:
1431 raise
1433 raise
1432 return -1e9 # file does not exist, tell early
1434 return -1e9 # file does not exist, tell early
1433 for kw in slow:
1435 for kw in slow:
1434 if kw in f:
1436 if kw in f:
1435 val *= 10
1437 val *= 10
1436 return val
1438 return val
1437 tests.sort(key=sortkey)
1439 tests.sort(key=sortkey)
1438
1440
1439 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1441 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1440
1442
1441 if 'PYTHONHASHSEED' not in os.environ:
1443 if 'PYTHONHASHSEED' not in os.environ:
1442 # use a random python hash seed all the time
1444 # use a random python hash seed all the time
1443 # we do the randomness ourself to know what seed is used
1445 # we do the randomness ourself to know what seed is used
1444 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1446 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1445
1447
1446 if self.options.tmpdir:
1448 if self.options.tmpdir:
1447 self.options.keep_tmpdir = True
1449 self.options.keep_tmpdir = True
1448 tmpdir = self.options.tmpdir
1450 tmpdir = self.options.tmpdir
1449 if os.path.exists(tmpdir):
1451 if os.path.exists(tmpdir):
1450 # Meaning of tmpdir has changed since 1.3: we used to create
1452 # Meaning of tmpdir has changed since 1.3: we used to create
1451 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1453 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1452 # tmpdir already exists.
1454 # tmpdir already exists.
1453 print "error: temp dir %r already exists" % tmpdir
1455 print "error: temp dir %r already exists" % tmpdir
1454 return 1
1456 return 1
1455
1457
1456 # Automatically removing tmpdir sounds convenient, but could
1458 # Automatically removing tmpdir sounds convenient, but could
1457 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1459 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1458 # or "--tmpdir=$HOME".
1460 # or "--tmpdir=$HOME".
1459 #vlog("# Removing temp dir", tmpdir)
1461 #vlog("# Removing temp dir", tmpdir)
1460 #shutil.rmtree(tmpdir)
1462 #shutil.rmtree(tmpdir)
1461 os.makedirs(tmpdir)
1463 os.makedirs(tmpdir)
1462 else:
1464 else:
1463 d = None
1465 d = None
1464 if os.name == 'nt':
1466 if os.name == 'nt':
1465 # without this, we get the default temp dir location, but
1467 # without this, we get the default temp dir location, but
1466 # in all lowercase, which causes troubles with paths (issue3490)
1468 # in all lowercase, which causes troubles with paths (issue3490)
1467 d = os.getenv('TMP')
1469 d = os.getenv('TMP')
1468 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1470 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1469 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1471 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1470
1472
1471 if self.options.with_hg:
1473 if self.options.with_hg:
1472 self._installdir = None
1474 self._installdir = None
1473 self._bindir = os.path.dirname(os.path.realpath(
1475 self._bindir = os.path.dirname(os.path.realpath(
1474 self.options.with_hg))
1476 self.options.with_hg))
1475 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1477 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1476 os.makedirs(self._tmpbindir)
1478 os.makedirs(self._tmpbindir)
1477
1479
1478 # This looks redundant with how Python initializes sys.path from
1480 # This looks redundant with how Python initializes sys.path from
1479 # the location of the script being executed. Needed because the
1481 # the location of the script being executed. Needed because the
1480 # "hg" specified by --with-hg is not the only Python script
1482 # "hg" specified by --with-hg is not the only Python script
1481 # executed in the test suite that needs to import 'mercurial'
1483 # executed in the test suite that needs to import 'mercurial'
1482 # ... which means it's not really redundant at all.
1484 # ... which means it's not really redundant at all.
1483 self._pythondir = self._bindir
1485 self._pythondir = self._bindir
1484 else:
1486 else:
1485 self._installdir = os.path.join(self._hgtmp, "install")
1487 self._installdir = os.path.join(self._hgtmp, "install")
1486 self._bindir = os.environ["BINDIR"] = \
1488 self._bindir = os.environ["BINDIR"] = \
1487 os.path.join(self._installdir, "bin")
1489 os.path.join(self._installdir, "bin")
1488 self._tmpbindir = self._bindir
1490 self._tmpbindir = self._bindir
1489 self._pythondir = os.path.join(self._installdir, "lib", "python")
1491 self._pythondir = os.path.join(self._installdir, "lib", "python")
1490
1492
1491 os.environ["BINDIR"] = self._bindir
1493 os.environ["BINDIR"] = self._bindir
1492 os.environ["PYTHON"] = PYTHON
1494 os.environ["PYTHON"] = PYTHON
1493
1495
1494 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1496 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1495 if self._tmpbindir != self._bindir:
1497 if self._tmpbindir != self._bindir:
1496 path = [self._tmpbindir] + path
1498 path = [self._tmpbindir] + path
1497 os.environ["PATH"] = os.pathsep.join(path)
1499 os.environ["PATH"] = os.pathsep.join(path)
1498
1500
1499 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1501 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1500 # can run .../tests/run-tests.py test-foo where test-foo
1502 # can run .../tests/run-tests.py test-foo where test-foo
1501 # adds an extension to HGRC. Also include run-test.py directory to
1503 # adds an extension to HGRC. Also include run-test.py directory to
1502 # import modules like heredoctest.
1504 # import modules like heredoctest.
1503 pypath = [self._pythondir, self._testdir,
1505 pypath = [self._pythondir, self._testdir,
1504 os.path.abspath(os.path.dirname(__file__))]
1506 os.path.abspath(os.path.dirname(__file__))]
1505 # We have to augment PYTHONPATH, rather than simply replacing
1507 # We have to augment PYTHONPATH, rather than simply replacing
1506 # it, in case external libraries are only available via current
1508 # it, in case external libraries are only available via current
1507 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1509 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1508 # are in /opt/subversion.)
1510 # are in /opt/subversion.)
1509 oldpypath = os.environ.get(IMPL_PATH)
1511 oldpypath = os.environ.get(IMPL_PATH)
1510 if oldpypath:
1512 if oldpypath:
1511 pypath.append(oldpypath)
1513 pypath.append(oldpypath)
1512 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1514 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1513
1515
1514 self._coveragefile = os.path.join(self._testdir, '.coverage')
1516 self._coveragefile = os.path.join(self._testdir, '.coverage')
1515
1517
1516 vlog("# Using TESTDIR", self._testdir)
1518 vlog("# Using TESTDIR", self._testdir)
1517 vlog("# Using HGTMP", self._hgtmp)
1519 vlog("# Using HGTMP", self._hgtmp)
1518 vlog("# Using PATH", os.environ["PATH"])
1520 vlog("# Using PATH", os.environ["PATH"])
1519 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1521 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1520
1522
1521 try:
1523 try:
1522 return self._runtests(tests) or 0
1524 return self._runtests(tests) or 0
1523 finally:
1525 finally:
1524 time.sleep(.1)
1526 time.sleep(.1)
1525 self._cleanup()
1527 self._cleanup()
1526
1528
1527 def findtests(self, args):
1529 def findtests(self, args):
1528 """Finds possible test files from arguments.
1530 """Finds possible test files from arguments.
1529
1531
1530 If you wish to inject custom tests into the test harness, this would
1532 If you wish to inject custom tests into the test harness, this would
1531 be a good function to monkeypatch or override in a derived class.
1533 be a good function to monkeypatch or override in a derived class.
1532 """
1534 """
1533 if not args:
1535 if not args:
1534 if self.options.changed:
1536 if self.options.changed:
1535 proc = Popen4('hg st --rev "%s" -man0 .' %
1537 proc = Popen4('hg st --rev "%s" -man0 .' %
1536 self.options.changed, None, 0)
1538 self.options.changed, None, 0)
1537 stdout, stderr = proc.communicate()
1539 stdout, stderr = proc.communicate()
1538 args = stdout.strip('\0').split('\0')
1540 args = stdout.strip('\0').split('\0')
1539 else:
1541 else:
1540 args = os.listdir('.')
1542 args = os.listdir('.')
1541
1543
1542 return [t for t in args
1544 return [t for t in args
1543 if os.path.basename(t).startswith('test-')
1545 if os.path.basename(t).startswith('test-')
1544 and (t.endswith('.py') or t.endswith('.t'))]
1546 and (t.endswith('.py') or t.endswith('.t'))]
1545
1547
1546 def _runtests(self, tests):
1548 def _runtests(self, tests):
1547 try:
1549 try:
1548 if self._installdir:
1550 if self._installdir:
1549 self._installhg()
1551 self._installhg()
1550 self._checkhglib("Testing")
1552 self._checkhglib("Testing")
1551 else:
1553 else:
1552 self._usecorrectpython()
1554 self._usecorrectpython()
1553
1555
1554 if self.options.restart:
1556 if self.options.restart:
1555 orig = list(tests)
1557 orig = list(tests)
1556 while tests:
1558 while tests:
1557 if os.path.exists(tests[0] + ".err"):
1559 if os.path.exists(tests[0] + ".err"):
1558 break
1560 break
1559 tests.pop(0)
1561 tests.pop(0)
1560 if not tests:
1562 if not tests:
1561 print "running all tests"
1563 print "running all tests"
1562 tests = orig
1564 tests = orig
1563
1565
1564 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1566 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1565
1567
1566 failed = False
1568 failed = False
1567 warned = False
1569 warned = False
1568
1570
1569 suite = TestSuite(self._testdir,
1571 suite = TestSuite(self._testdir,
1570 jobs=self.options.jobs,
1572 jobs=self.options.jobs,
1571 whitelist=self.options.whitelisted,
1573 whitelist=self.options.whitelisted,
1572 blacklist=self.options.blacklist,
1574 blacklist=self.options.blacklist,
1573 retest=self.options.retest,
1575 retest=self.options.retest,
1574 keywords=self.options.keywords,
1576 keywords=self.options.keywords,
1575 loop=self.options.loop,
1577 loop=self.options.loop,
1576 tests=tests)
1578 tests=tests)
1577 verbosity = 1
1579 verbosity = 1
1578 if self.options.verbose:
1580 if self.options.verbose:
1579 verbosity = 2
1581 verbosity = 2
1580 runner = TextTestRunner(self, verbosity=verbosity)
1582 runner = TextTestRunner(self, verbosity=verbosity)
1581 result = runner.run(suite)
1583 result = runner.run(suite)
1582
1584
1583 if result.failures:
1585 if result.failures:
1584 failed = True
1586 failed = True
1585 if result.warned:
1587 if result.warned:
1586 warned = True
1588 warned = True
1587
1589
1588 if self.options.anycoverage:
1590 if self.options.anycoverage:
1589 self._outputcoverage()
1591 self._outputcoverage()
1590 except KeyboardInterrupt:
1592 except KeyboardInterrupt:
1591 failed = True
1593 failed = True
1592 print "\ninterrupted!"
1594 print "\ninterrupted!"
1593
1595
1594 if failed:
1596 if failed:
1595 return 1
1597 return 1
1596 if warned:
1598 if warned:
1597 return 80
1599 return 80
1598
1600
1599 def _gettest(self, test, count):
1601 def _gettest(self, test, count):
1600 """Obtain a Test by looking at its filename.
1602 """Obtain a Test by looking at its filename.
1601
1603
1602 Returns a Test instance. The Test may not be runnable if it doesn't
1604 Returns a Test instance. The Test may not be runnable if it doesn't
1603 map to a known type.
1605 map to a known type.
1604 """
1606 """
1605 lctest = test.lower()
1607 lctest = test.lower()
1606 testcls = Test
1608 testcls = Test
1607
1609
1608 for ext, cls in self.TESTTYPES:
1610 for ext, cls in self.TESTTYPES:
1609 if lctest.endswith(ext):
1611 if lctest.endswith(ext):
1610 testcls = cls
1612 testcls = cls
1611 break
1613 break
1612
1614
1613 refpath = os.path.join(self._testdir, test)
1615 refpath = os.path.join(self._testdir, test)
1614 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1616 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1615
1617
1616 return testcls(refpath, tmpdir,
1618 return testcls(refpath, tmpdir,
1617 keeptmpdir=self.options.keep_tmpdir,
1619 keeptmpdir=self.options.keep_tmpdir,
1618 debug=self.options.debug,
1620 debug=self.options.debug,
1619 timeout=self.options.timeout,
1621 timeout=self.options.timeout,
1620 startport=self.options.port + count * 3,
1622 startport=self.options.port + count * 3,
1621 extraconfigopts=self.options.extra_config_opt,
1623 extraconfigopts=self.options.extra_config_opt,
1622 py3kwarnings=self.options.py3k_warnings,
1624 py3kwarnings=self.options.py3k_warnings,
1623 shell=self.options.shell)
1625 shell=self.options.shell)
1624
1626
1625 def _cleanup(self):
1627 def _cleanup(self):
1626 """Clean up state from this test invocation."""
1628 """Clean up state from this test invocation."""
1627
1629
1628 if self.options.keep_tmpdir:
1630 if self.options.keep_tmpdir:
1629 return
1631 return
1630
1632
1631 vlog("# Cleaning up HGTMP", self._hgtmp)
1633 vlog("# Cleaning up HGTMP", self._hgtmp)
1632 shutil.rmtree(self._hgtmp, True)
1634 shutil.rmtree(self._hgtmp, True)
1633 for f in self._createdfiles:
1635 for f in self._createdfiles:
1634 try:
1636 try:
1635 os.remove(f)
1637 os.remove(f)
1636 except OSError:
1638 except OSError:
1637 pass
1639 pass
1638
1640
1639 def _usecorrectpython(self):
1641 def _usecorrectpython(self):
1640 """Configure the environment to use the appropriate Python in tests."""
1642 """Configure the environment to use the appropriate Python in tests."""
1641 # Tests must use the same interpreter as us or bad things will happen.
1643 # Tests must use the same interpreter as us or bad things will happen.
1642 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1644 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1643 if getattr(os, 'symlink', None):
1645 if getattr(os, 'symlink', None):
1644 vlog("# Making python executable in test path a symlink to '%s'" %
1646 vlog("# Making python executable in test path a symlink to '%s'" %
1645 sys.executable)
1647 sys.executable)
1646 mypython = os.path.join(self._tmpbindir, pyexename)
1648 mypython = os.path.join(self._tmpbindir, pyexename)
1647 try:
1649 try:
1648 if os.readlink(mypython) == sys.executable:
1650 if os.readlink(mypython) == sys.executable:
1649 return
1651 return
1650 os.unlink(mypython)
1652 os.unlink(mypython)
1651 except OSError, err:
1653 except OSError, err:
1652 if err.errno != errno.ENOENT:
1654 if err.errno != errno.ENOENT:
1653 raise
1655 raise
1654 if self._findprogram(pyexename) != sys.executable:
1656 if self._findprogram(pyexename) != sys.executable:
1655 try:
1657 try:
1656 os.symlink(sys.executable, mypython)
1658 os.symlink(sys.executable, mypython)
1657 self._createdfiles.append(mypython)
1659 self._createdfiles.append(mypython)
1658 except OSError, err:
1660 except OSError, err:
1659 # child processes may race, which is harmless
1661 # child processes may race, which is harmless
1660 if err.errno != errno.EEXIST:
1662 if err.errno != errno.EEXIST:
1661 raise
1663 raise
1662 else:
1664 else:
1663 exedir, exename = os.path.split(sys.executable)
1665 exedir, exename = os.path.split(sys.executable)
1664 vlog("# Modifying search path to find %s as %s in '%s'" %
1666 vlog("# Modifying search path to find %s as %s in '%s'" %
1665 (exename, pyexename, exedir))
1667 (exename, pyexename, exedir))
1666 path = os.environ['PATH'].split(os.pathsep)
1668 path = os.environ['PATH'].split(os.pathsep)
1667 while exedir in path:
1669 while exedir in path:
1668 path.remove(exedir)
1670 path.remove(exedir)
1669 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1671 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1670 if not self._findprogram(pyexename):
1672 if not self._findprogram(pyexename):
1671 print "WARNING: Cannot find %s in search path" % pyexename
1673 print "WARNING: Cannot find %s in search path" % pyexename
1672
1674
1673 def _installhg(self):
1675 def _installhg(self):
1674 """Install hg into the test environment.
1676 """Install hg into the test environment.
1675
1677
1676 This will also configure hg with the appropriate testing settings.
1678 This will also configure hg with the appropriate testing settings.
1677 """
1679 """
1678 vlog("# Performing temporary installation of HG")
1680 vlog("# Performing temporary installation of HG")
1679 installerrs = os.path.join("tests", "install.err")
1681 installerrs = os.path.join("tests", "install.err")
1680 compiler = ''
1682 compiler = ''
1681 if self.options.compiler:
1683 if self.options.compiler:
1682 compiler = '--compiler ' + self.options.compiler
1684 compiler = '--compiler ' + self.options.compiler
1683 pure = self.options.pure and "--pure" or ""
1685 pure = self.options.pure and "--pure" or ""
1684 py3 = ''
1686 py3 = ''
1685 if sys.version_info[0] == 3:
1687 if sys.version_info[0] == 3:
1686 py3 = '--c2to3'
1688 py3 = '--c2to3'
1687
1689
1688 # Run installer in hg root
1690 # Run installer in hg root
1689 script = os.path.realpath(sys.argv[0])
1691 script = os.path.realpath(sys.argv[0])
1690 hgroot = os.path.dirname(os.path.dirname(script))
1692 hgroot = os.path.dirname(os.path.dirname(script))
1691 os.chdir(hgroot)
1693 os.chdir(hgroot)
1692 nohome = '--home=""'
1694 nohome = '--home=""'
1693 if os.name == 'nt':
1695 if os.name == 'nt':
1694 # The --home="" trick works only on OS where os.sep == '/'
1696 # The --home="" trick works only on OS where os.sep == '/'
1695 # because of a distutils convert_path() fast-path. Avoid it at
1697 # because of a distutils convert_path() fast-path. Avoid it at
1696 # least on Windows for now, deal with .pydistutils.cfg bugs
1698 # least on Windows for now, deal with .pydistutils.cfg bugs
1697 # when they happen.
1699 # when they happen.
1698 nohome = ''
1700 nohome = ''
1699 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1701 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1700 ' build %(compiler)s --build-base="%(base)s"'
1702 ' build %(compiler)s --build-base="%(base)s"'
1701 ' install --force --prefix="%(prefix)s"'
1703 ' install --force --prefix="%(prefix)s"'
1702 ' --install-lib="%(libdir)s"'
1704 ' --install-lib="%(libdir)s"'
1703 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1705 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1704 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1706 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1705 'compiler': compiler,
1707 'compiler': compiler,
1706 'base': os.path.join(self._hgtmp, "build"),
1708 'base': os.path.join(self._hgtmp, "build"),
1707 'prefix': self._installdir, 'libdir': self._pythondir,
1709 'prefix': self._installdir, 'libdir': self._pythondir,
1708 'bindir': self._bindir,
1710 'bindir': self._bindir,
1709 'nohome': nohome, 'logfile': installerrs})
1711 'nohome': nohome, 'logfile': installerrs})
1710 vlog("# Running", cmd)
1712 vlog("# Running", cmd)
1711 if os.system(cmd) == 0:
1713 if os.system(cmd) == 0:
1712 if not self.options.verbose:
1714 if not self.options.verbose:
1713 os.remove(installerrs)
1715 os.remove(installerrs)
1714 else:
1716 else:
1715 f = open(installerrs, 'rb')
1717 f = open(installerrs, 'rb')
1716 for line in f:
1718 for line in f:
1717 print line,
1719 print line,
1718 f.close()
1720 f.close()
1719 sys.exit(1)
1721 sys.exit(1)
1720 os.chdir(self._testdir)
1722 os.chdir(self._testdir)
1721
1723
1722 self._usecorrectpython()
1724 self._usecorrectpython()
1723
1725
1724 if self.options.py3k_warnings and not self.options.anycoverage:
1726 if self.options.py3k_warnings and not self.options.anycoverage:
1725 vlog("# Updating hg command to enable Py3k Warnings switch")
1727 vlog("# Updating hg command to enable Py3k Warnings switch")
1726 f = open(os.path.join(self._bindir, 'hg'), 'rb')
1728 f = open(os.path.join(self._bindir, 'hg'), 'rb')
1727 lines = [line.rstrip() for line in f]
1729 lines = [line.rstrip() for line in f]
1728 lines[0] += ' -3'
1730 lines[0] += ' -3'
1729 f.close()
1731 f.close()
1730 f = open(os.path.join(self._bindir, 'hg'), 'wb')
1732 f = open(os.path.join(self._bindir, 'hg'), 'wb')
1731 for line in lines:
1733 for line in lines:
1732 f.write(line + '\n')
1734 f.write(line + '\n')
1733 f.close()
1735 f.close()
1734
1736
1735 hgbat = os.path.join(self._bindir, 'hg.bat')
1737 hgbat = os.path.join(self._bindir, 'hg.bat')
1736 if os.path.isfile(hgbat):
1738 if os.path.isfile(hgbat):
1737 # hg.bat expects to be put in bin/scripts while run-tests.py
1739 # hg.bat expects to be put in bin/scripts while run-tests.py
1738 # installation layout put it in bin/ directly. Fix it
1740 # installation layout put it in bin/ directly. Fix it
1739 f = open(hgbat, 'rb')
1741 f = open(hgbat, 'rb')
1740 data = f.read()
1742 data = f.read()
1741 f.close()
1743 f.close()
1742 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1744 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1743 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1745 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1744 '"%~dp0python" "%~dp0hg" %*')
1746 '"%~dp0python" "%~dp0hg" %*')
1745 f = open(hgbat, 'wb')
1747 f = open(hgbat, 'wb')
1746 f.write(data)
1748 f.write(data)
1747 f.close()
1749 f.close()
1748 else:
1750 else:
1749 print 'WARNING: cannot fix hg.bat reference to python.exe'
1751 print 'WARNING: cannot fix hg.bat reference to python.exe'
1750
1752
1751 if self.options.anycoverage:
1753 if self.options.anycoverage:
1752 custom = os.path.join(self._testdir, 'sitecustomize.py')
1754 custom = os.path.join(self._testdir, 'sitecustomize.py')
1753 target = os.path.join(self._pythondir, 'sitecustomize.py')
1755 target = os.path.join(self._pythondir, 'sitecustomize.py')
1754 vlog('# Installing coverage trigger to %s' % target)
1756 vlog('# Installing coverage trigger to %s' % target)
1755 shutil.copyfile(custom, target)
1757 shutil.copyfile(custom, target)
1756 rc = os.path.join(self._testdir, '.coveragerc')
1758 rc = os.path.join(self._testdir, '.coveragerc')
1757 vlog('# Installing coverage rc to %s' % rc)
1759 vlog('# Installing coverage rc to %s' % rc)
1758 os.environ['COVERAGE_PROCESS_START'] = rc
1760 os.environ['COVERAGE_PROCESS_START'] = rc
1759 fn = os.path.join(self._installdir, '..', '.coverage')
1761 fn = os.path.join(self._installdir, '..', '.coverage')
1760 os.environ['COVERAGE_FILE'] = fn
1762 os.environ['COVERAGE_FILE'] = fn
1761
1763
1762 def _checkhglib(self, verb):
1764 def _checkhglib(self, verb):
1763 """Ensure that the 'mercurial' package imported by python is
1765 """Ensure that the 'mercurial' package imported by python is
1764 the one we expect it to be. If not, print a warning to stderr."""
1766 the one we expect it to be. If not, print a warning to stderr."""
1765 if ((self._bindir == self._pythondir) and
1767 if ((self._bindir == self._pythondir) and
1766 (self._bindir != self._tmpbindir)):
1768 (self._bindir != self._tmpbindir)):
1767 # The pythondir has been infered from --with-hg flag.
1769 # The pythondir has been infered from --with-hg flag.
1768 # We cannot expect anything sensible here
1770 # We cannot expect anything sensible here
1769 return
1771 return
1770 expecthg = os.path.join(self._pythondir, 'mercurial')
1772 expecthg = os.path.join(self._pythondir, 'mercurial')
1771 actualhg = self._gethgpath()
1773 actualhg = self._gethgpath()
1772 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1774 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1773 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1775 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1774 ' (expected %s)\n'
1776 ' (expected %s)\n'
1775 % (verb, actualhg, expecthg))
1777 % (verb, actualhg, expecthg))
1776 def _gethgpath(self):
1778 def _gethgpath(self):
1777 """Return the path to the mercurial package that is actually found by
1779 """Return the path to the mercurial package that is actually found by
1778 the current Python interpreter."""
1780 the current Python interpreter."""
1779 if self._hgpath is not None:
1781 if self._hgpath is not None:
1780 return self._hgpath
1782 return self._hgpath
1781
1783
1782 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1784 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1783 pipe = os.popen(cmd % PYTHON)
1785 pipe = os.popen(cmd % PYTHON)
1784 try:
1786 try:
1785 self._hgpath = pipe.read().strip()
1787 self._hgpath = pipe.read().strip()
1786 finally:
1788 finally:
1787 pipe.close()
1789 pipe.close()
1788
1790
1789 return self._hgpath
1791 return self._hgpath
1790
1792
1791 def _outputcoverage(self):
1793 def _outputcoverage(self):
1792 """Produce code coverage output."""
1794 """Produce code coverage output."""
1793 vlog('# Producing coverage report')
1795 vlog('# Producing coverage report')
1794 os.chdir(self._pythondir)
1796 os.chdir(self._pythondir)
1795
1797
1796 def covrun(*args):
1798 def covrun(*args):
1797 cmd = 'coverage %s' % ' '.join(args)
1799 cmd = 'coverage %s' % ' '.join(args)
1798 vlog('# Running: %s' % cmd)
1800 vlog('# Running: %s' % cmd)
1799 os.system(cmd)
1801 os.system(cmd)
1800
1802
1801 covrun('-c')
1803 covrun('-c')
1802 omit = ','.join(os.path.join(x, '*') for x in
1804 omit = ','.join(os.path.join(x, '*') for x in
1803 [self._bindir, self._testdir])
1805 [self._bindir, self._testdir])
1804 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1806 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1805 if self.options.htmlcov:
1807 if self.options.htmlcov:
1806 htmldir = os.path.join(self._testdir, 'htmlcov')
1808 htmldir = os.path.join(self._testdir, 'htmlcov')
1807 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1809 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1808 '"--omit=%s"' % omit)
1810 '"--omit=%s"' % omit)
1809 if self.options.annotate:
1811 if self.options.annotate:
1810 adir = os.path.join(self._testdir, 'annotated')
1812 adir = os.path.join(self._testdir, 'annotated')
1811 if not os.path.isdir(adir):
1813 if not os.path.isdir(adir):
1812 os.mkdir(adir)
1814 os.mkdir(adir)
1813 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1815 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1814
1816
1815 def _findprogram(self, program):
1817 def _findprogram(self, program):
1816 """Search PATH for a executable program"""
1818 """Search PATH for a executable program"""
1817 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1819 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1818 name = os.path.join(p, program)
1820 name = os.path.join(p, program)
1819 if os.name == 'nt' or os.access(name, os.X_OK):
1821 if os.name == 'nt' or os.access(name, os.X_OK):
1820 return name
1822 return name
1821 return None
1823 return None
1822
1824
1823 def _checktools(self):
1825 def _checktools(self):
1824 """Ensure tools required to run tests are present."""
1826 """Ensure tools required to run tests are present."""
1825 for p in self.REQUIREDTOOLS:
1827 for p in self.REQUIREDTOOLS:
1826 if os.name == 'nt' and not p.endswith('.exe'):
1828 if os.name == 'nt' and not p.endswith('.exe'):
1827 p += '.exe'
1829 p += '.exe'
1828 found = self._findprogram(p)
1830 found = self._findprogram(p)
1829 if found:
1831 if found:
1830 vlog("# Found prerequisite", p, "at", found)
1832 vlog("# Found prerequisite", p, "at", found)
1831 else:
1833 else:
1832 print "WARNING: Did not find prerequisite tool: %s " % p
1834 print "WARNING: Did not find prerequisite tool: %s " % p
1833
1835
1834 if __name__ == '__main__':
1836 if __name__ == '__main__':
1835 runner = TestRunner()
1837 runner = TestRunner()
1836 sys.exit(runner.run(sys.argv[1:]))
1838 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now