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