##// END OF EJS Templates
run-tests: exit with non-0 exit code when tests fail or warn...
Gregory Szorc -
r21613:b3213b9f default
parent child Browse files
Show More
@@ -1,1785 +1,1792 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 # The result object handles diff calculation for us.
533 # The result object handles diff calculation for us.
534 self._result.addOutputMismatch(self, ret, out, self._refout)
534 self._result.addOutputMismatch(self, ret, out, self._refout)
535
535
536 if ret:
536 if ret:
537 msg = 'output changed and ' + describe(ret)
537 msg = 'output changed and ' + describe(ret)
538 else:
538 else:
539 msg = 'output changed'
539 msg = 'output changed'
540
540
541 if (ret != 0 or out != self._refout) and not self._skipped \
541 if (ret != 0 or out != self._refout) and not self._skipped \
542 and not self._debug:
542 and not self._debug:
543 f = open(self.errpath, 'wb')
543 f = open(self.errpath, 'wb')
544 for line in out:
544 for line in out:
545 f.write(line)
545 f.write(line)
546 f.close()
546 f.close()
547
547
548 self.fail(msg)
548 self.fail(msg)
549 elif ret:
549 elif ret:
550 self.fail(describe(ret))
550 self.fail(describe(ret))
551
551
552 def tearDown(self):
552 def tearDown(self):
553 """Tasks to perform after run()."""
553 """Tasks to perform after run()."""
554 for entry in self._daemonpids:
554 for entry in self._daemonpids:
555 killdaemons(entry)
555 killdaemons(entry)
556 self._daemonpids = []
556 self._daemonpids = []
557
557
558 if not self._keeptmpdir:
558 if not self._keeptmpdir:
559 shutil.rmtree(self._testtmp, True)
559 shutil.rmtree(self._testtmp, True)
560 shutil.rmtree(self._threadtmp, True)
560 shutil.rmtree(self._threadtmp, True)
561
561
562 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
562 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
563 and not self._debug and self._out:
563 and not self._debug and self._out:
564 f = open(self.errpath, 'wb')
564 f = open(self.errpath, 'wb')
565 for line in self._out:
565 for line in self._out:
566 f.write(line)
566 f.write(line)
567 f.close()
567 f.close()
568
568
569 vlog("# Ret was:", self._ret)
569 vlog("# Ret was:", self._ret)
570
570
571 def _run(self, replacements, env):
571 def _run(self, replacements, env):
572 # This should be implemented in child classes to run tests.
572 # This should be implemented in child classes to run tests.
573 raise SkipTest('unknown test type')
573 raise SkipTest('unknown test type')
574
574
575 def abort(self):
575 def abort(self):
576 """Terminate execution of this test."""
576 """Terminate execution of this test."""
577 self._aborted = True
577 self._aborted = True
578
578
579 def _getreplacements(self):
579 def _getreplacements(self):
580 """Obtain a mapping of text replacements to apply to test output.
580 """Obtain a mapping of text replacements to apply to test output.
581
581
582 Test output needs to be normalized so it can be compared to expected
582 Test output needs to be normalized so it can be compared to expected
583 output. This function defines how some of that normalization will
583 output. This function defines how some of that normalization will
584 occur.
584 occur.
585 """
585 """
586 r = [
586 r = [
587 (r':%s\b' % self._startport, ':$HGPORT'),
587 (r':%s\b' % self._startport, ':$HGPORT'),
588 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
588 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
589 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
589 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
590 ]
590 ]
591
591
592 if os.name == 'nt':
592 if os.name == 'nt':
593 r.append(
593 r.append(
594 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
594 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
595 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
595 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
596 for c in self._testtmp), '$TESTTMP'))
596 for c in self._testtmp), '$TESTTMP'))
597 else:
597 else:
598 r.append((re.escape(self._testtmp), '$TESTTMP'))
598 r.append((re.escape(self._testtmp), '$TESTTMP'))
599
599
600 return r
600 return r
601
601
602 def _getenv(self):
602 def _getenv(self):
603 """Obtain environment variables to use during test execution."""
603 """Obtain environment variables to use during test execution."""
604 env = os.environ.copy()
604 env = os.environ.copy()
605 env['TESTTMP'] = self._testtmp
605 env['TESTTMP'] = self._testtmp
606 env['HOME'] = self._testtmp
606 env['HOME'] = self._testtmp
607 env["HGPORT"] = str(self._startport)
607 env["HGPORT"] = str(self._startport)
608 env["HGPORT1"] = str(self._startport + 1)
608 env["HGPORT1"] = str(self._startport + 1)
609 env["HGPORT2"] = str(self._startport + 2)
609 env["HGPORT2"] = str(self._startport + 2)
610 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
610 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
611 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
611 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
612 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
612 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
613 env["HGMERGE"] = "internal:merge"
613 env["HGMERGE"] = "internal:merge"
614 env["HGUSER"] = "test"
614 env["HGUSER"] = "test"
615 env["HGENCODING"] = "ascii"
615 env["HGENCODING"] = "ascii"
616 env["HGENCODINGMODE"] = "strict"
616 env["HGENCODINGMODE"] = "strict"
617
617
618 # Reset some environment variables to well-known values so that
618 # Reset some environment variables to well-known values so that
619 # the tests produce repeatable output.
619 # the tests produce repeatable output.
620 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
620 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
621 env['TZ'] = 'GMT'
621 env['TZ'] = 'GMT'
622 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
622 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
623 env['COLUMNS'] = '80'
623 env['COLUMNS'] = '80'
624 env['TERM'] = 'xterm'
624 env['TERM'] = 'xterm'
625
625
626 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
626 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
627 'NO_PROXY').split():
627 'NO_PROXY').split():
628 if k in env:
628 if k in env:
629 del env[k]
629 del env[k]
630
630
631 # unset env related to hooks
631 # unset env related to hooks
632 for k in env.keys():
632 for k in env.keys():
633 if k.startswith('HG_'):
633 if k.startswith('HG_'):
634 del env[k]
634 del env[k]
635
635
636 return env
636 return env
637
637
638 def _createhgrc(self, path):
638 def _createhgrc(self, path):
639 """Create an hgrc file for this test."""
639 """Create an hgrc file for this test."""
640 hgrc = open(path, 'w')
640 hgrc = open(path, 'w')
641 hgrc.write('[ui]\n')
641 hgrc.write('[ui]\n')
642 hgrc.write('slash = True\n')
642 hgrc.write('slash = True\n')
643 hgrc.write('interactive = False\n')
643 hgrc.write('interactive = False\n')
644 hgrc.write('[defaults]\n')
644 hgrc.write('[defaults]\n')
645 hgrc.write('backout = -d "0 0"\n')
645 hgrc.write('backout = -d "0 0"\n')
646 hgrc.write('commit = -d "0 0"\n')
646 hgrc.write('commit = -d "0 0"\n')
647 hgrc.write('shelve = --date "0 0"\n')
647 hgrc.write('shelve = --date "0 0"\n')
648 hgrc.write('tag = -d "0 0"\n')
648 hgrc.write('tag = -d "0 0"\n')
649 for opt in self._extraconfigopts:
649 for opt in self._extraconfigopts:
650 section, key = opt.split('.', 1)
650 section, key = opt.split('.', 1)
651 assert '=' in key, ('extra config opt %s must '
651 assert '=' in key, ('extra config opt %s must '
652 'have an = for assignment' % opt)
652 'have an = for assignment' % opt)
653 hgrc.write('[%s]\n%s\n' % (section, key))
653 hgrc.write('[%s]\n%s\n' % (section, key))
654 hgrc.close()
654 hgrc.close()
655
655
656 def fail(self, msg):
656 def fail(self, msg):
657 # unittest differentiates between errored and failed.
657 # unittest differentiates between errored and failed.
658 # Failed is denoted by AssertionError (by default at least).
658 # Failed is denoted by AssertionError (by default at least).
659 raise AssertionError(msg)
659 raise AssertionError(msg)
660
660
661 class PythonTest(Test):
661 class PythonTest(Test):
662 """A Python-based test."""
662 """A Python-based test."""
663
663
664 @property
664 @property
665 def refpath(self):
665 def refpath(self):
666 return os.path.join(self._testdir, '%s.out' % self.name)
666 return os.path.join(self._testdir, '%s.out' % self.name)
667
667
668 def _run(self, replacements, env):
668 def _run(self, replacements, env):
669 py3kswitch = self._py3kwarnings and ' -3' or ''
669 py3kswitch = self._py3kwarnings and ' -3' or ''
670 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
670 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
671 vlog("# Running", cmd)
671 vlog("# Running", cmd)
672 if os.name == 'nt':
672 if os.name == 'nt':
673 replacements.append((r'\r\n', '\n'))
673 replacements.append((r'\r\n', '\n'))
674 result = run(cmd, self._testtmp, replacements, env,
674 result = run(cmd, self._testtmp, replacements, env,
675 debug=self._debug, timeout=self._timeout)
675 debug=self._debug, timeout=self._timeout)
676 if self._aborted:
676 if self._aborted:
677 raise KeyboardInterrupt()
677 raise KeyboardInterrupt()
678
678
679 return result
679 return result
680
680
681 class TTest(Test):
681 class TTest(Test):
682 """A "t test" is a test backed by a .t file."""
682 """A "t test" is a test backed by a .t file."""
683
683
684 SKIPPED_PREFIX = 'skipped: '
684 SKIPPED_PREFIX = 'skipped: '
685 FAILED_PREFIX = 'hghave check failed: '
685 FAILED_PREFIX = 'hghave check failed: '
686 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
686 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
687
687
688 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
688 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))
689 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
690 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
690 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
691
691
692 @property
692 @property
693 def refpath(self):
693 def refpath(self):
694 return os.path.join(self._testdir, self.name)
694 return os.path.join(self._testdir, self.name)
695
695
696 def _run(self, replacements, env):
696 def _run(self, replacements, env):
697 f = open(self.path)
697 f = open(self.path)
698 lines = f.readlines()
698 lines = f.readlines()
699 f.close()
699 f.close()
700
700
701 salt, script, after, expected = self._parsetest(lines)
701 salt, script, after, expected = self._parsetest(lines)
702
702
703 # Write out the generated script.
703 # Write out the generated script.
704 fname = '%s.sh' % self._testtmp
704 fname = '%s.sh' % self._testtmp
705 f = open(fname, 'w')
705 f = open(fname, 'w')
706 for l in script:
706 for l in script:
707 f.write(l)
707 f.write(l)
708 f.close()
708 f.close()
709
709
710 cmd = '%s "%s"' % (self._shell, fname)
710 cmd = '%s "%s"' % (self._shell, fname)
711 vlog("# Running", cmd)
711 vlog("# Running", cmd)
712
712
713 exitcode, output = run(cmd, self._testtmp, replacements, env,
713 exitcode, output = run(cmd, self._testtmp, replacements, env,
714 debug=self._debug, timeout=self._timeout)
714 debug=self._debug, timeout=self._timeout)
715
715
716 if self._aborted:
716 if self._aborted:
717 raise KeyboardInterrupt()
717 raise KeyboardInterrupt()
718
718
719 # Do not merge output if skipped. Return hghave message instead.
719 # Do not merge output if skipped. Return hghave message instead.
720 # Similarly, with --debug, output is None.
720 # Similarly, with --debug, output is None.
721 if exitcode == self.SKIPPED_STATUS or output is None:
721 if exitcode == self.SKIPPED_STATUS or output is None:
722 return exitcode, output
722 return exitcode, output
723
723
724 return self._processoutput(exitcode, output, salt, after, expected)
724 return self._processoutput(exitcode, output, salt, after, expected)
725
725
726 def _hghave(self, reqs):
726 def _hghave(self, reqs):
727 # TODO do something smarter when all other uses of hghave are gone.
727 # TODO do something smarter when all other uses of hghave are gone.
728 tdir = self._testdir.replace('\\', '/')
728 tdir = self._testdir.replace('\\', '/')
729 proc = Popen4('%s -c "%s/hghave %s"' %
729 proc = Popen4('%s -c "%s/hghave %s"' %
730 (self._shell, tdir, ' '.join(reqs)),
730 (self._shell, tdir, ' '.join(reqs)),
731 self._testtmp, 0)
731 self._testtmp, 0)
732 stdout, stderr = proc.communicate()
732 stdout, stderr = proc.communicate()
733 ret = proc.wait()
733 ret = proc.wait()
734 if wifexited(ret):
734 if wifexited(ret):
735 ret = os.WEXITSTATUS(ret)
735 ret = os.WEXITSTATUS(ret)
736 if ret == 2:
736 if ret == 2:
737 print stdout
737 print stdout
738 sys.exit(1)
738 sys.exit(1)
739
739
740 return ret == 0
740 return ret == 0
741
741
742 def _parsetest(self, lines):
742 def _parsetest(self, lines):
743 # We generate a shell script which outputs unique markers to line
743 # We generate a shell script which outputs unique markers to line
744 # up script results with our source. These markers include input
744 # up script results with our source. These markers include input
745 # line number and the last return code.
745 # line number and the last return code.
746 salt = "SALT" + str(time.time())
746 salt = "SALT" + str(time.time())
747 def addsalt(line, inpython):
747 def addsalt(line, inpython):
748 if inpython:
748 if inpython:
749 script.append('%s %d 0\n' % (salt, line))
749 script.append('%s %d 0\n' % (salt, line))
750 else:
750 else:
751 script.append('echo %s %s $?\n' % (salt, line))
751 script.append('echo %s %s $?\n' % (salt, line))
752
752
753 script = []
753 script = []
754
754
755 # After we run the shell script, we re-unify the script output
755 # After we run the shell script, we re-unify the script output
756 # with non-active parts of the source, with synchronization by our
756 # with non-active parts of the source, with synchronization by our
757 # SALT line number markers. The after table contains the non-active
757 # SALT line number markers. The after table contains the non-active
758 # components, ordered by line number.
758 # components, ordered by line number.
759 after = {}
759 after = {}
760
760
761 # Expected shell script output.
761 # Expected shell script output.
762 expected = {}
762 expected = {}
763
763
764 pos = prepos = -1
764 pos = prepos = -1
765
765
766 # True or False when in a true or false conditional section
766 # True or False when in a true or false conditional section
767 skipping = None
767 skipping = None
768
768
769 # We keep track of whether or not we're in a Python block so we
769 # We keep track of whether or not we're in a Python block so we
770 # can generate the surrounding doctest magic.
770 # can generate the surrounding doctest magic.
771 inpython = False
771 inpython = False
772
772
773 if self._debug:
773 if self._debug:
774 script.append('set -x\n')
774 script.append('set -x\n')
775 if os.getenv('MSYSTEM'):
775 if os.getenv('MSYSTEM'):
776 script.append('alias pwd="pwd -W"\n')
776 script.append('alias pwd="pwd -W"\n')
777
777
778 for n, l in enumerate(lines):
778 for n, l in enumerate(lines):
779 if not l.endswith('\n'):
779 if not l.endswith('\n'):
780 l += '\n'
780 l += '\n'
781 if l.startswith('#if'):
781 if l.startswith('#if'):
782 lsplit = l.split()
782 lsplit = l.split()
783 if len(lsplit) < 2 or lsplit[0] != '#if':
783 if len(lsplit) < 2 or lsplit[0] != '#if':
784 after.setdefault(pos, []).append(' !!! invalid #if\n')
784 after.setdefault(pos, []).append(' !!! invalid #if\n')
785 if skipping is not None:
785 if skipping is not None:
786 after.setdefault(pos, []).append(' !!! nested #if\n')
786 after.setdefault(pos, []).append(' !!! nested #if\n')
787 skipping = not self._hghave(lsplit[1:])
787 skipping = not self._hghave(lsplit[1:])
788 after.setdefault(pos, []).append(l)
788 after.setdefault(pos, []).append(l)
789 elif l.startswith('#else'):
789 elif l.startswith('#else'):
790 if skipping is None:
790 if skipping is None:
791 after.setdefault(pos, []).append(' !!! missing #if\n')
791 after.setdefault(pos, []).append(' !!! missing #if\n')
792 skipping = not skipping
792 skipping = not skipping
793 after.setdefault(pos, []).append(l)
793 after.setdefault(pos, []).append(l)
794 elif l.startswith('#endif'):
794 elif l.startswith('#endif'):
795 if skipping is None:
795 if skipping is None:
796 after.setdefault(pos, []).append(' !!! missing #if\n')
796 after.setdefault(pos, []).append(' !!! missing #if\n')
797 skipping = None
797 skipping = None
798 after.setdefault(pos, []).append(l)
798 after.setdefault(pos, []).append(l)
799 elif skipping:
799 elif skipping:
800 after.setdefault(pos, []).append(l)
800 after.setdefault(pos, []).append(l)
801 elif l.startswith(' >>> '): # python inlines
801 elif l.startswith(' >>> '): # python inlines
802 after.setdefault(pos, []).append(l)
802 after.setdefault(pos, []).append(l)
803 prepos = pos
803 prepos = pos
804 pos = n
804 pos = n
805 if not inpython:
805 if not inpython:
806 # We've just entered a Python block. Add the header.
806 # We've just entered a Python block. Add the header.
807 inpython = True
807 inpython = True
808 addsalt(prepos, False) # Make sure we report the exit code.
808 addsalt(prepos, False) # Make sure we report the exit code.
809 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
809 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
810 addsalt(n, True)
810 addsalt(n, True)
811 script.append(l[2:])
811 script.append(l[2:])
812 elif l.startswith(' ... '): # python inlines
812 elif l.startswith(' ... '): # python inlines
813 after.setdefault(prepos, []).append(l)
813 after.setdefault(prepos, []).append(l)
814 script.append(l[2:])
814 script.append(l[2:])
815 elif l.startswith(' $ '): # commands
815 elif l.startswith(' $ '): # commands
816 if inpython:
816 if inpython:
817 script.append('EOF\n')
817 script.append('EOF\n')
818 inpython = False
818 inpython = False
819 after.setdefault(pos, []).append(l)
819 after.setdefault(pos, []).append(l)
820 prepos = pos
820 prepos = pos
821 pos = n
821 pos = n
822 addsalt(n, False)
822 addsalt(n, False)
823 cmd = l[4:].split()
823 cmd = l[4:].split()
824 if len(cmd) == 2 and cmd[0] == 'cd':
824 if len(cmd) == 2 and cmd[0] == 'cd':
825 l = ' $ cd %s || exit 1\n' % cmd[1]
825 l = ' $ cd %s || exit 1\n' % cmd[1]
826 script.append(l[4:])
826 script.append(l[4:])
827 elif l.startswith(' > '): # continuations
827 elif l.startswith(' > '): # continuations
828 after.setdefault(prepos, []).append(l)
828 after.setdefault(prepos, []).append(l)
829 script.append(l[4:])
829 script.append(l[4:])
830 elif l.startswith(' '): # results
830 elif l.startswith(' '): # results
831 # Queue up a list of expected results.
831 # Queue up a list of expected results.
832 expected.setdefault(pos, []).append(l[2:])
832 expected.setdefault(pos, []).append(l[2:])
833 else:
833 else:
834 if inpython:
834 if inpython:
835 script.append('EOF\n')
835 script.append('EOF\n')
836 inpython = False
836 inpython = False
837 # Non-command/result. Queue up for merged output.
837 # Non-command/result. Queue up for merged output.
838 after.setdefault(pos, []).append(l)
838 after.setdefault(pos, []).append(l)
839
839
840 if inpython:
840 if inpython:
841 script.append('EOF\n')
841 script.append('EOF\n')
842 if skipping is not None:
842 if skipping is not None:
843 after.setdefault(pos, []).append(' !!! missing #endif\n')
843 after.setdefault(pos, []).append(' !!! missing #endif\n')
844 addsalt(n + 1, False)
844 addsalt(n + 1, False)
845
845
846 return salt, script, after, expected
846 return salt, script, after, expected
847
847
848 def _processoutput(self, exitcode, output, salt, after, expected):
848 def _processoutput(self, exitcode, output, salt, after, expected):
849 # Merge the script output back into a unified test.
849 # Merge the script output back into a unified test.
850 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
850 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
851 if exitcode != 0:
851 if exitcode != 0:
852 warnonly = 3
852 warnonly = 3
853
853
854 pos = -1
854 pos = -1
855 postout = []
855 postout = []
856 for l in output:
856 for l in output:
857 lout, lcmd = l, None
857 lout, lcmd = l, None
858 if salt in l:
858 if salt in l:
859 lout, lcmd = l.split(salt, 1)
859 lout, lcmd = l.split(salt, 1)
860
860
861 if lout:
861 if lout:
862 if not lout.endswith('\n'):
862 if not lout.endswith('\n'):
863 lout += ' (no-eol)\n'
863 lout += ' (no-eol)\n'
864
864
865 # Find the expected output at the current position.
865 # Find the expected output at the current position.
866 el = None
866 el = None
867 if expected.get(pos, None):
867 if expected.get(pos, None):
868 el = expected[pos].pop(0)
868 el = expected[pos].pop(0)
869
869
870 r = TTest.linematch(el, lout)
870 r = TTest.linematch(el, lout)
871 if isinstance(r, str):
871 if isinstance(r, str):
872 if r == '+glob':
872 if r == '+glob':
873 lout = el[:-1] + ' (glob)\n'
873 lout = el[:-1] + ' (glob)\n'
874 r = '' # Warn only this line.
874 r = '' # Warn only this line.
875 elif r == '-glob':
875 elif r == '-glob':
876 lout = ''.join(el.rsplit(' (glob)', 1))
876 lout = ''.join(el.rsplit(' (glob)', 1))
877 r = '' # Warn only this line.
877 r = '' # Warn only this line.
878 else:
878 else:
879 log('\ninfo, unknown linematch result: %r\n' % r)
879 log('\ninfo, unknown linematch result: %r\n' % r)
880 r = False
880 r = False
881 if r:
881 if r:
882 postout.append(' ' + el)
882 postout.append(' ' + el)
883 else:
883 else:
884 if self.NEEDESCAPE(lout):
884 if self.NEEDESCAPE(lout):
885 lout = TTest._stringescape('%s (esc)\n' %
885 lout = TTest._stringescape('%s (esc)\n' %
886 lout.rstrip('\n'))
886 lout.rstrip('\n'))
887 postout.append(' ' + lout) # Let diff deal with it.
887 postout.append(' ' + lout) # Let diff deal with it.
888 if r != '': # If line failed.
888 if r != '': # If line failed.
889 warnonly = 3 # for sure not
889 warnonly = 3 # for sure not
890 elif warnonly == 1: # Is "not yet" and line is warn only.
890 elif warnonly == 1: # Is "not yet" and line is warn only.
891 warnonly = 2 # Yes do warn.
891 warnonly = 2 # Yes do warn.
892
892
893 if lcmd:
893 if lcmd:
894 # Add on last return code.
894 # Add on last return code.
895 ret = int(lcmd.split()[1])
895 ret = int(lcmd.split()[1])
896 if ret != 0:
896 if ret != 0:
897 postout.append(' [%s]\n' % ret)
897 postout.append(' [%s]\n' % ret)
898 if pos in after:
898 if pos in after:
899 # Merge in non-active test bits.
899 # Merge in non-active test bits.
900 postout += after.pop(pos)
900 postout += after.pop(pos)
901 pos = int(lcmd.split()[0])
901 pos = int(lcmd.split()[0])
902
902
903 if pos in after:
903 if pos in after:
904 postout += after.pop(pos)
904 postout += after.pop(pos)
905
905
906 if warnonly == 2:
906 if warnonly == 2:
907 exitcode = False # Set exitcode to warned.
907 exitcode = False # Set exitcode to warned.
908
908
909 return exitcode, postout
909 return exitcode, postout
910
910
911 @staticmethod
911 @staticmethod
912 def rematch(el, l):
912 def rematch(el, l):
913 try:
913 try:
914 # use \Z to ensure that the regex matches to the end of the string
914 # use \Z to ensure that the regex matches to the end of the string
915 if os.name == 'nt':
915 if os.name == 'nt':
916 return re.match(el + r'\r?\n\Z', l)
916 return re.match(el + r'\r?\n\Z', l)
917 return re.match(el + r'\n\Z', l)
917 return re.match(el + r'\n\Z', l)
918 except re.error:
918 except re.error:
919 # el is an invalid regex
919 # el is an invalid regex
920 return False
920 return False
921
921
922 @staticmethod
922 @staticmethod
923 def globmatch(el, l):
923 def globmatch(el, l):
924 # The only supported special characters are * and ? plus / which also
924 # The only supported special characters are * and ? plus / which also
925 # matches \ on windows. Escaping of these characters is supported.
925 # matches \ on windows. Escaping of these characters is supported.
926 if el + '\n' == l:
926 if el + '\n' == l:
927 if os.altsep:
927 if os.altsep:
928 # matching on "/" is not needed for this line
928 # matching on "/" is not needed for this line
929 return '-glob'
929 return '-glob'
930 return True
930 return True
931 i, n = 0, len(el)
931 i, n = 0, len(el)
932 res = ''
932 res = ''
933 while i < n:
933 while i < n:
934 c = el[i]
934 c = el[i]
935 i += 1
935 i += 1
936 if c == '\\' and el[i] in '*?\\/':
936 if c == '\\' and el[i] in '*?\\/':
937 res += el[i - 1:i + 1]
937 res += el[i - 1:i + 1]
938 i += 1
938 i += 1
939 elif c == '*':
939 elif c == '*':
940 res += '.*'
940 res += '.*'
941 elif c == '?':
941 elif c == '?':
942 res += '.'
942 res += '.'
943 elif c == '/' and os.altsep:
943 elif c == '/' and os.altsep:
944 res += '[/\\\\]'
944 res += '[/\\\\]'
945 else:
945 else:
946 res += re.escape(c)
946 res += re.escape(c)
947 return TTest.rematch(res, l)
947 return TTest.rematch(res, l)
948
948
949 @staticmethod
949 @staticmethod
950 def linematch(el, l):
950 def linematch(el, l):
951 if el == l: # perfect match (fast)
951 if el == l: # perfect match (fast)
952 return True
952 return True
953 if el:
953 if el:
954 if el.endswith(" (esc)\n"):
954 if el.endswith(" (esc)\n"):
955 el = el[:-7].decode('string-escape') + '\n'
955 el = el[:-7].decode('string-escape') + '\n'
956 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
956 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
957 return True
957 return True
958 if el.endswith(" (re)\n"):
958 if el.endswith(" (re)\n"):
959 return TTest.rematch(el[:-6], l)
959 return TTest.rematch(el[:-6], l)
960 if el.endswith(" (glob)\n"):
960 if el.endswith(" (glob)\n"):
961 return TTest.globmatch(el[:-8], l)
961 return TTest.globmatch(el[:-8], l)
962 if os.altsep and l.replace('\\', '/') == el:
962 if os.altsep and l.replace('\\', '/') == el:
963 return '+glob'
963 return '+glob'
964 return False
964 return False
965
965
966 @staticmethod
966 @staticmethod
967 def parsehghaveoutput(lines):
967 def parsehghaveoutput(lines):
968 '''Parse hghave log lines.
968 '''Parse hghave log lines.
969
969
970 Return tuple of lists (missing, failed):
970 Return tuple of lists (missing, failed):
971 * the missing/unknown features
971 * the missing/unknown features
972 * the features for which existence check failed'''
972 * the features for which existence check failed'''
973 missing = []
973 missing = []
974 failed = []
974 failed = []
975 for line in lines:
975 for line in lines:
976 if line.startswith(TTest.SKIPPED_PREFIX):
976 if line.startswith(TTest.SKIPPED_PREFIX):
977 line = line.splitlines()[0]
977 line = line.splitlines()[0]
978 missing.append(line[len(TTest.SKIPPED_PREFIX):])
978 missing.append(line[len(TTest.SKIPPED_PREFIX):])
979 elif line.startswith(TTest.FAILED_PREFIX):
979 elif line.startswith(TTest.FAILED_PREFIX):
980 line = line.splitlines()[0]
980 line = line.splitlines()[0]
981 failed.append(line[len(TTest.FAILED_PREFIX):])
981 failed.append(line[len(TTest.FAILED_PREFIX):])
982
982
983 return missing, failed
983 return missing, failed
984
984
985 @staticmethod
985 @staticmethod
986 def _escapef(m):
986 def _escapef(m):
987 return TTest.ESCAPEMAP[m.group(0)]
987 return TTest.ESCAPEMAP[m.group(0)]
988
988
989 @staticmethod
989 @staticmethod
990 def _stringescape(s):
990 def _stringescape(s):
991 return TTest.ESCAPESUB(TTest._escapef, s)
991 return TTest.ESCAPESUB(TTest._escapef, s)
992
992
993
993
994 wifexited = getattr(os, "WIFEXITED", lambda x: False)
994 wifexited = getattr(os, "WIFEXITED", lambda x: False)
995 def run(cmd, wd, replacements, env, debug=False, timeout=None):
995 def run(cmd, wd, replacements, env, debug=False, timeout=None):
996 """Run command in a sub-process, capturing the output (stdout and stderr).
996 """Run command in a sub-process, capturing the output (stdout and stderr).
997 Return a tuple (exitcode, output). output is None in debug mode."""
997 Return a tuple (exitcode, output). output is None in debug mode."""
998 if debug:
998 if debug:
999 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
999 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1000 ret = proc.wait()
1000 ret = proc.wait()
1001 return (ret, None)
1001 return (ret, None)
1002
1002
1003 proc = Popen4(cmd, wd, timeout, env)
1003 proc = Popen4(cmd, wd, timeout, env)
1004 def cleanup():
1004 def cleanup():
1005 terminate(proc)
1005 terminate(proc)
1006 ret = proc.wait()
1006 ret = proc.wait()
1007 if ret == 0:
1007 if ret == 0:
1008 ret = signal.SIGTERM << 8
1008 ret = signal.SIGTERM << 8
1009 killdaemons(env['DAEMON_PIDS'])
1009 killdaemons(env['DAEMON_PIDS'])
1010 return ret
1010 return ret
1011
1011
1012 output = ''
1012 output = ''
1013 proc.tochild.close()
1013 proc.tochild.close()
1014
1014
1015 try:
1015 try:
1016 output = proc.fromchild.read()
1016 output = proc.fromchild.read()
1017 except KeyboardInterrupt:
1017 except KeyboardInterrupt:
1018 vlog('# Handling keyboard interrupt')
1018 vlog('# Handling keyboard interrupt')
1019 cleanup()
1019 cleanup()
1020 raise
1020 raise
1021
1021
1022 ret = proc.wait()
1022 ret = proc.wait()
1023 if wifexited(ret):
1023 if wifexited(ret):
1024 ret = os.WEXITSTATUS(ret)
1024 ret = os.WEXITSTATUS(ret)
1025
1025
1026 if proc.timeout:
1026 if proc.timeout:
1027 ret = 'timeout'
1027 ret = 'timeout'
1028
1028
1029 if ret:
1029 if ret:
1030 killdaemons(env['DAEMON_PIDS'])
1030 killdaemons(env['DAEMON_PIDS'])
1031
1031
1032 for s, r in replacements:
1032 for s, r in replacements:
1033 output = re.sub(s, r, output)
1033 output = re.sub(s, r, output)
1034 return ret, output.splitlines(True)
1034 return ret, output.splitlines(True)
1035
1035
1036 iolock = threading.Lock()
1036 iolock = threading.Lock()
1037
1037
1038 class SkipTest(Exception):
1038 class SkipTest(Exception):
1039 """Raised to indicate that a test is to be skipped."""
1039 """Raised to indicate that a test is to be skipped."""
1040
1040
1041 class IgnoreTest(Exception):
1041 class IgnoreTest(Exception):
1042 """Raised to indicate that a test is to be ignored."""
1042 """Raised to indicate that a test is to be ignored."""
1043
1043
1044 class WarnTest(Exception):
1044 class WarnTest(Exception):
1045 """Raised to indicate that a test warned."""
1045 """Raised to indicate that a test warned."""
1046
1046
1047 class TestResult(unittest._TextTestResult):
1047 class TestResult(unittest._TextTestResult):
1048 """Holds results when executing via unittest."""
1048 """Holds results when executing via unittest."""
1049 # Don't worry too much about accessing the non-public _TextTestResult.
1049 # Don't worry too much about accessing the non-public _TextTestResult.
1050 # It is relatively common in Python testing tools.
1050 # It is relatively common in Python testing tools.
1051 def __init__(self, options, *args, **kwargs):
1051 def __init__(self, options, *args, **kwargs):
1052 super(TestResult, self).__init__(*args, **kwargs)
1052 super(TestResult, self).__init__(*args, **kwargs)
1053
1053
1054 self._options = options
1054 self._options = options
1055
1055
1056 # unittest.TestResult didn't have skipped until 2.7. We need to
1056 # unittest.TestResult didn't have skipped until 2.7. We need to
1057 # polyfill it.
1057 # polyfill it.
1058 self.skipped = []
1058 self.skipped = []
1059
1059
1060 # We have a custom "ignored" result that isn't present in any Python
1060 # 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
1061 # unittest implementation. It is very similar to skipped. It may make
1062 # sense to map it into skip some day.
1062 # sense to map it into skip some day.
1063 self.ignored = []
1063 self.ignored = []
1064
1064
1065 # We have a custom "warned" result that isn't present in any Python
1065 # 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
1066 # unittest implementation. It is very similar to failed. It may make
1067 # sense to map it into fail some day.
1067 # sense to map it into fail some day.
1068 self.warned = []
1068 self.warned = []
1069
1069
1070 self.times = []
1070 self.times = []
1071 self._started = {}
1071 self._started = {}
1072
1072
1073 def addFailure(self, test, reason):
1073 def addFailure(self, test, reason):
1074 self.failures.append((test, reason))
1074 self.failures.append((test, reason))
1075
1075
1076 if self._options.first:
1076 if self._options.first:
1077 self.stop()
1077 self.stop()
1078
1078
1079 def addError(self, *args, **kwargs):
1079 def addError(self, *args, **kwargs):
1080 super(TestResult, self).addError(*args, **kwargs)
1080 super(TestResult, self).addError(*args, **kwargs)
1081
1081
1082 if self._options.first:
1082 if self._options.first:
1083 self.stop()
1083 self.stop()
1084
1084
1085 # Polyfill.
1085 # Polyfill.
1086 def addSkip(self, test, reason):
1086 def addSkip(self, test, reason):
1087 self.skipped.append((test, reason))
1087 self.skipped.append((test, reason))
1088
1088
1089 if self.showAll:
1089 if self.showAll:
1090 self.stream.writeln('skipped %s' % reason)
1090 self.stream.writeln('skipped %s' % reason)
1091 else:
1091 else:
1092 self.stream.write('s')
1092 self.stream.write('s')
1093 self.stream.flush()
1093 self.stream.flush()
1094
1094
1095 def addIgnore(self, test, reason):
1095 def addIgnore(self, test, reason):
1096 self.ignored.append((test, reason))
1096 self.ignored.append((test, reason))
1097
1097
1098 if self.showAll:
1098 if self.showAll:
1099 self.stream.writeln('ignored %s' % reason)
1099 self.stream.writeln('ignored %s' % reason)
1100 else:
1100 else:
1101 self.stream.write('i')
1101 self.stream.write('i')
1102 self.stream.flush()
1102 self.stream.flush()
1103
1103
1104 def addWarn(self, test, reason):
1104 def addWarn(self, test, reason):
1105 self.warned.append((test, reason))
1105 self.warned.append((test, reason))
1106
1106
1107 if self._options.first:
1107 if self._options.first:
1108 self.stop()
1108 self.stop()
1109
1109
1110 if self.showAll:
1110 if self.showAll:
1111 self.stream.writeln('warned %s' % reason)
1111 self.stream.writeln('warned %s' % reason)
1112 else:
1112 else:
1113 self.stream.write('~')
1113 self.stream.write('~')
1114 self.stream.flush()
1114 self.stream.flush()
1115
1115
1116 def addOutputMismatch(self, test, ret, got, expected):
1116 def addOutputMismatch(self, test, ret, got, expected):
1117 """Record a mismatch in test output for a particular test."""
1117 """Record a mismatch in test output for a particular test."""
1118
1118
1119 if self._options.nodiff:
1119 if self._options.nodiff:
1120 return
1120 return
1121
1121
1122 if self._options.view:
1122 if self._options.view:
1123 os.system("%s %s %s" % (self._view, test.refpath, test.errpath))
1123 os.system("%s %s %s" % (self._view, test.refpath, test.errpath))
1124 else:
1124 else:
1125 failed, lines = getdiff(expected, got,
1125 failed, lines = getdiff(expected, got,
1126 test.refpath, test.errpath)
1126 test.refpath, test.errpath)
1127 if failed:
1127 if failed:
1128 self.addFailure(test, 'diff generation failed')
1128 self.addFailure(test, 'diff generation failed')
1129 else:
1129 else:
1130 self.stream.write('\n')
1130 self.stream.write('\n')
1131 for line in lines:
1131 for line in lines:
1132 self.stream.write(line)
1132 self.stream.write(line)
1133 self.stream.flush()
1133 self.stream.flush()
1134
1134
1135 if ret or not self._options.interactive or \
1135 if ret or not self._options.interactive or \
1136 not os.path.exists(test.errpath):
1136 not os.path.exists(test.errpath):
1137 return
1137 return
1138
1138
1139 iolock.acquire()
1139 iolock.acquire()
1140 print 'Accept this change? [n] ',
1140 print 'Accept this change? [n] ',
1141 answer = sys.stdin.readline().strip()
1141 answer = sys.stdin.readline().strip()
1142 iolock.release()
1142 iolock.release()
1143 if answer.lower() in ('y', 'yes'):
1143 if answer.lower() in ('y', 'yes'):
1144 if test.name.endswith('.t'):
1144 if test.name.endswith('.t'):
1145 rename(test.errpath, test.path)
1145 rename(test.errpath, test.path)
1146 else:
1146 else:
1147 rename(test.errpath, '%s.out' % test.path)
1147 rename(test.errpath, '%s.out' % test.path)
1148
1148
1149 def startTest(self, test):
1149 def startTest(self, test):
1150 super(TestResult, self).startTest(test)
1150 super(TestResult, self).startTest(test)
1151
1151
1152 self._started[test.name] = time.time()
1152 self._started[test.name] = time.time()
1153
1153
1154 def stopTest(self, test, interrupted=False):
1154 def stopTest(self, test, interrupted=False):
1155 super(TestResult, self).stopTest(test)
1155 super(TestResult, self).stopTest(test)
1156
1156
1157 self.times.append((test.name, time.time() - self._started[test.name]))
1157 self.times.append((test.name, time.time() - self._started[test.name]))
1158 del self._started[test.name]
1158 del self._started[test.name]
1159
1159
1160 if interrupted:
1160 if interrupted:
1161 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1161 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1162 test.name, self.times[-1][1]))
1162 test.name, self.times[-1][1]))
1163
1163
1164 class TestSuite(unittest.TestSuite):
1164 class TestSuite(unittest.TestSuite):
1165 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1165 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1166
1166
1167 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1167 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1168 retest=False, keywords=None, loop=False,
1168 retest=False, keywords=None, loop=False,
1169 *args, **kwargs):
1169 *args, **kwargs):
1170 """Create a new instance that can run tests with a configuration.
1170 """Create a new instance that can run tests with a configuration.
1171
1171
1172 testdir specifies the directory where tests are executed from. This
1172 testdir specifies the directory where tests are executed from. This
1173 is typically the ``tests`` directory from Mercurial's source
1173 is typically the ``tests`` directory from Mercurial's source
1174 repository.
1174 repository.
1175
1175
1176 jobs specifies the number of jobs to run concurrently. Each test
1176 jobs specifies the number of jobs to run concurrently. Each test
1177 executes on its own thread. Tests actually spawn new processes, so
1177 executes on its own thread. Tests actually spawn new processes, so
1178 state mutation should not be an issue.
1178 state mutation should not be an issue.
1179
1179
1180 whitelist and blacklist denote tests that have been whitelisted and
1180 whitelist and blacklist denote tests that have been whitelisted and
1181 blacklisted, respectively. These arguments don't belong in TestSuite.
1181 blacklisted, respectively. These arguments don't belong in TestSuite.
1182 Instead, whitelist and blacklist should be handled by the thing that
1182 Instead, whitelist and blacklist should be handled by the thing that
1183 populates the TestSuite with tests. They are present to preserve
1183 populates the TestSuite with tests. They are present to preserve
1184 backwards compatible behavior which reports skipped tests as part
1184 backwards compatible behavior which reports skipped tests as part
1185 of the results.
1185 of the results.
1186
1186
1187 retest denotes whether to retest failed tests. This arguably belongs
1187 retest denotes whether to retest failed tests. This arguably belongs
1188 outside of TestSuite.
1188 outside of TestSuite.
1189
1189
1190 keywords denotes key words that will be used to filter which tests
1190 keywords denotes key words that will be used to filter which tests
1191 to execute. This arguably belongs outside of TestSuite.
1191 to execute. This arguably belongs outside of TestSuite.
1192
1192
1193 loop denotes whether to loop over tests forever.
1193 loop denotes whether to loop over tests forever.
1194 """
1194 """
1195 super(TestSuite, self).__init__(*args, **kwargs)
1195 super(TestSuite, self).__init__(*args, **kwargs)
1196
1196
1197 self._jobs = jobs
1197 self._jobs = jobs
1198 self._whitelist = whitelist
1198 self._whitelist = whitelist
1199 self._blacklist = blacklist
1199 self._blacklist = blacklist
1200 self._retest = retest
1200 self._retest = retest
1201 self._keywords = keywords
1201 self._keywords = keywords
1202 self._loop = loop
1202 self._loop = loop
1203
1203
1204 def run(self, result):
1204 def run(self, result):
1205 # We have a number of filters that need to be applied. We do this
1205 # 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
1206 # here instead of inside Test because it makes the running logic for
1207 # Test simpler.
1207 # Test simpler.
1208 tests = []
1208 tests = []
1209 for test in self._tests:
1209 for test in self._tests:
1210 if not os.path.exists(test.path):
1210 if not os.path.exists(test.path):
1211 result.addSkip(test, "Doesn't exist")
1211 result.addSkip(test, "Doesn't exist")
1212 continue
1212 continue
1213
1213
1214 if not (self._whitelist and test.name in self._whitelist):
1214 if not (self._whitelist and test.name in self._whitelist):
1215 if self._blacklist and test.name in self._blacklist:
1215 if self._blacklist and test.name in self._blacklist:
1216 result.addSkip(test, 'blacklisted')
1216 result.addSkip(test, 'blacklisted')
1217 continue
1217 continue
1218
1218
1219 if self._retest and not os.path.exists(test.errpath):
1219 if self._retest and not os.path.exists(test.errpath):
1220 result.addIgnore(test, 'not retesting')
1220 result.addIgnore(test, 'not retesting')
1221 continue
1221 continue
1222
1222
1223 if self._keywords:
1223 if self._keywords:
1224 f = open(test.path)
1224 f = open(test.path)
1225 t = f.read().lower() + test.name.lower()
1225 t = f.read().lower() + test.name.lower()
1226 f.close()
1226 f.close()
1227 ignored = False
1227 ignored = False
1228 for k in self._keywords.lower().split():
1228 for k in self._keywords.lower().split():
1229 if k not in t:
1229 if k not in t:
1230 result.addIgnore(test, "doesn't match keyword")
1230 result.addIgnore(test, "doesn't match keyword")
1231 ignored = True
1231 ignored = True
1232 break
1232 break
1233
1233
1234 if ignored:
1234 if ignored:
1235 continue
1235 continue
1236
1236
1237 tests.append(test)
1237 tests.append(test)
1238
1238
1239 runtests = list(tests)
1239 runtests = list(tests)
1240 done = queue.Queue()
1240 done = queue.Queue()
1241 running = 0
1241 running = 0
1242
1242
1243 def job(test, result):
1243 def job(test, result):
1244 try:
1244 try:
1245 test(result)
1245 test(result)
1246 done.put(None)
1246 done.put(None)
1247 except KeyboardInterrupt:
1247 except KeyboardInterrupt:
1248 pass
1248 pass
1249 except: # re-raises
1249 except: # re-raises
1250 done.put(('!', test, 'run-test raised an error, see traceback'))
1250 done.put(('!', test, 'run-test raised an error, see traceback'))
1251 raise
1251 raise
1252
1252
1253 try:
1253 try:
1254 while tests or running:
1254 while tests or running:
1255 if not done.empty() or running == self._jobs or not tests:
1255 if not done.empty() or running == self._jobs or not tests:
1256 try:
1256 try:
1257 done.get(True, 1)
1257 done.get(True, 1)
1258 if result and result.shouldStop:
1258 if result and result.shouldStop:
1259 break
1259 break
1260 except queue.Empty:
1260 except queue.Empty:
1261 continue
1261 continue
1262 running -= 1
1262 running -= 1
1263 if tests and not running == self._jobs:
1263 if tests and not running == self._jobs:
1264 test = tests.pop(0)
1264 test = tests.pop(0)
1265 if self._loop:
1265 if self._loop:
1266 tests.append(test)
1266 tests.append(test)
1267 t = threading.Thread(target=job, name=test.name,
1267 t = threading.Thread(target=job, name=test.name,
1268 args=(test, result))
1268 args=(test, result))
1269 t.start()
1269 t.start()
1270 running += 1
1270 running += 1
1271 except KeyboardInterrupt:
1271 except KeyboardInterrupt:
1272 for test in runtests:
1272 for test in runtests:
1273 test.abort()
1273 test.abort()
1274
1274
1275 return result
1275 return result
1276
1276
1277 class TextTestRunner(unittest.TextTestRunner):
1277 class TextTestRunner(unittest.TextTestRunner):
1278 """Custom unittest test runner that uses appropriate settings."""
1278 """Custom unittest test runner that uses appropriate settings."""
1279
1279
1280 def __init__(self, runner, *args, **kwargs):
1280 def __init__(self, runner, *args, **kwargs):
1281 super(TextTestRunner, self).__init__(*args, **kwargs)
1281 super(TextTestRunner, self).__init__(*args, **kwargs)
1282
1282
1283 self._runner = runner
1283 self._runner = runner
1284
1284
1285 def run(self, test):
1285 def run(self, test):
1286 result = TestResult(self._runner.options, self.stream,
1286 result = TestResult(self._runner.options, self.stream,
1287 self.descriptions, self.verbosity)
1287 self.descriptions, self.verbosity)
1288
1288
1289 test(result)
1289 test(result)
1290
1290
1291 failed = len(result.failures)
1291 failed = len(result.failures)
1292 warned = len(result.warned)
1292 warned = len(result.warned)
1293 skipped = len(result.skipped)
1293 skipped = len(result.skipped)
1294 ignored = len(result.ignored)
1294 ignored = len(result.ignored)
1295
1295
1296 self.stream.writeln('')
1296 self.stream.writeln('')
1297
1297
1298 if not self._runner.options.noskips:
1298 if not self._runner.options.noskips:
1299 for test, msg in result.skipped:
1299 for test, msg in result.skipped:
1300 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1300 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1301 for test, msg in result.warned:
1301 for test, msg in result.warned:
1302 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1302 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1303 for test, msg in result.failures:
1303 for test, msg in result.failures:
1304 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1304 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1305 for test, msg in result.errors:
1305 for test, msg in result.errors:
1306 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1306 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1307
1307
1308 self._runner._checkhglib('Tested')
1308 self._runner._checkhglib('Tested')
1309
1309
1310 # This differs from unittest's default output in that we don't count
1310 # 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.
1311 # skipped and ignored tests as part of the total test count.
1312 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1312 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1313 % (result.testsRun - skipped - ignored,
1313 % (result.testsRun - skipped - ignored,
1314 skipped + ignored, warned, failed))
1314 skipped + ignored, warned, failed))
1315 if failed:
1315 if failed:
1316 self.stream.writeln('python hash seed: %s' %
1316 self.stream.writeln('python hash seed: %s' %
1317 os.environ['PYTHONHASHSEED'])
1317 os.environ['PYTHONHASHSEED'])
1318 if self._runner.options.time:
1318 if self._runner.options.time:
1319 self.printtimes(result.times)
1319 self.printtimes(result.times)
1320
1320
1321 return result
1322
1321 def printtimes(self, times):
1323 def printtimes(self, times):
1322 self.stream.writeln('# Producing time report')
1324 self.stream.writeln('# Producing time report')
1323 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1325 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1324 cols = '%7.3f %s'
1326 cols = '%7.3f %s'
1325 self.stream.writeln('%-7s %s' % ('Time', 'Test'))
1327 self.stream.writeln('%-7s %s' % ('Time', 'Test'))
1326 for test, timetaken in times:
1328 for test, timetaken in times:
1327 self.stream.writeln(cols % (timetaken, test))
1329 self.stream.writeln(cols % (timetaken, test))
1328
1330
1329 class TestRunner(object):
1331 class TestRunner(object):
1330 """Holds context for executing tests.
1332 """Holds context for executing tests.
1331
1333
1332 Tests rely on a lot of state. This object holds it for them.
1334 Tests rely on a lot of state. This object holds it for them.
1333 """
1335 """
1334
1336
1335 # Programs required to run tests.
1337 # Programs required to run tests.
1336 REQUIREDTOOLS = [
1338 REQUIREDTOOLS = [
1337 os.path.basename(sys.executable),
1339 os.path.basename(sys.executable),
1338 'diff',
1340 'diff',
1339 'grep',
1341 'grep',
1340 'unzip',
1342 'unzip',
1341 'gunzip',
1343 'gunzip',
1342 'bunzip2',
1344 'bunzip2',
1343 'sed',
1345 'sed',
1344 ]
1346 ]
1345
1347
1346 # Maps file extensions to test class.
1348 # Maps file extensions to test class.
1347 TESTTYPES = [
1349 TESTTYPES = [
1348 ('.py', PythonTest),
1350 ('.py', PythonTest),
1349 ('.t', TTest),
1351 ('.t', TTest),
1350 ]
1352 ]
1351
1353
1352 def __init__(self):
1354 def __init__(self):
1353 self.options = None
1355 self.options = None
1354 self._testdir = None
1356 self._testdir = None
1355 self._hgtmp = None
1357 self._hgtmp = None
1356 self._installdir = None
1358 self._installdir = None
1357 self._bindir = None
1359 self._bindir = None
1358 self._tmpbinddir = None
1360 self._tmpbinddir = None
1359 self._pythondir = None
1361 self._pythondir = None
1360 self._coveragefile = None
1362 self._coveragefile = None
1361 self._createdfiles = []
1363 self._createdfiles = []
1362 self._hgpath = None
1364 self._hgpath = None
1363
1365
1364 def run(self, args, parser=None):
1366 def run(self, args, parser=None):
1365 """Run the test suite."""
1367 """Run the test suite."""
1366 oldmask = os.umask(022)
1368 oldmask = os.umask(022)
1367 try:
1369 try:
1368 parser = parser or getparser()
1370 parser = parser or getparser()
1369 options, args = parseargs(args, parser)
1371 options, args = parseargs(args, parser)
1370 self.options = options
1372 self.options = options
1371
1373
1372 self._checktools()
1374 self._checktools()
1373 tests = self.findtests(args)
1375 tests = self.findtests(args)
1374 return self._run(tests)
1376 return self._run(tests)
1375 finally:
1377 finally:
1376 os.umask(oldmask)
1378 os.umask(oldmask)
1377
1379
1378 def _run(self, tests):
1380 def _run(self, tests):
1379 if self.options.random:
1381 if self.options.random:
1380 random.shuffle(tests)
1382 random.shuffle(tests)
1381 else:
1383 else:
1382 # keywords for slow tests
1384 # keywords for slow tests
1383 slow = 'svn gendoc check-code-hg'.split()
1385 slow = 'svn gendoc check-code-hg'.split()
1384 def sortkey(f):
1386 def sortkey(f):
1385 # run largest tests first, as they tend to take the longest
1387 # run largest tests first, as they tend to take the longest
1386 try:
1388 try:
1387 val = -os.stat(f).st_size
1389 val = -os.stat(f).st_size
1388 except OSError, e:
1390 except OSError, e:
1389 if e.errno != errno.ENOENT:
1391 if e.errno != errno.ENOENT:
1390 raise
1392 raise
1391 return -1e9 # file does not exist, tell early
1393 return -1e9 # file does not exist, tell early
1392 for kw in slow:
1394 for kw in slow:
1393 if kw in f:
1395 if kw in f:
1394 val *= 10
1396 val *= 10
1395 return val
1397 return val
1396 tests.sort(key=sortkey)
1398 tests.sort(key=sortkey)
1397
1399
1398 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1400 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1399
1401
1400 if 'PYTHONHASHSEED' not in os.environ:
1402 if 'PYTHONHASHSEED' not in os.environ:
1401 # use a random python hash seed all the time
1403 # use a random python hash seed all the time
1402 # we do the randomness ourself to know what seed is used
1404 # we do the randomness ourself to know what seed is used
1403 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1405 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1404
1406
1405 if self.options.tmpdir:
1407 if self.options.tmpdir:
1406 self.options.keep_tmpdir = True
1408 self.options.keep_tmpdir = True
1407 tmpdir = self.options.tmpdir
1409 tmpdir = self.options.tmpdir
1408 if os.path.exists(tmpdir):
1410 if os.path.exists(tmpdir):
1409 # Meaning of tmpdir has changed since 1.3: we used to create
1411 # Meaning of tmpdir has changed since 1.3: we used to create
1410 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1412 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1411 # tmpdir already exists.
1413 # tmpdir already exists.
1412 print "error: temp dir %r already exists" % tmpdir
1414 print "error: temp dir %r already exists" % tmpdir
1413 return 1
1415 return 1
1414
1416
1415 # Automatically removing tmpdir sounds convenient, but could
1417 # Automatically removing tmpdir sounds convenient, but could
1416 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1418 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1417 # or "--tmpdir=$HOME".
1419 # or "--tmpdir=$HOME".
1418 #vlog("# Removing temp dir", tmpdir)
1420 #vlog("# Removing temp dir", tmpdir)
1419 #shutil.rmtree(tmpdir)
1421 #shutil.rmtree(tmpdir)
1420 os.makedirs(tmpdir)
1422 os.makedirs(tmpdir)
1421 else:
1423 else:
1422 d = None
1424 d = None
1423 if os.name == 'nt':
1425 if os.name == 'nt':
1424 # without this, we get the default temp dir location, but
1426 # without this, we get the default temp dir location, but
1425 # in all lowercase, which causes troubles with paths (issue3490)
1427 # in all lowercase, which causes troubles with paths (issue3490)
1426 d = os.getenv('TMP')
1428 d = os.getenv('TMP')
1427 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1429 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1428 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1430 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1429
1431
1430 if self.options.with_hg:
1432 if self.options.with_hg:
1431 self._installdir = None
1433 self._installdir = None
1432 self._bindir = os.path.dirname(os.path.realpath(
1434 self._bindir = os.path.dirname(os.path.realpath(
1433 self.options.with_hg))
1435 self.options.with_hg))
1434 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1436 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1435 os.makedirs(self._tmpbindir)
1437 os.makedirs(self._tmpbindir)
1436
1438
1437 # This looks redundant with how Python initializes sys.path from
1439 # This looks redundant with how Python initializes sys.path from
1438 # the location of the script being executed. Needed because the
1440 # the location of the script being executed. Needed because the
1439 # "hg" specified by --with-hg is not the only Python script
1441 # "hg" specified by --with-hg is not the only Python script
1440 # executed in the test suite that needs to import 'mercurial'
1442 # executed in the test suite that needs to import 'mercurial'
1441 # ... which means it's not really redundant at all.
1443 # ... which means it's not really redundant at all.
1442 self._pythondir = self._bindir
1444 self._pythondir = self._bindir
1443 else:
1445 else:
1444 self._installdir = os.path.join(self._hgtmp, "install")
1446 self._installdir = os.path.join(self._hgtmp, "install")
1445 self._bindir = os.environ["BINDIR"] = \
1447 self._bindir = os.environ["BINDIR"] = \
1446 os.path.join(self._installdir, "bin")
1448 os.path.join(self._installdir, "bin")
1447 self._tmpbindir = self._bindir
1449 self._tmpbindir = self._bindir
1448 self._pythondir = os.path.join(self._installdir, "lib", "python")
1450 self._pythondir = os.path.join(self._installdir, "lib", "python")
1449
1451
1450 os.environ["BINDIR"] = self._bindir
1452 os.environ["BINDIR"] = self._bindir
1451 os.environ["PYTHON"] = PYTHON
1453 os.environ["PYTHON"] = PYTHON
1452
1454
1453 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1455 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1454 if self._tmpbindir != self._bindir:
1456 if self._tmpbindir != self._bindir:
1455 path = [self._tmpbindir] + path
1457 path = [self._tmpbindir] + path
1456 os.environ["PATH"] = os.pathsep.join(path)
1458 os.environ["PATH"] = os.pathsep.join(path)
1457
1459
1458 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1460 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1459 # can run .../tests/run-tests.py test-foo where test-foo
1461 # can run .../tests/run-tests.py test-foo where test-foo
1460 # adds an extension to HGRC. Also include run-test.py directory to
1462 # adds an extension to HGRC. Also include run-test.py directory to
1461 # import modules like heredoctest.
1463 # import modules like heredoctest.
1462 pypath = [self._pythondir, self._testdir,
1464 pypath = [self._pythondir, self._testdir,
1463 os.path.abspath(os.path.dirname(__file__))]
1465 os.path.abspath(os.path.dirname(__file__))]
1464 # We have to augment PYTHONPATH, rather than simply replacing
1466 # We have to augment PYTHONPATH, rather than simply replacing
1465 # it, in case external libraries are only available via current
1467 # it, in case external libraries are only available via current
1466 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1468 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1467 # are in /opt/subversion.)
1469 # are in /opt/subversion.)
1468 oldpypath = os.environ.get(IMPL_PATH)
1470 oldpypath = os.environ.get(IMPL_PATH)
1469 if oldpypath:
1471 if oldpypath:
1470 pypath.append(oldpypath)
1472 pypath.append(oldpypath)
1471 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1473 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1472
1474
1473 self._coveragefile = os.path.join(self._testdir, '.coverage')
1475 self._coveragefile = os.path.join(self._testdir, '.coverage')
1474
1476
1475 vlog("# Using TESTDIR", self._testdir)
1477 vlog("# Using TESTDIR", self._testdir)
1476 vlog("# Using HGTMP", self._hgtmp)
1478 vlog("# Using HGTMP", self._hgtmp)
1477 vlog("# Using PATH", os.environ["PATH"])
1479 vlog("# Using PATH", os.environ["PATH"])
1478 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1480 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1479
1481
1480 try:
1482 try:
1481 return self._runtests(tests) or 0
1483 return self._runtests(tests) or 0
1482 finally:
1484 finally:
1483 time.sleep(.1)
1485 time.sleep(.1)
1484 self._cleanup()
1486 self._cleanup()
1485
1487
1486 def findtests(self, args):
1488 def findtests(self, args):
1487 """Finds possible test files from arguments.
1489 """Finds possible test files from arguments.
1488
1490
1489 If you wish to inject custom tests into the test harness, this would
1491 If you wish to inject custom tests into the test harness, this would
1490 be a good function to monkeypatch or override in a derived class.
1492 be a good function to monkeypatch or override in a derived class.
1491 """
1493 """
1492 if not args:
1494 if not args:
1493 if self.options.changed:
1495 if self.options.changed:
1494 proc = Popen4('hg st --rev "%s" -man0 .' %
1496 proc = Popen4('hg st --rev "%s" -man0 .' %
1495 self.options.changed, None, 0)
1497 self.options.changed, None, 0)
1496 stdout, stderr = proc.communicate()
1498 stdout, stderr = proc.communicate()
1497 args = stdout.strip('\0').split('\0')
1499 args = stdout.strip('\0').split('\0')
1498 else:
1500 else:
1499 args = os.listdir('.')
1501 args = os.listdir('.')
1500
1502
1501 return [t for t in args
1503 return [t for t in args
1502 if os.path.basename(t).startswith('test-')
1504 if os.path.basename(t).startswith('test-')
1503 and (t.endswith('.py') or t.endswith('.t'))]
1505 and (t.endswith('.py') or t.endswith('.t'))]
1504
1506
1505 def _runtests(self, tests):
1507 def _runtests(self, tests):
1506 try:
1508 try:
1507 if self._installdir:
1509 if self._installdir:
1508 self._installhg()
1510 self._installhg()
1509 self._checkhglib("Testing")
1511 self._checkhglib("Testing")
1510 else:
1512 else:
1511 self._usecorrectpython()
1513 self._usecorrectpython()
1512
1514
1513 if self.options.restart:
1515 if self.options.restart:
1514 orig = list(tests)
1516 orig = list(tests)
1515 while tests:
1517 while tests:
1516 if os.path.exists(tests[0] + ".err"):
1518 if os.path.exists(tests[0] + ".err"):
1517 break
1519 break
1518 tests.pop(0)
1520 tests.pop(0)
1519 if not tests:
1521 if not tests:
1520 print "running all tests"
1522 print "running all tests"
1521 tests = orig
1523 tests = orig
1522
1524
1523 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1525 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1524
1526
1525 failed = False
1527 failed = False
1526 warned = False
1528 warned = False
1527
1529
1528 suite = TestSuite(self._testdir,
1530 suite = TestSuite(self._testdir,
1529 jobs=self.options.jobs,
1531 jobs=self.options.jobs,
1530 whitelist=self.options.whitelisted,
1532 whitelist=self.options.whitelisted,
1531 blacklist=self.options.blacklist,
1533 blacklist=self.options.blacklist,
1532 retest=self.options.retest,
1534 retest=self.options.retest,
1533 keywords=self.options.keywords,
1535 keywords=self.options.keywords,
1534 loop=self.options.loop,
1536 loop=self.options.loop,
1535 tests=tests)
1537 tests=tests)
1536 verbosity = 1
1538 verbosity = 1
1537 if self.options.verbose:
1539 if self.options.verbose:
1538 verbosity = 2
1540 verbosity = 2
1539 runner = TextTestRunner(self, verbosity=verbosity)
1541 runner = TextTestRunner(self, verbosity=verbosity)
1540 runner.run(suite)
1542 result = runner.run(suite)
1543
1544 if result.failures:
1545 failed = True
1546 if result.warned:
1547 warned = True
1541
1548
1542 if self.options.anycoverage:
1549 if self.options.anycoverage:
1543 self._outputcoverage()
1550 self._outputcoverage()
1544 except KeyboardInterrupt:
1551 except KeyboardInterrupt:
1545 failed = True
1552 failed = True
1546 print "\ninterrupted!"
1553 print "\ninterrupted!"
1547
1554
1548 if failed:
1555 if failed:
1549 return 1
1556 return 1
1550 if warned:
1557 if warned:
1551 return 80
1558 return 80
1552
1559
1553 def _gettest(self, test, count):
1560 def _gettest(self, test, count):
1554 """Obtain a Test by looking at its filename.
1561 """Obtain a Test by looking at its filename.
1555
1562
1556 Returns a Test instance. The Test may not be runnable if it doesn't
1563 Returns a Test instance. The Test may not be runnable if it doesn't
1557 map to a known type.
1564 map to a known type.
1558 """
1565 """
1559 lctest = test.lower()
1566 lctest = test.lower()
1560 testcls = Test
1567 testcls = Test
1561
1568
1562 for ext, cls in self.TESTTYPES:
1569 for ext, cls in self.TESTTYPES:
1563 if lctest.endswith(ext):
1570 if lctest.endswith(ext):
1564 testcls = cls
1571 testcls = cls
1565 break
1572 break
1566
1573
1567 refpath = os.path.join(self._testdir, test)
1574 refpath = os.path.join(self._testdir, test)
1568 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1575 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1569
1576
1570 return testcls(refpath, tmpdir,
1577 return testcls(refpath, tmpdir,
1571 keeptmpdir=self.options.keep_tmpdir,
1578 keeptmpdir=self.options.keep_tmpdir,
1572 debug=self.options.debug,
1579 debug=self.options.debug,
1573 timeout=self.options.timeout,
1580 timeout=self.options.timeout,
1574 startport=self.options.port + count * 3,
1581 startport=self.options.port + count * 3,
1575 extraconfigopts=self.options.extra_config_opt,
1582 extraconfigopts=self.options.extra_config_opt,
1576 py3kwarnings=self.options.py3k_warnings,
1583 py3kwarnings=self.options.py3k_warnings,
1577 shell=self.options.shell)
1584 shell=self.options.shell)
1578
1585
1579 def _cleanup(self):
1586 def _cleanup(self):
1580 """Clean up state from this test invocation."""
1587 """Clean up state from this test invocation."""
1581
1588
1582 if self.options.keep_tmpdir:
1589 if self.options.keep_tmpdir:
1583 return
1590 return
1584
1591
1585 vlog("# Cleaning up HGTMP", self._hgtmp)
1592 vlog("# Cleaning up HGTMP", self._hgtmp)
1586 shutil.rmtree(self._hgtmp, True)
1593 shutil.rmtree(self._hgtmp, True)
1587 for f in self._createdfiles:
1594 for f in self._createdfiles:
1588 try:
1595 try:
1589 os.remove(f)
1596 os.remove(f)
1590 except OSError:
1597 except OSError:
1591 pass
1598 pass
1592
1599
1593 def _usecorrectpython(self):
1600 def _usecorrectpython(self):
1594 """Configure the environment to use the appropriate Python in tests."""
1601 """Configure the environment to use the appropriate Python in tests."""
1595 # Tests must use the same interpreter as us or bad things will happen.
1602 # Tests must use the same interpreter as us or bad things will happen.
1596 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1603 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1597 if getattr(os, 'symlink', None):
1604 if getattr(os, 'symlink', None):
1598 vlog("# Making python executable in test path a symlink to '%s'" %
1605 vlog("# Making python executable in test path a symlink to '%s'" %
1599 sys.executable)
1606 sys.executable)
1600 mypython = os.path.join(self._tmpbindir, pyexename)
1607 mypython = os.path.join(self._tmpbindir, pyexename)
1601 try:
1608 try:
1602 if os.readlink(mypython) == sys.executable:
1609 if os.readlink(mypython) == sys.executable:
1603 return
1610 return
1604 os.unlink(mypython)
1611 os.unlink(mypython)
1605 except OSError, err:
1612 except OSError, err:
1606 if err.errno != errno.ENOENT:
1613 if err.errno != errno.ENOENT:
1607 raise
1614 raise
1608 if self._findprogram(pyexename) != sys.executable:
1615 if self._findprogram(pyexename) != sys.executable:
1609 try:
1616 try:
1610 os.symlink(sys.executable, mypython)
1617 os.symlink(sys.executable, mypython)
1611 self._createdfiles.append(mypython)
1618 self._createdfiles.append(mypython)
1612 except OSError, err:
1619 except OSError, err:
1613 # child processes may race, which is harmless
1620 # child processes may race, which is harmless
1614 if err.errno != errno.EEXIST:
1621 if err.errno != errno.EEXIST:
1615 raise
1622 raise
1616 else:
1623 else:
1617 exedir, exename = os.path.split(sys.executable)
1624 exedir, exename = os.path.split(sys.executable)
1618 vlog("# Modifying search path to find %s as %s in '%s'" %
1625 vlog("# Modifying search path to find %s as %s in '%s'" %
1619 (exename, pyexename, exedir))
1626 (exename, pyexename, exedir))
1620 path = os.environ['PATH'].split(os.pathsep)
1627 path = os.environ['PATH'].split(os.pathsep)
1621 while exedir in path:
1628 while exedir in path:
1622 path.remove(exedir)
1629 path.remove(exedir)
1623 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1630 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1624 if not self._findprogram(pyexename):
1631 if not self._findprogram(pyexename):
1625 print "WARNING: Cannot find %s in search path" % pyexename
1632 print "WARNING: Cannot find %s in search path" % pyexename
1626
1633
1627 def _installhg(self):
1634 def _installhg(self):
1628 """Install hg into the test environment.
1635 """Install hg into the test environment.
1629
1636
1630 This will also configure hg with the appropriate testing settings.
1637 This will also configure hg with the appropriate testing settings.
1631 """
1638 """
1632 vlog("# Performing temporary installation of HG")
1639 vlog("# Performing temporary installation of HG")
1633 installerrs = os.path.join("tests", "install.err")
1640 installerrs = os.path.join("tests", "install.err")
1634 compiler = ''
1641 compiler = ''
1635 if self.options.compiler:
1642 if self.options.compiler:
1636 compiler = '--compiler ' + self.options.compiler
1643 compiler = '--compiler ' + self.options.compiler
1637 pure = self.options.pure and "--pure" or ""
1644 pure = self.options.pure and "--pure" or ""
1638 py3 = ''
1645 py3 = ''
1639 if sys.version_info[0] == 3:
1646 if sys.version_info[0] == 3:
1640 py3 = '--c2to3'
1647 py3 = '--c2to3'
1641
1648
1642 # Run installer in hg root
1649 # Run installer in hg root
1643 script = os.path.realpath(sys.argv[0])
1650 script = os.path.realpath(sys.argv[0])
1644 hgroot = os.path.dirname(os.path.dirname(script))
1651 hgroot = os.path.dirname(os.path.dirname(script))
1645 os.chdir(hgroot)
1652 os.chdir(hgroot)
1646 nohome = '--home=""'
1653 nohome = '--home=""'
1647 if os.name == 'nt':
1654 if os.name == 'nt':
1648 # The --home="" trick works only on OS where os.sep == '/'
1655 # The --home="" trick works only on OS where os.sep == '/'
1649 # because of a distutils convert_path() fast-path. Avoid it at
1656 # because of a distutils convert_path() fast-path. Avoid it at
1650 # least on Windows for now, deal with .pydistutils.cfg bugs
1657 # least on Windows for now, deal with .pydistutils.cfg bugs
1651 # when they happen.
1658 # when they happen.
1652 nohome = ''
1659 nohome = ''
1653 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1660 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1654 ' build %(compiler)s --build-base="%(base)s"'
1661 ' build %(compiler)s --build-base="%(base)s"'
1655 ' install --force --prefix="%(prefix)s"'
1662 ' install --force --prefix="%(prefix)s"'
1656 ' --install-lib="%(libdir)s"'
1663 ' --install-lib="%(libdir)s"'
1657 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1664 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1658 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1665 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1659 'compiler': compiler,
1666 'compiler': compiler,
1660 'base': os.path.join(self._hgtmp, "build"),
1667 'base': os.path.join(self._hgtmp, "build"),
1661 'prefix': self._installdir, 'libdir': self._pythondir,
1668 'prefix': self._installdir, 'libdir': self._pythondir,
1662 'bindir': self._bindir,
1669 'bindir': self._bindir,
1663 'nohome': nohome, 'logfile': installerrs})
1670 'nohome': nohome, 'logfile': installerrs})
1664 vlog("# Running", cmd)
1671 vlog("# Running", cmd)
1665 if os.system(cmd) == 0:
1672 if os.system(cmd) == 0:
1666 if not self.options.verbose:
1673 if not self.options.verbose:
1667 os.remove(installerrs)
1674 os.remove(installerrs)
1668 else:
1675 else:
1669 f = open(installerrs)
1676 f = open(installerrs)
1670 for line in f:
1677 for line in f:
1671 print line,
1678 print line,
1672 f.close()
1679 f.close()
1673 sys.exit(1)
1680 sys.exit(1)
1674 os.chdir(self._testdir)
1681 os.chdir(self._testdir)
1675
1682
1676 self._usecorrectpython()
1683 self._usecorrectpython()
1677
1684
1678 if self.options.py3k_warnings and not self.options.anycoverage:
1685 if self.options.py3k_warnings and not self.options.anycoverage:
1679 vlog("# Updating hg command to enable Py3k Warnings switch")
1686 vlog("# Updating hg command to enable Py3k Warnings switch")
1680 f = open(os.path.join(self._bindir, 'hg'), 'r')
1687 f = open(os.path.join(self._bindir, 'hg'), 'r')
1681 lines = [line.rstrip() for line in f]
1688 lines = [line.rstrip() for line in f]
1682 lines[0] += ' -3'
1689 lines[0] += ' -3'
1683 f.close()
1690 f.close()
1684 f = open(os.path.join(self._bindir, 'hg'), 'w')
1691 f = open(os.path.join(self._bindir, 'hg'), 'w')
1685 for line in lines:
1692 for line in lines:
1686 f.write(line + '\n')
1693 f.write(line + '\n')
1687 f.close()
1694 f.close()
1688
1695
1689 hgbat = os.path.join(self._bindir, 'hg.bat')
1696 hgbat = os.path.join(self._bindir, 'hg.bat')
1690 if os.path.isfile(hgbat):
1697 if os.path.isfile(hgbat):
1691 # hg.bat expects to be put in bin/scripts while run-tests.py
1698 # hg.bat expects to be put in bin/scripts while run-tests.py
1692 # installation layout put it in bin/ directly. Fix it
1699 # installation layout put it in bin/ directly. Fix it
1693 f = open(hgbat, 'rb')
1700 f = open(hgbat, 'rb')
1694 data = f.read()
1701 data = f.read()
1695 f.close()
1702 f.close()
1696 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1703 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1697 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1704 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1698 '"%~dp0python" "%~dp0hg" %*')
1705 '"%~dp0python" "%~dp0hg" %*')
1699 f = open(hgbat, 'wb')
1706 f = open(hgbat, 'wb')
1700 f.write(data)
1707 f.write(data)
1701 f.close()
1708 f.close()
1702 else:
1709 else:
1703 print 'WARNING: cannot fix hg.bat reference to python.exe'
1710 print 'WARNING: cannot fix hg.bat reference to python.exe'
1704
1711
1705 if self.options.anycoverage:
1712 if self.options.anycoverage:
1706 custom = os.path.join(self._testdir, 'sitecustomize.py')
1713 custom = os.path.join(self._testdir, 'sitecustomize.py')
1707 target = os.path.join(self._pythondir, 'sitecustomize.py')
1714 target = os.path.join(self._pythondir, 'sitecustomize.py')
1708 vlog('# Installing coverage trigger to %s' % target)
1715 vlog('# Installing coverage trigger to %s' % target)
1709 shutil.copyfile(custom, target)
1716 shutil.copyfile(custom, target)
1710 rc = os.path.join(self._testdir, '.coveragerc')
1717 rc = os.path.join(self._testdir, '.coveragerc')
1711 vlog('# Installing coverage rc to %s' % rc)
1718 vlog('# Installing coverage rc to %s' % rc)
1712 os.environ['COVERAGE_PROCESS_START'] = rc
1719 os.environ['COVERAGE_PROCESS_START'] = rc
1713 fn = os.path.join(self._installdir, '..', '.coverage')
1720 fn = os.path.join(self._installdir, '..', '.coverage')
1714 os.environ['COVERAGE_FILE'] = fn
1721 os.environ['COVERAGE_FILE'] = fn
1715
1722
1716 def _checkhglib(self, verb):
1723 def _checkhglib(self, verb):
1717 """Ensure that the 'mercurial' package imported by python is
1724 """Ensure that the 'mercurial' package imported by python is
1718 the one we expect it to be. If not, print a warning to stderr."""
1725 the one we expect it to be. If not, print a warning to stderr."""
1719 expecthg = os.path.join(self._pythondir, 'mercurial')
1726 expecthg = os.path.join(self._pythondir, 'mercurial')
1720 actualhg = self._gethgpath()
1727 actualhg = self._gethgpath()
1721 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1728 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1722 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1729 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1723 ' (expected %s)\n'
1730 ' (expected %s)\n'
1724 % (verb, actualhg, expecthg))
1731 % (verb, actualhg, expecthg))
1725 def _gethgpath(self):
1732 def _gethgpath(self):
1726 """Return the path to the mercurial package that is actually found by
1733 """Return the path to the mercurial package that is actually found by
1727 the current Python interpreter."""
1734 the current Python interpreter."""
1728 if self._hgpath is not None:
1735 if self._hgpath is not None:
1729 return self._hgpath
1736 return self._hgpath
1730
1737
1731 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1738 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1732 pipe = os.popen(cmd % PYTHON)
1739 pipe = os.popen(cmd % PYTHON)
1733 try:
1740 try:
1734 self._hgpath = pipe.read().strip()
1741 self._hgpath = pipe.read().strip()
1735 finally:
1742 finally:
1736 pipe.close()
1743 pipe.close()
1737
1744
1738 return self._hgpath
1745 return self._hgpath
1739
1746
1740 def _outputcoverage(self):
1747 def _outputcoverage(self):
1741 """Produce code coverage output."""
1748 """Produce code coverage output."""
1742 vlog('# Producing coverage report')
1749 vlog('# Producing coverage report')
1743 os.chdir(self._pythondir)
1750 os.chdir(self._pythondir)
1744
1751
1745 def covrun(*args):
1752 def covrun(*args):
1746 cmd = 'coverage %s' % ' '.join(args)
1753 cmd = 'coverage %s' % ' '.join(args)
1747 vlog('# Running: %s' % cmd)
1754 vlog('# Running: %s' % cmd)
1748 os.system(cmd)
1755 os.system(cmd)
1749
1756
1750 covrun('-c')
1757 covrun('-c')
1751 omit = ','.join(os.path.join(x, '*') for x in
1758 omit = ','.join(os.path.join(x, '*') for x in
1752 [self._bindir, self._testdir])
1759 [self._bindir, self._testdir])
1753 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1760 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1754 if self.options.htmlcov:
1761 if self.options.htmlcov:
1755 htmldir = os.path.join(self._testdir, 'htmlcov')
1762 htmldir = os.path.join(self._testdir, 'htmlcov')
1756 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1763 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1757 '"--omit=%s"' % omit)
1764 '"--omit=%s"' % omit)
1758 if self.options.annotate:
1765 if self.options.annotate:
1759 adir = os.path.join(self._testdir, 'annotated')
1766 adir = os.path.join(self._testdir, 'annotated')
1760 if not os.path.isdir(adir):
1767 if not os.path.isdir(adir):
1761 os.mkdir(adir)
1768 os.mkdir(adir)
1762 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1769 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1763
1770
1764 def _findprogram(self, program):
1771 def _findprogram(self, program):
1765 """Search PATH for a executable program"""
1772 """Search PATH for a executable program"""
1766 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1773 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1767 name = os.path.join(p, program)
1774 name = os.path.join(p, program)
1768 if os.name == 'nt' or os.access(name, os.X_OK):
1775 if os.name == 'nt' or os.access(name, os.X_OK):
1769 return name
1776 return name
1770 return None
1777 return None
1771
1778
1772 def _checktools(self):
1779 def _checktools(self):
1773 """Ensure tools required to run tests are present."""
1780 """Ensure tools required to run tests are present."""
1774 for p in self.REQUIREDTOOLS:
1781 for p in self.REQUIREDTOOLS:
1775 if os.name == 'nt' and not p.endswith('.exe'):
1782 if os.name == 'nt' and not p.endswith('.exe'):
1776 p += '.exe'
1783 p += '.exe'
1777 found = self._findprogram(p)
1784 found = self._findprogram(p)
1778 if found:
1785 if found:
1779 vlog("# Found prerequisite", p, "at", found)
1786 vlog("# Found prerequisite", p, "at", found)
1780 else:
1787 else:
1781 print "WARNING: Did not find prerequisite tool: %s " % p
1788 print "WARNING: Did not find prerequisite tool: %s " % p
1782
1789
1783 if __name__ == '__main__':
1790 if __name__ == '__main__':
1784 runner = TestRunner()
1791 runner = TestRunner()
1785 sys.exit(runner.run(sys.argv[1:]))
1792 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now