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