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