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