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