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