##// END OF EJS Templates
run-tests: move t test parsing into its own function...
Gregory Szorc -
r21312:986b8a58 default
parent child Browse files
Show More
@@ -1,1397 +1,1407 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
60
61 processlock = threading.Lock()
61 processlock = threading.Lock()
62
62
63 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
63 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
64 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
64 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
65 # zombies but it's pretty harmless even if we do.
65 # zombies but it's pretty harmless even if we do.
66 if sys.version_info < (2, 5):
66 if sys.version_info < (2, 5):
67 subprocess._cleanup = lambda: None
67 subprocess._cleanup = lambda: None
68
68
69 closefds = os.name == 'posix'
69 closefds = os.name == 'posix'
70 def Popen4(cmd, wd, timeout, env=None):
70 def Popen4(cmd, wd, timeout, env=None):
71 processlock.acquire()
71 processlock.acquire()
72 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
72 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
73 close_fds=closefds,
73 close_fds=closefds,
74 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
74 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
75 stderr=subprocess.STDOUT)
75 stderr=subprocess.STDOUT)
76 processlock.release()
76 processlock.release()
77
77
78 p.fromchild = p.stdout
78 p.fromchild = p.stdout
79 p.tochild = p.stdin
79 p.tochild = p.stdin
80 p.childerr = p.stderr
80 p.childerr = p.stderr
81
81
82 p.timeout = False
82 p.timeout = False
83 if timeout:
83 if timeout:
84 def t():
84 def t():
85 start = time.time()
85 start = time.time()
86 while time.time() - start < timeout and p.returncode is None:
86 while time.time() - start < timeout and p.returncode is None:
87 time.sleep(.1)
87 time.sleep(.1)
88 p.timeout = True
88 p.timeout = True
89 if p.returncode is None:
89 if p.returncode is None:
90 terminate(p)
90 terminate(p)
91 threading.Thread(target=t).start()
91 threading.Thread(target=t).start()
92
92
93 return p
93 return p
94
94
95 # reserved exit code to skip test (used by hghave)
95 # reserved exit code to skip test (used by hghave)
96 SKIPPED_STATUS = 80
96 SKIPPED_STATUS = 80
97 SKIPPED_PREFIX = 'skipped: '
97 SKIPPED_PREFIX = 'skipped: '
98 FAILED_PREFIX = 'hghave check failed: '
98 FAILED_PREFIX = 'hghave check failed: '
99 PYTHON = sys.executable.replace('\\', '/')
99 PYTHON = sys.executable.replace('\\', '/')
100 IMPL_PATH = 'PYTHONPATH'
100 IMPL_PATH = 'PYTHONPATH'
101 if 'java' in sys.platform:
101 if 'java' in sys.platform:
102 IMPL_PATH = 'JYTHONPATH'
102 IMPL_PATH = 'JYTHONPATH'
103
103
104 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
104 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
105
105
106 requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
106 requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
107 "gunzip", "bunzip2", "sed"]
107 "gunzip", "bunzip2", "sed"]
108 createdfiles = []
108 createdfiles = []
109
109
110 defaults = {
110 defaults = {
111 'jobs': ('HGTEST_JOBS', 1),
111 'jobs': ('HGTEST_JOBS', 1),
112 'timeout': ('HGTEST_TIMEOUT', 180),
112 'timeout': ('HGTEST_TIMEOUT', 180),
113 'port': ('HGTEST_PORT', 20059),
113 'port': ('HGTEST_PORT', 20059),
114 'shell': ('HGTEST_SHELL', 'sh'),
114 'shell': ('HGTEST_SHELL', 'sh'),
115 }
115 }
116
116
117 def parselistfiles(files, listtype, warn=True):
117 def parselistfiles(files, listtype, warn=True):
118 entries = dict()
118 entries = dict()
119 for filename in files:
119 for filename in files:
120 try:
120 try:
121 path = os.path.expanduser(os.path.expandvars(filename))
121 path = os.path.expanduser(os.path.expandvars(filename))
122 f = open(path, "r")
122 f = open(path, "r")
123 except IOError, err:
123 except IOError, err:
124 if err.errno != errno.ENOENT:
124 if err.errno != errno.ENOENT:
125 raise
125 raise
126 if warn:
126 if warn:
127 print "warning: no such %s file: %s" % (listtype, filename)
127 print "warning: no such %s file: %s" % (listtype, filename)
128 continue
128 continue
129
129
130 for line in f.readlines():
130 for line in f.readlines():
131 line = line.split('#', 1)[0].strip()
131 line = line.split('#', 1)[0].strip()
132 if line:
132 if line:
133 entries[line] = filename
133 entries[line] = filename
134
134
135 f.close()
135 f.close()
136 return entries
136 return entries
137
137
138 def getparser():
138 def getparser():
139 parser = optparse.OptionParser("%prog [options] [tests]")
139 parser = optparse.OptionParser("%prog [options] [tests]")
140
140
141 # keep these sorted
141 # keep these sorted
142 parser.add_option("--blacklist", action="append",
142 parser.add_option("--blacklist", action="append",
143 help="skip tests listed in the specified blacklist file")
143 help="skip tests listed in the specified blacklist file")
144 parser.add_option("--whitelist", action="append",
144 parser.add_option("--whitelist", action="append",
145 help="always run tests listed in the specified whitelist file")
145 help="always run tests listed in the specified whitelist file")
146 parser.add_option("--changed", type="string",
146 parser.add_option("--changed", type="string",
147 help="run tests that are changed in parent rev or working directory")
147 help="run tests that are changed in parent rev or working directory")
148 parser.add_option("-C", "--annotate", action="store_true",
148 parser.add_option("-C", "--annotate", action="store_true",
149 help="output files annotated with coverage")
149 help="output files annotated with coverage")
150 parser.add_option("-c", "--cover", action="store_true",
150 parser.add_option("-c", "--cover", action="store_true",
151 help="print a test coverage report")
151 help="print a test coverage report")
152 parser.add_option("-d", "--debug", action="store_true",
152 parser.add_option("-d", "--debug", action="store_true",
153 help="debug mode: write output of test scripts to console"
153 help="debug mode: write output of test scripts to console"
154 " rather than capturing and diffing it (disables timeout)")
154 " rather than capturing and diffing it (disables timeout)")
155 parser.add_option("-f", "--first", action="store_true",
155 parser.add_option("-f", "--first", action="store_true",
156 help="exit on the first test failure")
156 help="exit on the first test failure")
157 parser.add_option("-H", "--htmlcov", action="store_true",
157 parser.add_option("-H", "--htmlcov", action="store_true",
158 help="create an HTML report of the coverage of the files")
158 help="create an HTML report of the coverage of the files")
159 parser.add_option("-i", "--interactive", action="store_true",
159 parser.add_option("-i", "--interactive", action="store_true",
160 help="prompt to accept changed output")
160 help="prompt to accept changed output")
161 parser.add_option("-j", "--jobs", type="int",
161 parser.add_option("-j", "--jobs", type="int",
162 help="number of jobs to run in parallel"
162 help="number of jobs to run in parallel"
163 " (default: $%s or %d)" % defaults['jobs'])
163 " (default: $%s or %d)" % defaults['jobs'])
164 parser.add_option("--keep-tmpdir", action="store_true",
164 parser.add_option("--keep-tmpdir", action="store_true",
165 help="keep temporary directory after running tests")
165 help="keep temporary directory after running tests")
166 parser.add_option("-k", "--keywords",
166 parser.add_option("-k", "--keywords",
167 help="run tests matching keywords")
167 help="run tests matching keywords")
168 parser.add_option("-l", "--local", action="store_true",
168 parser.add_option("-l", "--local", action="store_true",
169 help="shortcut for --with-hg=<testdir>/../hg")
169 help="shortcut for --with-hg=<testdir>/../hg")
170 parser.add_option("--loop", action="store_true",
170 parser.add_option("--loop", action="store_true",
171 help="loop tests repeatedly")
171 help="loop tests repeatedly")
172 parser.add_option("-n", "--nodiff", action="store_true",
172 parser.add_option("-n", "--nodiff", action="store_true",
173 help="skip showing test changes")
173 help="skip showing test changes")
174 parser.add_option("-p", "--port", type="int",
174 parser.add_option("-p", "--port", type="int",
175 help="port on which servers should listen"
175 help="port on which servers should listen"
176 " (default: $%s or %d)" % defaults['port'])
176 " (default: $%s or %d)" % defaults['port'])
177 parser.add_option("--compiler", type="string",
177 parser.add_option("--compiler", type="string",
178 help="compiler to build with")
178 help="compiler to build with")
179 parser.add_option("--pure", action="store_true",
179 parser.add_option("--pure", action="store_true",
180 help="use pure Python code instead of C extensions")
180 help="use pure Python code instead of C extensions")
181 parser.add_option("-R", "--restart", action="store_true",
181 parser.add_option("-R", "--restart", action="store_true",
182 help="restart at last error")
182 help="restart at last error")
183 parser.add_option("-r", "--retest", action="store_true",
183 parser.add_option("-r", "--retest", action="store_true",
184 help="retest failed tests")
184 help="retest failed tests")
185 parser.add_option("-S", "--noskips", action="store_true",
185 parser.add_option("-S", "--noskips", action="store_true",
186 help="don't report skip tests verbosely")
186 help="don't report skip tests verbosely")
187 parser.add_option("--shell", type="string",
187 parser.add_option("--shell", type="string",
188 help="shell to use (default: $%s or %s)" % defaults['shell'])
188 help="shell to use (default: $%s or %s)" % defaults['shell'])
189 parser.add_option("-t", "--timeout", type="int",
189 parser.add_option("-t", "--timeout", type="int",
190 help="kill errant tests after TIMEOUT seconds"
190 help="kill errant tests after TIMEOUT seconds"
191 " (default: $%s or %d)" % defaults['timeout'])
191 " (default: $%s or %d)" % defaults['timeout'])
192 parser.add_option("--time", action="store_true",
192 parser.add_option("--time", action="store_true",
193 help="time how long each test takes")
193 help="time how long each test takes")
194 parser.add_option("--tmpdir", type="string",
194 parser.add_option("--tmpdir", type="string",
195 help="run tests in the given temporary directory"
195 help="run tests in the given temporary directory"
196 " (implies --keep-tmpdir)")
196 " (implies --keep-tmpdir)")
197 parser.add_option("-v", "--verbose", action="store_true",
197 parser.add_option("-v", "--verbose", action="store_true",
198 help="output verbose messages")
198 help="output verbose messages")
199 parser.add_option("--view", type="string",
199 parser.add_option("--view", type="string",
200 help="external diff viewer")
200 help="external diff viewer")
201 parser.add_option("--with-hg", type="string",
201 parser.add_option("--with-hg", type="string",
202 metavar="HG",
202 metavar="HG",
203 help="test using specified hg script rather than a "
203 help="test using specified hg script rather than a "
204 "temporary installation")
204 "temporary installation")
205 parser.add_option("-3", "--py3k-warnings", action="store_true",
205 parser.add_option("-3", "--py3k-warnings", action="store_true",
206 help="enable Py3k warnings on Python 2.6+")
206 help="enable Py3k warnings on Python 2.6+")
207 parser.add_option('--extra-config-opt', action="append",
207 parser.add_option('--extra-config-opt', action="append",
208 help='set the given config opt in the test hgrc')
208 help='set the given config opt in the test hgrc')
209 parser.add_option('--random', action="store_true",
209 parser.add_option('--random', action="store_true",
210 help='run tests in random order')
210 help='run tests in random order')
211
211
212 for option, (envvar, default) in defaults.items():
212 for option, (envvar, default) in defaults.items():
213 defaults[option] = type(default)(os.environ.get(envvar, default))
213 defaults[option] = type(default)(os.environ.get(envvar, default))
214 parser.set_defaults(**defaults)
214 parser.set_defaults(**defaults)
215
215
216 return parser
216 return parser
217
217
218 def parseargs(args, parser):
218 def parseargs(args, parser):
219 (options, args) = parser.parse_args(args)
219 (options, args) = parser.parse_args(args)
220
220
221 # jython is always pure
221 # jython is always pure
222 if 'java' in sys.platform or '__pypy__' in sys.modules:
222 if 'java' in sys.platform or '__pypy__' in sys.modules:
223 options.pure = True
223 options.pure = True
224
224
225 if options.with_hg:
225 if options.with_hg:
226 options.with_hg = os.path.expanduser(options.with_hg)
226 options.with_hg = os.path.expanduser(options.with_hg)
227 if not (os.path.isfile(options.with_hg) and
227 if not (os.path.isfile(options.with_hg) and
228 os.access(options.with_hg, os.X_OK)):
228 os.access(options.with_hg, os.X_OK)):
229 parser.error('--with-hg must specify an executable hg script')
229 parser.error('--with-hg must specify an executable hg script')
230 if not os.path.basename(options.with_hg) == 'hg':
230 if not os.path.basename(options.with_hg) == 'hg':
231 sys.stderr.write('warning: --with-hg should specify an hg script\n')
231 sys.stderr.write('warning: --with-hg should specify an hg script\n')
232 if options.local:
232 if options.local:
233 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
233 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
234 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
234 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
235 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
235 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
236 parser.error('--local specified, but %r not found or not executable'
236 parser.error('--local specified, but %r not found or not executable'
237 % hgbin)
237 % hgbin)
238 options.with_hg = hgbin
238 options.with_hg = hgbin
239
239
240 options.anycoverage = options.cover or options.annotate or options.htmlcov
240 options.anycoverage = options.cover or options.annotate or options.htmlcov
241 if options.anycoverage:
241 if options.anycoverage:
242 try:
242 try:
243 import coverage
243 import coverage
244 covver = version.StrictVersion(coverage.__version__).version
244 covver = version.StrictVersion(coverage.__version__).version
245 if covver < (3, 3):
245 if covver < (3, 3):
246 parser.error('coverage options require coverage 3.3 or later')
246 parser.error('coverage options require coverage 3.3 or later')
247 except ImportError:
247 except ImportError:
248 parser.error('coverage options now require the coverage package')
248 parser.error('coverage options now require the coverage package')
249
249
250 if options.anycoverage and options.local:
250 if options.anycoverage and options.local:
251 # this needs some path mangling somewhere, I guess
251 # this needs some path mangling somewhere, I guess
252 parser.error("sorry, coverage options do not work when --local "
252 parser.error("sorry, coverage options do not work when --local "
253 "is specified")
253 "is specified")
254
254
255 global verbose
255 global verbose
256 if options.verbose:
256 if options.verbose:
257 verbose = ''
257 verbose = ''
258
258
259 if options.tmpdir:
259 if options.tmpdir:
260 options.tmpdir = os.path.expanduser(options.tmpdir)
260 options.tmpdir = os.path.expanduser(options.tmpdir)
261
261
262 if options.jobs < 1:
262 if options.jobs < 1:
263 parser.error('--jobs must be positive')
263 parser.error('--jobs must be positive')
264 if options.interactive and options.debug:
264 if options.interactive and options.debug:
265 parser.error("-i/--interactive and -d/--debug are incompatible")
265 parser.error("-i/--interactive and -d/--debug are incompatible")
266 if options.debug:
266 if options.debug:
267 if options.timeout != defaults['timeout']:
267 if options.timeout != defaults['timeout']:
268 sys.stderr.write(
268 sys.stderr.write(
269 'warning: --timeout option ignored with --debug\n')
269 'warning: --timeout option ignored with --debug\n')
270 options.timeout = 0
270 options.timeout = 0
271 if options.py3k_warnings:
271 if options.py3k_warnings:
272 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
272 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
273 parser.error('--py3k-warnings can only be used on Python 2.6+')
273 parser.error('--py3k-warnings can only be used on Python 2.6+')
274 if options.blacklist:
274 if options.blacklist:
275 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
275 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
276 if options.whitelist:
276 if options.whitelist:
277 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
277 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
278 else:
278 else:
279 options.whitelisted = {}
279 options.whitelisted = {}
280
280
281 return (options, args)
281 return (options, args)
282
282
283 def rename(src, dst):
283 def rename(src, dst):
284 """Like os.rename(), trade atomicity and opened files friendliness
284 """Like os.rename(), trade atomicity and opened files friendliness
285 for existing destination support.
285 for existing destination support.
286 """
286 """
287 shutil.copy(src, dst)
287 shutil.copy(src, dst)
288 os.remove(src)
288 os.remove(src)
289
289
290 def parsehghaveoutput(lines):
290 def parsehghaveoutput(lines):
291 '''Parse hghave log lines.
291 '''Parse hghave log lines.
292 Return tuple of lists (missing, failed):
292 Return tuple of lists (missing, failed):
293 * the missing/unknown features
293 * the missing/unknown features
294 * the features for which existence check failed'''
294 * the features for which existence check failed'''
295 missing = []
295 missing = []
296 failed = []
296 failed = []
297 for line in lines:
297 for line in lines:
298 if line.startswith(SKIPPED_PREFIX):
298 if line.startswith(SKIPPED_PREFIX):
299 line = line.splitlines()[0]
299 line = line.splitlines()[0]
300 missing.append(line[len(SKIPPED_PREFIX):])
300 missing.append(line[len(SKIPPED_PREFIX):])
301 elif line.startswith(FAILED_PREFIX):
301 elif line.startswith(FAILED_PREFIX):
302 line = line.splitlines()[0]
302 line = line.splitlines()[0]
303 failed.append(line[len(FAILED_PREFIX):])
303 failed.append(line[len(FAILED_PREFIX):])
304
304
305 return missing, failed
305 return missing, failed
306
306
307 def showdiff(expected, output, ref, err):
307 def showdiff(expected, output, ref, err):
308 print
308 print
309 servefail = False
309 servefail = False
310 for line in difflib.unified_diff(expected, output, ref, err):
310 for line in difflib.unified_diff(expected, output, ref, err):
311 sys.stdout.write(line)
311 sys.stdout.write(line)
312 if not servefail and line.startswith(
312 if not servefail and line.startswith(
313 '+ abort: child process failed to start'):
313 '+ abort: child process failed to start'):
314 servefail = True
314 servefail = True
315 return {'servefail': servefail}
315 return {'servefail': servefail}
316
316
317
317
318 verbose = False
318 verbose = False
319 def vlog(*msg):
319 def vlog(*msg):
320 if verbose is not False:
320 if verbose is not False:
321 iolock.acquire()
321 iolock.acquire()
322 if verbose:
322 if verbose:
323 print verbose,
323 print verbose,
324 for m in msg:
324 for m in msg:
325 print m,
325 print m,
326 print
326 print
327 sys.stdout.flush()
327 sys.stdout.flush()
328 iolock.release()
328 iolock.release()
329
329
330 def log(*msg):
330 def log(*msg):
331 iolock.acquire()
331 iolock.acquire()
332 if verbose:
332 if verbose:
333 print verbose,
333 print verbose,
334 for m in msg:
334 for m in msg:
335 print m,
335 print m,
336 print
336 print
337 sys.stdout.flush()
337 sys.stdout.flush()
338 iolock.release()
338 iolock.release()
339
339
340 def findprogram(program):
340 def findprogram(program):
341 """Search PATH for a executable program"""
341 """Search PATH for a executable program"""
342 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
342 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
343 name = os.path.join(p, program)
343 name = os.path.join(p, program)
344 if os.name == 'nt' or os.access(name, os.X_OK):
344 if os.name == 'nt' or os.access(name, os.X_OK):
345 return name
345 return name
346 return None
346 return None
347
347
348 def createhgrc(path, options):
348 def createhgrc(path, options):
349 # create a fresh hgrc
349 # create a fresh hgrc
350 hgrc = open(path, 'w')
350 hgrc = open(path, 'w')
351 hgrc.write('[ui]\n')
351 hgrc.write('[ui]\n')
352 hgrc.write('slash = True\n')
352 hgrc.write('slash = True\n')
353 hgrc.write('interactive = False\n')
353 hgrc.write('interactive = False\n')
354 hgrc.write('[defaults]\n')
354 hgrc.write('[defaults]\n')
355 hgrc.write('backout = -d "0 0"\n')
355 hgrc.write('backout = -d "0 0"\n')
356 hgrc.write('commit = -d "0 0"\n')
356 hgrc.write('commit = -d "0 0"\n')
357 hgrc.write('shelve = --date "0 0"\n')
357 hgrc.write('shelve = --date "0 0"\n')
358 hgrc.write('tag = -d "0 0"\n')
358 hgrc.write('tag = -d "0 0"\n')
359 if options.extra_config_opt:
359 if options.extra_config_opt:
360 for opt in options.extra_config_opt:
360 for opt in options.extra_config_opt:
361 section, key = opt.split('.', 1)
361 section, key = opt.split('.', 1)
362 assert '=' in key, ('extra config opt %s must '
362 assert '=' in key, ('extra config opt %s must '
363 'have an = for assignment' % opt)
363 'have an = for assignment' % opt)
364 hgrc.write('[%s]\n%s\n' % (section, key))
364 hgrc.write('[%s]\n%s\n' % (section, key))
365 hgrc.close()
365 hgrc.close()
366
366
367 def checktools():
367 def checktools():
368 # Before we go any further, check for pre-requisite tools
368 # Before we go any further, check for pre-requisite tools
369 # stuff from coreutils (cat, rm, etc) are not tested
369 # stuff from coreutils (cat, rm, etc) are not tested
370 for p in requiredtools:
370 for p in requiredtools:
371 if os.name == 'nt' and not p.endswith('.exe'):
371 if os.name == 'nt' and not p.endswith('.exe'):
372 p += '.exe'
372 p += '.exe'
373 found = findprogram(p)
373 found = findprogram(p)
374 if found:
374 if found:
375 vlog("# Found prerequisite", p, "at", found)
375 vlog("# Found prerequisite", p, "at", found)
376 else:
376 else:
377 print "WARNING: Did not find prerequisite tool: "+p
377 print "WARNING: Did not find prerequisite tool: "+p
378
378
379 def terminate(proc):
379 def terminate(proc):
380 """Terminate subprocess (with fallback for Python versions < 2.6)"""
380 """Terminate subprocess (with fallback for Python versions < 2.6)"""
381 vlog('# Terminating process %d' % proc.pid)
381 vlog('# Terminating process %d' % proc.pid)
382 try:
382 try:
383 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
383 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
384 except OSError:
384 except OSError:
385 pass
385 pass
386
386
387 def killdaemons(pidfile):
387 def killdaemons(pidfile):
388 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
388 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
389 logfn=vlog)
389 logfn=vlog)
390
390
391 def cleanup(options):
391 def cleanup(options):
392 if not options.keep_tmpdir:
392 if not options.keep_tmpdir:
393 vlog("# Cleaning up HGTMP", HGTMP)
393 vlog("# Cleaning up HGTMP", HGTMP)
394 shutil.rmtree(HGTMP, True)
394 shutil.rmtree(HGTMP, True)
395 for f in createdfiles:
395 for f in createdfiles:
396 try:
396 try:
397 os.remove(f)
397 os.remove(f)
398 except OSError:
398 except OSError:
399 pass
399 pass
400
400
401 def usecorrectpython():
401 def usecorrectpython():
402 # some tests run python interpreter. they must use same
402 # some tests run python interpreter. they must use same
403 # interpreter we use or bad things will happen.
403 # interpreter we use or bad things will happen.
404 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
404 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
405 if getattr(os, 'symlink', None):
405 if getattr(os, 'symlink', None):
406 vlog("# Making python executable in test path a symlink to '%s'" %
406 vlog("# Making python executable in test path a symlink to '%s'" %
407 sys.executable)
407 sys.executable)
408 mypython = os.path.join(TMPBINDIR, pyexename)
408 mypython = os.path.join(TMPBINDIR, pyexename)
409 try:
409 try:
410 if os.readlink(mypython) == sys.executable:
410 if os.readlink(mypython) == sys.executable:
411 return
411 return
412 os.unlink(mypython)
412 os.unlink(mypython)
413 except OSError, err:
413 except OSError, err:
414 if err.errno != errno.ENOENT:
414 if err.errno != errno.ENOENT:
415 raise
415 raise
416 if findprogram(pyexename) != sys.executable:
416 if findprogram(pyexename) != sys.executable:
417 try:
417 try:
418 os.symlink(sys.executable, mypython)
418 os.symlink(sys.executable, mypython)
419 createdfiles.append(mypython)
419 createdfiles.append(mypython)
420 except OSError, err:
420 except OSError, err:
421 # child processes may race, which is harmless
421 # child processes may race, which is harmless
422 if err.errno != errno.EEXIST:
422 if err.errno != errno.EEXIST:
423 raise
423 raise
424 else:
424 else:
425 exedir, exename = os.path.split(sys.executable)
425 exedir, exename = os.path.split(sys.executable)
426 vlog("# Modifying search path to find %s as %s in '%s'" %
426 vlog("# Modifying search path to find %s as %s in '%s'" %
427 (exename, pyexename, exedir))
427 (exename, pyexename, exedir))
428 path = os.environ['PATH'].split(os.pathsep)
428 path = os.environ['PATH'].split(os.pathsep)
429 while exedir in path:
429 while exedir in path:
430 path.remove(exedir)
430 path.remove(exedir)
431 os.environ['PATH'] = os.pathsep.join([exedir] + path)
431 os.environ['PATH'] = os.pathsep.join([exedir] + path)
432 if not findprogram(pyexename):
432 if not findprogram(pyexename):
433 print "WARNING: Cannot find %s in search path" % pyexename
433 print "WARNING: Cannot find %s in search path" % pyexename
434
434
435 def installhg(options):
435 def installhg(options):
436 vlog("# Performing temporary installation of HG")
436 vlog("# Performing temporary installation of HG")
437 installerrs = os.path.join("tests", "install.err")
437 installerrs = os.path.join("tests", "install.err")
438 compiler = ''
438 compiler = ''
439 if options.compiler:
439 if options.compiler:
440 compiler = '--compiler ' + options.compiler
440 compiler = '--compiler ' + options.compiler
441 pure = options.pure and "--pure" or ""
441 pure = options.pure and "--pure" or ""
442 py3 = ''
442 py3 = ''
443 if sys.version_info[0] == 3:
443 if sys.version_info[0] == 3:
444 py3 = '--c2to3'
444 py3 = '--c2to3'
445
445
446 # Run installer in hg root
446 # Run installer in hg root
447 script = os.path.realpath(sys.argv[0])
447 script = os.path.realpath(sys.argv[0])
448 hgroot = os.path.dirname(os.path.dirname(script))
448 hgroot = os.path.dirname(os.path.dirname(script))
449 os.chdir(hgroot)
449 os.chdir(hgroot)
450 nohome = '--home=""'
450 nohome = '--home=""'
451 if os.name == 'nt':
451 if os.name == 'nt':
452 # The --home="" trick works only on OS where os.sep == '/'
452 # The --home="" trick works only on OS where os.sep == '/'
453 # because of a distutils convert_path() fast-path. Avoid it at
453 # because of a distutils convert_path() fast-path. Avoid it at
454 # least on Windows for now, deal with .pydistutils.cfg bugs
454 # least on Windows for now, deal with .pydistutils.cfg bugs
455 # when they happen.
455 # when they happen.
456 nohome = ''
456 nohome = ''
457 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
457 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
458 ' build %(compiler)s --build-base="%(base)s"'
458 ' build %(compiler)s --build-base="%(base)s"'
459 ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"'
459 ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"'
460 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
460 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
461 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
461 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
462 'compiler': compiler, 'base': os.path.join(HGTMP, "build"),
462 'compiler': compiler, 'base': os.path.join(HGTMP, "build"),
463 'prefix': INST, 'libdir': PYTHONDIR, 'bindir': BINDIR,
463 'prefix': INST, 'libdir': PYTHONDIR, 'bindir': BINDIR,
464 'nohome': nohome, 'logfile': installerrs})
464 'nohome': nohome, 'logfile': installerrs})
465 vlog("# Running", cmd)
465 vlog("# Running", cmd)
466 if os.system(cmd) == 0:
466 if os.system(cmd) == 0:
467 if not options.verbose:
467 if not options.verbose:
468 os.remove(installerrs)
468 os.remove(installerrs)
469 else:
469 else:
470 f = open(installerrs)
470 f = open(installerrs)
471 for line in f:
471 for line in f:
472 print line,
472 print line,
473 f.close()
473 f.close()
474 sys.exit(1)
474 sys.exit(1)
475 os.chdir(TESTDIR)
475 os.chdir(TESTDIR)
476
476
477 usecorrectpython()
477 usecorrectpython()
478
478
479 if options.py3k_warnings and not options.anycoverage:
479 if options.py3k_warnings and not options.anycoverage:
480 vlog("# Updating hg command to enable Py3k Warnings switch")
480 vlog("# Updating hg command to enable Py3k Warnings switch")
481 f = open(os.path.join(BINDIR, 'hg'), 'r')
481 f = open(os.path.join(BINDIR, 'hg'), 'r')
482 lines = [line.rstrip() for line in f]
482 lines = [line.rstrip() for line in f]
483 lines[0] += ' -3'
483 lines[0] += ' -3'
484 f.close()
484 f.close()
485 f = open(os.path.join(BINDIR, 'hg'), 'w')
485 f = open(os.path.join(BINDIR, 'hg'), 'w')
486 for line in lines:
486 for line in lines:
487 f.write(line + '\n')
487 f.write(line + '\n')
488 f.close()
488 f.close()
489
489
490 hgbat = os.path.join(BINDIR, 'hg.bat')
490 hgbat = os.path.join(BINDIR, 'hg.bat')
491 if os.path.isfile(hgbat):
491 if os.path.isfile(hgbat):
492 # hg.bat expects to be put in bin/scripts while run-tests.py
492 # hg.bat expects to be put in bin/scripts while run-tests.py
493 # installation layout put it in bin/ directly. Fix it
493 # installation layout put it in bin/ directly. Fix it
494 f = open(hgbat, 'rb')
494 f = open(hgbat, 'rb')
495 data = f.read()
495 data = f.read()
496 f.close()
496 f.close()
497 if '"%~dp0..\python" "%~dp0hg" %*' in data:
497 if '"%~dp0..\python" "%~dp0hg" %*' in data:
498 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
498 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
499 '"%~dp0python" "%~dp0hg" %*')
499 '"%~dp0python" "%~dp0hg" %*')
500 f = open(hgbat, 'wb')
500 f = open(hgbat, 'wb')
501 f.write(data)
501 f.write(data)
502 f.close()
502 f.close()
503 else:
503 else:
504 print 'WARNING: cannot fix hg.bat reference to python.exe'
504 print 'WARNING: cannot fix hg.bat reference to python.exe'
505
505
506 if options.anycoverage:
506 if options.anycoverage:
507 custom = os.path.join(TESTDIR, 'sitecustomize.py')
507 custom = os.path.join(TESTDIR, 'sitecustomize.py')
508 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
508 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
509 vlog('# Installing coverage trigger to %s' % target)
509 vlog('# Installing coverage trigger to %s' % target)
510 shutil.copyfile(custom, target)
510 shutil.copyfile(custom, target)
511 rc = os.path.join(TESTDIR, '.coveragerc')
511 rc = os.path.join(TESTDIR, '.coveragerc')
512 vlog('# Installing coverage rc to %s' % rc)
512 vlog('# Installing coverage rc to %s' % rc)
513 os.environ['COVERAGE_PROCESS_START'] = rc
513 os.environ['COVERAGE_PROCESS_START'] = rc
514 fn = os.path.join(INST, '..', '.coverage')
514 fn = os.path.join(INST, '..', '.coverage')
515 os.environ['COVERAGE_FILE'] = fn
515 os.environ['COVERAGE_FILE'] = fn
516
516
517 def outputtimes(options):
517 def outputtimes(options):
518 vlog('# Producing time report')
518 vlog('# Producing time report')
519 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
519 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
520 cols = '%7.3f %s'
520 cols = '%7.3f %s'
521 print '\n%-7s %s' % ('Time', 'Test')
521 print '\n%-7s %s' % ('Time', 'Test')
522 for test, timetaken in times:
522 for test, timetaken in times:
523 print cols % (timetaken, test)
523 print cols % (timetaken, test)
524
524
525 def outputcoverage(options):
525 def outputcoverage(options):
526
526
527 vlog('# Producing coverage report')
527 vlog('# Producing coverage report')
528 os.chdir(PYTHONDIR)
528 os.chdir(PYTHONDIR)
529
529
530 def covrun(*args):
530 def covrun(*args):
531 cmd = 'coverage %s' % ' '.join(args)
531 cmd = 'coverage %s' % ' '.join(args)
532 vlog('# Running: %s' % cmd)
532 vlog('# Running: %s' % cmd)
533 os.system(cmd)
533 os.system(cmd)
534
534
535 covrun('-c')
535 covrun('-c')
536 omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
536 omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
537 covrun('-i', '-r', '"--omit=%s"' % omit) # report
537 covrun('-i', '-r', '"--omit=%s"' % omit) # report
538 if options.htmlcov:
538 if options.htmlcov:
539 htmldir = os.path.join(TESTDIR, 'htmlcov')
539 htmldir = os.path.join(TESTDIR, 'htmlcov')
540 covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
540 covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
541 if options.annotate:
541 if options.annotate:
542 adir = os.path.join(TESTDIR, 'annotated')
542 adir = os.path.join(TESTDIR, 'annotated')
543 if not os.path.isdir(adir):
543 if not os.path.isdir(adir):
544 os.mkdir(adir)
544 os.mkdir(adir)
545 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
545 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
546
546
547 class Test(object):
547 class Test(object):
548 """Encapsulates a single, runnable test.
548 """Encapsulates a single, runnable test.
549
549
550 Test instances can be run multiple times via run(). However, multiple
550 Test instances can be run multiple times via run(). However, multiple
551 runs cannot be run concurrently.
551 runs cannot be run concurrently.
552 """
552 """
553
553
554 def __init__(self, path, options, count):
554 def __init__(self, path, options, count):
555 self._path = path
555 self._path = path
556 self._options = options
556 self._options = options
557 self._count = count
557 self._count = count
558
558
559 self._threadtmp = os.path.join(HGTMP, 'child%d' % count)
559 self._threadtmp = os.path.join(HGTMP, 'child%d' % count)
560 os.mkdir(self._threadtmp)
560 os.mkdir(self._threadtmp)
561
561
562 def cleanup(self):
562 def cleanup(self):
563 if self._threadtmp and not self._options.keep_tmpdir:
563 if self._threadtmp and not self._options.keep_tmpdir:
564 shutil.rmtree(self._threadtmp, True)
564 shutil.rmtree(self._threadtmp, True)
565
565
566 def run(self, result, refpath):
566 def run(self, result, refpath):
567 testtmp = os.path.join(self._threadtmp, os.path.basename(self._path))
567 testtmp = os.path.join(self._threadtmp, os.path.basename(self._path))
568 os.mkdir(testtmp)
568 os.mkdir(testtmp)
569 replacements, port = self._getreplacements(testtmp)
569 replacements, port = self._getreplacements(testtmp)
570 env = self._getenv(testtmp, port)
570 env = self._getenv(testtmp, port)
571 createhgrc(env['HGRCPATH'], self._options)
571 createhgrc(env['HGRCPATH'], self._options)
572
572
573 starttime = time.time()
573 starttime = time.time()
574
574
575 def updateduration():
575 def updateduration():
576 result.duration = time.time() - starttime
576 result.duration = time.time() - starttime
577
577
578 try:
578 try:
579 ret, out = self._run(testtmp, replacements, env)
579 ret, out = self._run(testtmp, replacements, env)
580 updateduration()
580 updateduration()
581 result.ret = ret
581 result.ret = ret
582 result.out = out
582 result.out = out
583 except KeyboardInterrupt:
583 except KeyboardInterrupt:
584 updateduration()
584 updateduration()
585 result.interrupted = True
585 result.interrupted = True
586 except Exception, e:
586 except Exception, e:
587 updateduration()
587 updateduration()
588 result.exception = e
588 result.exception = e
589
589
590 killdaemons(env['DAEMON_PIDS'])
590 killdaemons(env['DAEMON_PIDS'])
591
591
592 # If we're not in --debug mode and reference output file exists,
592 # If we're not in --debug mode and reference output file exists,
593 # check test output against it.
593 # check test output against it.
594 if self._options.debug:
594 if self._options.debug:
595 result.refout = None # to match "out is None"
595 result.refout = None # to match "out is None"
596 elif os.path.exists(refpath):
596 elif os.path.exists(refpath):
597 f = open(refpath, 'r')
597 f = open(refpath, 'r')
598 result.refout = f.read().splitlines(True)
598 result.refout = f.read().splitlines(True)
599 f.close()
599 f.close()
600 else:
600 else:
601 result.refout = []
601 result.refout = []
602
602
603 if not self._options.keep_tmpdir:
603 if not self._options.keep_tmpdir:
604 shutil.rmtree(testtmp)
604 shutil.rmtree(testtmp)
605
605
606 def _run(self, testtmp, replacements, env):
606 def _run(self, testtmp, replacements, env):
607 raise NotImplemented('Subclasses must implement Test.run()')
607 raise NotImplemented('Subclasses must implement Test.run()')
608
608
609 def _getreplacements(self, testtmp):
609 def _getreplacements(self, testtmp):
610 port = self._options.port + self._count * 3
610 port = self._options.port + self._count * 3
611 r = [
611 r = [
612 (r':%s\b' % port, ':$HGPORT'),
612 (r':%s\b' % port, ':$HGPORT'),
613 (r':%s\b' % (port + 1), ':$HGPORT1'),
613 (r':%s\b' % (port + 1), ':$HGPORT1'),
614 (r':%s\b' % (port + 2), ':$HGPORT2'),
614 (r':%s\b' % (port + 2), ':$HGPORT2'),
615 ]
615 ]
616
616
617 if os.name == 'nt':
617 if os.name == 'nt':
618 r.append(
618 r.append(
619 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
619 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
620 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
620 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
621 for c in testtmp), '$TESTTMP'))
621 for c in testtmp), '$TESTTMP'))
622 else:
622 else:
623 r.append((re.escape(testtmp), '$TESTTMP'))
623 r.append((re.escape(testtmp), '$TESTTMP'))
624
624
625 return r, port
625 return r, port
626
626
627 def _getenv(self, testtmp, port):
627 def _getenv(self, testtmp, port):
628 env = os.environ.copy()
628 env = os.environ.copy()
629 env['TESTTMP'] = testtmp
629 env['TESTTMP'] = testtmp
630 env['HOME'] = testtmp
630 env['HOME'] = testtmp
631 env["HGPORT"] = str(port)
631 env["HGPORT"] = str(port)
632 env["HGPORT1"] = str(port + 1)
632 env["HGPORT1"] = str(port + 1)
633 env["HGPORT2"] = str(port + 2)
633 env["HGPORT2"] = str(port + 2)
634 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
634 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
635 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
635 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
636 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
636 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
637 env["HGMERGE"] = "internal:merge"
637 env["HGMERGE"] = "internal:merge"
638 env["HGUSER"] = "test"
638 env["HGUSER"] = "test"
639 env["HGENCODING"] = "ascii"
639 env["HGENCODING"] = "ascii"
640 env["HGENCODINGMODE"] = "strict"
640 env["HGENCODINGMODE"] = "strict"
641
641
642 # Reset some environment variables to well-known values so that
642 # Reset some environment variables to well-known values so that
643 # the tests produce repeatable output.
643 # the tests produce repeatable output.
644 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
644 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
645 env['TZ'] = 'GMT'
645 env['TZ'] = 'GMT'
646 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
646 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
647 env['COLUMNS'] = '80'
647 env['COLUMNS'] = '80'
648 env['TERM'] = 'xterm'
648 env['TERM'] = 'xterm'
649
649
650 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
650 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
651 'NO_PROXY').split():
651 'NO_PROXY').split():
652 if k in env:
652 if k in env:
653 del env[k]
653 del env[k]
654
654
655 # unset env related to hooks
655 # unset env related to hooks
656 for k in env.keys():
656 for k in env.keys():
657 if k.startswith('HG_'):
657 if k.startswith('HG_'):
658 del env[k]
658 del env[k]
659
659
660 return env
660 return env
661
661
662 class TestResult(object):
662 class TestResult(object):
663 """Holds the result of a test execution."""
663 """Holds the result of a test execution."""
664
664
665 def __init__(self):
665 def __init__(self):
666 self.ret = None
666 self.ret = None
667 self.out = None
667 self.out = None
668 self.duration = None
668 self.duration = None
669 self.interrupted = False
669 self.interrupted = False
670 self.exception = None
670 self.exception = None
671 self.refout = None
671 self.refout = None
672
672
673 @property
673 @property
674 def skipped(self):
674 def skipped(self):
675 """Whether the test was skipped."""
675 """Whether the test was skipped."""
676 return self.ret == SKIPPED_STATUS
676 return self.ret == SKIPPED_STATUS
677
677
678 class PythonTest(Test):
678 class PythonTest(Test):
679 """A Python-based test."""
679 """A Python-based test."""
680 def _run(self, testtmp, replacements, env):
680 def _run(self, testtmp, replacements, env):
681 py3kswitch = self._options.py3k_warnings and ' -3' or ''
681 py3kswitch = self._options.py3k_warnings and ' -3' or ''
682 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
682 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
683 vlog("# Running", cmd)
683 vlog("# Running", cmd)
684 if os.name == 'nt':
684 if os.name == 'nt':
685 replacements.append((r'\r\n', '\n'))
685 replacements.append((r'\r\n', '\n'))
686 return run(cmd, testtmp, self._options, replacements, env)
686 return run(cmd, testtmp, self._options, replacements, env)
687
687
688
688
689 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
689 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
690 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
690 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
691 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
691 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
692 escapemap.update({'\\': '\\\\', '\r': r'\r'})
692 escapemap.update({'\\': '\\\\', '\r': r'\r'})
693 def escapef(m):
693 def escapef(m):
694 return escapemap[m.group(0)]
694 return escapemap[m.group(0)]
695 def stringescape(s):
695 def stringescape(s):
696 return escapesub(escapef, s)
696 return escapesub(escapef, s)
697
697
698 def rematch(el, l):
698 def rematch(el, l):
699 try:
699 try:
700 # use \Z to ensure that the regex matches to the end of the string
700 # use \Z to ensure that the regex matches to the end of the string
701 if os.name == 'nt':
701 if os.name == 'nt':
702 return re.match(el + r'\r?\n\Z', l)
702 return re.match(el + r'\r?\n\Z', l)
703 return re.match(el + r'\n\Z', l)
703 return re.match(el + r'\n\Z', l)
704 except re.error:
704 except re.error:
705 # el is an invalid regex
705 # el is an invalid regex
706 return False
706 return False
707
707
708 def globmatch(el, l):
708 def globmatch(el, l):
709 # The only supported special characters are * and ? plus / which also
709 # The only supported special characters are * and ? plus / which also
710 # matches \ on windows. Escaping of these characters is supported.
710 # matches \ on windows. Escaping of these characters is supported.
711 if el + '\n' == l:
711 if el + '\n' == l:
712 if os.altsep:
712 if os.altsep:
713 # matching on "/" is not needed for this line
713 # matching on "/" is not needed for this line
714 return '-glob'
714 return '-glob'
715 return True
715 return True
716 i, n = 0, len(el)
716 i, n = 0, len(el)
717 res = ''
717 res = ''
718 while i < n:
718 while i < n:
719 c = el[i]
719 c = el[i]
720 i += 1
720 i += 1
721 if c == '\\' and el[i] in '*?\\/':
721 if c == '\\' and el[i] in '*?\\/':
722 res += el[i - 1:i + 1]
722 res += el[i - 1:i + 1]
723 i += 1
723 i += 1
724 elif c == '*':
724 elif c == '*':
725 res += '.*'
725 res += '.*'
726 elif c == '?':
726 elif c == '?':
727 res += '.'
727 res += '.'
728 elif c == '/' and os.altsep:
728 elif c == '/' and os.altsep:
729 res += '[/\\\\]'
729 res += '[/\\\\]'
730 else:
730 else:
731 res += re.escape(c)
731 res += re.escape(c)
732 return rematch(res, l)
732 return rematch(res, l)
733
733
734 def linematch(el, l):
734 def linematch(el, l):
735 if el == l: # perfect match (fast)
735 if el == l: # perfect match (fast)
736 return True
736 return True
737 if el:
737 if el:
738 if el.endswith(" (esc)\n"):
738 if el.endswith(" (esc)\n"):
739 el = el[:-7].decode('string-escape') + '\n'
739 el = el[:-7].decode('string-escape') + '\n'
740 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
740 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
741 return True
741 return True
742 if el.endswith(" (re)\n"):
742 if el.endswith(" (re)\n"):
743 return rematch(el[:-6], l)
743 return rematch(el[:-6], l)
744 if el.endswith(" (glob)\n"):
744 if el.endswith(" (glob)\n"):
745 return globmatch(el[:-8], l)
745 return globmatch(el[:-8], l)
746 if os.altsep and l.replace('\\', '/') == el:
746 if os.altsep and l.replace('\\', '/') == el:
747 return '+glob'
747 return '+glob'
748 return False
748 return False
749
749
750 def tsttest(test, wd, options, replacements, env):
750 def tsttest(t, test, wd, options, replacements, env):
751 # We generate a shell script which outputs unique markers to line
752 # up script results with our source. These markers include input
753 # line number and the last return code
754 salt = "SALT" + str(time.time())
755 def addsalt(line, inpython):
756 if inpython:
757 script.append('%s %d 0\n' % (salt, line))
758 else:
759 script.append('echo %s %s $?\n' % (salt, line))
760
761 # After we run the shell script, we re-unify the script output
762 # with non-active parts of the source, with synchronization by our
763 # SALT line number markers. The after table contains the
764 # non-active components, ordered by line number
765 after = {}
766 pos = prepos = -1
767
768 # Expected shell script output
769 expected = {}
770
771 # We keep track of whether or not we're in a Python block so we
772 # can generate the surrounding doctest magic
773 inpython = False
774
775 # True or False when in a true or false conditional section
776 skipping = None
777
778 def hghave(reqs):
779 # TODO: do something smarter when all other uses of hghave is gone
780 tdir = TESTDIR.replace('\\', '/')
781 proc = Popen4('%s -c "%s/hghave %s"' %
782 (options.shell, tdir, ' '.join(reqs)), wd, 0)
783 stdout, stderr = proc.communicate()
784 ret = proc.wait()
785 if wifexited(ret):
786 ret = os.WEXITSTATUS(ret)
787 if ret == 2:
788 print stdout
789 sys.exit(1)
790 return ret == 0
791
792 f = open(test)
751 f = open(test)
793 t = f.readlines()
752 tlines = f.readlines()
794 f.close()
753 f.close()
795
754
796 script = []
755 salt, script, after, expected = t._parsetest(tlines, wd)
797 if options.debug:
798 script.append('set -x\n')
799 if os.getenv('MSYSTEM'):
800 script.append('alias pwd="pwd -W"\n')
801 n = 0
802 for n, l in enumerate(t):
803 if not l.endswith('\n'):
804 l += '\n'
805 if l.startswith('#if'):
806 lsplit = l.split()
807 if len(lsplit) < 2 or lsplit[0] != '#if':
808 after.setdefault(pos, []).append(' !!! invalid #if\n')
809 if skipping is not None:
810 after.setdefault(pos, []).append(' !!! nested #if\n')
811 skipping = not hghave(lsplit[1:])
812 after.setdefault(pos, []).append(l)
813 elif l.startswith('#else'):
814 if skipping is None:
815 after.setdefault(pos, []).append(' !!! missing #if\n')
816 skipping = not skipping
817 after.setdefault(pos, []).append(l)
818 elif l.startswith('#endif'):
819 if skipping is None:
820 after.setdefault(pos, []).append(' !!! missing #if\n')
821 skipping = None
822 after.setdefault(pos, []).append(l)
823 elif skipping:
824 after.setdefault(pos, []).append(l)
825 elif l.startswith(' >>> '): # python inlines
826 after.setdefault(pos, []).append(l)
827 prepos = pos
828 pos = n
829 if not inpython:
830 # we've just entered a Python block, add the header
831 inpython = True
832 addsalt(prepos, False) # make sure we report the exit code
833 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
834 addsalt(n, True)
835 script.append(l[2:])
836 elif l.startswith(' ... '): # python inlines
837 after.setdefault(prepos, []).append(l)
838 script.append(l[2:])
839 elif l.startswith(' $ '): # commands
840 if inpython:
841 script.append("EOF\n")
842 inpython = False
843 after.setdefault(pos, []).append(l)
844 prepos = pos
845 pos = n
846 addsalt(n, False)
847 cmd = l[4:].split()
848 if len(cmd) == 2 and cmd[0] == 'cd':
849 l = ' $ cd %s || exit 1\n' % cmd[1]
850 script.append(l[4:])
851 elif l.startswith(' > '): # continuations
852 after.setdefault(prepos, []).append(l)
853 script.append(l[4:])
854 elif l.startswith(' '): # results
855 # queue up a list of expected results
856 expected.setdefault(pos, []).append(l[2:])
857 else:
858 if inpython:
859 script.append("EOF\n")
860 inpython = False
861 # non-command/result - queue up for merged output
862 after.setdefault(pos, []).append(l)
863
864 if inpython:
865 script.append("EOF\n")
866 if skipping is not None:
867 after.setdefault(pos, []).append(' !!! missing #endif\n')
868 addsalt(n + 1, False)
869
756
870 # Write out the script and execute it
757 # Write out the script and execute it
871 name = wd + '.sh'
758 name = wd + '.sh'
872 f = open(name, 'w')
759 f = open(name, 'w')
873 for l in script:
760 for l in script:
874 f.write(l)
761 f.write(l)
875 f.close()
762 f.close()
876
763
877 cmd = '%s "%s"' % (options.shell, name)
764 cmd = '%s "%s"' % (options.shell, name)
878 vlog("# Running", cmd)
765 vlog("# Running", cmd)
879 exitcode, output = run(cmd, wd, options, replacements, env)
766 exitcode, output = run(cmd, wd, options, replacements, env)
880 # do not merge output if skipped, return hghave message instead
767 # do not merge output if skipped, return hghave message instead
881 # similarly, with --debug, output is None
768 # similarly, with --debug, output is None
882 if exitcode == SKIPPED_STATUS or output is None:
769 if exitcode == SKIPPED_STATUS or output is None:
883 return exitcode, output
770 return exitcode, output
884
771
885 # Merge the script output back into a unified test
772 # Merge the script output back into a unified test
886
773
887 warnonly = 1 # 1: not yet, 2: yes, 3: for sure not
774 warnonly = 1 # 1: not yet, 2: yes, 3: for sure not
888 if exitcode != 0: # failure has been reported
775 if exitcode != 0: # failure has been reported
889 warnonly = 3 # set to "for sure not"
776 warnonly = 3 # set to "for sure not"
890 pos = -1
777 pos = -1
891 postout = []
778 postout = []
892 for l in output:
779 for l in output:
893 lout, lcmd = l, None
780 lout, lcmd = l, None
894 if salt in l:
781 if salt in l:
895 lout, lcmd = l.split(salt, 1)
782 lout, lcmd = l.split(salt, 1)
896
783
897 if lout:
784 if lout:
898 if not lout.endswith('\n'):
785 if not lout.endswith('\n'):
899 lout += ' (no-eol)\n'
786 lout += ' (no-eol)\n'
900
787
901 # find the expected output at the current position
788 # find the expected output at the current position
902 el = None
789 el = None
903 if pos in expected and expected[pos]:
790 if pos in expected and expected[pos]:
904 el = expected[pos].pop(0)
791 el = expected[pos].pop(0)
905
792
906 r = linematch(el, lout)
793 r = linematch(el, lout)
907 if isinstance(r, str):
794 if isinstance(r, str):
908 if r == '+glob':
795 if r == '+glob':
909 lout = el[:-1] + ' (glob)\n'
796 lout = el[:-1] + ' (glob)\n'
910 r = '' # warn only this line
797 r = '' # warn only this line
911 elif r == '-glob':
798 elif r == '-glob':
912 lout = ''.join(el.rsplit(' (glob)', 1))
799 lout = ''.join(el.rsplit(' (glob)', 1))
913 r = '' # warn only this line
800 r = '' # warn only this line
914 else:
801 else:
915 log('\ninfo, unknown linematch result: %r\n' % r)
802 log('\ninfo, unknown linematch result: %r\n' % r)
916 r = False
803 r = False
917 if r:
804 if r:
918 postout.append(" " + el)
805 postout.append(" " + el)
919 else:
806 else:
920 if needescape(lout):
807 if needescape(lout):
921 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
808 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
922 postout.append(" " + lout) # let diff deal with it
809 postout.append(" " + lout) # let diff deal with it
923 if r != '': # if line failed
810 if r != '': # if line failed
924 warnonly = 3 # set to "for sure not"
811 warnonly = 3 # set to "for sure not"
925 elif warnonly == 1: # is "not yet" (and line is warn only)
812 elif warnonly == 1: # is "not yet" (and line is warn only)
926 warnonly = 2 # set to "yes" do warn
813 warnonly = 2 # set to "yes" do warn
927
814
928 if lcmd:
815 if lcmd:
929 # add on last return code
816 # add on last return code
930 ret = int(lcmd.split()[1])
817 ret = int(lcmd.split()[1])
931 if ret != 0:
818 if ret != 0:
932 postout.append(" [%s]\n" % ret)
819 postout.append(" [%s]\n" % ret)
933 if pos in after:
820 if pos in after:
934 # merge in non-active test bits
821 # merge in non-active test bits
935 postout += after.pop(pos)
822 postout += after.pop(pos)
936 pos = int(lcmd.split()[0])
823 pos = int(lcmd.split()[0])
937
824
938 if pos in after:
825 if pos in after:
939 postout += after.pop(pos)
826 postout += after.pop(pos)
940
827
941 if warnonly == 2:
828 if warnonly == 2:
942 exitcode = False # set exitcode to warned
829 exitcode = False # set exitcode to warned
943 return exitcode, postout
830 return exitcode, postout
944
831
945 class TTest(Test):
832 class TTest(Test):
946 """A "t test" is a test backed by a .t file."""
833 """A "t test" is a test backed by a .t file."""
947
834
948 def _run(self, testtmp, replacements, env):
835 def _run(self, testtmp, replacements, env):
949 return tsttest(self._path, testtmp, self._options, replacements,
836 return tsttest(self, self._path, testtmp, self._options, replacements,
950 env)
837 env)
951
838
839 def _hghave(self, reqs, testtmp):
840 # TODO do something smarter when all other uses of hghave are gone.
841 tdir = TESTDIR.replace('\\', '/')
842 proc = Popen4('%s -c "%s/hghave %s"' %
843 (self._options.shell, tdir, ' '.join(reqs)),
844 testtmp, 0)
845 stdout, stderr = proc.communicate()
846 ret = proc.wait()
847 if wifexited(ret):
848 ret = os.WEXITSTATUS(ret)
849 if ret == 2:
850 print stdout
851 sys.exit(1)
852
853 return ret == 0
854
855 def _parsetest(self, lines, testtmp):
856 # We generate a shell script which outputs unique markers to line
857 # up script results with our source. These markers include input
858 # line number and the last return code.
859 salt = "SALT" + str(time.time())
860 def addsalt(line, inpython):
861 if inpython:
862 script.append('%s %d 0\n' % (salt, line))
863 else:
864 script.append('echo %s %s $?\n' % (salt, line))
865
866 script = []
867
868 # After we run the shell script, we re-unify the script output
869 # with non-active parts of the source, with synchronization by our
870 # SALT line number markers. The after table contains the non-active
871 # components, ordered by line number.
872 after = {}
873
874 # Expected shell script output.
875 expected = {}
876
877 pos = prepos = -1
878
879 # True or False when in a true or false conditional section
880 skipping = None
881
882 # We keep track of whether or not we're in a Python block so we
883 # can generate the surrounding doctest magic.
884 inpython = False
885
886 if self._options.debug:
887 script.append('set -x\n')
888 if os.getenv('MSYSTEM'):
889 script.append('alias pwd="pwd -W"\n')
890
891 for n, l in enumerate(lines):
892 if not l.endswith('\n'):
893 l += '\n'
894 if l.startswith('#if'):
895 lsplit = l.split()
896 if len(lsplit) < 2 or lsplit[0] != '#if':
897 after.setdefault(pos, []).append(' !!! invalid #if\n')
898 if skipping is not None:
899 after.setdefault(pos, []).append(' !!! nested #if\n')
900 skipping = not self._hghave(lsplit[1:], testtmp)
901 after.setdefault(pos, []).append(l)
902 elif l.startswith('#else'):
903 if skipping is None:
904 after.setdefault(pos, []).append(' !!! missing #if\n')
905 skipping = not skipping
906 after.setdefault(pos, []).append(l)
907 elif l.startswith('#endif'):
908 if skipping is None:
909 after.setdefault(pos, []).append(' !!! missing #if\n')
910 skipping = None
911 after.setdefault(pos, []).append(l)
912 elif skipping:
913 after.setdefault(pos, []).append(l)
914 elif l.startswith(' >>> '): # python inlines
915 after.setdefault(pos, []).append(l)
916 prepos = pos
917 pos = n
918 if not inpython:
919 # We've just entered a Python block. Add the header.
920 inpython = True
921 addsalt(prepos, False) # Make sure we report the exit code.
922 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
923 addsalt(n, True)
924 script.append(l[2:])
925 elif l.startswith(' ... '): # python inlines
926 after.setdefault(prepos, []).append(l)
927 script.append(l[2:])
928 elif l.startswith(' $ '): # commands
929 if inpython:
930 script.append('EOF\n')
931 inpython = False
932 after.setdefault(pos, []).append(l)
933 prepos = pos
934 pos = n
935 addsalt(n, False)
936 cmd = l[4:].split()
937 if len(cmd) == 2 and cmd[0] == 'cd':
938 l = ' $ cd %s || exit 1\n' % cmd[1]
939 script.append(l[4:])
940 elif l.startswith(' > '): # continuations
941 after.setdefault(prepos, []).append(l)
942 script.append(l[4:])
943 elif l.startswith(' '): # results
944 # Queue up a list of expected results.
945 expected.setdefault(pos, []).append(l[2:])
946 else:
947 if inpython:
948 script.append('EOF\n')
949 inpython = False
950 # Non-command/result. Queue up for merged output.
951 after.setdefault(pos, []).append(l)
952
953 if inpython:
954 script.append('EOF\n')
955 if skipping is not None:
956 after.setdefault(pos, []).append(' !!! missing #endif\n')
957 addsalt(n + 1, False)
958
959 return salt, script, after, expected
960
961
952 wifexited = getattr(os, "WIFEXITED", lambda x: False)
962 wifexited = getattr(os, "WIFEXITED", lambda x: False)
953 def run(cmd, wd, options, replacements, env):
963 def run(cmd, wd, options, replacements, env):
954 """Run command in a sub-process, capturing the output (stdout and stderr).
964 """Run command in a sub-process, capturing the output (stdout and stderr).
955 Return a tuple (exitcode, output). output is None in debug mode."""
965 Return a tuple (exitcode, output). output is None in debug mode."""
956 # TODO: Use subprocess.Popen if we're running on Python 2.4
966 # TODO: Use subprocess.Popen if we're running on Python 2.4
957 if options.debug:
967 if options.debug:
958 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
968 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
959 ret = proc.wait()
969 ret = proc.wait()
960 return (ret, None)
970 return (ret, None)
961
971
962 proc = Popen4(cmd, wd, options.timeout, env)
972 proc = Popen4(cmd, wd, options.timeout, env)
963 def cleanup():
973 def cleanup():
964 terminate(proc)
974 terminate(proc)
965 ret = proc.wait()
975 ret = proc.wait()
966 if ret == 0:
976 if ret == 0:
967 ret = signal.SIGTERM << 8
977 ret = signal.SIGTERM << 8
968 killdaemons(env['DAEMON_PIDS'])
978 killdaemons(env['DAEMON_PIDS'])
969 return ret
979 return ret
970
980
971 output = ''
981 output = ''
972 proc.tochild.close()
982 proc.tochild.close()
973
983
974 try:
984 try:
975 output = proc.fromchild.read()
985 output = proc.fromchild.read()
976 except KeyboardInterrupt:
986 except KeyboardInterrupt:
977 vlog('# Handling keyboard interrupt')
987 vlog('# Handling keyboard interrupt')
978 cleanup()
988 cleanup()
979 raise
989 raise
980
990
981 ret = proc.wait()
991 ret = proc.wait()
982 if wifexited(ret):
992 if wifexited(ret):
983 ret = os.WEXITSTATUS(ret)
993 ret = os.WEXITSTATUS(ret)
984
994
985 if proc.timeout:
995 if proc.timeout:
986 ret = 'timeout'
996 ret = 'timeout'
987
997
988 if ret:
998 if ret:
989 killdaemons(env['DAEMON_PIDS'])
999 killdaemons(env['DAEMON_PIDS'])
990
1000
991 if abort:
1001 if abort:
992 raise KeyboardInterrupt()
1002 raise KeyboardInterrupt()
993
1003
994 for s, r in replacements:
1004 for s, r in replacements:
995 output = re.sub(s, r, output)
1005 output = re.sub(s, r, output)
996 return ret, output.splitlines(True)
1006 return ret, output.splitlines(True)
997
1007
998 def runone(options, test, count):
1008 def runone(options, test, count):
999 '''returns a result element: (code, test, msg)'''
1009 '''returns a result element: (code, test, msg)'''
1000
1010
1001 def skip(msg):
1011 def skip(msg):
1002 if options.verbose:
1012 if options.verbose:
1003 log("\nSkipping %s: %s" % (testpath, msg))
1013 log("\nSkipping %s: %s" % (testpath, msg))
1004 return 's', test, msg
1014 return 's', test, msg
1005
1015
1006 def fail(msg, ret):
1016 def fail(msg, ret):
1007 warned = ret is False
1017 warned = ret is False
1008 if not options.nodiff:
1018 if not options.nodiff:
1009 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', test, msg))
1019 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', test, msg))
1010 if (not ret and options.interactive
1020 if (not ret and options.interactive
1011 and os.path.exists(testpath + ".err")):
1021 and os.path.exists(testpath + ".err")):
1012 iolock.acquire()
1022 iolock.acquire()
1013 print "Accept this change? [n] ",
1023 print "Accept this change? [n] ",
1014 answer = sys.stdin.readline().strip()
1024 answer = sys.stdin.readline().strip()
1015 iolock.release()
1025 iolock.release()
1016 if answer.lower() in "y yes".split():
1026 if answer.lower() in "y yes".split():
1017 if test.endswith(".t"):
1027 if test.endswith(".t"):
1018 rename(testpath + ".err", testpath)
1028 rename(testpath + ".err", testpath)
1019 else:
1029 else:
1020 rename(testpath + ".err", testpath + ".out")
1030 rename(testpath + ".err", testpath + ".out")
1021 return '.', test, ''
1031 return '.', test, ''
1022 return warned and '~' or '!', test, msg
1032 return warned and '~' or '!', test, msg
1023
1033
1024 def success():
1034 def success():
1025 return '.', test, ''
1035 return '.', test, ''
1026
1036
1027 def ignore(msg):
1037 def ignore(msg):
1028 return 'i', test, msg
1038 return 'i', test, msg
1029
1039
1030 def describe(ret):
1040 def describe(ret):
1031 if ret < 0:
1041 if ret < 0:
1032 return 'killed by signal %d' % -ret
1042 return 'killed by signal %d' % -ret
1033 return 'returned error code %d' % ret
1043 return 'returned error code %d' % ret
1034
1044
1035 testpath = os.path.join(TESTDIR, test)
1045 testpath = os.path.join(TESTDIR, test)
1036 err = os.path.join(TESTDIR, test + ".err")
1046 err = os.path.join(TESTDIR, test + ".err")
1037 lctest = test.lower()
1047 lctest = test.lower()
1038
1048
1039 if not os.path.exists(testpath):
1049 if not os.path.exists(testpath):
1040 return skip("doesn't exist")
1050 return skip("doesn't exist")
1041
1051
1042 if not (options.whitelisted and test in options.whitelisted):
1052 if not (options.whitelisted and test in options.whitelisted):
1043 if options.blacklist and test in options.blacklist:
1053 if options.blacklist and test in options.blacklist:
1044 return skip("blacklisted")
1054 return skip("blacklisted")
1045
1055
1046 if options.retest and not os.path.exists(test + ".err"):
1056 if options.retest and not os.path.exists(test + ".err"):
1047 return ignore("not retesting")
1057 return ignore("not retesting")
1048
1058
1049 if options.keywords:
1059 if options.keywords:
1050 fp = open(test)
1060 fp = open(test)
1051 t = fp.read().lower() + test.lower()
1061 t = fp.read().lower() + test.lower()
1052 fp.close()
1062 fp.close()
1053 for k in options.keywords.lower().split():
1063 for k in options.keywords.lower().split():
1054 if k in t:
1064 if k in t:
1055 break
1065 break
1056 else:
1066 else:
1057 return ignore("doesn't match keyword")
1067 return ignore("doesn't match keyword")
1058
1068
1059 if not os.path.basename(lctest).startswith("test-"):
1069 if not os.path.basename(lctest).startswith("test-"):
1060 return skip("not a test file")
1070 return skip("not a test file")
1061 for ext, cls, out in testtypes:
1071 for ext, cls, out in testtypes:
1062 if lctest.endswith(ext):
1072 if lctest.endswith(ext):
1063 runner = cls
1073 runner = cls
1064 ref = os.path.join(TESTDIR, test + out)
1074 ref = os.path.join(TESTDIR, test + out)
1065 break
1075 break
1066 else:
1076 else:
1067 return skip("unknown test type")
1077 return skip("unknown test type")
1068
1078
1069 vlog("# Test", test)
1079 vlog("# Test", test)
1070
1080
1071 if os.path.exists(err):
1081 if os.path.exists(err):
1072 os.remove(err) # Remove any previous output files
1082 os.remove(err) # Remove any previous output files
1073
1083
1074 t = runner(testpath, options, count)
1084 t = runner(testpath, options, count)
1075 res = TestResult()
1085 res = TestResult()
1076 t.run(res, ref)
1086 t.run(res, ref)
1077 t.cleanup()
1087 t.cleanup()
1078
1088
1079 if res.interrupted:
1089 if res.interrupted:
1080 log('INTERRUPTED: %s (after %d seconds)' % (test, res.duration))
1090 log('INTERRUPTED: %s (after %d seconds)' % (test, res.duration))
1081 raise KeyboardInterrupt()
1091 raise KeyboardInterrupt()
1082
1092
1083 if res.exception:
1093 if res.exception:
1084 return fail('Exception during execution: %s' % res.exception, 255)
1094 return fail('Exception during execution: %s' % res.exception, 255)
1085
1095
1086 ret = res.ret
1096 ret = res.ret
1087 out = res.out
1097 out = res.out
1088
1098
1089 times.append((test, res.duration))
1099 times.append((test, res.duration))
1090 vlog("# Ret was:", ret)
1100 vlog("# Ret was:", ret)
1091
1101
1092 skipped = res.skipped
1102 skipped = res.skipped
1093 refout = res.refout
1103 refout = res.refout
1094
1104
1095 if (ret != 0 or out != refout) and not skipped and not options.debug:
1105 if (ret != 0 or out != refout) and not skipped and not options.debug:
1096 # Save errors to a file for diagnosis
1106 # Save errors to a file for diagnosis
1097 f = open(err, "wb")
1107 f = open(err, "wb")
1098 for line in out:
1108 for line in out:
1099 f.write(line)
1109 f.write(line)
1100 f.close()
1110 f.close()
1101
1111
1102 if skipped:
1112 if skipped:
1103 if out is None: # debug mode: nothing to parse
1113 if out is None: # debug mode: nothing to parse
1104 missing = ['unknown']
1114 missing = ['unknown']
1105 failed = None
1115 failed = None
1106 else:
1116 else:
1107 missing, failed = parsehghaveoutput(out)
1117 missing, failed = parsehghaveoutput(out)
1108 if not missing:
1118 if not missing:
1109 missing = ['irrelevant']
1119 missing = ['irrelevant']
1110 if failed:
1120 if failed:
1111 result = fail("hghave failed checking for %s" % failed[-1], ret)
1121 result = fail("hghave failed checking for %s" % failed[-1], ret)
1112 skipped = False
1122 skipped = False
1113 else:
1123 else:
1114 result = skip(missing[-1])
1124 result = skip(missing[-1])
1115 elif ret == 'timeout':
1125 elif ret == 'timeout':
1116 result = fail("timed out", ret)
1126 result = fail("timed out", ret)
1117 elif out != refout:
1127 elif out != refout:
1118 info = {}
1128 info = {}
1119 if not options.nodiff:
1129 if not options.nodiff:
1120 iolock.acquire()
1130 iolock.acquire()
1121 if options.view:
1131 if options.view:
1122 os.system("%s %s %s" % (options.view, ref, err))
1132 os.system("%s %s %s" % (options.view, ref, err))
1123 else:
1133 else:
1124 info = showdiff(refout, out, ref, err)
1134 info = showdiff(refout, out, ref, err)
1125 iolock.release()
1135 iolock.release()
1126 msg = ""
1136 msg = ""
1127 if info.get('servefail'): msg += "serve failed and "
1137 if info.get('servefail'): msg += "serve failed and "
1128 if ret:
1138 if ret:
1129 msg += "output changed and " + describe(ret)
1139 msg += "output changed and " + describe(ret)
1130 else:
1140 else:
1131 msg += "output changed"
1141 msg += "output changed"
1132 result = fail(msg, ret)
1142 result = fail(msg, ret)
1133 elif ret:
1143 elif ret:
1134 result = fail(describe(ret), ret)
1144 result = fail(describe(ret), ret)
1135 else:
1145 else:
1136 result = success()
1146 result = success()
1137
1147
1138 if not options.verbose:
1148 if not options.verbose:
1139 iolock.acquire()
1149 iolock.acquire()
1140 sys.stdout.write(result[0])
1150 sys.stdout.write(result[0])
1141 sys.stdout.flush()
1151 sys.stdout.flush()
1142 iolock.release()
1152 iolock.release()
1143
1153
1144 return result
1154 return result
1145
1155
1146 _hgpath = None
1156 _hgpath = None
1147
1157
1148 def _gethgpath():
1158 def _gethgpath():
1149 """Return the path to the mercurial package that is actually found by
1159 """Return the path to the mercurial package that is actually found by
1150 the current Python interpreter."""
1160 the current Python interpreter."""
1151 global _hgpath
1161 global _hgpath
1152 if _hgpath is not None:
1162 if _hgpath is not None:
1153 return _hgpath
1163 return _hgpath
1154
1164
1155 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1165 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1156 pipe = os.popen(cmd % PYTHON)
1166 pipe = os.popen(cmd % PYTHON)
1157 try:
1167 try:
1158 _hgpath = pipe.read().strip()
1168 _hgpath = pipe.read().strip()
1159 finally:
1169 finally:
1160 pipe.close()
1170 pipe.close()
1161 return _hgpath
1171 return _hgpath
1162
1172
1163 def _checkhglib(verb):
1173 def _checkhglib(verb):
1164 """Ensure that the 'mercurial' package imported by python is
1174 """Ensure that the 'mercurial' package imported by python is
1165 the one we expect it to be. If not, print a warning to stderr."""
1175 the one we expect it to be. If not, print a warning to stderr."""
1166 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1176 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1167 actualhg = _gethgpath()
1177 actualhg = _gethgpath()
1168 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1178 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1169 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1179 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1170 ' (expected %s)\n'
1180 ' (expected %s)\n'
1171 % (verb, actualhg, expecthg))
1181 % (verb, actualhg, expecthg))
1172
1182
1173 results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
1183 results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
1174 times = []
1184 times = []
1175 iolock = threading.Lock()
1185 iolock = threading.Lock()
1176 abort = False
1186 abort = False
1177
1187
1178 def scheduletests(options, tests):
1188 def scheduletests(options, tests):
1179 jobs = options.jobs
1189 jobs = options.jobs
1180 done = queue.Queue()
1190 done = queue.Queue()
1181 running = 0
1191 running = 0
1182 count = 0
1192 count = 0
1183 global abort
1193 global abort
1184
1194
1185 def job(test, count):
1195 def job(test, count):
1186 try:
1196 try:
1187 done.put(runone(options, test, count))
1197 done.put(runone(options, test, count))
1188 except KeyboardInterrupt:
1198 except KeyboardInterrupt:
1189 pass
1199 pass
1190 except: # re-raises
1200 except: # re-raises
1191 done.put(('!', test, 'run-test raised an error, see traceback'))
1201 done.put(('!', test, 'run-test raised an error, see traceback'))
1192 raise
1202 raise
1193
1203
1194 try:
1204 try:
1195 while tests or running:
1205 while tests or running:
1196 if not done.empty() or running == jobs or not tests:
1206 if not done.empty() or running == jobs or not tests:
1197 try:
1207 try:
1198 code, test, msg = done.get(True, 1)
1208 code, test, msg = done.get(True, 1)
1199 results[code].append((test, msg))
1209 results[code].append((test, msg))
1200 if options.first and code not in '.si':
1210 if options.first and code not in '.si':
1201 break
1211 break
1202 except queue.Empty:
1212 except queue.Empty:
1203 continue
1213 continue
1204 running -= 1
1214 running -= 1
1205 if tests and not running == jobs:
1215 if tests and not running == jobs:
1206 test = tests.pop(0)
1216 test = tests.pop(0)
1207 if options.loop:
1217 if options.loop:
1208 tests.append(test)
1218 tests.append(test)
1209 t = threading.Thread(target=job, name=test, args=(test, count))
1219 t = threading.Thread(target=job, name=test, args=(test, count))
1210 t.start()
1220 t.start()
1211 running += 1
1221 running += 1
1212 count += 1
1222 count += 1
1213 except KeyboardInterrupt:
1223 except KeyboardInterrupt:
1214 abort = True
1224 abort = True
1215
1225
1216 def runtests(options, tests):
1226 def runtests(options, tests):
1217 try:
1227 try:
1218 if INST:
1228 if INST:
1219 installhg(options)
1229 installhg(options)
1220 _checkhglib("Testing")
1230 _checkhglib("Testing")
1221 else:
1231 else:
1222 usecorrectpython()
1232 usecorrectpython()
1223
1233
1224 if options.restart:
1234 if options.restart:
1225 orig = list(tests)
1235 orig = list(tests)
1226 while tests:
1236 while tests:
1227 if os.path.exists(tests[0] + ".err"):
1237 if os.path.exists(tests[0] + ".err"):
1228 break
1238 break
1229 tests.pop(0)
1239 tests.pop(0)
1230 if not tests:
1240 if not tests:
1231 print "running all tests"
1241 print "running all tests"
1232 tests = orig
1242 tests = orig
1233
1243
1234 scheduletests(options, tests)
1244 scheduletests(options, tests)
1235
1245
1236 failed = len(results['!'])
1246 failed = len(results['!'])
1237 warned = len(results['~'])
1247 warned = len(results['~'])
1238 tested = len(results['.']) + failed + warned
1248 tested = len(results['.']) + failed + warned
1239 skipped = len(results['s'])
1249 skipped = len(results['s'])
1240 ignored = len(results['i'])
1250 ignored = len(results['i'])
1241
1251
1242 print
1252 print
1243 if not options.noskips:
1253 if not options.noskips:
1244 for s in results['s']:
1254 for s in results['s']:
1245 print "Skipped %s: %s" % s
1255 print "Skipped %s: %s" % s
1246 for s in results['~']:
1256 for s in results['~']:
1247 print "Warned %s: %s" % s
1257 print "Warned %s: %s" % s
1248 for s in results['!']:
1258 for s in results['!']:
1249 print "Failed %s: %s" % s
1259 print "Failed %s: %s" % s
1250 _checkhglib("Tested")
1260 _checkhglib("Tested")
1251 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1261 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1252 tested, skipped + ignored, warned, failed)
1262 tested, skipped + ignored, warned, failed)
1253 if results['!']:
1263 if results['!']:
1254 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1264 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1255 if options.time:
1265 if options.time:
1256 outputtimes(options)
1266 outputtimes(options)
1257
1267
1258 if options.anycoverage:
1268 if options.anycoverage:
1259 outputcoverage(options)
1269 outputcoverage(options)
1260 except KeyboardInterrupt:
1270 except KeyboardInterrupt:
1261 failed = True
1271 failed = True
1262 print "\ninterrupted!"
1272 print "\ninterrupted!"
1263
1273
1264 if failed:
1274 if failed:
1265 return 1
1275 return 1
1266 if warned:
1276 if warned:
1267 return 80
1277 return 80
1268
1278
1269 testtypes = [('.py', PythonTest, '.out'),
1279 testtypes = [('.py', PythonTest, '.out'),
1270 ('.t', TTest, '')]
1280 ('.t', TTest, '')]
1271
1281
1272 def main(args, parser=None):
1282 def main(args, parser=None):
1273 parser = parser or getparser()
1283 parser = parser or getparser()
1274 (options, args) = parseargs(args, parser)
1284 (options, args) = parseargs(args, parser)
1275 os.umask(022)
1285 os.umask(022)
1276
1286
1277 checktools()
1287 checktools()
1278
1288
1279 if not args:
1289 if not args:
1280 if options.changed:
1290 if options.changed:
1281 proc = Popen4('hg st --rev "%s" -man0 .' % options.changed,
1291 proc = Popen4('hg st --rev "%s" -man0 .' % options.changed,
1282 None, 0)
1292 None, 0)
1283 stdout, stderr = proc.communicate()
1293 stdout, stderr = proc.communicate()
1284 args = stdout.strip('\0').split('\0')
1294 args = stdout.strip('\0').split('\0')
1285 else:
1295 else:
1286 args = os.listdir(".")
1296 args = os.listdir(".")
1287
1297
1288 tests = [t for t in args
1298 tests = [t for t in args
1289 if os.path.basename(t).startswith("test-")
1299 if os.path.basename(t).startswith("test-")
1290 and (t.endswith(".py") or t.endswith(".t"))]
1300 and (t.endswith(".py") or t.endswith(".t"))]
1291
1301
1292 if options.random:
1302 if options.random:
1293 random.shuffle(tests)
1303 random.shuffle(tests)
1294 else:
1304 else:
1295 # keywords for slow tests
1305 # keywords for slow tests
1296 slow = 'svn gendoc check-code-hg'.split()
1306 slow = 'svn gendoc check-code-hg'.split()
1297 def sortkey(f):
1307 def sortkey(f):
1298 # run largest tests first, as they tend to take the longest
1308 # run largest tests first, as they tend to take the longest
1299 try:
1309 try:
1300 val = -os.stat(f).st_size
1310 val = -os.stat(f).st_size
1301 except OSError, e:
1311 except OSError, e:
1302 if e.errno != errno.ENOENT:
1312 if e.errno != errno.ENOENT:
1303 raise
1313 raise
1304 return -1e9 # file does not exist, tell early
1314 return -1e9 # file does not exist, tell early
1305 for kw in slow:
1315 for kw in slow:
1306 if kw in f:
1316 if kw in f:
1307 val *= 10
1317 val *= 10
1308 return val
1318 return val
1309 tests.sort(key=sortkey)
1319 tests.sort(key=sortkey)
1310
1320
1311 if 'PYTHONHASHSEED' not in os.environ:
1321 if 'PYTHONHASHSEED' not in os.environ:
1312 # use a random python hash seed all the time
1322 # use a random python hash seed all the time
1313 # we do the randomness ourself to know what seed is used
1323 # we do the randomness ourself to know what seed is used
1314 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1324 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1315
1325
1316 global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
1326 global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
1317 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1327 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1318 if options.tmpdir:
1328 if options.tmpdir:
1319 options.keep_tmpdir = True
1329 options.keep_tmpdir = True
1320 tmpdir = options.tmpdir
1330 tmpdir = options.tmpdir
1321 if os.path.exists(tmpdir):
1331 if os.path.exists(tmpdir):
1322 # Meaning of tmpdir has changed since 1.3: we used to create
1332 # Meaning of tmpdir has changed since 1.3: we used to create
1323 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1333 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1324 # tmpdir already exists.
1334 # tmpdir already exists.
1325 print "error: temp dir %r already exists" % tmpdir
1335 print "error: temp dir %r already exists" % tmpdir
1326 return 1
1336 return 1
1327
1337
1328 # Automatically removing tmpdir sounds convenient, but could
1338 # Automatically removing tmpdir sounds convenient, but could
1329 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1339 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1330 # or "--tmpdir=$HOME".
1340 # or "--tmpdir=$HOME".
1331 #vlog("# Removing temp dir", tmpdir)
1341 #vlog("# Removing temp dir", tmpdir)
1332 #shutil.rmtree(tmpdir)
1342 #shutil.rmtree(tmpdir)
1333 os.makedirs(tmpdir)
1343 os.makedirs(tmpdir)
1334 else:
1344 else:
1335 d = None
1345 d = None
1336 if os.name == 'nt':
1346 if os.name == 'nt':
1337 # without this, we get the default temp dir location, but
1347 # without this, we get the default temp dir location, but
1338 # in all lowercase, which causes troubles with paths (issue3490)
1348 # in all lowercase, which causes troubles with paths (issue3490)
1339 d = os.getenv('TMP')
1349 d = os.getenv('TMP')
1340 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1350 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1341 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1351 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1342
1352
1343 if options.with_hg:
1353 if options.with_hg:
1344 INST = None
1354 INST = None
1345 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1355 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1346 TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
1356 TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
1347 os.makedirs(TMPBINDIR)
1357 os.makedirs(TMPBINDIR)
1348
1358
1349 # This looks redundant with how Python initializes sys.path from
1359 # This looks redundant with how Python initializes sys.path from
1350 # the location of the script being executed. Needed because the
1360 # the location of the script being executed. Needed because the
1351 # "hg" specified by --with-hg is not the only Python script
1361 # "hg" specified by --with-hg is not the only Python script
1352 # executed in the test suite that needs to import 'mercurial'
1362 # executed in the test suite that needs to import 'mercurial'
1353 # ... which means it's not really redundant at all.
1363 # ... which means it's not really redundant at all.
1354 PYTHONDIR = BINDIR
1364 PYTHONDIR = BINDIR
1355 else:
1365 else:
1356 INST = os.path.join(HGTMP, "install")
1366 INST = os.path.join(HGTMP, "install")
1357 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1367 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1358 TMPBINDIR = BINDIR
1368 TMPBINDIR = BINDIR
1359 PYTHONDIR = os.path.join(INST, "lib", "python")
1369 PYTHONDIR = os.path.join(INST, "lib", "python")
1360
1370
1361 os.environ["BINDIR"] = BINDIR
1371 os.environ["BINDIR"] = BINDIR
1362 os.environ["PYTHON"] = PYTHON
1372 os.environ["PYTHON"] = PYTHON
1363
1373
1364 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1374 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1365 if TMPBINDIR != BINDIR:
1375 if TMPBINDIR != BINDIR:
1366 path = [TMPBINDIR] + path
1376 path = [TMPBINDIR] + path
1367 os.environ["PATH"] = os.pathsep.join(path)
1377 os.environ["PATH"] = os.pathsep.join(path)
1368
1378
1369 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1379 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1370 # can run .../tests/run-tests.py test-foo where test-foo
1380 # can run .../tests/run-tests.py test-foo where test-foo
1371 # adds an extension to HGRC. Also include run-test.py directory to import
1381 # adds an extension to HGRC. Also include run-test.py directory to import
1372 # modules like heredoctest.
1382 # modules like heredoctest.
1373 pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
1383 pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
1374 # We have to augment PYTHONPATH, rather than simply replacing
1384 # We have to augment PYTHONPATH, rather than simply replacing
1375 # it, in case external libraries are only available via current
1385 # it, in case external libraries are only available via current
1376 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1386 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1377 # are in /opt/subversion.)
1387 # are in /opt/subversion.)
1378 oldpypath = os.environ.get(IMPL_PATH)
1388 oldpypath = os.environ.get(IMPL_PATH)
1379 if oldpypath:
1389 if oldpypath:
1380 pypath.append(oldpypath)
1390 pypath.append(oldpypath)
1381 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1391 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1382
1392
1383 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1393 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1384
1394
1385 vlog("# Using TESTDIR", TESTDIR)
1395 vlog("# Using TESTDIR", TESTDIR)
1386 vlog("# Using HGTMP", HGTMP)
1396 vlog("# Using HGTMP", HGTMP)
1387 vlog("# Using PATH", os.environ["PATH"])
1397 vlog("# Using PATH", os.environ["PATH"])
1388 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1398 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1389
1399
1390 try:
1400 try:
1391 return runtests(options, tests) or 0
1401 return runtests(options, tests) or 0
1392 finally:
1402 finally:
1393 time.sleep(.1)
1403 time.sleep(.1)
1394 cleanup(options)
1404 cleanup(options)
1395
1405
1396 if __name__ == '__main__':
1406 if __name__ == '__main__':
1397 sys.exit(main(sys.argv[1:]))
1407 sys.exit(main(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now