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