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