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