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