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