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