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