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