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