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