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