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