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