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