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