##// END OF EJS Templates
tests: work around non-thread-safeness of sysconfig.get_config_var()
Manuel Jacob -
r50129:9dad3284 stable
parent child Browse files
Show More
@@ -1,4086 +1,4091 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 # sysconfig is not thread-safe (https://github.com/python/cpython/issues/92452)
1004 sysconfiglock = threading.Lock()
1005
1006
1003 class Test(unittest.TestCase):
1007 class Test(unittest.TestCase):
1004 """Encapsulates a single, runnable test.
1008 """Encapsulates a single, runnable test.
1005
1009
1006 While this class conforms to the unittest.TestCase API, it differs in that
1010 While this class conforms to the unittest.TestCase API, it differs in that
1007 instances need to be instantiated manually. (Typically, unittest.TestCase
1011 instances need to be instantiated manually. (Typically, unittest.TestCase
1008 classes are instantiated automatically by scanning modules.)
1012 classes are instantiated automatically by scanning modules.)
1009 """
1013 """
1010
1014
1011 # Status code reserved for skipped tests (used by hghave).
1015 # Status code reserved for skipped tests (used by hghave).
1012 SKIPPED_STATUS = 80
1016 SKIPPED_STATUS = 80
1013
1017
1014 def __init__(
1018 def __init__(
1015 self,
1019 self,
1016 path,
1020 path,
1017 outputdir,
1021 outputdir,
1018 tmpdir,
1022 tmpdir,
1019 keeptmpdir=False,
1023 keeptmpdir=False,
1020 debug=False,
1024 debug=False,
1021 first=False,
1025 first=False,
1022 timeout=None,
1026 timeout=None,
1023 startport=None,
1027 startport=None,
1024 extraconfigopts=None,
1028 extraconfigopts=None,
1025 shell=None,
1029 shell=None,
1026 hgcommand=None,
1030 hgcommand=None,
1027 slowtimeout=None,
1031 slowtimeout=None,
1028 usechg=False,
1032 usechg=False,
1029 chgdebug=False,
1033 chgdebug=False,
1030 useipv6=False,
1034 useipv6=False,
1031 ):
1035 ):
1032 """Create a test from parameters.
1036 """Create a test from parameters.
1033
1037
1034 path is the full path to the file defining the test.
1038 path is the full path to the file defining the test.
1035
1039
1036 tmpdir is the main temporary directory to use for this test.
1040 tmpdir is the main temporary directory to use for this test.
1037
1041
1038 keeptmpdir determines whether to keep the test's temporary directory
1042 keeptmpdir determines whether to keep the test's temporary directory
1039 after execution. It defaults to removal (False).
1043 after execution. It defaults to removal (False).
1040
1044
1041 debug mode will make the test execute verbosely, with unfiltered
1045 debug mode will make the test execute verbosely, with unfiltered
1042 output.
1046 output.
1043
1047
1044 timeout controls the maximum run time of the test. It is ignored when
1048 timeout controls the maximum run time of the test. It is ignored when
1045 debug is True. See slowtimeout for tests with #require slow.
1049 debug is True. See slowtimeout for tests with #require slow.
1046
1050
1047 slowtimeout overrides timeout if the test has #require slow.
1051 slowtimeout overrides timeout if the test has #require slow.
1048
1052
1049 startport controls the starting port number to use for this test. Each
1053 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
1054 test will reserve 3 port numbers for execution. It is the caller's
1051 responsibility to allocate a non-overlapping port range to Test
1055 responsibility to allocate a non-overlapping port range to Test
1052 instances.
1056 instances.
1053
1057
1054 extraconfigopts is an iterable of extra hgrc config options. Values
1058 extraconfigopts is an iterable of extra hgrc config options. Values
1055 must have the form "key=value" (something understood by hgrc). Values
1059 must have the form "key=value" (something understood by hgrc). Values
1056 of the form "foo.key=value" will result in "[foo] key=value".
1060 of the form "foo.key=value" will result in "[foo] key=value".
1057
1061
1058 shell is the shell to execute tests in.
1062 shell is the shell to execute tests in.
1059 """
1063 """
1060 if timeout is None:
1064 if timeout is None:
1061 timeout = defaults['timeout']
1065 timeout = defaults['timeout']
1062 if startport is None:
1066 if startport is None:
1063 startport = defaults['port']
1067 startport = defaults['port']
1064 if slowtimeout is None:
1068 if slowtimeout is None:
1065 slowtimeout = defaults['slowtimeout']
1069 slowtimeout = defaults['slowtimeout']
1066 self.path = path
1070 self.path = path
1067 self.relpath = os.path.relpath(path)
1071 self.relpath = os.path.relpath(path)
1068 self.bname = os.path.basename(path)
1072 self.bname = os.path.basename(path)
1069 self.name = _bytes2sys(self.bname)
1073 self.name = _bytes2sys(self.bname)
1070 self._testdir = os.path.dirname(path)
1074 self._testdir = os.path.dirname(path)
1071 self._outputdir = outputdir
1075 self._outputdir = outputdir
1072 self._tmpname = os.path.basename(path)
1076 self._tmpname = os.path.basename(path)
1073 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
1077 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
1074
1078
1075 self._threadtmp = tmpdir
1079 self._threadtmp = tmpdir
1076 self._keeptmpdir = keeptmpdir
1080 self._keeptmpdir = keeptmpdir
1077 self._debug = debug
1081 self._debug = debug
1078 self._first = first
1082 self._first = first
1079 self._timeout = timeout
1083 self._timeout = timeout
1080 self._slowtimeout = slowtimeout
1084 self._slowtimeout = slowtimeout
1081 self._startport = startport
1085 self._startport = startport
1082 self._extraconfigopts = extraconfigopts or []
1086 self._extraconfigopts = extraconfigopts or []
1083 self._shell = _sys2bytes(shell)
1087 self._shell = _sys2bytes(shell)
1084 self._hgcommand = hgcommand or b'hg'
1088 self._hgcommand = hgcommand or b'hg'
1085 self._usechg = usechg
1089 self._usechg = usechg
1086 self._chgdebug = chgdebug
1090 self._chgdebug = chgdebug
1087 self._useipv6 = useipv6
1091 self._useipv6 = useipv6
1088
1092
1089 self._aborted = False
1093 self._aborted = False
1090 self._daemonpids = []
1094 self._daemonpids = []
1091 self._finished = None
1095 self._finished = None
1092 self._ret = None
1096 self._ret = None
1093 self._out = None
1097 self._out = None
1094 self._skipped = None
1098 self._skipped = None
1095 self._testtmp = None
1099 self._testtmp = None
1096 self._chgsockdir = None
1100 self._chgsockdir = None
1097
1101
1098 self._refout = self.readrefout()
1102 self._refout = self.readrefout()
1099
1103
1100 def readrefout(self):
1104 def readrefout(self):
1101 """read reference output"""
1105 """read reference output"""
1102 # If we're not in --debug mode and reference output file exists,
1106 # If we're not in --debug mode and reference output file exists,
1103 # check test output against it.
1107 # check test output against it.
1104 if self._debug:
1108 if self._debug:
1105 return None # to match "out is None"
1109 return None # to match "out is None"
1106 elif os.path.exists(self.refpath):
1110 elif os.path.exists(self.refpath):
1107 with open(self.refpath, 'rb') as f:
1111 with open(self.refpath, 'rb') as f:
1108 return f.read().splitlines(True)
1112 return f.read().splitlines(True)
1109 else:
1113 else:
1110 return []
1114 return []
1111
1115
1112 # needed to get base class __repr__ running
1116 # needed to get base class __repr__ running
1113 @property
1117 @property
1114 def _testMethodName(self):
1118 def _testMethodName(self):
1115 return self.name
1119 return self.name
1116
1120
1117 def __str__(self):
1121 def __str__(self):
1118 return self.name
1122 return self.name
1119
1123
1120 def shortDescription(self):
1124 def shortDescription(self):
1121 return self.name
1125 return self.name
1122
1126
1123 def setUp(self):
1127 def setUp(self):
1124 """Tasks to perform before run()."""
1128 """Tasks to perform before run()."""
1125 self._finished = False
1129 self._finished = False
1126 self._ret = None
1130 self._ret = None
1127 self._out = None
1131 self._out = None
1128 self._skipped = None
1132 self._skipped = None
1129
1133
1130 try:
1134 try:
1131 os.mkdir(self._threadtmp)
1135 os.mkdir(self._threadtmp)
1132 except OSError as e:
1136 except OSError as e:
1133 if e.errno != errno.EEXIST:
1137 if e.errno != errno.EEXIST:
1134 raise
1138 raise
1135
1139
1136 name = self._tmpname
1140 name = self._tmpname
1137 self._testtmp = os.path.join(self._threadtmp, name)
1141 self._testtmp = os.path.join(self._threadtmp, name)
1138 os.mkdir(self._testtmp)
1142 os.mkdir(self._testtmp)
1139
1143
1140 # Remove any previous output files.
1144 # Remove any previous output files.
1141 if os.path.exists(self.errpath):
1145 if os.path.exists(self.errpath):
1142 try:
1146 try:
1143 os.remove(self.errpath)
1147 os.remove(self.errpath)
1144 except OSError as e:
1148 except OSError as e:
1145 # We might have raced another test to clean up a .err
1149 # We might have raced another test to clean up a .err
1146 # file, so ignore ENOENT when removing a previous .err
1150 # file, so ignore ENOENT when removing a previous .err
1147 # file.
1151 # file.
1148 if e.errno != errno.ENOENT:
1152 if e.errno != errno.ENOENT:
1149 raise
1153 raise
1150
1154
1151 if self._usechg:
1155 if self._usechg:
1152 self._chgsockdir = os.path.join(
1156 self._chgsockdir = os.path.join(
1153 self._threadtmp, b'%s.chgsock' % name
1157 self._threadtmp, b'%s.chgsock' % name
1154 )
1158 )
1155 os.mkdir(self._chgsockdir)
1159 os.mkdir(self._chgsockdir)
1156
1160
1157 def run(self, result):
1161 def run(self, result):
1158 """Run this test and report results against a TestResult instance."""
1162 """Run this test and report results against a TestResult instance."""
1159 # This function is extremely similar to unittest.TestCase.run(). Once
1163 # This function is extremely similar to unittest.TestCase.run(). Once
1160 # we require Python 2.7 (or at least its version of unittest), this
1164 # we require Python 2.7 (or at least its version of unittest), this
1161 # function can largely go away.
1165 # function can largely go away.
1162 self._result = result
1166 self._result = result
1163 result.startTest(self)
1167 result.startTest(self)
1164 try:
1168 try:
1165 try:
1169 try:
1166 self.setUp()
1170 self.setUp()
1167 except (KeyboardInterrupt, SystemExit):
1171 except (KeyboardInterrupt, SystemExit):
1168 self._aborted = True
1172 self._aborted = True
1169 raise
1173 raise
1170 except Exception:
1174 except Exception:
1171 result.addError(self, sys.exc_info())
1175 result.addError(self, sys.exc_info())
1172 return
1176 return
1173
1177
1174 success = False
1178 success = False
1175 try:
1179 try:
1176 self.runTest()
1180 self.runTest()
1177 except KeyboardInterrupt:
1181 except KeyboardInterrupt:
1178 self._aborted = True
1182 self._aborted = True
1179 raise
1183 raise
1180 except unittest.SkipTest as e:
1184 except unittest.SkipTest as e:
1181 result.addSkip(self, str(e))
1185 result.addSkip(self, str(e))
1182 # The base class will have already counted this as a
1186 # The base class will have already counted this as a
1183 # test we "ran", but we want to exclude skipped tests
1187 # test we "ran", but we want to exclude skipped tests
1184 # from those we count towards those run.
1188 # from those we count towards those run.
1185 result.testsRun -= 1
1189 result.testsRun -= 1
1186 except self.failureException as e:
1190 except self.failureException as e:
1187 # This differs from unittest in that we don't capture
1191 # This differs from unittest in that we don't capture
1188 # the stack trace. This is for historical reasons and
1192 # the stack trace. This is for historical reasons and
1189 # this decision could be revisited in the future,
1193 # this decision could be revisited in the future,
1190 # especially for PythonTest instances.
1194 # especially for PythonTest instances.
1191 if result.addFailure(self, str(e)):
1195 if result.addFailure(self, str(e)):
1192 success = True
1196 success = True
1193 except Exception:
1197 except Exception:
1194 result.addError(self, sys.exc_info())
1198 result.addError(self, sys.exc_info())
1195 else:
1199 else:
1196 success = True
1200 success = True
1197
1201
1198 try:
1202 try:
1199 self.tearDown()
1203 self.tearDown()
1200 except (KeyboardInterrupt, SystemExit):
1204 except (KeyboardInterrupt, SystemExit):
1201 self._aborted = True
1205 self._aborted = True
1202 raise
1206 raise
1203 except Exception:
1207 except Exception:
1204 result.addError(self, sys.exc_info())
1208 result.addError(self, sys.exc_info())
1205 success = False
1209 success = False
1206
1210
1207 if success:
1211 if success:
1208 result.addSuccess(self)
1212 result.addSuccess(self)
1209 finally:
1213 finally:
1210 result.stopTest(self, interrupted=self._aborted)
1214 result.stopTest(self, interrupted=self._aborted)
1211
1215
1212 def runTest(self):
1216 def runTest(self):
1213 """Run this test instance.
1217 """Run this test instance.
1214
1218
1215 This will return a tuple describing the result of the test.
1219 This will return a tuple describing the result of the test.
1216 """
1220 """
1217 env = self._getenv()
1221 env = self._getenv()
1218 self._genrestoreenv(env)
1222 self._genrestoreenv(env)
1219 self._daemonpids.append(env['DAEMON_PIDS'])
1223 self._daemonpids.append(env['DAEMON_PIDS'])
1220 self._createhgrc(env['HGRCPATH'])
1224 self._createhgrc(env['HGRCPATH'])
1221
1225
1222 vlog('# Test', self.name)
1226 vlog('# Test', self.name)
1223
1227
1224 ret, out = self._run(env)
1228 ret, out = self._run(env)
1225 self._finished = True
1229 self._finished = True
1226 self._ret = ret
1230 self._ret = ret
1227 self._out = out
1231 self._out = out
1228
1232
1229 def describe(ret):
1233 def describe(ret):
1230 if ret < 0:
1234 if ret < 0:
1231 return 'killed by signal: %d' % -ret
1235 return 'killed by signal: %d' % -ret
1232 return 'returned error code %d' % ret
1236 return 'returned error code %d' % ret
1233
1237
1234 self._skipped = False
1238 self._skipped = False
1235
1239
1236 if ret == self.SKIPPED_STATUS:
1240 if ret == self.SKIPPED_STATUS:
1237 if out is None: # Debug mode, nothing to parse.
1241 if out is None: # Debug mode, nothing to parse.
1238 missing = ['unknown']
1242 missing = ['unknown']
1239 failed = None
1243 failed = None
1240 else:
1244 else:
1241 missing, failed = TTest.parsehghaveoutput(out)
1245 missing, failed = TTest.parsehghaveoutput(out)
1242
1246
1243 if not missing:
1247 if not missing:
1244 missing = ['skipped']
1248 missing = ['skipped']
1245
1249
1246 if failed:
1250 if failed:
1247 self.fail('hg have failed checking for %s' % failed[-1])
1251 self.fail('hg have failed checking for %s' % failed[-1])
1248 else:
1252 else:
1249 self._skipped = True
1253 self._skipped = True
1250 raise unittest.SkipTest(missing[-1])
1254 raise unittest.SkipTest(missing[-1])
1251 elif ret == 'timeout':
1255 elif ret == 'timeout':
1252 self.fail('timed out')
1256 self.fail('timed out')
1253 elif ret is False:
1257 elif ret is False:
1254 self.fail('no result code from test')
1258 self.fail('no result code from test')
1255 elif out != self._refout:
1259 elif out != self._refout:
1256 # Diff generation may rely on written .err file.
1260 # Diff generation may rely on written .err file.
1257 if (
1261 if (
1258 (ret != 0 or out != self._refout)
1262 (ret != 0 or out != self._refout)
1259 and not self._skipped
1263 and not self._skipped
1260 and not self._debug
1264 and not self._debug
1261 ):
1265 ):
1262 with open(self.errpath, 'wb') as f:
1266 with open(self.errpath, 'wb') as f:
1263 for line in out:
1267 for line in out:
1264 f.write(line)
1268 f.write(line)
1265
1269
1266 # The result object handles diff calculation for us.
1270 # The result object handles diff calculation for us.
1267 with firstlock:
1271 with firstlock:
1268 if self._result.addOutputMismatch(self, ret, out, self._refout):
1272 if self._result.addOutputMismatch(self, ret, out, self._refout):
1269 # change was accepted, skip failing
1273 # change was accepted, skip failing
1270 return
1274 return
1271 if self._first:
1275 if self._first:
1272 global firsterror
1276 global firsterror
1273 firsterror = True
1277 firsterror = True
1274
1278
1275 if ret:
1279 if ret:
1276 msg = 'output changed and ' + describe(ret)
1280 msg = 'output changed and ' + describe(ret)
1277 else:
1281 else:
1278 msg = 'output changed'
1282 msg = 'output changed'
1279
1283
1280 self.fail(msg)
1284 self.fail(msg)
1281 elif ret:
1285 elif ret:
1282 self.fail(describe(ret))
1286 self.fail(describe(ret))
1283
1287
1284 def tearDown(self):
1288 def tearDown(self):
1285 """Tasks to perform after run()."""
1289 """Tasks to perform after run()."""
1286 for entry in self._daemonpids:
1290 for entry in self._daemonpids:
1287 killdaemons(entry)
1291 killdaemons(entry)
1288 self._daemonpids = []
1292 self._daemonpids = []
1289
1293
1290 if self._keeptmpdir:
1294 if self._keeptmpdir:
1291 log(
1295 log(
1292 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1296 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1293 % (
1297 % (
1294 _bytes2sys(self._testtmp),
1298 _bytes2sys(self._testtmp),
1295 _bytes2sys(self._threadtmp),
1299 _bytes2sys(self._threadtmp),
1296 )
1300 )
1297 )
1301 )
1298 else:
1302 else:
1299 try:
1303 try:
1300 shutil.rmtree(self._testtmp)
1304 shutil.rmtree(self._testtmp)
1301 except OSError:
1305 except OSError:
1302 # unreadable directory may be left in $TESTTMP; fix permission
1306 # unreadable directory may be left in $TESTTMP; fix permission
1303 # and try again
1307 # and try again
1304 makecleanable(self._testtmp)
1308 makecleanable(self._testtmp)
1305 shutil.rmtree(self._testtmp, True)
1309 shutil.rmtree(self._testtmp, True)
1306 shutil.rmtree(self._threadtmp, True)
1310 shutil.rmtree(self._threadtmp, True)
1307
1311
1308 if self._usechg:
1312 if self._usechg:
1309 # chgservers will stop automatically after they find the socket
1313 # chgservers will stop automatically after they find the socket
1310 # files are deleted
1314 # files are deleted
1311 shutil.rmtree(self._chgsockdir, True)
1315 shutil.rmtree(self._chgsockdir, True)
1312
1316
1313 if (
1317 if (
1314 (self._ret != 0 or self._out != self._refout)
1318 (self._ret != 0 or self._out != self._refout)
1315 and not self._skipped
1319 and not self._skipped
1316 and not self._debug
1320 and not self._debug
1317 and self._out
1321 and self._out
1318 ):
1322 ):
1319 with open(self.errpath, 'wb') as f:
1323 with open(self.errpath, 'wb') as f:
1320 for line in self._out:
1324 for line in self._out:
1321 f.write(line)
1325 f.write(line)
1322
1326
1323 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1327 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1324
1328
1325 def _run(self, env):
1329 def _run(self, env):
1326 # This should be implemented in child classes to run tests.
1330 # This should be implemented in child classes to run tests.
1327 raise unittest.SkipTest('unknown test type')
1331 raise unittest.SkipTest('unknown test type')
1328
1332
1329 def abort(self):
1333 def abort(self):
1330 """Terminate execution of this test."""
1334 """Terminate execution of this test."""
1331 self._aborted = True
1335 self._aborted = True
1332
1336
1333 def _portmap(self, i):
1337 def _portmap(self, i):
1334 offset = b'' if i == 0 else b'%d' % i
1338 offset = b'' if i == 0 else b'%d' % i
1335 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1339 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1336
1340
1337 def _getreplacements(self):
1341 def _getreplacements(self):
1338 """Obtain a mapping of text replacements to apply to test output.
1342 """Obtain a mapping of text replacements to apply to test output.
1339
1343
1340 Test output needs to be normalized so it can be compared to expected
1344 Test output needs to be normalized so it can be compared to expected
1341 output. This function defines how some of that normalization will
1345 output. This function defines how some of that normalization will
1342 occur.
1346 occur.
1343 """
1347 """
1344 r = [
1348 r = [
1345 # This list should be parallel to defineport in _getenv
1349 # This list should be parallel to defineport in _getenv
1346 self._portmap(0),
1350 self._portmap(0),
1347 self._portmap(1),
1351 self._portmap(1),
1348 self._portmap(2),
1352 self._portmap(2),
1349 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1353 (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$'),
1354 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1351 ]
1355 ]
1352 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1356 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1353 if WINDOWS:
1357 if WINDOWS:
1354 # JSON output escapes backslashes in Windows paths, so also catch a
1358 # JSON output escapes backslashes in Windows paths, so also catch a
1355 # double-escape.
1359 # double-escape.
1356 replaced = self._testtmp.replace(b'\\', br'\\')
1360 replaced = self._testtmp.replace(b'\\', br'\\')
1357 r.append((self._escapepath(replaced), b'$STR_REPR_TESTTMP'))
1361 r.append((self._escapepath(replaced), b'$STR_REPR_TESTTMP'))
1358
1362
1359 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1363 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1360
1364
1361 if os.path.exists(replacementfile):
1365 if os.path.exists(replacementfile):
1362 data = {}
1366 data = {}
1363 with open(replacementfile, mode='rb') as source:
1367 with open(replacementfile, mode='rb') as source:
1364 # the intermediate 'compile' step help with debugging
1368 # the intermediate 'compile' step help with debugging
1365 code = compile(source.read(), replacementfile, 'exec')
1369 code = compile(source.read(), replacementfile, 'exec')
1366 exec(code, data)
1370 exec(code, data)
1367 for value in data.get('substitutions', ()):
1371 for value in data.get('substitutions', ()):
1368 if len(value) != 2:
1372 if len(value) != 2:
1369 msg = 'malformatted substitution in %s: %r'
1373 msg = 'malformatted substitution in %s: %r'
1370 msg %= (replacementfile, value)
1374 msg %= (replacementfile, value)
1371 raise ValueError(msg)
1375 raise ValueError(msg)
1372 r.append(value)
1376 r.append(value)
1373 return r
1377 return r
1374
1378
1375 def _escapepath(self, p):
1379 def _escapepath(self, p):
1376 if WINDOWS:
1380 if WINDOWS:
1377 return b''.join(
1381 return b''.join(
1378 c.isalpha()
1382 c.isalpha()
1379 and b'[%s%s]' % (c.lower(), c.upper())
1383 and b'[%s%s]' % (c.lower(), c.upper())
1380 or c in b'/\\'
1384 or c in b'/\\'
1381 and br'[/\\]'
1385 and br'[/\\]'
1382 or c.isdigit()
1386 or c.isdigit()
1383 and c
1387 and c
1384 or b'\\' + c
1388 or b'\\' + c
1385 for c in [p[i : i + 1] for i in range(len(p))]
1389 for c in [p[i : i + 1] for i in range(len(p))]
1386 )
1390 )
1387 else:
1391 else:
1388 return re.escape(p)
1392 return re.escape(p)
1389
1393
1390 def _localip(self):
1394 def _localip(self):
1391 if self._useipv6:
1395 if self._useipv6:
1392 return b'::1'
1396 return b'::1'
1393 else:
1397 else:
1394 return b'127.0.0.1'
1398 return b'127.0.0.1'
1395
1399
1396 def _genrestoreenv(self, testenv):
1400 def _genrestoreenv(self, testenv):
1397 """Generate a script that can be used by tests to restore the original
1401 """Generate a script that can be used by tests to restore the original
1398 environment."""
1402 environment."""
1399 # Put the restoreenv script inside self._threadtmp
1403 # Put the restoreenv script inside self._threadtmp
1400 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1404 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1401 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1405 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1402
1406
1403 # Only restore environment variable names that the shell allows
1407 # Only restore environment variable names that the shell allows
1404 # us to export.
1408 # us to export.
1405 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1409 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1406
1410
1407 # Do not restore these variables; otherwise tests would fail.
1411 # Do not restore these variables; otherwise tests would fail.
1408 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1412 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1409
1413
1410 with open(scriptpath, 'w') as envf:
1414 with open(scriptpath, 'w') as envf:
1411 for name, value in origenviron.items():
1415 for name, value in origenviron.items():
1412 if not name_regex.match(name):
1416 if not name_regex.match(name):
1413 # Skip environment variables with unusual names not
1417 # Skip environment variables with unusual names not
1414 # allowed by most shells.
1418 # allowed by most shells.
1415 continue
1419 continue
1416 if name in reqnames:
1420 if name in reqnames:
1417 continue
1421 continue
1418 envf.write('%s=%s\n' % (name, shellquote(value)))
1422 envf.write('%s=%s\n' % (name, shellquote(value)))
1419
1423
1420 for name in testenv:
1424 for name in testenv:
1421 if name in origenviron or name in reqnames:
1425 if name in origenviron or name in reqnames:
1422 continue
1426 continue
1423 envf.write('unset %s\n' % (name,))
1427 envf.write('unset %s\n' % (name,))
1424
1428
1425 def _getenv(self):
1429 def _getenv(self):
1426 """Obtain environment variables to use during test execution."""
1430 """Obtain environment variables to use during test execution."""
1427
1431
1428 def defineport(i):
1432 def defineport(i):
1429 offset = '' if i == 0 else '%s' % i
1433 offset = '' if i == 0 else '%s' % i
1430 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1434 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1431
1435
1432 env = os.environ.copy()
1436 env = os.environ.copy()
1433 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1437 with sysconfiglock:
1438 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1434 env['HGEMITWARNINGS'] = '1'
1439 env['HGEMITWARNINGS'] = '1'
1435 env['TESTTMP'] = _bytes2sys(self._testtmp)
1440 env['TESTTMP'] = _bytes2sys(self._testtmp)
1436 # the FORWARD_SLASH version is useful when running `sh` on non unix
1441 # the FORWARD_SLASH version is useful when running `sh` on non unix
1437 # system (e.g. Windows)
1442 # system (e.g. Windows)
1438 env['TESTTMP_FORWARD_SLASH'] = env['TESTTMP'].replace(os.sep, '/')
1443 env['TESTTMP_FORWARD_SLASH'] = env['TESTTMP'].replace(os.sep, '/')
1439 uid_file = os.path.join(_bytes2sys(self._testtmp), 'UID')
1444 uid_file = os.path.join(_bytes2sys(self._testtmp), 'UID')
1440 env['HGTEST_UUIDFILE'] = uid_file
1445 env['HGTEST_UUIDFILE'] = uid_file
1441 env['TESTNAME'] = self.name
1446 env['TESTNAME'] = self.name
1442 env['HOME'] = _bytes2sys(self._testtmp)
1447 env['HOME'] = _bytes2sys(self._testtmp)
1443 if WINDOWS:
1448 if WINDOWS:
1444 env['REALUSERPROFILE'] = env['USERPROFILE']
1449 env['REALUSERPROFILE'] = env['USERPROFILE']
1445 # py3.8+ ignores HOME: https://bugs.python.org/issue36264
1450 # py3.8+ ignores HOME: https://bugs.python.org/issue36264
1446 env['USERPROFILE'] = env['HOME']
1451 env['USERPROFILE'] = env['HOME']
1447 formated_timeout = _bytes2sys(b"%d" % default_defaults['timeout'][1])
1452 formated_timeout = _bytes2sys(b"%d" % default_defaults['timeout'][1])
1448 env['HGTEST_TIMEOUT_DEFAULT'] = formated_timeout
1453 env['HGTEST_TIMEOUT_DEFAULT'] = formated_timeout
1449 env['HGTEST_TIMEOUT'] = _bytes2sys(b"%d" % self._timeout)
1454 env['HGTEST_TIMEOUT'] = _bytes2sys(b"%d" % self._timeout)
1450 # This number should match portneeded in _getport
1455 # This number should match portneeded in _getport
1451 for port in xrange(3):
1456 for port in xrange(3):
1452 # This list should be parallel to _portmap in _getreplacements
1457 # This list should be parallel to _portmap in _getreplacements
1453 defineport(port)
1458 defineport(port)
1454 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1459 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1455 env["DAEMON_PIDS"] = _bytes2sys(
1460 env["DAEMON_PIDS"] = _bytes2sys(
1456 os.path.join(self._threadtmp, b'daemon.pids')
1461 os.path.join(self._threadtmp, b'daemon.pids')
1457 )
1462 )
1458 env["HGEDITOR"] = (
1463 env["HGEDITOR"] = (
1459 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1464 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1460 )
1465 )
1461 env["HGUSER"] = "test"
1466 env["HGUSER"] = "test"
1462 env["HGENCODING"] = "ascii"
1467 env["HGENCODING"] = "ascii"
1463 env["HGENCODINGMODE"] = "strict"
1468 env["HGENCODINGMODE"] = "strict"
1464 env["HGHOSTNAME"] = "test-hostname"
1469 env["HGHOSTNAME"] = "test-hostname"
1465 env['HGIPV6'] = str(int(self._useipv6))
1470 env['HGIPV6'] = str(int(self._useipv6))
1466 # See contrib/catapipe.py for how to use this functionality.
1471 # See contrib/catapipe.py for how to use this functionality.
1467 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1472 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1468 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1473 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1469 # non-test one in as a default, otherwise set to devnull
1474 # non-test one in as a default, otherwise set to devnull
1470 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1475 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1471 'HGCATAPULTSERVERPIPE', os.devnull
1476 'HGCATAPULTSERVERPIPE', os.devnull
1472 )
1477 )
1473
1478
1474 extraextensions = []
1479 extraextensions = []
1475 for opt in self._extraconfigopts:
1480 for opt in self._extraconfigopts:
1476 section, key = opt.split('.', 1)
1481 section, key = opt.split('.', 1)
1477 if section != 'extensions':
1482 if section != 'extensions':
1478 continue
1483 continue
1479 name = key.split('=', 1)[0]
1484 name = key.split('=', 1)[0]
1480 extraextensions.append(name)
1485 extraextensions.append(name)
1481
1486
1482 if extraextensions:
1487 if extraextensions:
1483 env['HGTESTEXTRAEXTENSIONS'] = ' '.join(extraextensions)
1488 env['HGTESTEXTRAEXTENSIONS'] = ' '.join(extraextensions)
1484
1489
1485 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1490 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1486 # IP addresses.
1491 # IP addresses.
1487 env['LOCALIP'] = _bytes2sys(self._localip())
1492 env['LOCALIP'] = _bytes2sys(self._localip())
1488
1493
1489 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1494 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1490 # but this is needed for testing python instances like dummyssh,
1495 # but this is needed for testing python instances like dummyssh,
1491 # dummysmtpd.py, and dumbhttp.py.
1496 # dummysmtpd.py, and dumbhttp.py.
1492 if PYTHON3 and WINDOWS:
1497 if PYTHON3 and WINDOWS:
1493 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1498 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1494
1499
1495 # Modified HOME in test environment can confuse Rust tools. So set
1500 # Modified HOME in test environment can confuse Rust tools. So set
1496 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1501 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1497 # present and these variables aren't already defined.
1502 # present and these variables aren't already defined.
1498 cargo_home_path = os.path.expanduser('~/.cargo')
1503 cargo_home_path = os.path.expanduser('~/.cargo')
1499 rustup_home_path = os.path.expanduser('~/.rustup')
1504 rustup_home_path = os.path.expanduser('~/.rustup')
1500
1505
1501 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1506 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1502 env['CARGO_HOME'] = cargo_home_path
1507 env['CARGO_HOME'] = cargo_home_path
1503 if (
1508 if (
1504 os.path.exists(rustup_home_path)
1509 os.path.exists(rustup_home_path)
1505 and b'RUSTUP_HOME' not in osenvironb
1510 and b'RUSTUP_HOME' not in osenvironb
1506 ):
1511 ):
1507 env['RUSTUP_HOME'] = rustup_home_path
1512 env['RUSTUP_HOME'] = rustup_home_path
1508
1513
1509 # Reset some environment variables to well-known values so that
1514 # Reset some environment variables to well-known values so that
1510 # the tests produce repeatable output.
1515 # the tests produce repeatable output.
1511 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1516 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1512 env['TZ'] = 'GMT'
1517 env['TZ'] = 'GMT'
1513 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1518 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1514 env['COLUMNS'] = '80'
1519 env['COLUMNS'] = '80'
1515 env['TERM'] = 'xterm'
1520 env['TERM'] = 'xterm'
1516
1521
1517 dropped = [
1522 dropped = [
1518 'CDPATH',
1523 'CDPATH',
1519 'CHGDEBUG',
1524 'CHGDEBUG',
1520 'EDITOR',
1525 'EDITOR',
1521 'GREP_OPTIONS',
1526 'GREP_OPTIONS',
1522 'HG',
1527 'HG',
1523 'HGMERGE',
1528 'HGMERGE',
1524 'HGPLAIN',
1529 'HGPLAIN',
1525 'HGPLAINEXCEPT',
1530 'HGPLAINEXCEPT',
1526 'HGPROF',
1531 'HGPROF',
1527 'http_proxy',
1532 'http_proxy',
1528 'no_proxy',
1533 'no_proxy',
1529 'NO_PROXY',
1534 'NO_PROXY',
1530 'PAGER',
1535 'PAGER',
1531 'VISUAL',
1536 'VISUAL',
1532 ]
1537 ]
1533
1538
1534 for k in dropped:
1539 for k in dropped:
1535 if k in env:
1540 if k in env:
1536 del env[k]
1541 del env[k]
1537
1542
1538 # unset env related to hooks
1543 # unset env related to hooks
1539 for k in list(env):
1544 for k in list(env):
1540 if k.startswith('HG_'):
1545 if k.startswith('HG_'):
1541 del env[k]
1546 del env[k]
1542
1547
1543 if self._usechg:
1548 if self._usechg:
1544 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1549 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1545 if self._chgdebug:
1550 if self._chgdebug:
1546 env['CHGDEBUG'] = 'true'
1551 env['CHGDEBUG'] = 'true'
1547
1552
1548 return env
1553 return env
1549
1554
1550 def _createhgrc(self, path):
1555 def _createhgrc(self, path):
1551 """Create an hgrc file for this test."""
1556 """Create an hgrc file for this test."""
1552 with open(path, 'wb') as hgrc:
1557 with open(path, 'wb') as hgrc:
1553 hgrc.write(b'[ui]\n')
1558 hgrc.write(b'[ui]\n')
1554 hgrc.write(b'slash = True\n')
1559 hgrc.write(b'slash = True\n')
1555 hgrc.write(b'interactive = False\n')
1560 hgrc.write(b'interactive = False\n')
1556 hgrc.write(b'detailed-exit-code = True\n')
1561 hgrc.write(b'detailed-exit-code = True\n')
1557 hgrc.write(b'merge = internal:merge\n')
1562 hgrc.write(b'merge = internal:merge\n')
1558 hgrc.write(b'mergemarkers = detailed\n')
1563 hgrc.write(b'mergemarkers = detailed\n')
1559 hgrc.write(b'promptecho = True\n')
1564 hgrc.write(b'promptecho = True\n')
1560 dummyssh = os.path.join(self._testdir, b'dummyssh')
1565 dummyssh = os.path.join(self._testdir, b'dummyssh')
1561 hgrc.write(b'ssh = "%s" "%s"\n' % (PYTHON, dummyssh))
1566 hgrc.write(b'ssh = "%s" "%s"\n' % (PYTHON, dummyssh))
1562 hgrc.write(b'timeout.warn=15\n')
1567 hgrc.write(b'timeout.warn=15\n')
1563 hgrc.write(b'[chgserver]\n')
1568 hgrc.write(b'[chgserver]\n')
1564 hgrc.write(b'idletimeout=60\n')
1569 hgrc.write(b'idletimeout=60\n')
1565 hgrc.write(b'[defaults]\n')
1570 hgrc.write(b'[defaults]\n')
1566 hgrc.write(b'[devel]\n')
1571 hgrc.write(b'[devel]\n')
1567 hgrc.write(b'all-warnings = true\n')
1572 hgrc.write(b'all-warnings = true\n')
1568 hgrc.write(b'default-date = 0 0\n')
1573 hgrc.write(b'default-date = 0 0\n')
1569 hgrc.write(b'[largefiles]\n')
1574 hgrc.write(b'[largefiles]\n')
1570 hgrc.write(
1575 hgrc.write(
1571 b'usercache = %s\n'
1576 b'usercache = %s\n'
1572 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1577 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1573 )
1578 )
1574 hgrc.write(b'[lfs]\n')
1579 hgrc.write(b'[lfs]\n')
1575 hgrc.write(
1580 hgrc.write(
1576 b'usercache = %s\n'
1581 b'usercache = %s\n'
1577 % (os.path.join(self._testtmp, b'.cache/lfs'))
1582 % (os.path.join(self._testtmp, b'.cache/lfs'))
1578 )
1583 )
1579 hgrc.write(b'[web]\n')
1584 hgrc.write(b'[web]\n')
1580 hgrc.write(b'address = localhost\n')
1585 hgrc.write(b'address = localhost\n')
1581 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1586 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1582 hgrc.write(b'server-header = testing stub value\n')
1587 hgrc.write(b'server-header = testing stub value\n')
1583
1588
1584 for opt in self._extraconfigopts:
1589 for opt in self._extraconfigopts:
1585 section, key = _sys2bytes(opt).split(b'.', 1)
1590 section, key = _sys2bytes(opt).split(b'.', 1)
1586 assert b'=' in key, (
1591 assert b'=' in key, (
1587 'extra config opt %s must ' 'have an = for assignment' % opt
1592 'extra config opt %s must ' 'have an = for assignment' % opt
1588 )
1593 )
1589 hgrc.write(b'[%s]\n%s\n' % (section, key))
1594 hgrc.write(b'[%s]\n%s\n' % (section, key))
1590
1595
1591 def fail(self, msg):
1596 def fail(self, msg):
1592 # unittest differentiates between errored and failed.
1597 # unittest differentiates between errored and failed.
1593 # Failed is denoted by AssertionError (by default at least).
1598 # Failed is denoted by AssertionError (by default at least).
1594 raise AssertionError(msg)
1599 raise AssertionError(msg)
1595
1600
1596 def _runcommand(self, cmd, env, normalizenewlines=False):
1601 def _runcommand(self, cmd, env, normalizenewlines=False):
1597 """Run command in a sub-process, capturing the output (stdout and
1602 """Run command in a sub-process, capturing the output (stdout and
1598 stderr).
1603 stderr).
1599
1604
1600 Return a tuple (exitcode, output). output is None in debug mode.
1605 Return a tuple (exitcode, output). output is None in debug mode.
1601 """
1606 """
1602 if self._debug:
1607 if self._debug:
1603 proc = subprocess.Popen(
1608 proc = subprocess.Popen(
1604 _bytes2sys(cmd),
1609 _bytes2sys(cmd),
1605 shell=True,
1610 shell=True,
1606 close_fds=closefds,
1611 close_fds=closefds,
1607 cwd=_bytes2sys(self._testtmp),
1612 cwd=_bytes2sys(self._testtmp),
1608 env=env,
1613 env=env,
1609 )
1614 )
1610 ret = proc.wait()
1615 ret = proc.wait()
1611 return (ret, None)
1616 return (ret, None)
1612
1617
1613 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1618 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1614
1619
1615 def cleanup():
1620 def cleanup():
1616 terminate(proc)
1621 terminate(proc)
1617 ret = proc.wait()
1622 ret = proc.wait()
1618 if ret == 0:
1623 if ret == 0:
1619 ret = signal.SIGTERM << 8
1624 ret = signal.SIGTERM << 8
1620 killdaemons(env['DAEMON_PIDS'])
1625 killdaemons(env['DAEMON_PIDS'])
1621 return ret
1626 return ret
1622
1627
1623 proc.tochild.close()
1628 proc.tochild.close()
1624
1629
1625 try:
1630 try:
1626 output = proc.fromchild.read()
1631 output = proc.fromchild.read()
1627 except KeyboardInterrupt:
1632 except KeyboardInterrupt:
1628 vlog('# Handling keyboard interrupt')
1633 vlog('# Handling keyboard interrupt')
1629 cleanup()
1634 cleanup()
1630 raise
1635 raise
1631
1636
1632 ret = proc.wait()
1637 ret = proc.wait()
1633 if wifexited(ret):
1638 if wifexited(ret):
1634 ret = os.WEXITSTATUS(ret)
1639 ret = os.WEXITSTATUS(ret)
1635
1640
1636 if proc.timeout:
1641 if proc.timeout:
1637 ret = 'timeout'
1642 ret = 'timeout'
1638
1643
1639 if ret:
1644 if ret:
1640 killdaemons(env['DAEMON_PIDS'])
1645 killdaemons(env['DAEMON_PIDS'])
1641
1646
1642 for s, r in self._getreplacements():
1647 for s, r in self._getreplacements():
1643 output = re.sub(s, r, output)
1648 output = re.sub(s, r, output)
1644
1649
1645 if normalizenewlines:
1650 if normalizenewlines:
1646 output = output.replace(b'\r\n', b'\n')
1651 output = output.replace(b'\r\n', b'\n')
1647
1652
1648 return ret, output.splitlines(True)
1653 return ret, output.splitlines(True)
1649
1654
1650
1655
1651 class PythonTest(Test):
1656 class PythonTest(Test):
1652 """A Python-based test."""
1657 """A Python-based test."""
1653
1658
1654 @property
1659 @property
1655 def refpath(self):
1660 def refpath(self):
1656 return os.path.join(self._testdir, b'%s.out' % self.bname)
1661 return os.path.join(self._testdir, b'%s.out' % self.bname)
1657
1662
1658 def _run(self, env):
1663 def _run(self, env):
1659 # Quote the python(3) executable for Windows
1664 # Quote the python(3) executable for Windows
1660 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1665 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1661 vlog("# Running", cmd.decode("utf-8"))
1666 vlog("# Running", cmd.decode("utf-8"))
1662 result = self._runcommand(cmd, env, normalizenewlines=WINDOWS)
1667 result = self._runcommand(cmd, env, normalizenewlines=WINDOWS)
1663 if self._aborted:
1668 if self._aborted:
1664 raise KeyboardInterrupt()
1669 raise KeyboardInterrupt()
1665
1670
1666 return result
1671 return result
1667
1672
1668
1673
1669 # Some glob patterns apply only in some circumstances, so the script
1674 # Some glob patterns apply only in some circumstances, so the script
1670 # might want to remove (glob) annotations that otherwise should be
1675 # might want to remove (glob) annotations that otherwise should be
1671 # retained.
1676 # retained.
1672 checkcodeglobpats = [
1677 checkcodeglobpats = [
1673 # On Windows it looks like \ doesn't require a (glob), but we know
1678 # On Windows it looks like \ doesn't require a (glob), but we know
1674 # better.
1679 # better.
1675 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1680 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1676 re.compile(br'^moving \S+/.*[^)]$'),
1681 re.compile(br'^moving \S+/.*[^)]$'),
1677 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1682 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1678 # Not all platforms have 127.0.0.1 as loopback (though most do),
1683 # Not all platforms have 127.0.0.1 as loopback (though most do),
1679 # so we always glob that too.
1684 # so we always glob that too.
1680 re.compile(br'.*\$LOCALIP.*$'),
1685 re.compile(br'.*\$LOCALIP.*$'),
1681 ]
1686 ]
1682
1687
1683 bchr = chr
1688 bchr = chr
1684 if PYTHON3:
1689 if PYTHON3:
1685 bchr = lambda x: bytes([x])
1690 bchr = lambda x: bytes([x])
1686
1691
1687 WARN_UNDEFINED = 1
1692 WARN_UNDEFINED = 1
1688 WARN_YES = 2
1693 WARN_YES = 2
1689 WARN_NO = 3
1694 WARN_NO = 3
1690
1695
1691 MARK_OPTIONAL = b" (?)\n"
1696 MARK_OPTIONAL = b" (?)\n"
1692
1697
1693
1698
1694 def isoptional(line):
1699 def isoptional(line):
1695 return line.endswith(MARK_OPTIONAL)
1700 return line.endswith(MARK_OPTIONAL)
1696
1701
1697
1702
1698 class TTest(Test):
1703 class TTest(Test):
1699 """A "t test" is a test backed by a .t file."""
1704 """A "t test" is a test backed by a .t file."""
1700
1705
1701 SKIPPED_PREFIX = b'skipped: '
1706 SKIPPED_PREFIX = b'skipped: '
1702 FAILED_PREFIX = b'hghave check failed: '
1707 FAILED_PREFIX = b'hghave check failed: '
1703 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1708 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1704
1709
1705 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1710 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1706 ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)}
1711 ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)}
1707 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1712 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1708
1713
1709 def __init__(self, path, *args, **kwds):
1714 def __init__(self, path, *args, **kwds):
1710 # accept an extra "case" parameter
1715 # accept an extra "case" parameter
1711 case = kwds.pop('case', [])
1716 case = kwds.pop('case', [])
1712 self._case = case
1717 self._case = case
1713 self._allcases = {x for y in parsettestcases(path) for x in y}
1718 self._allcases = {x for y in parsettestcases(path) for x in y}
1714 super(TTest, self).__init__(path, *args, **kwds)
1719 super(TTest, self).__init__(path, *args, **kwds)
1715 if case:
1720 if case:
1716 casepath = b'#'.join(case)
1721 casepath = b'#'.join(case)
1717 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1722 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1718 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1723 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1719 self._tmpname += b'-%s' % casepath.replace(b'#', b'-')
1724 self._tmpname += b'-%s' % casepath.replace(b'#', b'-')
1720 self._have = {}
1725 self._have = {}
1721
1726
1722 @property
1727 @property
1723 def refpath(self):
1728 def refpath(self):
1724 return os.path.join(self._testdir, self.bname)
1729 return os.path.join(self._testdir, self.bname)
1725
1730
1726 def _run(self, env):
1731 def _run(self, env):
1727 with open(self.path, 'rb') as f:
1732 with open(self.path, 'rb') as f:
1728 lines = f.readlines()
1733 lines = f.readlines()
1729
1734
1730 # .t file is both reference output and the test input, keep reference
1735 # .t file is both reference output and the test input, keep reference
1731 # output updated with the the test input. This avoids some race
1736 # output updated with the the test input. This avoids some race
1732 # conditions where the reference output does not match the actual test.
1737 # conditions where the reference output does not match the actual test.
1733 if self._refout is not None:
1738 if self._refout is not None:
1734 self._refout = lines
1739 self._refout = lines
1735
1740
1736 salt, script, after, expected = self._parsetest(lines)
1741 salt, script, after, expected = self._parsetest(lines)
1737
1742
1738 # Write out the generated script.
1743 # Write out the generated script.
1739 fname = b'%s.sh' % self._testtmp
1744 fname = b'%s.sh' % self._testtmp
1740 with open(fname, 'wb') as f:
1745 with open(fname, 'wb') as f:
1741 for l in script:
1746 for l in script:
1742 f.write(l)
1747 f.write(l)
1743
1748
1744 cmd = b'%s "%s"' % (self._shell, fname)
1749 cmd = b'%s "%s"' % (self._shell, fname)
1745 vlog("# Running", cmd.decode("utf-8"))
1750 vlog("# Running", cmd.decode("utf-8"))
1746
1751
1747 exitcode, output = self._runcommand(cmd, env)
1752 exitcode, output = self._runcommand(cmd, env)
1748
1753
1749 if self._aborted:
1754 if self._aborted:
1750 raise KeyboardInterrupt()
1755 raise KeyboardInterrupt()
1751
1756
1752 # Do not merge output if skipped. Return hghave message instead.
1757 # Do not merge output if skipped. Return hghave message instead.
1753 # Similarly, with --debug, output is None.
1758 # Similarly, with --debug, output is None.
1754 if exitcode == self.SKIPPED_STATUS or output is None:
1759 if exitcode == self.SKIPPED_STATUS or output is None:
1755 return exitcode, output
1760 return exitcode, output
1756
1761
1757 return self._processoutput(exitcode, output, salt, after, expected)
1762 return self._processoutput(exitcode, output, salt, after, expected)
1758
1763
1759 def _hghave(self, reqs):
1764 def _hghave(self, reqs):
1760 allreqs = b' '.join(reqs)
1765 allreqs = b' '.join(reqs)
1761
1766
1762 self._detectslow(reqs)
1767 self._detectslow(reqs)
1763
1768
1764 if allreqs in self._have:
1769 if allreqs in self._have:
1765 return self._have.get(allreqs)
1770 return self._have.get(allreqs)
1766
1771
1767 # TODO do something smarter when all other uses of hghave are gone.
1772 # TODO do something smarter when all other uses of hghave are gone.
1768 runtestdir = osenvironb[b'RUNTESTDIR']
1773 runtestdir = osenvironb[b'RUNTESTDIR']
1769 tdir = runtestdir.replace(b'\\', b'/')
1774 tdir = runtestdir.replace(b'\\', b'/')
1770 proc = Popen4(
1775 proc = Popen4(
1771 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1776 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1772 self._testtmp,
1777 self._testtmp,
1773 0,
1778 0,
1774 self._getenv(),
1779 self._getenv(),
1775 )
1780 )
1776 stdout, stderr = proc.communicate()
1781 stdout, stderr = proc.communicate()
1777 ret = proc.wait()
1782 ret = proc.wait()
1778 if wifexited(ret):
1783 if wifexited(ret):
1779 ret = os.WEXITSTATUS(ret)
1784 ret = os.WEXITSTATUS(ret)
1780 if ret == 2:
1785 if ret == 2:
1781 print(stdout.decode('utf-8'))
1786 print(stdout.decode('utf-8'))
1782 sys.exit(1)
1787 sys.exit(1)
1783
1788
1784 if ret != 0:
1789 if ret != 0:
1785 self._have[allreqs] = (False, stdout)
1790 self._have[allreqs] = (False, stdout)
1786 return False, stdout
1791 return False, stdout
1787
1792
1788 self._have[allreqs] = (True, None)
1793 self._have[allreqs] = (True, None)
1789 return True, None
1794 return True, None
1790
1795
1791 def _detectslow(self, reqs):
1796 def _detectslow(self, reqs):
1792 """update the timeout of slow test when appropriate"""
1797 """update the timeout of slow test when appropriate"""
1793 if b'slow' in reqs:
1798 if b'slow' in reqs:
1794 self._timeout = self._slowtimeout
1799 self._timeout = self._slowtimeout
1795
1800
1796 def _iftest(self, args):
1801 def _iftest(self, args):
1797 # implements "#if"
1802 # implements "#if"
1798 reqs = []
1803 reqs = []
1799 for arg in args:
1804 for arg in args:
1800 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1805 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1801 if arg[3:] in self._case:
1806 if arg[3:] in self._case:
1802 return False
1807 return False
1803 elif arg in self._allcases:
1808 elif arg in self._allcases:
1804 if arg not in self._case:
1809 if arg not in self._case:
1805 return False
1810 return False
1806 else:
1811 else:
1807 reqs.append(arg)
1812 reqs.append(arg)
1808 self._detectslow(reqs)
1813 self._detectslow(reqs)
1809 return self._hghave(reqs)[0]
1814 return self._hghave(reqs)[0]
1810
1815
1811 def _parsetest(self, lines):
1816 def _parsetest(self, lines):
1812 # We generate a shell script which outputs unique markers to line
1817 # We generate a shell script which outputs unique markers to line
1813 # up script results with our source. These markers include input
1818 # up script results with our source. These markers include input
1814 # line number and the last return code.
1819 # line number and the last return code.
1815 salt = b"SALT%d" % time.time()
1820 salt = b"SALT%d" % time.time()
1816
1821
1817 def addsalt(line, inpython):
1822 def addsalt(line, inpython):
1818 if inpython:
1823 if inpython:
1819 script.append(b'%s %d 0\n' % (salt, line))
1824 script.append(b'%s %d 0\n' % (salt, line))
1820 else:
1825 else:
1821 script.append(b'echo %s %d $?\n' % (salt, line))
1826 script.append(b'echo %s %d $?\n' % (salt, line))
1822
1827
1823 activetrace = []
1828 activetrace = []
1824 session = str(uuid.uuid4())
1829 session = str(uuid.uuid4())
1825 if PYTHON3:
1830 if PYTHON3:
1826 session = session.encode('ascii')
1831 session = session.encode('ascii')
1827 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1832 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1828 'HGCATAPULTSERVERPIPE'
1833 'HGCATAPULTSERVERPIPE'
1829 )
1834 )
1830
1835
1831 def toggletrace(cmd=None):
1836 def toggletrace(cmd=None):
1832 if not hgcatapult or hgcatapult == os.devnull:
1837 if not hgcatapult or hgcatapult == os.devnull:
1833 return
1838 return
1834
1839
1835 if activetrace:
1840 if activetrace:
1836 script.append(
1841 script.append(
1837 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1842 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1838 % (session, activetrace[0])
1843 % (session, activetrace[0])
1839 )
1844 )
1840 if cmd is None:
1845 if cmd is None:
1841 return
1846 return
1842
1847
1843 if isinstance(cmd, str):
1848 if isinstance(cmd, str):
1844 quoted = shellquote(cmd.strip())
1849 quoted = shellquote(cmd.strip())
1845 else:
1850 else:
1846 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1851 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1847 quoted = quoted.replace(b'\\', b'\\\\')
1852 quoted = quoted.replace(b'\\', b'\\\\')
1848 script.append(
1853 script.append(
1849 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1854 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1850 % (session, quoted)
1855 % (session, quoted)
1851 )
1856 )
1852 activetrace[0:] = [quoted]
1857 activetrace[0:] = [quoted]
1853
1858
1854 script = []
1859 script = []
1855
1860
1856 # After we run the shell script, we re-unify the script output
1861 # After we run the shell script, we re-unify the script output
1857 # with non-active parts of the source, with synchronization by our
1862 # with non-active parts of the source, with synchronization by our
1858 # SALT line number markers. The after table contains the non-active
1863 # SALT line number markers. The after table contains the non-active
1859 # components, ordered by line number.
1864 # components, ordered by line number.
1860 after = {}
1865 after = {}
1861
1866
1862 # Expected shell script output.
1867 # Expected shell script output.
1863 expected = {}
1868 expected = {}
1864
1869
1865 pos = prepos = -1
1870 pos = prepos = -1
1866
1871
1867 # True or False when in a true or false conditional section
1872 # True or False when in a true or false conditional section
1868 skipping = None
1873 skipping = None
1869
1874
1870 # We keep track of whether or not we're in a Python block so we
1875 # We keep track of whether or not we're in a Python block so we
1871 # can generate the surrounding doctest magic.
1876 # can generate the surrounding doctest magic.
1872 inpython = False
1877 inpython = False
1873
1878
1874 if self._debug:
1879 if self._debug:
1875 script.append(b'set -x\n')
1880 script.append(b'set -x\n')
1876 if os.getenv('MSYSTEM'):
1881 if os.getenv('MSYSTEM'):
1877 script.append(b'alias pwd="pwd -W"\n')
1882 script.append(b'alias pwd="pwd -W"\n')
1878
1883
1879 if hgcatapult and hgcatapult != os.devnull:
1884 if hgcatapult and hgcatapult != os.devnull:
1880 if PYTHON3:
1885 if PYTHON3:
1881 hgcatapult = hgcatapult.encode('utf8')
1886 hgcatapult = hgcatapult.encode('utf8')
1882 cataname = self.name.encode('utf8')
1887 cataname = self.name.encode('utf8')
1883 else:
1888 else:
1884 cataname = self.name
1889 cataname = self.name
1885
1890
1886 # Kludge: use a while loop to keep the pipe from getting
1891 # Kludge: use a while loop to keep the pipe from getting
1887 # closed by our echo commands. The still-running file gets
1892 # closed by our echo commands. The still-running file gets
1888 # reaped at the end of the script, which causes the while
1893 # reaped at the end of the script, which causes the while
1889 # loop to exit and closes the pipe. Sigh.
1894 # loop to exit and closes the pipe. Sigh.
1890 script.append(
1895 script.append(
1891 b'rtendtracing() {\n'
1896 b'rtendtracing() {\n'
1892 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1897 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1893 b' rm -f "$TESTTMP/.still-running"\n'
1898 b' rm -f "$TESTTMP/.still-running"\n'
1894 b'}\n'
1899 b'}\n'
1895 b'trap "rtendtracing" 0\n'
1900 b'trap "rtendtracing" 0\n'
1896 b'touch "$TESTTMP/.still-running"\n'
1901 b'touch "$TESTTMP/.still-running"\n'
1897 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1902 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1898 b'> %(catapult)s &\n'
1903 b'> %(catapult)s &\n'
1899 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1904 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1900 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1905 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1901 % {
1906 % {
1902 b'name': cataname,
1907 b'name': cataname,
1903 b'session': session,
1908 b'session': session,
1904 b'catapult': hgcatapult,
1909 b'catapult': hgcatapult,
1905 }
1910 }
1906 )
1911 )
1907
1912
1908 if self._case:
1913 if self._case:
1909 casestr = b'#'.join(self._case)
1914 casestr = b'#'.join(self._case)
1910 if isinstance(casestr, str):
1915 if isinstance(casestr, str):
1911 quoted = shellquote(casestr)
1916 quoted = shellquote(casestr)
1912 else:
1917 else:
1913 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1918 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1914 script.append(b'TESTCASE=%s\n' % quoted)
1919 script.append(b'TESTCASE=%s\n' % quoted)
1915 script.append(b'export TESTCASE\n')
1920 script.append(b'export TESTCASE\n')
1916
1921
1917 n = 0
1922 n = 0
1918 for n, l in enumerate(lines):
1923 for n, l in enumerate(lines):
1919 if not l.endswith(b'\n'):
1924 if not l.endswith(b'\n'):
1920 l += b'\n'
1925 l += b'\n'
1921 if l.startswith(b'#require'):
1926 if l.startswith(b'#require'):
1922 lsplit = l.split()
1927 lsplit = l.split()
1923 if len(lsplit) < 2 or lsplit[0] != b'#require':
1928 if len(lsplit) < 2 or lsplit[0] != b'#require':
1924 after.setdefault(pos, []).append(
1929 after.setdefault(pos, []).append(
1925 b' !!! invalid #require\n'
1930 b' !!! invalid #require\n'
1926 )
1931 )
1927 if not skipping:
1932 if not skipping:
1928 haveresult, message = self._hghave(lsplit[1:])
1933 haveresult, message = self._hghave(lsplit[1:])
1929 if not haveresult:
1934 if not haveresult:
1930 script = [b'echo "%s"\nexit 80\n' % message]
1935 script = [b'echo "%s"\nexit 80\n' % message]
1931 break
1936 break
1932 after.setdefault(pos, []).append(l)
1937 after.setdefault(pos, []).append(l)
1933 elif l.startswith(b'#if'):
1938 elif l.startswith(b'#if'):
1934 lsplit = l.split()
1939 lsplit = l.split()
1935 if len(lsplit) < 2 or lsplit[0] != b'#if':
1940 if len(lsplit) < 2 or lsplit[0] != b'#if':
1936 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1941 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1937 if skipping is not None:
1942 if skipping is not None:
1938 after.setdefault(pos, []).append(b' !!! nested #if\n')
1943 after.setdefault(pos, []).append(b' !!! nested #if\n')
1939 skipping = not self._iftest(lsplit[1:])
1944 skipping = not self._iftest(lsplit[1:])
1940 after.setdefault(pos, []).append(l)
1945 after.setdefault(pos, []).append(l)
1941 elif l.startswith(b'#else'):
1946 elif l.startswith(b'#else'):
1942 if skipping is None:
1947 if skipping is None:
1943 after.setdefault(pos, []).append(b' !!! missing #if\n')
1948 after.setdefault(pos, []).append(b' !!! missing #if\n')
1944 skipping = not skipping
1949 skipping = not skipping
1945 after.setdefault(pos, []).append(l)
1950 after.setdefault(pos, []).append(l)
1946 elif l.startswith(b'#endif'):
1951 elif l.startswith(b'#endif'):
1947 if skipping is None:
1952 if skipping is None:
1948 after.setdefault(pos, []).append(b' !!! missing #if\n')
1953 after.setdefault(pos, []).append(b' !!! missing #if\n')
1949 skipping = None
1954 skipping = None
1950 after.setdefault(pos, []).append(l)
1955 after.setdefault(pos, []).append(l)
1951 elif skipping:
1956 elif skipping:
1952 after.setdefault(pos, []).append(l)
1957 after.setdefault(pos, []).append(l)
1953 elif l.startswith(b' >>> '): # python inlines
1958 elif l.startswith(b' >>> '): # python inlines
1954 after.setdefault(pos, []).append(l)
1959 after.setdefault(pos, []).append(l)
1955 prepos = pos
1960 prepos = pos
1956 pos = n
1961 pos = n
1957 if not inpython:
1962 if not inpython:
1958 # We've just entered a Python block. Add the header.
1963 # We've just entered a Python block. Add the header.
1959 inpython = True
1964 inpython = True
1960 addsalt(prepos, False) # Make sure we report the exit code.
1965 addsalt(prepos, False) # Make sure we report the exit code.
1961 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1966 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1962 addsalt(n, True)
1967 addsalt(n, True)
1963 script.append(l[2:])
1968 script.append(l[2:])
1964 elif l.startswith(b' ... '): # python inlines
1969 elif l.startswith(b' ... '): # python inlines
1965 after.setdefault(prepos, []).append(l)
1970 after.setdefault(prepos, []).append(l)
1966 script.append(l[2:])
1971 script.append(l[2:])
1967 elif l.startswith(b' $ '): # commands
1972 elif l.startswith(b' $ '): # commands
1968 if inpython:
1973 if inpython:
1969 script.append(b'EOF\n')
1974 script.append(b'EOF\n')
1970 inpython = False
1975 inpython = False
1971 after.setdefault(pos, []).append(l)
1976 after.setdefault(pos, []).append(l)
1972 prepos = pos
1977 prepos = pos
1973 pos = n
1978 pos = n
1974 addsalt(n, False)
1979 addsalt(n, False)
1975 rawcmd = l[4:]
1980 rawcmd = l[4:]
1976 cmd = rawcmd.split()
1981 cmd = rawcmd.split()
1977 toggletrace(rawcmd)
1982 toggletrace(rawcmd)
1978 if len(cmd) == 2 and cmd[0] == b'cd':
1983 if len(cmd) == 2 and cmd[0] == b'cd':
1979 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1984 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1980 script.append(rawcmd)
1985 script.append(rawcmd)
1981 elif l.startswith(b' > '): # continuations
1986 elif l.startswith(b' > '): # continuations
1982 after.setdefault(prepos, []).append(l)
1987 after.setdefault(prepos, []).append(l)
1983 script.append(l[4:])
1988 script.append(l[4:])
1984 elif l.startswith(b' '): # results
1989 elif l.startswith(b' '): # results
1985 # Queue up a list of expected results.
1990 # Queue up a list of expected results.
1986 expected.setdefault(pos, []).append(l[2:])
1991 expected.setdefault(pos, []).append(l[2:])
1987 else:
1992 else:
1988 if inpython:
1993 if inpython:
1989 script.append(b'EOF\n')
1994 script.append(b'EOF\n')
1990 inpython = False
1995 inpython = False
1991 # Non-command/result. Queue up for merged output.
1996 # Non-command/result. Queue up for merged output.
1992 after.setdefault(pos, []).append(l)
1997 after.setdefault(pos, []).append(l)
1993
1998
1994 if inpython:
1999 if inpython:
1995 script.append(b'EOF\n')
2000 script.append(b'EOF\n')
1996 if skipping is not None:
2001 if skipping is not None:
1997 after.setdefault(pos, []).append(b' !!! missing #endif\n')
2002 after.setdefault(pos, []).append(b' !!! missing #endif\n')
1998 addsalt(n + 1, False)
2003 addsalt(n + 1, False)
1999 # Need to end any current per-command trace
2004 # Need to end any current per-command trace
2000 if activetrace:
2005 if activetrace:
2001 toggletrace()
2006 toggletrace()
2002 return salt, script, after, expected
2007 return salt, script, after, expected
2003
2008
2004 def _processoutput(self, exitcode, output, salt, after, expected):
2009 def _processoutput(self, exitcode, output, salt, after, expected):
2005 # Merge the script output back into a unified test.
2010 # Merge the script output back into a unified test.
2006 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
2011 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
2007 if exitcode != 0:
2012 if exitcode != 0:
2008 warnonly = WARN_NO
2013 warnonly = WARN_NO
2009
2014
2010 pos = -1
2015 pos = -1
2011 postout = []
2016 postout = []
2012 for out_rawline in output:
2017 for out_rawline in output:
2013 out_line, cmd_line = out_rawline, None
2018 out_line, cmd_line = out_rawline, None
2014 if salt in out_rawline:
2019 if salt in out_rawline:
2015 out_line, cmd_line = out_rawline.split(salt, 1)
2020 out_line, cmd_line = out_rawline.split(salt, 1)
2016
2021
2017 pos, postout, warnonly = self._process_out_line(
2022 pos, postout, warnonly = self._process_out_line(
2018 out_line, pos, postout, expected, warnonly
2023 out_line, pos, postout, expected, warnonly
2019 )
2024 )
2020 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
2025 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
2021
2026
2022 if pos in after:
2027 if pos in after:
2023 postout += after.pop(pos)
2028 postout += after.pop(pos)
2024
2029
2025 if warnonly == WARN_YES:
2030 if warnonly == WARN_YES:
2026 exitcode = False # Set exitcode to warned.
2031 exitcode = False # Set exitcode to warned.
2027
2032
2028 return exitcode, postout
2033 return exitcode, postout
2029
2034
2030 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
2035 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
2031 while out_line:
2036 while out_line:
2032 if not out_line.endswith(b'\n'):
2037 if not out_line.endswith(b'\n'):
2033 out_line += b' (no-eol)\n'
2038 out_line += b' (no-eol)\n'
2034
2039
2035 # Find the expected output at the current position.
2040 # Find the expected output at the current position.
2036 els = [None]
2041 els = [None]
2037 if expected.get(pos, None):
2042 if expected.get(pos, None):
2038 els = expected[pos]
2043 els = expected[pos]
2039
2044
2040 optional = []
2045 optional = []
2041 for i, el in enumerate(els):
2046 for i, el in enumerate(els):
2042 r = False
2047 r = False
2043 if el:
2048 if el:
2044 r, exact = self.linematch(el, out_line)
2049 r, exact = self.linematch(el, out_line)
2045 if isinstance(r, str):
2050 if isinstance(r, str):
2046 if r == '-glob':
2051 if r == '-glob':
2047 out_line = ''.join(el.rsplit(' (glob)', 1))
2052 out_line = ''.join(el.rsplit(' (glob)', 1))
2048 r = '' # Warn only this line.
2053 r = '' # Warn only this line.
2049 elif r == "retry":
2054 elif r == "retry":
2050 postout.append(b' ' + el)
2055 postout.append(b' ' + el)
2051 else:
2056 else:
2052 log('\ninfo, unknown linematch result: %r\n' % r)
2057 log('\ninfo, unknown linematch result: %r\n' % r)
2053 r = False
2058 r = False
2054 if r:
2059 if r:
2055 els.pop(i)
2060 els.pop(i)
2056 break
2061 break
2057 if el:
2062 if el:
2058 if isoptional(el):
2063 if isoptional(el):
2059 optional.append(i)
2064 optional.append(i)
2060 else:
2065 else:
2061 m = optline.match(el)
2066 m = optline.match(el)
2062 if m:
2067 if m:
2063 conditions = [c for c in m.group(2).split(b' ')]
2068 conditions = [c for c in m.group(2).split(b' ')]
2064
2069
2065 if not self._iftest(conditions):
2070 if not self._iftest(conditions):
2066 optional.append(i)
2071 optional.append(i)
2067 if exact:
2072 if exact:
2068 # Don't allow line to be matches against a later
2073 # Don't allow line to be matches against a later
2069 # line in the output
2074 # line in the output
2070 els.pop(i)
2075 els.pop(i)
2071 break
2076 break
2072
2077
2073 if r:
2078 if r:
2074 if r == "retry":
2079 if r == "retry":
2075 continue
2080 continue
2076 # clean up any optional leftovers
2081 # clean up any optional leftovers
2077 for i in optional:
2082 for i in optional:
2078 postout.append(b' ' + els[i])
2083 postout.append(b' ' + els[i])
2079 for i in reversed(optional):
2084 for i in reversed(optional):
2080 del els[i]
2085 del els[i]
2081 postout.append(b' ' + el)
2086 postout.append(b' ' + el)
2082 else:
2087 else:
2083 if self.NEEDESCAPE(out_line):
2088 if self.NEEDESCAPE(out_line):
2084 out_line = TTest._stringescape(
2089 out_line = TTest._stringescape(
2085 b'%s (esc)\n' % out_line.rstrip(b'\n')
2090 b'%s (esc)\n' % out_line.rstrip(b'\n')
2086 )
2091 )
2087 postout.append(b' ' + out_line) # Let diff deal with it.
2092 postout.append(b' ' + out_line) # Let diff deal with it.
2088 if r != '': # If line failed.
2093 if r != '': # If line failed.
2089 warnonly = WARN_NO
2094 warnonly = WARN_NO
2090 elif warnonly == WARN_UNDEFINED:
2095 elif warnonly == WARN_UNDEFINED:
2091 warnonly = WARN_YES
2096 warnonly = WARN_YES
2092 break
2097 break
2093 else:
2098 else:
2094 # clean up any optional leftovers
2099 # clean up any optional leftovers
2095 while expected.get(pos, None):
2100 while expected.get(pos, None):
2096 el = expected[pos].pop(0)
2101 el = expected[pos].pop(0)
2097 if el:
2102 if el:
2098 if not isoptional(el):
2103 if not isoptional(el):
2099 m = optline.match(el)
2104 m = optline.match(el)
2100 if m:
2105 if m:
2101 conditions = [c for c in m.group(2).split(b' ')]
2106 conditions = [c for c in m.group(2).split(b' ')]
2102
2107
2103 if self._iftest(conditions):
2108 if self._iftest(conditions):
2104 # Don't append as optional line
2109 # Don't append as optional line
2105 continue
2110 continue
2106 else:
2111 else:
2107 continue
2112 continue
2108 postout.append(b' ' + el)
2113 postout.append(b' ' + el)
2109 return pos, postout, warnonly
2114 return pos, postout, warnonly
2110
2115
2111 def _process_cmd_line(self, cmd_line, pos, postout, after):
2116 def _process_cmd_line(self, cmd_line, pos, postout, after):
2112 """process a "command" part of a line from unified test output"""
2117 """process a "command" part of a line from unified test output"""
2113 if cmd_line:
2118 if cmd_line:
2114 # Add on last return code.
2119 # Add on last return code.
2115 ret = int(cmd_line.split()[1])
2120 ret = int(cmd_line.split()[1])
2116 if ret != 0:
2121 if ret != 0:
2117 postout.append(b' [%d]\n' % ret)
2122 postout.append(b' [%d]\n' % ret)
2118 if pos in after:
2123 if pos in after:
2119 # Merge in non-active test bits.
2124 # Merge in non-active test bits.
2120 postout += after.pop(pos)
2125 postout += after.pop(pos)
2121 pos = int(cmd_line.split()[0])
2126 pos = int(cmd_line.split()[0])
2122 return pos, postout
2127 return pos, postout
2123
2128
2124 @staticmethod
2129 @staticmethod
2125 def rematch(el, l):
2130 def rematch(el, l):
2126 try:
2131 try:
2127 # parse any flags at the beginning of the regex. Only 'i' is
2132 # parse any flags at the beginning of the regex. Only 'i' is
2128 # supported right now, but this should be easy to extend.
2133 # supported right now, but this should be easy to extend.
2129 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
2134 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
2130 flags = flags or b''
2135 flags = flags or b''
2131 el = flags + b'(?:' + el + b')'
2136 el = flags + b'(?:' + el + b')'
2132 # use \Z to ensure that the regex matches to the end of the string
2137 # use \Z to ensure that the regex matches to the end of the string
2133 if WINDOWS:
2138 if WINDOWS:
2134 return re.match(el + br'\r?\n\Z', l)
2139 return re.match(el + br'\r?\n\Z', l)
2135 return re.match(el + br'\n\Z', l)
2140 return re.match(el + br'\n\Z', l)
2136 except re.error:
2141 except re.error:
2137 # el is an invalid regex
2142 # el is an invalid regex
2138 return False
2143 return False
2139
2144
2140 @staticmethod
2145 @staticmethod
2141 def globmatch(el, l):
2146 def globmatch(el, l):
2142 # The only supported special characters are * and ? plus / which also
2147 # The only supported special characters are * and ? plus / which also
2143 # matches \ on windows. Escaping of these characters is supported.
2148 # matches \ on windows. Escaping of these characters is supported.
2144 if el + b'\n' == l:
2149 if el + b'\n' == l:
2145 if os.altsep:
2150 if os.altsep:
2146 # matching on "/" is not needed for this line
2151 # matching on "/" is not needed for this line
2147 for pat in checkcodeglobpats:
2152 for pat in checkcodeglobpats:
2148 if pat.match(el):
2153 if pat.match(el):
2149 return True
2154 return True
2150 return b'-glob'
2155 return b'-glob'
2151 return True
2156 return True
2152 el = el.replace(b'$LOCALIP', b'*')
2157 el = el.replace(b'$LOCALIP', b'*')
2153 i, n = 0, len(el)
2158 i, n = 0, len(el)
2154 res = b''
2159 res = b''
2155 while i < n:
2160 while i < n:
2156 c = el[i : i + 1]
2161 c = el[i : i + 1]
2157 i += 1
2162 i += 1
2158 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2163 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2159 res += el[i - 1 : i + 1]
2164 res += el[i - 1 : i + 1]
2160 i += 1
2165 i += 1
2161 elif c == b'*':
2166 elif c == b'*':
2162 res += b'.*'
2167 res += b'.*'
2163 elif c == b'?':
2168 elif c == b'?':
2164 res += b'.'
2169 res += b'.'
2165 elif c == b'/' and os.altsep:
2170 elif c == b'/' and os.altsep:
2166 res += b'[/\\\\]'
2171 res += b'[/\\\\]'
2167 else:
2172 else:
2168 res += re.escape(c)
2173 res += re.escape(c)
2169 return TTest.rematch(res, l)
2174 return TTest.rematch(res, l)
2170
2175
2171 def linematch(self, el, l):
2176 def linematch(self, el, l):
2172 if el == l: # perfect match (fast)
2177 if el == l: # perfect match (fast)
2173 return True, True
2178 return True, True
2174 retry = False
2179 retry = False
2175 if isoptional(el):
2180 if isoptional(el):
2176 retry = "retry"
2181 retry = "retry"
2177 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2182 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2178 else:
2183 else:
2179 m = optline.match(el)
2184 m = optline.match(el)
2180 if m:
2185 if m:
2181 conditions = [c for c in m.group(2).split(b' ')]
2186 conditions = [c for c in m.group(2).split(b' ')]
2182
2187
2183 el = m.group(1) + b"\n"
2188 el = m.group(1) + b"\n"
2184 if not self._iftest(conditions):
2189 if not self._iftest(conditions):
2185 # listed feature missing, should not match
2190 # listed feature missing, should not match
2186 return "retry", False
2191 return "retry", False
2187
2192
2188 if el.endswith(b" (esc)\n"):
2193 if el.endswith(b" (esc)\n"):
2189 if PYTHON3:
2194 if PYTHON3:
2190 el = el[:-7].decode('unicode_escape') + '\n'
2195 el = el[:-7].decode('unicode_escape') + '\n'
2191 el = el.encode('latin-1')
2196 el = el.encode('latin-1')
2192 else:
2197 else:
2193 el = el[:-7].decode('string-escape') + '\n'
2198 el = el[:-7].decode('string-escape') + '\n'
2194 if el == l or WINDOWS and el[:-1] + b'\r\n' == l:
2199 if el == l or WINDOWS and el[:-1] + b'\r\n' == l:
2195 return True, True
2200 return True, True
2196 if el.endswith(b" (re)\n"):
2201 if el.endswith(b" (re)\n"):
2197 return (TTest.rematch(el[:-6], l) or retry), False
2202 return (TTest.rematch(el[:-6], l) or retry), False
2198 if el.endswith(b" (glob)\n"):
2203 if el.endswith(b" (glob)\n"):
2199 # ignore '(glob)' added to l by 'replacements'
2204 # ignore '(glob)' added to l by 'replacements'
2200 if l.endswith(b" (glob)\n"):
2205 if l.endswith(b" (glob)\n"):
2201 l = l[:-8] + b"\n"
2206 l = l[:-8] + b"\n"
2202 return (TTest.globmatch(el[:-8], l) or retry), False
2207 return (TTest.globmatch(el[:-8], l) or retry), False
2203 if os.altsep:
2208 if os.altsep:
2204 _l = l.replace(b'\\', b'/')
2209 _l = l.replace(b'\\', b'/')
2205 if el == _l or WINDOWS and el[:-1] + b'\r\n' == _l:
2210 if el == _l or WINDOWS and el[:-1] + b'\r\n' == _l:
2206 return True, True
2211 return True, True
2207 return retry, True
2212 return retry, True
2208
2213
2209 @staticmethod
2214 @staticmethod
2210 def parsehghaveoutput(lines):
2215 def parsehghaveoutput(lines):
2211 """Parse hghave log lines.
2216 """Parse hghave log lines.
2212
2217
2213 Return tuple of lists (missing, failed):
2218 Return tuple of lists (missing, failed):
2214 * the missing/unknown features
2219 * the missing/unknown features
2215 * the features for which existence check failed"""
2220 * the features for which existence check failed"""
2216 missing = []
2221 missing = []
2217 failed = []
2222 failed = []
2218 for line in lines:
2223 for line in lines:
2219 if line.startswith(TTest.SKIPPED_PREFIX):
2224 if line.startswith(TTest.SKIPPED_PREFIX):
2220 line = line.splitlines()[0]
2225 line = line.splitlines()[0]
2221 missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :]))
2226 missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :]))
2222 elif line.startswith(TTest.FAILED_PREFIX):
2227 elif line.startswith(TTest.FAILED_PREFIX):
2223 line = line.splitlines()[0]
2228 line = line.splitlines()[0]
2224 failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :]))
2229 failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :]))
2225
2230
2226 return missing, failed
2231 return missing, failed
2227
2232
2228 @staticmethod
2233 @staticmethod
2229 def _escapef(m):
2234 def _escapef(m):
2230 return TTest.ESCAPEMAP[m.group(0)]
2235 return TTest.ESCAPEMAP[m.group(0)]
2231
2236
2232 @staticmethod
2237 @staticmethod
2233 def _stringescape(s):
2238 def _stringescape(s):
2234 return TTest.ESCAPESUB(TTest._escapef, s)
2239 return TTest.ESCAPESUB(TTest._escapef, s)
2235
2240
2236
2241
2237 iolock = threading.RLock()
2242 iolock = threading.RLock()
2238 firstlock = threading.RLock()
2243 firstlock = threading.RLock()
2239 firsterror = False
2244 firsterror = False
2240
2245
2241 if PYTHON3:
2246 if PYTHON3:
2242 base_class = unittest.TextTestResult
2247 base_class = unittest.TextTestResult
2243 else:
2248 else:
2244 base_class = unittest._TextTestResult
2249 base_class = unittest._TextTestResult
2245
2250
2246
2251
2247 class TestResult(base_class):
2252 class TestResult(base_class):
2248 """Holds results when executing via unittest."""
2253 """Holds results when executing via unittest."""
2249
2254
2250 def __init__(self, options, *args, **kwargs):
2255 def __init__(self, options, *args, **kwargs):
2251 super(TestResult, self).__init__(*args, **kwargs)
2256 super(TestResult, self).__init__(*args, **kwargs)
2252
2257
2253 self._options = options
2258 self._options = options
2254
2259
2255 # unittest.TestResult didn't have skipped until 2.7. We need to
2260 # unittest.TestResult didn't have skipped until 2.7. We need to
2256 # polyfill it.
2261 # polyfill it.
2257 self.skipped = []
2262 self.skipped = []
2258
2263
2259 # We have a custom "ignored" result that isn't present in any Python
2264 # 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
2265 # unittest implementation. It is very similar to skipped. It may make
2261 # sense to map it into skip some day.
2266 # sense to map it into skip some day.
2262 self.ignored = []
2267 self.ignored = []
2263
2268
2264 self.times = []
2269 self.times = []
2265 self._firststarttime = None
2270 self._firststarttime = None
2266 # Data stored for the benefit of generating xunit reports.
2271 # Data stored for the benefit of generating xunit reports.
2267 self.successes = []
2272 self.successes = []
2268 self.faildata = {}
2273 self.faildata = {}
2269
2274
2270 if options.color == 'auto':
2275 if options.color == 'auto':
2271 isatty = self.stream.isatty()
2276 isatty = self.stream.isatty()
2272 # For some reason, redirecting stdout on Windows disables the ANSI
2277 # For some reason, redirecting stdout on Windows disables the ANSI
2273 # color processing of stderr, which is what is used to print the
2278 # color processing of stderr, which is what is used to print the
2274 # output. Therefore, both must be tty on Windows to enable color.
2279 # output. Therefore, both must be tty on Windows to enable color.
2275 if WINDOWS:
2280 if WINDOWS:
2276 isatty = isatty and sys.stdout.isatty()
2281 isatty = isatty and sys.stdout.isatty()
2277 self.color = pygmentspresent and isatty
2282 self.color = pygmentspresent and isatty
2278 elif options.color == 'never':
2283 elif options.color == 'never':
2279 self.color = False
2284 self.color = False
2280 else: # 'always', for testing purposes
2285 else: # 'always', for testing purposes
2281 self.color = pygmentspresent
2286 self.color = pygmentspresent
2282
2287
2283 def onStart(self, test):
2288 def onStart(self, test):
2284 """Can be overriden by custom TestResult"""
2289 """Can be overriden by custom TestResult"""
2285
2290
2286 def onEnd(self):
2291 def onEnd(self):
2287 """Can be overriden by custom TestResult"""
2292 """Can be overriden by custom TestResult"""
2288
2293
2289 def addFailure(self, test, reason):
2294 def addFailure(self, test, reason):
2290 self.failures.append((test, reason))
2295 self.failures.append((test, reason))
2291
2296
2292 if self._options.first:
2297 if self._options.first:
2293 self.stop()
2298 self.stop()
2294 else:
2299 else:
2295 with iolock:
2300 with iolock:
2296 if reason == "timed out":
2301 if reason == "timed out":
2297 self.stream.write('t')
2302 self.stream.write('t')
2298 else:
2303 else:
2299 if not self._options.nodiff:
2304 if not self._options.nodiff:
2300 self.stream.write('\n')
2305 self.stream.write('\n')
2301 # Exclude the '\n' from highlighting to lex correctly
2306 # Exclude the '\n' from highlighting to lex correctly
2302 formatted = 'ERROR: %s output changed\n' % test
2307 formatted = 'ERROR: %s output changed\n' % test
2303 self.stream.write(highlightmsg(formatted, self.color))
2308 self.stream.write(highlightmsg(formatted, self.color))
2304 self.stream.write('!')
2309 self.stream.write('!')
2305
2310
2306 self.stream.flush()
2311 self.stream.flush()
2307
2312
2308 def addSuccess(self, test):
2313 def addSuccess(self, test):
2309 with iolock:
2314 with iolock:
2310 super(TestResult, self).addSuccess(test)
2315 super(TestResult, self).addSuccess(test)
2311 self.successes.append(test)
2316 self.successes.append(test)
2312
2317
2313 def addError(self, test, err):
2318 def addError(self, test, err):
2314 super(TestResult, self).addError(test, err)
2319 super(TestResult, self).addError(test, err)
2315 if self._options.first:
2320 if self._options.first:
2316 self.stop()
2321 self.stop()
2317
2322
2318 # Polyfill.
2323 # Polyfill.
2319 def addSkip(self, test, reason):
2324 def addSkip(self, test, reason):
2320 self.skipped.append((test, reason))
2325 self.skipped.append((test, reason))
2321 with iolock:
2326 with iolock:
2322 if self.showAll:
2327 if self.showAll:
2323 self.stream.writeln('skipped %s' % reason)
2328 self.stream.writeln('skipped %s' % reason)
2324 else:
2329 else:
2325 self.stream.write('s')
2330 self.stream.write('s')
2326 self.stream.flush()
2331 self.stream.flush()
2327
2332
2328 def addIgnore(self, test, reason):
2333 def addIgnore(self, test, reason):
2329 self.ignored.append((test, reason))
2334 self.ignored.append((test, reason))
2330 with iolock:
2335 with iolock:
2331 if self.showAll:
2336 if self.showAll:
2332 self.stream.writeln('ignored %s' % reason)
2337 self.stream.writeln('ignored %s' % reason)
2333 else:
2338 else:
2334 if reason not in ('not retesting', "doesn't match keyword"):
2339 if reason not in ('not retesting', "doesn't match keyword"):
2335 self.stream.write('i')
2340 self.stream.write('i')
2336 else:
2341 else:
2337 self.testsRun += 1
2342 self.testsRun += 1
2338 self.stream.flush()
2343 self.stream.flush()
2339
2344
2340 def addOutputMismatch(self, test, ret, got, expected):
2345 def addOutputMismatch(self, test, ret, got, expected):
2341 """Record a mismatch in test output for a particular test."""
2346 """Record a mismatch in test output for a particular test."""
2342 if self.shouldStop or firsterror:
2347 if self.shouldStop or firsterror:
2343 # don't print, some other test case already failed and
2348 # don't print, some other test case already failed and
2344 # printed, we're just stale and probably failed due to our
2349 # printed, we're just stale and probably failed due to our
2345 # temp dir getting cleaned up.
2350 # temp dir getting cleaned up.
2346 return
2351 return
2347
2352
2348 accepted = False
2353 accepted = False
2349 lines = []
2354 lines = []
2350
2355
2351 with iolock:
2356 with iolock:
2352 if self._options.nodiff:
2357 if self._options.nodiff:
2353 pass
2358 pass
2354 elif self._options.view:
2359 elif self._options.view:
2355 v = self._options.view
2360 v = self._options.view
2356 subprocess.call(
2361 subprocess.call(
2357 r'"%s" "%s" "%s"'
2362 r'"%s" "%s" "%s"'
2358 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2363 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2359 shell=True,
2364 shell=True,
2360 )
2365 )
2361 else:
2366 else:
2362 servefail, lines = getdiff(
2367 servefail, lines = getdiff(
2363 expected, got, test.refpath, test.errpath
2368 expected, got, test.refpath, test.errpath
2364 )
2369 )
2365 self.stream.write('\n')
2370 self.stream.write('\n')
2366 for line in lines:
2371 for line in lines:
2367 line = highlightdiff(line, self.color)
2372 line = highlightdiff(line, self.color)
2368 if PYTHON3:
2373 if PYTHON3:
2369 self.stream.flush()
2374 self.stream.flush()
2370 self.stream.buffer.write(line)
2375 self.stream.buffer.write(line)
2371 self.stream.buffer.flush()
2376 self.stream.buffer.flush()
2372 else:
2377 else:
2373 self.stream.write(line)
2378 self.stream.write(line)
2374 self.stream.flush()
2379 self.stream.flush()
2375
2380
2376 if servefail:
2381 if servefail:
2377 raise test.failureException(
2382 raise test.failureException(
2378 'server failed to start (HGPORT=%s)' % test._startport
2383 'server failed to start (HGPORT=%s)' % test._startport
2379 )
2384 )
2380
2385
2381 # handle interactive prompt without releasing iolock
2386 # handle interactive prompt without releasing iolock
2382 if self._options.interactive:
2387 if self._options.interactive:
2383 if test.readrefout() != expected:
2388 if test.readrefout() != expected:
2384 self.stream.write(
2389 self.stream.write(
2385 'Reference output has changed (run again to prompt '
2390 'Reference output has changed (run again to prompt '
2386 'changes)'
2391 'changes)'
2387 )
2392 )
2388 else:
2393 else:
2389 self.stream.write('Accept this change? [y/N] ')
2394 self.stream.write('Accept this change? [y/N] ')
2390 self.stream.flush()
2395 self.stream.flush()
2391 answer = sys.stdin.readline().strip()
2396 answer = sys.stdin.readline().strip()
2392 if answer.lower() in ('y', 'yes'):
2397 if answer.lower() in ('y', 'yes'):
2393 if test.path.endswith(b'.t'):
2398 if test.path.endswith(b'.t'):
2394 rename(test.errpath, test.path)
2399 rename(test.errpath, test.path)
2395 else:
2400 else:
2396 rename(test.errpath, b'%s.out' % test.path)
2401 rename(test.errpath, b'%s.out' % test.path)
2397 accepted = True
2402 accepted = True
2398 if not accepted:
2403 if not accepted:
2399 self.faildata[test.name] = b''.join(lines)
2404 self.faildata[test.name] = b''.join(lines)
2400
2405
2401 return accepted
2406 return accepted
2402
2407
2403 def startTest(self, test):
2408 def startTest(self, test):
2404 super(TestResult, self).startTest(test)
2409 super(TestResult, self).startTest(test)
2405
2410
2406 # os.times module computes the user time and system time spent by
2411 # 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.
2412 # 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
2413 # 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
2414 # and not for Windows. Hence why we fall back to another function
2410 # for wall time calculations.
2415 # for wall time calculations.
2411 test.started_times = os.times()
2416 test.started_times = os.times()
2412 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2417 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2413 test.started_time = time.time()
2418 test.started_time = time.time()
2414 if self._firststarttime is None: # thread racy but irrelevant
2419 if self._firststarttime is None: # thread racy but irrelevant
2415 self._firststarttime = test.started_time
2420 self._firststarttime = test.started_time
2416
2421
2417 def stopTest(self, test, interrupted=False):
2422 def stopTest(self, test, interrupted=False):
2418 super(TestResult, self).stopTest(test)
2423 super(TestResult, self).stopTest(test)
2419
2424
2420 test.stopped_times = os.times()
2425 test.stopped_times = os.times()
2421 stopped_time = time.time()
2426 stopped_time = time.time()
2422
2427
2423 starttime = test.started_times
2428 starttime = test.started_times
2424 endtime = test.stopped_times
2429 endtime = test.stopped_times
2425 origin = self._firststarttime
2430 origin = self._firststarttime
2426 self.times.append(
2431 self.times.append(
2427 (
2432 (
2428 test.name,
2433 test.name,
2429 endtime[2] - starttime[2], # user space CPU time
2434 endtime[2] - starttime[2], # user space CPU time
2430 endtime[3] - starttime[3], # sys space CPU time
2435 endtime[3] - starttime[3], # sys space CPU time
2431 stopped_time - test.started_time, # real time
2436 stopped_time - test.started_time, # real time
2432 test.started_time - origin, # start date in run context
2437 test.started_time - origin, # start date in run context
2433 stopped_time - origin, # end date in run context
2438 stopped_time - origin, # end date in run context
2434 )
2439 )
2435 )
2440 )
2436
2441
2437 if interrupted:
2442 if interrupted:
2438 with iolock:
2443 with iolock:
2439 self.stream.writeln(
2444 self.stream.writeln(
2440 'INTERRUPTED: %s (after %d seconds)'
2445 'INTERRUPTED: %s (after %d seconds)'
2441 % (test.name, self.times[-1][3])
2446 % (test.name, self.times[-1][3])
2442 )
2447 )
2443
2448
2444
2449
2445 def getTestResult():
2450 def getTestResult():
2446 """
2451 """
2447 Returns the relevant test result
2452 Returns the relevant test result
2448 """
2453 """
2449 if "CUSTOM_TEST_RESULT" in os.environ:
2454 if "CUSTOM_TEST_RESULT" in os.environ:
2450 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2455 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2451 return testresultmodule.TestResult
2456 return testresultmodule.TestResult
2452 else:
2457 else:
2453 return TestResult
2458 return TestResult
2454
2459
2455
2460
2456 class TestSuite(unittest.TestSuite):
2461 class TestSuite(unittest.TestSuite):
2457 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2462 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2458
2463
2459 def __init__(
2464 def __init__(
2460 self,
2465 self,
2461 testdir,
2466 testdir,
2462 jobs=1,
2467 jobs=1,
2463 whitelist=None,
2468 whitelist=None,
2464 blacklist=None,
2469 blacklist=None,
2465 keywords=None,
2470 keywords=None,
2466 loop=False,
2471 loop=False,
2467 runs_per_test=1,
2472 runs_per_test=1,
2468 loadtest=None,
2473 loadtest=None,
2469 showchannels=False,
2474 showchannels=False,
2470 *args,
2475 *args,
2471 **kwargs
2476 **kwargs
2472 ):
2477 ):
2473 """Create a new instance that can run tests with a configuration.
2478 """Create a new instance that can run tests with a configuration.
2474
2479
2475 testdir specifies the directory where tests are executed from. This
2480 testdir specifies the directory where tests are executed from. This
2476 is typically the ``tests`` directory from Mercurial's source
2481 is typically the ``tests`` directory from Mercurial's source
2477 repository.
2482 repository.
2478
2483
2479 jobs specifies the number of jobs to run concurrently. Each test
2484 jobs specifies the number of jobs to run concurrently. Each test
2480 executes on its own thread. Tests actually spawn new processes, so
2485 executes on its own thread. Tests actually spawn new processes, so
2481 state mutation should not be an issue.
2486 state mutation should not be an issue.
2482
2487
2483 If there is only one job, it will use the main thread.
2488 If there is only one job, it will use the main thread.
2484
2489
2485 whitelist and blacklist denote tests that have been whitelisted and
2490 whitelist and blacklist denote tests that have been whitelisted and
2486 blacklisted, respectively. These arguments don't belong in TestSuite.
2491 blacklisted, respectively. These arguments don't belong in TestSuite.
2487 Instead, whitelist and blacklist should be handled by the thing that
2492 Instead, whitelist and blacklist should be handled by the thing that
2488 populates the TestSuite with tests. They are present to preserve
2493 populates the TestSuite with tests. They are present to preserve
2489 backwards compatible behavior which reports skipped tests as part
2494 backwards compatible behavior which reports skipped tests as part
2490 of the results.
2495 of the results.
2491
2496
2492 keywords denotes key words that will be used to filter which tests
2497 keywords denotes key words that will be used to filter which tests
2493 to execute. This arguably belongs outside of TestSuite.
2498 to execute. This arguably belongs outside of TestSuite.
2494
2499
2495 loop denotes whether to loop over tests forever.
2500 loop denotes whether to loop over tests forever.
2496 """
2501 """
2497 super(TestSuite, self).__init__(*args, **kwargs)
2502 super(TestSuite, self).__init__(*args, **kwargs)
2498
2503
2499 self._jobs = jobs
2504 self._jobs = jobs
2500 self._whitelist = whitelist
2505 self._whitelist = whitelist
2501 self._blacklist = blacklist
2506 self._blacklist = blacklist
2502 self._keywords = keywords
2507 self._keywords = keywords
2503 self._loop = loop
2508 self._loop = loop
2504 self._runs_per_test = runs_per_test
2509 self._runs_per_test = runs_per_test
2505 self._loadtest = loadtest
2510 self._loadtest = loadtest
2506 self._showchannels = showchannels
2511 self._showchannels = showchannels
2507
2512
2508 def run(self, result):
2513 def run(self, result):
2509 # We have a number of filters that need to be applied. We do this
2514 # 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
2515 # here instead of inside Test because it makes the running logic for
2511 # Test simpler.
2516 # Test simpler.
2512 tests = []
2517 tests = []
2513 num_tests = [0]
2518 num_tests = [0]
2514 for test in self._tests:
2519 for test in self._tests:
2515
2520
2516 def get():
2521 def get():
2517 num_tests[0] += 1
2522 num_tests[0] += 1
2518 if getattr(test, 'should_reload', False):
2523 if getattr(test, 'should_reload', False):
2519 return self._loadtest(test, num_tests[0])
2524 return self._loadtest(test, num_tests[0])
2520 return test
2525 return test
2521
2526
2522 if not os.path.exists(test.path):
2527 if not os.path.exists(test.path):
2523 result.addSkip(test, "Doesn't exist")
2528 result.addSkip(test, "Doesn't exist")
2524 continue
2529 continue
2525
2530
2526 is_whitelisted = self._whitelist and (
2531 is_whitelisted = self._whitelist and (
2527 test.relpath in self._whitelist or test.bname in self._whitelist
2532 test.relpath in self._whitelist or test.bname in self._whitelist
2528 )
2533 )
2529 if not is_whitelisted:
2534 if not is_whitelisted:
2530 is_blacklisted = self._blacklist and (
2535 is_blacklisted = self._blacklist and (
2531 test.relpath in self._blacklist
2536 test.relpath in self._blacklist
2532 or test.bname in self._blacklist
2537 or test.bname in self._blacklist
2533 )
2538 )
2534 if is_blacklisted:
2539 if is_blacklisted:
2535 result.addSkip(test, 'blacklisted')
2540 result.addSkip(test, 'blacklisted')
2536 continue
2541 continue
2537 if self._keywords:
2542 if self._keywords:
2538 with open(test.path, 'rb') as f:
2543 with open(test.path, 'rb') as f:
2539 t = f.read().lower() + test.bname.lower()
2544 t = f.read().lower() + test.bname.lower()
2540 ignored = False
2545 ignored = False
2541 for k in self._keywords.lower().split():
2546 for k in self._keywords.lower().split():
2542 if k not in t:
2547 if k not in t:
2543 result.addIgnore(test, "doesn't match keyword")
2548 result.addIgnore(test, "doesn't match keyword")
2544 ignored = True
2549 ignored = True
2545 break
2550 break
2546
2551
2547 if ignored:
2552 if ignored:
2548 continue
2553 continue
2549 for _ in xrange(self._runs_per_test):
2554 for _ in xrange(self._runs_per_test):
2550 tests.append(get())
2555 tests.append(get())
2551
2556
2552 runtests = list(tests)
2557 runtests = list(tests)
2553 done = queue.Queue()
2558 done = queue.Queue()
2554 running = 0
2559 running = 0
2555
2560
2556 channels_lock = threading.Lock()
2561 channels_lock = threading.Lock()
2557 channels = [""] * self._jobs
2562 channels = [""] * self._jobs
2558
2563
2559 def job(test, result):
2564 def job(test, result):
2560 with channels_lock:
2565 with channels_lock:
2561 for n, v in enumerate(channels):
2566 for n, v in enumerate(channels):
2562 if not v:
2567 if not v:
2563 channel = n
2568 channel = n
2564 break
2569 break
2565 else:
2570 else:
2566 raise ValueError('Could not find output channel')
2571 raise ValueError('Could not find output channel')
2567 channels[channel] = "=" + test.name[5:].split(".")[0]
2572 channels[channel] = "=" + test.name[5:].split(".")[0]
2568
2573
2569 r = None
2574 r = None
2570 try:
2575 try:
2571 test(result)
2576 test(result)
2572 except KeyboardInterrupt:
2577 except KeyboardInterrupt:
2573 pass
2578 pass
2574 except: # re-raises
2579 except: # re-raises
2575 r = ('!', test, 'run-test raised an error, see traceback')
2580 r = ('!', test, 'run-test raised an error, see traceback')
2576 raise
2581 raise
2577 finally:
2582 finally:
2578 try:
2583 try:
2579 channels[channel] = ''
2584 channels[channel] = ''
2580 except IndexError:
2585 except IndexError:
2581 pass
2586 pass
2582 done.put(r)
2587 done.put(r)
2583
2588
2584 def stat():
2589 def stat():
2585 count = 0
2590 count = 0
2586 while channels:
2591 while channels:
2587 d = '\n%03s ' % count
2592 d = '\n%03s ' % count
2588 for n, v in enumerate(channels):
2593 for n, v in enumerate(channels):
2589 if v:
2594 if v:
2590 d += v[0]
2595 d += v[0]
2591 channels[n] = v[1:] or '.'
2596 channels[n] = v[1:] or '.'
2592 else:
2597 else:
2593 d += ' '
2598 d += ' '
2594 d += ' '
2599 d += ' '
2595 with iolock:
2600 with iolock:
2596 sys.stdout.write(d + ' ')
2601 sys.stdout.write(d + ' ')
2597 sys.stdout.flush()
2602 sys.stdout.flush()
2598 for x in xrange(10):
2603 for x in xrange(10):
2599 if channels:
2604 if channels:
2600 time.sleep(0.1)
2605 time.sleep(0.1)
2601 count += 1
2606 count += 1
2602
2607
2603 stoppedearly = False
2608 stoppedearly = False
2604
2609
2605 if self._showchannels:
2610 if self._showchannels:
2606 statthread = threading.Thread(target=stat, name="stat")
2611 statthread = threading.Thread(target=stat, name="stat")
2607 statthread.start()
2612 statthread.start()
2608
2613
2609 try:
2614 try:
2610 while tests or running:
2615 while tests or running:
2611 if not done.empty() or running == self._jobs or not tests:
2616 if not done.empty() or running == self._jobs or not tests:
2612 try:
2617 try:
2613 done.get(True, 1)
2618 done.get(True, 1)
2614 running -= 1
2619 running -= 1
2615 if result and result.shouldStop:
2620 if result and result.shouldStop:
2616 stoppedearly = True
2621 stoppedearly = True
2617 break
2622 break
2618 except queue.Empty:
2623 except queue.Empty:
2619 continue
2624 continue
2620 if tests and not running == self._jobs:
2625 if tests and not running == self._jobs:
2621 test = tests.pop(0)
2626 test = tests.pop(0)
2622 if self._loop:
2627 if self._loop:
2623 if getattr(test, 'should_reload', False):
2628 if getattr(test, 'should_reload', False):
2624 num_tests[0] += 1
2629 num_tests[0] += 1
2625 tests.append(self._loadtest(test, num_tests[0]))
2630 tests.append(self._loadtest(test, num_tests[0]))
2626 else:
2631 else:
2627 tests.append(test)
2632 tests.append(test)
2628 if self._jobs == 1:
2633 if self._jobs == 1:
2629 job(test, result)
2634 job(test, result)
2630 else:
2635 else:
2631 t = threading.Thread(
2636 t = threading.Thread(
2632 target=job, name=test.name, args=(test, result)
2637 target=job, name=test.name, args=(test, result)
2633 )
2638 )
2634 t.start()
2639 t.start()
2635 running += 1
2640 running += 1
2636
2641
2637 # If we stop early we still need to wait on started tests to
2642 # If we stop early we still need to wait on started tests to
2638 # finish. Otherwise, there is a race between the test completing
2643 # finish. Otherwise, there is a race between the test completing
2639 # and the test's cleanup code running. This could result in the
2644 # and the test's cleanup code running. This could result in the
2640 # test reporting incorrect.
2645 # test reporting incorrect.
2641 if stoppedearly:
2646 if stoppedearly:
2642 while running:
2647 while running:
2643 try:
2648 try:
2644 done.get(True, 1)
2649 done.get(True, 1)
2645 running -= 1
2650 running -= 1
2646 except queue.Empty:
2651 except queue.Empty:
2647 continue
2652 continue
2648 except KeyboardInterrupt:
2653 except KeyboardInterrupt:
2649 for test in runtests:
2654 for test in runtests:
2650 test.abort()
2655 test.abort()
2651
2656
2652 channels = []
2657 channels = []
2653
2658
2654 return result
2659 return result
2655
2660
2656
2661
2657 # Save the most recent 5 wall-clock runtimes of each test to a
2662 # Save the most recent 5 wall-clock runtimes of each test to a
2658 # human-readable text file named .testtimes. Tests are sorted
2663 # human-readable text file named .testtimes. Tests are sorted
2659 # alphabetically, while times for each test are listed from oldest to
2664 # alphabetically, while times for each test are listed from oldest to
2660 # newest.
2665 # newest.
2661
2666
2662
2667
2663 def loadtimes(outputdir):
2668 def loadtimes(outputdir):
2664 times = []
2669 times = []
2665 try:
2670 try:
2666 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2671 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2667 for line in fp:
2672 for line in fp:
2668 m = re.match('(.*?) ([0-9. ]+)', line)
2673 m = re.match('(.*?) ([0-9. ]+)', line)
2669 times.append(
2674 times.append(
2670 (m.group(1), [float(t) for t in m.group(2).split()])
2675 (m.group(1), [float(t) for t in m.group(2).split()])
2671 )
2676 )
2672 except IOError as err:
2677 except IOError as err:
2673 if err.errno != errno.ENOENT:
2678 if err.errno != errno.ENOENT:
2674 raise
2679 raise
2675 return times
2680 return times
2676
2681
2677
2682
2678 def savetimes(outputdir, result):
2683 def savetimes(outputdir, result):
2679 saved = dict(loadtimes(outputdir))
2684 saved = dict(loadtimes(outputdir))
2680 maxruns = 5
2685 maxruns = 5
2681 skipped = {str(t[0]) for t in result.skipped}
2686 skipped = {str(t[0]) for t in result.skipped}
2682 for tdata in result.times:
2687 for tdata in result.times:
2683 test, real = tdata[0], tdata[3]
2688 test, real = tdata[0], tdata[3]
2684 if test not in skipped:
2689 if test not in skipped:
2685 ts = saved.setdefault(test, [])
2690 ts = saved.setdefault(test, [])
2686 ts.append(real)
2691 ts.append(real)
2687 ts[:] = ts[-maxruns:]
2692 ts[:] = ts[-maxruns:]
2688
2693
2689 fd, tmpname = tempfile.mkstemp(
2694 fd, tmpname = tempfile.mkstemp(
2690 prefix=b'.testtimes', dir=outputdir, text=True
2695 prefix=b'.testtimes', dir=outputdir, text=True
2691 )
2696 )
2692 with os.fdopen(fd, 'w') as fp:
2697 with os.fdopen(fd, 'w') as fp:
2693 for name, ts in sorted(saved.items()):
2698 for name, ts in sorted(saved.items()):
2694 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2699 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2695 timepath = os.path.join(outputdir, b'.testtimes')
2700 timepath = os.path.join(outputdir, b'.testtimes')
2696 try:
2701 try:
2697 os.unlink(timepath)
2702 os.unlink(timepath)
2698 except OSError:
2703 except OSError:
2699 pass
2704 pass
2700 try:
2705 try:
2701 os.rename(tmpname, timepath)
2706 os.rename(tmpname, timepath)
2702 except OSError:
2707 except OSError:
2703 pass
2708 pass
2704
2709
2705
2710
2706 class TextTestRunner(unittest.TextTestRunner):
2711 class TextTestRunner(unittest.TextTestRunner):
2707 """Custom unittest test runner that uses appropriate settings."""
2712 """Custom unittest test runner that uses appropriate settings."""
2708
2713
2709 def __init__(self, runner, *args, **kwargs):
2714 def __init__(self, runner, *args, **kwargs):
2710 super(TextTestRunner, self).__init__(*args, **kwargs)
2715 super(TextTestRunner, self).__init__(*args, **kwargs)
2711
2716
2712 self._runner = runner
2717 self._runner = runner
2713
2718
2714 self._result = getTestResult()(
2719 self._result = getTestResult()(
2715 self._runner.options, self.stream, self.descriptions, self.verbosity
2720 self._runner.options, self.stream, self.descriptions, self.verbosity
2716 )
2721 )
2717
2722
2718 def listtests(self, test):
2723 def listtests(self, test):
2719 test = sorted(test, key=lambda t: t.name)
2724 test = sorted(test, key=lambda t: t.name)
2720
2725
2721 self._result.onStart(test)
2726 self._result.onStart(test)
2722
2727
2723 for t in test:
2728 for t in test:
2724 print(t.name)
2729 print(t.name)
2725 self._result.addSuccess(t)
2730 self._result.addSuccess(t)
2726
2731
2727 if self._runner.options.xunit:
2732 if self._runner.options.xunit:
2728 with open(self._runner.options.xunit, "wb") as xuf:
2733 with open(self._runner.options.xunit, "wb") as xuf:
2729 self._writexunit(self._result, xuf)
2734 self._writexunit(self._result, xuf)
2730
2735
2731 if self._runner.options.json:
2736 if self._runner.options.json:
2732 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2737 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2733 with open(jsonpath, 'w') as fp:
2738 with open(jsonpath, 'w') as fp:
2734 self._writejson(self._result, fp)
2739 self._writejson(self._result, fp)
2735
2740
2736 return self._result
2741 return self._result
2737
2742
2738 def run(self, test):
2743 def run(self, test):
2739 self._result.onStart(test)
2744 self._result.onStart(test)
2740 test(self._result)
2745 test(self._result)
2741
2746
2742 failed = len(self._result.failures)
2747 failed = len(self._result.failures)
2743 skipped = len(self._result.skipped)
2748 skipped = len(self._result.skipped)
2744 ignored = len(self._result.ignored)
2749 ignored = len(self._result.ignored)
2745
2750
2746 with iolock:
2751 with iolock:
2747 self.stream.writeln('')
2752 self.stream.writeln('')
2748
2753
2749 if not self._runner.options.noskips:
2754 if not self._runner.options.noskips:
2750 for test, msg in sorted(
2755 for test, msg in sorted(
2751 self._result.skipped, key=lambda s: s[0].name
2756 self._result.skipped, key=lambda s: s[0].name
2752 ):
2757 ):
2753 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2758 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2754 msg = highlightmsg(formatted, self._result.color)
2759 msg = highlightmsg(formatted, self._result.color)
2755 self.stream.write(msg)
2760 self.stream.write(msg)
2756 for test, msg in sorted(
2761 for test, msg in sorted(
2757 self._result.failures, key=lambda f: f[0].name
2762 self._result.failures, key=lambda f: f[0].name
2758 ):
2763 ):
2759 formatted = 'Failed %s: %s\n' % (test.name, msg)
2764 formatted = 'Failed %s: %s\n' % (test.name, msg)
2760 self.stream.write(highlightmsg(formatted, self._result.color))
2765 self.stream.write(highlightmsg(formatted, self._result.color))
2761 for test, msg in sorted(
2766 for test, msg in sorted(
2762 self._result.errors, key=lambda e: e[0].name
2767 self._result.errors, key=lambda e: e[0].name
2763 ):
2768 ):
2764 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2769 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2765
2770
2766 if self._runner.options.xunit:
2771 if self._runner.options.xunit:
2767 with open(self._runner.options.xunit, "wb") as xuf:
2772 with open(self._runner.options.xunit, "wb") as xuf:
2768 self._writexunit(self._result, xuf)
2773 self._writexunit(self._result, xuf)
2769
2774
2770 if self._runner.options.json:
2775 if self._runner.options.json:
2771 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2776 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2772 with open(jsonpath, 'w') as fp:
2777 with open(jsonpath, 'w') as fp:
2773 self._writejson(self._result, fp)
2778 self._writejson(self._result, fp)
2774
2779
2775 self._runner._checkhglib('Tested')
2780 self._runner._checkhglib('Tested')
2776
2781
2777 savetimes(self._runner._outputdir, self._result)
2782 savetimes(self._runner._outputdir, self._result)
2778
2783
2779 if failed and self._runner.options.known_good_rev:
2784 if failed and self._runner.options.known_good_rev:
2780 self._bisecttests(t for t, m in self._result.failures)
2785 self._bisecttests(t for t, m in self._result.failures)
2781 self.stream.writeln(
2786 self.stream.writeln(
2782 '# Ran %d tests, %d skipped, %d failed.'
2787 '# Ran %d tests, %d skipped, %d failed.'
2783 % (self._result.testsRun, skipped + ignored, failed)
2788 % (self._result.testsRun, skipped + ignored, failed)
2784 )
2789 )
2785 if failed:
2790 if failed:
2786 self.stream.writeln(
2791 self.stream.writeln(
2787 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2792 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2788 )
2793 )
2789 if self._runner.options.time:
2794 if self._runner.options.time:
2790 self.printtimes(self._result.times)
2795 self.printtimes(self._result.times)
2791
2796
2792 if self._runner.options.exceptions:
2797 if self._runner.options.exceptions:
2793 exceptions = aggregateexceptions(
2798 exceptions = aggregateexceptions(
2794 os.path.join(self._runner._outputdir, b'exceptions')
2799 os.path.join(self._runner._outputdir, b'exceptions')
2795 )
2800 )
2796
2801
2797 self.stream.writeln('Exceptions Report:')
2802 self.stream.writeln('Exceptions Report:')
2798 self.stream.writeln(
2803 self.stream.writeln(
2799 '%d total from %d frames'
2804 '%d total from %d frames'
2800 % (exceptions['total'], len(exceptions['exceptioncounts']))
2805 % (exceptions['total'], len(exceptions['exceptioncounts']))
2801 )
2806 )
2802 combined = exceptions['combined']
2807 combined = exceptions['combined']
2803 for key in sorted(combined, key=combined.get, reverse=True):
2808 for key in sorted(combined, key=combined.get, reverse=True):
2804 frame, line, exc = key
2809 frame, line, exc = key
2805 totalcount, testcount, leastcount, leasttest = combined[key]
2810 totalcount, testcount, leastcount, leasttest = combined[key]
2806
2811
2807 self.stream.writeln(
2812 self.stream.writeln(
2808 '%d (%d tests)\t%s: %s (%s - %d total)'
2813 '%d (%d tests)\t%s: %s (%s - %d total)'
2809 % (
2814 % (
2810 totalcount,
2815 totalcount,
2811 testcount,
2816 testcount,
2812 frame,
2817 frame,
2813 exc,
2818 exc,
2814 leasttest,
2819 leasttest,
2815 leastcount,
2820 leastcount,
2816 )
2821 )
2817 )
2822 )
2818
2823
2819 self.stream.flush()
2824 self.stream.flush()
2820
2825
2821 return self._result
2826 return self._result
2822
2827
2823 def _bisecttests(self, tests):
2828 def _bisecttests(self, tests):
2824 bisectcmd = ['hg', 'bisect']
2829 bisectcmd = ['hg', 'bisect']
2825 bisectrepo = self._runner.options.bisect_repo
2830 bisectrepo = self._runner.options.bisect_repo
2826 if bisectrepo:
2831 if bisectrepo:
2827 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2832 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2828
2833
2829 def pread(args):
2834 def pread(args):
2830 env = os.environ.copy()
2835 env = os.environ.copy()
2831 env['HGPLAIN'] = '1'
2836 env['HGPLAIN'] = '1'
2832 p = subprocess.Popen(
2837 p = subprocess.Popen(
2833 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2838 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2834 )
2839 )
2835 data = p.stdout.read()
2840 data = p.stdout.read()
2836 p.wait()
2841 p.wait()
2837 return data
2842 return data
2838
2843
2839 for test in tests:
2844 for test in tests:
2840 pread(bisectcmd + ['--reset']),
2845 pread(bisectcmd + ['--reset']),
2841 pread(bisectcmd + ['--bad', '.'])
2846 pread(bisectcmd + ['--bad', '.'])
2842 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2847 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2843 # TODO: we probably need to forward more options
2848 # TODO: we probably need to forward more options
2844 # that alter hg's behavior inside the tests.
2849 # that alter hg's behavior inside the tests.
2845 opts = ''
2850 opts = ''
2846 withhg = self._runner.options.with_hg
2851 withhg = self._runner.options.with_hg
2847 if withhg:
2852 if withhg:
2848 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2853 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2849 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2854 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2850 data = pread(bisectcmd + ['--command', rtc])
2855 data = pread(bisectcmd + ['--command', rtc])
2851 m = re.search(
2856 m = re.search(
2852 (
2857 (
2853 br'\nThe first (?P<goodbad>bad|good) revision '
2858 br'\nThe first (?P<goodbad>bad|good) revision '
2854 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2859 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2855 br'summary: +(?P<summary>[^\n]+)\n'
2860 br'summary: +(?P<summary>[^\n]+)\n'
2856 ),
2861 ),
2857 data,
2862 data,
2858 (re.MULTILINE | re.DOTALL),
2863 (re.MULTILINE | re.DOTALL),
2859 )
2864 )
2860 if m is None:
2865 if m is None:
2861 self.stream.writeln(
2866 self.stream.writeln(
2862 'Failed to identify failure point for %s' % test
2867 'Failed to identify failure point for %s' % test
2863 )
2868 )
2864 continue
2869 continue
2865 dat = m.groupdict()
2870 dat = m.groupdict()
2866 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2871 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2867 self.stream.writeln(
2872 self.stream.writeln(
2868 '%s %s by %s (%s)'
2873 '%s %s by %s (%s)'
2869 % (
2874 % (
2870 test,
2875 test,
2871 verb,
2876 verb,
2872 dat['node'].decode('ascii'),
2877 dat['node'].decode('ascii'),
2873 dat['summary'].decode('utf8', 'ignore'),
2878 dat['summary'].decode('utf8', 'ignore'),
2874 )
2879 )
2875 )
2880 )
2876
2881
2877 def printtimes(self, times):
2882 def printtimes(self, times):
2878 # iolock held by run
2883 # iolock held by run
2879 self.stream.writeln('# Producing time report')
2884 self.stream.writeln('# Producing time report')
2880 times.sort(key=lambda t: (t[3]))
2885 times.sort(key=lambda t: (t[3]))
2881 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2886 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2882 self.stream.writeln(
2887 self.stream.writeln(
2883 '%-7s %-7s %-7s %-7s %-7s %s'
2888 '%-7s %-7s %-7s %-7s %-7s %s'
2884 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2889 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2885 )
2890 )
2886 for tdata in times:
2891 for tdata in times:
2887 test = tdata[0]
2892 test = tdata[0]
2888 cuser, csys, real, start, end = tdata[1:6]
2893 cuser, csys, real, start, end = tdata[1:6]
2889 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2894 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2890
2895
2891 @staticmethod
2896 @staticmethod
2892 def _writexunit(result, outf):
2897 def _writexunit(result, outf):
2893 # See http://llg.cubic.org/docs/junit/ for a reference.
2898 # See http://llg.cubic.org/docs/junit/ for a reference.
2894 timesd = {t[0]: t[3] for t in result.times}
2899 timesd = {t[0]: t[3] for t in result.times}
2895 doc = minidom.Document()
2900 doc = minidom.Document()
2896 s = doc.createElement('testsuite')
2901 s = doc.createElement('testsuite')
2897 s.setAttribute('errors', "0") # TODO
2902 s.setAttribute('errors', "0") # TODO
2898 s.setAttribute('failures', str(len(result.failures)))
2903 s.setAttribute('failures', str(len(result.failures)))
2899 s.setAttribute('name', 'run-tests')
2904 s.setAttribute('name', 'run-tests')
2900 s.setAttribute(
2905 s.setAttribute(
2901 'skipped', str(len(result.skipped) + len(result.ignored))
2906 'skipped', str(len(result.skipped) + len(result.ignored))
2902 )
2907 )
2903 s.setAttribute('tests', str(result.testsRun))
2908 s.setAttribute('tests', str(result.testsRun))
2904 doc.appendChild(s)
2909 doc.appendChild(s)
2905 for tc in result.successes:
2910 for tc in result.successes:
2906 t = doc.createElement('testcase')
2911 t = doc.createElement('testcase')
2907 t.setAttribute('name', tc.name)
2912 t.setAttribute('name', tc.name)
2908 tctime = timesd.get(tc.name)
2913 tctime = timesd.get(tc.name)
2909 if tctime is not None:
2914 if tctime is not None:
2910 t.setAttribute('time', '%.3f' % tctime)
2915 t.setAttribute('time', '%.3f' % tctime)
2911 s.appendChild(t)
2916 s.appendChild(t)
2912 for tc, err in sorted(result.faildata.items()):
2917 for tc, err in sorted(result.faildata.items()):
2913 t = doc.createElement('testcase')
2918 t = doc.createElement('testcase')
2914 t.setAttribute('name', tc)
2919 t.setAttribute('name', tc)
2915 tctime = timesd.get(tc)
2920 tctime = timesd.get(tc)
2916 if tctime is not None:
2921 if tctime is not None:
2917 t.setAttribute('time', '%.3f' % tctime)
2922 t.setAttribute('time', '%.3f' % tctime)
2918 # createCDATASection expects a unicode or it will
2923 # createCDATASection expects a unicode or it will
2919 # convert using default conversion rules, which will
2924 # convert using default conversion rules, which will
2920 # fail if string isn't ASCII.
2925 # fail if string isn't ASCII.
2921 err = cdatasafe(err).decode('utf-8', 'replace')
2926 err = cdatasafe(err).decode('utf-8', 'replace')
2922 cd = doc.createCDATASection(err)
2927 cd = doc.createCDATASection(err)
2923 # Use 'failure' here instead of 'error' to match errors = 0,
2928 # Use 'failure' here instead of 'error' to match errors = 0,
2924 # failures = len(result.failures) in the testsuite element.
2929 # failures = len(result.failures) in the testsuite element.
2925 failelem = doc.createElement('failure')
2930 failelem = doc.createElement('failure')
2926 failelem.setAttribute('message', 'output changed')
2931 failelem.setAttribute('message', 'output changed')
2927 failelem.setAttribute('type', 'output-mismatch')
2932 failelem.setAttribute('type', 'output-mismatch')
2928 failelem.appendChild(cd)
2933 failelem.appendChild(cd)
2929 t.appendChild(failelem)
2934 t.appendChild(failelem)
2930 s.appendChild(t)
2935 s.appendChild(t)
2931 for tc, message in result.skipped:
2936 for tc, message in result.skipped:
2932 # According to the schema, 'skipped' has no attributes. So store
2937 # According to the schema, 'skipped' has no attributes. So store
2933 # the skip message as a text node instead.
2938 # the skip message as a text node instead.
2934 t = doc.createElement('testcase')
2939 t = doc.createElement('testcase')
2935 t.setAttribute('name', tc.name)
2940 t.setAttribute('name', tc.name)
2936 binmessage = message.encode('utf-8')
2941 binmessage = message.encode('utf-8')
2937 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2942 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2938 cd = doc.createCDATASection(message)
2943 cd = doc.createCDATASection(message)
2939 skipelem = doc.createElement('skipped')
2944 skipelem = doc.createElement('skipped')
2940 skipelem.appendChild(cd)
2945 skipelem.appendChild(cd)
2941 t.appendChild(skipelem)
2946 t.appendChild(skipelem)
2942 s.appendChild(t)
2947 s.appendChild(t)
2943 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2948 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2944
2949
2945 @staticmethod
2950 @staticmethod
2946 def _writejson(result, outf):
2951 def _writejson(result, outf):
2947 timesd = {}
2952 timesd = {}
2948 for tdata in result.times:
2953 for tdata in result.times:
2949 test = tdata[0]
2954 test = tdata[0]
2950 timesd[test] = tdata[1:]
2955 timesd[test] = tdata[1:]
2951
2956
2952 outcome = {}
2957 outcome = {}
2953 groups = [
2958 groups = [
2954 ('success', ((tc, None) for tc in result.successes)),
2959 ('success', ((tc, None) for tc in result.successes)),
2955 ('failure', result.failures),
2960 ('failure', result.failures),
2956 ('skip', result.skipped),
2961 ('skip', result.skipped),
2957 ]
2962 ]
2958 for res, testcases in groups:
2963 for res, testcases in groups:
2959 for tc, __ in testcases:
2964 for tc, __ in testcases:
2960 if tc.name in timesd:
2965 if tc.name in timesd:
2961 diff = result.faildata.get(tc.name, b'')
2966 diff = result.faildata.get(tc.name, b'')
2962 try:
2967 try:
2963 diff = diff.decode('unicode_escape')
2968 diff = diff.decode('unicode_escape')
2964 except UnicodeDecodeError as e:
2969 except UnicodeDecodeError as e:
2965 diff = '%r decoding diff, sorry' % e
2970 diff = '%r decoding diff, sorry' % e
2966 tres = {
2971 tres = {
2967 'result': res,
2972 'result': res,
2968 'time': ('%0.3f' % timesd[tc.name][2]),
2973 'time': ('%0.3f' % timesd[tc.name][2]),
2969 'cuser': ('%0.3f' % timesd[tc.name][0]),
2974 'cuser': ('%0.3f' % timesd[tc.name][0]),
2970 'csys': ('%0.3f' % timesd[tc.name][1]),
2975 'csys': ('%0.3f' % timesd[tc.name][1]),
2971 'start': ('%0.3f' % timesd[tc.name][3]),
2976 'start': ('%0.3f' % timesd[tc.name][3]),
2972 'end': ('%0.3f' % timesd[tc.name][4]),
2977 'end': ('%0.3f' % timesd[tc.name][4]),
2973 'diff': diff,
2978 'diff': diff,
2974 }
2979 }
2975 else:
2980 else:
2976 # blacklisted test
2981 # blacklisted test
2977 tres = {'result': res}
2982 tres = {'result': res}
2978
2983
2979 outcome[tc.name] = tres
2984 outcome[tc.name] = tres
2980 jsonout = json.dumps(
2985 jsonout = json.dumps(
2981 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2986 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2982 )
2987 )
2983 outf.writelines(("testreport =", jsonout))
2988 outf.writelines(("testreport =", jsonout))
2984
2989
2985
2990
2986 def sorttests(testdescs, previoustimes, shuffle=False):
2991 def sorttests(testdescs, previoustimes, shuffle=False):
2987 """Do an in-place sort of tests."""
2992 """Do an in-place sort of tests."""
2988 if shuffle:
2993 if shuffle:
2989 random.shuffle(testdescs)
2994 random.shuffle(testdescs)
2990 return
2995 return
2991
2996
2992 if previoustimes:
2997 if previoustimes:
2993
2998
2994 def sortkey(f):
2999 def sortkey(f):
2995 f = f['path']
3000 f = f['path']
2996 if f in previoustimes:
3001 if f in previoustimes:
2997 # Use most recent time as estimate
3002 # Use most recent time as estimate
2998 return -(previoustimes[f][-1])
3003 return -(previoustimes[f][-1])
2999 else:
3004 else:
3000 # Default to a rather arbitrary value of 1 second for new tests
3005 # Default to a rather arbitrary value of 1 second for new tests
3001 return -1.0
3006 return -1.0
3002
3007
3003 else:
3008 else:
3004 # keywords for slow tests
3009 # keywords for slow tests
3005 slow = {
3010 slow = {
3006 b'svn': 10,
3011 b'svn': 10,
3007 b'cvs': 10,
3012 b'cvs': 10,
3008 b'hghave': 10,
3013 b'hghave': 10,
3009 b'largefiles-update': 10,
3014 b'largefiles-update': 10,
3010 b'run-tests': 10,
3015 b'run-tests': 10,
3011 b'corruption': 10,
3016 b'corruption': 10,
3012 b'race': 10,
3017 b'race': 10,
3013 b'i18n': 10,
3018 b'i18n': 10,
3014 b'check': 100,
3019 b'check': 100,
3015 b'gendoc': 100,
3020 b'gendoc': 100,
3016 b'contrib-perf': 200,
3021 b'contrib-perf': 200,
3017 b'merge-combination': 100,
3022 b'merge-combination': 100,
3018 }
3023 }
3019 perf = {}
3024 perf = {}
3020
3025
3021 def sortkey(f):
3026 def sortkey(f):
3022 # run largest tests first, as they tend to take the longest
3027 # run largest tests first, as they tend to take the longest
3023 f = f['path']
3028 f = f['path']
3024 try:
3029 try:
3025 return perf[f]
3030 return perf[f]
3026 except KeyError:
3031 except KeyError:
3027 try:
3032 try:
3028 val = -os.stat(f).st_size
3033 val = -os.stat(f).st_size
3029 except OSError as e:
3034 except OSError as e:
3030 if e.errno != errno.ENOENT:
3035 if e.errno != errno.ENOENT:
3031 raise
3036 raise
3032 perf[f] = -1e9 # file does not exist, tell early
3037 perf[f] = -1e9 # file does not exist, tell early
3033 return -1e9
3038 return -1e9
3034 for kw, mul in slow.items():
3039 for kw, mul in slow.items():
3035 if kw in f:
3040 if kw in f:
3036 val *= mul
3041 val *= mul
3037 if f.endswith(b'.py'):
3042 if f.endswith(b'.py'):
3038 val /= 10.0
3043 val /= 10.0
3039 perf[f] = val / 1000.0
3044 perf[f] = val / 1000.0
3040 return perf[f]
3045 return perf[f]
3041
3046
3042 testdescs.sort(key=sortkey)
3047 testdescs.sort(key=sortkey)
3043
3048
3044
3049
3045 class TestRunner(object):
3050 class TestRunner(object):
3046 """Holds context for executing tests.
3051 """Holds context for executing tests.
3047
3052
3048 Tests rely on a lot of state. This object holds it for them.
3053 Tests rely on a lot of state. This object holds it for them.
3049 """
3054 """
3050
3055
3051 # Programs required to run tests.
3056 # Programs required to run tests.
3052 REQUIREDTOOLS = [
3057 REQUIREDTOOLS = [
3053 b'diff',
3058 b'diff',
3054 b'grep',
3059 b'grep',
3055 b'unzip',
3060 b'unzip',
3056 b'gunzip',
3061 b'gunzip',
3057 b'bunzip2',
3062 b'bunzip2',
3058 b'sed',
3063 b'sed',
3059 ]
3064 ]
3060
3065
3061 # Maps file extensions to test class.
3066 # Maps file extensions to test class.
3062 TESTTYPES = [
3067 TESTTYPES = [
3063 (b'.py', PythonTest),
3068 (b'.py', PythonTest),
3064 (b'.t', TTest),
3069 (b'.t', TTest),
3065 ]
3070 ]
3066
3071
3067 def __init__(self):
3072 def __init__(self):
3068 self.options = None
3073 self.options = None
3069 self._hgroot = None
3074 self._hgroot = None
3070 self._testdir = None
3075 self._testdir = None
3071 self._outputdir = None
3076 self._outputdir = None
3072 self._hgtmp = None
3077 self._hgtmp = None
3073 self._installdir = None
3078 self._installdir = None
3074 self._bindir = None
3079 self._bindir = None
3075 # a place for run-tests.py to generate executable it needs
3080 # a place for run-tests.py to generate executable it needs
3076 self._custom_bin_dir = None
3081 self._custom_bin_dir = None
3077 self._pythondir = None
3082 self._pythondir = None
3078 # True if we had to infer the pythondir from --with-hg
3083 # True if we had to infer the pythondir from --with-hg
3079 self._pythondir_inferred = False
3084 self._pythondir_inferred = False
3080 self._coveragefile = None
3085 self._coveragefile = None
3081 self._createdfiles = []
3086 self._createdfiles = []
3082 self._hgcommand = None
3087 self._hgcommand = None
3083 self._hgpath = None
3088 self._hgpath = None
3084 self._portoffset = 0
3089 self._portoffset = 0
3085 self._ports = {}
3090 self._ports = {}
3086
3091
3087 def run(self, args, parser=None):
3092 def run(self, args, parser=None):
3088 """Run the test suite."""
3093 """Run the test suite."""
3089 oldmask = os.umask(0o22)
3094 oldmask = os.umask(0o22)
3090 try:
3095 try:
3091 parser = parser or getparser()
3096 parser = parser or getparser()
3092 options = parseargs(args, parser)
3097 options = parseargs(args, parser)
3093 tests = [_sys2bytes(a) for a in options.tests]
3098 tests = [_sys2bytes(a) for a in options.tests]
3094 if options.test_list is not None:
3099 if options.test_list is not None:
3095 for listfile in options.test_list:
3100 for listfile in options.test_list:
3096 with open(listfile, 'rb') as f:
3101 with open(listfile, 'rb') as f:
3097 tests.extend(t for t in f.read().splitlines() if t)
3102 tests.extend(t for t in f.read().splitlines() if t)
3098 self.options = options
3103 self.options = options
3099
3104
3100 self._checktools()
3105 self._checktools()
3101 testdescs = self.findtests(tests)
3106 testdescs = self.findtests(tests)
3102 if options.profile_runner:
3107 if options.profile_runner:
3103 import statprof
3108 import statprof
3104
3109
3105 statprof.start()
3110 statprof.start()
3106 result = self._run(testdescs)
3111 result = self._run(testdescs)
3107 if options.profile_runner:
3112 if options.profile_runner:
3108 statprof.stop()
3113 statprof.stop()
3109 statprof.display()
3114 statprof.display()
3110 return result
3115 return result
3111
3116
3112 finally:
3117 finally:
3113 os.umask(oldmask)
3118 os.umask(oldmask)
3114
3119
3115 def _run(self, testdescs):
3120 def _run(self, testdescs):
3116 testdir = getcwdb()
3121 testdir = getcwdb()
3117 # assume all tests in same folder for now
3122 # assume all tests in same folder for now
3118 if testdescs:
3123 if testdescs:
3119 pathname = os.path.dirname(testdescs[0]['path'])
3124 pathname = os.path.dirname(testdescs[0]['path'])
3120 if pathname:
3125 if pathname:
3121 testdir = os.path.join(testdir, pathname)
3126 testdir = os.path.join(testdir, pathname)
3122 self._testdir = osenvironb[b'TESTDIR'] = testdir
3127 self._testdir = osenvironb[b'TESTDIR'] = testdir
3123 osenvironb[b'TESTDIR_FORWARD_SLASH'] = osenvironb[b'TESTDIR'].replace(
3128 osenvironb[b'TESTDIR_FORWARD_SLASH'] = osenvironb[b'TESTDIR'].replace(
3124 os.sep.encode('ascii'), b'/'
3129 os.sep.encode('ascii'), b'/'
3125 )
3130 )
3126
3131
3127 if self.options.outputdir:
3132 if self.options.outputdir:
3128 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
3133 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
3129 else:
3134 else:
3130 self._outputdir = getcwdb()
3135 self._outputdir = getcwdb()
3131 if testdescs and pathname:
3136 if testdescs and pathname:
3132 self._outputdir = os.path.join(self._outputdir, pathname)
3137 self._outputdir = os.path.join(self._outputdir, pathname)
3133 previoustimes = {}
3138 previoustimes = {}
3134 if self.options.order_by_runtime:
3139 if self.options.order_by_runtime:
3135 previoustimes = dict(loadtimes(self._outputdir))
3140 previoustimes = dict(loadtimes(self._outputdir))
3136 sorttests(testdescs, previoustimes, shuffle=self.options.random)
3141 sorttests(testdescs, previoustimes, shuffle=self.options.random)
3137
3142
3138 if 'PYTHONHASHSEED' not in os.environ:
3143 if 'PYTHONHASHSEED' not in os.environ:
3139 # use a random python hash seed all the time
3144 # use a random python hash seed all the time
3140 # we do the randomness ourself to know what seed is used
3145 # we do the randomness ourself to know what seed is used
3141 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
3146 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
3142
3147
3143 # Rayon (Rust crate for multi-threading) will use all logical CPU cores
3148 # Rayon (Rust crate for multi-threading) will use all logical CPU cores
3144 # by default, causing thrashing on high-cpu-count systems.
3149 # by default, causing thrashing on high-cpu-count systems.
3145 # Setting its limit to 3 during tests should still let us uncover
3150 # Setting its limit to 3 during tests should still let us uncover
3146 # multi-threading bugs while keeping the thrashing reasonable.
3151 # multi-threading bugs while keeping the thrashing reasonable.
3147 os.environ.setdefault("RAYON_NUM_THREADS", "3")
3152 os.environ.setdefault("RAYON_NUM_THREADS", "3")
3148
3153
3149 if self.options.tmpdir:
3154 if self.options.tmpdir:
3150 self.options.keep_tmpdir = True
3155 self.options.keep_tmpdir = True
3151 tmpdir = _sys2bytes(self.options.tmpdir)
3156 tmpdir = _sys2bytes(self.options.tmpdir)
3152 if os.path.exists(tmpdir):
3157 if os.path.exists(tmpdir):
3153 # Meaning of tmpdir has changed since 1.3: we used to create
3158 # Meaning of tmpdir has changed since 1.3: we used to create
3154 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
3159 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
3155 # tmpdir already exists.
3160 # tmpdir already exists.
3156 print("error: temp dir %r already exists" % tmpdir)
3161 print("error: temp dir %r already exists" % tmpdir)
3157 return 1
3162 return 1
3158
3163
3159 os.makedirs(tmpdir)
3164 os.makedirs(tmpdir)
3160 else:
3165 else:
3161 d = None
3166 d = None
3162 if WINDOWS:
3167 if WINDOWS:
3163 # without this, we get the default temp dir location, but
3168 # without this, we get the default temp dir location, but
3164 # in all lowercase, which causes troubles with paths (issue3490)
3169 # in all lowercase, which causes troubles with paths (issue3490)
3165 d = osenvironb.get(b'TMP', None)
3170 d = osenvironb.get(b'TMP', None)
3166 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
3171 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
3167
3172
3168 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3173 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3169
3174
3170 self._custom_bin_dir = os.path.join(self._hgtmp, b'custom-bin')
3175 self._custom_bin_dir = os.path.join(self._hgtmp, b'custom-bin')
3171 os.makedirs(self._custom_bin_dir)
3176 os.makedirs(self._custom_bin_dir)
3172
3177
3173 if self.options.with_hg:
3178 if self.options.with_hg:
3174 self._installdir = None
3179 self._installdir = None
3175 whg = self.options.with_hg
3180 whg = self.options.with_hg
3176 self._bindir = os.path.dirname(os.path.realpath(whg))
3181 self._bindir = os.path.dirname(os.path.realpath(whg))
3177 assert isinstance(self._bindir, bytes)
3182 assert isinstance(self._bindir, bytes)
3178 self._hgcommand = os.path.basename(whg)
3183 self._hgcommand = os.path.basename(whg)
3179
3184
3180 normbin = os.path.normpath(os.path.abspath(whg))
3185 normbin = os.path.normpath(os.path.abspath(whg))
3181 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3186 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3182
3187
3183 # Other Python scripts in the test harness need to
3188 # Other Python scripts in the test harness need to
3184 # `import mercurial`. If `hg` is a Python script, we assume
3189 # `import mercurial`. If `hg` is a Python script, we assume
3185 # the Mercurial modules are relative to its path and tell the tests
3190 # the Mercurial modules are relative to its path and tell the tests
3186 # to load Python modules from its directory.
3191 # to load Python modules from its directory.
3187 with open(whg, 'rb') as fh:
3192 with open(whg, 'rb') as fh:
3188 initial = fh.read(1024)
3193 initial = fh.read(1024)
3189
3194
3190 if re.match(b'#!.*python', initial):
3195 if re.match(b'#!.*python', initial):
3191 self._pythondir = self._bindir
3196 self._pythondir = self._bindir
3192 # If it looks like our in-repo Rust binary, use the source root.
3197 # If it looks like our in-repo Rust binary, use the source root.
3193 # This is a bit hacky. But rhg is still not supported outside the
3198 # This is a bit hacky. But rhg is still not supported outside the
3194 # source directory. So until it is, do the simple thing.
3199 # source directory. So until it is, do the simple thing.
3195 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3200 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3196 self._pythondir = os.path.dirname(self._testdir)
3201 self._pythondir = os.path.dirname(self._testdir)
3197 # Fall back to the legacy behavior.
3202 # Fall back to the legacy behavior.
3198 else:
3203 else:
3199 self._pythondir = self._bindir
3204 self._pythondir = self._bindir
3200 self._pythondir_inferred = True
3205 self._pythondir_inferred = True
3201
3206
3202 else:
3207 else:
3203 self._installdir = os.path.join(self._hgtmp, b"install")
3208 self._installdir = os.path.join(self._hgtmp, b"install")
3204 self._bindir = os.path.join(self._installdir, b"bin")
3209 self._bindir = os.path.join(self._installdir, b"bin")
3205 self._hgcommand = b'hg'
3210 self._hgcommand = b'hg'
3206 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3211 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3207
3212
3208 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3213 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3209 # a python script and feed it to python.exe. Legacy stdio is force
3214 # a python script and feed it to python.exe. Legacy stdio is force
3210 # enabled by hg.exe, and this is a more realistic way to launch hg
3215 # enabled by hg.exe, and this is a more realistic way to launch hg
3211 # anyway.
3216 # anyway.
3212 if WINDOWS and not self._hgcommand.endswith(b'.exe'):
3217 if WINDOWS and not self._hgcommand.endswith(b'.exe'):
3213 self._hgcommand += b'.exe'
3218 self._hgcommand += b'.exe'
3214
3219
3215 real_hg = os.path.join(self._bindir, self._hgcommand)
3220 real_hg = os.path.join(self._bindir, self._hgcommand)
3216 osenvironb[b'HGTEST_REAL_HG'] = real_hg
3221 osenvironb[b'HGTEST_REAL_HG'] = real_hg
3217 # set CHGHG, then replace "hg" command by "chg"
3222 # set CHGHG, then replace "hg" command by "chg"
3218 chgbindir = self._bindir
3223 chgbindir = self._bindir
3219 if self.options.chg or self.options.with_chg:
3224 if self.options.chg or self.options.with_chg:
3220 osenvironb[b'CHG_INSTALLED_AS_HG'] = b'1'
3225 osenvironb[b'CHG_INSTALLED_AS_HG'] = b'1'
3221 osenvironb[b'CHGHG'] = real_hg
3226 osenvironb[b'CHGHG'] = real_hg
3222 else:
3227 else:
3223 # drop flag for hghave
3228 # drop flag for hghave
3224 osenvironb.pop(b'CHG_INSTALLED_AS_HG', None)
3229 osenvironb.pop(b'CHG_INSTALLED_AS_HG', None)
3225 if self.options.chg:
3230 if self.options.chg:
3226 self._hgcommand = b'chg'
3231 self._hgcommand = b'chg'
3227 elif self.options.with_chg:
3232 elif self.options.with_chg:
3228 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3233 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3229 self._hgcommand = os.path.basename(self.options.with_chg)
3234 self._hgcommand = os.path.basename(self.options.with_chg)
3230
3235
3231 # configure fallback and replace "hg" command by "rhg"
3236 # configure fallback and replace "hg" command by "rhg"
3232 rhgbindir = self._bindir
3237 rhgbindir = self._bindir
3233 if self.options.rhg or self.options.with_rhg:
3238 if self.options.rhg or self.options.with_rhg:
3234 # Affects hghave.py
3239 # Affects hghave.py
3235 osenvironb[b'RHG_INSTALLED_AS_HG'] = b'1'
3240 osenvironb[b'RHG_INSTALLED_AS_HG'] = b'1'
3236 # Affects configuration. Alternatives would be setting configuration through
3241 # Affects configuration. Alternatives would be setting configuration through
3237 # `$HGRCPATH` but some tests override that, or changing `_hgcommand` to include
3242 # `$HGRCPATH` but some tests override that, or changing `_hgcommand` to include
3238 # `--config` but that disrupts tests that print command lines and check expected
3243 # `--config` but that disrupts tests that print command lines and check expected
3239 # output.
3244 # output.
3240 osenvironb[b'RHG_ON_UNSUPPORTED'] = b'fallback'
3245 osenvironb[b'RHG_ON_UNSUPPORTED'] = b'fallback'
3241 osenvironb[b'RHG_FALLBACK_EXECUTABLE'] = real_hg
3246 osenvironb[b'RHG_FALLBACK_EXECUTABLE'] = real_hg
3242 else:
3247 else:
3243 # drop flag for hghave
3248 # drop flag for hghave
3244 osenvironb.pop(b'RHG_INSTALLED_AS_HG', None)
3249 osenvironb.pop(b'RHG_INSTALLED_AS_HG', None)
3245 if self.options.rhg:
3250 if self.options.rhg:
3246 self._hgcommand = b'rhg'
3251 self._hgcommand = b'rhg'
3247 elif self.options.with_rhg:
3252 elif self.options.with_rhg:
3248 rhgbindir = os.path.dirname(os.path.realpath(self.options.with_rhg))
3253 rhgbindir = os.path.dirname(os.path.realpath(self.options.with_rhg))
3249 self._hgcommand = os.path.basename(self.options.with_rhg)
3254 self._hgcommand = os.path.basename(self.options.with_rhg)
3250
3255
3251 if self.options.pyoxidized:
3256 if self.options.pyoxidized:
3252 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
3257 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
3253 reporootdir = os.path.dirname(testdir)
3258 reporootdir = os.path.dirname(testdir)
3254 # XXX we should ideally install stuff instead of using the local build
3259 # XXX we should ideally install stuff instead of using the local build
3255 bin_path = (
3260 bin_path = (
3256 b'build/pyoxidizer/x86_64-pc-windows-msvc/release/app/hg.exe'
3261 b'build/pyoxidizer/x86_64-pc-windows-msvc/release/app/hg.exe'
3257 )
3262 )
3258 full_path = os.path.join(reporootdir, bin_path)
3263 full_path = os.path.join(reporootdir, bin_path)
3259 self._hgcommand = full_path
3264 self._hgcommand = full_path
3260 # Affects hghave.py
3265 # Affects hghave.py
3261 osenvironb[b'PYOXIDIZED_INSTALLED_AS_HG'] = b'1'
3266 osenvironb[b'PYOXIDIZED_INSTALLED_AS_HG'] = b'1'
3262 else:
3267 else:
3263 osenvironb.pop(b'PYOXIDIZED_INSTALLED_AS_HG', None)
3268 osenvironb.pop(b'PYOXIDIZED_INSTALLED_AS_HG', None)
3264
3269
3265 osenvironb[b"BINDIR"] = self._bindir
3270 osenvironb[b"BINDIR"] = self._bindir
3266 osenvironb[b"PYTHON"] = PYTHON
3271 osenvironb[b"PYTHON"] = PYTHON
3267
3272
3268 fileb = _sys2bytes(__file__)
3273 fileb = _sys2bytes(__file__)
3269 runtestdir = os.path.abspath(os.path.dirname(fileb))
3274 runtestdir = os.path.abspath(os.path.dirname(fileb))
3270 osenvironb[b'RUNTESTDIR'] = runtestdir
3275 osenvironb[b'RUNTESTDIR'] = runtestdir
3271 osenvironb[b'RUNTESTDIR_FORWARD_SLASH'] = runtestdir.replace(
3276 osenvironb[b'RUNTESTDIR_FORWARD_SLASH'] = runtestdir.replace(
3272 os.sep.encode('ascii'), b'/'
3277 os.sep.encode('ascii'), b'/'
3273 )
3278 )
3274 if PYTHON3:
3279 if PYTHON3:
3275 sepb = _sys2bytes(os.pathsep)
3280 sepb = _sys2bytes(os.pathsep)
3276 else:
3281 else:
3277 sepb = os.pathsep
3282 sepb = os.pathsep
3278 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3283 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3279 if os.path.islink(__file__):
3284 if os.path.islink(__file__):
3280 # test helper will likely be at the end of the symlink
3285 # test helper will likely be at the end of the symlink
3281 realfile = os.path.realpath(fileb)
3286 realfile = os.path.realpath(fileb)
3282 realdir = os.path.abspath(os.path.dirname(realfile))
3287 realdir = os.path.abspath(os.path.dirname(realfile))
3283 path.insert(2, realdir)
3288 path.insert(2, realdir)
3284 if chgbindir != self._bindir:
3289 if chgbindir != self._bindir:
3285 path.insert(1, chgbindir)
3290 path.insert(1, chgbindir)
3286 if rhgbindir != self._bindir:
3291 if rhgbindir != self._bindir:
3287 path.insert(1, rhgbindir)
3292 path.insert(1, rhgbindir)
3288 if self._testdir != runtestdir:
3293 if self._testdir != runtestdir:
3289 path = [self._testdir] + path
3294 path = [self._testdir] + path
3290 path = [self._custom_bin_dir] + path
3295 path = [self._custom_bin_dir] + path
3291 osenvironb[b"PATH"] = sepb.join(path)
3296 osenvironb[b"PATH"] = sepb.join(path)
3292
3297
3293 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3298 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3294 # can run .../tests/run-tests.py test-foo where test-foo
3299 # can run .../tests/run-tests.py test-foo where test-foo
3295 # adds an extension to HGRC. Also include run-test.py directory to
3300 # adds an extension to HGRC. Also include run-test.py directory to
3296 # import modules like heredoctest.
3301 # import modules like heredoctest.
3297 pypath = [self._pythondir, self._testdir, runtestdir]
3302 pypath = [self._pythondir, self._testdir, runtestdir]
3298 # We have to augment PYTHONPATH, rather than simply replacing
3303 # We have to augment PYTHONPATH, rather than simply replacing
3299 # it, in case external libraries are only available via current
3304 # it, in case external libraries are only available via current
3300 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3305 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3301 # are in /opt/subversion.)
3306 # are in /opt/subversion.)
3302 oldpypath = osenvironb.get(IMPL_PATH)
3307 oldpypath = osenvironb.get(IMPL_PATH)
3303 if oldpypath:
3308 if oldpypath:
3304 pypath.append(oldpypath)
3309 pypath.append(oldpypath)
3305 osenvironb[IMPL_PATH] = sepb.join(pypath)
3310 osenvironb[IMPL_PATH] = sepb.join(pypath)
3306
3311
3307 if self.options.pure:
3312 if self.options.pure:
3308 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3313 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3309 os.environ["HGMODULEPOLICY"] = "py"
3314 os.environ["HGMODULEPOLICY"] = "py"
3310 if self.options.rust:
3315 if self.options.rust:
3311 os.environ["HGMODULEPOLICY"] = "rust+c"
3316 os.environ["HGMODULEPOLICY"] = "rust+c"
3312 if self.options.no_rust:
3317 if self.options.no_rust:
3313 current_policy = os.environ.get("HGMODULEPOLICY", "")
3318 current_policy = os.environ.get("HGMODULEPOLICY", "")
3314 if current_policy.startswith("rust+"):
3319 if current_policy.startswith("rust+"):
3315 os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :]
3320 os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :]
3316 os.environ.pop("HGWITHRUSTEXT", None)
3321 os.environ.pop("HGWITHRUSTEXT", None)
3317
3322
3318 if self.options.allow_slow_tests:
3323 if self.options.allow_slow_tests:
3319 os.environ["HGTEST_SLOW"] = "slow"
3324 os.environ["HGTEST_SLOW"] = "slow"
3320 elif 'HGTEST_SLOW' in os.environ:
3325 elif 'HGTEST_SLOW' in os.environ:
3321 del os.environ['HGTEST_SLOW']
3326 del os.environ['HGTEST_SLOW']
3322
3327
3323 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3328 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3324
3329
3325 if self.options.exceptions:
3330 if self.options.exceptions:
3326 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3331 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3327 try:
3332 try:
3328 os.makedirs(exceptionsdir)
3333 os.makedirs(exceptionsdir)
3329 except OSError as e:
3334 except OSError as e:
3330 if e.errno != errno.EEXIST:
3335 if e.errno != errno.EEXIST:
3331 raise
3336 raise
3332
3337
3333 # Remove all existing exception reports.
3338 # Remove all existing exception reports.
3334 for f in os.listdir(exceptionsdir):
3339 for f in os.listdir(exceptionsdir):
3335 os.unlink(os.path.join(exceptionsdir, f))
3340 os.unlink(os.path.join(exceptionsdir, f))
3336
3341
3337 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3342 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3338 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3343 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3339 self.options.extra_config_opt.append(
3344 self.options.extra_config_opt.append(
3340 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3345 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3341 )
3346 )
3342
3347
3343 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3348 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3344 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3349 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3345 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3350 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3346 vlog("# Using PATH", os.environ["PATH"])
3351 vlog("# Using PATH", os.environ["PATH"])
3347 vlog(
3352 vlog(
3348 "# Using",
3353 "# Using",
3349 _bytes2sys(IMPL_PATH),
3354 _bytes2sys(IMPL_PATH),
3350 _bytes2sys(osenvironb[IMPL_PATH]),
3355 _bytes2sys(osenvironb[IMPL_PATH]),
3351 )
3356 )
3352 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3357 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3353
3358
3354 try:
3359 try:
3355 return self._runtests(testdescs) or 0
3360 return self._runtests(testdescs) or 0
3356 finally:
3361 finally:
3357 time.sleep(0.1)
3362 time.sleep(0.1)
3358 self._cleanup()
3363 self._cleanup()
3359
3364
3360 def findtests(self, args):
3365 def findtests(self, args):
3361 """Finds possible test files from arguments.
3366 """Finds possible test files from arguments.
3362
3367
3363 If you wish to inject custom tests into the test harness, this would
3368 If you wish to inject custom tests into the test harness, this would
3364 be a good function to monkeypatch or override in a derived class.
3369 be a good function to monkeypatch or override in a derived class.
3365 """
3370 """
3366 if not args:
3371 if not args:
3367 if self.options.changed:
3372 if self.options.changed:
3368 proc = Popen4(
3373 proc = Popen4(
3369 b'hg st --rev "%s" -man0 .'
3374 b'hg st --rev "%s" -man0 .'
3370 % _sys2bytes(self.options.changed),
3375 % _sys2bytes(self.options.changed),
3371 None,
3376 None,
3372 0,
3377 0,
3373 )
3378 )
3374 stdout, stderr = proc.communicate()
3379 stdout, stderr = proc.communicate()
3375 args = stdout.strip(b'\0').split(b'\0')
3380 args = stdout.strip(b'\0').split(b'\0')
3376 else:
3381 else:
3377 args = os.listdir(b'.')
3382 args = os.listdir(b'.')
3378
3383
3379 expanded_args = []
3384 expanded_args = []
3380 for arg in args:
3385 for arg in args:
3381 if os.path.isdir(arg):
3386 if os.path.isdir(arg):
3382 if not arg.endswith(b'/'):
3387 if not arg.endswith(b'/'):
3383 arg += b'/'
3388 arg += b'/'
3384 expanded_args.extend([arg + a for a in os.listdir(arg)])
3389 expanded_args.extend([arg + a for a in os.listdir(arg)])
3385 else:
3390 else:
3386 expanded_args.append(arg)
3391 expanded_args.append(arg)
3387 args = expanded_args
3392 args = expanded_args
3388
3393
3389 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3394 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3390 tests = []
3395 tests = []
3391 for t in args:
3396 for t in args:
3392 case = []
3397 case = []
3393
3398
3394 if not (
3399 if not (
3395 os.path.basename(t).startswith(b'test-')
3400 os.path.basename(t).startswith(b'test-')
3396 and (t.endswith(b'.py') or t.endswith(b'.t'))
3401 and (t.endswith(b'.py') or t.endswith(b'.t'))
3397 ):
3402 ):
3398
3403
3399 m = testcasepattern.match(os.path.basename(t))
3404 m = testcasepattern.match(os.path.basename(t))
3400 if m is not None:
3405 if m is not None:
3401 t_basename, casestr = m.groups()
3406 t_basename, casestr = m.groups()
3402 t = os.path.join(os.path.dirname(t), t_basename)
3407 t = os.path.join(os.path.dirname(t), t_basename)
3403 if casestr:
3408 if casestr:
3404 case = casestr.split(b'#')
3409 case = casestr.split(b'#')
3405 else:
3410 else:
3406 continue
3411 continue
3407
3412
3408 if t.endswith(b'.t'):
3413 if t.endswith(b'.t'):
3409 # .t file may contain multiple test cases
3414 # .t file may contain multiple test cases
3410 casedimensions = parsettestcases(t)
3415 casedimensions = parsettestcases(t)
3411 if casedimensions:
3416 if casedimensions:
3412 cases = []
3417 cases = []
3413
3418
3414 def addcases(case, casedimensions):
3419 def addcases(case, casedimensions):
3415 if not casedimensions:
3420 if not casedimensions:
3416 cases.append(case)
3421 cases.append(case)
3417 else:
3422 else:
3418 for c in casedimensions[0]:
3423 for c in casedimensions[0]:
3419 addcases(case + [c], casedimensions[1:])
3424 addcases(case + [c], casedimensions[1:])
3420
3425
3421 addcases([], casedimensions)
3426 addcases([], casedimensions)
3422 if case and case in cases:
3427 if case and case in cases:
3423 cases = [case]
3428 cases = [case]
3424 elif case:
3429 elif case:
3425 # Ignore invalid cases
3430 # Ignore invalid cases
3426 cases = []
3431 cases = []
3427 else:
3432 else:
3428 pass
3433 pass
3429 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3434 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3430 else:
3435 else:
3431 tests.append({'path': t})
3436 tests.append({'path': t})
3432 else:
3437 else:
3433 tests.append({'path': t})
3438 tests.append({'path': t})
3434
3439
3435 if self.options.retest:
3440 if self.options.retest:
3436 retest_args = []
3441 retest_args = []
3437 for test in tests:
3442 for test in tests:
3438 errpath = self._geterrpath(test)
3443 errpath = self._geterrpath(test)
3439 if os.path.exists(errpath):
3444 if os.path.exists(errpath):
3440 retest_args.append(test)
3445 retest_args.append(test)
3441 tests = retest_args
3446 tests = retest_args
3442 return tests
3447 return tests
3443
3448
3444 def _runtests(self, testdescs):
3449 def _runtests(self, testdescs):
3445 def _reloadtest(test, i):
3450 def _reloadtest(test, i):
3446 # convert a test back to its description dict
3451 # convert a test back to its description dict
3447 desc = {'path': test.path}
3452 desc = {'path': test.path}
3448 case = getattr(test, '_case', [])
3453 case = getattr(test, '_case', [])
3449 if case:
3454 if case:
3450 desc['case'] = case
3455 desc['case'] = case
3451 return self._gettest(desc, i)
3456 return self._gettest(desc, i)
3452
3457
3453 try:
3458 try:
3454 if self.options.restart:
3459 if self.options.restart:
3455 orig = list(testdescs)
3460 orig = list(testdescs)
3456 while testdescs:
3461 while testdescs:
3457 desc = testdescs[0]
3462 desc = testdescs[0]
3458 errpath = self._geterrpath(desc)
3463 errpath = self._geterrpath(desc)
3459 if os.path.exists(errpath):
3464 if os.path.exists(errpath):
3460 break
3465 break
3461 testdescs.pop(0)
3466 testdescs.pop(0)
3462 if not testdescs:
3467 if not testdescs:
3463 print("running all tests")
3468 print("running all tests")
3464 testdescs = orig
3469 testdescs = orig
3465
3470
3466 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3471 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3467 num_tests = len(tests) * self.options.runs_per_test
3472 num_tests = len(tests) * self.options.runs_per_test
3468
3473
3469 jobs = min(num_tests, self.options.jobs)
3474 jobs = min(num_tests, self.options.jobs)
3470
3475
3471 failed = False
3476 failed = False
3472 kws = self.options.keywords
3477 kws = self.options.keywords
3473 if kws is not None and PYTHON3:
3478 if kws is not None and PYTHON3:
3474 kws = kws.encode('utf-8')
3479 kws = kws.encode('utf-8')
3475
3480
3476 suite = TestSuite(
3481 suite = TestSuite(
3477 self._testdir,
3482 self._testdir,
3478 jobs=jobs,
3483 jobs=jobs,
3479 whitelist=self.options.whitelisted,
3484 whitelist=self.options.whitelisted,
3480 blacklist=self.options.blacklist,
3485 blacklist=self.options.blacklist,
3481 keywords=kws,
3486 keywords=kws,
3482 loop=self.options.loop,
3487 loop=self.options.loop,
3483 runs_per_test=self.options.runs_per_test,
3488 runs_per_test=self.options.runs_per_test,
3484 showchannels=self.options.showchannels,
3489 showchannels=self.options.showchannels,
3485 tests=tests,
3490 tests=tests,
3486 loadtest=_reloadtest,
3491 loadtest=_reloadtest,
3487 )
3492 )
3488 verbosity = 1
3493 verbosity = 1
3489 if self.options.list_tests:
3494 if self.options.list_tests:
3490 verbosity = 0
3495 verbosity = 0
3491 elif self.options.verbose:
3496 elif self.options.verbose:
3492 verbosity = 2
3497 verbosity = 2
3493 runner = TextTestRunner(self, verbosity=verbosity)
3498 runner = TextTestRunner(self, verbosity=verbosity)
3494
3499
3495 if self.options.list_tests:
3500 if self.options.list_tests:
3496 result = runner.listtests(suite)
3501 result = runner.listtests(suite)
3497 else:
3502 else:
3498 self._usecorrectpython()
3503 self._usecorrectpython()
3499 if self._installdir:
3504 if self._installdir:
3500 self._installhg()
3505 self._installhg()
3501 self._checkhglib("Testing")
3506 self._checkhglib("Testing")
3502 if self.options.chg:
3507 if self.options.chg:
3503 assert self._installdir
3508 assert self._installdir
3504 self._installchg()
3509 self._installchg()
3505 if self.options.rhg:
3510 if self.options.rhg:
3506 assert self._installdir
3511 assert self._installdir
3507 self._installrhg()
3512 self._installrhg()
3508 elif self.options.pyoxidized:
3513 elif self.options.pyoxidized:
3509 self._build_pyoxidized()
3514 self._build_pyoxidized()
3510 self._use_correct_mercurial()
3515 self._use_correct_mercurial()
3511
3516
3512 log(
3517 log(
3513 'running %d tests using %d parallel processes'
3518 'running %d tests using %d parallel processes'
3514 % (num_tests, jobs)
3519 % (num_tests, jobs)
3515 )
3520 )
3516
3521
3517 result = runner.run(suite)
3522 result = runner.run(suite)
3518
3523
3519 if result.failures or result.errors:
3524 if result.failures or result.errors:
3520 failed = True
3525 failed = True
3521
3526
3522 result.onEnd()
3527 result.onEnd()
3523
3528
3524 if self.options.anycoverage:
3529 if self.options.anycoverage:
3525 self._outputcoverage()
3530 self._outputcoverage()
3526 except KeyboardInterrupt:
3531 except KeyboardInterrupt:
3527 failed = True
3532 failed = True
3528 print("\ninterrupted!")
3533 print("\ninterrupted!")
3529
3534
3530 if failed:
3535 if failed:
3531 return 1
3536 return 1
3532
3537
3533 def _geterrpath(self, test):
3538 def _geterrpath(self, test):
3534 # test['path'] is a relative path
3539 # test['path'] is a relative path
3535 if 'case' in test:
3540 if 'case' in test:
3536 # for multiple dimensions test cases
3541 # for multiple dimensions test cases
3537 casestr = b'#'.join(test['case'])
3542 casestr = b'#'.join(test['case'])
3538 errpath = b'%s#%s.err' % (test['path'], casestr)
3543 errpath = b'%s#%s.err' % (test['path'], casestr)
3539 else:
3544 else:
3540 errpath = b'%s.err' % test['path']
3545 errpath = b'%s.err' % test['path']
3541 if self.options.outputdir:
3546 if self.options.outputdir:
3542 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
3547 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
3543 errpath = os.path.join(self._outputdir, errpath)
3548 errpath = os.path.join(self._outputdir, errpath)
3544 return errpath
3549 return errpath
3545
3550
3546 def _getport(self, count):
3551 def _getport(self, count):
3547 port = self._ports.get(count) # do we have a cached entry?
3552 port = self._ports.get(count) # do we have a cached entry?
3548 if port is None:
3553 if port is None:
3549 portneeded = 3
3554 portneeded = 3
3550 # above 100 tries we just give up and let test reports failure
3555 # above 100 tries we just give up and let test reports failure
3551 for tries in xrange(100):
3556 for tries in xrange(100):
3552 allfree = True
3557 allfree = True
3553 port = self.options.port + self._portoffset
3558 port = self.options.port + self._portoffset
3554 for idx in xrange(portneeded):
3559 for idx in xrange(portneeded):
3555 if not checkportisavailable(port + idx):
3560 if not checkportisavailable(port + idx):
3556 allfree = False
3561 allfree = False
3557 break
3562 break
3558 self._portoffset += portneeded
3563 self._portoffset += portneeded
3559 if allfree:
3564 if allfree:
3560 break
3565 break
3561 self._ports[count] = port
3566 self._ports[count] = port
3562 return port
3567 return port
3563
3568
3564 def _gettest(self, testdesc, count):
3569 def _gettest(self, testdesc, count):
3565 """Obtain a Test by looking at its filename.
3570 """Obtain a Test by looking at its filename.
3566
3571
3567 Returns a Test instance. The Test may not be runnable if it doesn't
3572 Returns a Test instance. The Test may not be runnable if it doesn't
3568 map to a known type.
3573 map to a known type.
3569 """
3574 """
3570 path = testdesc['path']
3575 path = testdesc['path']
3571 lctest = path.lower()
3576 lctest = path.lower()
3572 testcls = Test
3577 testcls = Test
3573
3578
3574 for ext, cls in self.TESTTYPES:
3579 for ext, cls in self.TESTTYPES:
3575 if lctest.endswith(ext):
3580 if lctest.endswith(ext):
3576 testcls = cls
3581 testcls = cls
3577 break
3582 break
3578
3583
3579 refpath = os.path.join(getcwdb(), path)
3584 refpath = os.path.join(getcwdb(), path)
3580 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3585 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3581
3586
3582 # extra keyword parameters. 'case' is used by .t tests
3587 # extra keyword parameters. 'case' is used by .t tests
3583 kwds = {k: testdesc[k] for k in ['case'] if k in testdesc}
3588 kwds = {k: testdesc[k] for k in ['case'] if k in testdesc}
3584
3589
3585 t = testcls(
3590 t = testcls(
3586 refpath,
3591 refpath,
3587 self._outputdir,
3592 self._outputdir,
3588 tmpdir,
3593 tmpdir,
3589 keeptmpdir=self.options.keep_tmpdir,
3594 keeptmpdir=self.options.keep_tmpdir,
3590 debug=self.options.debug,
3595 debug=self.options.debug,
3591 first=self.options.first,
3596 first=self.options.first,
3592 timeout=self.options.timeout,
3597 timeout=self.options.timeout,
3593 startport=self._getport(count),
3598 startport=self._getport(count),
3594 extraconfigopts=self.options.extra_config_opt,
3599 extraconfigopts=self.options.extra_config_opt,
3595 shell=self.options.shell,
3600 shell=self.options.shell,
3596 hgcommand=self._hgcommand,
3601 hgcommand=self._hgcommand,
3597 usechg=bool(self.options.with_chg or self.options.chg),
3602 usechg=bool(self.options.with_chg or self.options.chg),
3598 chgdebug=self.options.chg_debug,
3603 chgdebug=self.options.chg_debug,
3599 useipv6=useipv6,
3604 useipv6=useipv6,
3600 **kwds
3605 **kwds
3601 )
3606 )
3602 t.should_reload = True
3607 t.should_reload = True
3603 return t
3608 return t
3604
3609
3605 def _cleanup(self):
3610 def _cleanup(self):
3606 """Clean up state from this test invocation."""
3611 """Clean up state from this test invocation."""
3607 if self.options.keep_tmpdir:
3612 if self.options.keep_tmpdir:
3608 return
3613 return
3609
3614
3610 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3615 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3611 shutil.rmtree(self._hgtmp, True)
3616 shutil.rmtree(self._hgtmp, True)
3612 for f in self._createdfiles:
3617 for f in self._createdfiles:
3613 try:
3618 try:
3614 os.remove(f)
3619 os.remove(f)
3615 except OSError:
3620 except OSError:
3616 pass
3621 pass
3617
3622
3618 def _usecorrectpython(self):
3623 def _usecorrectpython(self):
3619 """Configure the environment to use the appropriate Python in tests."""
3624 """Configure the environment to use the appropriate Python in tests."""
3620 # Tests must use the same interpreter as us or bad things will happen.
3625 # Tests must use the same interpreter as us or bad things will happen.
3621 if WINDOWS and PYTHON3:
3626 if WINDOWS and PYTHON3:
3622 pyexe_names = [b'python', b'python3', b'python.exe']
3627 pyexe_names = [b'python', b'python3', b'python.exe']
3623 elif WINDOWS:
3628 elif WINDOWS:
3624 pyexe_names = [b'python', b'python.exe']
3629 pyexe_names = [b'python', b'python.exe']
3625 elif PYTHON3:
3630 elif PYTHON3:
3626 pyexe_names = [b'python', b'python3']
3631 pyexe_names = [b'python', b'python3']
3627 else:
3632 else:
3628 pyexe_names = [b'python', b'python2']
3633 pyexe_names = [b'python', b'python2']
3629
3634
3630 # os.symlink() is a thing with py3 on Windows, but it requires
3635 # os.symlink() is a thing with py3 on Windows, but it requires
3631 # Administrator rights.
3636 # Administrator rights.
3632 if not WINDOWS and getattr(os, 'symlink', None):
3637 if not WINDOWS and getattr(os, 'symlink', None):
3633 msg = "# Making python executable in test path a symlink to '%s'"
3638 msg = "# Making python executable in test path a symlink to '%s'"
3634 msg %= sysexecutable
3639 msg %= sysexecutable
3635 vlog(msg)
3640 vlog(msg)
3636 for pyexename in pyexe_names:
3641 for pyexename in pyexe_names:
3637 mypython = os.path.join(self._custom_bin_dir, pyexename)
3642 mypython = os.path.join(self._custom_bin_dir, pyexename)
3638 try:
3643 try:
3639 if os.readlink(mypython) == sysexecutable:
3644 if os.readlink(mypython) == sysexecutable:
3640 continue
3645 continue
3641 os.unlink(mypython)
3646 os.unlink(mypython)
3642 except OSError as err:
3647 except OSError as err:
3643 if err.errno != errno.ENOENT:
3648 if err.errno != errno.ENOENT:
3644 raise
3649 raise
3645 if self._findprogram(pyexename) != sysexecutable:
3650 if self._findprogram(pyexename) != sysexecutable:
3646 try:
3651 try:
3647 os.symlink(sysexecutable, mypython)
3652 os.symlink(sysexecutable, mypython)
3648 self._createdfiles.append(mypython)
3653 self._createdfiles.append(mypython)
3649 except OSError as err:
3654 except OSError as err:
3650 # child processes may race, which is harmless
3655 # child processes may race, which is harmless
3651 if err.errno != errno.EEXIST:
3656 if err.errno != errno.EEXIST:
3652 raise
3657 raise
3653 elif WINDOWS and not os.getenv('MSYSTEM'):
3658 elif WINDOWS and not os.getenv('MSYSTEM'):
3654 raise AssertionError('cannot run test on Windows without MSYSTEM')
3659 raise AssertionError('cannot run test on Windows without MSYSTEM')
3655 else:
3660 else:
3656 # Generate explicit file instead of symlink
3661 # Generate explicit file instead of symlink
3657 #
3662 #
3658 # This is especially important as Windows doesn't have
3663 # This is especially important as Windows doesn't have
3659 # `python3.exe`, and MSYS cannot understand the reparse point with
3664 # `python3.exe`, and MSYS cannot understand the reparse point with
3660 # that name provided by Microsoft. Create a simple script on PATH
3665 # that name provided by Microsoft. Create a simple script on PATH
3661 # with that name that delegates to the py3 launcher so the shebang
3666 # with that name that delegates to the py3 launcher so the shebang
3662 # lines work.
3667 # lines work.
3663 esc_executable = _sys2bytes(shellquote(sysexecutable))
3668 esc_executable = _sys2bytes(shellquote(sysexecutable))
3664 for pyexename in pyexe_names:
3669 for pyexename in pyexe_names:
3665 stub_exec_path = os.path.join(self._custom_bin_dir, pyexename)
3670 stub_exec_path = os.path.join(self._custom_bin_dir, pyexename)
3666 with open(stub_exec_path, 'wb') as f:
3671 with open(stub_exec_path, 'wb') as f:
3667 f.write(b'#!/bin/sh\n')
3672 f.write(b'#!/bin/sh\n')
3668 f.write(b'%s "$@"\n' % esc_executable)
3673 f.write(b'%s "$@"\n' % esc_executable)
3669
3674
3670 if WINDOWS:
3675 if WINDOWS:
3671 if not PYTHON3:
3676 if not PYTHON3:
3672 # lets try to build a valid python3 executable for the
3677 # lets try to build a valid python3 executable for the
3673 # scrip that requires it.
3678 # scrip that requires it.
3674 py3exe_name = os.path.join(self._custom_bin_dir, b'python3')
3679 py3exe_name = os.path.join(self._custom_bin_dir, b'python3')
3675 with open(py3exe_name, 'wb') as f:
3680 with open(py3exe_name, 'wb') as f:
3676 f.write(b'#!/bin/sh\n')
3681 f.write(b'#!/bin/sh\n')
3677 f.write(b'py -3 "$@"\n')
3682 f.write(b'py -3 "$@"\n')
3678
3683
3679 # adjust the path to make sur the main python finds it own dll
3684 # adjust the path to make sur the main python finds it own dll
3680 path = os.environ['PATH'].split(os.pathsep)
3685 path = os.environ['PATH'].split(os.pathsep)
3681 main_exec_dir = os.path.dirname(sysexecutable)
3686 main_exec_dir = os.path.dirname(sysexecutable)
3682 extra_paths = [_bytes2sys(self._custom_bin_dir), main_exec_dir]
3687 extra_paths = [_bytes2sys(self._custom_bin_dir), main_exec_dir]
3683
3688
3684 # Binaries installed by pip into the user area like pylint.exe may
3689 # Binaries installed by pip into the user area like pylint.exe may
3685 # not be in PATH by default.
3690 # not be in PATH by default.
3686 appdata = os.environ.get('APPDATA')
3691 appdata = os.environ.get('APPDATA')
3687 vi = sys.version_info
3692 vi = sys.version_info
3688 if appdata is not None:
3693 if appdata is not None:
3689 python_dir = 'Python%d%d' % (vi[0], vi[1])
3694 python_dir = 'Python%d%d' % (vi[0], vi[1])
3690 scripts_path = [appdata, 'Python', python_dir, 'Scripts']
3695 scripts_path = [appdata, 'Python', python_dir, 'Scripts']
3691 if not PYTHON3:
3696 if not PYTHON3:
3692 scripts_path = [appdata, 'Python', 'Scripts']
3697 scripts_path = [appdata, 'Python', 'Scripts']
3693 scripts_dir = os.path.join(*scripts_path)
3698 scripts_dir = os.path.join(*scripts_path)
3694 extra_paths.append(scripts_dir)
3699 extra_paths.append(scripts_dir)
3695
3700
3696 os.environ['PATH'] = os.pathsep.join(extra_paths + path)
3701 os.environ['PATH'] = os.pathsep.join(extra_paths + path)
3697
3702
3698 def _use_correct_mercurial(self):
3703 def _use_correct_mercurial(self):
3699 target_exec = os.path.join(self._custom_bin_dir, b'hg')
3704 target_exec = os.path.join(self._custom_bin_dir, b'hg')
3700 if self._hgcommand != b'hg':
3705 if self._hgcommand != b'hg':
3701 # shutil.which only accept bytes from 3.8
3706 # shutil.which only accept bytes from 3.8
3702 real_exec = which(self._hgcommand)
3707 real_exec = which(self._hgcommand)
3703 if real_exec is None:
3708 if real_exec is None:
3704 raise ValueError('could not find exec path for "%s"', real_exec)
3709 raise ValueError('could not find exec path for "%s"', real_exec)
3705 if real_exec == target_exec:
3710 if real_exec == target_exec:
3706 # do not overwrite something with itself
3711 # do not overwrite something with itself
3707 return
3712 return
3708 if WINDOWS:
3713 if WINDOWS:
3709 with open(target_exec, 'wb') as f:
3714 with open(target_exec, 'wb') as f:
3710 f.write(b'#!/bin/sh\n')
3715 f.write(b'#!/bin/sh\n')
3711 escaped_exec = shellquote(_bytes2sys(real_exec))
3716 escaped_exec = shellquote(_bytes2sys(real_exec))
3712 f.write(b'%s "$@"\n' % _sys2bytes(escaped_exec))
3717 f.write(b'%s "$@"\n' % _sys2bytes(escaped_exec))
3713 else:
3718 else:
3714 os.symlink(real_exec, target_exec)
3719 os.symlink(real_exec, target_exec)
3715 self._createdfiles.append(target_exec)
3720 self._createdfiles.append(target_exec)
3716
3721
3717 def _installhg(self):
3722 def _installhg(self):
3718 """Install hg into the test environment.
3723 """Install hg into the test environment.
3719
3724
3720 This will also configure hg with the appropriate testing settings.
3725 This will also configure hg with the appropriate testing settings.
3721 """
3726 """
3722 vlog("# Performing temporary installation of HG")
3727 vlog("# Performing temporary installation of HG")
3723 installerrs = os.path.join(self._hgtmp, b"install.err")
3728 installerrs = os.path.join(self._hgtmp, b"install.err")
3724 compiler = ''
3729 compiler = ''
3725 if self.options.compiler:
3730 if self.options.compiler:
3726 compiler = '--compiler ' + self.options.compiler
3731 compiler = '--compiler ' + self.options.compiler
3727 setup_opts = b""
3732 setup_opts = b""
3728 if self.options.pure:
3733 if self.options.pure:
3729 setup_opts = b"--pure"
3734 setup_opts = b"--pure"
3730 elif self.options.rust:
3735 elif self.options.rust:
3731 setup_opts = b"--rust"
3736 setup_opts = b"--rust"
3732 elif self.options.no_rust:
3737 elif self.options.no_rust:
3733 setup_opts = b"--no-rust"
3738 setup_opts = b"--no-rust"
3734
3739
3735 # Run installer in hg root
3740 # Run installer in hg root
3736 script = os.path.realpath(sys.argv[0])
3741 script = os.path.realpath(sys.argv[0])
3737 exe = sysexecutable
3742 exe = sysexecutable
3738 if PYTHON3:
3743 if PYTHON3:
3739 compiler = _sys2bytes(compiler)
3744 compiler = _sys2bytes(compiler)
3740 script = _sys2bytes(script)
3745 script = _sys2bytes(script)
3741 exe = _sys2bytes(exe)
3746 exe = _sys2bytes(exe)
3742 hgroot = os.path.dirname(os.path.dirname(script))
3747 hgroot = os.path.dirname(os.path.dirname(script))
3743 self._hgroot = hgroot
3748 self._hgroot = hgroot
3744 os.chdir(hgroot)
3749 os.chdir(hgroot)
3745 nohome = b'--home=""'
3750 nohome = b'--home=""'
3746 if WINDOWS:
3751 if WINDOWS:
3747 # The --home="" trick works only on OS where os.sep == '/'
3752 # The --home="" trick works only on OS where os.sep == '/'
3748 # because of a distutils convert_path() fast-path. Avoid it at
3753 # because of a distutils convert_path() fast-path. Avoid it at
3749 # least on Windows for now, deal with .pydistutils.cfg bugs
3754 # least on Windows for now, deal with .pydistutils.cfg bugs
3750 # when they happen.
3755 # when they happen.
3751 nohome = b''
3756 nohome = b''
3752 cmd = (
3757 cmd = (
3753 b'"%(exe)s" setup.py %(setup_opts)s clean --all'
3758 b'"%(exe)s" setup.py %(setup_opts)s clean --all'
3754 b' build %(compiler)s --build-base="%(base)s"'
3759 b' build %(compiler)s --build-base="%(base)s"'
3755 b' install --force --prefix="%(prefix)s"'
3760 b' install --force --prefix="%(prefix)s"'
3756 b' --install-lib="%(libdir)s"'
3761 b' --install-lib="%(libdir)s"'
3757 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3762 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3758 % {
3763 % {
3759 b'exe': exe,
3764 b'exe': exe,
3760 b'setup_opts': setup_opts,
3765 b'setup_opts': setup_opts,
3761 b'compiler': compiler,
3766 b'compiler': compiler,
3762 b'base': os.path.join(self._hgtmp, b"build"),
3767 b'base': os.path.join(self._hgtmp, b"build"),
3763 b'prefix': self._installdir,
3768 b'prefix': self._installdir,
3764 b'libdir': self._pythondir,
3769 b'libdir': self._pythondir,
3765 b'bindir': self._bindir,
3770 b'bindir': self._bindir,
3766 b'nohome': nohome,
3771 b'nohome': nohome,
3767 b'logfile': installerrs,
3772 b'logfile': installerrs,
3768 }
3773 }
3769 )
3774 )
3770
3775
3771 # setuptools requires install directories to exist.
3776 # setuptools requires install directories to exist.
3772 def makedirs(p):
3777 def makedirs(p):
3773 try:
3778 try:
3774 os.makedirs(p)
3779 os.makedirs(p)
3775 except OSError as e:
3780 except OSError as e:
3776 if e.errno != errno.EEXIST:
3781 if e.errno != errno.EEXIST:
3777 raise
3782 raise
3778
3783
3779 makedirs(self._pythondir)
3784 makedirs(self._pythondir)
3780 makedirs(self._bindir)
3785 makedirs(self._bindir)
3781
3786
3782 vlog("# Running", cmd.decode("utf-8"))
3787 vlog("# Running", cmd.decode("utf-8"))
3783 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3788 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3784 if not self.options.verbose:
3789 if not self.options.verbose:
3785 try:
3790 try:
3786 os.remove(installerrs)
3791 os.remove(installerrs)
3787 except OSError as e:
3792 except OSError as e:
3788 if e.errno != errno.ENOENT:
3793 if e.errno != errno.ENOENT:
3789 raise
3794 raise
3790 else:
3795 else:
3791 with open(installerrs, 'rb') as f:
3796 with open(installerrs, 'rb') as f:
3792 for line in f:
3797 for line in f:
3793 if PYTHON3:
3798 if PYTHON3:
3794 sys.stdout.buffer.write(line)
3799 sys.stdout.buffer.write(line)
3795 else:
3800 else:
3796 sys.stdout.write(line)
3801 sys.stdout.write(line)
3797 sys.exit(1)
3802 sys.exit(1)
3798 os.chdir(self._testdir)
3803 os.chdir(self._testdir)
3799
3804
3800 hgbat = os.path.join(self._bindir, b'hg.bat')
3805 hgbat = os.path.join(self._bindir, b'hg.bat')
3801 if os.path.isfile(hgbat):
3806 if os.path.isfile(hgbat):
3802 # hg.bat expects to be put in bin/scripts while run-tests.py
3807 # hg.bat expects to be put in bin/scripts while run-tests.py
3803 # installation layout put it in bin/ directly. Fix it
3808 # installation layout put it in bin/ directly. Fix it
3804 with open(hgbat, 'rb') as f:
3809 with open(hgbat, 'rb') as f:
3805 data = f.read()
3810 data = f.read()
3806 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3811 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3807 data = data.replace(
3812 data = data.replace(
3808 br'"%~dp0..\python" "%~dp0hg" %*',
3813 br'"%~dp0..\python" "%~dp0hg" %*',
3809 b'"%~dp0python" "%~dp0hg" %*',
3814 b'"%~dp0python" "%~dp0hg" %*',
3810 )
3815 )
3811 with open(hgbat, 'wb') as f:
3816 with open(hgbat, 'wb') as f:
3812 f.write(data)
3817 f.write(data)
3813 else:
3818 else:
3814 print('WARNING: cannot fix hg.bat reference to python.exe')
3819 print('WARNING: cannot fix hg.bat reference to python.exe')
3815
3820
3816 if self.options.anycoverage:
3821 if self.options.anycoverage:
3817 custom = os.path.join(
3822 custom = os.path.join(
3818 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3823 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3819 )
3824 )
3820 target = os.path.join(self._pythondir, b'sitecustomize.py')
3825 target = os.path.join(self._pythondir, b'sitecustomize.py')
3821 vlog('# Installing coverage trigger to %s' % target)
3826 vlog('# Installing coverage trigger to %s' % target)
3822 shutil.copyfile(custom, target)
3827 shutil.copyfile(custom, target)
3823 rc = os.path.join(self._testdir, b'.coveragerc')
3828 rc = os.path.join(self._testdir, b'.coveragerc')
3824 vlog('# Installing coverage rc to %s' % rc)
3829 vlog('# Installing coverage rc to %s' % rc)
3825 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3830 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3826 covdir = os.path.join(self._installdir, b'..', b'coverage')
3831 covdir = os.path.join(self._installdir, b'..', b'coverage')
3827 try:
3832 try:
3828 os.mkdir(covdir)
3833 os.mkdir(covdir)
3829 except OSError as e:
3834 except OSError as e:
3830 if e.errno != errno.EEXIST:
3835 if e.errno != errno.EEXIST:
3831 raise
3836 raise
3832
3837
3833 osenvironb[b'COVERAGE_DIR'] = covdir
3838 osenvironb[b'COVERAGE_DIR'] = covdir
3834
3839
3835 def _checkhglib(self, verb):
3840 def _checkhglib(self, verb):
3836 """Ensure that the 'mercurial' package imported by python is
3841 """Ensure that the 'mercurial' package imported by python is
3837 the one we expect it to be. If not, print a warning to stderr."""
3842 the one we expect it to be. If not, print a warning to stderr."""
3838 if self._pythondir_inferred:
3843 if self._pythondir_inferred:
3839 # The pythondir has been inferred from --with-hg flag.
3844 # The pythondir has been inferred from --with-hg flag.
3840 # We cannot expect anything sensible here.
3845 # We cannot expect anything sensible here.
3841 return
3846 return
3842 expecthg = os.path.join(self._pythondir, b'mercurial')
3847 expecthg = os.path.join(self._pythondir, b'mercurial')
3843 actualhg = self._gethgpath()
3848 actualhg = self._gethgpath()
3844 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3849 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3845 sys.stderr.write(
3850 sys.stderr.write(
3846 'warning: %s with unexpected mercurial lib: %s\n'
3851 'warning: %s with unexpected mercurial lib: %s\n'
3847 ' (expected %s)\n' % (verb, actualhg, expecthg)
3852 ' (expected %s)\n' % (verb, actualhg, expecthg)
3848 )
3853 )
3849
3854
3850 def _gethgpath(self):
3855 def _gethgpath(self):
3851 """Return the path to the mercurial package that is actually found by
3856 """Return the path to the mercurial package that is actually found by
3852 the current Python interpreter."""
3857 the current Python interpreter."""
3853 if self._hgpath is not None:
3858 if self._hgpath is not None:
3854 return self._hgpath
3859 return self._hgpath
3855
3860
3856 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3861 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3857 cmd = cmd % PYTHON
3862 cmd = cmd % PYTHON
3858 if PYTHON3:
3863 if PYTHON3:
3859 cmd = _bytes2sys(cmd)
3864 cmd = _bytes2sys(cmd)
3860
3865
3861 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3866 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3862 out, err = p.communicate()
3867 out, err = p.communicate()
3863
3868
3864 self._hgpath = out.strip()
3869 self._hgpath = out.strip()
3865
3870
3866 return self._hgpath
3871 return self._hgpath
3867
3872
3868 def _installchg(self):
3873 def _installchg(self):
3869 """Install chg into the test environment"""
3874 """Install chg into the test environment"""
3870 vlog('# Performing temporary installation of CHG')
3875 vlog('# Performing temporary installation of CHG')
3871 assert os.path.dirname(self._bindir) == self._installdir
3876 assert os.path.dirname(self._bindir) == self._installdir
3872 assert self._hgroot, 'must be called after _installhg()'
3877 assert self._hgroot, 'must be called after _installhg()'
3873 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3878 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3874 b'make': b'make', # TODO: switch by option or environment?
3879 b'make': b'make', # TODO: switch by option or environment?
3875 b'prefix': self._installdir,
3880 b'prefix': self._installdir,
3876 }
3881 }
3877 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3882 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3878 vlog("# Running", cmd)
3883 vlog("# Running", cmd)
3879 proc = subprocess.Popen(
3884 proc = subprocess.Popen(
3880 cmd,
3885 cmd,
3881 shell=True,
3886 shell=True,
3882 cwd=cwd,
3887 cwd=cwd,
3883 stdin=subprocess.PIPE,
3888 stdin=subprocess.PIPE,
3884 stdout=subprocess.PIPE,
3889 stdout=subprocess.PIPE,
3885 stderr=subprocess.STDOUT,
3890 stderr=subprocess.STDOUT,
3886 )
3891 )
3887 out, _err = proc.communicate()
3892 out, _err = proc.communicate()
3888 if proc.returncode != 0:
3893 if proc.returncode != 0:
3889 if PYTHON3:
3894 if PYTHON3:
3890 sys.stdout.buffer.write(out)
3895 sys.stdout.buffer.write(out)
3891 else:
3896 else:
3892 sys.stdout.write(out)
3897 sys.stdout.write(out)
3893 sys.exit(1)
3898 sys.exit(1)
3894
3899
3895 def _installrhg(self):
3900 def _installrhg(self):
3896 """Install rhg into the test environment"""
3901 """Install rhg into the test environment"""
3897 vlog('# Performing temporary installation of rhg')
3902 vlog('# Performing temporary installation of rhg')
3898 assert os.path.dirname(self._bindir) == self._installdir
3903 assert os.path.dirname(self._bindir) == self._installdir
3899 assert self._hgroot, 'must be called after _installhg()'
3904 assert self._hgroot, 'must be called after _installhg()'
3900 cmd = b'"%(make)s" install-rhg PREFIX="%(prefix)s"' % {
3905 cmd = b'"%(make)s" install-rhg PREFIX="%(prefix)s"' % {
3901 b'make': b'make', # TODO: switch by option or environment?
3906 b'make': b'make', # TODO: switch by option or environment?
3902 b'prefix': self._installdir,
3907 b'prefix': self._installdir,
3903 }
3908 }
3904 cwd = self._hgroot
3909 cwd = self._hgroot
3905 vlog("# Running", cmd)
3910 vlog("# Running", cmd)
3906 proc = subprocess.Popen(
3911 proc = subprocess.Popen(
3907 cmd,
3912 cmd,
3908 shell=True,
3913 shell=True,
3909 cwd=cwd,
3914 cwd=cwd,
3910 stdin=subprocess.PIPE,
3915 stdin=subprocess.PIPE,
3911 stdout=subprocess.PIPE,
3916 stdout=subprocess.PIPE,
3912 stderr=subprocess.STDOUT,
3917 stderr=subprocess.STDOUT,
3913 )
3918 )
3914 out, _err = proc.communicate()
3919 out, _err = proc.communicate()
3915 if proc.returncode != 0:
3920 if proc.returncode != 0:
3916 if PYTHON3:
3921 if PYTHON3:
3917 sys.stdout.buffer.write(out)
3922 sys.stdout.buffer.write(out)
3918 else:
3923 else:
3919 sys.stdout.write(out)
3924 sys.stdout.write(out)
3920 sys.exit(1)
3925 sys.exit(1)
3921
3926
3922 def _build_pyoxidized(self):
3927 def _build_pyoxidized(self):
3923 """build a pyoxidized version of mercurial into the test environment
3928 """build a pyoxidized version of mercurial into the test environment
3924
3929
3925 Ideally this function would be `install_pyoxidier` and would both build
3930 Ideally this function would be `install_pyoxidier` and would both build
3926 and install pyoxidier. However we are starting small to get pyoxidizer
3931 and install pyoxidier. However we are starting small to get pyoxidizer
3927 build binary to testing quickly.
3932 build binary to testing quickly.
3928 """
3933 """
3929 vlog('# build a pyoxidized version of Mercurial')
3934 vlog('# build a pyoxidized version of Mercurial')
3930 assert os.path.dirname(self._bindir) == self._installdir
3935 assert os.path.dirname(self._bindir) == self._installdir
3931 assert self._hgroot, 'must be called after _installhg()'
3936 assert self._hgroot, 'must be called after _installhg()'
3932 cmd = b'"%(make)s" pyoxidizer-windows-tests' % {
3937 cmd = b'"%(make)s" pyoxidizer-windows-tests' % {
3933 b'make': b'make',
3938 b'make': b'make',
3934 }
3939 }
3935 cwd = self._hgroot
3940 cwd = self._hgroot
3936 vlog("# Running", cmd)
3941 vlog("# Running", cmd)
3937 proc = subprocess.Popen(
3942 proc = subprocess.Popen(
3938 _bytes2sys(cmd),
3943 _bytes2sys(cmd),
3939 shell=True,
3944 shell=True,
3940 cwd=_bytes2sys(cwd),
3945 cwd=_bytes2sys(cwd),
3941 stdin=subprocess.PIPE,
3946 stdin=subprocess.PIPE,
3942 stdout=subprocess.PIPE,
3947 stdout=subprocess.PIPE,
3943 stderr=subprocess.STDOUT,
3948 stderr=subprocess.STDOUT,
3944 )
3949 )
3945 out, _err = proc.communicate()
3950 out, _err = proc.communicate()
3946 if proc.returncode != 0:
3951 if proc.returncode != 0:
3947 if PYTHON3:
3952 if PYTHON3:
3948 sys.stdout.buffer.write(out)
3953 sys.stdout.buffer.write(out)
3949 else:
3954 else:
3950 sys.stdout.write(out)
3955 sys.stdout.write(out)
3951 sys.exit(1)
3956 sys.exit(1)
3952
3957
3953 def _outputcoverage(self):
3958 def _outputcoverage(self):
3954 """Produce code coverage output."""
3959 """Produce code coverage output."""
3955 import coverage
3960 import coverage
3956
3961
3957 coverage = coverage.coverage
3962 coverage = coverage.coverage
3958
3963
3959 vlog('# Producing coverage report')
3964 vlog('# Producing coverage report')
3960 # chdir is the easiest way to get short, relative paths in the
3965 # chdir is the easiest way to get short, relative paths in the
3961 # output.
3966 # output.
3962 os.chdir(self._hgroot)
3967 os.chdir(self._hgroot)
3963 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3968 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3964 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3969 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3965
3970
3966 # Map install directory paths back to source directory.
3971 # Map install directory paths back to source directory.
3967 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3972 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3968
3973
3969 cov.combine()
3974 cov.combine()
3970
3975
3971 omit = [
3976 omit = [
3972 _bytes2sys(os.path.join(x, b'*'))
3977 _bytes2sys(os.path.join(x, b'*'))
3973 for x in [self._bindir, self._testdir]
3978 for x in [self._bindir, self._testdir]
3974 ]
3979 ]
3975 cov.report(ignore_errors=True, omit=omit)
3980 cov.report(ignore_errors=True, omit=omit)
3976
3981
3977 if self.options.htmlcov:
3982 if self.options.htmlcov:
3978 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3983 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3979 cov.html_report(directory=htmldir, omit=omit)
3984 cov.html_report(directory=htmldir, omit=omit)
3980 if self.options.annotate:
3985 if self.options.annotate:
3981 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3986 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3982 if not os.path.isdir(adir):
3987 if not os.path.isdir(adir):
3983 os.mkdir(adir)
3988 os.mkdir(adir)
3984 cov.annotate(directory=adir, omit=omit)
3989 cov.annotate(directory=adir, omit=omit)
3985
3990
3986 def _findprogram(self, program):
3991 def _findprogram(self, program):
3987 """Search PATH for a executable program"""
3992 """Search PATH for a executable program"""
3988 dpb = _sys2bytes(os.defpath)
3993 dpb = _sys2bytes(os.defpath)
3989 sepb = _sys2bytes(os.pathsep)
3994 sepb = _sys2bytes(os.pathsep)
3990 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3995 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3991 name = os.path.join(p, program)
3996 name = os.path.join(p, program)
3992 if WINDOWS or os.access(name, os.X_OK):
3997 if WINDOWS or os.access(name, os.X_OK):
3993 return _bytes2sys(name)
3998 return _bytes2sys(name)
3994 return None
3999 return None
3995
4000
3996 def _checktools(self):
4001 def _checktools(self):
3997 """Ensure tools required to run tests are present."""
4002 """Ensure tools required to run tests are present."""
3998 for p in self.REQUIREDTOOLS:
4003 for p in self.REQUIREDTOOLS:
3999 if WINDOWS and not p.endswith(b'.exe'):
4004 if WINDOWS and not p.endswith(b'.exe'):
4000 p += b'.exe'
4005 p += b'.exe'
4001 found = self._findprogram(p)
4006 found = self._findprogram(p)
4002 p = p.decode("utf-8")
4007 p = p.decode("utf-8")
4003 if found:
4008 if found:
4004 vlog("# Found prerequisite", p, "at", found)
4009 vlog("# Found prerequisite", p, "at", found)
4005 else:
4010 else:
4006 print("WARNING: Did not find prerequisite tool: %s " % p)
4011 print("WARNING: Did not find prerequisite tool: %s " % p)
4007
4012
4008
4013
4009 def aggregateexceptions(path):
4014 def aggregateexceptions(path):
4010 exceptioncounts = collections.Counter()
4015 exceptioncounts = collections.Counter()
4011 testsbyfailure = collections.defaultdict(set)
4016 testsbyfailure = collections.defaultdict(set)
4012 failuresbytest = collections.defaultdict(set)
4017 failuresbytest = collections.defaultdict(set)
4013
4018
4014 for f in os.listdir(path):
4019 for f in os.listdir(path):
4015 with open(os.path.join(path, f), 'rb') as fh:
4020 with open(os.path.join(path, f), 'rb') as fh:
4016 data = fh.read().split(b'\0')
4021 data = fh.read().split(b'\0')
4017 if len(data) != 5:
4022 if len(data) != 5:
4018 continue
4023 continue
4019
4024
4020 exc, mainframe, hgframe, hgline, testname = data
4025 exc, mainframe, hgframe, hgline, testname = data
4021 exc = exc.decode('utf-8')
4026 exc = exc.decode('utf-8')
4022 mainframe = mainframe.decode('utf-8')
4027 mainframe = mainframe.decode('utf-8')
4023 hgframe = hgframe.decode('utf-8')
4028 hgframe = hgframe.decode('utf-8')
4024 hgline = hgline.decode('utf-8')
4029 hgline = hgline.decode('utf-8')
4025 testname = testname.decode('utf-8')
4030 testname = testname.decode('utf-8')
4026
4031
4027 key = (hgframe, hgline, exc)
4032 key = (hgframe, hgline, exc)
4028 exceptioncounts[key] += 1
4033 exceptioncounts[key] += 1
4029 testsbyfailure[key].add(testname)
4034 testsbyfailure[key].add(testname)
4030 failuresbytest[testname].add(key)
4035 failuresbytest[testname].add(key)
4031
4036
4032 # Find test having fewest failures for each failure.
4037 # Find test having fewest failures for each failure.
4033 leastfailing = {}
4038 leastfailing = {}
4034 for key, tests in testsbyfailure.items():
4039 for key, tests in testsbyfailure.items():
4035 fewesttest = None
4040 fewesttest = None
4036 fewestcount = 99999999
4041 fewestcount = 99999999
4037 for test in sorted(tests):
4042 for test in sorted(tests):
4038 if len(failuresbytest[test]) < fewestcount:
4043 if len(failuresbytest[test]) < fewestcount:
4039 fewesttest = test
4044 fewesttest = test
4040 fewestcount = len(failuresbytest[test])
4045 fewestcount = len(failuresbytest[test])
4041
4046
4042 leastfailing[key] = (fewestcount, fewesttest)
4047 leastfailing[key] = (fewestcount, fewesttest)
4043
4048
4044 # Create a combined counter so we can sort by total occurrences and
4049 # Create a combined counter so we can sort by total occurrences and
4045 # impacted tests.
4050 # impacted tests.
4046 combined = {}
4051 combined = {}
4047 for key in exceptioncounts:
4052 for key in exceptioncounts:
4048 combined[key] = (
4053 combined[key] = (
4049 exceptioncounts[key],
4054 exceptioncounts[key],
4050 len(testsbyfailure[key]),
4055 len(testsbyfailure[key]),
4051 leastfailing[key][0],
4056 leastfailing[key][0],
4052 leastfailing[key][1],
4057 leastfailing[key][1],
4053 )
4058 )
4054
4059
4055 return {
4060 return {
4056 'exceptioncounts': exceptioncounts,
4061 'exceptioncounts': exceptioncounts,
4057 'total': sum(exceptioncounts.values()),
4062 'total': sum(exceptioncounts.values()),
4058 'combined': combined,
4063 'combined': combined,
4059 'leastfailing': leastfailing,
4064 'leastfailing': leastfailing,
4060 'byfailure': testsbyfailure,
4065 'byfailure': testsbyfailure,
4061 'bytest': failuresbytest,
4066 'bytest': failuresbytest,
4062 }
4067 }
4063
4068
4064
4069
4065 if __name__ == '__main__':
4070 if __name__ == '__main__':
4066 if WINDOWS and not os.getenv('MSYSTEM'):
4071 if WINDOWS and not os.getenv('MSYSTEM'):
4067 print('cannot run test on Windows without MSYSTEM', file=sys.stderr)
4072 print('cannot run test on Windows without MSYSTEM', file=sys.stderr)
4068 print(
4073 print(
4069 '(if you need to do so contact the mercurial devs: '
4074 '(if you need to do so contact the mercurial devs: '
4070 'mercurial@mercurial-scm.org)',
4075 'mercurial@mercurial-scm.org)',
4071 file=sys.stderr,
4076 file=sys.stderr,
4072 )
4077 )
4073 sys.exit(255)
4078 sys.exit(255)
4074
4079
4075 runner = TestRunner()
4080 runner = TestRunner()
4076
4081
4077 try:
4082 try:
4078 import msvcrt
4083 import msvcrt
4079
4084
4080 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
4085 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
4081 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
4086 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
4082 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
4087 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
4083 except ImportError:
4088 except ImportError:
4084 pass
4089 pass
4085
4090
4086 sys.exit(runner.run(sys.argv[1:]))
4091 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now