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