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