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