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