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