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