##// END OF EJS Templates
run-tests: skipped test should not produce 'i' mark while retesting...
anuraggoel -
r21739:11e5ad03 default
parent child Browse files
Show More
@@ -1,1805 +1,1806 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # run-tests.py - Run a set of tests on Mercurial
3 # run-tests.py - Run a set of tests on Mercurial
4 #
4 #
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 # Modifying this script is tricky because it has many modes:
10 # Modifying this script is tricky because it has many modes:
11 # - serial (default) vs parallel (-jN, N > 1)
11 # - serial (default) vs parallel (-jN, N > 1)
12 # - no coverage (default) vs coverage (-c, -C, -s)
12 # - no coverage (default) vs coverage (-c, -C, -s)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 # - tests are a mix of shell scripts and Python scripts
14 # - tests are a mix of shell scripts and Python scripts
15 #
15 #
16 # If you change this script, it is recommended that you ensure you
16 # If you change this script, it is recommended that you ensure you
17 # haven't broken it by running it in various modes with a representative
17 # haven't broken it by running it in various modes with a representative
18 # sample of test scripts. For example:
18 # sample of test scripts. For example:
19 #
19 #
20 # 1) serial, no coverage, temp install:
20 # 1) serial, no coverage, temp install:
21 # ./run-tests.py test-s*
21 # ./run-tests.py test-s*
22 # 2) serial, no coverage, local hg:
22 # 2) serial, no coverage, local hg:
23 # ./run-tests.py --local test-s*
23 # ./run-tests.py --local test-s*
24 # 3) serial, coverage, temp install:
24 # 3) serial, coverage, temp install:
25 # ./run-tests.py -c test-s*
25 # ./run-tests.py -c test-s*
26 # 4) serial, coverage, local hg:
26 # 4) serial, coverage, local hg:
27 # ./run-tests.py -c --local test-s* # unsupported
27 # ./run-tests.py -c --local test-s* # unsupported
28 # 5) parallel, no coverage, temp install:
28 # 5) parallel, no coverage, temp install:
29 # ./run-tests.py -j2 test-s*
29 # ./run-tests.py -j2 test-s*
30 # 6) parallel, no coverage, local hg:
30 # 6) parallel, no coverage, local hg:
31 # ./run-tests.py -j2 --local test-s*
31 # ./run-tests.py -j2 --local test-s*
32 # 7) parallel, coverage, temp install:
32 # 7) parallel, coverage, temp install:
33 # ./run-tests.py -j2 -c test-s* # currently broken
33 # ./run-tests.py -j2 -c test-s* # currently broken
34 # 8) parallel, coverage, local install:
34 # 8) parallel, coverage, local install:
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 # 9) parallel, custom tmp dir:
36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 #
38 #
39 # (You could use any subset of the tests: test-s* happens to match
39 # (You could use any subset of the tests: test-s* happens to match
40 # enough that it's worth doing parallel runs, few enough that it
40 # enough that it's worth doing parallel runs, few enough that it
41 # completes fairly quickly, includes both shell and Python scripts, and
41 # completes fairly quickly, includes both shell and Python scripts, and
42 # includes some scripts that run daemon processes.)
42 # includes some scripts that run daemon processes.)
43
43
44 from distutils import version
44 from distutils import version
45 import difflib
45 import difflib
46 import errno
46 import errno
47 import optparse
47 import optparse
48 import os
48 import os
49 import shutil
49 import shutil
50 import subprocess
50 import subprocess
51 import signal
51 import signal
52 import sys
52 import sys
53 import tempfile
53 import tempfile
54 import time
54 import time
55 import random
55 import random
56 import re
56 import re
57 import threading
57 import threading
58 import killdaemons as killmod
58 import killdaemons as killmod
59 import Queue as queue
59 import Queue as queue
60 import unittest
60 import unittest
61
61
62 processlock = threading.Lock()
62 processlock = threading.Lock()
63
63
64 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
64 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
65 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
65 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
66 # zombies but it's pretty harmless even if we do.
66 # zombies but it's pretty harmless even if we do.
67 if sys.version_info < (2, 5):
67 if sys.version_info < (2, 5):
68 subprocess._cleanup = lambda: None
68 subprocess._cleanup = lambda: None
69
69
70 closefds = os.name == 'posix'
70 closefds = os.name == 'posix'
71 def Popen4(cmd, wd, timeout, env=None):
71 def Popen4(cmd, wd, timeout, env=None):
72 processlock.acquire()
72 processlock.acquire()
73 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
73 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
74 close_fds=closefds,
74 close_fds=closefds,
75 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
75 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
76 stderr=subprocess.STDOUT)
76 stderr=subprocess.STDOUT)
77 processlock.release()
77 processlock.release()
78
78
79 p.fromchild = p.stdout
79 p.fromchild = p.stdout
80 p.tochild = p.stdin
80 p.tochild = p.stdin
81 p.childerr = p.stderr
81 p.childerr = p.stderr
82
82
83 p.timeout = False
83 p.timeout = False
84 if timeout:
84 if timeout:
85 def t():
85 def t():
86 start = time.time()
86 start = time.time()
87 while time.time() - start < timeout and p.returncode is None:
87 while time.time() - start < timeout and p.returncode is None:
88 time.sleep(.1)
88 time.sleep(.1)
89 p.timeout = True
89 p.timeout = True
90 if p.returncode is None:
90 if p.returncode is None:
91 terminate(p)
91 terminate(p)
92 threading.Thread(target=t).start()
92 threading.Thread(target=t).start()
93
93
94 return p
94 return p
95
95
96 PYTHON = sys.executable.replace('\\', '/')
96 PYTHON = sys.executable.replace('\\', '/')
97 IMPL_PATH = 'PYTHONPATH'
97 IMPL_PATH = 'PYTHONPATH'
98 if 'java' in sys.platform:
98 if 'java' in sys.platform:
99 IMPL_PATH = 'JYTHONPATH'
99 IMPL_PATH = 'JYTHONPATH'
100
100
101 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
101 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
102
102
103 defaults = {
103 defaults = {
104 'jobs': ('HGTEST_JOBS', 1),
104 'jobs': ('HGTEST_JOBS', 1),
105 'timeout': ('HGTEST_TIMEOUT', 180),
105 'timeout': ('HGTEST_TIMEOUT', 180),
106 'port': ('HGTEST_PORT', 20059),
106 'port': ('HGTEST_PORT', 20059),
107 'shell': ('HGTEST_SHELL', 'sh'),
107 'shell': ('HGTEST_SHELL', 'sh'),
108 }
108 }
109
109
110 def parselistfiles(files, listtype, warn=True):
110 def parselistfiles(files, listtype, warn=True):
111 entries = dict()
111 entries = dict()
112 for filename in files:
112 for filename in files:
113 try:
113 try:
114 path = os.path.expanduser(os.path.expandvars(filename))
114 path = os.path.expanduser(os.path.expandvars(filename))
115 f = open(path, "r")
115 f = open(path, "r")
116 except IOError, err:
116 except IOError, err:
117 if err.errno != errno.ENOENT:
117 if err.errno != errno.ENOENT:
118 raise
118 raise
119 if warn:
119 if warn:
120 print "warning: no such %s file: %s" % (listtype, filename)
120 print "warning: no such %s file: %s" % (listtype, filename)
121 continue
121 continue
122
122
123 for line in f.readlines():
123 for line in f.readlines():
124 line = line.split('#', 1)[0].strip()
124 line = line.split('#', 1)[0].strip()
125 if line:
125 if line:
126 entries[line] = filename
126 entries[line] = filename
127
127
128 f.close()
128 f.close()
129 return entries
129 return entries
130
130
131 def getparser():
131 def getparser():
132 """Obtain the OptionParser used by the CLI."""
132 """Obtain the OptionParser used by the CLI."""
133 parser = optparse.OptionParser("%prog [options] [tests]")
133 parser = optparse.OptionParser("%prog [options] [tests]")
134
134
135 # keep these sorted
135 # keep these sorted
136 parser.add_option("--blacklist", action="append",
136 parser.add_option("--blacklist", action="append",
137 help="skip tests listed in the specified blacklist file")
137 help="skip tests listed in the specified blacklist file")
138 parser.add_option("--whitelist", action="append",
138 parser.add_option("--whitelist", action="append",
139 help="always run tests listed in the specified whitelist file")
139 help="always run tests listed in the specified whitelist file")
140 parser.add_option("--changed", type="string",
140 parser.add_option("--changed", type="string",
141 help="run tests that are changed in parent rev or working directory")
141 help="run tests that are changed in parent rev or working directory")
142 parser.add_option("-C", "--annotate", action="store_true",
142 parser.add_option("-C", "--annotate", action="store_true",
143 help="output files annotated with coverage")
143 help="output files annotated with coverage")
144 parser.add_option("-c", "--cover", action="store_true",
144 parser.add_option("-c", "--cover", action="store_true",
145 help="print a test coverage report")
145 help="print a test coverage report")
146 parser.add_option("-d", "--debug", action="store_true",
146 parser.add_option("-d", "--debug", action="store_true",
147 help="debug mode: write output of test scripts to console"
147 help="debug mode: write output of test scripts to console"
148 " rather than capturing and diffing it (disables timeout)")
148 " rather than capturing and diffing it (disables timeout)")
149 parser.add_option("-f", "--first", action="store_true",
149 parser.add_option("-f", "--first", action="store_true",
150 help="exit on the first test failure")
150 help="exit on the first test failure")
151 parser.add_option("-H", "--htmlcov", action="store_true",
151 parser.add_option("-H", "--htmlcov", action="store_true",
152 help="create an HTML report of the coverage of the files")
152 help="create an HTML report of the coverage of the files")
153 parser.add_option("-i", "--interactive", action="store_true",
153 parser.add_option("-i", "--interactive", action="store_true",
154 help="prompt to accept changed output")
154 help="prompt to accept changed output")
155 parser.add_option("-j", "--jobs", type="int",
155 parser.add_option("-j", "--jobs", type="int",
156 help="number of jobs to run in parallel"
156 help="number of jobs to run in parallel"
157 " (default: $%s or %d)" % defaults['jobs'])
157 " (default: $%s or %d)" % defaults['jobs'])
158 parser.add_option("--keep-tmpdir", action="store_true",
158 parser.add_option("--keep-tmpdir", action="store_true",
159 help="keep temporary directory after running tests")
159 help="keep temporary directory after running tests")
160 parser.add_option("-k", "--keywords",
160 parser.add_option("-k", "--keywords",
161 help="run tests matching keywords")
161 help="run tests matching keywords")
162 parser.add_option("-l", "--local", action="store_true",
162 parser.add_option("-l", "--local", action="store_true",
163 help="shortcut for --with-hg=<testdir>/../hg")
163 help="shortcut for --with-hg=<testdir>/../hg")
164 parser.add_option("--loop", action="store_true",
164 parser.add_option("--loop", action="store_true",
165 help="loop tests repeatedly")
165 help="loop tests repeatedly")
166 parser.add_option("-n", "--nodiff", action="store_true",
166 parser.add_option("-n", "--nodiff", action="store_true",
167 help="skip showing test changes")
167 help="skip showing test changes")
168 parser.add_option("-p", "--port", type="int",
168 parser.add_option("-p", "--port", type="int",
169 help="port on which servers should listen"
169 help="port on which servers should listen"
170 " (default: $%s or %d)" % defaults['port'])
170 " (default: $%s or %d)" % defaults['port'])
171 parser.add_option("--compiler", type="string",
171 parser.add_option("--compiler", type="string",
172 help="compiler to build with")
172 help="compiler to build with")
173 parser.add_option("--pure", action="store_true",
173 parser.add_option("--pure", action="store_true",
174 help="use pure Python code instead of C extensions")
174 help="use pure Python code instead of C extensions")
175 parser.add_option("-R", "--restart", action="store_true",
175 parser.add_option("-R", "--restart", action="store_true",
176 help="restart at last error")
176 help="restart at last error")
177 parser.add_option("-r", "--retest", action="store_true",
177 parser.add_option("-r", "--retest", action="store_true",
178 help="retest failed tests")
178 help="retest failed tests")
179 parser.add_option("-S", "--noskips", action="store_true",
179 parser.add_option("-S", "--noskips", action="store_true",
180 help="don't report skip tests verbosely")
180 help="don't report skip tests verbosely")
181 parser.add_option("--shell", type="string",
181 parser.add_option("--shell", type="string",
182 help="shell to use (default: $%s or %s)" % defaults['shell'])
182 help="shell to use (default: $%s or %s)" % defaults['shell'])
183 parser.add_option("-t", "--timeout", type="int",
183 parser.add_option("-t", "--timeout", type="int",
184 help="kill errant tests after TIMEOUT seconds"
184 help="kill errant tests after TIMEOUT seconds"
185 " (default: $%s or %d)" % defaults['timeout'])
185 " (default: $%s or %d)" % defaults['timeout'])
186 parser.add_option("--time", action="store_true",
186 parser.add_option("--time", action="store_true",
187 help="time how long each test takes")
187 help="time how long each test takes")
188 parser.add_option("--tmpdir", type="string",
188 parser.add_option("--tmpdir", type="string",
189 help="run tests in the given temporary directory"
189 help="run tests in the given temporary directory"
190 " (implies --keep-tmpdir)")
190 " (implies --keep-tmpdir)")
191 parser.add_option("-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, 'r')
405 f = open(self.refpath, 'r')
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 result.addFailure(self, str(e))
472 result.addFailure(self, str(e))
473 except Exception:
473 except Exception:
474 result.addError(self, sys.exc_info())
474 result.addError(self, sys.exc_info())
475 else:
475 else:
476 success = True
476 success = True
477
477
478 try:
478 try:
479 self.tearDown()
479 self.tearDown()
480 except (KeyboardInterrupt, SystemExit):
480 except (KeyboardInterrupt, SystemExit):
481 self._aborted = True
481 self._aborted = True
482 raise
482 raise
483 except Exception:
483 except Exception:
484 result.addError(self, sys.exc_info())
484 result.addError(self, sys.exc_info())
485 success = False
485 success = False
486
486
487 if success:
487 if success:
488 result.addSuccess(self)
488 result.addSuccess(self)
489 finally:
489 finally:
490 result.stopTest(self, interrupted=self._aborted)
490 result.stopTest(self, interrupted=self._aborted)
491
491
492 def runTest(self):
492 def runTest(self):
493 """Run this test instance.
493 """Run this test instance.
494
494
495 This will return a tuple describing the result of the test.
495 This will return a tuple describing the result of the test.
496 """
496 """
497 replacements = self._getreplacements()
497 replacements = self._getreplacements()
498 env = self._getenv()
498 env = self._getenv()
499 self._daemonpids.append(env['DAEMON_PIDS'])
499 self._daemonpids.append(env['DAEMON_PIDS'])
500 self._createhgrc(env['HGRCPATH'])
500 self._createhgrc(env['HGRCPATH'])
501
501
502 vlog('# Test', self.name)
502 vlog('# Test', self.name)
503
503
504 ret, out = self._run(replacements, env)
504 ret, out = self._run(replacements, env)
505 self._finished = True
505 self._finished = True
506 self._ret = ret
506 self._ret = ret
507 self._out = out
507 self._out = out
508
508
509 def describe(ret):
509 def describe(ret):
510 if ret < 0:
510 if ret < 0:
511 return 'killed by signal: %d' % -ret
511 return 'killed by signal: %d' % -ret
512 return 'returned error code %d' % ret
512 return 'returned error code %d' % ret
513
513
514 self._skipped = False
514 self._skipped = False
515
515
516 if ret == self.SKIPPED_STATUS:
516 if ret == self.SKIPPED_STATUS:
517 if out is None: # Debug mode, nothing to parse.
517 if out is None: # Debug mode, nothing to parse.
518 missing = ['unknown']
518 missing = ['unknown']
519 failed = None
519 failed = None
520 else:
520 else:
521 missing, failed = TTest.parsehghaveoutput(out)
521 missing, failed = TTest.parsehghaveoutput(out)
522
522
523 if not missing:
523 if not missing:
524 missing = ['irrelevant']
524 missing = ['irrelevant']
525
525
526 if failed:
526 if failed:
527 self.fail('hg have failed checking for %s' % failed[-1])
527 self.fail('hg have failed checking for %s' % failed[-1])
528 else:
528 else:
529 self._skipped = True
529 self._skipped = True
530 raise SkipTest(missing[-1])
530 raise SkipTest(missing[-1])
531 elif ret == 'timeout':
531 elif ret == 'timeout':
532 self.fail('timed out')
532 self.fail('timed out')
533 elif ret is False:
533 elif ret is False:
534 raise WarnTest('no result code from test')
534 raise WarnTest('no result code from test')
535 elif out != self._refout:
535 elif out != self._refout:
536 # Diff generation may rely on written .err file.
536 # Diff generation may rely on written .err file.
537 if (ret != 0 or out != self._refout) and not self._skipped \
537 if (ret != 0 or out != self._refout) and not self._skipped \
538 and not self._debug:
538 and not self._debug:
539 f = open(self.errpath, 'wb')
539 f = open(self.errpath, 'wb')
540 for line in out:
540 for line in out:
541 f.write(line)
541 f.write(line)
542 f.close()
542 f.close()
543
543
544 # The result object handles diff calculation for us.
544 # The result object handles diff calculation for us.
545 self._result.addOutputMismatch(self, ret, out, self._refout)
545 self._result.addOutputMismatch(self, ret, out, self._refout)
546
546
547 if ret:
547 if ret:
548 msg = 'output changed and ' + describe(ret)
548 msg = 'output changed and ' + describe(ret)
549 else:
549 else:
550 msg = 'output changed'
550 msg = 'output changed'
551
551
552 self.fail(msg)
552 self.fail(msg)
553 elif ret:
553 elif ret:
554 self.fail(describe(ret))
554 self.fail(describe(ret))
555
555
556 def tearDown(self):
556 def tearDown(self):
557 """Tasks to perform after run()."""
557 """Tasks to perform after run()."""
558 for entry in self._daemonpids:
558 for entry in self._daemonpids:
559 killdaemons(entry)
559 killdaemons(entry)
560 self._daemonpids = []
560 self._daemonpids = []
561
561
562 if not self._keeptmpdir:
562 if not self._keeptmpdir:
563 shutil.rmtree(self._testtmp, True)
563 shutil.rmtree(self._testtmp, True)
564 shutil.rmtree(self._threadtmp, True)
564 shutil.rmtree(self._threadtmp, True)
565
565
566 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
566 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
567 and not self._debug and self._out:
567 and not self._debug and self._out:
568 f = open(self.errpath, 'wb')
568 f = open(self.errpath, 'wb')
569 for line in self._out:
569 for line in self._out:
570 f.write(line)
570 f.write(line)
571 f.close()
571 f.close()
572
572
573 vlog("# Ret was:", self._ret)
573 vlog("# Ret was:", self._ret)
574
574
575 def _run(self, replacements, env):
575 def _run(self, replacements, env):
576 # This should be implemented in child classes to run tests.
576 # This should be implemented in child classes to run tests.
577 raise SkipTest('unknown test type')
577 raise SkipTest('unknown test type')
578
578
579 def abort(self):
579 def abort(self):
580 """Terminate execution of this test."""
580 """Terminate execution of this test."""
581 self._aborted = True
581 self._aborted = True
582
582
583 def _getreplacements(self):
583 def _getreplacements(self):
584 """Obtain a mapping of text replacements to apply to test output.
584 """Obtain a mapping of text replacements to apply to test output.
585
585
586 Test output needs to be normalized so it can be compared to expected
586 Test output needs to be normalized so it can be compared to expected
587 output. This function defines how some of that normalization will
587 output. This function defines how some of that normalization will
588 occur.
588 occur.
589 """
589 """
590 r = [
590 r = [
591 (r':%s\b' % self._startport, ':$HGPORT'),
591 (r':%s\b' % self._startport, ':$HGPORT'),
592 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
592 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
593 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
593 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
594 ]
594 ]
595
595
596 if os.name == 'nt':
596 if os.name == 'nt':
597 r.append(
597 r.append(
598 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
598 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
599 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
599 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
600 for c in self._testtmp), '$TESTTMP'))
600 for c in self._testtmp), '$TESTTMP'))
601 else:
601 else:
602 r.append((re.escape(self._testtmp), '$TESTTMP'))
602 r.append((re.escape(self._testtmp), '$TESTTMP'))
603
603
604 return r
604 return r
605
605
606 def _getenv(self):
606 def _getenv(self):
607 """Obtain environment variables to use during test execution."""
607 """Obtain environment variables to use during test execution."""
608 env = os.environ.copy()
608 env = os.environ.copy()
609 env['TESTTMP'] = self._testtmp
609 env['TESTTMP'] = self._testtmp
610 env['HOME'] = self._testtmp
610 env['HOME'] = self._testtmp
611 env["HGPORT"] = str(self._startport)
611 env["HGPORT"] = str(self._startport)
612 env["HGPORT1"] = str(self._startport + 1)
612 env["HGPORT1"] = str(self._startport + 1)
613 env["HGPORT2"] = str(self._startport + 2)
613 env["HGPORT2"] = str(self._startport + 2)
614 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
614 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
615 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
615 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
616 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
616 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
617 env["HGMERGE"] = "internal:merge"
617 env["HGMERGE"] = "internal:merge"
618 env["HGUSER"] = "test"
618 env["HGUSER"] = "test"
619 env["HGENCODING"] = "ascii"
619 env["HGENCODING"] = "ascii"
620 env["HGENCODINGMODE"] = "strict"
620 env["HGENCODINGMODE"] = "strict"
621
621
622 # Reset some environment variables to well-known values so that
622 # Reset some environment variables to well-known values so that
623 # the tests produce repeatable output.
623 # the tests produce repeatable output.
624 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
624 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
625 env['TZ'] = 'GMT'
625 env['TZ'] = 'GMT'
626 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
626 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
627 env['COLUMNS'] = '80'
627 env['COLUMNS'] = '80'
628 env['TERM'] = 'xterm'
628 env['TERM'] = 'xterm'
629
629
630 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
630 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
631 'NO_PROXY').split():
631 'NO_PROXY').split():
632 if k in env:
632 if k in env:
633 del env[k]
633 del env[k]
634
634
635 # unset env related to hooks
635 # unset env related to hooks
636 for k in env.keys():
636 for k in env.keys():
637 if k.startswith('HG_'):
637 if k.startswith('HG_'):
638 del env[k]
638 del env[k]
639
639
640 return env
640 return env
641
641
642 def _createhgrc(self, path):
642 def _createhgrc(self, path):
643 """Create an hgrc file for this test."""
643 """Create an hgrc file for this test."""
644 hgrc = open(path, 'w')
644 hgrc = open(path, 'w')
645 hgrc.write('[ui]\n')
645 hgrc.write('[ui]\n')
646 hgrc.write('slash = True\n')
646 hgrc.write('slash = True\n')
647 hgrc.write('interactive = False\n')
647 hgrc.write('interactive = False\n')
648 hgrc.write('[defaults]\n')
648 hgrc.write('[defaults]\n')
649 hgrc.write('backout = -d "0 0"\n')
649 hgrc.write('backout = -d "0 0"\n')
650 hgrc.write('commit = -d "0 0"\n')
650 hgrc.write('commit = -d "0 0"\n')
651 hgrc.write('shelve = --date "0 0"\n')
651 hgrc.write('shelve = --date "0 0"\n')
652 hgrc.write('tag = -d "0 0"\n')
652 hgrc.write('tag = -d "0 0"\n')
653 for opt in self._extraconfigopts:
653 for opt in self._extraconfigopts:
654 section, key = opt.split('.', 1)
654 section, key = opt.split('.', 1)
655 assert '=' in key, ('extra config opt %s must '
655 assert '=' in key, ('extra config opt %s must '
656 'have an = for assignment' % opt)
656 'have an = for assignment' % opt)
657 hgrc.write('[%s]\n%s\n' % (section, key))
657 hgrc.write('[%s]\n%s\n' % (section, key))
658 hgrc.close()
658 hgrc.close()
659
659
660 def fail(self, msg):
660 def fail(self, msg):
661 # unittest differentiates between errored and failed.
661 # unittest differentiates between errored and failed.
662 # Failed is denoted by AssertionError (by default at least).
662 # Failed is denoted by AssertionError (by default at least).
663 raise AssertionError(msg)
663 raise AssertionError(msg)
664
664
665 class PythonTest(Test):
665 class PythonTest(Test):
666 """A Python-based test."""
666 """A Python-based test."""
667
667
668 @property
668 @property
669 def refpath(self):
669 def refpath(self):
670 return os.path.join(self._testdir, '%s.out' % self.name)
670 return os.path.join(self._testdir, '%s.out' % self.name)
671
671
672 def _run(self, replacements, env):
672 def _run(self, replacements, env):
673 py3kswitch = self._py3kwarnings and ' -3' or ''
673 py3kswitch = self._py3kwarnings and ' -3' or ''
674 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
674 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
675 vlog("# Running", cmd)
675 vlog("# Running", cmd)
676 if os.name == 'nt':
676 if os.name == 'nt':
677 replacements.append((r'\r\n', '\n'))
677 replacements.append((r'\r\n', '\n'))
678 result = run(cmd, self._testtmp, replacements, env,
678 result = run(cmd, self._testtmp, replacements, env,
679 debug=self._debug, timeout=self._timeout)
679 debug=self._debug, timeout=self._timeout)
680 if self._aborted:
680 if self._aborted:
681 raise KeyboardInterrupt()
681 raise KeyboardInterrupt()
682
682
683 return result
683 return result
684
684
685 class TTest(Test):
685 class TTest(Test):
686 """A "t test" is a test backed by a .t file."""
686 """A "t test" is a test backed by a .t file."""
687
687
688 SKIPPED_PREFIX = 'skipped: '
688 SKIPPED_PREFIX = 'skipped: '
689 FAILED_PREFIX = 'hghave check failed: '
689 FAILED_PREFIX = 'hghave check failed: '
690 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
690 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
691
691
692 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
692 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
693 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
693 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
694 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
694 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
695
695
696 @property
696 @property
697 def refpath(self):
697 def refpath(self):
698 return os.path.join(self._testdir, self.name)
698 return os.path.join(self._testdir, self.name)
699
699
700 def _run(self, replacements, env):
700 def _run(self, replacements, env):
701 f = open(self.path)
701 f = open(self.path)
702 lines = f.readlines()
702 lines = f.readlines()
703 f.close()
703 f.close()
704
704
705 salt, script, after, expected = self._parsetest(lines)
705 salt, script, after, expected = self._parsetest(lines)
706
706
707 # Write out the generated script.
707 # Write out the generated script.
708 fname = '%s.sh' % self._testtmp
708 fname = '%s.sh' % self._testtmp
709 f = open(fname, 'w')
709 f = open(fname, 'w')
710 for l in script:
710 for l in script:
711 f.write(l)
711 f.write(l)
712 f.close()
712 f.close()
713
713
714 cmd = '%s "%s"' % (self._shell, fname)
714 cmd = '%s "%s"' % (self._shell, fname)
715 vlog("# Running", cmd)
715 vlog("# Running", cmd)
716
716
717 exitcode, output = run(cmd, self._testtmp, replacements, env,
717 exitcode, output = run(cmd, self._testtmp, replacements, env,
718 debug=self._debug, timeout=self._timeout)
718 debug=self._debug, timeout=self._timeout)
719
719
720 if self._aborted:
720 if self._aborted:
721 raise KeyboardInterrupt()
721 raise KeyboardInterrupt()
722
722
723 # Do not merge output if skipped. Return hghave message instead.
723 # Do not merge output if skipped. Return hghave message instead.
724 # Similarly, with --debug, output is None.
724 # Similarly, with --debug, output is None.
725 if exitcode == self.SKIPPED_STATUS or output is None:
725 if exitcode == self.SKIPPED_STATUS or output is None:
726 return exitcode, output
726 return exitcode, output
727
727
728 return self._processoutput(exitcode, output, salt, after, expected)
728 return self._processoutput(exitcode, output, salt, after, expected)
729
729
730 def _hghave(self, reqs):
730 def _hghave(self, reqs):
731 # TODO do something smarter when all other uses of hghave are gone.
731 # TODO do something smarter when all other uses of hghave are gone.
732 tdir = self._testdir.replace('\\', '/')
732 tdir = self._testdir.replace('\\', '/')
733 proc = Popen4('%s -c "%s/hghave %s"' %
733 proc = Popen4('%s -c "%s/hghave %s"' %
734 (self._shell, tdir, ' '.join(reqs)),
734 (self._shell, tdir, ' '.join(reqs)),
735 self._testtmp, 0)
735 self._testtmp, 0)
736 stdout, stderr = proc.communicate()
736 stdout, stderr = proc.communicate()
737 ret = proc.wait()
737 ret = proc.wait()
738 if wifexited(ret):
738 if wifexited(ret):
739 ret = os.WEXITSTATUS(ret)
739 ret = os.WEXITSTATUS(ret)
740 if ret == 2:
740 if ret == 2:
741 print stdout
741 print stdout
742 sys.exit(1)
742 sys.exit(1)
743
743
744 return ret == 0
744 return ret == 0
745
745
746 def _parsetest(self, lines):
746 def _parsetest(self, lines):
747 # We generate a shell script which outputs unique markers to line
747 # We generate a shell script which outputs unique markers to line
748 # up script results with our source. These markers include input
748 # up script results with our source. These markers include input
749 # line number and the last return code.
749 # line number and the last return code.
750 salt = "SALT" + str(time.time())
750 salt = "SALT" + str(time.time())
751 def addsalt(line, inpython):
751 def addsalt(line, inpython):
752 if inpython:
752 if inpython:
753 script.append('%s %d 0\n' % (salt, line))
753 script.append('%s %d 0\n' % (salt, line))
754 else:
754 else:
755 script.append('echo %s %s $?\n' % (salt, line))
755 script.append('echo %s %s $?\n' % (salt, line))
756
756
757 script = []
757 script = []
758
758
759 # After we run the shell script, we re-unify the script output
759 # After we run the shell script, we re-unify the script output
760 # with non-active parts of the source, with synchronization by our
760 # with non-active parts of the source, with synchronization by our
761 # SALT line number markers. The after table contains the non-active
761 # SALT line number markers. The after table contains the non-active
762 # components, ordered by line number.
762 # components, ordered by line number.
763 after = {}
763 after = {}
764
764
765 # Expected shell script output.
765 # Expected shell script output.
766 expected = {}
766 expected = {}
767
767
768 pos = prepos = -1
768 pos = prepos = -1
769
769
770 # True or False when in a true or false conditional section
770 # True or False when in a true or false conditional section
771 skipping = None
771 skipping = None
772
772
773 # We keep track of whether or not we're in a Python block so we
773 # We keep track of whether or not we're in a Python block so we
774 # can generate the surrounding doctest magic.
774 # can generate the surrounding doctest magic.
775 inpython = False
775 inpython = False
776
776
777 if self._debug:
777 if self._debug:
778 script.append('set -x\n')
778 script.append('set -x\n')
779 if os.getenv('MSYSTEM'):
779 if os.getenv('MSYSTEM'):
780 script.append('alias pwd="pwd -W"\n')
780 script.append('alias pwd="pwd -W"\n')
781
781
782 for n, l in enumerate(lines):
782 for n, l in enumerate(lines):
783 if not l.endswith('\n'):
783 if not l.endswith('\n'):
784 l += '\n'
784 l += '\n'
785 if l.startswith('#if'):
785 if l.startswith('#if'):
786 lsplit = l.split()
786 lsplit = l.split()
787 if len(lsplit) < 2 or lsplit[0] != '#if':
787 if len(lsplit) < 2 or lsplit[0] != '#if':
788 after.setdefault(pos, []).append(' !!! invalid #if\n')
788 after.setdefault(pos, []).append(' !!! invalid #if\n')
789 if skipping is not None:
789 if skipping is not None:
790 after.setdefault(pos, []).append(' !!! nested #if\n')
790 after.setdefault(pos, []).append(' !!! nested #if\n')
791 skipping = not self._hghave(lsplit[1:])
791 skipping = not self._hghave(lsplit[1:])
792 after.setdefault(pos, []).append(l)
792 after.setdefault(pos, []).append(l)
793 elif l.startswith('#else'):
793 elif l.startswith('#else'):
794 if skipping is None:
794 if skipping is None:
795 after.setdefault(pos, []).append(' !!! missing #if\n')
795 after.setdefault(pos, []).append(' !!! missing #if\n')
796 skipping = not skipping
796 skipping = not skipping
797 after.setdefault(pos, []).append(l)
797 after.setdefault(pos, []).append(l)
798 elif l.startswith('#endif'):
798 elif l.startswith('#endif'):
799 if skipping is None:
799 if skipping is None:
800 after.setdefault(pos, []).append(' !!! missing #if\n')
800 after.setdefault(pos, []).append(' !!! missing #if\n')
801 skipping = None
801 skipping = None
802 after.setdefault(pos, []).append(l)
802 after.setdefault(pos, []).append(l)
803 elif skipping:
803 elif skipping:
804 after.setdefault(pos, []).append(l)
804 after.setdefault(pos, []).append(l)
805 elif l.startswith(' >>> '): # python inlines
805 elif l.startswith(' >>> '): # python inlines
806 after.setdefault(pos, []).append(l)
806 after.setdefault(pos, []).append(l)
807 prepos = pos
807 prepos = pos
808 pos = n
808 pos = n
809 if not inpython:
809 if not inpython:
810 # We've just entered a Python block. Add the header.
810 # We've just entered a Python block. Add the header.
811 inpython = True
811 inpython = True
812 addsalt(prepos, False) # Make sure we report the exit code.
812 addsalt(prepos, False) # Make sure we report the exit code.
813 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
813 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
814 addsalt(n, True)
814 addsalt(n, True)
815 script.append(l[2:])
815 script.append(l[2:])
816 elif l.startswith(' ... '): # python inlines
816 elif l.startswith(' ... '): # python inlines
817 after.setdefault(prepos, []).append(l)
817 after.setdefault(prepos, []).append(l)
818 script.append(l[2:])
818 script.append(l[2:])
819 elif l.startswith(' $ '): # commands
819 elif l.startswith(' $ '): # commands
820 if inpython:
820 if inpython:
821 script.append('EOF\n')
821 script.append('EOF\n')
822 inpython = False
822 inpython = False
823 after.setdefault(pos, []).append(l)
823 after.setdefault(pos, []).append(l)
824 prepos = pos
824 prepos = pos
825 pos = n
825 pos = n
826 addsalt(n, False)
826 addsalt(n, False)
827 cmd = l[4:].split()
827 cmd = l[4:].split()
828 if len(cmd) == 2 and cmd[0] == 'cd':
828 if len(cmd) == 2 and cmd[0] == 'cd':
829 l = ' $ cd %s || exit 1\n' % cmd[1]
829 l = ' $ cd %s || exit 1\n' % cmd[1]
830 script.append(l[4:])
830 script.append(l[4:])
831 elif l.startswith(' > '): # continuations
831 elif l.startswith(' > '): # continuations
832 after.setdefault(prepos, []).append(l)
832 after.setdefault(prepos, []).append(l)
833 script.append(l[4:])
833 script.append(l[4:])
834 elif l.startswith(' '): # results
834 elif l.startswith(' '): # results
835 # Queue up a list of expected results.
835 # Queue up a list of expected results.
836 expected.setdefault(pos, []).append(l[2:])
836 expected.setdefault(pos, []).append(l[2:])
837 else:
837 else:
838 if inpython:
838 if inpython:
839 script.append('EOF\n')
839 script.append('EOF\n')
840 inpython = False
840 inpython = False
841 # Non-command/result. Queue up for merged output.
841 # Non-command/result. Queue up for merged output.
842 after.setdefault(pos, []).append(l)
842 after.setdefault(pos, []).append(l)
843
843
844 if inpython:
844 if inpython:
845 script.append('EOF\n')
845 script.append('EOF\n')
846 if skipping is not None:
846 if skipping is not None:
847 after.setdefault(pos, []).append(' !!! missing #endif\n')
847 after.setdefault(pos, []).append(' !!! missing #endif\n')
848 addsalt(n + 1, False)
848 addsalt(n + 1, False)
849
849
850 return salt, script, after, expected
850 return salt, script, after, expected
851
851
852 def _processoutput(self, exitcode, output, salt, after, expected):
852 def _processoutput(self, exitcode, output, salt, after, expected):
853 # Merge the script output back into a unified test.
853 # Merge the script output back into a unified test.
854 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
854 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
855 if exitcode != 0:
855 if exitcode != 0:
856 warnonly = 3
856 warnonly = 3
857
857
858 pos = -1
858 pos = -1
859 postout = []
859 postout = []
860 for l in output:
860 for l in output:
861 lout, lcmd = l, None
861 lout, lcmd = l, None
862 if salt in l:
862 if salt in l:
863 lout, lcmd = l.split(salt, 1)
863 lout, lcmd = l.split(salt, 1)
864
864
865 if lout:
865 if lout:
866 if not lout.endswith('\n'):
866 if not lout.endswith('\n'):
867 lout += ' (no-eol)\n'
867 lout += ' (no-eol)\n'
868
868
869 # Find the expected output at the current position.
869 # Find the expected output at the current position.
870 el = None
870 el = None
871 if expected.get(pos, None):
871 if expected.get(pos, None):
872 el = expected[pos].pop(0)
872 el = expected[pos].pop(0)
873
873
874 r = TTest.linematch(el, lout)
874 r = TTest.linematch(el, lout)
875 if isinstance(r, str):
875 if isinstance(r, str):
876 if r == '+glob':
876 if r == '+glob':
877 lout = el[:-1] + ' (glob)\n'
877 lout = el[:-1] + ' (glob)\n'
878 r = '' # Warn only this line.
878 r = '' # Warn only this line.
879 elif r == '-glob':
879 elif r == '-glob':
880 lout = ''.join(el.rsplit(' (glob)', 1))
880 lout = ''.join(el.rsplit(' (glob)', 1))
881 r = '' # Warn only this line.
881 r = '' # Warn only this line.
882 else:
882 else:
883 log('\ninfo, unknown linematch result: %r\n' % r)
883 log('\ninfo, unknown linematch result: %r\n' % r)
884 r = False
884 r = False
885 if r:
885 if r:
886 postout.append(' ' + el)
886 postout.append(' ' + el)
887 else:
887 else:
888 if self.NEEDESCAPE(lout):
888 if self.NEEDESCAPE(lout):
889 lout = TTest._stringescape('%s (esc)\n' %
889 lout = TTest._stringescape('%s (esc)\n' %
890 lout.rstrip('\n'))
890 lout.rstrip('\n'))
891 postout.append(' ' + lout) # Let diff deal with it.
891 postout.append(' ' + lout) # Let diff deal with it.
892 if r != '': # If line failed.
892 if r != '': # If line failed.
893 warnonly = 3 # for sure not
893 warnonly = 3 # for sure not
894 elif warnonly == 1: # Is "not yet" and line is warn only.
894 elif warnonly == 1: # Is "not yet" and line is warn only.
895 warnonly = 2 # Yes do warn.
895 warnonly = 2 # Yes do warn.
896
896
897 if lcmd:
897 if lcmd:
898 # Add on last return code.
898 # Add on last return code.
899 ret = int(lcmd.split()[1])
899 ret = int(lcmd.split()[1])
900 if ret != 0:
900 if ret != 0:
901 postout.append(' [%s]\n' % ret)
901 postout.append(' [%s]\n' % ret)
902 if pos in after:
902 if pos in after:
903 # Merge in non-active test bits.
903 # Merge in non-active test bits.
904 postout += after.pop(pos)
904 postout += after.pop(pos)
905 pos = int(lcmd.split()[0])
905 pos = int(lcmd.split()[0])
906
906
907 if pos in after:
907 if pos in after:
908 postout += after.pop(pos)
908 postout += after.pop(pos)
909
909
910 if warnonly == 2:
910 if warnonly == 2:
911 exitcode = False # Set exitcode to warned.
911 exitcode = False # Set exitcode to warned.
912
912
913 return exitcode, postout
913 return exitcode, postout
914
914
915 @staticmethod
915 @staticmethod
916 def rematch(el, l):
916 def rematch(el, l):
917 try:
917 try:
918 # use \Z to ensure that the regex matches to the end of the string
918 # use \Z to ensure that the regex matches to the end of the string
919 if os.name == 'nt':
919 if os.name == 'nt':
920 return re.match(el + r'\r?\n\Z', l)
920 return re.match(el + r'\r?\n\Z', l)
921 return re.match(el + r'\n\Z', l)
921 return re.match(el + r'\n\Z', l)
922 except re.error:
922 except re.error:
923 # el is an invalid regex
923 # el is an invalid regex
924 return False
924 return False
925
925
926 @staticmethod
926 @staticmethod
927 def globmatch(el, l):
927 def globmatch(el, l):
928 # The only supported special characters are * and ? plus / which also
928 # The only supported special characters are * and ? plus / which also
929 # matches \ on windows. Escaping of these characters is supported.
929 # matches \ on windows. Escaping of these characters is supported.
930 if el + '\n' == l:
930 if el + '\n' == l:
931 if os.altsep:
931 if os.altsep:
932 # matching on "/" is not needed for this line
932 # matching on "/" is not needed for this line
933 return '-glob'
933 return '-glob'
934 return True
934 return True
935 i, n = 0, len(el)
935 i, n = 0, len(el)
936 res = ''
936 res = ''
937 while i < n:
937 while i < n:
938 c = el[i]
938 c = el[i]
939 i += 1
939 i += 1
940 if c == '\\' and el[i] in '*?\\/':
940 if c == '\\' and el[i] in '*?\\/':
941 res += el[i - 1:i + 1]
941 res += el[i - 1:i + 1]
942 i += 1
942 i += 1
943 elif c == '*':
943 elif c == '*':
944 res += '.*'
944 res += '.*'
945 elif c == '?':
945 elif c == '?':
946 res += '.'
946 res += '.'
947 elif c == '/' and os.altsep:
947 elif c == '/' and os.altsep:
948 res += '[/\\\\]'
948 res += '[/\\\\]'
949 else:
949 else:
950 res += re.escape(c)
950 res += re.escape(c)
951 return TTest.rematch(res, l)
951 return TTest.rematch(res, l)
952
952
953 @staticmethod
953 @staticmethod
954 def linematch(el, l):
954 def linematch(el, l):
955 if el == l: # perfect match (fast)
955 if el == l: # perfect match (fast)
956 return True
956 return True
957 if el:
957 if el:
958 if el.endswith(" (esc)\n"):
958 if el.endswith(" (esc)\n"):
959 el = el[:-7].decode('string-escape') + '\n'
959 el = el[:-7].decode('string-escape') + '\n'
960 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
960 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
961 return True
961 return True
962 if el.endswith(" (re)\n"):
962 if el.endswith(" (re)\n"):
963 return TTest.rematch(el[:-6], l)
963 return TTest.rematch(el[:-6], l)
964 if el.endswith(" (glob)\n"):
964 if el.endswith(" (glob)\n"):
965 return TTest.globmatch(el[:-8], l)
965 return TTest.globmatch(el[:-8], l)
966 if os.altsep and l.replace('\\', '/') == el:
966 if os.altsep and l.replace('\\', '/') == el:
967 return '+glob'
967 return '+glob'
968 return False
968 return False
969
969
970 @staticmethod
970 @staticmethod
971 def parsehghaveoutput(lines):
971 def parsehghaveoutput(lines):
972 '''Parse hghave log lines.
972 '''Parse hghave log lines.
973
973
974 Return tuple of lists (missing, failed):
974 Return tuple of lists (missing, failed):
975 * the missing/unknown features
975 * the missing/unknown features
976 * the features for which existence check failed'''
976 * the features for which existence check failed'''
977 missing = []
977 missing = []
978 failed = []
978 failed = []
979 for line in lines:
979 for line in lines:
980 if line.startswith(TTest.SKIPPED_PREFIX):
980 if line.startswith(TTest.SKIPPED_PREFIX):
981 line = line.splitlines()[0]
981 line = line.splitlines()[0]
982 missing.append(line[len(TTest.SKIPPED_PREFIX):])
982 missing.append(line[len(TTest.SKIPPED_PREFIX):])
983 elif line.startswith(TTest.FAILED_PREFIX):
983 elif line.startswith(TTest.FAILED_PREFIX):
984 line = line.splitlines()[0]
984 line = line.splitlines()[0]
985 failed.append(line[len(TTest.FAILED_PREFIX):])
985 failed.append(line[len(TTest.FAILED_PREFIX):])
986
986
987 return missing, failed
987 return missing, failed
988
988
989 @staticmethod
989 @staticmethod
990 def _escapef(m):
990 def _escapef(m):
991 return TTest.ESCAPEMAP[m.group(0)]
991 return TTest.ESCAPEMAP[m.group(0)]
992
992
993 @staticmethod
993 @staticmethod
994 def _stringescape(s):
994 def _stringescape(s):
995 return TTest.ESCAPESUB(TTest._escapef, s)
995 return TTest.ESCAPESUB(TTest._escapef, s)
996
996
997
997
998 wifexited = getattr(os, "WIFEXITED", lambda x: False)
998 wifexited = getattr(os, "WIFEXITED", lambda x: False)
999 def run(cmd, wd, replacements, env, debug=False, timeout=None):
999 def run(cmd, wd, replacements, env, debug=False, timeout=None):
1000 """Run command in a sub-process, capturing the output (stdout and stderr).
1000 """Run command in a sub-process, capturing the output (stdout and stderr).
1001 Return a tuple (exitcode, output). output is None in debug mode."""
1001 Return a tuple (exitcode, output). output is None in debug mode."""
1002 if debug:
1002 if debug:
1003 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1003 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1004 ret = proc.wait()
1004 ret = proc.wait()
1005 return (ret, None)
1005 return (ret, None)
1006
1006
1007 proc = Popen4(cmd, wd, timeout, env)
1007 proc = Popen4(cmd, wd, timeout, env)
1008 def cleanup():
1008 def cleanup():
1009 terminate(proc)
1009 terminate(proc)
1010 ret = proc.wait()
1010 ret = proc.wait()
1011 if ret == 0:
1011 if ret == 0:
1012 ret = signal.SIGTERM << 8
1012 ret = signal.SIGTERM << 8
1013 killdaemons(env['DAEMON_PIDS'])
1013 killdaemons(env['DAEMON_PIDS'])
1014 return ret
1014 return ret
1015
1015
1016 output = ''
1016 output = ''
1017 proc.tochild.close()
1017 proc.tochild.close()
1018
1018
1019 try:
1019 try:
1020 output = proc.fromchild.read()
1020 output = proc.fromchild.read()
1021 except KeyboardInterrupt:
1021 except KeyboardInterrupt:
1022 vlog('# Handling keyboard interrupt')
1022 vlog('# Handling keyboard interrupt')
1023 cleanup()
1023 cleanup()
1024 raise
1024 raise
1025
1025
1026 ret = proc.wait()
1026 ret = proc.wait()
1027 if wifexited(ret):
1027 if wifexited(ret):
1028 ret = os.WEXITSTATUS(ret)
1028 ret = os.WEXITSTATUS(ret)
1029
1029
1030 if proc.timeout:
1030 if proc.timeout:
1031 ret = 'timeout'
1031 ret = 'timeout'
1032
1032
1033 if ret:
1033 if ret:
1034 killdaemons(env['DAEMON_PIDS'])
1034 killdaemons(env['DAEMON_PIDS'])
1035
1035
1036 for s, r in replacements:
1036 for s, r in replacements:
1037 output = re.sub(s, r, output)
1037 output = re.sub(s, r, output)
1038 return ret, output.splitlines(True)
1038 return ret, output.splitlines(True)
1039
1039
1040 iolock = threading.Lock()
1040 iolock = threading.Lock()
1041
1041
1042 class SkipTest(Exception):
1042 class SkipTest(Exception):
1043 """Raised to indicate that a test is to be skipped."""
1043 """Raised to indicate that a test is to be skipped."""
1044
1044
1045 class IgnoreTest(Exception):
1045 class IgnoreTest(Exception):
1046 """Raised to indicate that a test is to be ignored."""
1046 """Raised to indicate that a test is to be ignored."""
1047
1047
1048 class WarnTest(Exception):
1048 class WarnTest(Exception):
1049 """Raised to indicate that a test warned."""
1049 """Raised to indicate that a test warned."""
1050
1050
1051 class TestResult(unittest._TextTestResult):
1051 class TestResult(unittest._TextTestResult):
1052 """Holds results when executing via unittest."""
1052 """Holds results when executing via unittest."""
1053 # Don't worry too much about accessing the non-public _TextTestResult.
1053 # Don't worry too much about accessing the non-public _TextTestResult.
1054 # It is relatively common in Python testing tools.
1054 # It is relatively common in Python testing tools.
1055 def __init__(self, options, *args, **kwargs):
1055 def __init__(self, options, *args, **kwargs):
1056 super(TestResult, self).__init__(*args, **kwargs)
1056 super(TestResult, self).__init__(*args, **kwargs)
1057
1057
1058 self._options = options
1058 self._options = options
1059
1059
1060 # unittest.TestResult didn't have skipped until 2.7. We need to
1060 # unittest.TestResult didn't have skipped until 2.7. We need to
1061 # polyfill it.
1061 # polyfill it.
1062 self.skipped = []
1062 self.skipped = []
1063
1063
1064 # We have a custom "ignored" result that isn't present in any Python
1064 # We have a custom "ignored" result that isn't present in any Python
1065 # unittest implementation. It is very similar to skipped. It may make
1065 # unittest implementation. It is very similar to skipped. It may make
1066 # sense to map it into skip some day.
1066 # sense to map it into skip some day.
1067 self.ignored = []
1067 self.ignored = []
1068
1068
1069 # We have a custom "warned" result that isn't present in any Python
1069 # We have a custom "warned" result that isn't present in any Python
1070 # unittest implementation. It is very similar to failed. It may make
1070 # unittest implementation. It is very similar to failed. It may make
1071 # sense to map it into fail some day.
1071 # sense to map it into fail some day.
1072 self.warned = []
1072 self.warned = []
1073
1073
1074 self.times = []
1074 self.times = []
1075 self._started = {}
1075 self._started = {}
1076
1076
1077 def addFailure(self, test, reason):
1077 def addFailure(self, test, reason):
1078 self.failures.append((test, reason))
1078 self.failures.append((test, reason))
1079
1079
1080 if self._options.first:
1080 if self._options.first:
1081 self.stop()
1081 self.stop()
1082 else:
1082 else:
1083 if not self._options.nodiff:
1083 if not self._options.nodiff:
1084 self.stream.write('\nERROR: %s output changed\n' % test)
1084 self.stream.write('\nERROR: %s output changed\n' % test)
1085 self.stream.write('!')
1085 self.stream.write('!')
1086
1086
1087 def addError(self, *args, **kwargs):
1087 def addError(self, *args, **kwargs):
1088 super(TestResult, self).addError(*args, **kwargs)
1088 super(TestResult, self).addError(*args, **kwargs)
1089
1089
1090 if self._options.first:
1090 if self._options.first:
1091 self.stop()
1091 self.stop()
1092
1092
1093 # Polyfill.
1093 # Polyfill.
1094 def addSkip(self, test, reason):
1094 def addSkip(self, test, reason):
1095 self.skipped.append((test, reason))
1095 self.skipped.append((test, reason))
1096
1096
1097 if self.showAll:
1097 if self.showAll:
1098 self.stream.writeln('skipped %s' % reason)
1098 self.stream.writeln('skipped %s' % reason)
1099 else:
1099 else:
1100 self.stream.write('s')
1100 self.stream.write('s')
1101 self.stream.flush()
1101 self.stream.flush()
1102
1102
1103 def addIgnore(self, test, reason):
1103 def addIgnore(self, test, reason):
1104 self.ignored.append((test, reason))
1104 self.ignored.append((test, reason))
1105
1105
1106 if self.showAll:
1106 if self.showAll:
1107 self.stream.writeln('ignored %s' % reason)
1107 self.stream.writeln('ignored %s' % reason)
1108 else:
1108 else:
1109 self.stream.write('i')
1109 if reason != 'not retesting':
1110 self.stream.write('i')
1110 self.stream.flush()
1111 self.stream.flush()
1111
1112
1112 def addWarn(self, test, reason):
1113 def addWarn(self, test, reason):
1113 self.warned.append((test, reason))
1114 self.warned.append((test, reason))
1114
1115
1115 if self._options.first:
1116 if self._options.first:
1116 self.stop()
1117 self.stop()
1117
1118
1118 if self.showAll:
1119 if self.showAll:
1119 self.stream.writeln('warned %s' % reason)
1120 self.stream.writeln('warned %s' % reason)
1120 else:
1121 else:
1121 self.stream.write('~')
1122 self.stream.write('~')
1122 self.stream.flush()
1123 self.stream.flush()
1123
1124
1124 def addOutputMismatch(self, test, ret, got, expected):
1125 def addOutputMismatch(self, test, ret, got, expected):
1125 """Record a mismatch in test output for a particular test."""
1126 """Record a mismatch in test output for a particular test."""
1126
1127
1127 if self._options.nodiff:
1128 if self._options.nodiff:
1128 return
1129 return
1129
1130
1130 if self._options.view:
1131 if self._options.view:
1131 os.system("%s %s %s" % (self._view, test.refpath, test.errpath))
1132 os.system("%s %s %s" % (self._view, test.refpath, test.errpath))
1132 else:
1133 else:
1133 failed, lines = getdiff(expected, got,
1134 failed, lines = getdiff(expected, got,
1134 test.refpath, test.errpath)
1135 test.refpath, test.errpath)
1135 if failed:
1136 if failed:
1136 self.addFailure(test, 'diff generation failed')
1137 self.addFailure(test, 'diff generation failed')
1137 else:
1138 else:
1138 self.stream.write('\n')
1139 self.stream.write('\n')
1139 for line in lines:
1140 for line in lines:
1140 self.stream.write(line)
1141 self.stream.write(line)
1141 self.stream.flush()
1142 self.stream.flush()
1142
1143
1143 if ret or not self._options.interactive or \
1144 if ret or not self._options.interactive or \
1144 not os.path.exists(test.errpath):
1145 not os.path.exists(test.errpath):
1145 return
1146 return
1146
1147
1147 iolock.acquire()
1148 iolock.acquire()
1148 print 'Accept this change? [n] ',
1149 print 'Accept this change? [n] ',
1149 answer = sys.stdin.readline().strip()
1150 answer = sys.stdin.readline().strip()
1150 iolock.release()
1151 iolock.release()
1151 if answer.lower() in ('y', 'yes'):
1152 if answer.lower() in ('y', 'yes'):
1152 if test.name.endswith('.t'):
1153 if test.name.endswith('.t'):
1153 rename(test.errpath, test.path)
1154 rename(test.errpath, test.path)
1154 else:
1155 else:
1155 rename(test.errpath, '%s.out' % test.path)
1156 rename(test.errpath, '%s.out' % test.path)
1156
1157
1157 def startTest(self, test):
1158 def startTest(self, test):
1158 super(TestResult, self).startTest(test)
1159 super(TestResult, self).startTest(test)
1159
1160
1160 self._started[test.name] = time.time()
1161 self._started[test.name] = time.time()
1161
1162
1162 def stopTest(self, test, interrupted=False):
1163 def stopTest(self, test, interrupted=False):
1163 super(TestResult, self).stopTest(test)
1164 super(TestResult, self).stopTest(test)
1164
1165
1165 self.times.append((test.name, time.time() - self._started[test.name]))
1166 self.times.append((test.name, time.time() - self._started[test.name]))
1166 del self._started[test.name]
1167 del self._started[test.name]
1167
1168
1168 if interrupted:
1169 if interrupted:
1169 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1170 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1170 test.name, self.times[-1][1]))
1171 test.name, self.times[-1][1]))
1171
1172
1172 class TestSuite(unittest.TestSuite):
1173 class TestSuite(unittest.TestSuite):
1173 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1174 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1174
1175
1175 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1176 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1176 retest=False, keywords=None, loop=False,
1177 retest=False, keywords=None, loop=False,
1177 *args, **kwargs):
1178 *args, **kwargs):
1178 """Create a new instance that can run tests with a configuration.
1179 """Create a new instance that can run tests with a configuration.
1179
1180
1180 testdir specifies the directory where tests are executed from. This
1181 testdir specifies the directory where tests are executed from. This
1181 is typically the ``tests`` directory from Mercurial's source
1182 is typically the ``tests`` directory from Mercurial's source
1182 repository.
1183 repository.
1183
1184
1184 jobs specifies the number of jobs to run concurrently. Each test
1185 jobs specifies the number of jobs to run concurrently. Each test
1185 executes on its own thread. Tests actually spawn new processes, so
1186 executes on its own thread. Tests actually spawn new processes, so
1186 state mutation should not be an issue.
1187 state mutation should not be an issue.
1187
1188
1188 whitelist and blacklist denote tests that have been whitelisted and
1189 whitelist and blacklist denote tests that have been whitelisted and
1189 blacklisted, respectively. These arguments don't belong in TestSuite.
1190 blacklisted, respectively. These arguments don't belong in TestSuite.
1190 Instead, whitelist and blacklist should be handled by the thing that
1191 Instead, whitelist and blacklist should be handled by the thing that
1191 populates the TestSuite with tests. They are present to preserve
1192 populates the TestSuite with tests. They are present to preserve
1192 backwards compatible behavior which reports skipped tests as part
1193 backwards compatible behavior which reports skipped tests as part
1193 of the results.
1194 of the results.
1194
1195
1195 retest denotes whether to retest failed tests. This arguably belongs
1196 retest denotes whether to retest failed tests. This arguably belongs
1196 outside of TestSuite.
1197 outside of TestSuite.
1197
1198
1198 keywords denotes key words that will be used to filter which tests
1199 keywords denotes key words that will be used to filter which tests
1199 to execute. This arguably belongs outside of TestSuite.
1200 to execute. This arguably belongs outside of TestSuite.
1200
1201
1201 loop denotes whether to loop over tests forever.
1202 loop denotes whether to loop over tests forever.
1202 """
1203 """
1203 super(TestSuite, self).__init__(*args, **kwargs)
1204 super(TestSuite, self).__init__(*args, **kwargs)
1204
1205
1205 self._jobs = jobs
1206 self._jobs = jobs
1206 self._whitelist = whitelist
1207 self._whitelist = whitelist
1207 self._blacklist = blacklist
1208 self._blacklist = blacklist
1208 self._retest = retest
1209 self._retest = retest
1209 self._keywords = keywords
1210 self._keywords = keywords
1210 self._loop = loop
1211 self._loop = loop
1211
1212
1212 def run(self, result):
1213 def run(self, result):
1213 # We have a number of filters that need to be applied. We do this
1214 # We have a number of filters that need to be applied. We do this
1214 # here instead of inside Test because it makes the running logic for
1215 # here instead of inside Test because it makes the running logic for
1215 # Test simpler.
1216 # Test simpler.
1216 tests = []
1217 tests = []
1217 for test in self._tests:
1218 for test in self._tests:
1218 if not os.path.exists(test.path):
1219 if not os.path.exists(test.path):
1219 result.addSkip(test, "Doesn't exist")
1220 result.addSkip(test, "Doesn't exist")
1220 continue
1221 continue
1221
1222
1222 if not (self._whitelist and test.name in self._whitelist):
1223 if not (self._whitelist and test.name in self._whitelist):
1223 if self._blacklist and test.name in self._blacklist:
1224 if self._blacklist and test.name in self._blacklist:
1224 result.addSkip(test, 'blacklisted')
1225 result.addSkip(test, 'blacklisted')
1225 continue
1226 continue
1226
1227
1227 if self._retest and not os.path.exists(test.errpath):
1228 if self._retest and not os.path.exists(test.errpath):
1228 result.addIgnore(test, 'not retesting')
1229 result.addIgnore(test, 'not retesting')
1229 continue
1230 continue
1230
1231
1231 if self._keywords:
1232 if self._keywords:
1232 f = open(test.path)
1233 f = open(test.path)
1233 t = f.read().lower() + test.name.lower()
1234 t = f.read().lower() + test.name.lower()
1234 f.close()
1235 f.close()
1235 ignored = False
1236 ignored = False
1236 for k in self._keywords.lower().split():
1237 for k in self._keywords.lower().split():
1237 if k not in t:
1238 if k not in t:
1238 result.addIgnore(test, "doesn't match keyword")
1239 result.addIgnore(test, "doesn't match keyword")
1239 ignored = True
1240 ignored = True
1240 break
1241 break
1241
1242
1242 if ignored:
1243 if ignored:
1243 continue
1244 continue
1244
1245
1245 tests.append(test)
1246 tests.append(test)
1246
1247
1247 runtests = list(tests)
1248 runtests = list(tests)
1248 done = queue.Queue()
1249 done = queue.Queue()
1249 running = 0
1250 running = 0
1250
1251
1251 def job(test, result):
1252 def job(test, result):
1252 try:
1253 try:
1253 test(result)
1254 test(result)
1254 done.put(None)
1255 done.put(None)
1255 except KeyboardInterrupt:
1256 except KeyboardInterrupt:
1256 pass
1257 pass
1257 except: # re-raises
1258 except: # re-raises
1258 done.put(('!', test, 'run-test raised an error, see traceback'))
1259 done.put(('!', test, 'run-test raised an error, see traceback'))
1259 raise
1260 raise
1260
1261
1261 try:
1262 try:
1262 while tests or running:
1263 while tests or running:
1263 if not done.empty() or running == self._jobs or not tests:
1264 if not done.empty() or running == self._jobs or not tests:
1264 try:
1265 try:
1265 done.get(True, 1)
1266 done.get(True, 1)
1266 if result and result.shouldStop:
1267 if result and result.shouldStop:
1267 break
1268 break
1268 except queue.Empty:
1269 except queue.Empty:
1269 continue
1270 continue
1270 running -= 1
1271 running -= 1
1271 if tests and not running == self._jobs:
1272 if tests and not running == self._jobs:
1272 test = tests.pop(0)
1273 test = tests.pop(0)
1273 if self._loop:
1274 if self._loop:
1274 tests.append(test)
1275 tests.append(test)
1275 t = threading.Thread(target=job, name=test.name,
1276 t = threading.Thread(target=job, name=test.name,
1276 args=(test, result))
1277 args=(test, result))
1277 t.start()
1278 t.start()
1278 running += 1
1279 running += 1
1279 except KeyboardInterrupt:
1280 except KeyboardInterrupt:
1280 for test in runtests:
1281 for test in runtests:
1281 test.abort()
1282 test.abort()
1282
1283
1283 return result
1284 return result
1284
1285
1285 class TextTestRunner(unittest.TextTestRunner):
1286 class TextTestRunner(unittest.TextTestRunner):
1286 """Custom unittest test runner that uses appropriate settings."""
1287 """Custom unittest test runner that uses appropriate settings."""
1287
1288
1288 def __init__(self, runner, *args, **kwargs):
1289 def __init__(self, runner, *args, **kwargs):
1289 super(TextTestRunner, self).__init__(*args, **kwargs)
1290 super(TextTestRunner, self).__init__(*args, **kwargs)
1290
1291
1291 self._runner = runner
1292 self._runner = runner
1292
1293
1293 def run(self, test):
1294 def run(self, test):
1294 result = TestResult(self._runner.options, self.stream,
1295 result = TestResult(self._runner.options, self.stream,
1295 self.descriptions, self.verbosity)
1296 self.descriptions, self.verbosity)
1296
1297
1297 test(result)
1298 test(result)
1298
1299
1299 failed = len(result.failures)
1300 failed = len(result.failures)
1300 warned = len(result.warned)
1301 warned = len(result.warned)
1301 skipped = len(result.skipped)
1302 skipped = len(result.skipped)
1302 ignored = len(result.ignored)
1303 ignored = len(result.ignored)
1303
1304
1304 self.stream.writeln('')
1305 self.stream.writeln('')
1305
1306
1306 if not self._runner.options.noskips:
1307 if not self._runner.options.noskips:
1307 for test, msg in result.skipped:
1308 for test, msg in result.skipped:
1308 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1309 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1309 for test, msg in result.warned:
1310 for test, msg in result.warned:
1310 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1311 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1311 for test, msg in result.failures:
1312 for test, msg in result.failures:
1312 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1313 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1313 for test, msg in result.errors:
1314 for test, msg in result.errors:
1314 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1315 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1315
1316
1316 self._runner._checkhglib('Tested')
1317 self._runner._checkhglib('Tested')
1317
1318
1318 # This differs from unittest's default output in that we don't count
1319 # This differs from unittest's default output in that we don't count
1319 # skipped and ignored tests as part of the total test count.
1320 # skipped and ignored tests as part of the total test count.
1320 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1321 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1321 % (result.testsRun - skipped - ignored,
1322 % (result.testsRun - skipped - ignored,
1322 skipped + ignored, warned, failed))
1323 skipped + ignored, warned, failed))
1323 if failed:
1324 if failed:
1324 self.stream.writeln('python hash seed: %s' %
1325 self.stream.writeln('python hash seed: %s' %
1325 os.environ['PYTHONHASHSEED'])
1326 os.environ['PYTHONHASHSEED'])
1326 if self._runner.options.time:
1327 if self._runner.options.time:
1327 self.printtimes(result.times)
1328 self.printtimes(result.times)
1328
1329
1329 return result
1330 return result
1330
1331
1331 def printtimes(self, times):
1332 def printtimes(self, times):
1332 self.stream.writeln('# Producing time report')
1333 self.stream.writeln('# Producing time report')
1333 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1334 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1334 cols = '%7.3f %s'
1335 cols = '%7.3f %s'
1335 self.stream.writeln('%-7s %s' % ('Time', 'Test'))
1336 self.stream.writeln('%-7s %s' % ('Time', 'Test'))
1336 for test, timetaken in times:
1337 for test, timetaken in times:
1337 self.stream.writeln(cols % (timetaken, test))
1338 self.stream.writeln(cols % (timetaken, test))
1338
1339
1339 class TestRunner(object):
1340 class TestRunner(object):
1340 """Holds context for executing tests.
1341 """Holds context for executing tests.
1341
1342
1342 Tests rely on a lot of state. This object holds it for them.
1343 Tests rely on a lot of state. This object holds it for them.
1343 """
1344 """
1344
1345
1345 # Programs required to run tests.
1346 # Programs required to run tests.
1346 REQUIREDTOOLS = [
1347 REQUIREDTOOLS = [
1347 os.path.basename(sys.executable),
1348 os.path.basename(sys.executable),
1348 'diff',
1349 'diff',
1349 'grep',
1350 'grep',
1350 'unzip',
1351 'unzip',
1351 'gunzip',
1352 'gunzip',
1352 'bunzip2',
1353 'bunzip2',
1353 'sed',
1354 'sed',
1354 ]
1355 ]
1355
1356
1356 # Maps file extensions to test class.
1357 # Maps file extensions to test class.
1357 TESTTYPES = [
1358 TESTTYPES = [
1358 ('.py', PythonTest),
1359 ('.py', PythonTest),
1359 ('.t', TTest),
1360 ('.t', TTest),
1360 ]
1361 ]
1361
1362
1362 def __init__(self):
1363 def __init__(self):
1363 self.options = None
1364 self.options = None
1364 self._testdir = None
1365 self._testdir = None
1365 self._hgtmp = None
1366 self._hgtmp = None
1366 self._installdir = None
1367 self._installdir = None
1367 self._bindir = None
1368 self._bindir = None
1368 self._tmpbinddir = None
1369 self._tmpbinddir = None
1369 self._pythondir = None
1370 self._pythondir = None
1370 self._coveragefile = None
1371 self._coveragefile = None
1371 self._createdfiles = []
1372 self._createdfiles = []
1372 self._hgpath = None
1373 self._hgpath = None
1373
1374
1374 def run(self, args, parser=None):
1375 def run(self, args, parser=None):
1375 """Run the test suite."""
1376 """Run the test suite."""
1376 oldmask = os.umask(022)
1377 oldmask = os.umask(022)
1377 try:
1378 try:
1378 parser = parser or getparser()
1379 parser = parser or getparser()
1379 options, args = parseargs(args, parser)
1380 options, args = parseargs(args, parser)
1380 self.options = options
1381 self.options = options
1381
1382
1382 self._checktools()
1383 self._checktools()
1383 tests = self.findtests(args)
1384 tests = self.findtests(args)
1384 return self._run(tests)
1385 return self._run(tests)
1385 finally:
1386 finally:
1386 os.umask(oldmask)
1387 os.umask(oldmask)
1387
1388
1388 def _run(self, tests):
1389 def _run(self, tests):
1389 if self.options.random:
1390 if self.options.random:
1390 random.shuffle(tests)
1391 random.shuffle(tests)
1391 else:
1392 else:
1392 # keywords for slow tests
1393 # keywords for slow tests
1393 slow = 'svn gendoc check-code-hg'.split()
1394 slow = 'svn gendoc check-code-hg'.split()
1394 def sortkey(f):
1395 def sortkey(f):
1395 # run largest tests first, as they tend to take the longest
1396 # run largest tests first, as they tend to take the longest
1396 try:
1397 try:
1397 val = -os.stat(f).st_size
1398 val = -os.stat(f).st_size
1398 except OSError, e:
1399 except OSError, e:
1399 if e.errno != errno.ENOENT:
1400 if e.errno != errno.ENOENT:
1400 raise
1401 raise
1401 return -1e9 # file does not exist, tell early
1402 return -1e9 # file does not exist, tell early
1402 for kw in slow:
1403 for kw in slow:
1403 if kw in f:
1404 if kw in f:
1404 val *= 10
1405 val *= 10
1405 return val
1406 return val
1406 tests.sort(key=sortkey)
1407 tests.sort(key=sortkey)
1407
1408
1408 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1409 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1409
1410
1410 if 'PYTHONHASHSEED' not in os.environ:
1411 if 'PYTHONHASHSEED' not in os.environ:
1411 # use a random python hash seed all the time
1412 # use a random python hash seed all the time
1412 # we do the randomness ourself to know what seed is used
1413 # we do the randomness ourself to know what seed is used
1413 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1414 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1414
1415
1415 if self.options.tmpdir:
1416 if self.options.tmpdir:
1416 self.options.keep_tmpdir = True
1417 self.options.keep_tmpdir = True
1417 tmpdir = self.options.tmpdir
1418 tmpdir = self.options.tmpdir
1418 if os.path.exists(tmpdir):
1419 if os.path.exists(tmpdir):
1419 # Meaning of tmpdir has changed since 1.3: we used to create
1420 # Meaning of tmpdir has changed since 1.3: we used to create
1420 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1421 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1421 # tmpdir already exists.
1422 # tmpdir already exists.
1422 print "error: temp dir %r already exists" % tmpdir
1423 print "error: temp dir %r already exists" % tmpdir
1423 return 1
1424 return 1
1424
1425
1425 # Automatically removing tmpdir sounds convenient, but could
1426 # Automatically removing tmpdir sounds convenient, but could
1426 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1427 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1427 # or "--tmpdir=$HOME".
1428 # or "--tmpdir=$HOME".
1428 #vlog("# Removing temp dir", tmpdir)
1429 #vlog("# Removing temp dir", tmpdir)
1429 #shutil.rmtree(tmpdir)
1430 #shutil.rmtree(tmpdir)
1430 os.makedirs(tmpdir)
1431 os.makedirs(tmpdir)
1431 else:
1432 else:
1432 d = None
1433 d = None
1433 if os.name == 'nt':
1434 if os.name == 'nt':
1434 # without this, we get the default temp dir location, but
1435 # without this, we get the default temp dir location, but
1435 # in all lowercase, which causes troubles with paths (issue3490)
1436 # in all lowercase, which causes troubles with paths (issue3490)
1436 d = os.getenv('TMP')
1437 d = os.getenv('TMP')
1437 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1438 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1438 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1439 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1439
1440
1440 if self.options.with_hg:
1441 if self.options.with_hg:
1441 self._installdir = None
1442 self._installdir = None
1442 self._bindir = os.path.dirname(os.path.realpath(
1443 self._bindir = os.path.dirname(os.path.realpath(
1443 self.options.with_hg))
1444 self.options.with_hg))
1444 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1445 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1445 os.makedirs(self._tmpbindir)
1446 os.makedirs(self._tmpbindir)
1446
1447
1447 # This looks redundant with how Python initializes sys.path from
1448 # This looks redundant with how Python initializes sys.path from
1448 # the location of the script being executed. Needed because the
1449 # the location of the script being executed. Needed because the
1449 # "hg" specified by --with-hg is not the only Python script
1450 # "hg" specified by --with-hg is not the only Python script
1450 # executed in the test suite that needs to import 'mercurial'
1451 # executed in the test suite that needs to import 'mercurial'
1451 # ... which means it's not really redundant at all.
1452 # ... which means it's not really redundant at all.
1452 self._pythondir = self._bindir
1453 self._pythondir = self._bindir
1453 else:
1454 else:
1454 self._installdir = os.path.join(self._hgtmp, "install")
1455 self._installdir = os.path.join(self._hgtmp, "install")
1455 self._bindir = os.environ["BINDIR"] = \
1456 self._bindir = os.environ["BINDIR"] = \
1456 os.path.join(self._installdir, "bin")
1457 os.path.join(self._installdir, "bin")
1457 self._tmpbindir = self._bindir
1458 self._tmpbindir = self._bindir
1458 self._pythondir = os.path.join(self._installdir, "lib", "python")
1459 self._pythondir = os.path.join(self._installdir, "lib", "python")
1459
1460
1460 os.environ["BINDIR"] = self._bindir
1461 os.environ["BINDIR"] = self._bindir
1461 os.environ["PYTHON"] = PYTHON
1462 os.environ["PYTHON"] = PYTHON
1462
1463
1463 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1464 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1464 if self._tmpbindir != self._bindir:
1465 if self._tmpbindir != self._bindir:
1465 path = [self._tmpbindir] + path
1466 path = [self._tmpbindir] + path
1466 os.environ["PATH"] = os.pathsep.join(path)
1467 os.environ["PATH"] = os.pathsep.join(path)
1467
1468
1468 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1469 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1469 # can run .../tests/run-tests.py test-foo where test-foo
1470 # can run .../tests/run-tests.py test-foo where test-foo
1470 # adds an extension to HGRC. Also include run-test.py directory to
1471 # adds an extension to HGRC. Also include run-test.py directory to
1471 # import modules like heredoctest.
1472 # import modules like heredoctest.
1472 pypath = [self._pythondir, self._testdir,
1473 pypath = [self._pythondir, self._testdir,
1473 os.path.abspath(os.path.dirname(__file__))]
1474 os.path.abspath(os.path.dirname(__file__))]
1474 # We have to augment PYTHONPATH, rather than simply replacing
1475 # We have to augment PYTHONPATH, rather than simply replacing
1475 # it, in case external libraries are only available via current
1476 # it, in case external libraries are only available via current
1476 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1477 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1477 # are in /opt/subversion.)
1478 # are in /opt/subversion.)
1478 oldpypath = os.environ.get(IMPL_PATH)
1479 oldpypath = os.environ.get(IMPL_PATH)
1479 if oldpypath:
1480 if oldpypath:
1480 pypath.append(oldpypath)
1481 pypath.append(oldpypath)
1481 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1482 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1482
1483
1483 self._coveragefile = os.path.join(self._testdir, '.coverage')
1484 self._coveragefile = os.path.join(self._testdir, '.coverage')
1484
1485
1485 vlog("# Using TESTDIR", self._testdir)
1486 vlog("# Using TESTDIR", self._testdir)
1486 vlog("# Using HGTMP", self._hgtmp)
1487 vlog("# Using HGTMP", self._hgtmp)
1487 vlog("# Using PATH", os.environ["PATH"])
1488 vlog("# Using PATH", os.environ["PATH"])
1488 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1489 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1489
1490
1490 try:
1491 try:
1491 return self._runtests(tests) or 0
1492 return self._runtests(tests) or 0
1492 finally:
1493 finally:
1493 time.sleep(.1)
1494 time.sleep(.1)
1494 self._cleanup()
1495 self._cleanup()
1495
1496
1496 def findtests(self, args):
1497 def findtests(self, args):
1497 """Finds possible test files from arguments.
1498 """Finds possible test files from arguments.
1498
1499
1499 If you wish to inject custom tests into the test harness, this would
1500 If you wish to inject custom tests into the test harness, this would
1500 be a good function to monkeypatch or override in a derived class.
1501 be a good function to monkeypatch or override in a derived class.
1501 """
1502 """
1502 if not args:
1503 if not args:
1503 if self.options.changed:
1504 if self.options.changed:
1504 proc = Popen4('hg st --rev "%s" -man0 .' %
1505 proc = Popen4('hg st --rev "%s" -man0 .' %
1505 self.options.changed, None, 0)
1506 self.options.changed, None, 0)
1506 stdout, stderr = proc.communicate()
1507 stdout, stderr = proc.communicate()
1507 args = stdout.strip('\0').split('\0')
1508 args = stdout.strip('\0').split('\0')
1508 else:
1509 else:
1509 args = os.listdir('.')
1510 args = os.listdir('.')
1510
1511
1511 return [t for t in args
1512 return [t for t in args
1512 if os.path.basename(t).startswith('test-')
1513 if os.path.basename(t).startswith('test-')
1513 and (t.endswith('.py') or t.endswith('.t'))]
1514 and (t.endswith('.py') or t.endswith('.t'))]
1514
1515
1515 def _runtests(self, tests):
1516 def _runtests(self, tests):
1516 try:
1517 try:
1517 if self._installdir:
1518 if self._installdir:
1518 self._installhg()
1519 self._installhg()
1519 self._checkhglib("Testing")
1520 self._checkhglib("Testing")
1520 else:
1521 else:
1521 self._usecorrectpython()
1522 self._usecorrectpython()
1522
1523
1523 if self.options.restart:
1524 if self.options.restart:
1524 orig = list(tests)
1525 orig = list(tests)
1525 while tests:
1526 while tests:
1526 if os.path.exists(tests[0] + ".err"):
1527 if os.path.exists(tests[0] + ".err"):
1527 break
1528 break
1528 tests.pop(0)
1529 tests.pop(0)
1529 if not tests:
1530 if not tests:
1530 print "running all tests"
1531 print "running all tests"
1531 tests = orig
1532 tests = orig
1532
1533
1533 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1534 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1534
1535
1535 failed = False
1536 failed = False
1536 warned = False
1537 warned = False
1537
1538
1538 suite = TestSuite(self._testdir,
1539 suite = TestSuite(self._testdir,
1539 jobs=self.options.jobs,
1540 jobs=self.options.jobs,
1540 whitelist=self.options.whitelisted,
1541 whitelist=self.options.whitelisted,
1541 blacklist=self.options.blacklist,
1542 blacklist=self.options.blacklist,
1542 retest=self.options.retest,
1543 retest=self.options.retest,
1543 keywords=self.options.keywords,
1544 keywords=self.options.keywords,
1544 loop=self.options.loop,
1545 loop=self.options.loop,
1545 tests=tests)
1546 tests=tests)
1546 verbosity = 1
1547 verbosity = 1
1547 if self.options.verbose:
1548 if self.options.verbose:
1548 verbosity = 2
1549 verbosity = 2
1549 runner = TextTestRunner(self, verbosity=verbosity)
1550 runner = TextTestRunner(self, verbosity=verbosity)
1550 result = runner.run(suite)
1551 result = runner.run(suite)
1551
1552
1552 if result.failures:
1553 if result.failures:
1553 failed = True
1554 failed = True
1554 if result.warned:
1555 if result.warned:
1555 warned = True
1556 warned = True
1556
1557
1557 if self.options.anycoverage:
1558 if self.options.anycoverage:
1558 self._outputcoverage()
1559 self._outputcoverage()
1559 except KeyboardInterrupt:
1560 except KeyboardInterrupt:
1560 failed = True
1561 failed = True
1561 print "\ninterrupted!"
1562 print "\ninterrupted!"
1562
1563
1563 if failed:
1564 if failed:
1564 return 1
1565 return 1
1565 if warned:
1566 if warned:
1566 return 80
1567 return 80
1567
1568
1568 def _gettest(self, test, count):
1569 def _gettest(self, test, count):
1569 """Obtain a Test by looking at its filename.
1570 """Obtain a Test by looking at its filename.
1570
1571
1571 Returns a Test instance. The Test may not be runnable if it doesn't
1572 Returns a Test instance. The Test may not be runnable if it doesn't
1572 map to a known type.
1573 map to a known type.
1573 """
1574 """
1574 lctest = test.lower()
1575 lctest = test.lower()
1575 testcls = Test
1576 testcls = Test
1576
1577
1577 for ext, cls in self.TESTTYPES:
1578 for ext, cls in self.TESTTYPES:
1578 if lctest.endswith(ext):
1579 if lctest.endswith(ext):
1579 testcls = cls
1580 testcls = cls
1580 break
1581 break
1581
1582
1582 refpath = os.path.join(self._testdir, test)
1583 refpath = os.path.join(self._testdir, test)
1583 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1584 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1584
1585
1585 return testcls(refpath, tmpdir,
1586 return testcls(refpath, tmpdir,
1586 keeptmpdir=self.options.keep_tmpdir,
1587 keeptmpdir=self.options.keep_tmpdir,
1587 debug=self.options.debug,
1588 debug=self.options.debug,
1588 timeout=self.options.timeout,
1589 timeout=self.options.timeout,
1589 startport=self.options.port + count * 3,
1590 startport=self.options.port + count * 3,
1590 extraconfigopts=self.options.extra_config_opt,
1591 extraconfigopts=self.options.extra_config_opt,
1591 py3kwarnings=self.options.py3k_warnings,
1592 py3kwarnings=self.options.py3k_warnings,
1592 shell=self.options.shell)
1593 shell=self.options.shell)
1593
1594
1594 def _cleanup(self):
1595 def _cleanup(self):
1595 """Clean up state from this test invocation."""
1596 """Clean up state from this test invocation."""
1596
1597
1597 if self.options.keep_tmpdir:
1598 if self.options.keep_tmpdir:
1598 return
1599 return
1599
1600
1600 vlog("# Cleaning up HGTMP", self._hgtmp)
1601 vlog("# Cleaning up HGTMP", self._hgtmp)
1601 shutil.rmtree(self._hgtmp, True)
1602 shutil.rmtree(self._hgtmp, True)
1602 for f in self._createdfiles:
1603 for f in self._createdfiles:
1603 try:
1604 try:
1604 os.remove(f)
1605 os.remove(f)
1605 except OSError:
1606 except OSError:
1606 pass
1607 pass
1607
1608
1608 def _usecorrectpython(self):
1609 def _usecorrectpython(self):
1609 """Configure the environment to use the appropriate Python in tests."""
1610 """Configure the environment to use the appropriate Python in tests."""
1610 # Tests must use the same interpreter as us or bad things will happen.
1611 # Tests must use the same interpreter as us or bad things will happen.
1611 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1612 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1612 if getattr(os, 'symlink', None):
1613 if getattr(os, 'symlink', None):
1613 vlog("# Making python executable in test path a symlink to '%s'" %
1614 vlog("# Making python executable in test path a symlink to '%s'" %
1614 sys.executable)
1615 sys.executable)
1615 mypython = os.path.join(self._tmpbindir, pyexename)
1616 mypython = os.path.join(self._tmpbindir, pyexename)
1616 try:
1617 try:
1617 if os.readlink(mypython) == sys.executable:
1618 if os.readlink(mypython) == sys.executable:
1618 return
1619 return
1619 os.unlink(mypython)
1620 os.unlink(mypython)
1620 except OSError, err:
1621 except OSError, err:
1621 if err.errno != errno.ENOENT:
1622 if err.errno != errno.ENOENT:
1622 raise
1623 raise
1623 if self._findprogram(pyexename) != sys.executable:
1624 if self._findprogram(pyexename) != sys.executable:
1624 try:
1625 try:
1625 os.symlink(sys.executable, mypython)
1626 os.symlink(sys.executable, mypython)
1626 self._createdfiles.append(mypython)
1627 self._createdfiles.append(mypython)
1627 except OSError, err:
1628 except OSError, err:
1628 # child processes may race, which is harmless
1629 # child processes may race, which is harmless
1629 if err.errno != errno.EEXIST:
1630 if err.errno != errno.EEXIST:
1630 raise
1631 raise
1631 else:
1632 else:
1632 exedir, exename = os.path.split(sys.executable)
1633 exedir, exename = os.path.split(sys.executable)
1633 vlog("# Modifying search path to find %s as %s in '%s'" %
1634 vlog("# Modifying search path to find %s as %s in '%s'" %
1634 (exename, pyexename, exedir))
1635 (exename, pyexename, exedir))
1635 path = os.environ['PATH'].split(os.pathsep)
1636 path = os.environ['PATH'].split(os.pathsep)
1636 while exedir in path:
1637 while exedir in path:
1637 path.remove(exedir)
1638 path.remove(exedir)
1638 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1639 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1639 if not self._findprogram(pyexename):
1640 if not self._findprogram(pyexename):
1640 print "WARNING: Cannot find %s in search path" % pyexename
1641 print "WARNING: Cannot find %s in search path" % pyexename
1641
1642
1642 def _installhg(self):
1643 def _installhg(self):
1643 """Install hg into the test environment.
1644 """Install hg into the test environment.
1644
1645
1645 This will also configure hg with the appropriate testing settings.
1646 This will also configure hg with the appropriate testing settings.
1646 """
1647 """
1647 vlog("# Performing temporary installation of HG")
1648 vlog("# Performing temporary installation of HG")
1648 installerrs = os.path.join("tests", "install.err")
1649 installerrs = os.path.join("tests", "install.err")
1649 compiler = ''
1650 compiler = ''
1650 if self.options.compiler:
1651 if self.options.compiler:
1651 compiler = '--compiler ' + self.options.compiler
1652 compiler = '--compiler ' + self.options.compiler
1652 pure = self.options.pure and "--pure" or ""
1653 pure = self.options.pure and "--pure" or ""
1653 py3 = ''
1654 py3 = ''
1654 if sys.version_info[0] == 3:
1655 if sys.version_info[0] == 3:
1655 py3 = '--c2to3'
1656 py3 = '--c2to3'
1656
1657
1657 # Run installer in hg root
1658 # Run installer in hg root
1658 script = os.path.realpath(sys.argv[0])
1659 script = os.path.realpath(sys.argv[0])
1659 hgroot = os.path.dirname(os.path.dirname(script))
1660 hgroot = os.path.dirname(os.path.dirname(script))
1660 os.chdir(hgroot)
1661 os.chdir(hgroot)
1661 nohome = '--home=""'
1662 nohome = '--home=""'
1662 if os.name == 'nt':
1663 if os.name == 'nt':
1663 # The --home="" trick works only on OS where os.sep == '/'
1664 # The --home="" trick works only on OS where os.sep == '/'
1664 # because of a distutils convert_path() fast-path. Avoid it at
1665 # because of a distutils convert_path() fast-path. Avoid it at
1665 # least on Windows for now, deal with .pydistutils.cfg bugs
1666 # least on Windows for now, deal with .pydistutils.cfg bugs
1666 # when they happen.
1667 # when they happen.
1667 nohome = ''
1668 nohome = ''
1668 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1669 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1669 ' build %(compiler)s --build-base="%(base)s"'
1670 ' build %(compiler)s --build-base="%(base)s"'
1670 ' install --force --prefix="%(prefix)s"'
1671 ' install --force --prefix="%(prefix)s"'
1671 ' --install-lib="%(libdir)s"'
1672 ' --install-lib="%(libdir)s"'
1672 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1673 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1673 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1674 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1674 'compiler': compiler,
1675 'compiler': compiler,
1675 'base': os.path.join(self._hgtmp, "build"),
1676 'base': os.path.join(self._hgtmp, "build"),
1676 'prefix': self._installdir, 'libdir': self._pythondir,
1677 'prefix': self._installdir, 'libdir': self._pythondir,
1677 'bindir': self._bindir,
1678 'bindir': self._bindir,
1678 'nohome': nohome, 'logfile': installerrs})
1679 'nohome': nohome, 'logfile': installerrs})
1679 vlog("# Running", cmd)
1680 vlog("# Running", cmd)
1680 if os.system(cmd) == 0:
1681 if os.system(cmd) == 0:
1681 if not self.options.verbose:
1682 if not self.options.verbose:
1682 os.remove(installerrs)
1683 os.remove(installerrs)
1683 else:
1684 else:
1684 f = open(installerrs)
1685 f = open(installerrs)
1685 for line in f:
1686 for line in f:
1686 print line,
1687 print line,
1687 f.close()
1688 f.close()
1688 sys.exit(1)
1689 sys.exit(1)
1689 os.chdir(self._testdir)
1690 os.chdir(self._testdir)
1690
1691
1691 self._usecorrectpython()
1692 self._usecorrectpython()
1692
1693
1693 if self.options.py3k_warnings and not self.options.anycoverage:
1694 if self.options.py3k_warnings and not self.options.anycoverage:
1694 vlog("# Updating hg command to enable Py3k Warnings switch")
1695 vlog("# Updating hg command to enable Py3k Warnings switch")
1695 f = open(os.path.join(self._bindir, 'hg'), 'r')
1696 f = open(os.path.join(self._bindir, 'hg'), 'r')
1696 lines = [line.rstrip() for line in f]
1697 lines = [line.rstrip() for line in f]
1697 lines[0] += ' -3'
1698 lines[0] += ' -3'
1698 f.close()
1699 f.close()
1699 f = open(os.path.join(self._bindir, 'hg'), 'w')
1700 f = open(os.path.join(self._bindir, 'hg'), 'w')
1700 for line in lines:
1701 for line in lines:
1701 f.write(line + '\n')
1702 f.write(line + '\n')
1702 f.close()
1703 f.close()
1703
1704
1704 hgbat = os.path.join(self._bindir, 'hg.bat')
1705 hgbat = os.path.join(self._bindir, 'hg.bat')
1705 if os.path.isfile(hgbat):
1706 if os.path.isfile(hgbat):
1706 # hg.bat expects to be put in bin/scripts while run-tests.py
1707 # hg.bat expects to be put in bin/scripts while run-tests.py
1707 # installation layout put it in bin/ directly. Fix it
1708 # installation layout put it in bin/ directly. Fix it
1708 f = open(hgbat, 'rb')
1709 f = open(hgbat, 'rb')
1709 data = f.read()
1710 data = f.read()
1710 f.close()
1711 f.close()
1711 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1712 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1712 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1713 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1713 '"%~dp0python" "%~dp0hg" %*')
1714 '"%~dp0python" "%~dp0hg" %*')
1714 f = open(hgbat, 'wb')
1715 f = open(hgbat, 'wb')
1715 f.write(data)
1716 f.write(data)
1716 f.close()
1717 f.close()
1717 else:
1718 else:
1718 print 'WARNING: cannot fix hg.bat reference to python.exe'
1719 print 'WARNING: cannot fix hg.bat reference to python.exe'
1719
1720
1720 if self.options.anycoverage:
1721 if self.options.anycoverage:
1721 custom = os.path.join(self._testdir, 'sitecustomize.py')
1722 custom = os.path.join(self._testdir, 'sitecustomize.py')
1722 target = os.path.join(self._pythondir, 'sitecustomize.py')
1723 target = os.path.join(self._pythondir, 'sitecustomize.py')
1723 vlog('# Installing coverage trigger to %s' % target)
1724 vlog('# Installing coverage trigger to %s' % target)
1724 shutil.copyfile(custom, target)
1725 shutil.copyfile(custom, target)
1725 rc = os.path.join(self._testdir, '.coveragerc')
1726 rc = os.path.join(self._testdir, '.coveragerc')
1726 vlog('# Installing coverage rc to %s' % rc)
1727 vlog('# Installing coverage rc to %s' % rc)
1727 os.environ['COVERAGE_PROCESS_START'] = rc
1728 os.environ['COVERAGE_PROCESS_START'] = rc
1728 fn = os.path.join(self._installdir, '..', '.coverage')
1729 fn = os.path.join(self._installdir, '..', '.coverage')
1729 os.environ['COVERAGE_FILE'] = fn
1730 os.environ['COVERAGE_FILE'] = fn
1730
1731
1731 def _checkhglib(self, verb):
1732 def _checkhglib(self, verb):
1732 """Ensure that the 'mercurial' package imported by python is
1733 """Ensure that the 'mercurial' package imported by python is
1733 the one we expect it to be. If not, print a warning to stderr."""
1734 the one we expect it to be. If not, print a warning to stderr."""
1734 if ((self._bindir == self._pythondir) and
1735 if ((self._bindir == self._pythondir) and
1735 (self._bindir != self._tmpbindir)):
1736 (self._bindir != self._tmpbindir)):
1736 # The pythondir has been infered from --with-hg flag.
1737 # The pythondir has been infered from --with-hg flag.
1737 # We cannot expect anything sensible here
1738 # We cannot expect anything sensible here
1738 return
1739 return
1739 expecthg = os.path.join(self._pythondir, 'mercurial')
1740 expecthg = os.path.join(self._pythondir, 'mercurial')
1740 actualhg = self._gethgpath()
1741 actualhg = self._gethgpath()
1741 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1742 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1742 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1743 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1743 ' (expected %s)\n'
1744 ' (expected %s)\n'
1744 % (verb, actualhg, expecthg))
1745 % (verb, actualhg, expecthg))
1745 def _gethgpath(self):
1746 def _gethgpath(self):
1746 """Return the path to the mercurial package that is actually found by
1747 """Return the path to the mercurial package that is actually found by
1747 the current Python interpreter."""
1748 the current Python interpreter."""
1748 if self._hgpath is not None:
1749 if self._hgpath is not None:
1749 return self._hgpath
1750 return self._hgpath
1750
1751
1751 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1752 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1752 pipe = os.popen(cmd % PYTHON)
1753 pipe = os.popen(cmd % PYTHON)
1753 try:
1754 try:
1754 self._hgpath = pipe.read().strip()
1755 self._hgpath = pipe.read().strip()
1755 finally:
1756 finally:
1756 pipe.close()
1757 pipe.close()
1757
1758
1758 return self._hgpath
1759 return self._hgpath
1759
1760
1760 def _outputcoverage(self):
1761 def _outputcoverage(self):
1761 """Produce code coverage output."""
1762 """Produce code coverage output."""
1762 vlog('# Producing coverage report')
1763 vlog('# Producing coverage report')
1763 os.chdir(self._pythondir)
1764 os.chdir(self._pythondir)
1764
1765
1765 def covrun(*args):
1766 def covrun(*args):
1766 cmd = 'coverage %s' % ' '.join(args)
1767 cmd = 'coverage %s' % ' '.join(args)
1767 vlog('# Running: %s' % cmd)
1768 vlog('# Running: %s' % cmd)
1768 os.system(cmd)
1769 os.system(cmd)
1769
1770
1770 covrun('-c')
1771 covrun('-c')
1771 omit = ','.join(os.path.join(x, '*') for x in
1772 omit = ','.join(os.path.join(x, '*') for x in
1772 [self._bindir, self._testdir])
1773 [self._bindir, self._testdir])
1773 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1774 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1774 if self.options.htmlcov:
1775 if self.options.htmlcov:
1775 htmldir = os.path.join(self._testdir, 'htmlcov')
1776 htmldir = os.path.join(self._testdir, 'htmlcov')
1776 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1777 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1777 '"--omit=%s"' % omit)
1778 '"--omit=%s"' % omit)
1778 if self.options.annotate:
1779 if self.options.annotate:
1779 adir = os.path.join(self._testdir, 'annotated')
1780 adir = os.path.join(self._testdir, 'annotated')
1780 if not os.path.isdir(adir):
1781 if not os.path.isdir(adir):
1781 os.mkdir(adir)
1782 os.mkdir(adir)
1782 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1783 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1783
1784
1784 def _findprogram(self, program):
1785 def _findprogram(self, program):
1785 """Search PATH for a executable program"""
1786 """Search PATH for a executable program"""
1786 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1787 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1787 name = os.path.join(p, program)
1788 name = os.path.join(p, program)
1788 if os.name == 'nt' or os.access(name, os.X_OK):
1789 if os.name == 'nt' or os.access(name, os.X_OK):
1789 return name
1790 return name
1790 return None
1791 return None
1791
1792
1792 def _checktools(self):
1793 def _checktools(self):
1793 """Ensure tools required to run tests are present."""
1794 """Ensure tools required to run tests are present."""
1794 for p in self.REQUIREDTOOLS:
1795 for p in self.REQUIREDTOOLS:
1795 if os.name == 'nt' and not p.endswith('.exe'):
1796 if os.name == 'nt' and not p.endswith('.exe'):
1796 p += '.exe'
1797 p += '.exe'
1797 found = self._findprogram(p)
1798 found = self._findprogram(p)
1798 if found:
1799 if found:
1799 vlog("# Found prerequisite", p, "at", found)
1800 vlog("# Found prerequisite", p, "at", found)
1800 else:
1801 else:
1801 print "WARNING: Did not find prerequisite tool: %s " % p
1802 print "WARNING: Did not find prerequisite tool: %s " % p
1802
1803
1803 if __name__ == '__main__':
1804 if __name__ == '__main__':
1804 runner = TestRunner()
1805 runner = TestRunner()
1805 sys.exit(runner.run(sys.argv[1:]))
1806 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now