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