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