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