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