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