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