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