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