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