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