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