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