##// END OF EJS Templates
run-tests: remove threadtmp in Test.cleanup()...
Gregory Szorc -
r21310:af9a0495 default
parent child Browse files
Show More
@@ -1,1397 +1,1400 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
562 def cleanup(self):
563 if self._threadtmp and not self._options.keep_tmpdir:
564 shutil.rmtree(self._threadtmp, True)
561
565
562 def run(self, result, refpath):
566 def run(self, result, refpath):
563 testtmp = os.path.join(self.threadtmp, os.path.basename(self._path))
567 testtmp = os.path.join(self._threadtmp, os.path.basename(self._path))
564 os.mkdir(testtmp)
568 os.mkdir(testtmp)
565 replacements, port = self._getreplacements(testtmp)
569 replacements, port = self._getreplacements(testtmp)
566 env = self._getenv(testtmp, port)
570 env = self._getenv(testtmp, port)
567 createhgrc(env['HGRCPATH'], self._options)
571 createhgrc(env['HGRCPATH'], self._options)
568
572
569 starttime = time.time()
573 starttime = time.time()
570
574
571 def updateduration():
575 def updateduration():
572 result.duration = time.time() - starttime
576 result.duration = time.time() - starttime
573
577
574 try:
578 try:
575 ret, out = self._run(testtmp, replacements, env)
579 ret, out = self._run(testtmp, replacements, env)
576 updateduration()
580 updateduration()
577 result.ret = ret
581 result.ret = ret
578 result.out = out
582 result.out = out
579 except KeyboardInterrupt:
583 except KeyboardInterrupt:
580 updateduration()
584 updateduration()
581 result.interrupted = True
585 result.interrupted = True
582 except Exception, e:
586 except Exception, e:
583 updateduration()
587 updateduration()
584 result.exception = e
588 result.exception = e
585
589
586 killdaemons(env['DAEMON_PIDS'])
590 killdaemons(env['DAEMON_PIDS'])
587
591
588 # 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,
589 # check test output against it.
593 # check test output against it.
590 if self._options.debug:
594 if self._options.debug:
591 result.refout = None # to match "out is None"
595 result.refout = None # to match "out is None"
592 elif os.path.exists(refpath):
596 elif os.path.exists(refpath):
593 f = open(refpath, 'r')
597 f = open(refpath, 'r')
594 result.refout = f.read().splitlines(True)
598 result.refout = f.read().splitlines(True)
595 f.close()
599 f.close()
596 else:
600 else:
597 result.refout = []
601 result.refout = []
598
602
599 if not self._options.keep_tmpdir:
603 if not self._options.keep_tmpdir:
600 shutil.rmtree(testtmp)
604 shutil.rmtree(testtmp)
601
605
602 def _run(self, testtmp, replacements, env):
606 def _run(self, testtmp, replacements, env):
603 raise NotImplemented('Subclasses must implement Test.run()')
607 raise NotImplemented('Subclasses must implement Test.run()')
604
608
605 def _getreplacements(self, testtmp):
609 def _getreplacements(self, testtmp):
606 port = self._options.port + self._count * 3
610 port = self._options.port + self._count * 3
607 r = [
611 r = [
608 (r':%s\b' % port, ':$HGPORT'),
612 (r':%s\b' % port, ':$HGPORT'),
609 (r':%s\b' % (port + 1), ':$HGPORT1'),
613 (r':%s\b' % (port + 1), ':$HGPORT1'),
610 (r':%s\b' % (port + 2), ':$HGPORT2'),
614 (r':%s\b' % (port + 2), ':$HGPORT2'),
611 ]
615 ]
612
616
613 if os.name == 'nt':
617 if os.name == 'nt':
614 r.append(
618 r.append(
615 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
619 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
616 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
620 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
617 for c in testtmp), '$TESTTMP'))
621 for c in testtmp), '$TESTTMP'))
618 else:
622 else:
619 r.append((re.escape(testtmp), '$TESTTMP'))
623 r.append((re.escape(testtmp), '$TESTTMP'))
620
624
621 return r, port
625 return r, port
622
626
623 def _getenv(self, testtmp, port):
627 def _getenv(self, testtmp, port):
624 env = os.environ.copy()
628 env = os.environ.copy()
625 env['TESTTMP'] = testtmp
629 env['TESTTMP'] = testtmp
626 env['HOME'] = testtmp
630 env['HOME'] = testtmp
627 env["HGPORT"] = str(port)
631 env["HGPORT"] = str(port)
628 env["HGPORT1"] = str(port + 1)
632 env["HGPORT1"] = str(port + 1)
629 env["HGPORT2"] = str(port + 2)
633 env["HGPORT2"] = str(port + 2)
630 env["HGRCPATH"] = os.path.join(self.threadtmp, '.hgrc')
634 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
631 env["DAEMON_PIDS"] = os.path.join(self.threadtmp, 'daemon.pids')
635 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
632 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
636 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
633 env["HGMERGE"] = "internal:merge"
637 env["HGMERGE"] = "internal:merge"
634 env["HGUSER"] = "test"
638 env["HGUSER"] = "test"
635 env["HGENCODING"] = "ascii"
639 env["HGENCODING"] = "ascii"
636 env["HGENCODINGMODE"] = "strict"
640 env["HGENCODINGMODE"] = "strict"
637
641
638 # Reset some environment variables to well-known values so that
642 # Reset some environment variables to well-known values so that
639 # the tests produce repeatable output.
643 # the tests produce repeatable output.
640 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
644 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
641 env['TZ'] = 'GMT'
645 env['TZ'] = 'GMT'
642 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
646 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
643 env['COLUMNS'] = '80'
647 env['COLUMNS'] = '80'
644 env['TERM'] = 'xterm'
648 env['TERM'] = 'xterm'
645
649
646 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 ' +
647 'NO_PROXY').split():
651 'NO_PROXY').split():
648 if k in env:
652 if k in env:
649 del env[k]
653 del env[k]
650
654
651 # unset env related to hooks
655 # unset env related to hooks
652 for k in env.keys():
656 for k in env.keys():
653 if k.startswith('HG_'):
657 if k.startswith('HG_'):
654 del env[k]
658 del env[k]
655
659
656 return env
660 return env
657
661
658 class TestResult(object):
662 class TestResult(object):
659 """Holds the result of a test execution."""
663 """Holds the result of a test execution."""
660
664
661 def __init__(self):
665 def __init__(self):
662 self.ret = None
666 self.ret = None
663 self.out = None
667 self.out = None
664 self.duration = None
668 self.duration = None
665 self.interrupted = False
669 self.interrupted = False
666 self.exception = None
670 self.exception = None
667 self.refout = None
671 self.refout = None
668
672
669 @property
673 @property
670 def skipped(self):
674 def skipped(self):
671 """Whether the test was skipped."""
675 """Whether the test was skipped."""
672 return self.ret == SKIPPED_STATUS
676 return self.ret == SKIPPED_STATUS
673
677
674 def pytest(test, wd, options, replacements, env):
678 def pytest(test, wd, options, replacements, env):
675 py3kswitch = options.py3k_warnings and ' -3' or ''
679 py3kswitch = options.py3k_warnings and ' -3' or ''
676 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
680 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
677 vlog("# Running", cmd)
681 vlog("# Running", cmd)
678 if os.name == 'nt':
682 if os.name == 'nt':
679 replacements.append((r'\r\n', '\n'))
683 replacements.append((r'\r\n', '\n'))
680 return run(cmd, wd, options, replacements, env)
684 return run(cmd, wd, options, replacements, env)
681
685
682 class PythonTest(Test):
686 class PythonTest(Test):
683 """A Python-based test."""
687 """A Python-based test."""
684 def _run(self, testtmp, replacements, env):
688 def _run(self, testtmp, replacements, env):
685 return pytest(self._path, testtmp, self._options, replacements,
689 return pytest(self._path, testtmp, self._options, replacements,
686 env)
690 env)
687
691
688 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
692 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
689 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
693 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
690 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
694 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
691 escapemap.update({'\\': '\\\\', '\r': r'\r'})
695 escapemap.update({'\\': '\\\\', '\r': r'\r'})
692 def escapef(m):
696 def escapef(m):
693 return escapemap[m.group(0)]
697 return escapemap[m.group(0)]
694 def stringescape(s):
698 def stringescape(s):
695 return escapesub(escapef, s)
699 return escapesub(escapef, s)
696
700
697 def rematch(el, l):
701 def rematch(el, l):
698 try:
702 try:
699 # use \Z to ensure that the regex matches to the end of the string
703 # use \Z to ensure that the regex matches to the end of the string
700 if os.name == 'nt':
704 if os.name == 'nt':
701 return re.match(el + r'\r?\n\Z', l)
705 return re.match(el + r'\r?\n\Z', l)
702 return re.match(el + r'\n\Z', l)
706 return re.match(el + r'\n\Z', l)
703 except re.error:
707 except re.error:
704 # el is an invalid regex
708 # el is an invalid regex
705 return False
709 return False
706
710
707 def globmatch(el, l):
711 def globmatch(el, l):
708 # The only supported special characters are * and ? plus / which also
712 # The only supported special characters are * and ? plus / which also
709 # matches \ on windows. Escaping of these characters is supported.
713 # matches \ on windows. Escaping of these characters is supported.
710 if el + '\n' == l:
714 if el + '\n' == l:
711 if os.altsep:
715 if os.altsep:
712 # matching on "/" is not needed for this line
716 # matching on "/" is not needed for this line
713 return '-glob'
717 return '-glob'
714 return True
718 return True
715 i, n = 0, len(el)
719 i, n = 0, len(el)
716 res = ''
720 res = ''
717 while i < n:
721 while i < n:
718 c = el[i]
722 c = el[i]
719 i += 1
723 i += 1
720 if c == '\\' and el[i] in '*?\\/':
724 if c == '\\' and el[i] in '*?\\/':
721 res += el[i - 1:i + 1]
725 res += el[i - 1:i + 1]
722 i += 1
726 i += 1
723 elif c == '*':
727 elif c == '*':
724 res += '.*'
728 res += '.*'
725 elif c == '?':
729 elif c == '?':
726 res += '.'
730 res += '.'
727 elif c == '/' and os.altsep:
731 elif c == '/' and os.altsep:
728 res += '[/\\\\]'
732 res += '[/\\\\]'
729 else:
733 else:
730 res += re.escape(c)
734 res += re.escape(c)
731 return rematch(res, l)
735 return rematch(res, l)
732
736
733 def linematch(el, l):
737 def linematch(el, l):
734 if el == l: # perfect match (fast)
738 if el == l: # perfect match (fast)
735 return True
739 return True
736 if el:
740 if el:
737 if el.endswith(" (esc)\n"):
741 if el.endswith(" (esc)\n"):
738 el = el[:-7].decode('string-escape') + '\n'
742 el = el[:-7].decode('string-escape') + '\n'
739 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
743 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
740 return True
744 return True
741 if el.endswith(" (re)\n"):
745 if el.endswith(" (re)\n"):
742 return rematch(el[:-6], l)
746 return rematch(el[:-6], l)
743 if el.endswith(" (glob)\n"):
747 if el.endswith(" (glob)\n"):
744 return globmatch(el[:-8], l)
748 return globmatch(el[:-8], l)
745 if os.altsep and l.replace('\\', '/') == el:
749 if os.altsep and l.replace('\\', '/') == el:
746 return '+glob'
750 return '+glob'
747 return False
751 return False
748
752
749 def tsttest(test, wd, options, replacements, env):
753 def tsttest(test, wd, options, replacements, env):
750 # We generate a shell script which outputs unique markers to line
754 # We generate a shell script which outputs unique markers to line
751 # up script results with our source. These markers include input
755 # up script results with our source. These markers include input
752 # line number and the last return code
756 # line number and the last return code
753 salt = "SALT" + str(time.time())
757 salt = "SALT" + str(time.time())
754 def addsalt(line, inpython):
758 def addsalt(line, inpython):
755 if inpython:
759 if inpython:
756 script.append('%s %d 0\n' % (salt, line))
760 script.append('%s %d 0\n' % (salt, line))
757 else:
761 else:
758 script.append('echo %s %s $?\n' % (salt, line))
762 script.append('echo %s %s $?\n' % (salt, line))
759
763
760 # After we run the shell script, we re-unify the script output
764 # After we run the shell script, we re-unify the script output
761 # with non-active parts of the source, with synchronization by our
765 # with non-active parts of the source, with synchronization by our
762 # SALT line number markers. The after table contains the
766 # SALT line number markers. The after table contains the
763 # non-active components, ordered by line number
767 # non-active components, ordered by line number
764 after = {}
768 after = {}
765 pos = prepos = -1
769 pos = prepos = -1
766
770
767 # Expected shell script output
771 # Expected shell script output
768 expected = {}
772 expected = {}
769
773
770 # We keep track of whether or not we're in a Python block so we
774 # We keep track of whether or not we're in a Python block so we
771 # can generate the surrounding doctest magic
775 # can generate the surrounding doctest magic
772 inpython = False
776 inpython = False
773
777
774 # True or False when in a true or false conditional section
778 # True or False when in a true or false conditional section
775 skipping = None
779 skipping = None
776
780
777 def hghave(reqs):
781 def hghave(reqs):
778 # TODO: do something smarter when all other uses of hghave is gone
782 # TODO: do something smarter when all other uses of hghave is gone
779 tdir = TESTDIR.replace('\\', '/')
783 tdir = TESTDIR.replace('\\', '/')
780 proc = Popen4('%s -c "%s/hghave %s"' %
784 proc = Popen4('%s -c "%s/hghave %s"' %
781 (options.shell, tdir, ' '.join(reqs)), wd, 0)
785 (options.shell, tdir, ' '.join(reqs)), wd, 0)
782 stdout, stderr = proc.communicate()
786 stdout, stderr = proc.communicate()
783 ret = proc.wait()
787 ret = proc.wait()
784 if wifexited(ret):
788 if wifexited(ret):
785 ret = os.WEXITSTATUS(ret)
789 ret = os.WEXITSTATUS(ret)
786 if ret == 2:
790 if ret == 2:
787 print stdout
791 print stdout
788 sys.exit(1)
792 sys.exit(1)
789 return ret == 0
793 return ret == 0
790
794
791 f = open(test)
795 f = open(test)
792 t = f.readlines()
796 t = f.readlines()
793 f.close()
797 f.close()
794
798
795 script = []
799 script = []
796 if options.debug:
800 if options.debug:
797 script.append('set -x\n')
801 script.append('set -x\n')
798 if os.getenv('MSYSTEM'):
802 if os.getenv('MSYSTEM'):
799 script.append('alias pwd="pwd -W"\n')
803 script.append('alias pwd="pwd -W"\n')
800 n = 0
804 n = 0
801 for n, l in enumerate(t):
805 for n, l in enumerate(t):
802 if not l.endswith('\n'):
806 if not l.endswith('\n'):
803 l += '\n'
807 l += '\n'
804 if l.startswith('#if'):
808 if l.startswith('#if'):
805 lsplit = l.split()
809 lsplit = l.split()
806 if len(lsplit) < 2 or lsplit[0] != '#if':
810 if len(lsplit) < 2 or lsplit[0] != '#if':
807 after.setdefault(pos, []).append(' !!! invalid #if\n')
811 after.setdefault(pos, []).append(' !!! invalid #if\n')
808 if skipping is not None:
812 if skipping is not None:
809 after.setdefault(pos, []).append(' !!! nested #if\n')
813 after.setdefault(pos, []).append(' !!! nested #if\n')
810 skipping = not hghave(lsplit[1:])
814 skipping = not hghave(lsplit[1:])
811 after.setdefault(pos, []).append(l)
815 after.setdefault(pos, []).append(l)
812 elif l.startswith('#else'):
816 elif l.startswith('#else'):
813 if skipping is None:
817 if skipping is None:
814 after.setdefault(pos, []).append(' !!! missing #if\n')
818 after.setdefault(pos, []).append(' !!! missing #if\n')
815 skipping = not skipping
819 skipping = not skipping
816 after.setdefault(pos, []).append(l)
820 after.setdefault(pos, []).append(l)
817 elif l.startswith('#endif'):
821 elif l.startswith('#endif'):
818 if skipping is None:
822 if skipping is None:
819 after.setdefault(pos, []).append(' !!! missing #if\n')
823 after.setdefault(pos, []).append(' !!! missing #if\n')
820 skipping = None
824 skipping = None
821 after.setdefault(pos, []).append(l)
825 after.setdefault(pos, []).append(l)
822 elif skipping:
826 elif skipping:
823 after.setdefault(pos, []).append(l)
827 after.setdefault(pos, []).append(l)
824 elif l.startswith(' >>> '): # python inlines
828 elif l.startswith(' >>> '): # python inlines
825 after.setdefault(pos, []).append(l)
829 after.setdefault(pos, []).append(l)
826 prepos = pos
830 prepos = pos
827 pos = n
831 pos = n
828 if not inpython:
832 if not inpython:
829 # we've just entered a Python block, add the header
833 # we've just entered a Python block, add the header
830 inpython = True
834 inpython = True
831 addsalt(prepos, False) # make sure we report the exit code
835 addsalt(prepos, False) # make sure we report the exit code
832 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
836 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
833 addsalt(n, True)
837 addsalt(n, True)
834 script.append(l[2:])
838 script.append(l[2:])
835 elif l.startswith(' ... '): # python inlines
839 elif l.startswith(' ... '): # python inlines
836 after.setdefault(prepos, []).append(l)
840 after.setdefault(prepos, []).append(l)
837 script.append(l[2:])
841 script.append(l[2:])
838 elif l.startswith(' $ '): # commands
842 elif l.startswith(' $ '): # commands
839 if inpython:
843 if inpython:
840 script.append("EOF\n")
844 script.append("EOF\n")
841 inpython = False
845 inpython = False
842 after.setdefault(pos, []).append(l)
846 after.setdefault(pos, []).append(l)
843 prepos = pos
847 prepos = pos
844 pos = n
848 pos = n
845 addsalt(n, False)
849 addsalt(n, False)
846 cmd = l[4:].split()
850 cmd = l[4:].split()
847 if len(cmd) == 2 and cmd[0] == 'cd':
851 if len(cmd) == 2 and cmd[0] == 'cd':
848 l = ' $ cd %s || exit 1\n' % cmd[1]
852 l = ' $ cd %s || exit 1\n' % cmd[1]
849 script.append(l[4:])
853 script.append(l[4:])
850 elif l.startswith(' > '): # continuations
854 elif l.startswith(' > '): # continuations
851 after.setdefault(prepos, []).append(l)
855 after.setdefault(prepos, []).append(l)
852 script.append(l[4:])
856 script.append(l[4:])
853 elif l.startswith(' '): # results
857 elif l.startswith(' '): # results
854 # queue up a list of expected results
858 # queue up a list of expected results
855 expected.setdefault(pos, []).append(l[2:])
859 expected.setdefault(pos, []).append(l[2:])
856 else:
860 else:
857 if inpython:
861 if inpython:
858 script.append("EOF\n")
862 script.append("EOF\n")
859 inpython = False
863 inpython = False
860 # non-command/result - queue up for merged output
864 # non-command/result - queue up for merged output
861 after.setdefault(pos, []).append(l)
865 after.setdefault(pos, []).append(l)
862
866
863 if inpython:
867 if inpython:
864 script.append("EOF\n")
868 script.append("EOF\n")
865 if skipping is not None:
869 if skipping is not None:
866 after.setdefault(pos, []).append(' !!! missing #endif\n')
870 after.setdefault(pos, []).append(' !!! missing #endif\n')
867 addsalt(n + 1, False)
871 addsalt(n + 1, False)
868
872
869 # Write out the script and execute it
873 # Write out the script and execute it
870 name = wd + '.sh'
874 name = wd + '.sh'
871 f = open(name, 'w')
875 f = open(name, 'w')
872 for l in script:
876 for l in script:
873 f.write(l)
877 f.write(l)
874 f.close()
878 f.close()
875
879
876 cmd = '%s "%s"' % (options.shell, name)
880 cmd = '%s "%s"' % (options.shell, name)
877 vlog("# Running", cmd)
881 vlog("# Running", cmd)
878 exitcode, output = run(cmd, wd, options, replacements, env)
882 exitcode, output = run(cmd, wd, options, replacements, env)
879 # do not merge output if skipped, return hghave message instead
883 # do not merge output if skipped, return hghave message instead
880 # similarly, with --debug, output is None
884 # similarly, with --debug, output is None
881 if exitcode == SKIPPED_STATUS or output is None:
885 if exitcode == SKIPPED_STATUS or output is None:
882 return exitcode, output
886 return exitcode, output
883
887
884 # Merge the script output back into a unified test
888 # Merge the script output back into a unified test
885
889
886 warnonly = 1 # 1: not yet, 2: yes, 3: for sure not
890 warnonly = 1 # 1: not yet, 2: yes, 3: for sure not
887 if exitcode != 0: # failure has been reported
891 if exitcode != 0: # failure has been reported
888 warnonly = 3 # set to "for sure not"
892 warnonly = 3 # set to "for sure not"
889 pos = -1
893 pos = -1
890 postout = []
894 postout = []
891 for l in output:
895 for l in output:
892 lout, lcmd = l, None
896 lout, lcmd = l, None
893 if salt in l:
897 if salt in l:
894 lout, lcmd = l.split(salt, 1)
898 lout, lcmd = l.split(salt, 1)
895
899
896 if lout:
900 if lout:
897 if not lout.endswith('\n'):
901 if not lout.endswith('\n'):
898 lout += ' (no-eol)\n'
902 lout += ' (no-eol)\n'
899
903
900 # find the expected output at the current position
904 # find the expected output at the current position
901 el = None
905 el = None
902 if pos in expected and expected[pos]:
906 if pos in expected and expected[pos]:
903 el = expected[pos].pop(0)
907 el = expected[pos].pop(0)
904
908
905 r = linematch(el, lout)
909 r = linematch(el, lout)
906 if isinstance(r, str):
910 if isinstance(r, str):
907 if r == '+glob':
911 if r == '+glob':
908 lout = el[:-1] + ' (glob)\n'
912 lout = el[:-1] + ' (glob)\n'
909 r = '' # warn only this line
913 r = '' # warn only this line
910 elif r == '-glob':
914 elif r == '-glob':
911 lout = ''.join(el.rsplit(' (glob)', 1))
915 lout = ''.join(el.rsplit(' (glob)', 1))
912 r = '' # warn only this line
916 r = '' # warn only this line
913 else:
917 else:
914 log('\ninfo, unknown linematch result: %r\n' % r)
918 log('\ninfo, unknown linematch result: %r\n' % r)
915 r = False
919 r = False
916 if r:
920 if r:
917 postout.append(" " + el)
921 postout.append(" " + el)
918 else:
922 else:
919 if needescape(lout):
923 if needescape(lout):
920 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
924 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
921 postout.append(" " + lout) # let diff deal with it
925 postout.append(" " + lout) # let diff deal with it
922 if r != '': # if line failed
926 if r != '': # if line failed
923 warnonly = 3 # set to "for sure not"
927 warnonly = 3 # set to "for sure not"
924 elif warnonly == 1: # is "not yet" (and line is warn only)
928 elif warnonly == 1: # is "not yet" (and line is warn only)
925 warnonly = 2 # set to "yes" do warn
929 warnonly = 2 # set to "yes" do warn
926
930
927 if lcmd:
931 if lcmd:
928 # add on last return code
932 # add on last return code
929 ret = int(lcmd.split()[1])
933 ret = int(lcmd.split()[1])
930 if ret != 0:
934 if ret != 0:
931 postout.append(" [%s]\n" % ret)
935 postout.append(" [%s]\n" % ret)
932 if pos in after:
936 if pos in after:
933 # merge in non-active test bits
937 # merge in non-active test bits
934 postout += after.pop(pos)
938 postout += after.pop(pos)
935 pos = int(lcmd.split()[0])
939 pos = int(lcmd.split()[0])
936
940
937 if pos in after:
941 if pos in after:
938 postout += after.pop(pos)
942 postout += after.pop(pos)
939
943
940 if warnonly == 2:
944 if warnonly == 2:
941 exitcode = False # set exitcode to warned
945 exitcode = False # set exitcode to warned
942 return exitcode, postout
946 return exitcode, postout
943
947
944 class TTest(Test):
948 class TTest(Test):
945 """A "t test" is a test backed by a .t file."""
949 """A "t test" is a test backed by a .t file."""
946
950
947 def _run(self, testtmp, replacements, env):
951 def _run(self, testtmp, replacements, env):
948 return tsttest(self._path, testtmp, self._options, replacements,
952 return tsttest(self._path, testtmp, self._options, replacements,
949 env)
953 env)
950
954
951 wifexited = getattr(os, "WIFEXITED", lambda x: False)
955 wifexited = getattr(os, "WIFEXITED", lambda x: False)
952 def run(cmd, wd, options, replacements, env):
956 def run(cmd, wd, options, replacements, env):
953 """Run command in a sub-process, capturing the output (stdout and stderr).
957 """Run command in a sub-process, capturing the output (stdout and stderr).
954 Return a tuple (exitcode, output). output is None in debug mode."""
958 Return a tuple (exitcode, output). output is None in debug mode."""
955 # TODO: Use subprocess.Popen if we're running on Python 2.4
959 # TODO: Use subprocess.Popen if we're running on Python 2.4
956 if options.debug:
960 if options.debug:
957 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
961 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
958 ret = proc.wait()
962 ret = proc.wait()
959 return (ret, None)
963 return (ret, None)
960
964
961 proc = Popen4(cmd, wd, options.timeout, env)
965 proc = Popen4(cmd, wd, options.timeout, env)
962 def cleanup():
966 def cleanup():
963 terminate(proc)
967 terminate(proc)
964 ret = proc.wait()
968 ret = proc.wait()
965 if ret == 0:
969 if ret == 0:
966 ret = signal.SIGTERM << 8
970 ret = signal.SIGTERM << 8
967 killdaemons(env['DAEMON_PIDS'])
971 killdaemons(env['DAEMON_PIDS'])
968 return ret
972 return ret
969
973
970 output = ''
974 output = ''
971 proc.tochild.close()
975 proc.tochild.close()
972
976
973 try:
977 try:
974 output = proc.fromchild.read()
978 output = proc.fromchild.read()
975 except KeyboardInterrupt:
979 except KeyboardInterrupt:
976 vlog('# Handling keyboard interrupt')
980 vlog('# Handling keyboard interrupt')
977 cleanup()
981 cleanup()
978 raise
982 raise
979
983
980 ret = proc.wait()
984 ret = proc.wait()
981 if wifexited(ret):
985 if wifexited(ret):
982 ret = os.WEXITSTATUS(ret)
986 ret = os.WEXITSTATUS(ret)
983
987
984 if proc.timeout:
988 if proc.timeout:
985 ret = 'timeout'
989 ret = 'timeout'
986
990
987 if ret:
991 if ret:
988 killdaemons(env['DAEMON_PIDS'])
992 killdaemons(env['DAEMON_PIDS'])
989
993
990 if abort:
994 if abort:
991 raise KeyboardInterrupt()
995 raise KeyboardInterrupt()
992
996
993 for s, r in replacements:
997 for s, r in replacements:
994 output = re.sub(s, r, output)
998 output = re.sub(s, r, output)
995 return ret, output.splitlines(True)
999 return ret, output.splitlines(True)
996
1000
997 def runone(options, test, count):
1001 def runone(options, test, count):
998 '''returns a result element: (code, test, msg)'''
1002 '''returns a result element: (code, test, msg)'''
999
1003
1000 def skip(msg):
1004 def skip(msg):
1001 if options.verbose:
1005 if options.verbose:
1002 log("\nSkipping %s: %s" % (testpath, msg))
1006 log("\nSkipping %s: %s" % (testpath, msg))
1003 return 's', test, msg
1007 return 's', test, msg
1004
1008
1005 def fail(msg, ret):
1009 def fail(msg, ret):
1006 warned = ret is False
1010 warned = ret is False
1007 if not options.nodiff:
1011 if not options.nodiff:
1008 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', test, msg))
1012 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', test, msg))
1009 if (not ret and options.interactive
1013 if (not ret and options.interactive
1010 and os.path.exists(testpath + ".err")):
1014 and os.path.exists(testpath + ".err")):
1011 iolock.acquire()
1015 iolock.acquire()
1012 print "Accept this change? [n] ",
1016 print "Accept this change? [n] ",
1013 answer = sys.stdin.readline().strip()
1017 answer = sys.stdin.readline().strip()
1014 iolock.release()
1018 iolock.release()
1015 if answer.lower() in "y yes".split():
1019 if answer.lower() in "y yes".split():
1016 if test.endswith(".t"):
1020 if test.endswith(".t"):
1017 rename(testpath + ".err", testpath)
1021 rename(testpath + ".err", testpath)
1018 else:
1022 else:
1019 rename(testpath + ".err", testpath + ".out")
1023 rename(testpath + ".err", testpath + ".out")
1020 return '.', test, ''
1024 return '.', test, ''
1021 return warned and '~' or '!', test, msg
1025 return warned and '~' or '!', test, msg
1022
1026
1023 def success():
1027 def success():
1024 return '.', test, ''
1028 return '.', test, ''
1025
1029
1026 def ignore(msg):
1030 def ignore(msg):
1027 return 'i', test, msg
1031 return 'i', test, msg
1028
1032
1029 def describe(ret):
1033 def describe(ret):
1030 if ret < 0:
1034 if ret < 0:
1031 return 'killed by signal %d' % -ret
1035 return 'killed by signal %d' % -ret
1032 return 'returned error code %d' % ret
1036 return 'returned error code %d' % ret
1033
1037
1034 testpath = os.path.join(TESTDIR, test)
1038 testpath = os.path.join(TESTDIR, test)
1035 err = os.path.join(TESTDIR, test + ".err")
1039 err = os.path.join(TESTDIR, test + ".err")
1036 lctest = test.lower()
1040 lctest = test.lower()
1037
1041
1038 if not os.path.exists(testpath):
1042 if not os.path.exists(testpath):
1039 return skip("doesn't exist")
1043 return skip("doesn't exist")
1040
1044
1041 if not (options.whitelisted and test in options.whitelisted):
1045 if not (options.whitelisted and test in options.whitelisted):
1042 if options.blacklist and test in options.blacklist:
1046 if options.blacklist and test in options.blacklist:
1043 return skip("blacklisted")
1047 return skip("blacklisted")
1044
1048
1045 if options.retest and not os.path.exists(test + ".err"):
1049 if options.retest and not os.path.exists(test + ".err"):
1046 return ignore("not retesting")
1050 return ignore("not retesting")
1047
1051
1048 if options.keywords:
1052 if options.keywords:
1049 fp = open(test)
1053 fp = open(test)
1050 t = fp.read().lower() + test.lower()
1054 t = fp.read().lower() + test.lower()
1051 fp.close()
1055 fp.close()
1052 for k in options.keywords.lower().split():
1056 for k in options.keywords.lower().split():
1053 if k in t:
1057 if k in t:
1054 break
1058 break
1055 else:
1059 else:
1056 return ignore("doesn't match keyword")
1060 return ignore("doesn't match keyword")
1057
1061
1058 if not os.path.basename(lctest).startswith("test-"):
1062 if not os.path.basename(lctest).startswith("test-"):
1059 return skip("not a test file")
1063 return skip("not a test file")
1060 for ext, cls, out in testtypes:
1064 for ext, cls, out in testtypes:
1061 if lctest.endswith(ext):
1065 if lctest.endswith(ext):
1062 runner = cls
1066 runner = cls
1063 ref = os.path.join(TESTDIR, test + out)
1067 ref = os.path.join(TESTDIR, test + out)
1064 break
1068 break
1065 else:
1069 else:
1066 return skip("unknown test type")
1070 return skip("unknown test type")
1067
1071
1068 vlog("# Test", test)
1072 vlog("# Test", test)
1069
1073
1070 if os.path.exists(err):
1074 if os.path.exists(err):
1071 os.remove(err) # Remove any previous output files
1075 os.remove(err) # Remove any previous output files
1072
1076
1073 t = runner(testpath, options, count)
1077 t = runner(testpath, options, count)
1074 res = TestResult()
1078 res = TestResult()
1075 t.run(res, ref)
1079 t.run(res, ref)
1080 t.cleanup()
1076
1081
1077 if res.interrupted:
1082 if res.interrupted:
1078 log('INTERRUPTED: %s (after %d seconds)' % (test, res.duration))
1083 log('INTERRUPTED: %s (after %d seconds)' % (test, res.duration))
1079 raise KeyboardInterrupt()
1084 raise KeyboardInterrupt()
1080
1085
1081 if res.exception:
1086 if res.exception:
1082 return fail('Exception during execution: %s' % res.exception, 255)
1087 return fail('Exception during execution: %s' % res.exception, 255)
1083
1088
1084 ret = res.ret
1089 ret = res.ret
1085 out = res.out
1090 out = res.out
1086
1091
1087 times.append((test, res.duration))
1092 times.append((test, res.duration))
1088 vlog("# Ret was:", ret)
1093 vlog("# Ret was:", ret)
1089
1094
1090 skipped = res.skipped
1095 skipped = res.skipped
1091 refout = res.refout
1096 refout = res.refout
1092
1097
1093 if (ret != 0 or out != refout) and not skipped and not options.debug:
1098 if (ret != 0 or out != refout) and not skipped and not options.debug:
1094 # Save errors to a file for diagnosis
1099 # Save errors to a file for diagnosis
1095 f = open(err, "wb")
1100 f = open(err, "wb")
1096 for line in out:
1101 for line in out:
1097 f.write(line)
1102 f.write(line)
1098 f.close()
1103 f.close()
1099
1104
1100 if skipped:
1105 if skipped:
1101 if out is None: # debug mode: nothing to parse
1106 if out is None: # debug mode: nothing to parse
1102 missing = ['unknown']
1107 missing = ['unknown']
1103 failed = None
1108 failed = None
1104 else:
1109 else:
1105 missing, failed = parsehghaveoutput(out)
1110 missing, failed = parsehghaveoutput(out)
1106 if not missing:
1111 if not missing:
1107 missing = ['irrelevant']
1112 missing = ['irrelevant']
1108 if failed:
1113 if failed:
1109 result = fail("hghave failed checking for %s" % failed[-1], ret)
1114 result = fail("hghave failed checking for %s" % failed[-1], ret)
1110 skipped = False
1115 skipped = False
1111 else:
1116 else:
1112 result = skip(missing[-1])
1117 result = skip(missing[-1])
1113 elif ret == 'timeout':
1118 elif ret == 'timeout':
1114 result = fail("timed out", ret)
1119 result = fail("timed out", ret)
1115 elif out != refout:
1120 elif out != refout:
1116 info = {}
1121 info = {}
1117 if not options.nodiff:
1122 if not options.nodiff:
1118 iolock.acquire()
1123 iolock.acquire()
1119 if options.view:
1124 if options.view:
1120 os.system("%s %s %s" % (options.view, ref, err))
1125 os.system("%s %s %s" % (options.view, ref, err))
1121 else:
1126 else:
1122 info = showdiff(refout, out, ref, err)
1127 info = showdiff(refout, out, ref, err)
1123 iolock.release()
1128 iolock.release()
1124 msg = ""
1129 msg = ""
1125 if info.get('servefail'): msg += "serve failed and "
1130 if info.get('servefail'): msg += "serve failed and "
1126 if ret:
1131 if ret:
1127 msg += "output changed and " + describe(ret)
1132 msg += "output changed and " + describe(ret)
1128 else:
1133 else:
1129 msg += "output changed"
1134 msg += "output changed"
1130 result = fail(msg, ret)
1135 result = fail(msg, ret)
1131 elif ret:
1136 elif ret:
1132 result = fail(describe(ret), ret)
1137 result = fail(describe(ret), ret)
1133 else:
1138 else:
1134 result = success()
1139 result = success()
1135
1140
1136 if not options.verbose:
1141 if not options.verbose:
1137 iolock.acquire()
1142 iolock.acquire()
1138 sys.stdout.write(result[0])
1143 sys.stdout.write(result[0])
1139 sys.stdout.flush()
1144 sys.stdout.flush()
1140 iolock.release()
1145 iolock.release()
1141
1146
1142 if not options.keep_tmpdir:
1143 shutil.rmtree(t.threadtmp, True)
1144 return result
1147 return result
1145
1148
1146 _hgpath = None
1149 _hgpath = None
1147
1150
1148 def _gethgpath():
1151 def _gethgpath():
1149 """Return the path to the mercurial package that is actually found by
1152 """Return the path to the mercurial package that is actually found by
1150 the current Python interpreter."""
1153 the current Python interpreter."""
1151 global _hgpath
1154 global _hgpath
1152 if _hgpath is not None:
1155 if _hgpath is not None:
1153 return _hgpath
1156 return _hgpath
1154
1157
1155 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1158 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1156 pipe = os.popen(cmd % PYTHON)
1159 pipe = os.popen(cmd % PYTHON)
1157 try:
1160 try:
1158 _hgpath = pipe.read().strip()
1161 _hgpath = pipe.read().strip()
1159 finally:
1162 finally:
1160 pipe.close()
1163 pipe.close()
1161 return _hgpath
1164 return _hgpath
1162
1165
1163 def _checkhglib(verb):
1166 def _checkhglib(verb):
1164 """Ensure that the 'mercurial' package imported by python is
1167 """Ensure that the 'mercurial' package imported by python is
1165 the one we expect it to be. If not, print a warning to stderr."""
1168 the one we expect it to be. If not, print a warning to stderr."""
1166 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1169 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1167 actualhg = _gethgpath()
1170 actualhg = _gethgpath()
1168 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1171 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1169 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1172 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1170 ' (expected %s)\n'
1173 ' (expected %s)\n'
1171 % (verb, actualhg, expecthg))
1174 % (verb, actualhg, expecthg))
1172
1175
1173 results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
1176 results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
1174 times = []
1177 times = []
1175 iolock = threading.Lock()
1178 iolock = threading.Lock()
1176 abort = False
1179 abort = False
1177
1180
1178 def scheduletests(options, tests):
1181 def scheduletests(options, tests):
1179 jobs = options.jobs
1182 jobs = options.jobs
1180 done = queue.Queue()
1183 done = queue.Queue()
1181 running = 0
1184 running = 0
1182 count = 0
1185 count = 0
1183 global abort
1186 global abort
1184
1187
1185 def job(test, count):
1188 def job(test, count):
1186 try:
1189 try:
1187 done.put(runone(options, test, count))
1190 done.put(runone(options, test, count))
1188 except KeyboardInterrupt:
1191 except KeyboardInterrupt:
1189 pass
1192 pass
1190 except: # re-raises
1193 except: # re-raises
1191 done.put(('!', test, 'run-test raised an error, see traceback'))
1194 done.put(('!', test, 'run-test raised an error, see traceback'))
1192 raise
1195 raise
1193
1196
1194 try:
1197 try:
1195 while tests or running:
1198 while tests or running:
1196 if not done.empty() or running == jobs or not tests:
1199 if not done.empty() or running == jobs or not tests:
1197 try:
1200 try:
1198 code, test, msg = done.get(True, 1)
1201 code, test, msg = done.get(True, 1)
1199 results[code].append((test, msg))
1202 results[code].append((test, msg))
1200 if options.first and code not in '.si':
1203 if options.first and code not in '.si':
1201 break
1204 break
1202 except queue.Empty:
1205 except queue.Empty:
1203 continue
1206 continue
1204 running -= 1
1207 running -= 1
1205 if tests and not running == jobs:
1208 if tests and not running == jobs:
1206 test = tests.pop(0)
1209 test = tests.pop(0)
1207 if options.loop:
1210 if options.loop:
1208 tests.append(test)
1211 tests.append(test)
1209 t = threading.Thread(target=job, name=test, args=(test, count))
1212 t = threading.Thread(target=job, name=test, args=(test, count))
1210 t.start()
1213 t.start()
1211 running += 1
1214 running += 1
1212 count += 1
1215 count += 1
1213 except KeyboardInterrupt:
1216 except KeyboardInterrupt:
1214 abort = True
1217 abort = True
1215
1218
1216 def runtests(options, tests):
1219 def runtests(options, tests):
1217 try:
1220 try:
1218 if INST:
1221 if INST:
1219 installhg(options)
1222 installhg(options)
1220 _checkhglib("Testing")
1223 _checkhglib("Testing")
1221 else:
1224 else:
1222 usecorrectpython()
1225 usecorrectpython()
1223
1226
1224 if options.restart:
1227 if options.restart:
1225 orig = list(tests)
1228 orig = list(tests)
1226 while tests:
1229 while tests:
1227 if os.path.exists(tests[0] + ".err"):
1230 if os.path.exists(tests[0] + ".err"):
1228 break
1231 break
1229 tests.pop(0)
1232 tests.pop(0)
1230 if not tests:
1233 if not tests:
1231 print "running all tests"
1234 print "running all tests"
1232 tests = orig
1235 tests = orig
1233
1236
1234 scheduletests(options, tests)
1237 scheduletests(options, tests)
1235
1238
1236 failed = len(results['!'])
1239 failed = len(results['!'])
1237 warned = len(results['~'])
1240 warned = len(results['~'])
1238 tested = len(results['.']) + failed + warned
1241 tested = len(results['.']) + failed + warned
1239 skipped = len(results['s'])
1242 skipped = len(results['s'])
1240 ignored = len(results['i'])
1243 ignored = len(results['i'])
1241
1244
1242 print
1245 print
1243 if not options.noskips:
1246 if not options.noskips:
1244 for s in results['s']:
1247 for s in results['s']:
1245 print "Skipped %s: %s" % s
1248 print "Skipped %s: %s" % s
1246 for s in results['~']:
1249 for s in results['~']:
1247 print "Warned %s: %s" % s
1250 print "Warned %s: %s" % s
1248 for s in results['!']:
1251 for s in results['!']:
1249 print "Failed %s: %s" % s
1252 print "Failed %s: %s" % s
1250 _checkhglib("Tested")
1253 _checkhglib("Tested")
1251 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1254 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1252 tested, skipped + ignored, warned, failed)
1255 tested, skipped + ignored, warned, failed)
1253 if results['!']:
1256 if results['!']:
1254 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1257 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1255 if options.time:
1258 if options.time:
1256 outputtimes(options)
1259 outputtimes(options)
1257
1260
1258 if options.anycoverage:
1261 if options.anycoverage:
1259 outputcoverage(options)
1262 outputcoverage(options)
1260 except KeyboardInterrupt:
1263 except KeyboardInterrupt:
1261 failed = True
1264 failed = True
1262 print "\ninterrupted!"
1265 print "\ninterrupted!"
1263
1266
1264 if failed:
1267 if failed:
1265 return 1
1268 return 1
1266 if warned:
1269 if warned:
1267 return 80
1270 return 80
1268
1271
1269 testtypes = [('.py', PythonTest, '.out'),
1272 testtypes = [('.py', PythonTest, '.out'),
1270 ('.t', TTest, '')]
1273 ('.t', TTest, '')]
1271
1274
1272 def main(args, parser=None):
1275 def main(args, parser=None):
1273 parser = parser or getparser()
1276 parser = parser or getparser()
1274 (options, args) = parseargs(args, parser)
1277 (options, args) = parseargs(args, parser)
1275 os.umask(022)
1278 os.umask(022)
1276
1279
1277 checktools()
1280 checktools()
1278
1281
1279 if not args:
1282 if not args:
1280 if options.changed:
1283 if options.changed:
1281 proc = Popen4('hg st --rev "%s" -man0 .' % options.changed,
1284 proc = Popen4('hg st --rev "%s" -man0 .' % options.changed,
1282 None, 0)
1285 None, 0)
1283 stdout, stderr = proc.communicate()
1286 stdout, stderr = proc.communicate()
1284 args = stdout.strip('\0').split('\0')
1287 args = stdout.strip('\0').split('\0')
1285 else:
1288 else:
1286 args = os.listdir(".")
1289 args = os.listdir(".")
1287
1290
1288 tests = [t for t in args
1291 tests = [t for t in args
1289 if os.path.basename(t).startswith("test-")
1292 if os.path.basename(t).startswith("test-")
1290 and (t.endswith(".py") or t.endswith(".t"))]
1293 and (t.endswith(".py") or t.endswith(".t"))]
1291
1294
1292 if options.random:
1295 if options.random:
1293 random.shuffle(tests)
1296 random.shuffle(tests)
1294 else:
1297 else:
1295 # keywords for slow tests
1298 # keywords for slow tests
1296 slow = 'svn gendoc check-code-hg'.split()
1299 slow = 'svn gendoc check-code-hg'.split()
1297 def sortkey(f):
1300 def sortkey(f):
1298 # run largest tests first, as they tend to take the longest
1301 # run largest tests first, as they tend to take the longest
1299 try:
1302 try:
1300 val = -os.stat(f).st_size
1303 val = -os.stat(f).st_size
1301 except OSError, e:
1304 except OSError, e:
1302 if e.errno != errno.ENOENT:
1305 if e.errno != errno.ENOENT:
1303 raise
1306 raise
1304 return -1e9 # file does not exist, tell early
1307 return -1e9 # file does not exist, tell early
1305 for kw in slow:
1308 for kw in slow:
1306 if kw in f:
1309 if kw in f:
1307 val *= 10
1310 val *= 10
1308 return val
1311 return val
1309 tests.sort(key=sortkey)
1312 tests.sort(key=sortkey)
1310
1313
1311 if 'PYTHONHASHSEED' not in os.environ:
1314 if 'PYTHONHASHSEED' not in os.environ:
1312 # use a random python hash seed all the time
1315 # use a random python hash seed all the time
1313 # we do the randomness ourself to know what seed is used
1316 # we do the randomness ourself to know what seed is used
1314 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1317 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1315
1318
1316 global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
1319 global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
1317 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1320 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1318 if options.tmpdir:
1321 if options.tmpdir:
1319 options.keep_tmpdir = True
1322 options.keep_tmpdir = True
1320 tmpdir = options.tmpdir
1323 tmpdir = options.tmpdir
1321 if os.path.exists(tmpdir):
1324 if os.path.exists(tmpdir):
1322 # Meaning of tmpdir has changed since 1.3: we used to create
1325 # Meaning of tmpdir has changed since 1.3: we used to create
1323 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1326 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1324 # tmpdir already exists.
1327 # tmpdir already exists.
1325 print "error: temp dir %r already exists" % tmpdir
1328 print "error: temp dir %r already exists" % tmpdir
1326 return 1
1329 return 1
1327
1330
1328 # Automatically removing tmpdir sounds convenient, but could
1331 # Automatically removing tmpdir sounds convenient, but could
1329 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1332 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1330 # or "--tmpdir=$HOME".
1333 # or "--tmpdir=$HOME".
1331 #vlog("# Removing temp dir", tmpdir)
1334 #vlog("# Removing temp dir", tmpdir)
1332 #shutil.rmtree(tmpdir)
1335 #shutil.rmtree(tmpdir)
1333 os.makedirs(tmpdir)
1336 os.makedirs(tmpdir)
1334 else:
1337 else:
1335 d = None
1338 d = None
1336 if os.name == 'nt':
1339 if os.name == 'nt':
1337 # without this, we get the default temp dir location, but
1340 # without this, we get the default temp dir location, but
1338 # in all lowercase, which causes troubles with paths (issue3490)
1341 # in all lowercase, which causes troubles with paths (issue3490)
1339 d = os.getenv('TMP')
1342 d = os.getenv('TMP')
1340 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1343 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1341 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1344 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1342
1345
1343 if options.with_hg:
1346 if options.with_hg:
1344 INST = None
1347 INST = None
1345 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1348 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1346 TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
1349 TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
1347 os.makedirs(TMPBINDIR)
1350 os.makedirs(TMPBINDIR)
1348
1351
1349 # This looks redundant with how Python initializes sys.path from
1352 # This looks redundant with how Python initializes sys.path from
1350 # the location of the script being executed. Needed because the
1353 # the location of the script being executed. Needed because the
1351 # "hg" specified by --with-hg is not the only Python script
1354 # "hg" specified by --with-hg is not the only Python script
1352 # executed in the test suite that needs to import 'mercurial'
1355 # executed in the test suite that needs to import 'mercurial'
1353 # ... which means it's not really redundant at all.
1356 # ... which means it's not really redundant at all.
1354 PYTHONDIR = BINDIR
1357 PYTHONDIR = BINDIR
1355 else:
1358 else:
1356 INST = os.path.join(HGTMP, "install")
1359 INST = os.path.join(HGTMP, "install")
1357 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1360 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1358 TMPBINDIR = BINDIR
1361 TMPBINDIR = BINDIR
1359 PYTHONDIR = os.path.join(INST, "lib", "python")
1362 PYTHONDIR = os.path.join(INST, "lib", "python")
1360
1363
1361 os.environ["BINDIR"] = BINDIR
1364 os.environ["BINDIR"] = BINDIR
1362 os.environ["PYTHON"] = PYTHON
1365 os.environ["PYTHON"] = PYTHON
1363
1366
1364 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1367 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1365 if TMPBINDIR != BINDIR:
1368 if TMPBINDIR != BINDIR:
1366 path = [TMPBINDIR] + path
1369 path = [TMPBINDIR] + path
1367 os.environ["PATH"] = os.pathsep.join(path)
1370 os.environ["PATH"] = os.pathsep.join(path)
1368
1371
1369 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1372 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1370 # can run .../tests/run-tests.py test-foo where test-foo
1373 # 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
1374 # adds an extension to HGRC. Also include run-test.py directory to import
1372 # modules like heredoctest.
1375 # modules like heredoctest.
1373 pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
1376 pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
1374 # We have to augment PYTHONPATH, rather than simply replacing
1377 # We have to augment PYTHONPATH, rather than simply replacing
1375 # it, in case external libraries are only available via current
1378 # it, in case external libraries are only available via current
1376 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1379 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1377 # are in /opt/subversion.)
1380 # are in /opt/subversion.)
1378 oldpypath = os.environ.get(IMPL_PATH)
1381 oldpypath = os.environ.get(IMPL_PATH)
1379 if oldpypath:
1382 if oldpypath:
1380 pypath.append(oldpypath)
1383 pypath.append(oldpypath)
1381 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1384 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1382
1385
1383 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1386 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1384
1387
1385 vlog("# Using TESTDIR", TESTDIR)
1388 vlog("# Using TESTDIR", TESTDIR)
1386 vlog("# Using HGTMP", HGTMP)
1389 vlog("# Using HGTMP", HGTMP)
1387 vlog("# Using PATH", os.environ["PATH"])
1390 vlog("# Using PATH", os.environ["PATH"])
1388 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1391 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1389
1392
1390 try:
1393 try:
1391 return runtests(options, tests) or 0
1394 return runtests(options, tests) or 0
1392 finally:
1395 finally:
1393 time.sleep(.1)
1396 time.sleep(.1)
1394 cleanup(options)
1397 cleanup(options)
1395
1398
1396 if __name__ == '__main__':
1399 if __name__ == '__main__':
1397 sys.exit(main(sys.argv[1:]))
1400 sys.exit(main(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now