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