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