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