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