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