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