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