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