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