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