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