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