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