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