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