##// END OF EJS Templates
testrunner: on error, color the "(case xxx)" part the same as filename...
Martin von Zweigbergk -
r35866:8a7140ec default
parent child Browse files
Show More
@@ -1,3036 +1,3037 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 # 10) parallel, pure, tests that call run-tests:
38 # 10) parallel, pure, tests that call run-tests:
39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 #
40 #
41 # (You could use any subset of the tests: test-s* happens to match
41 # (You could use any subset of the tests: test-s* happens to match
42 # enough that it's worth doing parallel runs, few enough that it
42 # enough that it's worth doing parallel runs, few enough that it
43 # completes fairly quickly, includes both shell and Python scripts, and
43 # completes fairly quickly, includes both shell and Python scripts, and
44 # includes some scripts that run daemon processes.)
44 # includes some scripts that run daemon processes.)
45
45
46 from __future__ import absolute_import, print_function
46 from __future__ import absolute_import, print_function
47
47
48 import argparse
48 import argparse
49 import collections
49 import collections
50 import difflib
50 import difflib
51 import distutils.version as version
51 import distutils.version as version
52 import errno
52 import errno
53 import json
53 import json
54 import os
54 import os
55 import random
55 import random
56 import re
56 import re
57 import shutil
57 import shutil
58 import signal
58 import signal
59 import socket
59 import socket
60 import subprocess
60 import subprocess
61 import sys
61 import sys
62 import sysconfig
62 import sysconfig
63 import tempfile
63 import tempfile
64 import threading
64 import threading
65 import time
65 import time
66 import unittest
66 import unittest
67 import xml.dom.minidom as minidom
67 import xml.dom.minidom as minidom
68
68
69 try:
69 try:
70 import Queue as queue
70 import Queue as queue
71 except ImportError:
71 except ImportError:
72 import queue
72 import queue
73
73
74 try:
74 try:
75 import shlex
75 import shlex
76 shellquote = shlex.quote
76 shellquote = shlex.quote
77 except (ImportError, AttributeError):
77 except (ImportError, AttributeError):
78 import pipes
78 import pipes
79 shellquote = pipes.quote
79 shellquote = pipes.quote
80
80
81 if os.environ.get('RTUNICODEPEDANTRY', False):
81 if os.environ.get('RTUNICODEPEDANTRY', False):
82 try:
82 try:
83 reload(sys)
83 reload(sys)
84 sys.setdefaultencoding("undefined")
84 sys.setdefaultencoding("undefined")
85 except NameError:
85 except NameError:
86 pass
86 pass
87
87
88 origenviron = os.environ.copy()
88 origenviron = os.environ.copy()
89 osenvironb = getattr(os, 'environb', os.environ)
89 osenvironb = getattr(os, 'environb', os.environ)
90 processlock = threading.Lock()
90 processlock = threading.Lock()
91
91
92 pygmentspresent = False
92 pygmentspresent = False
93 # ANSI color is unsupported prior to Windows 10
93 # ANSI color is unsupported prior to Windows 10
94 if os.name != 'nt':
94 if os.name != 'nt':
95 try: # is pygments installed
95 try: # is pygments installed
96 import pygments
96 import pygments
97 import pygments.lexers as lexers
97 import pygments.lexers as lexers
98 import pygments.lexer as lexer
98 import pygments.lexer as lexer
99 import pygments.formatters as formatters
99 import pygments.formatters as formatters
100 import pygments.token as token
100 import pygments.token as token
101 import pygments.style as style
101 import pygments.style as style
102 pygmentspresent = True
102 pygmentspresent = True
103 difflexer = lexers.DiffLexer()
103 difflexer = lexers.DiffLexer()
104 terminal256formatter = formatters.Terminal256Formatter()
104 terminal256formatter = formatters.Terminal256Formatter()
105 except ImportError:
105 except ImportError:
106 pass
106 pass
107
107
108 if pygmentspresent:
108 if pygmentspresent:
109 class TestRunnerStyle(style.Style):
109 class TestRunnerStyle(style.Style):
110 default_style = ""
110 default_style = ""
111 skipped = token.string_to_tokentype("Token.Generic.Skipped")
111 skipped = token.string_to_tokentype("Token.Generic.Skipped")
112 failed = token.string_to_tokentype("Token.Generic.Failed")
112 failed = token.string_to_tokentype("Token.Generic.Failed")
113 skippedname = token.string_to_tokentype("Token.Generic.SName")
113 skippedname = token.string_to_tokentype("Token.Generic.SName")
114 failedname = token.string_to_tokentype("Token.Generic.FName")
114 failedname = token.string_to_tokentype("Token.Generic.FName")
115 styles = {
115 styles = {
116 skipped: '#e5e5e5',
116 skipped: '#e5e5e5',
117 skippedname: '#00ffff',
117 skippedname: '#00ffff',
118 failed: '#7f0000',
118 failed: '#7f0000',
119 failedname: '#ff0000',
119 failedname: '#ff0000',
120 }
120 }
121
121
122 class TestRunnerLexer(lexer.RegexLexer):
122 class TestRunnerLexer(lexer.RegexLexer):
123 testpattern = r'[\w-]+\.(t|py)( \(case [\w-]+\))?'
123 tokens = {
124 tokens = {
124 'root': [
125 'root': [
125 (r'^Skipped', token.Generic.Skipped, 'skipped'),
126 (r'^Skipped', token.Generic.Skipped, 'skipped'),
126 (r'^Failed ', token.Generic.Failed, 'failed'),
127 (r'^Failed ', token.Generic.Failed, 'failed'),
127 (r'^ERROR: ', token.Generic.Failed, 'failed'),
128 (r'^ERROR: ', token.Generic.Failed, 'failed'),
128 ],
129 ],
129 'skipped': [
130 'skipped': [
130 (r'[\w-]+\.(t|py)', token.Generic.SName),
131 (testpattern, token.Generic.SName),
131 (r':.*', token.Generic.Skipped),
132 (r':.*', token.Generic.Skipped),
132 ],
133 ],
133 'failed': [
134 'failed': [
134 (r'[\w-]+\.(t|py)', token.Generic.FName),
135 (testpattern, token.Generic.FName),
135 (r'(:| ).*', token.Generic.Failed),
136 (r'(:| ).*', token.Generic.Failed),
136 ]
137 ]
137 }
138 }
138
139
139 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
140 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
140 runnerlexer = TestRunnerLexer()
141 runnerlexer = TestRunnerLexer()
141
142
142 if sys.version_info > (3, 5, 0):
143 if sys.version_info > (3, 5, 0):
143 PYTHON3 = True
144 PYTHON3 = True
144 xrange = range # we use xrange in one place, and we'd rather not use range
145 xrange = range # we use xrange in one place, and we'd rather not use range
145 def _bytespath(p):
146 def _bytespath(p):
146 if p is None:
147 if p is None:
147 return p
148 return p
148 return p.encode('utf-8')
149 return p.encode('utf-8')
149
150
150 def _strpath(p):
151 def _strpath(p):
151 if p is None:
152 if p is None:
152 return p
153 return p
153 return p.decode('utf-8')
154 return p.decode('utf-8')
154
155
155 elif sys.version_info >= (3, 0, 0):
156 elif sys.version_info >= (3, 0, 0):
156 print('%s is only supported on Python 3.5+ and 2.7, not %s' %
157 print('%s is only supported on Python 3.5+ and 2.7, not %s' %
157 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
158 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
158 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
159 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
159 else:
160 else:
160 PYTHON3 = False
161 PYTHON3 = False
161
162
162 # In python 2.x, path operations are generally done using
163 # In python 2.x, path operations are generally done using
163 # bytestrings by default, so we don't have to do any extra
164 # bytestrings by default, so we don't have to do any extra
164 # fiddling there. We define the wrapper functions anyway just to
165 # fiddling there. We define the wrapper functions anyway just to
165 # help keep code consistent between platforms.
166 # help keep code consistent between platforms.
166 def _bytespath(p):
167 def _bytespath(p):
167 return p
168 return p
168
169
169 _strpath = _bytespath
170 _strpath = _bytespath
170
171
171 # For Windows support
172 # For Windows support
172 wifexited = getattr(os, "WIFEXITED", lambda x: False)
173 wifexited = getattr(os, "WIFEXITED", lambda x: False)
173
174
174 # Whether to use IPv6
175 # Whether to use IPv6
175 def checksocketfamily(name, port=20058):
176 def checksocketfamily(name, port=20058):
176 """return true if we can listen on localhost using family=name
177 """return true if we can listen on localhost using family=name
177
178
178 name should be either 'AF_INET', or 'AF_INET6'.
179 name should be either 'AF_INET', or 'AF_INET6'.
179 port being used is okay - EADDRINUSE is considered as successful.
180 port being used is okay - EADDRINUSE is considered as successful.
180 """
181 """
181 family = getattr(socket, name, None)
182 family = getattr(socket, name, None)
182 if family is None:
183 if family is None:
183 return False
184 return False
184 try:
185 try:
185 s = socket.socket(family, socket.SOCK_STREAM)
186 s = socket.socket(family, socket.SOCK_STREAM)
186 s.bind(('localhost', port))
187 s.bind(('localhost', port))
187 s.close()
188 s.close()
188 return True
189 return True
189 except socket.error as exc:
190 except socket.error as exc:
190 if exc.errno == errno.EADDRINUSE:
191 if exc.errno == errno.EADDRINUSE:
191 return True
192 return True
192 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
193 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
193 return False
194 return False
194 else:
195 else:
195 raise
196 raise
196 else:
197 else:
197 return False
198 return False
198
199
199 # useipv6 will be set by parseargs
200 # useipv6 will be set by parseargs
200 useipv6 = None
201 useipv6 = None
201
202
202 def checkportisavailable(port):
203 def checkportisavailable(port):
203 """return true if a port seems free to bind on localhost"""
204 """return true if a port seems free to bind on localhost"""
204 if useipv6:
205 if useipv6:
205 family = socket.AF_INET6
206 family = socket.AF_INET6
206 else:
207 else:
207 family = socket.AF_INET
208 family = socket.AF_INET
208 try:
209 try:
209 s = socket.socket(family, socket.SOCK_STREAM)
210 s = socket.socket(family, socket.SOCK_STREAM)
210 s.bind(('localhost', port))
211 s.bind(('localhost', port))
211 s.close()
212 s.close()
212 return True
213 return True
213 except socket.error as exc:
214 except socket.error as exc:
214 if exc.errno not in (errno.EADDRINUSE, errno.EADDRNOTAVAIL,
215 if exc.errno not in (errno.EADDRINUSE, errno.EADDRNOTAVAIL,
215 errno.EPROTONOSUPPORT):
216 errno.EPROTONOSUPPORT):
216 raise
217 raise
217 return False
218 return False
218
219
219 closefds = os.name == 'posix'
220 closefds = os.name == 'posix'
220 def Popen4(cmd, wd, timeout, env=None):
221 def Popen4(cmd, wd, timeout, env=None):
221 processlock.acquire()
222 processlock.acquire()
222 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
223 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
223 close_fds=closefds,
224 close_fds=closefds,
224 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
225 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
225 stderr=subprocess.STDOUT)
226 stderr=subprocess.STDOUT)
226 processlock.release()
227 processlock.release()
227
228
228 p.fromchild = p.stdout
229 p.fromchild = p.stdout
229 p.tochild = p.stdin
230 p.tochild = p.stdin
230 p.childerr = p.stderr
231 p.childerr = p.stderr
231
232
232 p.timeout = False
233 p.timeout = False
233 if timeout:
234 if timeout:
234 def t():
235 def t():
235 start = time.time()
236 start = time.time()
236 while time.time() - start < timeout and p.returncode is None:
237 while time.time() - start < timeout and p.returncode is None:
237 time.sleep(.1)
238 time.sleep(.1)
238 p.timeout = True
239 p.timeout = True
239 if p.returncode is None:
240 if p.returncode is None:
240 terminate(p)
241 terminate(p)
241 threading.Thread(target=t).start()
242 threading.Thread(target=t).start()
242
243
243 return p
244 return p
244
245
245 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
246 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
246 IMPL_PATH = b'PYTHONPATH'
247 IMPL_PATH = b'PYTHONPATH'
247 if 'java' in sys.platform:
248 if 'java' in sys.platform:
248 IMPL_PATH = b'JYTHONPATH'
249 IMPL_PATH = b'JYTHONPATH'
249
250
250 defaults = {
251 defaults = {
251 'jobs': ('HGTEST_JOBS', 1),
252 'jobs': ('HGTEST_JOBS', 1),
252 'timeout': ('HGTEST_TIMEOUT', 180),
253 'timeout': ('HGTEST_TIMEOUT', 180),
253 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
254 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
254 'port': ('HGTEST_PORT', 20059),
255 'port': ('HGTEST_PORT', 20059),
255 'shell': ('HGTEST_SHELL', 'sh'),
256 'shell': ('HGTEST_SHELL', 'sh'),
256 }
257 }
257
258
258 def canonpath(path):
259 def canonpath(path):
259 return os.path.realpath(os.path.expanduser(path))
260 return os.path.realpath(os.path.expanduser(path))
260
261
261 def parselistfiles(files, listtype, warn=True):
262 def parselistfiles(files, listtype, warn=True):
262 entries = dict()
263 entries = dict()
263 for filename in files:
264 for filename in files:
264 try:
265 try:
265 path = os.path.expanduser(os.path.expandvars(filename))
266 path = os.path.expanduser(os.path.expandvars(filename))
266 f = open(path, "rb")
267 f = open(path, "rb")
267 except IOError as err:
268 except IOError as err:
268 if err.errno != errno.ENOENT:
269 if err.errno != errno.ENOENT:
269 raise
270 raise
270 if warn:
271 if warn:
271 print("warning: no such %s file: %s" % (listtype, filename))
272 print("warning: no such %s file: %s" % (listtype, filename))
272 continue
273 continue
273
274
274 for line in f.readlines():
275 for line in f.readlines():
275 line = line.split(b'#', 1)[0].strip()
276 line = line.split(b'#', 1)[0].strip()
276 if line:
277 if line:
277 entries[line] = filename
278 entries[line] = filename
278
279
279 f.close()
280 f.close()
280 return entries
281 return entries
281
282
282 def parsettestcases(path):
283 def parsettestcases(path):
283 """read a .t test file, return a set of test case names
284 """read a .t test file, return a set of test case names
284
285
285 If path does not exist, return an empty set.
286 If path does not exist, return an empty set.
286 """
287 """
287 cases = set()
288 cases = set()
288 try:
289 try:
289 with open(path, 'rb') as f:
290 with open(path, 'rb') as f:
290 for l in f:
291 for l in f:
291 if l.startswith(b'#testcases '):
292 if l.startswith(b'#testcases '):
292 cases.update(l[11:].split())
293 cases.update(l[11:].split())
293 except IOError as ex:
294 except IOError as ex:
294 if ex.errno != errno.ENOENT:
295 if ex.errno != errno.ENOENT:
295 raise
296 raise
296 return cases
297 return cases
297
298
298 def getparser():
299 def getparser():
299 """Obtain the OptionParser used by the CLI."""
300 """Obtain the OptionParser used by the CLI."""
300 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
301 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
301
302
302 selection = parser.add_argument_group('Test Selection')
303 selection = parser.add_argument_group('Test Selection')
303 selection.add_argument('--allow-slow-tests', action='store_true',
304 selection.add_argument('--allow-slow-tests', action='store_true',
304 help='allow extremely slow tests')
305 help='allow extremely slow tests')
305 selection.add_argument("--blacklist", action="append",
306 selection.add_argument("--blacklist", action="append",
306 help="skip tests listed in the specified blacklist file")
307 help="skip tests listed in the specified blacklist file")
307 selection.add_argument("--changed",
308 selection.add_argument("--changed",
308 help="run tests that are changed in parent rev or working directory")
309 help="run tests that are changed in parent rev or working directory")
309 selection.add_argument("-k", "--keywords",
310 selection.add_argument("-k", "--keywords",
310 help="run tests matching keywords")
311 help="run tests matching keywords")
311 selection.add_argument("-r", "--retest", action="store_true",
312 selection.add_argument("-r", "--retest", action="store_true",
312 help = "retest failed tests")
313 help = "retest failed tests")
313 selection.add_argument("--test-list", action="append",
314 selection.add_argument("--test-list", action="append",
314 help="read tests to run from the specified file")
315 help="read tests to run from the specified file")
315 selection.add_argument("--whitelist", action="append",
316 selection.add_argument("--whitelist", action="append",
316 help="always run tests listed in the specified whitelist file")
317 help="always run tests listed in the specified whitelist file")
317 selection.add_argument('tests', metavar='TESTS', nargs='*',
318 selection.add_argument('tests', metavar='TESTS', nargs='*',
318 help='Tests to run')
319 help='Tests to run')
319
320
320 harness = parser.add_argument_group('Test Harness Behavior')
321 harness = parser.add_argument_group('Test Harness Behavior')
321 harness.add_argument('--bisect-repo',
322 harness.add_argument('--bisect-repo',
322 metavar='bisect_repo',
323 metavar='bisect_repo',
323 help=("Path of a repo to bisect. Use together with "
324 help=("Path of a repo to bisect. Use together with "
324 "--known-good-rev"))
325 "--known-good-rev"))
325 harness.add_argument("-d", "--debug", action="store_true",
326 harness.add_argument("-d", "--debug", action="store_true",
326 help="debug mode: write output of test scripts to console"
327 help="debug mode: write output of test scripts to console"
327 " rather than capturing and diffing it (disables timeout)")
328 " rather than capturing and diffing it (disables timeout)")
328 harness.add_argument("-f", "--first", action="store_true",
329 harness.add_argument("-f", "--first", action="store_true",
329 help="exit on the first test failure")
330 help="exit on the first test failure")
330 harness.add_argument("-i", "--interactive", action="store_true",
331 harness.add_argument("-i", "--interactive", action="store_true",
331 help="prompt to accept changed output")
332 help="prompt to accept changed output")
332 harness.add_argument("-j", "--jobs", type=int,
333 harness.add_argument("-j", "--jobs", type=int,
333 help="number of jobs to run in parallel"
334 help="number of jobs to run in parallel"
334 " (default: $%s or %d)" % defaults['jobs'])
335 " (default: $%s or %d)" % defaults['jobs'])
335 harness.add_argument("--keep-tmpdir", action="store_true",
336 harness.add_argument("--keep-tmpdir", action="store_true",
336 help="keep temporary directory after running tests")
337 help="keep temporary directory after running tests")
337 harness.add_argument('--known-good-rev',
338 harness.add_argument('--known-good-rev',
338 metavar="known_good_rev",
339 metavar="known_good_rev",
339 help=("Automatically bisect any failures using this "
340 help=("Automatically bisect any failures using this "
340 "revision as a known-good revision."))
341 "revision as a known-good revision."))
341 harness.add_argument("--list-tests", action="store_true",
342 harness.add_argument("--list-tests", action="store_true",
342 help="list tests instead of running them")
343 help="list tests instead of running them")
343 harness.add_argument("--loop", action="store_true",
344 harness.add_argument("--loop", action="store_true",
344 help="loop tests repeatedly")
345 help="loop tests repeatedly")
345 harness.add_argument('--random', action="store_true",
346 harness.add_argument('--random', action="store_true",
346 help='run tests in random order')
347 help='run tests in random order')
347 harness.add_argument("-p", "--port", type=int,
348 harness.add_argument("-p", "--port", type=int,
348 help="port on which servers should listen"
349 help="port on which servers should listen"
349 " (default: $%s or %d)" % defaults['port'])
350 " (default: $%s or %d)" % defaults['port'])
350 harness.add_argument('--profile-runner', action='store_true',
351 harness.add_argument('--profile-runner', action='store_true',
351 help='run statprof on run-tests')
352 help='run statprof on run-tests')
352 harness.add_argument("-R", "--restart", action="store_true",
353 harness.add_argument("-R", "--restart", action="store_true",
353 help="restart at last error")
354 help="restart at last error")
354 harness.add_argument("--runs-per-test", type=int, dest="runs_per_test",
355 harness.add_argument("--runs-per-test", type=int, dest="runs_per_test",
355 help="run each test N times (default=1)", default=1)
356 help="run each test N times (default=1)", default=1)
356 harness.add_argument("--shell",
357 harness.add_argument("--shell",
357 help="shell to use (default: $%s or %s)" % defaults['shell'])
358 help="shell to use (default: $%s or %s)" % defaults['shell'])
358 harness.add_argument('--showchannels', action='store_true',
359 harness.add_argument('--showchannels', action='store_true',
359 help='show scheduling channels')
360 help='show scheduling channels')
360 harness.add_argument("--slowtimeout", type=int,
361 harness.add_argument("--slowtimeout", type=int,
361 help="kill errant slow tests after SLOWTIMEOUT seconds"
362 help="kill errant slow tests after SLOWTIMEOUT seconds"
362 " (default: $%s or %d)" % defaults['slowtimeout'])
363 " (default: $%s or %d)" % defaults['slowtimeout'])
363 harness.add_argument("-t", "--timeout", type=int,
364 harness.add_argument("-t", "--timeout", type=int,
364 help="kill errant tests after TIMEOUT seconds"
365 help="kill errant tests after TIMEOUT seconds"
365 " (default: $%s or %d)" % defaults['timeout'])
366 " (default: $%s or %d)" % defaults['timeout'])
366 harness.add_argument("--tmpdir",
367 harness.add_argument("--tmpdir",
367 help="run tests in the given temporary directory"
368 help="run tests in the given temporary directory"
368 " (implies --keep-tmpdir)")
369 " (implies --keep-tmpdir)")
369 harness.add_argument("-v", "--verbose", action="store_true",
370 harness.add_argument("-v", "--verbose", action="store_true",
370 help="output verbose messages")
371 help="output verbose messages")
371
372
372 hgconf = parser.add_argument_group('Mercurial Configuration')
373 hgconf = parser.add_argument_group('Mercurial Configuration')
373 hgconf.add_argument("--chg", action="store_true",
374 hgconf.add_argument("--chg", action="store_true",
374 help="install and use chg wrapper in place of hg")
375 help="install and use chg wrapper in place of hg")
375 hgconf.add_argument("--compiler",
376 hgconf.add_argument("--compiler",
376 help="compiler to build with")
377 help="compiler to build with")
377 hgconf.add_argument('--extra-config-opt', action="append", default=[],
378 hgconf.add_argument('--extra-config-opt', action="append", default=[],
378 help='set the given config opt in the test hgrc')
379 help='set the given config opt in the test hgrc')
379 hgconf.add_argument("-l", "--local", action="store_true",
380 hgconf.add_argument("-l", "--local", action="store_true",
380 help="shortcut for --with-hg=<testdir>/../hg, "
381 help="shortcut for --with-hg=<testdir>/../hg, "
381 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
382 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
382 hgconf.add_argument("--ipv6", action="store_true",
383 hgconf.add_argument("--ipv6", action="store_true",
383 help="prefer IPv6 to IPv4 for network related tests")
384 help="prefer IPv6 to IPv4 for network related tests")
384 hgconf.add_argument("--pure", action="store_true",
385 hgconf.add_argument("--pure", action="store_true",
385 help="use pure Python code instead of C extensions")
386 help="use pure Python code instead of C extensions")
386 hgconf.add_argument("-3", "--py3k-warnings", action="store_true",
387 hgconf.add_argument("-3", "--py3k-warnings", action="store_true",
387 help="enable Py3k warnings on Python 2.7+")
388 help="enable Py3k warnings on Python 2.7+")
388 hgconf.add_argument("--with-chg", metavar="CHG",
389 hgconf.add_argument("--with-chg", metavar="CHG",
389 help="use specified chg wrapper in place of hg")
390 help="use specified chg wrapper in place of hg")
390 hgconf.add_argument("--with-hg",
391 hgconf.add_argument("--with-hg",
391 metavar="HG",
392 metavar="HG",
392 help="test using specified hg script rather than a "
393 help="test using specified hg script rather than a "
393 "temporary installation")
394 "temporary installation")
394 # This option should be deleted once test-check-py3-compat.t and other
395 # This option should be deleted once test-check-py3-compat.t and other
395 # Python 3 tests run with Python 3.
396 # Python 3 tests run with Python 3.
396 hgconf.add_argument("--with-python3", metavar="PYTHON3",
397 hgconf.add_argument("--with-python3", metavar="PYTHON3",
397 help="Python 3 interpreter (if running under Python 2)"
398 help="Python 3 interpreter (if running under Python 2)"
398 " (TEMPORARY)")
399 " (TEMPORARY)")
399
400
400 reporting = parser.add_argument_group('Results Reporting')
401 reporting = parser.add_argument_group('Results Reporting')
401 reporting.add_argument("-C", "--annotate", action="store_true",
402 reporting.add_argument("-C", "--annotate", action="store_true",
402 help="output files annotated with coverage")
403 help="output files annotated with coverage")
403 reporting.add_argument("--color", choices=["always", "auto", "never"],
404 reporting.add_argument("--color", choices=["always", "auto", "never"],
404 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
405 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
405 help="colorisation: always|auto|never (default: auto)")
406 help="colorisation: always|auto|never (default: auto)")
406 reporting.add_argument("-c", "--cover", action="store_true",
407 reporting.add_argument("-c", "--cover", action="store_true",
407 help="print a test coverage report")
408 help="print a test coverage report")
408 reporting.add_argument('--exceptions', action='store_true',
409 reporting.add_argument('--exceptions', action='store_true',
409 help='log all exceptions and generate an exception report')
410 help='log all exceptions and generate an exception report')
410 reporting.add_argument("-H", "--htmlcov", action="store_true",
411 reporting.add_argument("-H", "--htmlcov", action="store_true",
411 help="create an HTML report of the coverage of the files")
412 help="create an HTML report of the coverage of the files")
412 reporting.add_argument("--json", action="store_true",
413 reporting.add_argument("--json", action="store_true",
413 help="store test result data in 'report.json' file")
414 help="store test result data in 'report.json' file")
414 reporting.add_argument("--outputdir",
415 reporting.add_argument("--outputdir",
415 help="directory to write error logs to (default=test directory)")
416 help="directory to write error logs to (default=test directory)")
416 reporting.add_argument("-n", "--nodiff", action="store_true",
417 reporting.add_argument("-n", "--nodiff", action="store_true",
417 help="skip showing test changes")
418 help="skip showing test changes")
418 reporting.add_argument("-S", "--noskips", action="store_true",
419 reporting.add_argument("-S", "--noskips", action="store_true",
419 help="don't report skip tests verbosely")
420 help="don't report skip tests verbosely")
420 reporting.add_argument("--time", action="store_true",
421 reporting.add_argument("--time", action="store_true",
421 help="time how long each test takes")
422 help="time how long each test takes")
422 reporting.add_argument("--view",
423 reporting.add_argument("--view",
423 help="external diff viewer")
424 help="external diff viewer")
424 reporting.add_argument("--xunit",
425 reporting.add_argument("--xunit",
425 help="record xunit results at specified path")
426 help="record xunit results at specified path")
426
427
427 for option, (envvar, default) in defaults.items():
428 for option, (envvar, default) in defaults.items():
428 defaults[option] = type(default)(os.environ.get(envvar, default))
429 defaults[option] = type(default)(os.environ.get(envvar, default))
429 parser.set_defaults(**defaults)
430 parser.set_defaults(**defaults)
430
431
431 return parser
432 return parser
432
433
433 def parseargs(args, parser):
434 def parseargs(args, parser):
434 """Parse arguments with our OptionParser and validate results."""
435 """Parse arguments with our OptionParser and validate results."""
435 options = parser.parse_args(args)
436 options = parser.parse_args(args)
436
437
437 # jython is always pure
438 # jython is always pure
438 if 'java' in sys.platform or '__pypy__' in sys.modules:
439 if 'java' in sys.platform or '__pypy__' in sys.modules:
439 options.pure = True
440 options.pure = True
440
441
441 if options.with_hg:
442 if options.with_hg:
442 options.with_hg = canonpath(_bytespath(options.with_hg))
443 options.with_hg = canonpath(_bytespath(options.with_hg))
443 if not (os.path.isfile(options.with_hg) and
444 if not (os.path.isfile(options.with_hg) and
444 os.access(options.with_hg, os.X_OK)):
445 os.access(options.with_hg, os.X_OK)):
445 parser.error('--with-hg must specify an executable hg script')
446 parser.error('--with-hg must specify an executable hg script')
446 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
447 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
447 sys.stderr.write('warning: --with-hg should specify an hg script\n')
448 sys.stderr.write('warning: --with-hg should specify an hg script\n')
448 if options.local:
449 if options.local:
449 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
450 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
450 reporootdir = os.path.dirname(testdir)
451 reporootdir = os.path.dirname(testdir)
451 pathandattrs = [(b'hg', 'with_hg')]
452 pathandattrs = [(b'hg', 'with_hg')]
452 if options.chg:
453 if options.chg:
453 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
454 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
454 for relpath, attr in pathandattrs:
455 for relpath, attr in pathandattrs:
455 binpath = os.path.join(reporootdir, relpath)
456 binpath = os.path.join(reporootdir, relpath)
456 if os.name != 'nt' and not os.access(binpath, os.X_OK):
457 if os.name != 'nt' and not os.access(binpath, os.X_OK):
457 parser.error('--local specified, but %r not found or '
458 parser.error('--local specified, but %r not found or '
458 'not executable' % binpath)
459 'not executable' % binpath)
459 setattr(options, attr, binpath)
460 setattr(options, attr, binpath)
460
461
461 if (options.chg or options.with_chg) and os.name == 'nt':
462 if (options.chg or options.with_chg) and os.name == 'nt':
462 parser.error('chg does not work on %s' % os.name)
463 parser.error('chg does not work on %s' % os.name)
463 if options.with_chg:
464 if options.with_chg:
464 options.chg = False # no installation to temporary location
465 options.chg = False # no installation to temporary location
465 options.with_chg = canonpath(_bytespath(options.with_chg))
466 options.with_chg = canonpath(_bytespath(options.with_chg))
466 if not (os.path.isfile(options.with_chg) and
467 if not (os.path.isfile(options.with_chg) and
467 os.access(options.with_chg, os.X_OK)):
468 os.access(options.with_chg, os.X_OK)):
468 parser.error('--with-chg must specify a chg executable')
469 parser.error('--with-chg must specify a chg executable')
469 if options.chg and options.with_hg:
470 if options.chg and options.with_hg:
470 # chg shares installation location with hg
471 # chg shares installation location with hg
471 parser.error('--chg does not work when --with-hg is specified '
472 parser.error('--chg does not work when --with-hg is specified '
472 '(use --with-chg instead)')
473 '(use --with-chg instead)')
473
474
474 if options.color == 'always' and not pygmentspresent:
475 if options.color == 'always' and not pygmentspresent:
475 sys.stderr.write('warning: --color=always ignored because '
476 sys.stderr.write('warning: --color=always ignored because '
476 'pygments is not installed\n')
477 'pygments is not installed\n')
477
478
478 if options.bisect_repo and not options.known_good_rev:
479 if options.bisect_repo and not options.known_good_rev:
479 parser.error("--bisect-repo cannot be used without --known-good-rev")
480 parser.error("--bisect-repo cannot be used without --known-good-rev")
480
481
481 global useipv6
482 global useipv6
482 if options.ipv6:
483 if options.ipv6:
483 useipv6 = checksocketfamily('AF_INET6')
484 useipv6 = checksocketfamily('AF_INET6')
484 else:
485 else:
485 # only use IPv6 if IPv4 is unavailable and IPv6 is available
486 # only use IPv6 if IPv4 is unavailable and IPv6 is available
486 useipv6 = ((not checksocketfamily('AF_INET'))
487 useipv6 = ((not checksocketfamily('AF_INET'))
487 and checksocketfamily('AF_INET6'))
488 and checksocketfamily('AF_INET6'))
488
489
489 options.anycoverage = options.cover or options.annotate or options.htmlcov
490 options.anycoverage = options.cover or options.annotate or options.htmlcov
490 if options.anycoverage:
491 if options.anycoverage:
491 try:
492 try:
492 import coverage
493 import coverage
493 covver = version.StrictVersion(coverage.__version__).version
494 covver = version.StrictVersion(coverage.__version__).version
494 if covver < (3, 3):
495 if covver < (3, 3):
495 parser.error('coverage options require coverage 3.3 or later')
496 parser.error('coverage options require coverage 3.3 or later')
496 except ImportError:
497 except ImportError:
497 parser.error('coverage options now require the coverage package')
498 parser.error('coverage options now require the coverage package')
498
499
499 if options.anycoverage and options.local:
500 if options.anycoverage and options.local:
500 # this needs some path mangling somewhere, I guess
501 # this needs some path mangling somewhere, I guess
501 parser.error("sorry, coverage options do not work when --local "
502 parser.error("sorry, coverage options do not work when --local "
502 "is specified")
503 "is specified")
503
504
504 if options.anycoverage and options.with_hg:
505 if options.anycoverage and options.with_hg:
505 parser.error("sorry, coverage options do not work when --with-hg "
506 parser.error("sorry, coverage options do not work when --with-hg "
506 "is specified")
507 "is specified")
507
508
508 global verbose
509 global verbose
509 if options.verbose:
510 if options.verbose:
510 verbose = ''
511 verbose = ''
511
512
512 if options.tmpdir:
513 if options.tmpdir:
513 options.tmpdir = canonpath(options.tmpdir)
514 options.tmpdir = canonpath(options.tmpdir)
514
515
515 if options.jobs < 1:
516 if options.jobs < 1:
516 parser.error('--jobs must be positive')
517 parser.error('--jobs must be positive')
517 if options.interactive and options.debug:
518 if options.interactive and options.debug:
518 parser.error("-i/--interactive and -d/--debug are incompatible")
519 parser.error("-i/--interactive and -d/--debug are incompatible")
519 if options.debug:
520 if options.debug:
520 if options.timeout != defaults['timeout']:
521 if options.timeout != defaults['timeout']:
521 sys.stderr.write(
522 sys.stderr.write(
522 'warning: --timeout option ignored with --debug\n')
523 'warning: --timeout option ignored with --debug\n')
523 if options.slowtimeout != defaults['slowtimeout']:
524 if options.slowtimeout != defaults['slowtimeout']:
524 sys.stderr.write(
525 sys.stderr.write(
525 'warning: --slowtimeout option ignored with --debug\n')
526 'warning: --slowtimeout option ignored with --debug\n')
526 options.timeout = 0
527 options.timeout = 0
527 options.slowtimeout = 0
528 options.slowtimeout = 0
528 if options.py3k_warnings:
529 if options.py3k_warnings:
529 if PYTHON3:
530 if PYTHON3:
530 parser.error(
531 parser.error(
531 '--py3k-warnings can only be used on Python 2.7')
532 '--py3k-warnings can only be used on Python 2.7')
532 if options.with_python3:
533 if options.with_python3:
533 if PYTHON3:
534 if PYTHON3:
534 parser.error('--with-python3 cannot be used when executing with '
535 parser.error('--with-python3 cannot be used when executing with '
535 'Python 3')
536 'Python 3')
536
537
537 options.with_python3 = canonpath(options.with_python3)
538 options.with_python3 = canonpath(options.with_python3)
538 # Verify Python3 executable is acceptable.
539 # Verify Python3 executable is acceptable.
539 proc = subprocess.Popen([options.with_python3, b'--version'],
540 proc = subprocess.Popen([options.with_python3, b'--version'],
540 stdout=subprocess.PIPE,
541 stdout=subprocess.PIPE,
541 stderr=subprocess.STDOUT)
542 stderr=subprocess.STDOUT)
542 out, _err = proc.communicate()
543 out, _err = proc.communicate()
543 ret = proc.wait()
544 ret = proc.wait()
544 if ret != 0:
545 if ret != 0:
545 parser.error('could not determine version of python 3')
546 parser.error('could not determine version of python 3')
546 if not out.startswith('Python '):
547 if not out.startswith('Python '):
547 parser.error('unexpected output from python3 --version: %s' %
548 parser.error('unexpected output from python3 --version: %s' %
548 out)
549 out)
549 vers = version.LooseVersion(out[len('Python '):])
550 vers = version.LooseVersion(out[len('Python '):])
550 if vers < version.LooseVersion('3.5.0'):
551 if vers < version.LooseVersion('3.5.0'):
551 parser.error('--with-python3 version must be 3.5.0 or greater; '
552 parser.error('--with-python3 version must be 3.5.0 or greater; '
552 'got %s' % out)
553 'got %s' % out)
553
554
554 if options.blacklist:
555 if options.blacklist:
555 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
556 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
556 if options.whitelist:
557 if options.whitelist:
557 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
558 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
558 else:
559 else:
559 options.whitelisted = {}
560 options.whitelisted = {}
560
561
561 if options.showchannels:
562 if options.showchannels:
562 options.nodiff = True
563 options.nodiff = True
563
564
564 return options
565 return options
565
566
566 def rename(src, dst):
567 def rename(src, dst):
567 """Like os.rename(), trade atomicity and opened files friendliness
568 """Like os.rename(), trade atomicity and opened files friendliness
568 for existing destination support.
569 for existing destination support.
569 """
570 """
570 shutil.copy(src, dst)
571 shutil.copy(src, dst)
571 os.remove(src)
572 os.remove(src)
572
573
573 _unified_diff = difflib.unified_diff
574 _unified_diff = difflib.unified_diff
574 if PYTHON3:
575 if PYTHON3:
575 import functools
576 import functools
576 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
577 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
577
578
578 def getdiff(expected, output, ref, err):
579 def getdiff(expected, output, ref, err):
579 servefail = False
580 servefail = False
580 lines = []
581 lines = []
581 for line in _unified_diff(expected, output, ref, err):
582 for line in _unified_diff(expected, output, ref, err):
582 if line.startswith(b'+++') or line.startswith(b'---'):
583 if line.startswith(b'+++') or line.startswith(b'---'):
583 line = line.replace(b'\\', b'/')
584 line = line.replace(b'\\', b'/')
584 if line.endswith(b' \n'):
585 if line.endswith(b' \n'):
585 line = line[:-2] + b'\n'
586 line = line[:-2] + b'\n'
586 lines.append(line)
587 lines.append(line)
587 if not servefail and line.startswith(
588 if not servefail and line.startswith(
588 b'+ abort: child process failed to start'):
589 b'+ abort: child process failed to start'):
589 servefail = True
590 servefail = True
590
591
591 return servefail, lines
592 return servefail, lines
592
593
593 verbose = False
594 verbose = False
594 def vlog(*msg):
595 def vlog(*msg):
595 """Log only when in verbose mode."""
596 """Log only when in verbose mode."""
596 if verbose is False:
597 if verbose is False:
597 return
598 return
598
599
599 return log(*msg)
600 return log(*msg)
600
601
601 # Bytes that break XML even in a CDATA block: control characters 0-31
602 # Bytes that break XML even in a CDATA block: control characters 0-31
602 # sans \t, \n and \r
603 # sans \t, \n and \r
603 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
604 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
604
605
605 # Match feature conditionalized output lines in the form, capturing the feature
606 # Match feature conditionalized output lines in the form, capturing the feature
606 # list in group 2, and the preceeding line output in group 1:
607 # list in group 2, and the preceeding line output in group 1:
607 #
608 #
608 # output..output (feature !)\n
609 # output..output (feature !)\n
609 optline = re.compile(b'(.*) \((.+?) !\)\n$')
610 optline = re.compile(b'(.*) \((.+?) !\)\n$')
610
611
611 def cdatasafe(data):
612 def cdatasafe(data):
612 """Make a string safe to include in a CDATA block.
613 """Make a string safe to include in a CDATA block.
613
614
614 Certain control characters are illegal in a CDATA block, and
615 Certain control characters are illegal in a CDATA block, and
615 there's no way to include a ]]> in a CDATA either. This function
616 there's no way to include a ]]> in a CDATA either. This function
616 replaces illegal bytes with ? and adds a space between the ]] so
617 replaces illegal bytes with ? and adds a space between the ]] so
617 that it won't break the CDATA block.
618 that it won't break the CDATA block.
618 """
619 """
619 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
620 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
620
621
621 def log(*msg):
622 def log(*msg):
622 """Log something to stdout.
623 """Log something to stdout.
623
624
624 Arguments are strings to print.
625 Arguments are strings to print.
625 """
626 """
626 with iolock:
627 with iolock:
627 if verbose:
628 if verbose:
628 print(verbose, end=' ')
629 print(verbose, end=' ')
629 for m in msg:
630 for m in msg:
630 print(m, end=' ')
631 print(m, end=' ')
631 print()
632 print()
632 sys.stdout.flush()
633 sys.stdout.flush()
633
634
634 def highlightdiff(line, color):
635 def highlightdiff(line, color):
635 if not color:
636 if not color:
636 return line
637 return line
637 assert pygmentspresent
638 assert pygmentspresent
638 return pygments.highlight(line.decode('latin1'), difflexer,
639 return pygments.highlight(line.decode('latin1'), difflexer,
639 terminal256formatter).encode('latin1')
640 terminal256formatter).encode('latin1')
640
641
641 def highlightmsg(msg, color):
642 def highlightmsg(msg, color):
642 if not color:
643 if not color:
643 return msg
644 return msg
644 assert pygmentspresent
645 assert pygmentspresent
645 return pygments.highlight(msg, runnerlexer, runnerformatter)
646 return pygments.highlight(msg, runnerlexer, runnerformatter)
646
647
647 def terminate(proc):
648 def terminate(proc):
648 """Terminate subprocess"""
649 """Terminate subprocess"""
649 vlog('# Terminating process %d' % proc.pid)
650 vlog('# Terminating process %d' % proc.pid)
650 try:
651 try:
651 proc.terminate()
652 proc.terminate()
652 except OSError:
653 except OSError:
653 pass
654 pass
654
655
655 def killdaemons(pidfile):
656 def killdaemons(pidfile):
656 import killdaemons as killmod
657 import killdaemons as killmod
657 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
658 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
658 logfn=vlog)
659 logfn=vlog)
659
660
660 class Test(unittest.TestCase):
661 class Test(unittest.TestCase):
661 """Encapsulates a single, runnable test.
662 """Encapsulates a single, runnable test.
662
663
663 While this class conforms to the unittest.TestCase API, it differs in that
664 While this class conforms to the unittest.TestCase API, it differs in that
664 instances need to be instantiated manually. (Typically, unittest.TestCase
665 instances need to be instantiated manually. (Typically, unittest.TestCase
665 classes are instantiated automatically by scanning modules.)
666 classes are instantiated automatically by scanning modules.)
666 """
667 """
667
668
668 # Status code reserved for skipped tests (used by hghave).
669 # Status code reserved for skipped tests (used by hghave).
669 SKIPPED_STATUS = 80
670 SKIPPED_STATUS = 80
670
671
671 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False,
672 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False,
672 debug=False,
673 debug=False,
673 first=False,
674 first=False,
674 timeout=None,
675 timeout=None,
675 startport=None, extraconfigopts=None,
676 startport=None, extraconfigopts=None,
676 py3kwarnings=False, shell=None, hgcommand=None,
677 py3kwarnings=False, shell=None, hgcommand=None,
677 slowtimeout=None, usechg=False,
678 slowtimeout=None, usechg=False,
678 useipv6=False):
679 useipv6=False):
679 """Create a test from parameters.
680 """Create a test from parameters.
680
681
681 path is the full path to the file defining the test.
682 path is the full path to the file defining the test.
682
683
683 tmpdir is the main temporary directory to use for this test.
684 tmpdir is the main temporary directory to use for this test.
684
685
685 keeptmpdir determines whether to keep the test's temporary directory
686 keeptmpdir determines whether to keep the test's temporary directory
686 after execution. It defaults to removal (False).
687 after execution. It defaults to removal (False).
687
688
688 debug mode will make the test execute verbosely, with unfiltered
689 debug mode will make the test execute verbosely, with unfiltered
689 output.
690 output.
690
691
691 timeout controls the maximum run time of the test. It is ignored when
692 timeout controls the maximum run time of the test. It is ignored when
692 debug is True. See slowtimeout for tests with #require slow.
693 debug is True. See slowtimeout for tests with #require slow.
693
694
694 slowtimeout overrides timeout if the test has #require slow.
695 slowtimeout overrides timeout if the test has #require slow.
695
696
696 startport controls the starting port number to use for this test. Each
697 startport controls the starting port number to use for this test. Each
697 test will reserve 3 port numbers for execution. It is the caller's
698 test will reserve 3 port numbers for execution. It is the caller's
698 responsibility to allocate a non-overlapping port range to Test
699 responsibility to allocate a non-overlapping port range to Test
699 instances.
700 instances.
700
701
701 extraconfigopts is an iterable of extra hgrc config options. Values
702 extraconfigopts is an iterable of extra hgrc config options. Values
702 must have the form "key=value" (something understood by hgrc). Values
703 must have the form "key=value" (something understood by hgrc). Values
703 of the form "foo.key=value" will result in "[foo] key=value".
704 of the form "foo.key=value" will result in "[foo] key=value".
704
705
705 py3kwarnings enables Py3k warnings.
706 py3kwarnings enables Py3k warnings.
706
707
707 shell is the shell to execute tests in.
708 shell is the shell to execute tests in.
708 """
709 """
709 if timeout is None:
710 if timeout is None:
710 timeout = defaults['timeout']
711 timeout = defaults['timeout']
711 if startport is None:
712 if startport is None:
712 startport = defaults['port']
713 startport = defaults['port']
713 if slowtimeout is None:
714 if slowtimeout is None:
714 slowtimeout = defaults['slowtimeout']
715 slowtimeout = defaults['slowtimeout']
715 self.path = path
716 self.path = path
716 self.bname = os.path.basename(path)
717 self.bname = os.path.basename(path)
717 self.name = _strpath(self.bname)
718 self.name = _strpath(self.bname)
718 self._testdir = os.path.dirname(path)
719 self._testdir = os.path.dirname(path)
719 self._outputdir = outputdir
720 self._outputdir = outputdir
720 self._tmpname = os.path.basename(path)
721 self._tmpname = os.path.basename(path)
721 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
722 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
722
723
723 self._threadtmp = tmpdir
724 self._threadtmp = tmpdir
724 self._keeptmpdir = keeptmpdir
725 self._keeptmpdir = keeptmpdir
725 self._debug = debug
726 self._debug = debug
726 self._first = first
727 self._first = first
727 self._timeout = timeout
728 self._timeout = timeout
728 self._slowtimeout = slowtimeout
729 self._slowtimeout = slowtimeout
729 self._startport = startport
730 self._startport = startport
730 self._extraconfigopts = extraconfigopts or []
731 self._extraconfigopts = extraconfigopts or []
731 self._py3kwarnings = py3kwarnings
732 self._py3kwarnings = py3kwarnings
732 self._shell = _bytespath(shell)
733 self._shell = _bytespath(shell)
733 self._hgcommand = hgcommand or b'hg'
734 self._hgcommand = hgcommand or b'hg'
734 self._usechg = usechg
735 self._usechg = usechg
735 self._useipv6 = useipv6
736 self._useipv6 = useipv6
736
737
737 self._aborted = False
738 self._aborted = False
738 self._daemonpids = []
739 self._daemonpids = []
739 self._finished = None
740 self._finished = None
740 self._ret = None
741 self._ret = None
741 self._out = None
742 self._out = None
742 self._skipped = None
743 self._skipped = None
743 self._testtmp = None
744 self._testtmp = None
744 self._chgsockdir = None
745 self._chgsockdir = None
745
746
746 self._refout = self.readrefout()
747 self._refout = self.readrefout()
747
748
748 def readrefout(self):
749 def readrefout(self):
749 """read reference output"""
750 """read reference output"""
750 # If we're not in --debug mode and reference output file exists,
751 # If we're not in --debug mode and reference output file exists,
751 # check test output against it.
752 # check test output against it.
752 if self._debug:
753 if self._debug:
753 return None # to match "out is None"
754 return None # to match "out is None"
754 elif os.path.exists(self.refpath):
755 elif os.path.exists(self.refpath):
755 with open(self.refpath, 'rb') as f:
756 with open(self.refpath, 'rb') as f:
756 return f.read().splitlines(True)
757 return f.read().splitlines(True)
757 else:
758 else:
758 return []
759 return []
759
760
760 # needed to get base class __repr__ running
761 # needed to get base class __repr__ running
761 @property
762 @property
762 def _testMethodName(self):
763 def _testMethodName(self):
763 return self.name
764 return self.name
764
765
765 def __str__(self):
766 def __str__(self):
766 return self.name
767 return self.name
767
768
768 def shortDescription(self):
769 def shortDescription(self):
769 return self.name
770 return self.name
770
771
771 def setUp(self):
772 def setUp(self):
772 """Tasks to perform before run()."""
773 """Tasks to perform before run()."""
773 self._finished = False
774 self._finished = False
774 self._ret = None
775 self._ret = None
775 self._out = None
776 self._out = None
776 self._skipped = None
777 self._skipped = None
777
778
778 try:
779 try:
779 os.mkdir(self._threadtmp)
780 os.mkdir(self._threadtmp)
780 except OSError as e:
781 except OSError as e:
781 if e.errno != errno.EEXIST:
782 if e.errno != errno.EEXIST:
782 raise
783 raise
783
784
784 name = self._tmpname
785 name = self._tmpname
785 self._testtmp = os.path.join(self._threadtmp, name)
786 self._testtmp = os.path.join(self._threadtmp, name)
786 os.mkdir(self._testtmp)
787 os.mkdir(self._testtmp)
787
788
788 # Remove any previous output files.
789 # Remove any previous output files.
789 if os.path.exists(self.errpath):
790 if os.path.exists(self.errpath):
790 try:
791 try:
791 os.remove(self.errpath)
792 os.remove(self.errpath)
792 except OSError as e:
793 except OSError as e:
793 # We might have raced another test to clean up a .err
794 # We might have raced another test to clean up a .err
794 # file, so ignore ENOENT when removing a previous .err
795 # file, so ignore ENOENT when removing a previous .err
795 # file.
796 # file.
796 if e.errno != errno.ENOENT:
797 if e.errno != errno.ENOENT:
797 raise
798 raise
798
799
799 if self._usechg:
800 if self._usechg:
800 self._chgsockdir = os.path.join(self._threadtmp,
801 self._chgsockdir = os.path.join(self._threadtmp,
801 b'%s.chgsock' % name)
802 b'%s.chgsock' % name)
802 os.mkdir(self._chgsockdir)
803 os.mkdir(self._chgsockdir)
803
804
804 def run(self, result):
805 def run(self, result):
805 """Run this test and report results against a TestResult instance."""
806 """Run this test and report results against a TestResult instance."""
806 # This function is extremely similar to unittest.TestCase.run(). Once
807 # This function is extremely similar to unittest.TestCase.run(). Once
807 # we require Python 2.7 (or at least its version of unittest), this
808 # we require Python 2.7 (or at least its version of unittest), this
808 # function can largely go away.
809 # function can largely go away.
809 self._result = result
810 self._result = result
810 result.startTest(self)
811 result.startTest(self)
811 try:
812 try:
812 try:
813 try:
813 self.setUp()
814 self.setUp()
814 except (KeyboardInterrupt, SystemExit):
815 except (KeyboardInterrupt, SystemExit):
815 self._aborted = True
816 self._aborted = True
816 raise
817 raise
817 except Exception:
818 except Exception:
818 result.addError(self, sys.exc_info())
819 result.addError(self, sys.exc_info())
819 return
820 return
820
821
821 success = False
822 success = False
822 try:
823 try:
823 self.runTest()
824 self.runTest()
824 except KeyboardInterrupt:
825 except KeyboardInterrupt:
825 self._aborted = True
826 self._aborted = True
826 raise
827 raise
827 except unittest.SkipTest as e:
828 except unittest.SkipTest as e:
828 result.addSkip(self, str(e))
829 result.addSkip(self, str(e))
829 # The base class will have already counted this as a
830 # The base class will have already counted this as a
830 # test we "ran", but we want to exclude skipped tests
831 # test we "ran", but we want to exclude skipped tests
831 # from those we count towards those run.
832 # from those we count towards those run.
832 result.testsRun -= 1
833 result.testsRun -= 1
833 except self.failureException as e:
834 except self.failureException as e:
834 # This differs from unittest in that we don't capture
835 # This differs from unittest in that we don't capture
835 # the stack trace. This is for historical reasons and
836 # the stack trace. This is for historical reasons and
836 # this decision could be revisited in the future,
837 # this decision could be revisited in the future,
837 # especially for PythonTest instances.
838 # especially for PythonTest instances.
838 if result.addFailure(self, str(e)):
839 if result.addFailure(self, str(e)):
839 success = True
840 success = True
840 except Exception:
841 except Exception:
841 result.addError(self, sys.exc_info())
842 result.addError(self, sys.exc_info())
842 else:
843 else:
843 success = True
844 success = True
844
845
845 try:
846 try:
846 self.tearDown()
847 self.tearDown()
847 except (KeyboardInterrupt, SystemExit):
848 except (KeyboardInterrupt, SystemExit):
848 self._aborted = True
849 self._aborted = True
849 raise
850 raise
850 except Exception:
851 except Exception:
851 result.addError(self, sys.exc_info())
852 result.addError(self, sys.exc_info())
852 success = False
853 success = False
853
854
854 if success:
855 if success:
855 result.addSuccess(self)
856 result.addSuccess(self)
856 finally:
857 finally:
857 result.stopTest(self, interrupted=self._aborted)
858 result.stopTest(self, interrupted=self._aborted)
858
859
859 def runTest(self):
860 def runTest(self):
860 """Run this test instance.
861 """Run this test instance.
861
862
862 This will return a tuple describing the result of the test.
863 This will return a tuple describing the result of the test.
863 """
864 """
864 env = self._getenv()
865 env = self._getenv()
865 self._genrestoreenv(env)
866 self._genrestoreenv(env)
866 self._daemonpids.append(env['DAEMON_PIDS'])
867 self._daemonpids.append(env['DAEMON_PIDS'])
867 self._createhgrc(env['HGRCPATH'])
868 self._createhgrc(env['HGRCPATH'])
868
869
869 vlog('# Test', self.name)
870 vlog('# Test', self.name)
870
871
871 ret, out = self._run(env)
872 ret, out = self._run(env)
872 self._finished = True
873 self._finished = True
873 self._ret = ret
874 self._ret = ret
874 self._out = out
875 self._out = out
875
876
876 def describe(ret):
877 def describe(ret):
877 if ret < 0:
878 if ret < 0:
878 return 'killed by signal: %d' % -ret
879 return 'killed by signal: %d' % -ret
879 return 'returned error code %d' % ret
880 return 'returned error code %d' % ret
880
881
881 self._skipped = False
882 self._skipped = False
882
883
883 if ret == self.SKIPPED_STATUS:
884 if ret == self.SKIPPED_STATUS:
884 if out is None: # Debug mode, nothing to parse.
885 if out is None: # Debug mode, nothing to parse.
885 missing = ['unknown']
886 missing = ['unknown']
886 failed = None
887 failed = None
887 else:
888 else:
888 missing, failed = TTest.parsehghaveoutput(out)
889 missing, failed = TTest.parsehghaveoutput(out)
889
890
890 if not missing:
891 if not missing:
891 missing = ['skipped']
892 missing = ['skipped']
892
893
893 if failed:
894 if failed:
894 self.fail('hg have failed checking for %s' % failed[-1])
895 self.fail('hg have failed checking for %s' % failed[-1])
895 else:
896 else:
896 self._skipped = True
897 self._skipped = True
897 raise unittest.SkipTest(missing[-1])
898 raise unittest.SkipTest(missing[-1])
898 elif ret == 'timeout':
899 elif ret == 'timeout':
899 self.fail('timed out')
900 self.fail('timed out')
900 elif ret is False:
901 elif ret is False:
901 self.fail('no result code from test')
902 self.fail('no result code from test')
902 elif out != self._refout:
903 elif out != self._refout:
903 # Diff generation may rely on written .err file.
904 # Diff generation may rely on written .err file.
904 if (ret != 0 or out != self._refout) and not self._skipped \
905 if (ret != 0 or out != self._refout) and not self._skipped \
905 and not self._debug:
906 and not self._debug:
906 with open(self.errpath, 'wb') as f:
907 with open(self.errpath, 'wb') as f:
907 for line in out:
908 for line in out:
908 f.write(line)
909 f.write(line)
909
910
910 # The result object handles diff calculation for us.
911 # The result object handles diff calculation for us.
911 with firstlock:
912 with firstlock:
912 if self._result.addOutputMismatch(self, ret, out, self._refout):
913 if self._result.addOutputMismatch(self, ret, out, self._refout):
913 # change was accepted, skip failing
914 # change was accepted, skip failing
914 return
915 return
915 if self._first:
916 if self._first:
916 global firsterror
917 global firsterror
917 firsterror = True
918 firsterror = True
918
919
919 if ret:
920 if ret:
920 msg = 'output changed and ' + describe(ret)
921 msg = 'output changed and ' + describe(ret)
921 else:
922 else:
922 msg = 'output changed'
923 msg = 'output changed'
923
924
924 self.fail(msg)
925 self.fail(msg)
925 elif ret:
926 elif ret:
926 self.fail(describe(ret))
927 self.fail(describe(ret))
927
928
928 def tearDown(self):
929 def tearDown(self):
929 """Tasks to perform after run()."""
930 """Tasks to perform after run()."""
930 for entry in self._daemonpids:
931 for entry in self._daemonpids:
931 killdaemons(entry)
932 killdaemons(entry)
932 self._daemonpids = []
933 self._daemonpids = []
933
934
934 if self._keeptmpdir:
935 if self._keeptmpdir:
935 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
936 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
936 (self._testtmp.decode('utf-8'),
937 (self._testtmp.decode('utf-8'),
937 self._threadtmp.decode('utf-8')))
938 self._threadtmp.decode('utf-8')))
938 else:
939 else:
939 shutil.rmtree(self._testtmp, True)
940 shutil.rmtree(self._testtmp, True)
940 shutil.rmtree(self._threadtmp, True)
941 shutil.rmtree(self._threadtmp, True)
941
942
942 if self._usechg:
943 if self._usechg:
943 # chgservers will stop automatically after they find the socket
944 # chgservers will stop automatically after they find the socket
944 # files are deleted
945 # files are deleted
945 shutil.rmtree(self._chgsockdir, True)
946 shutil.rmtree(self._chgsockdir, True)
946
947
947 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
948 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
948 and not self._debug and self._out:
949 and not self._debug and self._out:
949 with open(self.errpath, 'wb') as f:
950 with open(self.errpath, 'wb') as f:
950 for line in self._out:
951 for line in self._out:
951 f.write(line)
952 f.write(line)
952
953
953 vlog("# Ret was:", self._ret, '(%s)' % self.name)
954 vlog("# Ret was:", self._ret, '(%s)' % self.name)
954
955
955 def _run(self, env):
956 def _run(self, env):
956 # This should be implemented in child classes to run tests.
957 # This should be implemented in child classes to run tests.
957 raise unittest.SkipTest('unknown test type')
958 raise unittest.SkipTest('unknown test type')
958
959
959 def abort(self):
960 def abort(self):
960 """Terminate execution of this test."""
961 """Terminate execution of this test."""
961 self._aborted = True
962 self._aborted = True
962
963
963 def _portmap(self, i):
964 def _portmap(self, i):
964 offset = b'' if i == 0 else b'%d' % i
965 offset = b'' if i == 0 else b'%d' % i
965 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
966 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
966
967
967 def _getreplacements(self):
968 def _getreplacements(self):
968 """Obtain a mapping of text replacements to apply to test output.
969 """Obtain a mapping of text replacements to apply to test output.
969
970
970 Test output needs to be normalized so it can be compared to expected
971 Test output needs to be normalized so it can be compared to expected
971 output. This function defines how some of that normalization will
972 output. This function defines how some of that normalization will
972 occur.
973 occur.
973 """
974 """
974 r = [
975 r = [
975 # This list should be parallel to defineport in _getenv
976 # This list should be parallel to defineport in _getenv
976 self._portmap(0),
977 self._portmap(0),
977 self._portmap(1),
978 self._portmap(1),
978 self._portmap(2),
979 self._portmap(2),
979 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
980 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
980 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
981 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
981 ]
982 ]
982 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
983 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
983
984
984 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
985 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
985
986
986 if os.path.exists(replacementfile):
987 if os.path.exists(replacementfile):
987 data = {}
988 data = {}
988 with open(replacementfile, mode='rb') as source:
989 with open(replacementfile, mode='rb') as source:
989 # the intermediate 'compile' step help with debugging
990 # the intermediate 'compile' step help with debugging
990 code = compile(source.read(), replacementfile, 'exec')
991 code = compile(source.read(), replacementfile, 'exec')
991 exec(code, data)
992 exec(code, data)
992 r.extend(data.get('substitutions', ()))
993 r.extend(data.get('substitutions', ()))
993 return r
994 return r
994
995
995 def _escapepath(self, p):
996 def _escapepath(self, p):
996 if os.name == 'nt':
997 if os.name == 'nt':
997 return (
998 return (
998 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
999 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
999 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
1000 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
1000 for c in p))
1001 for c in p))
1001 )
1002 )
1002 else:
1003 else:
1003 return re.escape(p)
1004 return re.escape(p)
1004
1005
1005 def _localip(self):
1006 def _localip(self):
1006 if self._useipv6:
1007 if self._useipv6:
1007 return b'::1'
1008 return b'::1'
1008 else:
1009 else:
1009 return b'127.0.0.1'
1010 return b'127.0.0.1'
1010
1011
1011 def _genrestoreenv(self, testenv):
1012 def _genrestoreenv(self, testenv):
1012 """Generate a script that can be used by tests to restore the original
1013 """Generate a script that can be used by tests to restore the original
1013 environment."""
1014 environment."""
1014 # Put the restoreenv script inside self._threadtmp
1015 # Put the restoreenv script inside self._threadtmp
1015 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1016 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1016 testenv['HGTEST_RESTOREENV'] = scriptpath
1017 testenv['HGTEST_RESTOREENV'] = scriptpath
1017
1018
1018 # Only restore environment variable names that the shell allows
1019 # Only restore environment variable names that the shell allows
1019 # us to export.
1020 # us to export.
1020 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1021 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1021
1022
1022 # Do not restore these variables; otherwise tests would fail.
1023 # Do not restore these variables; otherwise tests would fail.
1023 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1024 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1024
1025
1025 with open(scriptpath, 'w') as envf:
1026 with open(scriptpath, 'w') as envf:
1026 for name, value in origenviron.items():
1027 for name, value in origenviron.items():
1027 if not name_regex.match(name):
1028 if not name_regex.match(name):
1028 # Skip environment variables with unusual names not
1029 # Skip environment variables with unusual names not
1029 # allowed by most shells.
1030 # allowed by most shells.
1030 continue
1031 continue
1031 if name in reqnames:
1032 if name in reqnames:
1032 continue
1033 continue
1033 envf.write('%s=%s\n' % (name, shellquote(value)))
1034 envf.write('%s=%s\n' % (name, shellquote(value)))
1034
1035
1035 for name in testenv:
1036 for name in testenv:
1036 if name in origenviron or name in reqnames:
1037 if name in origenviron or name in reqnames:
1037 continue
1038 continue
1038 envf.write('unset %s\n' % (name,))
1039 envf.write('unset %s\n' % (name,))
1039
1040
1040 def _getenv(self):
1041 def _getenv(self):
1041 """Obtain environment variables to use during test execution."""
1042 """Obtain environment variables to use during test execution."""
1042 def defineport(i):
1043 def defineport(i):
1043 offset = '' if i == 0 else '%s' % i
1044 offset = '' if i == 0 else '%s' % i
1044 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1045 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1045 env = os.environ.copy()
1046 env = os.environ.copy()
1046 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1047 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1047 env['HGEMITWARNINGS'] = '1'
1048 env['HGEMITWARNINGS'] = '1'
1048 env['TESTTMP'] = self._testtmp
1049 env['TESTTMP'] = self._testtmp
1049 env['HOME'] = self._testtmp
1050 env['HOME'] = self._testtmp
1050 # This number should match portneeded in _getport
1051 # This number should match portneeded in _getport
1051 for port in xrange(3):
1052 for port in xrange(3):
1052 # This list should be parallel to _portmap in _getreplacements
1053 # This list should be parallel to _portmap in _getreplacements
1053 defineport(port)
1054 defineport(port)
1054 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
1055 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
1055 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
1056 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
1056 env["HGEDITOR"] = ('"' + sys.executable + '"'
1057 env["HGEDITOR"] = ('"' + sys.executable + '"'
1057 + ' -c "import sys; sys.exit(0)"')
1058 + ' -c "import sys; sys.exit(0)"')
1058 env["HGMERGE"] = "internal:merge"
1059 env["HGMERGE"] = "internal:merge"
1059 env["HGUSER"] = "test"
1060 env["HGUSER"] = "test"
1060 env["HGENCODING"] = "ascii"
1061 env["HGENCODING"] = "ascii"
1061 env["HGENCODINGMODE"] = "strict"
1062 env["HGENCODINGMODE"] = "strict"
1062 env['HGIPV6'] = str(int(self._useipv6))
1063 env['HGIPV6'] = str(int(self._useipv6))
1063
1064
1064 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1065 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1065 # IP addresses.
1066 # IP addresses.
1066 env['LOCALIP'] = self._localip()
1067 env['LOCALIP'] = self._localip()
1067
1068
1068 # Reset some environment variables to well-known values so that
1069 # Reset some environment variables to well-known values so that
1069 # the tests produce repeatable output.
1070 # the tests produce repeatable output.
1070 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1071 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1071 env['TZ'] = 'GMT'
1072 env['TZ'] = 'GMT'
1072 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1073 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1073 env['COLUMNS'] = '80'
1074 env['COLUMNS'] = '80'
1074 env['TERM'] = 'xterm'
1075 env['TERM'] = 'xterm'
1075
1076
1076 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
1077 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
1077 'HGPLAIN HGPLAINEXCEPT EDITOR VISUAL PAGER ' +
1078 'HGPLAIN HGPLAINEXCEPT EDITOR VISUAL PAGER ' +
1078 'NO_PROXY CHGDEBUG').split():
1079 'NO_PROXY CHGDEBUG').split():
1079 if k in env:
1080 if k in env:
1080 del env[k]
1081 del env[k]
1081
1082
1082 # unset env related to hooks
1083 # unset env related to hooks
1083 for k in env.keys():
1084 for k in env.keys():
1084 if k.startswith('HG_'):
1085 if k.startswith('HG_'):
1085 del env[k]
1086 del env[k]
1086
1087
1087 if self._usechg:
1088 if self._usechg:
1088 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1089 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1089
1090
1090 return env
1091 return env
1091
1092
1092 def _createhgrc(self, path):
1093 def _createhgrc(self, path):
1093 """Create an hgrc file for this test."""
1094 """Create an hgrc file for this test."""
1094 with open(path, 'wb') as hgrc:
1095 with open(path, 'wb') as hgrc:
1095 hgrc.write(b'[ui]\n')
1096 hgrc.write(b'[ui]\n')
1096 hgrc.write(b'slash = True\n')
1097 hgrc.write(b'slash = True\n')
1097 hgrc.write(b'interactive = False\n')
1098 hgrc.write(b'interactive = False\n')
1098 hgrc.write(b'mergemarkers = detailed\n')
1099 hgrc.write(b'mergemarkers = detailed\n')
1099 hgrc.write(b'promptecho = True\n')
1100 hgrc.write(b'promptecho = True\n')
1100 hgrc.write(b'[defaults]\n')
1101 hgrc.write(b'[defaults]\n')
1101 hgrc.write(b'[devel]\n')
1102 hgrc.write(b'[devel]\n')
1102 hgrc.write(b'all-warnings = true\n')
1103 hgrc.write(b'all-warnings = true\n')
1103 hgrc.write(b'default-date = 0 0\n')
1104 hgrc.write(b'default-date = 0 0\n')
1104 hgrc.write(b'[largefiles]\n')
1105 hgrc.write(b'[largefiles]\n')
1105 hgrc.write(b'usercache = %s\n' %
1106 hgrc.write(b'usercache = %s\n' %
1106 (os.path.join(self._testtmp, b'.cache/largefiles')))
1107 (os.path.join(self._testtmp, b'.cache/largefiles')))
1107 hgrc.write(b'[lfs]\n')
1108 hgrc.write(b'[lfs]\n')
1108 hgrc.write(b'usercache = %s\n' %
1109 hgrc.write(b'usercache = %s\n' %
1109 (os.path.join(self._testtmp, b'.cache/lfs')))
1110 (os.path.join(self._testtmp, b'.cache/lfs')))
1110 hgrc.write(b'[web]\n')
1111 hgrc.write(b'[web]\n')
1111 hgrc.write(b'address = localhost\n')
1112 hgrc.write(b'address = localhost\n')
1112 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1113 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1113
1114
1114 for opt in self._extraconfigopts:
1115 for opt in self._extraconfigopts:
1115 section, key = opt.encode('utf-8').split(b'.', 1)
1116 section, key = opt.encode('utf-8').split(b'.', 1)
1116 assert b'=' in key, ('extra config opt %s must '
1117 assert b'=' in key, ('extra config opt %s must '
1117 'have an = for assignment' % opt)
1118 'have an = for assignment' % opt)
1118 hgrc.write(b'[%s]\n%s\n' % (section, key))
1119 hgrc.write(b'[%s]\n%s\n' % (section, key))
1119
1120
1120 def fail(self, msg):
1121 def fail(self, msg):
1121 # unittest differentiates between errored and failed.
1122 # unittest differentiates between errored and failed.
1122 # Failed is denoted by AssertionError (by default at least).
1123 # Failed is denoted by AssertionError (by default at least).
1123 raise AssertionError(msg)
1124 raise AssertionError(msg)
1124
1125
1125 def _runcommand(self, cmd, env, normalizenewlines=False):
1126 def _runcommand(self, cmd, env, normalizenewlines=False):
1126 """Run command in a sub-process, capturing the output (stdout and
1127 """Run command in a sub-process, capturing the output (stdout and
1127 stderr).
1128 stderr).
1128
1129
1129 Return a tuple (exitcode, output). output is None in debug mode.
1130 Return a tuple (exitcode, output). output is None in debug mode.
1130 """
1131 """
1131 if self._debug:
1132 if self._debug:
1132 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
1133 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
1133 env=env)
1134 env=env)
1134 ret = proc.wait()
1135 ret = proc.wait()
1135 return (ret, None)
1136 return (ret, None)
1136
1137
1137 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1138 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1138 def cleanup():
1139 def cleanup():
1139 terminate(proc)
1140 terminate(proc)
1140 ret = proc.wait()
1141 ret = proc.wait()
1141 if ret == 0:
1142 if ret == 0:
1142 ret = signal.SIGTERM << 8
1143 ret = signal.SIGTERM << 8
1143 killdaemons(env['DAEMON_PIDS'])
1144 killdaemons(env['DAEMON_PIDS'])
1144 return ret
1145 return ret
1145
1146
1146 output = ''
1147 output = ''
1147 proc.tochild.close()
1148 proc.tochild.close()
1148
1149
1149 try:
1150 try:
1150 output = proc.fromchild.read()
1151 output = proc.fromchild.read()
1151 except KeyboardInterrupt:
1152 except KeyboardInterrupt:
1152 vlog('# Handling keyboard interrupt')
1153 vlog('# Handling keyboard interrupt')
1153 cleanup()
1154 cleanup()
1154 raise
1155 raise
1155
1156
1156 ret = proc.wait()
1157 ret = proc.wait()
1157 if wifexited(ret):
1158 if wifexited(ret):
1158 ret = os.WEXITSTATUS(ret)
1159 ret = os.WEXITSTATUS(ret)
1159
1160
1160 if proc.timeout:
1161 if proc.timeout:
1161 ret = 'timeout'
1162 ret = 'timeout'
1162
1163
1163 if ret:
1164 if ret:
1164 killdaemons(env['DAEMON_PIDS'])
1165 killdaemons(env['DAEMON_PIDS'])
1165
1166
1166 for s, r in self._getreplacements():
1167 for s, r in self._getreplacements():
1167 output = re.sub(s, r, output)
1168 output = re.sub(s, r, output)
1168
1169
1169 if normalizenewlines:
1170 if normalizenewlines:
1170 output = output.replace('\r\n', '\n')
1171 output = output.replace('\r\n', '\n')
1171
1172
1172 return ret, output.splitlines(True)
1173 return ret, output.splitlines(True)
1173
1174
1174 class PythonTest(Test):
1175 class PythonTest(Test):
1175 """A Python-based test."""
1176 """A Python-based test."""
1176
1177
1177 @property
1178 @property
1178 def refpath(self):
1179 def refpath(self):
1179 return os.path.join(self._testdir, b'%s.out' % self.bname)
1180 return os.path.join(self._testdir, b'%s.out' % self.bname)
1180
1181
1181 def _run(self, env):
1182 def _run(self, env):
1182 py3kswitch = self._py3kwarnings and b' -3' or b''
1183 py3kswitch = self._py3kwarnings and b' -3' or b''
1183 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
1184 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
1184 vlog("# Running", cmd)
1185 vlog("# Running", cmd)
1185 normalizenewlines = os.name == 'nt'
1186 normalizenewlines = os.name == 'nt'
1186 result = self._runcommand(cmd, env,
1187 result = self._runcommand(cmd, env,
1187 normalizenewlines=normalizenewlines)
1188 normalizenewlines=normalizenewlines)
1188 if self._aborted:
1189 if self._aborted:
1189 raise KeyboardInterrupt()
1190 raise KeyboardInterrupt()
1190
1191
1191 return result
1192 return result
1192
1193
1193 # Some glob patterns apply only in some circumstances, so the script
1194 # Some glob patterns apply only in some circumstances, so the script
1194 # might want to remove (glob) annotations that otherwise should be
1195 # might want to remove (glob) annotations that otherwise should be
1195 # retained.
1196 # retained.
1196 checkcodeglobpats = [
1197 checkcodeglobpats = [
1197 # On Windows it looks like \ doesn't require a (glob), but we know
1198 # On Windows it looks like \ doesn't require a (glob), but we know
1198 # better.
1199 # better.
1199 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1200 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1200 re.compile(br'^moving \S+/.*[^)]$'),
1201 re.compile(br'^moving \S+/.*[^)]$'),
1201 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1202 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1202 # Not all platforms have 127.0.0.1 as loopback (though most do),
1203 # Not all platforms have 127.0.0.1 as loopback (though most do),
1203 # so we always glob that too.
1204 # so we always glob that too.
1204 re.compile(br'.*\$LOCALIP.*$'),
1205 re.compile(br'.*\$LOCALIP.*$'),
1205 ]
1206 ]
1206
1207
1207 bchr = chr
1208 bchr = chr
1208 if PYTHON3:
1209 if PYTHON3:
1209 bchr = lambda x: bytes([x])
1210 bchr = lambda x: bytes([x])
1210
1211
1211 class TTest(Test):
1212 class TTest(Test):
1212 """A "t test" is a test backed by a .t file."""
1213 """A "t test" is a test backed by a .t file."""
1213
1214
1214 SKIPPED_PREFIX = b'skipped: '
1215 SKIPPED_PREFIX = b'skipped: '
1215 FAILED_PREFIX = b'hghave check failed: '
1216 FAILED_PREFIX = b'hghave check failed: '
1216 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1217 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1217
1218
1218 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1219 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1219 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1220 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1220 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1221 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1221
1222
1222 def __init__(self, path, *args, **kwds):
1223 def __init__(self, path, *args, **kwds):
1223 # accept an extra "case" parameter
1224 # accept an extra "case" parameter
1224 case = kwds.pop('case', None)
1225 case = kwds.pop('case', None)
1225 self._case = case
1226 self._case = case
1226 self._allcases = parsettestcases(path)
1227 self._allcases = parsettestcases(path)
1227 super(TTest, self).__init__(path, *args, **kwds)
1228 super(TTest, self).__init__(path, *args, **kwds)
1228 if case:
1229 if case:
1229 self.name = '%s (case %s)' % (self.name, _strpath(case))
1230 self.name = '%s (case %s)' % (self.name, _strpath(case))
1230 self.errpath = b'%s.%s.err' % (self.errpath[:-4], case)
1231 self.errpath = b'%s.%s.err' % (self.errpath[:-4], case)
1231 self._tmpname += b'-%s' % case
1232 self._tmpname += b'-%s' % case
1232
1233
1233 @property
1234 @property
1234 def refpath(self):
1235 def refpath(self):
1235 return os.path.join(self._testdir, self.bname)
1236 return os.path.join(self._testdir, self.bname)
1236
1237
1237 def _run(self, env):
1238 def _run(self, env):
1238 with open(self.path, 'rb') as f:
1239 with open(self.path, 'rb') as f:
1239 lines = f.readlines()
1240 lines = f.readlines()
1240
1241
1241 # .t file is both reference output and the test input, keep reference
1242 # .t file is both reference output and the test input, keep reference
1242 # output updated with the the test input. This avoids some race
1243 # output updated with the the test input. This avoids some race
1243 # conditions where the reference output does not match the actual test.
1244 # conditions where the reference output does not match the actual test.
1244 if self._refout is not None:
1245 if self._refout is not None:
1245 self._refout = lines
1246 self._refout = lines
1246
1247
1247 salt, script, after, expected = self._parsetest(lines)
1248 salt, script, after, expected = self._parsetest(lines)
1248
1249
1249 # Write out the generated script.
1250 # Write out the generated script.
1250 fname = b'%s.sh' % self._testtmp
1251 fname = b'%s.sh' % self._testtmp
1251 with open(fname, 'wb') as f:
1252 with open(fname, 'wb') as f:
1252 for l in script:
1253 for l in script:
1253 f.write(l)
1254 f.write(l)
1254
1255
1255 cmd = b'%s "%s"' % (self._shell, fname)
1256 cmd = b'%s "%s"' % (self._shell, fname)
1256 vlog("# Running", cmd)
1257 vlog("# Running", cmd)
1257
1258
1258 exitcode, output = self._runcommand(cmd, env)
1259 exitcode, output = self._runcommand(cmd, env)
1259
1260
1260 if self._aborted:
1261 if self._aborted:
1261 raise KeyboardInterrupt()
1262 raise KeyboardInterrupt()
1262
1263
1263 # Do not merge output if skipped. Return hghave message instead.
1264 # Do not merge output if skipped. Return hghave message instead.
1264 # Similarly, with --debug, output is None.
1265 # Similarly, with --debug, output is None.
1265 if exitcode == self.SKIPPED_STATUS or output is None:
1266 if exitcode == self.SKIPPED_STATUS or output is None:
1266 return exitcode, output
1267 return exitcode, output
1267
1268
1268 return self._processoutput(exitcode, output, salt, after, expected)
1269 return self._processoutput(exitcode, output, salt, after, expected)
1269
1270
1270 def _hghave(self, reqs):
1271 def _hghave(self, reqs):
1271 # TODO do something smarter when all other uses of hghave are gone.
1272 # TODO do something smarter when all other uses of hghave are gone.
1272 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1273 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1273 tdir = runtestdir.replace(b'\\', b'/')
1274 tdir = runtestdir.replace(b'\\', b'/')
1274 proc = Popen4(b'%s -c "%s/hghave %s"' %
1275 proc = Popen4(b'%s -c "%s/hghave %s"' %
1275 (self._shell, tdir, b' '.join(reqs)),
1276 (self._shell, tdir, b' '.join(reqs)),
1276 self._testtmp, 0, self._getenv())
1277 self._testtmp, 0, self._getenv())
1277 stdout, stderr = proc.communicate()
1278 stdout, stderr = proc.communicate()
1278 ret = proc.wait()
1279 ret = proc.wait()
1279 if wifexited(ret):
1280 if wifexited(ret):
1280 ret = os.WEXITSTATUS(ret)
1281 ret = os.WEXITSTATUS(ret)
1281 if ret == 2:
1282 if ret == 2:
1282 print(stdout.decode('utf-8'))
1283 print(stdout.decode('utf-8'))
1283 sys.exit(1)
1284 sys.exit(1)
1284
1285
1285 if ret != 0:
1286 if ret != 0:
1286 return False, stdout
1287 return False, stdout
1287
1288
1288 if b'slow' in reqs:
1289 if b'slow' in reqs:
1289 self._timeout = self._slowtimeout
1290 self._timeout = self._slowtimeout
1290 return True, None
1291 return True, None
1291
1292
1292 def _iftest(self, args):
1293 def _iftest(self, args):
1293 # implements "#if"
1294 # implements "#if"
1294 reqs = []
1295 reqs = []
1295 for arg in args:
1296 for arg in args:
1296 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1297 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1297 if arg[3:] == self._case:
1298 if arg[3:] == self._case:
1298 return False
1299 return False
1299 elif arg in self._allcases:
1300 elif arg in self._allcases:
1300 if arg != self._case:
1301 if arg != self._case:
1301 return False
1302 return False
1302 else:
1303 else:
1303 reqs.append(arg)
1304 reqs.append(arg)
1304 return self._hghave(reqs)[0]
1305 return self._hghave(reqs)[0]
1305
1306
1306 def _parsetest(self, lines):
1307 def _parsetest(self, lines):
1307 # We generate a shell script which outputs unique markers to line
1308 # We generate a shell script which outputs unique markers to line
1308 # up script results with our source. These markers include input
1309 # up script results with our source. These markers include input
1309 # line number and the last return code.
1310 # line number and the last return code.
1310 salt = b"SALT%d" % time.time()
1311 salt = b"SALT%d" % time.time()
1311 def addsalt(line, inpython):
1312 def addsalt(line, inpython):
1312 if inpython:
1313 if inpython:
1313 script.append(b'%s %d 0\n' % (salt, line))
1314 script.append(b'%s %d 0\n' % (salt, line))
1314 else:
1315 else:
1315 script.append(b'echo %s %d $?\n' % (salt, line))
1316 script.append(b'echo %s %d $?\n' % (salt, line))
1316
1317
1317 script = []
1318 script = []
1318
1319
1319 # After we run the shell script, we re-unify the script output
1320 # After we run the shell script, we re-unify the script output
1320 # with non-active parts of the source, with synchronization by our
1321 # with non-active parts of the source, with synchronization by our
1321 # SALT line number markers. The after table contains the non-active
1322 # SALT line number markers. The after table contains the non-active
1322 # components, ordered by line number.
1323 # components, ordered by line number.
1323 after = {}
1324 after = {}
1324
1325
1325 # Expected shell script output.
1326 # Expected shell script output.
1326 expected = {}
1327 expected = {}
1327
1328
1328 pos = prepos = -1
1329 pos = prepos = -1
1329
1330
1330 # True or False when in a true or false conditional section
1331 # True or False when in a true or false conditional section
1331 skipping = None
1332 skipping = None
1332
1333
1333 # We keep track of whether or not we're in a Python block so we
1334 # We keep track of whether or not we're in a Python block so we
1334 # can generate the surrounding doctest magic.
1335 # can generate the surrounding doctest magic.
1335 inpython = False
1336 inpython = False
1336
1337
1337 if self._debug:
1338 if self._debug:
1338 script.append(b'set -x\n')
1339 script.append(b'set -x\n')
1339 if self._hgcommand != b'hg':
1340 if self._hgcommand != b'hg':
1340 script.append(b'alias hg="%s"\n' % self._hgcommand)
1341 script.append(b'alias hg="%s"\n' % self._hgcommand)
1341 if os.getenv('MSYSTEM'):
1342 if os.getenv('MSYSTEM'):
1342 script.append(b'alias pwd="pwd -W"\n')
1343 script.append(b'alias pwd="pwd -W"\n')
1343 if self._case:
1344 if self._case:
1344 if isinstance(self._case, str):
1345 if isinstance(self._case, str):
1345 quoted = shellquote(self._case)
1346 quoted = shellquote(self._case)
1346 else:
1347 else:
1347 quoted = shellquote(self._case.decode('utf8')).encode('utf8')
1348 quoted = shellquote(self._case.decode('utf8')).encode('utf8')
1348 script.append(b'TESTCASE=%s\n' % quoted)
1349 script.append(b'TESTCASE=%s\n' % quoted)
1349 script.append(b'export TESTCASE\n')
1350 script.append(b'export TESTCASE\n')
1350
1351
1351 n = 0
1352 n = 0
1352 for n, l in enumerate(lines):
1353 for n, l in enumerate(lines):
1353 if not l.endswith(b'\n'):
1354 if not l.endswith(b'\n'):
1354 l += b'\n'
1355 l += b'\n'
1355 if l.startswith(b'#require'):
1356 if l.startswith(b'#require'):
1356 lsplit = l.split()
1357 lsplit = l.split()
1357 if len(lsplit) < 2 or lsplit[0] != b'#require':
1358 if len(lsplit) < 2 or lsplit[0] != b'#require':
1358 after.setdefault(pos, []).append(' !!! invalid #require\n')
1359 after.setdefault(pos, []).append(' !!! invalid #require\n')
1359 haveresult, message = self._hghave(lsplit[1:])
1360 haveresult, message = self._hghave(lsplit[1:])
1360 if not haveresult:
1361 if not haveresult:
1361 script = [b'echo "%s"\nexit 80\n' % message]
1362 script = [b'echo "%s"\nexit 80\n' % message]
1362 break
1363 break
1363 after.setdefault(pos, []).append(l)
1364 after.setdefault(pos, []).append(l)
1364 elif l.startswith(b'#if'):
1365 elif l.startswith(b'#if'):
1365 lsplit = l.split()
1366 lsplit = l.split()
1366 if len(lsplit) < 2 or lsplit[0] != b'#if':
1367 if len(lsplit) < 2 or lsplit[0] != b'#if':
1367 after.setdefault(pos, []).append(' !!! invalid #if\n')
1368 after.setdefault(pos, []).append(' !!! invalid #if\n')
1368 if skipping is not None:
1369 if skipping is not None:
1369 after.setdefault(pos, []).append(' !!! nested #if\n')
1370 after.setdefault(pos, []).append(' !!! nested #if\n')
1370 skipping = not self._iftest(lsplit[1:])
1371 skipping = not self._iftest(lsplit[1:])
1371 after.setdefault(pos, []).append(l)
1372 after.setdefault(pos, []).append(l)
1372 elif l.startswith(b'#else'):
1373 elif l.startswith(b'#else'):
1373 if skipping is None:
1374 if skipping is None:
1374 after.setdefault(pos, []).append(' !!! missing #if\n')
1375 after.setdefault(pos, []).append(' !!! missing #if\n')
1375 skipping = not skipping
1376 skipping = not skipping
1376 after.setdefault(pos, []).append(l)
1377 after.setdefault(pos, []).append(l)
1377 elif l.startswith(b'#endif'):
1378 elif l.startswith(b'#endif'):
1378 if skipping is None:
1379 if skipping is None:
1379 after.setdefault(pos, []).append(' !!! missing #if\n')
1380 after.setdefault(pos, []).append(' !!! missing #if\n')
1380 skipping = None
1381 skipping = None
1381 after.setdefault(pos, []).append(l)
1382 after.setdefault(pos, []).append(l)
1382 elif skipping:
1383 elif skipping:
1383 after.setdefault(pos, []).append(l)
1384 after.setdefault(pos, []).append(l)
1384 elif l.startswith(b' >>> '): # python inlines
1385 elif l.startswith(b' >>> '): # python inlines
1385 after.setdefault(pos, []).append(l)
1386 after.setdefault(pos, []).append(l)
1386 prepos = pos
1387 prepos = pos
1387 pos = n
1388 pos = n
1388 if not inpython:
1389 if not inpython:
1389 # We've just entered a Python block. Add the header.
1390 # We've just entered a Python block. Add the header.
1390 inpython = True
1391 inpython = True
1391 addsalt(prepos, False) # Make sure we report the exit code.
1392 addsalt(prepos, False) # Make sure we report the exit code.
1392 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1393 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1393 addsalt(n, True)
1394 addsalt(n, True)
1394 script.append(l[2:])
1395 script.append(l[2:])
1395 elif l.startswith(b' ... '): # python inlines
1396 elif l.startswith(b' ... '): # python inlines
1396 after.setdefault(prepos, []).append(l)
1397 after.setdefault(prepos, []).append(l)
1397 script.append(l[2:])
1398 script.append(l[2:])
1398 elif l.startswith(b' $ '): # commands
1399 elif l.startswith(b' $ '): # commands
1399 if inpython:
1400 if inpython:
1400 script.append(b'EOF\n')
1401 script.append(b'EOF\n')
1401 inpython = False
1402 inpython = False
1402 after.setdefault(pos, []).append(l)
1403 after.setdefault(pos, []).append(l)
1403 prepos = pos
1404 prepos = pos
1404 pos = n
1405 pos = n
1405 addsalt(n, False)
1406 addsalt(n, False)
1406 cmd = l[4:].split()
1407 cmd = l[4:].split()
1407 if len(cmd) == 2 and cmd[0] == b'cd':
1408 if len(cmd) == 2 and cmd[0] == b'cd':
1408 l = b' $ cd %s || exit 1\n' % cmd[1]
1409 l = b' $ cd %s || exit 1\n' % cmd[1]
1409 script.append(l[4:])
1410 script.append(l[4:])
1410 elif l.startswith(b' > '): # continuations
1411 elif l.startswith(b' > '): # continuations
1411 after.setdefault(prepos, []).append(l)
1412 after.setdefault(prepos, []).append(l)
1412 script.append(l[4:])
1413 script.append(l[4:])
1413 elif l.startswith(b' '): # results
1414 elif l.startswith(b' '): # results
1414 # Queue up a list of expected results.
1415 # Queue up a list of expected results.
1415 expected.setdefault(pos, []).append(l[2:])
1416 expected.setdefault(pos, []).append(l[2:])
1416 else:
1417 else:
1417 if inpython:
1418 if inpython:
1418 script.append(b'EOF\n')
1419 script.append(b'EOF\n')
1419 inpython = False
1420 inpython = False
1420 # Non-command/result. Queue up for merged output.
1421 # Non-command/result. Queue up for merged output.
1421 after.setdefault(pos, []).append(l)
1422 after.setdefault(pos, []).append(l)
1422
1423
1423 if inpython:
1424 if inpython:
1424 script.append(b'EOF\n')
1425 script.append(b'EOF\n')
1425 if skipping is not None:
1426 if skipping is not None:
1426 after.setdefault(pos, []).append(' !!! missing #endif\n')
1427 after.setdefault(pos, []).append(' !!! missing #endif\n')
1427 addsalt(n + 1, False)
1428 addsalt(n + 1, False)
1428
1429
1429 return salt, script, after, expected
1430 return salt, script, after, expected
1430
1431
1431 def _processoutput(self, exitcode, output, salt, after, expected):
1432 def _processoutput(self, exitcode, output, salt, after, expected):
1432 # Merge the script output back into a unified test.
1433 # Merge the script output back into a unified test.
1433 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1434 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1434 if exitcode != 0:
1435 if exitcode != 0:
1435 warnonly = 3
1436 warnonly = 3
1436
1437
1437 pos = -1
1438 pos = -1
1438 postout = []
1439 postout = []
1439 for l in output:
1440 for l in output:
1440 lout, lcmd = l, None
1441 lout, lcmd = l, None
1441 if salt in l:
1442 if salt in l:
1442 lout, lcmd = l.split(salt, 1)
1443 lout, lcmd = l.split(salt, 1)
1443
1444
1444 while lout:
1445 while lout:
1445 if not lout.endswith(b'\n'):
1446 if not lout.endswith(b'\n'):
1446 lout += b' (no-eol)\n'
1447 lout += b' (no-eol)\n'
1447
1448
1448 # Find the expected output at the current position.
1449 # Find the expected output at the current position.
1449 els = [None]
1450 els = [None]
1450 if expected.get(pos, None):
1451 if expected.get(pos, None):
1451 els = expected[pos]
1452 els = expected[pos]
1452
1453
1453 i = 0
1454 i = 0
1454 optional = []
1455 optional = []
1455 while i < len(els):
1456 while i < len(els):
1456 el = els[i]
1457 el = els[i]
1457
1458
1458 r = self.linematch(el, lout)
1459 r = self.linematch(el, lout)
1459 if isinstance(r, str):
1460 if isinstance(r, str):
1460 if r == '-glob':
1461 if r == '-glob':
1461 lout = ''.join(el.rsplit(' (glob)', 1))
1462 lout = ''.join(el.rsplit(' (glob)', 1))
1462 r = '' # Warn only this line.
1463 r = '' # Warn only this line.
1463 elif r == "retry":
1464 elif r == "retry":
1464 postout.append(b' ' + el)
1465 postout.append(b' ' + el)
1465 els.pop(i)
1466 els.pop(i)
1466 break
1467 break
1467 else:
1468 else:
1468 log('\ninfo, unknown linematch result: %r\n' % r)
1469 log('\ninfo, unknown linematch result: %r\n' % r)
1469 r = False
1470 r = False
1470 if r:
1471 if r:
1471 els.pop(i)
1472 els.pop(i)
1472 break
1473 break
1473 if el:
1474 if el:
1474 if el.endswith(b" (?)\n"):
1475 if el.endswith(b" (?)\n"):
1475 optional.append(i)
1476 optional.append(i)
1476 else:
1477 else:
1477 m = optline.match(el)
1478 m = optline.match(el)
1478 if m:
1479 if m:
1479 conditions = [
1480 conditions = [
1480 c for c in m.group(2).split(b' ')]
1481 c for c in m.group(2).split(b' ')]
1481
1482
1482 if not self._iftest(conditions):
1483 if not self._iftest(conditions):
1483 optional.append(i)
1484 optional.append(i)
1484
1485
1485 i += 1
1486 i += 1
1486
1487
1487 if r:
1488 if r:
1488 if r == "retry":
1489 if r == "retry":
1489 continue
1490 continue
1490 # clean up any optional leftovers
1491 # clean up any optional leftovers
1491 for i in optional:
1492 for i in optional:
1492 postout.append(b' ' + els[i])
1493 postout.append(b' ' + els[i])
1493 for i in reversed(optional):
1494 for i in reversed(optional):
1494 del els[i]
1495 del els[i]
1495 postout.append(b' ' + el)
1496 postout.append(b' ' + el)
1496 else:
1497 else:
1497 if self.NEEDESCAPE(lout):
1498 if self.NEEDESCAPE(lout):
1498 lout = TTest._stringescape(b'%s (esc)\n' %
1499 lout = TTest._stringescape(b'%s (esc)\n' %
1499 lout.rstrip(b'\n'))
1500 lout.rstrip(b'\n'))
1500 postout.append(b' ' + lout) # Let diff deal with it.
1501 postout.append(b' ' + lout) # Let diff deal with it.
1501 if r != '': # If line failed.
1502 if r != '': # If line failed.
1502 warnonly = 3 # for sure not
1503 warnonly = 3 # for sure not
1503 elif warnonly == 1: # Is "not yet" and line is warn only.
1504 elif warnonly == 1: # Is "not yet" and line is warn only.
1504 warnonly = 2 # Yes do warn.
1505 warnonly = 2 # Yes do warn.
1505 break
1506 break
1506 else:
1507 else:
1507 # clean up any optional leftovers
1508 # clean up any optional leftovers
1508 while expected.get(pos, None):
1509 while expected.get(pos, None):
1509 el = expected[pos].pop(0)
1510 el = expected[pos].pop(0)
1510 if el:
1511 if el:
1511 if not el.endswith(b" (?)\n"):
1512 if not el.endswith(b" (?)\n"):
1512 m = optline.match(el)
1513 m = optline.match(el)
1513 if m:
1514 if m:
1514 conditions = [c for c in m.group(2).split(b' ')]
1515 conditions = [c for c in m.group(2).split(b' ')]
1515
1516
1516 if self._iftest(conditions):
1517 if self._iftest(conditions):
1517 # Don't append as optional line
1518 # Don't append as optional line
1518 continue
1519 continue
1519 else:
1520 else:
1520 continue
1521 continue
1521 postout.append(b' ' + el)
1522 postout.append(b' ' + el)
1522
1523
1523 if lcmd:
1524 if lcmd:
1524 # Add on last return code.
1525 # Add on last return code.
1525 ret = int(lcmd.split()[1])
1526 ret = int(lcmd.split()[1])
1526 if ret != 0:
1527 if ret != 0:
1527 postout.append(b' [%d]\n' % ret)
1528 postout.append(b' [%d]\n' % ret)
1528 if pos in after:
1529 if pos in after:
1529 # Merge in non-active test bits.
1530 # Merge in non-active test bits.
1530 postout += after.pop(pos)
1531 postout += after.pop(pos)
1531 pos = int(lcmd.split()[0])
1532 pos = int(lcmd.split()[0])
1532
1533
1533 if pos in after:
1534 if pos in after:
1534 postout += after.pop(pos)
1535 postout += after.pop(pos)
1535
1536
1536 if warnonly == 2:
1537 if warnonly == 2:
1537 exitcode = False # Set exitcode to warned.
1538 exitcode = False # Set exitcode to warned.
1538
1539
1539 return exitcode, postout
1540 return exitcode, postout
1540
1541
1541 @staticmethod
1542 @staticmethod
1542 def rematch(el, l):
1543 def rematch(el, l):
1543 try:
1544 try:
1544 el = b'(?:' + el + b')'
1545 el = b'(?:' + el + b')'
1545 # use \Z to ensure that the regex matches to the end of the string
1546 # use \Z to ensure that the regex matches to the end of the string
1546 if os.name == 'nt':
1547 if os.name == 'nt':
1547 return re.match(el + br'\r?\n\Z', l)
1548 return re.match(el + br'\r?\n\Z', l)
1548 return re.match(el + br'\n\Z', l)
1549 return re.match(el + br'\n\Z', l)
1549 except re.error:
1550 except re.error:
1550 # el is an invalid regex
1551 # el is an invalid regex
1551 return False
1552 return False
1552
1553
1553 @staticmethod
1554 @staticmethod
1554 def globmatch(el, l):
1555 def globmatch(el, l):
1555 # The only supported special characters are * and ? plus / which also
1556 # The only supported special characters are * and ? plus / which also
1556 # matches \ on windows. Escaping of these characters is supported.
1557 # matches \ on windows. Escaping of these characters is supported.
1557 if el + b'\n' == l:
1558 if el + b'\n' == l:
1558 if os.altsep:
1559 if os.altsep:
1559 # matching on "/" is not needed for this line
1560 # matching on "/" is not needed for this line
1560 for pat in checkcodeglobpats:
1561 for pat in checkcodeglobpats:
1561 if pat.match(el):
1562 if pat.match(el):
1562 return True
1563 return True
1563 return b'-glob'
1564 return b'-glob'
1564 return True
1565 return True
1565 el = el.replace(b'$LOCALIP', b'*')
1566 el = el.replace(b'$LOCALIP', b'*')
1566 i, n = 0, len(el)
1567 i, n = 0, len(el)
1567 res = b''
1568 res = b''
1568 while i < n:
1569 while i < n:
1569 c = el[i:i + 1]
1570 c = el[i:i + 1]
1570 i += 1
1571 i += 1
1571 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1572 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1572 res += el[i - 1:i + 1]
1573 res += el[i - 1:i + 1]
1573 i += 1
1574 i += 1
1574 elif c == b'*':
1575 elif c == b'*':
1575 res += b'.*'
1576 res += b'.*'
1576 elif c == b'?':
1577 elif c == b'?':
1577 res += b'.'
1578 res += b'.'
1578 elif c == b'/' and os.altsep:
1579 elif c == b'/' and os.altsep:
1579 res += b'[/\\\\]'
1580 res += b'[/\\\\]'
1580 else:
1581 else:
1581 res += re.escape(c)
1582 res += re.escape(c)
1582 return TTest.rematch(res, l)
1583 return TTest.rematch(res, l)
1583
1584
1584 def linematch(self, el, l):
1585 def linematch(self, el, l):
1585 retry = False
1586 retry = False
1586 if el == l: # perfect match (fast)
1587 if el == l: # perfect match (fast)
1587 return True
1588 return True
1588 if el:
1589 if el:
1589 if el.endswith(b" (?)\n"):
1590 if el.endswith(b" (?)\n"):
1590 retry = "retry"
1591 retry = "retry"
1591 el = el[:-5] + b"\n"
1592 el = el[:-5] + b"\n"
1592 else:
1593 else:
1593 m = optline.match(el)
1594 m = optline.match(el)
1594 if m:
1595 if m:
1595 conditions = [c for c in m.group(2).split(b' ')]
1596 conditions = [c for c in m.group(2).split(b' ')]
1596
1597
1597 el = m.group(1) + b"\n"
1598 el = m.group(1) + b"\n"
1598 if not self._iftest(conditions):
1599 if not self._iftest(conditions):
1599 retry = "retry" # Not required by listed features
1600 retry = "retry" # Not required by listed features
1600
1601
1601 if el.endswith(b" (esc)\n"):
1602 if el.endswith(b" (esc)\n"):
1602 if PYTHON3:
1603 if PYTHON3:
1603 el = el[:-7].decode('unicode_escape') + '\n'
1604 el = el[:-7].decode('unicode_escape') + '\n'
1604 el = el.encode('utf-8')
1605 el = el.encode('utf-8')
1605 else:
1606 else:
1606 el = el[:-7].decode('string-escape') + '\n'
1607 el = el[:-7].decode('string-escape') + '\n'
1607 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1608 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1608 return True
1609 return True
1609 if el.endswith(b" (re)\n"):
1610 if el.endswith(b" (re)\n"):
1610 return TTest.rematch(el[:-6], l) or retry
1611 return TTest.rematch(el[:-6], l) or retry
1611 if el.endswith(b" (glob)\n"):
1612 if el.endswith(b" (glob)\n"):
1612 # ignore '(glob)' added to l by 'replacements'
1613 # ignore '(glob)' added to l by 'replacements'
1613 if l.endswith(b" (glob)\n"):
1614 if l.endswith(b" (glob)\n"):
1614 l = l[:-8] + b"\n"
1615 l = l[:-8] + b"\n"
1615 return TTest.globmatch(el[:-8], l) or retry
1616 return TTest.globmatch(el[:-8], l) or retry
1616 if os.altsep:
1617 if os.altsep:
1617 _l = l.replace(b'\\', b'/')
1618 _l = l.replace(b'\\', b'/')
1618 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1619 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1619 return True
1620 return True
1620 return retry
1621 return retry
1621
1622
1622 @staticmethod
1623 @staticmethod
1623 def parsehghaveoutput(lines):
1624 def parsehghaveoutput(lines):
1624 '''Parse hghave log lines.
1625 '''Parse hghave log lines.
1625
1626
1626 Return tuple of lists (missing, failed):
1627 Return tuple of lists (missing, failed):
1627 * the missing/unknown features
1628 * the missing/unknown features
1628 * the features for which existence check failed'''
1629 * the features for which existence check failed'''
1629 missing = []
1630 missing = []
1630 failed = []
1631 failed = []
1631 for line in lines:
1632 for line in lines:
1632 if line.startswith(TTest.SKIPPED_PREFIX):
1633 if line.startswith(TTest.SKIPPED_PREFIX):
1633 line = line.splitlines()[0]
1634 line = line.splitlines()[0]
1634 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1635 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1635 elif line.startswith(TTest.FAILED_PREFIX):
1636 elif line.startswith(TTest.FAILED_PREFIX):
1636 line = line.splitlines()[0]
1637 line = line.splitlines()[0]
1637 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1638 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1638
1639
1639 return missing, failed
1640 return missing, failed
1640
1641
1641 @staticmethod
1642 @staticmethod
1642 def _escapef(m):
1643 def _escapef(m):
1643 return TTest.ESCAPEMAP[m.group(0)]
1644 return TTest.ESCAPEMAP[m.group(0)]
1644
1645
1645 @staticmethod
1646 @staticmethod
1646 def _stringescape(s):
1647 def _stringescape(s):
1647 return TTest.ESCAPESUB(TTest._escapef, s)
1648 return TTest.ESCAPESUB(TTest._escapef, s)
1648
1649
1649 iolock = threading.RLock()
1650 iolock = threading.RLock()
1650 firstlock = threading.RLock()
1651 firstlock = threading.RLock()
1651 firsterror = False
1652 firsterror = False
1652
1653
1653 class TestResult(unittest._TextTestResult):
1654 class TestResult(unittest._TextTestResult):
1654 """Holds results when executing via unittest."""
1655 """Holds results when executing via unittest."""
1655 # Don't worry too much about accessing the non-public _TextTestResult.
1656 # Don't worry too much about accessing the non-public _TextTestResult.
1656 # It is relatively common in Python testing tools.
1657 # It is relatively common in Python testing tools.
1657 def __init__(self, options, *args, **kwargs):
1658 def __init__(self, options, *args, **kwargs):
1658 super(TestResult, self).__init__(*args, **kwargs)
1659 super(TestResult, self).__init__(*args, **kwargs)
1659
1660
1660 self._options = options
1661 self._options = options
1661
1662
1662 # unittest.TestResult didn't have skipped until 2.7. We need to
1663 # unittest.TestResult didn't have skipped until 2.7. We need to
1663 # polyfill it.
1664 # polyfill it.
1664 self.skipped = []
1665 self.skipped = []
1665
1666
1666 # We have a custom "ignored" result that isn't present in any Python
1667 # We have a custom "ignored" result that isn't present in any Python
1667 # unittest implementation. It is very similar to skipped. It may make
1668 # unittest implementation. It is very similar to skipped. It may make
1668 # sense to map it into skip some day.
1669 # sense to map it into skip some day.
1669 self.ignored = []
1670 self.ignored = []
1670
1671
1671 self.times = []
1672 self.times = []
1672 self._firststarttime = None
1673 self._firststarttime = None
1673 # Data stored for the benefit of generating xunit reports.
1674 # Data stored for the benefit of generating xunit reports.
1674 self.successes = []
1675 self.successes = []
1675 self.faildata = {}
1676 self.faildata = {}
1676
1677
1677 if options.color == 'auto':
1678 if options.color == 'auto':
1678 self.color = pygmentspresent and self.stream.isatty()
1679 self.color = pygmentspresent and self.stream.isatty()
1679 elif options.color == 'never':
1680 elif options.color == 'never':
1680 self.color = False
1681 self.color = False
1681 else: # 'always', for testing purposes
1682 else: # 'always', for testing purposes
1682 self.color = pygmentspresent
1683 self.color = pygmentspresent
1683
1684
1684 def addFailure(self, test, reason):
1685 def addFailure(self, test, reason):
1685 self.failures.append((test, reason))
1686 self.failures.append((test, reason))
1686
1687
1687 if self._options.first:
1688 if self._options.first:
1688 self.stop()
1689 self.stop()
1689 else:
1690 else:
1690 with iolock:
1691 with iolock:
1691 if reason == "timed out":
1692 if reason == "timed out":
1692 self.stream.write('t')
1693 self.stream.write('t')
1693 else:
1694 else:
1694 if not self._options.nodiff:
1695 if not self._options.nodiff:
1695 self.stream.write('\n')
1696 self.stream.write('\n')
1696 # Exclude the '\n' from highlighting to lex correctly
1697 # Exclude the '\n' from highlighting to lex correctly
1697 formatted = 'ERROR: %s output changed\n' % test
1698 formatted = 'ERROR: %s output changed\n' % test
1698 self.stream.write(highlightmsg(formatted, self.color))
1699 self.stream.write(highlightmsg(formatted, self.color))
1699 self.stream.write('!')
1700 self.stream.write('!')
1700
1701
1701 self.stream.flush()
1702 self.stream.flush()
1702
1703
1703 def addSuccess(self, test):
1704 def addSuccess(self, test):
1704 with iolock:
1705 with iolock:
1705 super(TestResult, self).addSuccess(test)
1706 super(TestResult, self).addSuccess(test)
1706 self.successes.append(test)
1707 self.successes.append(test)
1707
1708
1708 def addError(self, test, err):
1709 def addError(self, test, err):
1709 super(TestResult, self).addError(test, err)
1710 super(TestResult, self).addError(test, err)
1710 if self._options.first:
1711 if self._options.first:
1711 self.stop()
1712 self.stop()
1712
1713
1713 # Polyfill.
1714 # Polyfill.
1714 def addSkip(self, test, reason):
1715 def addSkip(self, test, reason):
1715 self.skipped.append((test, reason))
1716 self.skipped.append((test, reason))
1716 with iolock:
1717 with iolock:
1717 if self.showAll:
1718 if self.showAll:
1718 self.stream.writeln('skipped %s' % reason)
1719 self.stream.writeln('skipped %s' % reason)
1719 else:
1720 else:
1720 self.stream.write('s')
1721 self.stream.write('s')
1721 self.stream.flush()
1722 self.stream.flush()
1722
1723
1723 def addIgnore(self, test, reason):
1724 def addIgnore(self, test, reason):
1724 self.ignored.append((test, reason))
1725 self.ignored.append((test, reason))
1725 with iolock:
1726 with iolock:
1726 if self.showAll:
1727 if self.showAll:
1727 self.stream.writeln('ignored %s' % reason)
1728 self.stream.writeln('ignored %s' % reason)
1728 else:
1729 else:
1729 if reason not in ('not retesting', "doesn't match keyword"):
1730 if reason not in ('not retesting', "doesn't match keyword"):
1730 self.stream.write('i')
1731 self.stream.write('i')
1731 else:
1732 else:
1732 self.testsRun += 1
1733 self.testsRun += 1
1733 self.stream.flush()
1734 self.stream.flush()
1734
1735
1735 def addOutputMismatch(self, test, ret, got, expected):
1736 def addOutputMismatch(self, test, ret, got, expected):
1736 """Record a mismatch in test output for a particular test."""
1737 """Record a mismatch in test output for a particular test."""
1737 if self.shouldStop or firsterror:
1738 if self.shouldStop or firsterror:
1738 # don't print, some other test case already failed and
1739 # don't print, some other test case already failed and
1739 # printed, we're just stale and probably failed due to our
1740 # printed, we're just stale and probably failed due to our
1740 # temp dir getting cleaned up.
1741 # temp dir getting cleaned up.
1741 return
1742 return
1742
1743
1743 accepted = False
1744 accepted = False
1744 lines = []
1745 lines = []
1745
1746
1746 with iolock:
1747 with iolock:
1747 if self._options.nodiff:
1748 if self._options.nodiff:
1748 pass
1749 pass
1749 elif self._options.view:
1750 elif self._options.view:
1750 v = self._options.view
1751 v = self._options.view
1751 if PYTHON3:
1752 if PYTHON3:
1752 v = _bytespath(v)
1753 v = _bytespath(v)
1753 os.system(b"%s %s %s" %
1754 os.system(b"%s %s %s" %
1754 (v, test.refpath, test.errpath))
1755 (v, test.refpath, test.errpath))
1755 else:
1756 else:
1756 servefail, lines = getdiff(expected, got,
1757 servefail, lines = getdiff(expected, got,
1757 test.refpath, test.errpath)
1758 test.refpath, test.errpath)
1758 if servefail:
1759 if servefail:
1759 raise test.failureException(
1760 raise test.failureException(
1760 'server failed to start (HGPORT=%s)' % test._startport)
1761 'server failed to start (HGPORT=%s)' % test._startport)
1761 else:
1762 else:
1762 self.stream.write('\n')
1763 self.stream.write('\n')
1763 for line in lines:
1764 for line in lines:
1764 line = highlightdiff(line, self.color)
1765 line = highlightdiff(line, self.color)
1765 if PYTHON3:
1766 if PYTHON3:
1766 self.stream.flush()
1767 self.stream.flush()
1767 self.stream.buffer.write(line)
1768 self.stream.buffer.write(line)
1768 self.stream.buffer.flush()
1769 self.stream.buffer.flush()
1769 else:
1770 else:
1770 self.stream.write(line)
1771 self.stream.write(line)
1771 self.stream.flush()
1772 self.stream.flush()
1772
1773
1773 # handle interactive prompt without releasing iolock
1774 # handle interactive prompt without releasing iolock
1774 if self._options.interactive:
1775 if self._options.interactive:
1775 if test.readrefout() != expected:
1776 if test.readrefout() != expected:
1776 self.stream.write(
1777 self.stream.write(
1777 'Reference output has changed (run again to prompt '
1778 'Reference output has changed (run again to prompt '
1778 'changes)')
1779 'changes)')
1779 else:
1780 else:
1780 self.stream.write('Accept this change? [n] ')
1781 self.stream.write('Accept this change? [n] ')
1781 answer = sys.stdin.readline().strip()
1782 answer = sys.stdin.readline().strip()
1782 if answer.lower() in ('y', 'yes'):
1783 if answer.lower() in ('y', 'yes'):
1783 if test.path.endswith(b'.t'):
1784 if test.path.endswith(b'.t'):
1784 rename(test.errpath, test.path)
1785 rename(test.errpath, test.path)
1785 else:
1786 else:
1786 rename(test.errpath, '%s.out' % test.path)
1787 rename(test.errpath, '%s.out' % test.path)
1787 accepted = True
1788 accepted = True
1788 if not accepted:
1789 if not accepted:
1789 self.faildata[test.name] = b''.join(lines)
1790 self.faildata[test.name] = b''.join(lines)
1790
1791
1791 return accepted
1792 return accepted
1792
1793
1793 def startTest(self, test):
1794 def startTest(self, test):
1794 super(TestResult, self).startTest(test)
1795 super(TestResult, self).startTest(test)
1795
1796
1796 # os.times module computes the user time and system time spent by
1797 # os.times module computes the user time and system time spent by
1797 # child's processes along with real elapsed time taken by a process.
1798 # child's processes along with real elapsed time taken by a process.
1798 # This module has one limitation. It can only work for Linux user
1799 # This module has one limitation. It can only work for Linux user
1799 # and not for Windows.
1800 # and not for Windows.
1800 test.started = os.times()
1801 test.started = os.times()
1801 if self._firststarttime is None: # thread racy but irrelevant
1802 if self._firststarttime is None: # thread racy but irrelevant
1802 self._firststarttime = test.started[4]
1803 self._firststarttime = test.started[4]
1803
1804
1804 def stopTest(self, test, interrupted=False):
1805 def stopTest(self, test, interrupted=False):
1805 super(TestResult, self).stopTest(test)
1806 super(TestResult, self).stopTest(test)
1806
1807
1807 test.stopped = os.times()
1808 test.stopped = os.times()
1808
1809
1809 starttime = test.started
1810 starttime = test.started
1810 endtime = test.stopped
1811 endtime = test.stopped
1811 origin = self._firststarttime
1812 origin = self._firststarttime
1812 self.times.append((test.name,
1813 self.times.append((test.name,
1813 endtime[2] - starttime[2], # user space CPU time
1814 endtime[2] - starttime[2], # user space CPU time
1814 endtime[3] - starttime[3], # sys space CPU time
1815 endtime[3] - starttime[3], # sys space CPU time
1815 endtime[4] - starttime[4], # real time
1816 endtime[4] - starttime[4], # real time
1816 starttime[4] - origin, # start date in run context
1817 starttime[4] - origin, # start date in run context
1817 endtime[4] - origin, # end date in run context
1818 endtime[4] - origin, # end date in run context
1818 ))
1819 ))
1819
1820
1820 if interrupted:
1821 if interrupted:
1821 with iolock:
1822 with iolock:
1822 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1823 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1823 test.name, self.times[-1][3]))
1824 test.name, self.times[-1][3]))
1824
1825
1825 class TestSuite(unittest.TestSuite):
1826 class TestSuite(unittest.TestSuite):
1826 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1827 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1827
1828
1828 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1829 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1829 retest=False, keywords=None, loop=False, runs_per_test=1,
1830 retest=False, keywords=None, loop=False, runs_per_test=1,
1830 loadtest=None, showchannels=False,
1831 loadtest=None, showchannels=False,
1831 *args, **kwargs):
1832 *args, **kwargs):
1832 """Create a new instance that can run tests with a configuration.
1833 """Create a new instance that can run tests with a configuration.
1833
1834
1834 testdir specifies the directory where tests are executed from. This
1835 testdir specifies the directory where tests are executed from. This
1835 is typically the ``tests`` directory from Mercurial's source
1836 is typically the ``tests`` directory from Mercurial's source
1836 repository.
1837 repository.
1837
1838
1838 jobs specifies the number of jobs to run concurrently. Each test
1839 jobs specifies the number of jobs to run concurrently. Each test
1839 executes on its own thread. Tests actually spawn new processes, so
1840 executes on its own thread. Tests actually spawn new processes, so
1840 state mutation should not be an issue.
1841 state mutation should not be an issue.
1841
1842
1842 If there is only one job, it will use the main thread.
1843 If there is only one job, it will use the main thread.
1843
1844
1844 whitelist and blacklist denote tests that have been whitelisted and
1845 whitelist and blacklist denote tests that have been whitelisted and
1845 blacklisted, respectively. These arguments don't belong in TestSuite.
1846 blacklisted, respectively. These arguments don't belong in TestSuite.
1846 Instead, whitelist and blacklist should be handled by the thing that
1847 Instead, whitelist and blacklist should be handled by the thing that
1847 populates the TestSuite with tests. They are present to preserve
1848 populates the TestSuite with tests. They are present to preserve
1848 backwards compatible behavior which reports skipped tests as part
1849 backwards compatible behavior which reports skipped tests as part
1849 of the results.
1850 of the results.
1850
1851
1851 retest denotes whether to retest failed tests. This arguably belongs
1852 retest denotes whether to retest failed tests. This arguably belongs
1852 outside of TestSuite.
1853 outside of TestSuite.
1853
1854
1854 keywords denotes key words that will be used to filter which tests
1855 keywords denotes key words that will be used to filter which tests
1855 to execute. This arguably belongs outside of TestSuite.
1856 to execute. This arguably belongs outside of TestSuite.
1856
1857
1857 loop denotes whether to loop over tests forever.
1858 loop denotes whether to loop over tests forever.
1858 """
1859 """
1859 super(TestSuite, self).__init__(*args, **kwargs)
1860 super(TestSuite, self).__init__(*args, **kwargs)
1860
1861
1861 self._jobs = jobs
1862 self._jobs = jobs
1862 self._whitelist = whitelist
1863 self._whitelist = whitelist
1863 self._blacklist = blacklist
1864 self._blacklist = blacklist
1864 self._retest = retest
1865 self._retest = retest
1865 self._keywords = keywords
1866 self._keywords = keywords
1866 self._loop = loop
1867 self._loop = loop
1867 self._runs_per_test = runs_per_test
1868 self._runs_per_test = runs_per_test
1868 self._loadtest = loadtest
1869 self._loadtest = loadtest
1869 self._showchannels = showchannels
1870 self._showchannels = showchannels
1870
1871
1871 def run(self, result):
1872 def run(self, result):
1872 # We have a number of filters that need to be applied. We do this
1873 # We have a number of filters that need to be applied. We do this
1873 # here instead of inside Test because it makes the running logic for
1874 # here instead of inside Test because it makes the running logic for
1874 # Test simpler.
1875 # Test simpler.
1875 tests = []
1876 tests = []
1876 num_tests = [0]
1877 num_tests = [0]
1877 for test in self._tests:
1878 for test in self._tests:
1878 def get():
1879 def get():
1879 num_tests[0] += 1
1880 num_tests[0] += 1
1880 if getattr(test, 'should_reload', False):
1881 if getattr(test, 'should_reload', False):
1881 return self._loadtest(test, num_tests[0])
1882 return self._loadtest(test, num_tests[0])
1882 return test
1883 return test
1883 if not os.path.exists(test.path):
1884 if not os.path.exists(test.path):
1884 result.addSkip(test, "Doesn't exist")
1885 result.addSkip(test, "Doesn't exist")
1885 continue
1886 continue
1886
1887
1887 if not (self._whitelist and test.bname in self._whitelist):
1888 if not (self._whitelist and test.bname in self._whitelist):
1888 if self._blacklist and test.bname in self._blacklist:
1889 if self._blacklist and test.bname in self._blacklist:
1889 result.addSkip(test, 'blacklisted')
1890 result.addSkip(test, 'blacklisted')
1890 continue
1891 continue
1891
1892
1892 if self._retest and not os.path.exists(test.errpath):
1893 if self._retest and not os.path.exists(test.errpath):
1893 result.addIgnore(test, 'not retesting')
1894 result.addIgnore(test, 'not retesting')
1894 continue
1895 continue
1895
1896
1896 if self._keywords:
1897 if self._keywords:
1897 with open(test.path, 'rb') as f:
1898 with open(test.path, 'rb') as f:
1898 t = f.read().lower() + test.bname.lower()
1899 t = f.read().lower() + test.bname.lower()
1899 ignored = False
1900 ignored = False
1900 for k in self._keywords.lower().split():
1901 for k in self._keywords.lower().split():
1901 if k not in t:
1902 if k not in t:
1902 result.addIgnore(test, "doesn't match keyword")
1903 result.addIgnore(test, "doesn't match keyword")
1903 ignored = True
1904 ignored = True
1904 break
1905 break
1905
1906
1906 if ignored:
1907 if ignored:
1907 continue
1908 continue
1908 for _ in xrange(self._runs_per_test):
1909 for _ in xrange(self._runs_per_test):
1909 tests.append(get())
1910 tests.append(get())
1910
1911
1911 runtests = list(tests)
1912 runtests = list(tests)
1912 done = queue.Queue()
1913 done = queue.Queue()
1913 running = 0
1914 running = 0
1914
1915
1915 channels = [""] * self._jobs
1916 channels = [""] * self._jobs
1916
1917
1917 def job(test, result):
1918 def job(test, result):
1918 for n, v in enumerate(channels):
1919 for n, v in enumerate(channels):
1919 if not v:
1920 if not v:
1920 channel = n
1921 channel = n
1921 break
1922 break
1922 else:
1923 else:
1923 raise ValueError('Could not find output channel')
1924 raise ValueError('Could not find output channel')
1924 channels[channel] = "=" + test.name[5:].split(".")[0]
1925 channels[channel] = "=" + test.name[5:].split(".")[0]
1925 try:
1926 try:
1926 test(result)
1927 test(result)
1927 done.put(None)
1928 done.put(None)
1928 except KeyboardInterrupt:
1929 except KeyboardInterrupt:
1929 pass
1930 pass
1930 except: # re-raises
1931 except: # re-raises
1931 done.put(('!', test, 'run-test raised an error, see traceback'))
1932 done.put(('!', test, 'run-test raised an error, see traceback'))
1932 raise
1933 raise
1933 finally:
1934 finally:
1934 try:
1935 try:
1935 channels[channel] = ''
1936 channels[channel] = ''
1936 except IndexError:
1937 except IndexError:
1937 pass
1938 pass
1938
1939
1939 def stat():
1940 def stat():
1940 count = 0
1941 count = 0
1941 while channels:
1942 while channels:
1942 d = '\n%03s ' % count
1943 d = '\n%03s ' % count
1943 for n, v in enumerate(channels):
1944 for n, v in enumerate(channels):
1944 if v:
1945 if v:
1945 d += v[0]
1946 d += v[0]
1946 channels[n] = v[1:] or '.'
1947 channels[n] = v[1:] or '.'
1947 else:
1948 else:
1948 d += ' '
1949 d += ' '
1949 d += ' '
1950 d += ' '
1950 with iolock:
1951 with iolock:
1951 sys.stdout.write(d + ' ')
1952 sys.stdout.write(d + ' ')
1952 sys.stdout.flush()
1953 sys.stdout.flush()
1953 for x in xrange(10):
1954 for x in xrange(10):
1954 if channels:
1955 if channels:
1955 time.sleep(.1)
1956 time.sleep(.1)
1956 count += 1
1957 count += 1
1957
1958
1958 stoppedearly = False
1959 stoppedearly = False
1959
1960
1960 if self._showchannels:
1961 if self._showchannels:
1961 statthread = threading.Thread(target=stat, name="stat")
1962 statthread = threading.Thread(target=stat, name="stat")
1962 statthread.start()
1963 statthread.start()
1963
1964
1964 try:
1965 try:
1965 while tests or running:
1966 while tests or running:
1966 if not done.empty() or running == self._jobs or not tests:
1967 if not done.empty() or running == self._jobs or not tests:
1967 try:
1968 try:
1968 done.get(True, 1)
1969 done.get(True, 1)
1969 running -= 1
1970 running -= 1
1970 if result and result.shouldStop:
1971 if result and result.shouldStop:
1971 stoppedearly = True
1972 stoppedearly = True
1972 break
1973 break
1973 except queue.Empty:
1974 except queue.Empty:
1974 continue
1975 continue
1975 if tests and not running == self._jobs:
1976 if tests and not running == self._jobs:
1976 test = tests.pop(0)
1977 test = tests.pop(0)
1977 if self._loop:
1978 if self._loop:
1978 if getattr(test, 'should_reload', False):
1979 if getattr(test, 'should_reload', False):
1979 num_tests[0] += 1
1980 num_tests[0] += 1
1980 tests.append(
1981 tests.append(
1981 self._loadtest(test, num_tests[0]))
1982 self._loadtest(test, num_tests[0]))
1982 else:
1983 else:
1983 tests.append(test)
1984 tests.append(test)
1984 if self._jobs == 1:
1985 if self._jobs == 1:
1985 job(test, result)
1986 job(test, result)
1986 else:
1987 else:
1987 t = threading.Thread(target=job, name=test.name,
1988 t = threading.Thread(target=job, name=test.name,
1988 args=(test, result))
1989 args=(test, result))
1989 t.start()
1990 t.start()
1990 running += 1
1991 running += 1
1991
1992
1992 # If we stop early we still need to wait on started tests to
1993 # If we stop early we still need to wait on started tests to
1993 # finish. Otherwise, there is a race between the test completing
1994 # finish. Otherwise, there is a race between the test completing
1994 # and the test's cleanup code running. This could result in the
1995 # and the test's cleanup code running. This could result in the
1995 # test reporting incorrect.
1996 # test reporting incorrect.
1996 if stoppedearly:
1997 if stoppedearly:
1997 while running:
1998 while running:
1998 try:
1999 try:
1999 done.get(True, 1)
2000 done.get(True, 1)
2000 running -= 1
2001 running -= 1
2001 except queue.Empty:
2002 except queue.Empty:
2002 continue
2003 continue
2003 except KeyboardInterrupt:
2004 except KeyboardInterrupt:
2004 for test in runtests:
2005 for test in runtests:
2005 test.abort()
2006 test.abort()
2006
2007
2007 channels = []
2008 channels = []
2008
2009
2009 return result
2010 return result
2010
2011
2011 # Save the most recent 5 wall-clock runtimes of each test to a
2012 # Save the most recent 5 wall-clock runtimes of each test to a
2012 # human-readable text file named .testtimes. Tests are sorted
2013 # human-readable text file named .testtimes. Tests are sorted
2013 # alphabetically, while times for each test are listed from oldest to
2014 # alphabetically, while times for each test are listed from oldest to
2014 # newest.
2015 # newest.
2015
2016
2016 def loadtimes(outputdir):
2017 def loadtimes(outputdir):
2017 times = []
2018 times = []
2018 try:
2019 try:
2019 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
2020 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
2020 for line in fp:
2021 for line in fp:
2021 ts = line.split()
2022 ts = line.split()
2022 times.append((ts[0], [float(t) for t in ts[1:]]))
2023 times.append((ts[0], [float(t) for t in ts[1:]]))
2023 except IOError as err:
2024 except IOError as err:
2024 if err.errno != errno.ENOENT:
2025 if err.errno != errno.ENOENT:
2025 raise
2026 raise
2026 return times
2027 return times
2027
2028
2028 def savetimes(outputdir, result):
2029 def savetimes(outputdir, result):
2029 saved = dict(loadtimes(outputdir))
2030 saved = dict(loadtimes(outputdir))
2030 maxruns = 5
2031 maxruns = 5
2031 skipped = set([str(t[0]) for t in result.skipped])
2032 skipped = set([str(t[0]) for t in result.skipped])
2032 for tdata in result.times:
2033 for tdata in result.times:
2033 test, real = tdata[0], tdata[3]
2034 test, real = tdata[0], tdata[3]
2034 if test not in skipped:
2035 if test not in skipped:
2035 ts = saved.setdefault(test, [])
2036 ts = saved.setdefault(test, [])
2036 ts.append(real)
2037 ts.append(real)
2037 ts[:] = ts[-maxruns:]
2038 ts[:] = ts[-maxruns:]
2038
2039
2039 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2040 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2040 dir=outputdir, text=True)
2041 dir=outputdir, text=True)
2041 with os.fdopen(fd, 'w') as fp:
2042 with os.fdopen(fd, 'w') as fp:
2042 for name, ts in sorted(saved.items()):
2043 for name, ts in sorted(saved.items()):
2043 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2044 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2044 timepath = os.path.join(outputdir, b'.testtimes')
2045 timepath = os.path.join(outputdir, b'.testtimes')
2045 try:
2046 try:
2046 os.unlink(timepath)
2047 os.unlink(timepath)
2047 except OSError:
2048 except OSError:
2048 pass
2049 pass
2049 try:
2050 try:
2050 os.rename(tmpname, timepath)
2051 os.rename(tmpname, timepath)
2051 except OSError:
2052 except OSError:
2052 pass
2053 pass
2053
2054
2054 class TextTestRunner(unittest.TextTestRunner):
2055 class TextTestRunner(unittest.TextTestRunner):
2055 """Custom unittest test runner that uses appropriate settings."""
2056 """Custom unittest test runner that uses appropriate settings."""
2056
2057
2057 def __init__(self, runner, *args, **kwargs):
2058 def __init__(self, runner, *args, **kwargs):
2058 super(TextTestRunner, self).__init__(*args, **kwargs)
2059 super(TextTestRunner, self).__init__(*args, **kwargs)
2059
2060
2060 self._runner = runner
2061 self._runner = runner
2061
2062
2062 def listtests(self, test):
2063 def listtests(self, test):
2063 result = TestResult(self._runner.options, self.stream,
2064 result = TestResult(self._runner.options, self.stream,
2064 self.descriptions, 0)
2065 self.descriptions, 0)
2065 test = sorted(test, key=lambda t: t.name)
2066 test = sorted(test, key=lambda t: t.name)
2066 for t in test:
2067 for t in test:
2067 print(t.name)
2068 print(t.name)
2068 result.addSuccess(t)
2069 result.addSuccess(t)
2069
2070
2070 if self._runner.options.xunit:
2071 if self._runner.options.xunit:
2071 with open(self._runner.options.xunit, "wb") as xuf:
2072 with open(self._runner.options.xunit, "wb") as xuf:
2072 self._writexunit(result, xuf)
2073 self._writexunit(result, xuf)
2073
2074
2074 if self._runner.options.json:
2075 if self._runner.options.json:
2075 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2076 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2076 with open(jsonpath, 'w') as fp:
2077 with open(jsonpath, 'w') as fp:
2077 self._writejson(result, fp)
2078 self._writejson(result, fp)
2078
2079
2079 return result
2080 return result
2080
2081
2081 def run(self, test):
2082 def run(self, test):
2082 result = TestResult(self._runner.options, self.stream,
2083 result = TestResult(self._runner.options, self.stream,
2083 self.descriptions, self.verbosity)
2084 self.descriptions, self.verbosity)
2084
2085
2085 test(result)
2086 test(result)
2086
2087
2087 failed = len(result.failures)
2088 failed = len(result.failures)
2088 skipped = len(result.skipped)
2089 skipped = len(result.skipped)
2089 ignored = len(result.ignored)
2090 ignored = len(result.ignored)
2090
2091
2091 with iolock:
2092 with iolock:
2092 self.stream.writeln('')
2093 self.stream.writeln('')
2093
2094
2094 if not self._runner.options.noskips:
2095 if not self._runner.options.noskips:
2095 for test, msg in result.skipped:
2096 for test, msg in result.skipped:
2096 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2097 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2097 self.stream.write(highlightmsg(formatted, result.color))
2098 self.stream.write(highlightmsg(formatted, result.color))
2098 for test, msg in result.failures:
2099 for test, msg in result.failures:
2099 formatted = 'Failed %s: %s\n' % (test.name, msg)
2100 formatted = 'Failed %s: %s\n' % (test.name, msg)
2100 self.stream.write(highlightmsg(formatted, result.color))
2101 self.stream.write(highlightmsg(formatted, result.color))
2101 for test, msg in result.errors:
2102 for test, msg in result.errors:
2102 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2103 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2103
2104
2104 if self._runner.options.xunit:
2105 if self._runner.options.xunit:
2105 with open(self._runner.options.xunit, "wb") as xuf:
2106 with open(self._runner.options.xunit, "wb") as xuf:
2106 self._writexunit(result, xuf)
2107 self._writexunit(result, xuf)
2107
2108
2108 if self._runner.options.json:
2109 if self._runner.options.json:
2109 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2110 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2110 with open(jsonpath, 'w') as fp:
2111 with open(jsonpath, 'w') as fp:
2111 self._writejson(result, fp)
2112 self._writejson(result, fp)
2112
2113
2113 self._runner._checkhglib('Tested')
2114 self._runner._checkhglib('Tested')
2114
2115
2115 savetimes(self._runner._outputdir, result)
2116 savetimes(self._runner._outputdir, result)
2116
2117
2117 if failed and self._runner.options.known_good_rev:
2118 if failed and self._runner.options.known_good_rev:
2118 self._bisecttests(t for t, m in result.failures)
2119 self._bisecttests(t for t, m in result.failures)
2119 self.stream.writeln(
2120 self.stream.writeln(
2120 '# Ran %d tests, %d skipped, %d failed.'
2121 '# Ran %d tests, %d skipped, %d failed.'
2121 % (result.testsRun, skipped + ignored, failed))
2122 % (result.testsRun, skipped + ignored, failed))
2122 if failed:
2123 if failed:
2123 self.stream.writeln('python hash seed: %s' %
2124 self.stream.writeln('python hash seed: %s' %
2124 os.environ['PYTHONHASHSEED'])
2125 os.environ['PYTHONHASHSEED'])
2125 if self._runner.options.time:
2126 if self._runner.options.time:
2126 self.printtimes(result.times)
2127 self.printtimes(result.times)
2127
2128
2128 if self._runner.options.exceptions:
2129 if self._runner.options.exceptions:
2129 exceptions = aggregateexceptions(
2130 exceptions = aggregateexceptions(
2130 os.path.join(self._runner._outputdir, b'exceptions'))
2131 os.path.join(self._runner._outputdir, b'exceptions'))
2131 total = sum(exceptions.values())
2132 total = sum(exceptions.values())
2132
2133
2133 self.stream.writeln('Exceptions Report:')
2134 self.stream.writeln('Exceptions Report:')
2134 self.stream.writeln('%d total from %d frames' %
2135 self.stream.writeln('%d total from %d frames' %
2135 (total, len(exceptions)))
2136 (total, len(exceptions)))
2136 for (frame, line, exc), count in exceptions.most_common():
2137 for (frame, line, exc), count in exceptions.most_common():
2137 self.stream.writeln('%d\t%s: %s' % (count, frame, exc))
2138 self.stream.writeln('%d\t%s: %s' % (count, frame, exc))
2138
2139
2139 self.stream.flush()
2140 self.stream.flush()
2140
2141
2141 return result
2142 return result
2142
2143
2143 def _bisecttests(self, tests):
2144 def _bisecttests(self, tests):
2144 bisectcmd = ['hg', 'bisect']
2145 bisectcmd = ['hg', 'bisect']
2145 bisectrepo = self._runner.options.bisect_repo
2146 bisectrepo = self._runner.options.bisect_repo
2146 if bisectrepo:
2147 if bisectrepo:
2147 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2148 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2148 def pread(args):
2149 def pread(args):
2149 env = os.environ.copy()
2150 env = os.environ.copy()
2150 env['HGPLAIN'] = '1'
2151 env['HGPLAIN'] = '1'
2151 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2152 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2152 stdout=subprocess.PIPE, env=env)
2153 stdout=subprocess.PIPE, env=env)
2153 data = p.stdout.read()
2154 data = p.stdout.read()
2154 p.wait()
2155 p.wait()
2155 return data
2156 return data
2156 for test in tests:
2157 for test in tests:
2157 pread(bisectcmd + ['--reset']),
2158 pread(bisectcmd + ['--reset']),
2158 pread(bisectcmd + ['--bad', '.'])
2159 pread(bisectcmd + ['--bad', '.'])
2159 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2160 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2160 # TODO: we probably need to forward more options
2161 # TODO: we probably need to forward more options
2161 # that alter hg's behavior inside the tests.
2162 # that alter hg's behavior inside the tests.
2162 opts = ''
2163 opts = ''
2163 withhg = self._runner.options.with_hg
2164 withhg = self._runner.options.with_hg
2164 if withhg:
2165 if withhg:
2165 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2166 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2166 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2167 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2167 test)
2168 test)
2168 data = pread(bisectcmd + ['--command', rtc])
2169 data = pread(bisectcmd + ['--command', rtc])
2169 m = re.search(
2170 m = re.search(
2170 (br'\nThe first (?P<goodbad>bad|good) revision '
2171 (br'\nThe first (?P<goodbad>bad|good) revision '
2171 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2172 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2172 br'summary: +(?P<summary>[^\n]+)\n'),
2173 br'summary: +(?P<summary>[^\n]+)\n'),
2173 data, (re.MULTILINE | re.DOTALL))
2174 data, (re.MULTILINE | re.DOTALL))
2174 if m is None:
2175 if m is None:
2175 self.stream.writeln(
2176 self.stream.writeln(
2176 'Failed to identify failure point for %s' % test)
2177 'Failed to identify failure point for %s' % test)
2177 continue
2178 continue
2178 dat = m.groupdict()
2179 dat = m.groupdict()
2179 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2180 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2180 self.stream.writeln(
2181 self.stream.writeln(
2181 '%s %s by %s (%s)' % (
2182 '%s %s by %s (%s)' % (
2182 test, verb, dat['node'], dat['summary']))
2183 test, verb, dat['node'], dat['summary']))
2183
2184
2184 def printtimes(self, times):
2185 def printtimes(self, times):
2185 # iolock held by run
2186 # iolock held by run
2186 self.stream.writeln('# Producing time report')
2187 self.stream.writeln('# Producing time report')
2187 times.sort(key=lambda t: (t[3]))
2188 times.sort(key=lambda t: (t[3]))
2188 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2189 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2189 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2190 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2190 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2191 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2191 for tdata in times:
2192 for tdata in times:
2192 test = tdata[0]
2193 test = tdata[0]
2193 cuser, csys, real, start, end = tdata[1:6]
2194 cuser, csys, real, start, end = tdata[1:6]
2194 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2195 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2195
2196
2196 @staticmethod
2197 @staticmethod
2197 def _writexunit(result, outf):
2198 def _writexunit(result, outf):
2198 # See http://llg.cubic.org/docs/junit/ for a reference.
2199 # See http://llg.cubic.org/docs/junit/ for a reference.
2199 timesd = dict((t[0], t[3]) for t in result.times)
2200 timesd = dict((t[0], t[3]) for t in result.times)
2200 doc = minidom.Document()
2201 doc = minidom.Document()
2201 s = doc.createElement('testsuite')
2202 s = doc.createElement('testsuite')
2202 s.setAttribute('name', 'run-tests')
2203 s.setAttribute('name', 'run-tests')
2203 s.setAttribute('tests', str(result.testsRun))
2204 s.setAttribute('tests', str(result.testsRun))
2204 s.setAttribute('errors', "0") # TODO
2205 s.setAttribute('errors', "0") # TODO
2205 s.setAttribute('failures', str(len(result.failures)))
2206 s.setAttribute('failures', str(len(result.failures)))
2206 s.setAttribute('skipped', str(len(result.skipped) +
2207 s.setAttribute('skipped', str(len(result.skipped) +
2207 len(result.ignored)))
2208 len(result.ignored)))
2208 doc.appendChild(s)
2209 doc.appendChild(s)
2209 for tc in result.successes:
2210 for tc in result.successes:
2210 t = doc.createElement('testcase')
2211 t = doc.createElement('testcase')
2211 t.setAttribute('name', tc.name)
2212 t.setAttribute('name', tc.name)
2212 tctime = timesd.get(tc.name)
2213 tctime = timesd.get(tc.name)
2213 if tctime is not None:
2214 if tctime is not None:
2214 t.setAttribute('time', '%.3f' % tctime)
2215 t.setAttribute('time', '%.3f' % tctime)
2215 s.appendChild(t)
2216 s.appendChild(t)
2216 for tc, err in sorted(result.faildata.items()):
2217 for tc, err in sorted(result.faildata.items()):
2217 t = doc.createElement('testcase')
2218 t = doc.createElement('testcase')
2218 t.setAttribute('name', tc)
2219 t.setAttribute('name', tc)
2219 tctime = timesd.get(tc)
2220 tctime = timesd.get(tc)
2220 if tctime is not None:
2221 if tctime is not None:
2221 t.setAttribute('time', '%.3f' % tctime)
2222 t.setAttribute('time', '%.3f' % tctime)
2222 # createCDATASection expects a unicode or it will
2223 # createCDATASection expects a unicode or it will
2223 # convert using default conversion rules, which will
2224 # convert using default conversion rules, which will
2224 # fail if string isn't ASCII.
2225 # fail if string isn't ASCII.
2225 err = cdatasafe(err).decode('utf-8', 'replace')
2226 err = cdatasafe(err).decode('utf-8', 'replace')
2226 cd = doc.createCDATASection(err)
2227 cd = doc.createCDATASection(err)
2227 # Use 'failure' here instead of 'error' to match errors = 0,
2228 # Use 'failure' here instead of 'error' to match errors = 0,
2228 # failures = len(result.failures) in the testsuite element.
2229 # failures = len(result.failures) in the testsuite element.
2229 failelem = doc.createElement('failure')
2230 failelem = doc.createElement('failure')
2230 failelem.setAttribute('message', 'output changed')
2231 failelem.setAttribute('message', 'output changed')
2231 failelem.setAttribute('type', 'output-mismatch')
2232 failelem.setAttribute('type', 'output-mismatch')
2232 failelem.appendChild(cd)
2233 failelem.appendChild(cd)
2233 t.appendChild(failelem)
2234 t.appendChild(failelem)
2234 s.appendChild(t)
2235 s.appendChild(t)
2235 for tc, message in result.skipped:
2236 for tc, message in result.skipped:
2236 # According to the schema, 'skipped' has no attributes. So store
2237 # According to the schema, 'skipped' has no attributes. So store
2237 # the skip message as a text node instead.
2238 # the skip message as a text node instead.
2238 t = doc.createElement('testcase')
2239 t = doc.createElement('testcase')
2239 t.setAttribute('name', tc.name)
2240 t.setAttribute('name', tc.name)
2240 binmessage = message.encode('utf-8')
2241 binmessage = message.encode('utf-8')
2241 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2242 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2242 cd = doc.createCDATASection(message)
2243 cd = doc.createCDATASection(message)
2243 skipelem = doc.createElement('skipped')
2244 skipelem = doc.createElement('skipped')
2244 skipelem.appendChild(cd)
2245 skipelem.appendChild(cd)
2245 t.appendChild(skipelem)
2246 t.appendChild(skipelem)
2246 s.appendChild(t)
2247 s.appendChild(t)
2247 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2248 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2248
2249
2249 @staticmethod
2250 @staticmethod
2250 def _writejson(result, outf):
2251 def _writejson(result, outf):
2251 timesd = {}
2252 timesd = {}
2252 for tdata in result.times:
2253 for tdata in result.times:
2253 test = tdata[0]
2254 test = tdata[0]
2254 timesd[test] = tdata[1:]
2255 timesd[test] = tdata[1:]
2255
2256
2256 outcome = {}
2257 outcome = {}
2257 groups = [('success', ((tc, None)
2258 groups = [('success', ((tc, None)
2258 for tc in result.successes)),
2259 for tc in result.successes)),
2259 ('failure', result.failures),
2260 ('failure', result.failures),
2260 ('skip', result.skipped)]
2261 ('skip', result.skipped)]
2261 for res, testcases in groups:
2262 for res, testcases in groups:
2262 for tc, __ in testcases:
2263 for tc, __ in testcases:
2263 if tc.name in timesd:
2264 if tc.name in timesd:
2264 diff = result.faildata.get(tc.name, b'')
2265 diff = result.faildata.get(tc.name, b'')
2265 try:
2266 try:
2266 diff = diff.decode('unicode_escape')
2267 diff = diff.decode('unicode_escape')
2267 except UnicodeDecodeError as e:
2268 except UnicodeDecodeError as e:
2268 diff = '%r decoding diff, sorry' % e
2269 diff = '%r decoding diff, sorry' % e
2269 tres = {'result': res,
2270 tres = {'result': res,
2270 'time': ('%0.3f' % timesd[tc.name][2]),
2271 'time': ('%0.3f' % timesd[tc.name][2]),
2271 'cuser': ('%0.3f' % timesd[tc.name][0]),
2272 'cuser': ('%0.3f' % timesd[tc.name][0]),
2272 'csys': ('%0.3f' % timesd[tc.name][1]),
2273 'csys': ('%0.3f' % timesd[tc.name][1]),
2273 'start': ('%0.3f' % timesd[tc.name][3]),
2274 'start': ('%0.3f' % timesd[tc.name][3]),
2274 'end': ('%0.3f' % timesd[tc.name][4]),
2275 'end': ('%0.3f' % timesd[tc.name][4]),
2275 'diff': diff,
2276 'diff': diff,
2276 }
2277 }
2277 else:
2278 else:
2278 # blacklisted test
2279 # blacklisted test
2279 tres = {'result': res}
2280 tres = {'result': res}
2280
2281
2281 outcome[tc.name] = tres
2282 outcome[tc.name] = tres
2282 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2283 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2283 separators=(',', ': '))
2284 separators=(',', ': '))
2284 outf.writelines(("testreport =", jsonout))
2285 outf.writelines(("testreport =", jsonout))
2285
2286
2286 def sorttests(testdescs, shuffle=False):
2287 def sorttests(testdescs, shuffle=False):
2287 """Do an in-place sort of tests."""
2288 """Do an in-place sort of tests."""
2288 if shuffle:
2289 if shuffle:
2289 random.shuffle(testdescs)
2290 random.shuffle(testdescs)
2290 return
2291 return
2291
2292
2292 # keywords for slow tests
2293 # keywords for slow tests
2293 slow = {b'svn': 10,
2294 slow = {b'svn': 10,
2294 b'cvs': 10,
2295 b'cvs': 10,
2295 b'hghave': 10,
2296 b'hghave': 10,
2296 b'largefiles-update': 10,
2297 b'largefiles-update': 10,
2297 b'run-tests': 10,
2298 b'run-tests': 10,
2298 b'corruption': 10,
2299 b'corruption': 10,
2299 b'race': 10,
2300 b'race': 10,
2300 b'i18n': 10,
2301 b'i18n': 10,
2301 b'check': 100,
2302 b'check': 100,
2302 b'gendoc': 100,
2303 b'gendoc': 100,
2303 b'contrib-perf': 200,
2304 b'contrib-perf': 200,
2304 }
2305 }
2305 perf = {}
2306 perf = {}
2306
2307
2307 def sortkey(f):
2308 def sortkey(f):
2308 # run largest tests first, as they tend to take the longest
2309 # run largest tests first, as they tend to take the longest
2309 f = f['path']
2310 f = f['path']
2310 try:
2311 try:
2311 return perf[f]
2312 return perf[f]
2312 except KeyError:
2313 except KeyError:
2313 try:
2314 try:
2314 val = -os.stat(f).st_size
2315 val = -os.stat(f).st_size
2315 except OSError as e:
2316 except OSError as e:
2316 if e.errno != errno.ENOENT:
2317 if e.errno != errno.ENOENT:
2317 raise
2318 raise
2318 perf[f] = -1e9 # file does not exist, tell early
2319 perf[f] = -1e9 # file does not exist, tell early
2319 return -1e9
2320 return -1e9
2320 for kw, mul in slow.items():
2321 for kw, mul in slow.items():
2321 if kw in f:
2322 if kw in f:
2322 val *= mul
2323 val *= mul
2323 if f.endswith(b'.py'):
2324 if f.endswith(b'.py'):
2324 val /= 10.0
2325 val /= 10.0
2325 perf[f] = val / 1000.0
2326 perf[f] = val / 1000.0
2326 return perf[f]
2327 return perf[f]
2327
2328
2328 testdescs.sort(key=sortkey)
2329 testdescs.sort(key=sortkey)
2329
2330
2330 class TestRunner(object):
2331 class TestRunner(object):
2331 """Holds context for executing tests.
2332 """Holds context for executing tests.
2332
2333
2333 Tests rely on a lot of state. This object holds it for them.
2334 Tests rely on a lot of state. This object holds it for them.
2334 """
2335 """
2335
2336
2336 # Programs required to run tests.
2337 # Programs required to run tests.
2337 REQUIREDTOOLS = [
2338 REQUIREDTOOLS = [
2338 b'diff',
2339 b'diff',
2339 b'grep',
2340 b'grep',
2340 b'unzip',
2341 b'unzip',
2341 b'gunzip',
2342 b'gunzip',
2342 b'bunzip2',
2343 b'bunzip2',
2343 b'sed',
2344 b'sed',
2344 ]
2345 ]
2345
2346
2346 # Maps file extensions to test class.
2347 # Maps file extensions to test class.
2347 TESTTYPES = [
2348 TESTTYPES = [
2348 (b'.py', PythonTest),
2349 (b'.py', PythonTest),
2349 (b'.t', TTest),
2350 (b'.t', TTest),
2350 ]
2351 ]
2351
2352
2352 def __init__(self):
2353 def __init__(self):
2353 self.options = None
2354 self.options = None
2354 self._hgroot = None
2355 self._hgroot = None
2355 self._testdir = None
2356 self._testdir = None
2356 self._outputdir = None
2357 self._outputdir = None
2357 self._hgtmp = None
2358 self._hgtmp = None
2358 self._installdir = None
2359 self._installdir = None
2359 self._bindir = None
2360 self._bindir = None
2360 self._tmpbinddir = None
2361 self._tmpbinddir = None
2361 self._pythondir = None
2362 self._pythondir = None
2362 self._coveragefile = None
2363 self._coveragefile = None
2363 self._createdfiles = []
2364 self._createdfiles = []
2364 self._hgcommand = None
2365 self._hgcommand = None
2365 self._hgpath = None
2366 self._hgpath = None
2366 self._portoffset = 0
2367 self._portoffset = 0
2367 self._ports = {}
2368 self._ports = {}
2368
2369
2369 def run(self, args, parser=None):
2370 def run(self, args, parser=None):
2370 """Run the test suite."""
2371 """Run the test suite."""
2371 oldmask = os.umask(0o22)
2372 oldmask = os.umask(0o22)
2372 try:
2373 try:
2373 parser = parser or getparser()
2374 parser = parser or getparser()
2374 options = parseargs(args, parser)
2375 options = parseargs(args, parser)
2375 tests = [_bytespath(a) for a in options.tests]
2376 tests = [_bytespath(a) for a in options.tests]
2376 if options.test_list is not None:
2377 if options.test_list is not None:
2377 for listfile in options.test_list:
2378 for listfile in options.test_list:
2378 with open(listfile, 'rb') as f:
2379 with open(listfile, 'rb') as f:
2379 tests.extend(t for t in f.read().splitlines() if t)
2380 tests.extend(t for t in f.read().splitlines() if t)
2380 self.options = options
2381 self.options = options
2381
2382
2382 self._checktools()
2383 self._checktools()
2383 testdescs = self.findtests(tests)
2384 testdescs = self.findtests(tests)
2384 if options.profile_runner:
2385 if options.profile_runner:
2385 import statprof
2386 import statprof
2386 statprof.start()
2387 statprof.start()
2387 result = self._run(testdescs)
2388 result = self._run(testdescs)
2388 if options.profile_runner:
2389 if options.profile_runner:
2389 statprof.stop()
2390 statprof.stop()
2390 statprof.display()
2391 statprof.display()
2391 return result
2392 return result
2392
2393
2393 finally:
2394 finally:
2394 os.umask(oldmask)
2395 os.umask(oldmask)
2395
2396
2396 def _run(self, testdescs):
2397 def _run(self, testdescs):
2397 sorttests(testdescs, shuffle=self.options.random)
2398 sorttests(testdescs, shuffle=self.options.random)
2398
2399
2399 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2400 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2400 os, 'getcwdb', os.getcwd)()
2401 os, 'getcwdb', os.getcwd)()
2401 # assume all tests in same folder for now
2402 # assume all tests in same folder for now
2402 if testdescs:
2403 if testdescs:
2403 pathname = os.path.dirname(testdescs[0]['path'])
2404 pathname = os.path.dirname(testdescs[0]['path'])
2404 if pathname:
2405 if pathname:
2405 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2406 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2406 pathname)
2407 pathname)
2407 if self.options.outputdir:
2408 if self.options.outputdir:
2408 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2409 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2409 else:
2410 else:
2410 self._outputdir = self._testdir
2411 self._outputdir = self._testdir
2411 if testdescs and pathname:
2412 if testdescs and pathname:
2412 self._outputdir = os.path.join(self._outputdir, pathname)
2413 self._outputdir = os.path.join(self._outputdir, pathname)
2413
2414
2414 if 'PYTHONHASHSEED' not in os.environ:
2415 if 'PYTHONHASHSEED' not in os.environ:
2415 # use a random python hash seed all the time
2416 # use a random python hash seed all the time
2416 # we do the randomness ourself to know what seed is used
2417 # we do the randomness ourself to know what seed is used
2417 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2418 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2418
2419
2419 if self.options.tmpdir:
2420 if self.options.tmpdir:
2420 self.options.keep_tmpdir = True
2421 self.options.keep_tmpdir = True
2421 tmpdir = _bytespath(self.options.tmpdir)
2422 tmpdir = _bytespath(self.options.tmpdir)
2422 if os.path.exists(tmpdir):
2423 if os.path.exists(tmpdir):
2423 # Meaning of tmpdir has changed since 1.3: we used to create
2424 # Meaning of tmpdir has changed since 1.3: we used to create
2424 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2425 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2425 # tmpdir already exists.
2426 # tmpdir already exists.
2426 print("error: temp dir %r already exists" % tmpdir)
2427 print("error: temp dir %r already exists" % tmpdir)
2427 return 1
2428 return 1
2428
2429
2429 os.makedirs(tmpdir)
2430 os.makedirs(tmpdir)
2430 else:
2431 else:
2431 d = None
2432 d = None
2432 if os.name == 'nt':
2433 if os.name == 'nt':
2433 # without this, we get the default temp dir location, but
2434 # without this, we get the default temp dir location, but
2434 # in all lowercase, which causes troubles with paths (issue3490)
2435 # in all lowercase, which causes troubles with paths (issue3490)
2435 d = osenvironb.get(b'TMP', None)
2436 d = osenvironb.get(b'TMP', None)
2436 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2437 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2437
2438
2438 self._hgtmp = osenvironb[b'HGTMP'] = (
2439 self._hgtmp = osenvironb[b'HGTMP'] = (
2439 os.path.realpath(tmpdir))
2440 os.path.realpath(tmpdir))
2440
2441
2441 if self.options.with_hg:
2442 if self.options.with_hg:
2442 self._installdir = None
2443 self._installdir = None
2443 whg = self.options.with_hg
2444 whg = self.options.with_hg
2444 self._bindir = os.path.dirname(os.path.realpath(whg))
2445 self._bindir = os.path.dirname(os.path.realpath(whg))
2445 assert isinstance(self._bindir, bytes)
2446 assert isinstance(self._bindir, bytes)
2446 self._hgcommand = os.path.basename(whg)
2447 self._hgcommand = os.path.basename(whg)
2447 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2448 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2448 os.makedirs(self._tmpbindir)
2449 os.makedirs(self._tmpbindir)
2449
2450
2450 normbin = os.path.normpath(os.path.abspath(whg))
2451 normbin = os.path.normpath(os.path.abspath(whg))
2451 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2452 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2452
2453
2453 # Other Python scripts in the test harness need to
2454 # Other Python scripts in the test harness need to
2454 # `import mercurial`. If `hg` is a Python script, we assume
2455 # `import mercurial`. If `hg` is a Python script, we assume
2455 # the Mercurial modules are relative to its path and tell the tests
2456 # the Mercurial modules are relative to its path and tell the tests
2456 # to load Python modules from its directory.
2457 # to load Python modules from its directory.
2457 with open(whg, 'rb') as fh:
2458 with open(whg, 'rb') as fh:
2458 initial = fh.read(1024)
2459 initial = fh.read(1024)
2459
2460
2460 if re.match(b'#!.*python', initial):
2461 if re.match(b'#!.*python', initial):
2461 self._pythondir = self._bindir
2462 self._pythondir = self._bindir
2462 # If it looks like our in-repo Rust binary, use the source root.
2463 # If it looks like our in-repo Rust binary, use the source root.
2463 # This is a bit hacky. But rhg is still not supported outside the
2464 # This is a bit hacky. But rhg is still not supported outside the
2464 # source directory. So until it is, do the simple thing.
2465 # source directory. So until it is, do the simple thing.
2465 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2466 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2466 self._pythondir = os.path.dirname(self._testdir)
2467 self._pythondir = os.path.dirname(self._testdir)
2467 # Fall back to the legacy behavior.
2468 # Fall back to the legacy behavior.
2468 else:
2469 else:
2469 self._pythondir = self._bindir
2470 self._pythondir = self._bindir
2470
2471
2471 else:
2472 else:
2472 self._installdir = os.path.join(self._hgtmp, b"install")
2473 self._installdir = os.path.join(self._hgtmp, b"install")
2473 self._bindir = os.path.join(self._installdir, b"bin")
2474 self._bindir = os.path.join(self._installdir, b"bin")
2474 self._hgcommand = b'hg'
2475 self._hgcommand = b'hg'
2475 self._tmpbindir = self._bindir
2476 self._tmpbindir = self._bindir
2476 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2477 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2477
2478
2478 # set CHGHG, then replace "hg" command by "chg"
2479 # set CHGHG, then replace "hg" command by "chg"
2479 chgbindir = self._bindir
2480 chgbindir = self._bindir
2480 if self.options.chg or self.options.with_chg:
2481 if self.options.chg or self.options.with_chg:
2481 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2482 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2482 else:
2483 else:
2483 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2484 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2484 if self.options.chg:
2485 if self.options.chg:
2485 self._hgcommand = b'chg'
2486 self._hgcommand = b'chg'
2486 elif self.options.with_chg:
2487 elif self.options.with_chg:
2487 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2488 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2488 self._hgcommand = os.path.basename(self.options.with_chg)
2489 self._hgcommand = os.path.basename(self.options.with_chg)
2489
2490
2490 osenvironb[b"BINDIR"] = self._bindir
2491 osenvironb[b"BINDIR"] = self._bindir
2491 osenvironb[b"PYTHON"] = PYTHON
2492 osenvironb[b"PYTHON"] = PYTHON
2492
2493
2493 if self.options.with_python3:
2494 if self.options.with_python3:
2494 osenvironb[b'PYTHON3'] = self.options.with_python3
2495 osenvironb[b'PYTHON3'] = self.options.with_python3
2495
2496
2496 fileb = _bytespath(__file__)
2497 fileb = _bytespath(__file__)
2497 runtestdir = os.path.abspath(os.path.dirname(fileb))
2498 runtestdir = os.path.abspath(os.path.dirname(fileb))
2498 osenvironb[b'RUNTESTDIR'] = runtestdir
2499 osenvironb[b'RUNTESTDIR'] = runtestdir
2499 if PYTHON3:
2500 if PYTHON3:
2500 sepb = _bytespath(os.pathsep)
2501 sepb = _bytespath(os.pathsep)
2501 else:
2502 else:
2502 sepb = os.pathsep
2503 sepb = os.pathsep
2503 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2504 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2504 if os.path.islink(__file__):
2505 if os.path.islink(__file__):
2505 # test helper will likely be at the end of the symlink
2506 # test helper will likely be at the end of the symlink
2506 realfile = os.path.realpath(fileb)
2507 realfile = os.path.realpath(fileb)
2507 realdir = os.path.abspath(os.path.dirname(realfile))
2508 realdir = os.path.abspath(os.path.dirname(realfile))
2508 path.insert(2, realdir)
2509 path.insert(2, realdir)
2509 if chgbindir != self._bindir:
2510 if chgbindir != self._bindir:
2510 path.insert(1, chgbindir)
2511 path.insert(1, chgbindir)
2511 if self._testdir != runtestdir:
2512 if self._testdir != runtestdir:
2512 path = [self._testdir] + path
2513 path = [self._testdir] + path
2513 if self._tmpbindir != self._bindir:
2514 if self._tmpbindir != self._bindir:
2514 path = [self._tmpbindir] + path
2515 path = [self._tmpbindir] + path
2515 osenvironb[b"PATH"] = sepb.join(path)
2516 osenvironb[b"PATH"] = sepb.join(path)
2516
2517
2517 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2518 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2518 # can run .../tests/run-tests.py test-foo where test-foo
2519 # can run .../tests/run-tests.py test-foo where test-foo
2519 # adds an extension to HGRC. Also include run-test.py directory to
2520 # adds an extension to HGRC. Also include run-test.py directory to
2520 # import modules like heredoctest.
2521 # import modules like heredoctest.
2521 pypath = [self._pythondir, self._testdir, runtestdir]
2522 pypath = [self._pythondir, self._testdir, runtestdir]
2522 # We have to augment PYTHONPATH, rather than simply replacing
2523 # We have to augment PYTHONPATH, rather than simply replacing
2523 # it, in case external libraries are only available via current
2524 # it, in case external libraries are only available via current
2524 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2525 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2525 # are in /opt/subversion.)
2526 # are in /opt/subversion.)
2526 oldpypath = osenvironb.get(IMPL_PATH)
2527 oldpypath = osenvironb.get(IMPL_PATH)
2527 if oldpypath:
2528 if oldpypath:
2528 pypath.append(oldpypath)
2529 pypath.append(oldpypath)
2529 osenvironb[IMPL_PATH] = sepb.join(pypath)
2530 osenvironb[IMPL_PATH] = sepb.join(pypath)
2530
2531
2531 if self.options.pure:
2532 if self.options.pure:
2532 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2533 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2533 os.environ["HGMODULEPOLICY"] = "py"
2534 os.environ["HGMODULEPOLICY"] = "py"
2534
2535
2535 if self.options.allow_slow_tests:
2536 if self.options.allow_slow_tests:
2536 os.environ["HGTEST_SLOW"] = "slow"
2537 os.environ["HGTEST_SLOW"] = "slow"
2537 elif 'HGTEST_SLOW' in os.environ:
2538 elif 'HGTEST_SLOW' in os.environ:
2538 del os.environ['HGTEST_SLOW']
2539 del os.environ['HGTEST_SLOW']
2539
2540
2540 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2541 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2541
2542
2542 if self.options.exceptions:
2543 if self.options.exceptions:
2543 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2544 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2544 try:
2545 try:
2545 os.makedirs(exceptionsdir)
2546 os.makedirs(exceptionsdir)
2546 except OSError as e:
2547 except OSError as e:
2547 if e.errno != errno.EEXIST:
2548 if e.errno != errno.EEXIST:
2548 raise
2549 raise
2549
2550
2550 # Remove all existing exception reports.
2551 # Remove all existing exception reports.
2551 for f in os.listdir(exceptionsdir):
2552 for f in os.listdir(exceptionsdir):
2552 os.unlink(os.path.join(exceptionsdir, f))
2553 os.unlink(os.path.join(exceptionsdir, f))
2553
2554
2554 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2555 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2555 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2556 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2556 self.options.extra_config_opt.append(
2557 self.options.extra_config_opt.append(
2557 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2558 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2558
2559
2559 vlog("# Using TESTDIR", self._testdir)
2560 vlog("# Using TESTDIR", self._testdir)
2560 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2561 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2561 vlog("# Using HGTMP", self._hgtmp)
2562 vlog("# Using HGTMP", self._hgtmp)
2562 vlog("# Using PATH", os.environ["PATH"])
2563 vlog("# Using PATH", os.environ["PATH"])
2563 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2564 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2564 vlog("# Writing to directory", self._outputdir)
2565 vlog("# Writing to directory", self._outputdir)
2565
2566
2566 try:
2567 try:
2567 return self._runtests(testdescs) or 0
2568 return self._runtests(testdescs) or 0
2568 finally:
2569 finally:
2569 time.sleep(.1)
2570 time.sleep(.1)
2570 self._cleanup()
2571 self._cleanup()
2571
2572
2572 def findtests(self, args):
2573 def findtests(self, args):
2573 """Finds possible test files from arguments.
2574 """Finds possible test files from arguments.
2574
2575
2575 If you wish to inject custom tests into the test harness, this would
2576 If you wish to inject custom tests into the test harness, this would
2576 be a good function to monkeypatch or override in a derived class.
2577 be a good function to monkeypatch or override in a derived class.
2577 """
2578 """
2578 if not args:
2579 if not args:
2579 if self.options.changed:
2580 if self.options.changed:
2580 proc = Popen4('hg st --rev "%s" -man0 .' %
2581 proc = Popen4('hg st --rev "%s" -man0 .' %
2581 self.options.changed, None, 0)
2582 self.options.changed, None, 0)
2582 stdout, stderr = proc.communicate()
2583 stdout, stderr = proc.communicate()
2583 args = stdout.strip(b'\0').split(b'\0')
2584 args = stdout.strip(b'\0').split(b'\0')
2584 else:
2585 else:
2585 args = os.listdir(b'.')
2586 args = os.listdir(b'.')
2586
2587
2587 expanded_args = []
2588 expanded_args = []
2588 for arg in args:
2589 for arg in args:
2589 if os.path.isdir(arg):
2590 if os.path.isdir(arg):
2590 if not arg.endswith(b'/'):
2591 if not arg.endswith(b'/'):
2591 arg += b'/'
2592 arg += b'/'
2592 expanded_args.extend([arg + a for a in os.listdir(arg)])
2593 expanded_args.extend([arg + a for a in os.listdir(arg)])
2593 else:
2594 else:
2594 expanded_args.append(arg)
2595 expanded_args.append(arg)
2595 args = expanded_args
2596 args = expanded_args
2596
2597
2597 tests = []
2598 tests = []
2598 for t in args:
2599 for t in args:
2599 if not (os.path.basename(t).startswith(b'test-')
2600 if not (os.path.basename(t).startswith(b'test-')
2600 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2601 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2601 continue
2602 continue
2602 if t.endswith(b'.t'):
2603 if t.endswith(b'.t'):
2603 # .t file may contain multiple test cases
2604 # .t file may contain multiple test cases
2604 cases = sorted(parsettestcases(t))
2605 cases = sorted(parsettestcases(t))
2605 if cases:
2606 if cases:
2606 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2607 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2607 else:
2608 else:
2608 tests.append({'path': t})
2609 tests.append({'path': t})
2609 else:
2610 else:
2610 tests.append({'path': t})
2611 tests.append({'path': t})
2611 return tests
2612 return tests
2612
2613
2613 def _runtests(self, testdescs):
2614 def _runtests(self, testdescs):
2614 def _reloadtest(test, i):
2615 def _reloadtest(test, i):
2615 # convert a test back to its description dict
2616 # convert a test back to its description dict
2616 desc = {'path': test.path}
2617 desc = {'path': test.path}
2617 case = getattr(test, '_case', None)
2618 case = getattr(test, '_case', None)
2618 if case:
2619 if case:
2619 desc['case'] = case
2620 desc['case'] = case
2620 return self._gettest(desc, i)
2621 return self._gettest(desc, i)
2621
2622
2622 try:
2623 try:
2623 if self.options.restart:
2624 if self.options.restart:
2624 orig = list(testdescs)
2625 orig = list(testdescs)
2625 while testdescs:
2626 while testdescs:
2626 desc = testdescs[0]
2627 desc = testdescs[0]
2627 # desc['path'] is a relative path
2628 # desc['path'] is a relative path
2628 if 'case' in desc:
2629 if 'case' in desc:
2629 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2630 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2630 else:
2631 else:
2631 errpath = b'%s.err' % desc['path']
2632 errpath = b'%s.err' % desc['path']
2632 errpath = os.path.join(self._outputdir, errpath)
2633 errpath = os.path.join(self._outputdir, errpath)
2633 if os.path.exists(errpath):
2634 if os.path.exists(errpath):
2634 break
2635 break
2635 testdescs.pop(0)
2636 testdescs.pop(0)
2636 if not testdescs:
2637 if not testdescs:
2637 print("running all tests")
2638 print("running all tests")
2638 testdescs = orig
2639 testdescs = orig
2639
2640
2640 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2641 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2641
2642
2642 failed = False
2643 failed = False
2643 kws = self.options.keywords
2644 kws = self.options.keywords
2644 if kws is not None and PYTHON3:
2645 if kws is not None and PYTHON3:
2645 kws = kws.encode('utf-8')
2646 kws = kws.encode('utf-8')
2646
2647
2647 suite = TestSuite(self._testdir,
2648 suite = TestSuite(self._testdir,
2648 jobs=self.options.jobs,
2649 jobs=self.options.jobs,
2649 whitelist=self.options.whitelisted,
2650 whitelist=self.options.whitelisted,
2650 blacklist=self.options.blacklist,
2651 blacklist=self.options.blacklist,
2651 retest=self.options.retest,
2652 retest=self.options.retest,
2652 keywords=kws,
2653 keywords=kws,
2653 loop=self.options.loop,
2654 loop=self.options.loop,
2654 runs_per_test=self.options.runs_per_test,
2655 runs_per_test=self.options.runs_per_test,
2655 showchannels=self.options.showchannels,
2656 showchannels=self.options.showchannels,
2656 tests=tests, loadtest=_reloadtest)
2657 tests=tests, loadtest=_reloadtest)
2657 verbosity = 1
2658 verbosity = 1
2658 if self.options.verbose:
2659 if self.options.verbose:
2659 verbosity = 2
2660 verbosity = 2
2660 runner = TextTestRunner(self, verbosity=verbosity)
2661 runner = TextTestRunner(self, verbosity=verbosity)
2661
2662
2662 if self.options.list_tests:
2663 if self.options.list_tests:
2663 result = runner.listtests(suite)
2664 result = runner.listtests(suite)
2664 else:
2665 else:
2665 if self._installdir:
2666 if self._installdir:
2666 self._installhg()
2667 self._installhg()
2667 self._checkhglib("Testing")
2668 self._checkhglib("Testing")
2668 else:
2669 else:
2669 self._usecorrectpython()
2670 self._usecorrectpython()
2670 if self.options.chg:
2671 if self.options.chg:
2671 assert self._installdir
2672 assert self._installdir
2672 self._installchg()
2673 self._installchg()
2673
2674
2674 result = runner.run(suite)
2675 result = runner.run(suite)
2675
2676
2676 if result.failures:
2677 if result.failures:
2677 failed = True
2678 failed = True
2678
2679
2679 if self.options.anycoverage:
2680 if self.options.anycoverage:
2680 self._outputcoverage()
2681 self._outputcoverage()
2681 except KeyboardInterrupt:
2682 except KeyboardInterrupt:
2682 failed = True
2683 failed = True
2683 print("\ninterrupted!")
2684 print("\ninterrupted!")
2684
2685
2685 if failed:
2686 if failed:
2686 return 1
2687 return 1
2687
2688
2688 def _getport(self, count):
2689 def _getport(self, count):
2689 port = self._ports.get(count) # do we have a cached entry?
2690 port = self._ports.get(count) # do we have a cached entry?
2690 if port is None:
2691 if port is None:
2691 portneeded = 3
2692 portneeded = 3
2692 # above 100 tries we just give up and let test reports failure
2693 # above 100 tries we just give up and let test reports failure
2693 for tries in xrange(100):
2694 for tries in xrange(100):
2694 allfree = True
2695 allfree = True
2695 port = self.options.port + self._portoffset
2696 port = self.options.port + self._portoffset
2696 for idx in xrange(portneeded):
2697 for idx in xrange(portneeded):
2697 if not checkportisavailable(port + idx):
2698 if not checkportisavailable(port + idx):
2698 allfree = False
2699 allfree = False
2699 break
2700 break
2700 self._portoffset += portneeded
2701 self._portoffset += portneeded
2701 if allfree:
2702 if allfree:
2702 break
2703 break
2703 self._ports[count] = port
2704 self._ports[count] = port
2704 return port
2705 return port
2705
2706
2706 def _gettest(self, testdesc, count):
2707 def _gettest(self, testdesc, count):
2707 """Obtain a Test by looking at its filename.
2708 """Obtain a Test by looking at its filename.
2708
2709
2709 Returns a Test instance. The Test may not be runnable if it doesn't
2710 Returns a Test instance. The Test may not be runnable if it doesn't
2710 map to a known type.
2711 map to a known type.
2711 """
2712 """
2712 path = testdesc['path']
2713 path = testdesc['path']
2713 lctest = path.lower()
2714 lctest = path.lower()
2714 testcls = Test
2715 testcls = Test
2715
2716
2716 for ext, cls in self.TESTTYPES:
2717 for ext, cls in self.TESTTYPES:
2717 if lctest.endswith(ext):
2718 if lctest.endswith(ext):
2718 testcls = cls
2719 testcls = cls
2719 break
2720 break
2720
2721
2721 refpath = os.path.join(self._testdir, path)
2722 refpath = os.path.join(self._testdir, path)
2722 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2723 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2723
2724
2724 # extra keyword parameters. 'case' is used by .t tests
2725 # extra keyword parameters. 'case' is used by .t tests
2725 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2726 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2726
2727
2727 t = testcls(refpath, self._outputdir, tmpdir,
2728 t = testcls(refpath, self._outputdir, tmpdir,
2728 keeptmpdir=self.options.keep_tmpdir,
2729 keeptmpdir=self.options.keep_tmpdir,
2729 debug=self.options.debug,
2730 debug=self.options.debug,
2730 first=self.options.first,
2731 first=self.options.first,
2731 timeout=self.options.timeout,
2732 timeout=self.options.timeout,
2732 startport=self._getport(count),
2733 startport=self._getport(count),
2733 extraconfigopts=self.options.extra_config_opt,
2734 extraconfigopts=self.options.extra_config_opt,
2734 py3kwarnings=self.options.py3k_warnings,
2735 py3kwarnings=self.options.py3k_warnings,
2735 shell=self.options.shell,
2736 shell=self.options.shell,
2736 hgcommand=self._hgcommand,
2737 hgcommand=self._hgcommand,
2737 usechg=bool(self.options.with_chg or self.options.chg),
2738 usechg=bool(self.options.with_chg or self.options.chg),
2738 useipv6=useipv6, **kwds)
2739 useipv6=useipv6, **kwds)
2739 t.should_reload = True
2740 t.should_reload = True
2740 return t
2741 return t
2741
2742
2742 def _cleanup(self):
2743 def _cleanup(self):
2743 """Clean up state from this test invocation."""
2744 """Clean up state from this test invocation."""
2744 if self.options.keep_tmpdir:
2745 if self.options.keep_tmpdir:
2745 return
2746 return
2746
2747
2747 vlog("# Cleaning up HGTMP", self._hgtmp)
2748 vlog("# Cleaning up HGTMP", self._hgtmp)
2748 shutil.rmtree(self._hgtmp, True)
2749 shutil.rmtree(self._hgtmp, True)
2749 for f in self._createdfiles:
2750 for f in self._createdfiles:
2750 try:
2751 try:
2751 os.remove(f)
2752 os.remove(f)
2752 except OSError:
2753 except OSError:
2753 pass
2754 pass
2754
2755
2755 def _usecorrectpython(self):
2756 def _usecorrectpython(self):
2756 """Configure the environment to use the appropriate Python in tests."""
2757 """Configure the environment to use the appropriate Python in tests."""
2757 # Tests must use the same interpreter as us or bad things will happen.
2758 # Tests must use the same interpreter as us or bad things will happen.
2758 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2759 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2759 if getattr(os, 'symlink', None):
2760 if getattr(os, 'symlink', None):
2760 vlog("# Making python executable in test path a symlink to '%s'" %
2761 vlog("# Making python executable in test path a symlink to '%s'" %
2761 sys.executable)
2762 sys.executable)
2762 mypython = os.path.join(self._tmpbindir, pyexename)
2763 mypython = os.path.join(self._tmpbindir, pyexename)
2763 try:
2764 try:
2764 if os.readlink(mypython) == sys.executable:
2765 if os.readlink(mypython) == sys.executable:
2765 return
2766 return
2766 os.unlink(mypython)
2767 os.unlink(mypython)
2767 except OSError as err:
2768 except OSError as err:
2768 if err.errno != errno.ENOENT:
2769 if err.errno != errno.ENOENT:
2769 raise
2770 raise
2770 if self._findprogram(pyexename) != sys.executable:
2771 if self._findprogram(pyexename) != sys.executable:
2771 try:
2772 try:
2772 os.symlink(sys.executable, mypython)
2773 os.symlink(sys.executable, mypython)
2773 self._createdfiles.append(mypython)
2774 self._createdfiles.append(mypython)
2774 except OSError as err:
2775 except OSError as err:
2775 # child processes may race, which is harmless
2776 # child processes may race, which is harmless
2776 if err.errno != errno.EEXIST:
2777 if err.errno != errno.EEXIST:
2777 raise
2778 raise
2778 else:
2779 else:
2779 exedir, exename = os.path.split(sys.executable)
2780 exedir, exename = os.path.split(sys.executable)
2780 vlog("# Modifying search path to find %s as %s in '%s'" %
2781 vlog("# Modifying search path to find %s as %s in '%s'" %
2781 (exename, pyexename, exedir))
2782 (exename, pyexename, exedir))
2782 path = os.environ['PATH'].split(os.pathsep)
2783 path = os.environ['PATH'].split(os.pathsep)
2783 while exedir in path:
2784 while exedir in path:
2784 path.remove(exedir)
2785 path.remove(exedir)
2785 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2786 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2786 if not self._findprogram(pyexename):
2787 if not self._findprogram(pyexename):
2787 print("WARNING: Cannot find %s in search path" % pyexename)
2788 print("WARNING: Cannot find %s in search path" % pyexename)
2788
2789
2789 def _installhg(self):
2790 def _installhg(self):
2790 """Install hg into the test environment.
2791 """Install hg into the test environment.
2791
2792
2792 This will also configure hg with the appropriate testing settings.
2793 This will also configure hg with the appropriate testing settings.
2793 """
2794 """
2794 vlog("# Performing temporary installation of HG")
2795 vlog("# Performing temporary installation of HG")
2795 installerrs = os.path.join(self._hgtmp, b"install.err")
2796 installerrs = os.path.join(self._hgtmp, b"install.err")
2796 compiler = ''
2797 compiler = ''
2797 if self.options.compiler:
2798 if self.options.compiler:
2798 compiler = '--compiler ' + self.options.compiler
2799 compiler = '--compiler ' + self.options.compiler
2799 if self.options.pure:
2800 if self.options.pure:
2800 pure = b"--pure"
2801 pure = b"--pure"
2801 else:
2802 else:
2802 pure = b""
2803 pure = b""
2803
2804
2804 # Run installer in hg root
2805 # Run installer in hg root
2805 script = os.path.realpath(sys.argv[0])
2806 script = os.path.realpath(sys.argv[0])
2806 exe = sys.executable
2807 exe = sys.executable
2807 if PYTHON3:
2808 if PYTHON3:
2808 compiler = _bytespath(compiler)
2809 compiler = _bytespath(compiler)
2809 script = _bytespath(script)
2810 script = _bytespath(script)
2810 exe = _bytespath(exe)
2811 exe = _bytespath(exe)
2811 hgroot = os.path.dirname(os.path.dirname(script))
2812 hgroot = os.path.dirname(os.path.dirname(script))
2812 self._hgroot = hgroot
2813 self._hgroot = hgroot
2813 os.chdir(hgroot)
2814 os.chdir(hgroot)
2814 nohome = b'--home=""'
2815 nohome = b'--home=""'
2815 if os.name == 'nt':
2816 if os.name == 'nt':
2816 # The --home="" trick works only on OS where os.sep == '/'
2817 # The --home="" trick works only on OS where os.sep == '/'
2817 # because of a distutils convert_path() fast-path. Avoid it at
2818 # because of a distutils convert_path() fast-path. Avoid it at
2818 # least on Windows for now, deal with .pydistutils.cfg bugs
2819 # least on Windows for now, deal with .pydistutils.cfg bugs
2819 # when they happen.
2820 # when they happen.
2820 nohome = b''
2821 nohome = b''
2821 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2822 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2822 b' build %(compiler)s --build-base="%(base)s"'
2823 b' build %(compiler)s --build-base="%(base)s"'
2823 b' install --force --prefix="%(prefix)s"'
2824 b' install --force --prefix="%(prefix)s"'
2824 b' --install-lib="%(libdir)s"'
2825 b' --install-lib="%(libdir)s"'
2825 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2826 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2826 % {b'exe': exe, b'pure': pure,
2827 % {b'exe': exe, b'pure': pure,
2827 b'compiler': compiler,
2828 b'compiler': compiler,
2828 b'base': os.path.join(self._hgtmp, b"build"),
2829 b'base': os.path.join(self._hgtmp, b"build"),
2829 b'prefix': self._installdir, b'libdir': self._pythondir,
2830 b'prefix': self._installdir, b'libdir': self._pythondir,
2830 b'bindir': self._bindir,
2831 b'bindir': self._bindir,
2831 b'nohome': nohome, b'logfile': installerrs})
2832 b'nohome': nohome, b'logfile': installerrs})
2832
2833
2833 # setuptools requires install directories to exist.
2834 # setuptools requires install directories to exist.
2834 def makedirs(p):
2835 def makedirs(p):
2835 try:
2836 try:
2836 os.makedirs(p)
2837 os.makedirs(p)
2837 except OSError as e:
2838 except OSError as e:
2838 if e.errno != errno.EEXIST:
2839 if e.errno != errno.EEXIST:
2839 raise
2840 raise
2840 makedirs(self._pythondir)
2841 makedirs(self._pythondir)
2841 makedirs(self._bindir)
2842 makedirs(self._bindir)
2842
2843
2843 vlog("# Running", cmd)
2844 vlog("# Running", cmd)
2844 if os.system(cmd) == 0:
2845 if os.system(cmd) == 0:
2845 if not self.options.verbose:
2846 if not self.options.verbose:
2846 try:
2847 try:
2847 os.remove(installerrs)
2848 os.remove(installerrs)
2848 except OSError as e:
2849 except OSError as e:
2849 if e.errno != errno.ENOENT:
2850 if e.errno != errno.ENOENT:
2850 raise
2851 raise
2851 else:
2852 else:
2852 with open(installerrs, 'rb') as f:
2853 with open(installerrs, 'rb') as f:
2853 for line in f:
2854 for line in f:
2854 if PYTHON3:
2855 if PYTHON3:
2855 sys.stdout.buffer.write(line)
2856 sys.stdout.buffer.write(line)
2856 else:
2857 else:
2857 sys.stdout.write(line)
2858 sys.stdout.write(line)
2858 sys.exit(1)
2859 sys.exit(1)
2859 os.chdir(self._testdir)
2860 os.chdir(self._testdir)
2860
2861
2861 self._usecorrectpython()
2862 self._usecorrectpython()
2862
2863
2863 if self.options.py3k_warnings and not self.options.anycoverage:
2864 if self.options.py3k_warnings and not self.options.anycoverage:
2864 vlog("# Updating hg command to enable Py3k Warnings switch")
2865 vlog("# Updating hg command to enable Py3k Warnings switch")
2865 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2866 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2866 lines = [line.rstrip() for line in f]
2867 lines = [line.rstrip() for line in f]
2867 lines[0] += ' -3'
2868 lines[0] += ' -3'
2868 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2869 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2869 for line in lines:
2870 for line in lines:
2870 f.write(line + '\n')
2871 f.write(line + '\n')
2871
2872
2872 hgbat = os.path.join(self._bindir, b'hg.bat')
2873 hgbat = os.path.join(self._bindir, b'hg.bat')
2873 if os.path.isfile(hgbat):
2874 if os.path.isfile(hgbat):
2874 # hg.bat expects to be put in bin/scripts while run-tests.py
2875 # hg.bat expects to be put in bin/scripts while run-tests.py
2875 # installation layout put it in bin/ directly. Fix it
2876 # installation layout put it in bin/ directly. Fix it
2876 with open(hgbat, 'rb') as f:
2877 with open(hgbat, 'rb') as f:
2877 data = f.read()
2878 data = f.read()
2878 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2879 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2879 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2880 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2880 b'"%~dp0python" "%~dp0hg" %*')
2881 b'"%~dp0python" "%~dp0hg" %*')
2881 with open(hgbat, 'wb') as f:
2882 with open(hgbat, 'wb') as f:
2882 f.write(data)
2883 f.write(data)
2883 else:
2884 else:
2884 print('WARNING: cannot fix hg.bat reference to python.exe')
2885 print('WARNING: cannot fix hg.bat reference to python.exe')
2885
2886
2886 if self.options.anycoverage:
2887 if self.options.anycoverage:
2887 custom = os.path.join(self._testdir, 'sitecustomize.py')
2888 custom = os.path.join(self._testdir, 'sitecustomize.py')
2888 target = os.path.join(self._pythondir, 'sitecustomize.py')
2889 target = os.path.join(self._pythondir, 'sitecustomize.py')
2889 vlog('# Installing coverage trigger to %s' % target)
2890 vlog('# Installing coverage trigger to %s' % target)
2890 shutil.copyfile(custom, target)
2891 shutil.copyfile(custom, target)
2891 rc = os.path.join(self._testdir, '.coveragerc')
2892 rc = os.path.join(self._testdir, '.coveragerc')
2892 vlog('# Installing coverage rc to %s' % rc)
2893 vlog('# Installing coverage rc to %s' % rc)
2893 os.environ['COVERAGE_PROCESS_START'] = rc
2894 os.environ['COVERAGE_PROCESS_START'] = rc
2894 covdir = os.path.join(self._installdir, '..', 'coverage')
2895 covdir = os.path.join(self._installdir, '..', 'coverage')
2895 try:
2896 try:
2896 os.mkdir(covdir)
2897 os.mkdir(covdir)
2897 except OSError as e:
2898 except OSError as e:
2898 if e.errno != errno.EEXIST:
2899 if e.errno != errno.EEXIST:
2899 raise
2900 raise
2900
2901
2901 os.environ['COVERAGE_DIR'] = covdir
2902 os.environ['COVERAGE_DIR'] = covdir
2902
2903
2903 def _checkhglib(self, verb):
2904 def _checkhglib(self, verb):
2904 """Ensure that the 'mercurial' package imported by python is
2905 """Ensure that the 'mercurial' package imported by python is
2905 the one we expect it to be. If not, print a warning to stderr."""
2906 the one we expect it to be. If not, print a warning to stderr."""
2906 if ((self._bindir == self._pythondir) and
2907 if ((self._bindir == self._pythondir) and
2907 (self._bindir != self._tmpbindir)):
2908 (self._bindir != self._tmpbindir)):
2908 # The pythondir has been inferred from --with-hg flag.
2909 # The pythondir has been inferred from --with-hg flag.
2909 # We cannot expect anything sensible here.
2910 # We cannot expect anything sensible here.
2910 return
2911 return
2911 expecthg = os.path.join(self._pythondir, b'mercurial')
2912 expecthg = os.path.join(self._pythondir, b'mercurial')
2912 actualhg = self._gethgpath()
2913 actualhg = self._gethgpath()
2913 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2914 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2914 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2915 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2915 ' (expected %s)\n'
2916 ' (expected %s)\n'
2916 % (verb, actualhg, expecthg))
2917 % (verb, actualhg, expecthg))
2917 def _gethgpath(self):
2918 def _gethgpath(self):
2918 """Return the path to the mercurial package that is actually found by
2919 """Return the path to the mercurial package that is actually found by
2919 the current Python interpreter."""
2920 the current Python interpreter."""
2920 if self._hgpath is not None:
2921 if self._hgpath is not None:
2921 return self._hgpath
2922 return self._hgpath
2922
2923
2923 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2924 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2924 cmd = cmd % PYTHON
2925 cmd = cmd % PYTHON
2925 if PYTHON3:
2926 if PYTHON3:
2926 cmd = _strpath(cmd)
2927 cmd = _strpath(cmd)
2927 pipe = os.popen(cmd)
2928 pipe = os.popen(cmd)
2928 try:
2929 try:
2929 self._hgpath = _bytespath(pipe.read().strip())
2930 self._hgpath = _bytespath(pipe.read().strip())
2930 finally:
2931 finally:
2931 pipe.close()
2932 pipe.close()
2932
2933
2933 return self._hgpath
2934 return self._hgpath
2934
2935
2935 def _installchg(self):
2936 def _installchg(self):
2936 """Install chg into the test environment"""
2937 """Install chg into the test environment"""
2937 vlog('# Performing temporary installation of CHG')
2938 vlog('# Performing temporary installation of CHG')
2938 assert os.path.dirname(self._bindir) == self._installdir
2939 assert os.path.dirname(self._bindir) == self._installdir
2939 assert self._hgroot, 'must be called after _installhg()'
2940 assert self._hgroot, 'must be called after _installhg()'
2940 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2941 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2941 % {b'make': 'make', # TODO: switch by option or environment?
2942 % {b'make': 'make', # TODO: switch by option or environment?
2942 b'prefix': self._installdir})
2943 b'prefix': self._installdir})
2943 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2944 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2944 vlog("# Running", cmd)
2945 vlog("# Running", cmd)
2945 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2946 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2946 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2947 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2947 stderr=subprocess.STDOUT)
2948 stderr=subprocess.STDOUT)
2948 out, _err = proc.communicate()
2949 out, _err = proc.communicate()
2949 if proc.returncode != 0:
2950 if proc.returncode != 0:
2950 if PYTHON3:
2951 if PYTHON3:
2951 sys.stdout.buffer.write(out)
2952 sys.stdout.buffer.write(out)
2952 else:
2953 else:
2953 sys.stdout.write(out)
2954 sys.stdout.write(out)
2954 sys.exit(1)
2955 sys.exit(1)
2955
2956
2956 def _outputcoverage(self):
2957 def _outputcoverage(self):
2957 """Produce code coverage output."""
2958 """Produce code coverage output."""
2958 import coverage
2959 import coverage
2959 coverage = coverage.coverage
2960 coverage = coverage.coverage
2960
2961
2961 vlog('# Producing coverage report')
2962 vlog('# Producing coverage report')
2962 # chdir is the easiest way to get short, relative paths in the
2963 # chdir is the easiest way to get short, relative paths in the
2963 # output.
2964 # output.
2964 os.chdir(self._hgroot)
2965 os.chdir(self._hgroot)
2965 covdir = os.path.join(self._installdir, '..', 'coverage')
2966 covdir = os.path.join(self._installdir, '..', 'coverage')
2966 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2967 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2967
2968
2968 # Map install directory paths back to source directory.
2969 # Map install directory paths back to source directory.
2969 cov.config.paths['srcdir'] = ['.', self._pythondir]
2970 cov.config.paths['srcdir'] = ['.', self._pythondir]
2970
2971
2971 cov.combine()
2972 cov.combine()
2972
2973
2973 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2974 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2974 cov.report(ignore_errors=True, omit=omit)
2975 cov.report(ignore_errors=True, omit=omit)
2975
2976
2976 if self.options.htmlcov:
2977 if self.options.htmlcov:
2977 htmldir = os.path.join(self._outputdir, 'htmlcov')
2978 htmldir = os.path.join(self._outputdir, 'htmlcov')
2978 cov.html_report(directory=htmldir, omit=omit)
2979 cov.html_report(directory=htmldir, omit=omit)
2979 if self.options.annotate:
2980 if self.options.annotate:
2980 adir = os.path.join(self._outputdir, 'annotated')
2981 adir = os.path.join(self._outputdir, 'annotated')
2981 if not os.path.isdir(adir):
2982 if not os.path.isdir(adir):
2982 os.mkdir(adir)
2983 os.mkdir(adir)
2983 cov.annotate(directory=adir, omit=omit)
2984 cov.annotate(directory=adir, omit=omit)
2984
2985
2985 def _findprogram(self, program):
2986 def _findprogram(self, program):
2986 """Search PATH for a executable program"""
2987 """Search PATH for a executable program"""
2987 dpb = _bytespath(os.defpath)
2988 dpb = _bytespath(os.defpath)
2988 sepb = _bytespath(os.pathsep)
2989 sepb = _bytespath(os.pathsep)
2989 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2990 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2990 name = os.path.join(p, program)
2991 name = os.path.join(p, program)
2991 if os.name == 'nt' or os.access(name, os.X_OK):
2992 if os.name == 'nt' or os.access(name, os.X_OK):
2992 return name
2993 return name
2993 return None
2994 return None
2994
2995
2995 def _checktools(self):
2996 def _checktools(self):
2996 """Ensure tools required to run tests are present."""
2997 """Ensure tools required to run tests are present."""
2997 for p in self.REQUIREDTOOLS:
2998 for p in self.REQUIREDTOOLS:
2998 if os.name == 'nt' and not p.endswith('.exe'):
2999 if os.name == 'nt' and not p.endswith('.exe'):
2999 p += '.exe'
3000 p += '.exe'
3000 found = self._findprogram(p)
3001 found = self._findprogram(p)
3001 if found:
3002 if found:
3002 vlog("# Found prerequisite", p, "at", found)
3003 vlog("# Found prerequisite", p, "at", found)
3003 else:
3004 else:
3004 print("WARNING: Did not find prerequisite tool: %s " %
3005 print("WARNING: Did not find prerequisite tool: %s " %
3005 p.decode("utf-8"))
3006 p.decode("utf-8"))
3006
3007
3007 def aggregateexceptions(path):
3008 def aggregateexceptions(path):
3008 exceptions = collections.Counter()
3009 exceptions = collections.Counter()
3009
3010
3010 for f in os.listdir(path):
3011 for f in os.listdir(path):
3011 with open(os.path.join(path, f), 'rb') as fh:
3012 with open(os.path.join(path, f), 'rb') as fh:
3012 data = fh.read().split(b'\0')
3013 data = fh.read().split(b'\0')
3013 if len(data) != 4:
3014 if len(data) != 4:
3014 continue
3015 continue
3015
3016
3016 exc, mainframe, hgframe, hgline = data
3017 exc, mainframe, hgframe, hgline = data
3017 exc = exc.decode('utf-8')
3018 exc = exc.decode('utf-8')
3018 mainframe = mainframe.decode('utf-8')
3019 mainframe = mainframe.decode('utf-8')
3019 hgframe = hgframe.decode('utf-8')
3020 hgframe = hgframe.decode('utf-8')
3020 hgline = hgline.decode('utf-8')
3021 hgline = hgline.decode('utf-8')
3021 exceptions[(hgframe, hgline, exc)] += 1
3022 exceptions[(hgframe, hgline, exc)] += 1
3022
3023
3023 return exceptions
3024 return exceptions
3024
3025
3025 if __name__ == '__main__':
3026 if __name__ == '__main__':
3026 runner = TestRunner()
3027 runner = TestRunner()
3027
3028
3028 try:
3029 try:
3029 import msvcrt
3030 import msvcrt
3030 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3031 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3031 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3032 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3032 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3033 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3033 except ImportError:
3034 except ImportError:
3034 pass
3035 pass
3035
3036
3036 sys.exit(runner.run(sys.argv[1:]))
3037 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now