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