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