##// END OF EJS Templates
run-tests: add a dedicated 'isoptional' function...
marmoute -
r43171:75bd5990 default
parent child Browse files
Show More
@@ -1,3351 +1,3356 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"
1310
1311 def isoptional(line):
1312 return line.endswith(MARK_OPTIONAL)
1313
1309 class TTest(Test):
1314 class TTest(Test):
1310 """A "t test" is a test backed by a .t file."""
1315 """A "t test" is a test backed by a .t file."""
1311
1316
1312 SKIPPED_PREFIX = b'skipped: '
1317 SKIPPED_PREFIX = b'skipped: '
1313 FAILED_PREFIX = b'hghave check failed: '
1318 FAILED_PREFIX = b'hghave check failed: '
1314 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1319 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1315
1320
1316 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1321 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1317 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))
1318 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1323 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1319
1324
1320 def __init__(self, path, *args, **kwds):
1325 def __init__(self, path, *args, **kwds):
1321 # accept an extra "case" parameter
1326 # accept an extra "case" parameter
1322 case = kwds.pop('case', [])
1327 case = kwds.pop('case', [])
1323 self._case = case
1328 self._case = case
1324 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}
1325 super(TTest, self).__init__(path, *args, **kwds)
1330 super(TTest, self).__init__(path, *args, **kwds)
1326 if case:
1331 if case:
1327 casepath = b'#'.join(case)
1332 casepath = b'#'.join(case)
1328 self.name = '%s#%s' % (self.name, _strpath(casepath))
1333 self.name = '%s#%s' % (self.name, _strpath(casepath))
1329 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1334 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1330 self._tmpname += b'-%s' % casepath
1335 self._tmpname += b'-%s' % casepath
1331 self._have = {}
1336 self._have = {}
1332
1337
1333 @property
1338 @property
1334 def refpath(self):
1339 def refpath(self):
1335 return os.path.join(self._testdir, self.bname)
1340 return os.path.join(self._testdir, self.bname)
1336
1341
1337 def _run(self, env):
1342 def _run(self, env):
1338 with open(self.path, 'rb') as f:
1343 with open(self.path, 'rb') as f:
1339 lines = f.readlines()
1344 lines = f.readlines()
1340
1345
1341 # .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
1342 # output updated with the the test input. This avoids some race
1347 # output updated with the the test input. This avoids some race
1343 # conditions where the reference output does not match the actual test.
1348 # conditions where the reference output does not match the actual test.
1344 if self._refout is not None:
1349 if self._refout is not None:
1345 self._refout = lines
1350 self._refout = lines
1346
1351
1347 salt, script, after, expected = self._parsetest(lines)
1352 salt, script, after, expected = self._parsetest(lines)
1348
1353
1349 # Write out the generated script.
1354 # Write out the generated script.
1350 fname = b'%s.sh' % self._testtmp
1355 fname = b'%s.sh' % self._testtmp
1351 with open(fname, 'wb') as f:
1356 with open(fname, 'wb') as f:
1352 for l in script:
1357 for l in script:
1353 f.write(l)
1358 f.write(l)
1354
1359
1355 cmd = b'%s "%s"' % (self._shell, fname)
1360 cmd = b'%s "%s"' % (self._shell, fname)
1356 vlog("# Running", cmd)
1361 vlog("# Running", cmd)
1357
1362
1358 exitcode, output = self._runcommand(cmd, env)
1363 exitcode, output = self._runcommand(cmd, env)
1359
1364
1360 if self._aborted:
1365 if self._aborted:
1361 raise KeyboardInterrupt()
1366 raise KeyboardInterrupt()
1362
1367
1363 # Do not merge output if skipped. Return hghave message instead.
1368 # Do not merge output if skipped. Return hghave message instead.
1364 # Similarly, with --debug, output is None.
1369 # Similarly, with --debug, output is None.
1365 if exitcode == self.SKIPPED_STATUS or output is None:
1370 if exitcode == self.SKIPPED_STATUS or output is None:
1366 return exitcode, output
1371 return exitcode, output
1367
1372
1368 return self._processoutput(exitcode, output, salt, after, expected)
1373 return self._processoutput(exitcode, output, salt, after, expected)
1369
1374
1370 def _hghave(self, reqs):
1375 def _hghave(self, reqs):
1371 allreqs = b' '.join(reqs)
1376 allreqs = b' '.join(reqs)
1372
1377
1373 self._detectslow(reqs)
1378 self._detectslow(reqs)
1374
1379
1375 if allreqs in self._have:
1380 if allreqs in self._have:
1376 return self._have.get(allreqs)
1381 return self._have.get(allreqs)
1377
1382
1378 # 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.
1379 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1384 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1380 tdir = runtestdir.replace(b'\\', b'/')
1385 tdir = runtestdir.replace(b'\\', b'/')
1381 proc = Popen4(b'%s -c "%s/hghave %s"' %
1386 proc = Popen4(b'%s -c "%s/hghave %s"' %
1382 (self._shell, tdir, allreqs),
1387 (self._shell, tdir, allreqs),
1383 self._testtmp, 0, self._getenv())
1388 self._testtmp, 0, self._getenv())
1384 stdout, stderr = proc.communicate()
1389 stdout, stderr = proc.communicate()
1385 ret = proc.wait()
1390 ret = proc.wait()
1386 if wifexited(ret):
1391 if wifexited(ret):
1387 ret = os.WEXITSTATUS(ret)
1392 ret = os.WEXITSTATUS(ret)
1388 if ret == 2:
1393 if ret == 2:
1389 print(stdout.decode('utf-8'))
1394 print(stdout.decode('utf-8'))
1390 sys.exit(1)
1395 sys.exit(1)
1391
1396
1392 if ret != 0:
1397 if ret != 0:
1393 self._have[allreqs] = (False, stdout)
1398 self._have[allreqs] = (False, stdout)
1394 return False, stdout
1399 return False, stdout
1395
1400
1396 self._have[allreqs] = (True, None)
1401 self._have[allreqs] = (True, None)
1397 return True, None
1402 return True, None
1398
1403
1399 def _detectslow(self, reqs):
1404 def _detectslow(self, reqs):
1400 """update the timeout of slow test when appropriate"""
1405 """update the timeout of slow test when appropriate"""
1401 if b'slow' in reqs:
1406 if b'slow' in reqs:
1402 self._timeout = self._slowtimeout
1407 self._timeout = self._slowtimeout
1403
1408
1404 def _iftest(self, args):
1409 def _iftest(self, args):
1405 # implements "#if"
1410 # implements "#if"
1406 reqs = []
1411 reqs = []
1407 for arg in args:
1412 for arg in args:
1408 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1413 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1409 if arg[3:] in self._case:
1414 if arg[3:] in self._case:
1410 return False
1415 return False
1411 elif arg in self._allcases:
1416 elif arg in self._allcases:
1412 if arg not in self._case:
1417 if arg not in self._case:
1413 return False
1418 return False
1414 else:
1419 else:
1415 reqs.append(arg)
1420 reqs.append(arg)
1416 self._detectslow(reqs)
1421 self._detectslow(reqs)
1417 return self._hghave(reqs)[0]
1422 return self._hghave(reqs)[0]
1418
1423
1419 def _parsetest(self, lines):
1424 def _parsetest(self, lines):
1420 # We generate a shell script which outputs unique markers to line
1425 # We generate a shell script which outputs unique markers to line
1421 # up script results with our source. These markers include input
1426 # up script results with our source. These markers include input
1422 # line number and the last return code.
1427 # line number and the last return code.
1423 salt = b"SALT%d" % time.time()
1428 salt = b"SALT%d" % time.time()
1424 def addsalt(line, inpython):
1429 def addsalt(line, inpython):
1425 if inpython:
1430 if inpython:
1426 script.append(b'%s %d 0\n' % (salt, line))
1431 script.append(b'%s %d 0\n' % (salt, line))
1427 else:
1432 else:
1428 script.append(b'echo %s %d $?\n' % (salt, line))
1433 script.append(b'echo %s %d $?\n' % (salt, line))
1429 activetrace = []
1434 activetrace = []
1430 session = str(uuid.uuid4())
1435 session = str(uuid.uuid4())
1431 if PYTHON3:
1436 if PYTHON3:
1432 session = session.encode('ascii')
1437 session = session.encode('ascii')
1433 hgcatapult = (os.getenv('HGTESTCATAPULTSERVERPIPE') or
1438 hgcatapult = (os.getenv('HGTESTCATAPULTSERVERPIPE') or
1434 os.getenv('HGCATAPULTSERVERPIPE'))
1439 os.getenv('HGCATAPULTSERVERPIPE'))
1435 def toggletrace(cmd=None):
1440 def toggletrace(cmd=None):
1436 if not hgcatapult or hgcatapult == os.devnull:
1441 if not hgcatapult or hgcatapult == os.devnull:
1437 return
1442 return
1438
1443
1439 if activetrace:
1444 if activetrace:
1440 script.append(
1445 script.append(
1441 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (
1446 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (
1442 session, activetrace[0]))
1447 session, activetrace[0]))
1443 if cmd is None:
1448 if cmd is None:
1444 return
1449 return
1445
1450
1446 if isinstance(cmd, str):
1451 if isinstance(cmd, str):
1447 quoted = shellquote(cmd.strip())
1452 quoted = shellquote(cmd.strip())
1448 else:
1453 else:
1449 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1454 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1450 quoted = quoted.replace(b'\\', b'\\\\')
1455 quoted = quoted.replace(b'\\', b'\\\\')
1451 script.append(
1456 script.append(
1452 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (
1457 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (
1453 session, quoted))
1458 session, quoted))
1454 activetrace[0:] = [quoted]
1459 activetrace[0:] = [quoted]
1455
1460
1456 script = []
1461 script = []
1457
1462
1458 # 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
1459 # with non-active parts of the source, with synchronization by our
1464 # with non-active parts of the source, with synchronization by our
1460 # SALT line number markers. The after table contains the non-active
1465 # SALT line number markers. The after table contains the non-active
1461 # components, ordered by line number.
1466 # components, ordered by line number.
1462 after = {}
1467 after = {}
1463
1468
1464 # Expected shell script output.
1469 # Expected shell script output.
1465 expected = {}
1470 expected = {}
1466
1471
1467 pos = prepos = -1
1472 pos = prepos = -1
1468
1473
1469 # True or False when in a true or false conditional section
1474 # True or False when in a true or false conditional section
1470 skipping = None
1475 skipping = None
1471
1476
1472 # 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
1473 # can generate the surrounding doctest magic.
1478 # can generate the surrounding doctest magic.
1474 inpython = False
1479 inpython = False
1475
1480
1476 if self._debug:
1481 if self._debug:
1477 script.append(b'set -x\n')
1482 script.append(b'set -x\n')
1478 if self._hgcommand != b'hg':
1483 if self._hgcommand != b'hg':
1479 script.append(b'alias hg="%s"\n' % self._hgcommand)
1484 script.append(b'alias hg="%s"\n' % self._hgcommand)
1480 if os.getenv('MSYSTEM'):
1485 if os.getenv('MSYSTEM'):
1481 script.append(b'alias pwd="pwd -W"\n')
1486 script.append(b'alias pwd="pwd -W"\n')
1482
1487
1483 if hgcatapult and hgcatapult != os.devnull:
1488 if hgcatapult and hgcatapult != os.devnull:
1484 if PYTHON3:
1489 if PYTHON3:
1485 hgcatapult = hgcatapult.encode('utf8')
1490 hgcatapult = hgcatapult.encode('utf8')
1486 cataname = self.name.encode('utf8')
1491 cataname = self.name.encode('utf8')
1487 else:
1492 else:
1488 cataname = self.name
1493 cataname = self.name
1489
1494
1490 # Kludge: use a while loop to keep the pipe from getting
1495 # Kludge: use a while loop to keep the pipe from getting
1491 # closed by our echo commands. The still-running file gets
1496 # closed by our echo commands. The still-running file gets
1492 # reaped at the end of the script, which causes the while
1497 # reaped at the end of the script, which causes the while
1493 # loop to exit and closes the pipe. Sigh.
1498 # loop to exit and closes the pipe. Sigh.
1494 script.append(
1499 script.append(
1495 b'rtendtracing() {\n'
1500 b'rtendtracing() {\n'
1496 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1501 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1497 b' rm -f "$TESTTMP/.still-running"\n'
1502 b' rm -f "$TESTTMP/.still-running"\n'
1498 b'}\n'
1503 b'}\n'
1499 b'trap "rtendtracing" 0\n'
1504 b'trap "rtendtracing" 0\n'
1500 b'touch "$TESTTMP/.still-running"\n'
1505 b'touch "$TESTTMP/.still-running"\n'
1501 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1506 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1502 b'> %(catapult)s &\n'
1507 b'> %(catapult)s &\n'
1503 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1508 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1504 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1509 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1505 % {
1510 % {
1506 b'name': cataname,
1511 b'name': cataname,
1507 b'session': session,
1512 b'session': session,
1508 b'catapult': hgcatapult,
1513 b'catapult': hgcatapult,
1509 }
1514 }
1510 )
1515 )
1511
1516
1512 if self._case:
1517 if self._case:
1513 casestr = b'#'.join(self._case)
1518 casestr = b'#'.join(self._case)
1514 if isinstance(self._case, str):
1519 if isinstance(self._case, str):
1515 quoted = shellquote(casestr)
1520 quoted = shellquote(casestr)
1516 else:
1521 else:
1517 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1522 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1518 script.append(b'TESTCASE=%s\n' % quoted)
1523 script.append(b'TESTCASE=%s\n' % quoted)
1519 script.append(b'export TESTCASE\n')
1524 script.append(b'export TESTCASE\n')
1520
1525
1521 n = 0
1526 n = 0
1522 for n, l in enumerate(lines):
1527 for n, l in enumerate(lines):
1523 if not l.endswith(b'\n'):
1528 if not l.endswith(b'\n'):
1524 l += b'\n'
1529 l += b'\n'
1525 if l.startswith(b'#require'):
1530 if l.startswith(b'#require'):
1526 lsplit = l.split()
1531 lsplit = l.split()
1527 if len(lsplit) < 2 or lsplit[0] != b'#require':
1532 if len(lsplit) < 2 or lsplit[0] != b'#require':
1528 after.setdefault(pos, []).append(' !!! invalid #require\n')
1533 after.setdefault(pos, []).append(' !!! invalid #require\n')
1529 if not skipping:
1534 if not skipping:
1530 haveresult, message = self._hghave(lsplit[1:])
1535 haveresult, message = self._hghave(lsplit[1:])
1531 if not haveresult:
1536 if not haveresult:
1532 script = [b'echo "%s"\nexit 80\n' % message]
1537 script = [b'echo "%s"\nexit 80\n' % message]
1533 break
1538 break
1534 after.setdefault(pos, []).append(l)
1539 after.setdefault(pos, []).append(l)
1535 elif l.startswith(b'#if'):
1540 elif l.startswith(b'#if'):
1536 lsplit = l.split()
1541 lsplit = l.split()
1537 if len(lsplit) < 2 or lsplit[0] != b'#if':
1542 if len(lsplit) < 2 or lsplit[0] != b'#if':
1538 after.setdefault(pos, []).append(' !!! invalid #if\n')
1543 after.setdefault(pos, []).append(' !!! invalid #if\n')
1539 if skipping is not None:
1544 if skipping is not None:
1540 after.setdefault(pos, []).append(' !!! nested #if\n')
1545 after.setdefault(pos, []).append(' !!! nested #if\n')
1541 skipping = not self._iftest(lsplit[1:])
1546 skipping = not self._iftest(lsplit[1:])
1542 after.setdefault(pos, []).append(l)
1547 after.setdefault(pos, []).append(l)
1543 elif l.startswith(b'#else'):
1548 elif l.startswith(b'#else'):
1544 if skipping is None:
1549 if skipping is None:
1545 after.setdefault(pos, []).append(' !!! missing #if\n')
1550 after.setdefault(pos, []).append(' !!! missing #if\n')
1546 skipping = not skipping
1551 skipping = not skipping
1547 after.setdefault(pos, []).append(l)
1552 after.setdefault(pos, []).append(l)
1548 elif l.startswith(b'#endif'):
1553 elif l.startswith(b'#endif'):
1549 if skipping is None:
1554 if skipping is None:
1550 after.setdefault(pos, []).append(' !!! missing #if\n')
1555 after.setdefault(pos, []).append(' !!! missing #if\n')
1551 skipping = None
1556 skipping = None
1552 after.setdefault(pos, []).append(l)
1557 after.setdefault(pos, []).append(l)
1553 elif skipping:
1558 elif skipping:
1554 after.setdefault(pos, []).append(l)
1559 after.setdefault(pos, []).append(l)
1555 elif l.startswith(b' >>> '): # python inlines
1560 elif l.startswith(b' >>> '): # python inlines
1556 after.setdefault(pos, []).append(l)
1561 after.setdefault(pos, []).append(l)
1557 prepos = pos
1562 prepos = pos
1558 pos = n
1563 pos = n
1559 if not inpython:
1564 if not inpython:
1560 # We've just entered a Python block. Add the header.
1565 # We've just entered a Python block. Add the header.
1561 inpython = True
1566 inpython = True
1562 addsalt(prepos, False) # Make sure we report the exit code.
1567 addsalt(prepos, False) # Make sure we report the exit code.
1563 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1568 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1564 addsalt(n, True)
1569 addsalt(n, True)
1565 script.append(l[2:])
1570 script.append(l[2:])
1566 elif l.startswith(b' ... '): # python inlines
1571 elif l.startswith(b' ... '): # python inlines
1567 after.setdefault(prepos, []).append(l)
1572 after.setdefault(prepos, []).append(l)
1568 script.append(l[2:])
1573 script.append(l[2:])
1569 elif l.startswith(b' $ '): # commands
1574 elif l.startswith(b' $ '): # commands
1570 if inpython:
1575 if inpython:
1571 script.append(b'EOF\n')
1576 script.append(b'EOF\n')
1572 inpython = False
1577 inpython = False
1573 after.setdefault(pos, []).append(l)
1578 after.setdefault(pos, []).append(l)
1574 prepos = pos
1579 prepos = pos
1575 pos = n
1580 pos = n
1576 addsalt(n, False)
1581 addsalt(n, False)
1577 rawcmd = l[4:]
1582 rawcmd = l[4:]
1578 cmd = rawcmd.split()
1583 cmd = rawcmd.split()
1579 toggletrace(rawcmd)
1584 toggletrace(rawcmd)
1580 if len(cmd) == 2 and cmd[0] == b'cd':
1585 if len(cmd) == 2 and cmd[0] == b'cd':
1581 l = b' $ cd %s || exit 1\n' % cmd[1]
1586 l = b' $ cd %s || exit 1\n' % cmd[1]
1582 script.append(rawcmd)
1587 script.append(rawcmd)
1583 elif l.startswith(b' > '): # continuations
1588 elif l.startswith(b' > '): # continuations
1584 after.setdefault(prepos, []).append(l)
1589 after.setdefault(prepos, []).append(l)
1585 script.append(l[4:])
1590 script.append(l[4:])
1586 elif l.startswith(b' '): # results
1591 elif l.startswith(b' '): # results
1587 # Queue up a list of expected results.
1592 # Queue up a list of expected results.
1588 expected.setdefault(pos, []).append(l[2:])
1593 expected.setdefault(pos, []).append(l[2:])
1589 else:
1594 else:
1590 if inpython:
1595 if inpython:
1591 script.append(b'EOF\n')
1596 script.append(b'EOF\n')
1592 inpython = False
1597 inpython = False
1593 # Non-command/result. Queue up for merged output.
1598 # Non-command/result. Queue up for merged output.
1594 after.setdefault(pos, []).append(l)
1599 after.setdefault(pos, []).append(l)
1595
1600
1596 if inpython:
1601 if inpython:
1597 script.append(b'EOF\n')
1602 script.append(b'EOF\n')
1598 if skipping is not None:
1603 if skipping is not None:
1599 after.setdefault(pos, []).append(' !!! missing #endif\n')
1604 after.setdefault(pos, []).append(' !!! missing #endif\n')
1600 addsalt(n + 1, False)
1605 addsalt(n + 1, False)
1601 # Need to end any current per-command trace
1606 # Need to end any current per-command trace
1602 if activetrace:
1607 if activetrace:
1603 toggletrace()
1608 toggletrace()
1604 return salt, script, after, expected
1609 return salt, script, after, expected
1605
1610
1606 def _processoutput(self, exitcode, output, salt, after, expected):
1611 def _processoutput(self, exitcode, output, salt, after, expected):
1607 # Merge the script output back into a unified test.
1612 # Merge the script output back into a unified test.
1608 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
1609 if exitcode != 0:
1614 if exitcode != 0:
1610 warnonly = WARN_NO
1615 warnonly = WARN_NO
1611
1616
1612 pos = -1
1617 pos = -1
1613 postout = []
1618 postout = []
1614 for out_rawline in output:
1619 for out_rawline in output:
1615 out_line, cmd_line = out_rawline, None
1620 out_line, cmd_line = out_rawline, None
1616 if salt in out_rawline:
1621 if salt in out_rawline:
1617 out_line, cmd_line = out_rawline.split(salt, 1)
1622 out_line, cmd_line = out_rawline.split(salt, 1)
1618
1623
1619 pos, postout, warnonly = self._process_out_line(out_line,
1624 pos, postout, warnonly = self._process_out_line(out_line,
1620 pos,
1625 pos,
1621 postout,
1626 postout,
1622 expected,
1627 expected,
1623 warnonly)
1628 warnonly)
1624 pos, postout = self._process_cmd_line(cmd_line, pos, postout,
1629 pos, postout = self._process_cmd_line(cmd_line, pos, postout,
1625 after)
1630 after)
1626
1631
1627 if pos in after:
1632 if pos in after:
1628 postout += after.pop(pos)
1633 postout += after.pop(pos)
1629
1634
1630 if warnonly == WARN_YES:
1635 if warnonly == WARN_YES:
1631 exitcode = False # Set exitcode to warned.
1636 exitcode = False # Set exitcode to warned.
1632
1637
1633 return exitcode, postout
1638 return exitcode, postout
1634
1639
1635 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1640 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1636 while out_line:
1641 while out_line:
1637 if not out_line.endswith(b'\n'):
1642 if not out_line.endswith(b'\n'):
1638 out_line += b' (no-eol)\n'
1643 out_line += b' (no-eol)\n'
1639
1644
1640 # Find the expected output at the current position.
1645 # Find the expected output at the current position.
1641 els = [None]
1646 els = [None]
1642 if expected.get(pos, None):
1647 if expected.get(pos, None):
1643 els = expected[pos]
1648 els = expected[pos]
1644
1649
1645 optional = []
1650 optional = []
1646 for i, el in enumerate(els):
1651 for i, el in enumerate(els):
1647 r = False
1652 r = False
1648 if el:
1653 if el:
1649 r, exact = self.linematch(el, out_line)
1654 r, exact = self.linematch(el, out_line)
1650 if isinstance(r, str):
1655 if isinstance(r, str):
1651 if r == '-glob':
1656 if r == '-glob':
1652 out_line = ''.join(el.rsplit(' (glob)', 1))
1657 out_line = ''.join(el.rsplit(' (glob)', 1))
1653 r = '' # Warn only this line.
1658 r = '' # Warn only this line.
1654 elif r == "retry":
1659 elif r == "retry":
1655 postout.append(b' ' + el)
1660 postout.append(b' ' + el)
1656 else:
1661 else:
1657 log('\ninfo, unknown linematch result: %r\n' % r)
1662 log('\ninfo, unknown linematch result: %r\n' % r)
1658 r = False
1663 r = False
1659 if r:
1664 if r:
1660 els.pop(i)
1665 els.pop(i)
1661 break
1666 break
1662 if el:
1667 if el:
1663 if el.endswith(b" (?)\n"):
1668 if isoptional(el):
1664 optional.append(i)
1669 optional.append(i)
1665 else:
1670 else:
1666 m = optline.match(el)
1671 m = optline.match(el)
1667 if m:
1672 if m:
1668 conditions = [
1673 conditions = [
1669 c for c in m.group(2).split(b' ')]
1674 c for c in m.group(2).split(b' ')]
1670
1675
1671 if not self._iftest(conditions):
1676 if not self._iftest(conditions):
1672 optional.append(i)
1677 optional.append(i)
1673 if exact:
1678 if exact:
1674 # Don't allow line to be matches against a later
1679 # Don't allow line to be matches against a later
1675 # line in the output
1680 # line in the output
1676 els.pop(i)
1681 els.pop(i)
1677 break
1682 break
1678
1683
1679 if r:
1684 if r:
1680 if r == "retry":
1685 if r == "retry":
1681 continue
1686 continue
1682 # clean up any optional leftovers
1687 # clean up any optional leftovers
1683 for i in optional:
1688 for i in optional:
1684 postout.append(b' ' + els[i])
1689 postout.append(b' ' + els[i])
1685 for i in reversed(optional):
1690 for i in reversed(optional):
1686 del els[i]
1691 del els[i]
1687 postout.append(b' ' + el)
1692 postout.append(b' ' + el)
1688 else:
1693 else:
1689 if self.NEEDESCAPE(out_line):
1694 if self.NEEDESCAPE(out_line):
1690 out_line = TTest._stringescape(b'%s (esc)\n' %
1695 out_line = TTest._stringescape(b'%s (esc)\n' %
1691 out_line.rstrip(b'\n'))
1696 out_line.rstrip(b'\n'))
1692 postout.append(b' ' + out_line) # Let diff deal with it.
1697 postout.append(b' ' + out_line) # Let diff deal with it.
1693 if r != '': # If line failed.
1698 if r != '': # If line failed.
1694 warnonly = WARN_NO
1699 warnonly = WARN_NO
1695 elif warnonly == WARN_UNDEFINED:
1700 elif warnonly == WARN_UNDEFINED:
1696 warnonly = WARN_YES
1701 warnonly = WARN_YES
1697 break
1702 break
1698 else:
1703 else:
1699 # clean up any optional leftovers
1704 # clean up any optional leftovers
1700 while expected.get(pos, None):
1705 while expected.get(pos, None):
1701 el = expected[pos].pop(0)
1706 el = expected[pos].pop(0)
1702 if el:
1707 if el:
1703 if not el.endswith(b" (?)\n"):
1708 if not isoptional(el):
1704 m = optline.match(el)
1709 m = optline.match(el)
1705 if m:
1710 if m:
1706 conditions = [c for c in m.group(2).split(b' ')]
1711 conditions = [c for c in m.group(2).split(b' ')]
1707
1712
1708 if self._iftest(conditions):
1713 if self._iftest(conditions):
1709 # Don't append as optional line
1714 # Don't append as optional line
1710 continue
1715 continue
1711 else:
1716 else:
1712 continue
1717 continue
1713 postout.append(b' ' + el)
1718 postout.append(b' ' + el)
1714 return pos, postout, warnonly
1719 return pos, postout, warnonly
1715
1720
1716 def _process_cmd_line(self, cmd_line, pos, postout, after):
1721 def _process_cmd_line(self, cmd_line, pos, postout, after):
1717 """process a "command" part of a line from unified test output"""
1722 """process a "command" part of a line from unified test output"""
1718 if cmd_line:
1723 if cmd_line:
1719 # Add on last return code.
1724 # Add on last return code.
1720 ret = int(cmd_line.split()[1])
1725 ret = int(cmd_line.split()[1])
1721 if ret != 0:
1726 if ret != 0:
1722 postout.append(b' [%d]\n' % ret)
1727 postout.append(b' [%d]\n' % ret)
1723 if pos in after:
1728 if pos in after:
1724 # Merge in non-active test bits.
1729 # Merge in non-active test bits.
1725 postout += after.pop(pos)
1730 postout += after.pop(pos)
1726 pos = int(cmd_line.split()[0])
1731 pos = int(cmd_line.split()[0])
1727 return pos, postout
1732 return pos, postout
1728
1733
1729 @staticmethod
1734 @staticmethod
1730 def rematch(el, l):
1735 def rematch(el, l):
1731 try:
1736 try:
1732 el = b'(?:' + el + b')'
1737 el = b'(?:' + el + b')'
1733 # 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
1734 if os.name == 'nt':
1739 if os.name == 'nt':
1735 return re.match(el + br'\r?\n\Z', l)
1740 return re.match(el + br'\r?\n\Z', l)
1736 return re.match(el + br'\n\Z', l)
1741 return re.match(el + br'\n\Z', l)
1737 except re.error:
1742 except re.error:
1738 # el is an invalid regex
1743 # el is an invalid regex
1739 return False
1744 return False
1740
1745
1741 @staticmethod
1746 @staticmethod
1742 def globmatch(el, l):
1747 def globmatch(el, l):
1743 # The only supported special characters are * and ? plus / which also
1748 # The only supported special characters are * and ? plus / which also
1744 # matches \ on windows. Escaping of these characters is supported.
1749 # matches \ on windows. Escaping of these characters is supported.
1745 if el + b'\n' == l:
1750 if el + b'\n' == l:
1746 if os.altsep:
1751 if os.altsep:
1747 # matching on "/" is not needed for this line
1752 # matching on "/" is not needed for this line
1748 for pat in checkcodeglobpats:
1753 for pat in checkcodeglobpats:
1749 if pat.match(el):
1754 if pat.match(el):
1750 return True
1755 return True
1751 return b'-glob'
1756 return b'-glob'
1752 return True
1757 return True
1753 el = el.replace(b'$LOCALIP', b'*')
1758 el = el.replace(b'$LOCALIP', b'*')
1754 i, n = 0, len(el)
1759 i, n = 0, len(el)
1755 res = b''
1760 res = b''
1756 while i < n:
1761 while i < n:
1757 c = el[i:i + 1]
1762 c = el[i:i + 1]
1758 i += 1
1763 i += 1
1759 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'*?\\/':
1760 res += el[i - 1:i + 1]
1765 res += el[i - 1:i + 1]
1761 i += 1
1766 i += 1
1762 elif c == b'*':
1767 elif c == b'*':
1763 res += b'.*'
1768 res += b'.*'
1764 elif c == b'?':
1769 elif c == b'?':
1765 res += b'.'
1770 res += b'.'
1766 elif c == b'/' and os.altsep:
1771 elif c == b'/' and os.altsep:
1767 res += b'[/\\\\]'
1772 res += b'[/\\\\]'
1768 else:
1773 else:
1769 res += re.escape(c)
1774 res += re.escape(c)
1770 return TTest.rematch(res, l)
1775 return TTest.rematch(res, l)
1771
1776
1772 def linematch(self, el, l):
1777 def linematch(self, el, l):
1773 if el == l: # perfect match (fast)
1778 if el == l: # perfect match (fast)
1774 return True, True
1779 return True, True
1775 retry = False
1780 retry = False
1776 if el.endswith(b" (?)\n"):
1781 if isoptional(el):
1777 retry = "retry"
1782 retry = "retry"
1778 el = el[:-5] + b"\n"
1783 el = el[:-len(MARK_OPTIONAL)] + b"\n"
1779 else:
1784 else:
1780 m = optline.match(el)
1785 m = optline.match(el)
1781 if m:
1786 if m:
1782 conditions = [c for c in m.group(2).split(b' ')]
1787 conditions = [c for c in m.group(2).split(b' ')]
1783
1788
1784 el = m.group(1) + b"\n"
1789 el = m.group(1) + b"\n"
1785 if not self._iftest(conditions):
1790 if not self._iftest(conditions):
1786 # listed feature missing, should not match
1791 # listed feature missing, should not match
1787 return "retry", False
1792 return "retry", False
1788
1793
1789 if el.endswith(b" (esc)\n"):
1794 if el.endswith(b" (esc)\n"):
1790 if PYTHON3:
1795 if PYTHON3:
1791 el = el[:-7].decode('unicode_escape') + '\n'
1796 el = el[:-7].decode('unicode_escape') + '\n'
1792 el = el.encode('utf-8')
1797 el = el.encode('utf-8')
1793 else:
1798 else:
1794 el = el[:-7].decode('string-escape') + '\n'
1799 el = el[:-7].decode('string-escape') + '\n'
1795 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:
1796 return True, True
1801 return True, True
1797 if el.endswith(b" (re)\n"):
1802 if el.endswith(b" (re)\n"):
1798 return (TTest.rematch(el[:-6], l) or retry), False
1803 return (TTest.rematch(el[:-6], l) or retry), False
1799 if el.endswith(b" (glob)\n"):
1804 if el.endswith(b" (glob)\n"):
1800 # ignore '(glob)' added to l by 'replacements'
1805 # ignore '(glob)' added to l by 'replacements'
1801 if l.endswith(b" (glob)\n"):
1806 if l.endswith(b" (glob)\n"):
1802 l = l[:-8] + b"\n"
1807 l = l[:-8] + b"\n"
1803 return (TTest.globmatch(el[:-8], l) or retry), False
1808 return (TTest.globmatch(el[:-8], l) or retry), False
1804 if os.altsep:
1809 if os.altsep:
1805 _l = l.replace(b'\\', b'/')
1810 _l = l.replace(b'\\', b'/')
1806 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:
1807 return True, True
1812 return True, True
1808 return retry, True
1813 return retry, True
1809
1814
1810 @staticmethod
1815 @staticmethod
1811 def parsehghaveoutput(lines):
1816 def parsehghaveoutput(lines):
1812 '''Parse hghave log lines.
1817 '''Parse hghave log lines.
1813
1818
1814 Return tuple of lists (missing, failed):
1819 Return tuple of lists (missing, failed):
1815 * the missing/unknown features
1820 * the missing/unknown features
1816 * the features for which existence check failed'''
1821 * the features for which existence check failed'''
1817 missing = []
1822 missing = []
1818 failed = []
1823 failed = []
1819 for line in lines:
1824 for line in lines:
1820 if line.startswith(TTest.SKIPPED_PREFIX):
1825 if line.startswith(TTest.SKIPPED_PREFIX):
1821 line = line.splitlines()[0]
1826 line = line.splitlines()[0]
1822 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1827 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1823 elif line.startswith(TTest.FAILED_PREFIX):
1828 elif line.startswith(TTest.FAILED_PREFIX):
1824 line = line.splitlines()[0]
1829 line = line.splitlines()[0]
1825 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1830 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1826
1831
1827 return missing, failed
1832 return missing, failed
1828
1833
1829 @staticmethod
1834 @staticmethod
1830 def _escapef(m):
1835 def _escapef(m):
1831 return TTest.ESCAPEMAP[m.group(0)]
1836 return TTest.ESCAPEMAP[m.group(0)]
1832
1837
1833 @staticmethod
1838 @staticmethod
1834 def _stringescape(s):
1839 def _stringescape(s):
1835 return TTest.ESCAPESUB(TTest._escapef, s)
1840 return TTest.ESCAPESUB(TTest._escapef, s)
1836
1841
1837 iolock = threading.RLock()
1842 iolock = threading.RLock()
1838 firstlock = threading.RLock()
1843 firstlock = threading.RLock()
1839 firsterror = False
1844 firsterror = False
1840
1845
1841 class TestResult(unittest._TextTestResult):
1846 class TestResult(unittest._TextTestResult):
1842 """Holds results when executing via unittest."""
1847 """Holds results when executing via unittest."""
1843 # Don't worry too much about accessing the non-public _TextTestResult.
1848 # Don't worry too much about accessing the non-public _TextTestResult.
1844 # It is relatively common in Python testing tools.
1849 # It is relatively common in Python testing tools.
1845 def __init__(self, options, *args, **kwargs):
1850 def __init__(self, options, *args, **kwargs):
1846 super(TestResult, self).__init__(*args, **kwargs)
1851 super(TestResult, self).__init__(*args, **kwargs)
1847
1852
1848 self._options = options
1853 self._options = options
1849
1854
1850 # 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
1851 # polyfill it.
1856 # polyfill it.
1852 self.skipped = []
1857 self.skipped = []
1853
1858
1854 # 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
1855 # unittest implementation. It is very similar to skipped. It may make
1860 # unittest implementation. It is very similar to skipped. It may make
1856 # sense to map it into skip some day.
1861 # sense to map it into skip some day.
1857 self.ignored = []
1862 self.ignored = []
1858
1863
1859 self.times = []
1864 self.times = []
1860 self._firststarttime = None
1865 self._firststarttime = None
1861 # Data stored for the benefit of generating xunit reports.
1866 # Data stored for the benefit of generating xunit reports.
1862 self.successes = []
1867 self.successes = []
1863 self.faildata = {}
1868 self.faildata = {}
1864
1869
1865 if options.color == 'auto':
1870 if options.color == 'auto':
1866 self.color = pygmentspresent and self.stream.isatty()
1871 self.color = pygmentspresent and self.stream.isatty()
1867 elif options.color == 'never':
1872 elif options.color == 'never':
1868 self.color = False
1873 self.color = False
1869 else: # 'always', for testing purposes
1874 else: # 'always', for testing purposes
1870 self.color = pygmentspresent
1875 self.color = pygmentspresent
1871
1876
1872 def onStart(self, test):
1877 def onStart(self, test):
1873 """ Can be overriden by custom TestResult
1878 """ Can be overriden by custom TestResult
1874 """
1879 """
1875
1880
1876 def onEnd(self):
1881 def onEnd(self):
1877 """ Can be overriden by custom TestResult
1882 """ Can be overriden by custom TestResult
1878 """
1883 """
1879
1884
1880 def addFailure(self, test, reason):
1885 def addFailure(self, test, reason):
1881 self.failures.append((test, reason))
1886 self.failures.append((test, reason))
1882
1887
1883 if self._options.first:
1888 if self._options.first:
1884 self.stop()
1889 self.stop()
1885 else:
1890 else:
1886 with iolock:
1891 with iolock:
1887 if reason == "timed out":
1892 if reason == "timed out":
1888 self.stream.write('t')
1893 self.stream.write('t')
1889 else:
1894 else:
1890 if not self._options.nodiff:
1895 if not self._options.nodiff:
1891 self.stream.write('\n')
1896 self.stream.write('\n')
1892 # Exclude the '\n' from highlighting to lex correctly
1897 # Exclude the '\n' from highlighting to lex correctly
1893 formatted = 'ERROR: %s output changed\n' % test
1898 formatted = 'ERROR: %s output changed\n' % test
1894 self.stream.write(highlightmsg(formatted, self.color))
1899 self.stream.write(highlightmsg(formatted, self.color))
1895 self.stream.write('!')
1900 self.stream.write('!')
1896
1901
1897 self.stream.flush()
1902 self.stream.flush()
1898
1903
1899 def addSuccess(self, test):
1904 def addSuccess(self, test):
1900 with iolock:
1905 with iolock:
1901 super(TestResult, self).addSuccess(test)
1906 super(TestResult, self).addSuccess(test)
1902 self.successes.append(test)
1907 self.successes.append(test)
1903
1908
1904 def addError(self, test, err):
1909 def addError(self, test, err):
1905 super(TestResult, self).addError(test, err)
1910 super(TestResult, self).addError(test, err)
1906 if self._options.first:
1911 if self._options.first:
1907 self.stop()
1912 self.stop()
1908
1913
1909 # Polyfill.
1914 # Polyfill.
1910 def addSkip(self, test, reason):
1915 def addSkip(self, test, reason):
1911 self.skipped.append((test, reason))
1916 self.skipped.append((test, reason))
1912 with iolock:
1917 with iolock:
1913 if self.showAll:
1918 if self.showAll:
1914 self.stream.writeln('skipped %s' % reason)
1919 self.stream.writeln('skipped %s' % reason)
1915 else:
1920 else:
1916 self.stream.write('s')
1921 self.stream.write('s')
1917 self.stream.flush()
1922 self.stream.flush()
1918
1923
1919 def addIgnore(self, test, reason):
1924 def addIgnore(self, test, reason):
1920 self.ignored.append((test, reason))
1925 self.ignored.append((test, reason))
1921 with iolock:
1926 with iolock:
1922 if self.showAll:
1927 if self.showAll:
1923 self.stream.writeln('ignored %s' % reason)
1928 self.stream.writeln('ignored %s' % reason)
1924 else:
1929 else:
1925 if reason not in ('not retesting', "doesn't match keyword"):
1930 if reason not in ('not retesting', "doesn't match keyword"):
1926 self.stream.write('i')
1931 self.stream.write('i')
1927 else:
1932 else:
1928 self.testsRun += 1
1933 self.testsRun += 1
1929 self.stream.flush()
1934 self.stream.flush()
1930
1935
1931 def addOutputMismatch(self, test, ret, got, expected):
1936 def addOutputMismatch(self, test, ret, got, expected):
1932 """Record a mismatch in test output for a particular test."""
1937 """Record a mismatch in test output for a particular test."""
1933 if self.shouldStop or firsterror:
1938 if self.shouldStop or firsterror:
1934 # don't print, some other test case already failed and
1939 # don't print, some other test case already failed and
1935 # printed, we're just stale and probably failed due to our
1940 # printed, we're just stale and probably failed due to our
1936 # temp dir getting cleaned up.
1941 # temp dir getting cleaned up.
1937 return
1942 return
1938
1943
1939 accepted = False
1944 accepted = False
1940 lines = []
1945 lines = []
1941
1946
1942 with iolock:
1947 with iolock:
1943 if self._options.nodiff:
1948 if self._options.nodiff:
1944 pass
1949 pass
1945 elif self._options.view:
1950 elif self._options.view:
1946 v = self._options.view
1951 v = self._options.view
1947 subprocess.call(r'"%s" "%s" "%s"' %
1952 subprocess.call(r'"%s" "%s" "%s"' %
1948 (v, _strpath(test.refpath),
1953 (v, _strpath(test.refpath),
1949 _strpath(test.errpath)), shell=True)
1954 _strpath(test.errpath)), shell=True)
1950 else:
1955 else:
1951 servefail, lines = getdiff(expected, got,
1956 servefail, lines = getdiff(expected, got,
1952 test.refpath, test.errpath)
1957 test.refpath, test.errpath)
1953 self.stream.write('\n')
1958 self.stream.write('\n')
1954 for line in lines:
1959 for line in lines:
1955 line = highlightdiff(line, self.color)
1960 line = highlightdiff(line, self.color)
1956 if PYTHON3:
1961 if PYTHON3:
1957 self.stream.flush()
1962 self.stream.flush()
1958 self.stream.buffer.write(line)
1963 self.stream.buffer.write(line)
1959 self.stream.buffer.flush()
1964 self.stream.buffer.flush()
1960 else:
1965 else:
1961 self.stream.write(line)
1966 self.stream.write(line)
1962 self.stream.flush()
1967 self.stream.flush()
1963
1968
1964 if servefail:
1969 if servefail:
1965 raise test.failureException(
1970 raise test.failureException(
1966 'server failed to start (HGPORT=%s)' % test._startport)
1971 'server failed to start (HGPORT=%s)' % test._startport)
1967
1972
1968 # handle interactive prompt without releasing iolock
1973 # handle interactive prompt without releasing iolock
1969 if self._options.interactive:
1974 if self._options.interactive:
1970 if test.readrefout() != expected:
1975 if test.readrefout() != expected:
1971 self.stream.write(
1976 self.stream.write(
1972 'Reference output has changed (run again to prompt '
1977 'Reference output has changed (run again to prompt '
1973 'changes)')
1978 'changes)')
1974 else:
1979 else:
1975 self.stream.write('Accept this change? [n] ')
1980 self.stream.write('Accept this change? [n] ')
1976 self.stream.flush()
1981 self.stream.flush()
1977 answer = sys.stdin.readline().strip()
1982 answer = sys.stdin.readline().strip()
1978 if answer.lower() in ('y', 'yes'):
1983 if answer.lower() in ('y', 'yes'):
1979 if test.path.endswith(b'.t'):
1984 if test.path.endswith(b'.t'):
1980 rename(test.errpath, test.path)
1985 rename(test.errpath, test.path)
1981 else:
1986 else:
1982 rename(test.errpath, '%s.out' % test.path)
1987 rename(test.errpath, '%s.out' % test.path)
1983 accepted = True
1988 accepted = True
1984 if not accepted:
1989 if not accepted:
1985 self.faildata[test.name] = b''.join(lines)
1990 self.faildata[test.name] = b''.join(lines)
1986
1991
1987 return accepted
1992 return accepted
1988
1993
1989 def startTest(self, test):
1994 def startTest(self, test):
1990 super(TestResult, self).startTest(test)
1995 super(TestResult, self).startTest(test)
1991
1996
1992 # 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
1993 # 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.
1994 # 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
1995 # and not for Windows.
2000 # and not for Windows.
1996 test.started = os.times()
2001 test.started = os.times()
1997 if self._firststarttime is None: # thread racy but irrelevant
2002 if self._firststarttime is None: # thread racy but irrelevant
1998 self._firststarttime = test.started[4]
2003 self._firststarttime = test.started[4]
1999
2004
2000 def stopTest(self, test, interrupted=False):
2005 def stopTest(self, test, interrupted=False):
2001 super(TestResult, self).stopTest(test)
2006 super(TestResult, self).stopTest(test)
2002
2007
2003 test.stopped = os.times()
2008 test.stopped = os.times()
2004
2009
2005 starttime = test.started
2010 starttime = test.started
2006 endtime = test.stopped
2011 endtime = test.stopped
2007 origin = self._firststarttime
2012 origin = self._firststarttime
2008 self.times.append((test.name,
2013 self.times.append((test.name,
2009 endtime[2] - starttime[2], # user space CPU time
2014 endtime[2] - starttime[2], # user space CPU time
2010 endtime[3] - starttime[3], # sys space CPU time
2015 endtime[3] - starttime[3], # sys space CPU time
2011 endtime[4] - starttime[4], # real time
2016 endtime[4] - starttime[4], # real time
2012 starttime[4] - origin, # start date in run context
2017 starttime[4] - origin, # start date in run context
2013 endtime[4] - origin, # end date in run context
2018 endtime[4] - origin, # end date in run context
2014 ))
2019 ))
2015
2020
2016 if interrupted:
2021 if interrupted:
2017 with iolock:
2022 with iolock:
2018 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
2023 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
2019 test.name, self.times[-1][3]))
2024 test.name, self.times[-1][3]))
2020
2025
2021 def getTestResult():
2026 def getTestResult():
2022 """
2027 """
2023 Returns the relevant test result
2028 Returns the relevant test result
2024 """
2029 """
2025 if "CUSTOM_TEST_RESULT" in os.environ:
2030 if "CUSTOM_TEST_RESULT" in os.environ:
2026 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2031 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2027 return testresultmodule.TestResult
2032 return testresultmodule.TestResult
2028 else:
2033 else:
2029 return TestResult
2034 return TestResult
2030
2035
2031 class TestSuite(unittest.TestSuite):
2036 class TestSuite(unittest.TestSuite):
2032 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2037 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2033
2038
2034 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
2039 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
2035 retest=False, keywords=None, loop=False, runs_per_test=1,
2040 retest=False, keywords=None, loop=False, runs_per_test=1,
2036 loadtest=None, showchannels=False,
2041 loadtest=None, showchannels=False,
2037 *args, **kwargs):
2042 *args, **kwargs):
2038 """Create a new instance that can run tests with a configuration.
2043 """Create a new instance that can run tests with a configuration.
2039
2044
2040 testdir specifies the directory where tests are executed from. This
2045 testdir specifies the directory where tests are executed from. This
2041 is typically the ``tests`` directory from Mercurial's source
2046 is typically the ``tests`` directory from Mercurial's source
2042 repository.
2047 repository.
2043
2048
2044 jobs specifies the number of jobs to run concurrently. Each test
2049 jobs specifies the number of jobs to run concurrently. Each test
2045 executes on its own thread. Tests actually spawn new processes, so
2050 executes on its own thread. Tests actually spawn new processes, so
2046 state mutation should not be an issue.
2051 state mutation should not be an issue.
2047
2052
2048 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.
2049
2054
2050 whitelist and blacklist denote tests that have been whitelisted and
2055 whitelist and blacklist denote tests that have been whitelisted and
2051 blacklisted, respectively. These arguments don't belong in TestSuite.
2056 blacklisted, respectively. These arguments don't belong in TestSuite.
2052 Instead, whitelist and blacklist should be handled by the thing that
2057 Instead, whitelist and blacklist should be handled by the thing that
2053 populates the TestSuite with tests. They are present to preserve
2058 populates the TestSuite with tests. They are present to preserve
2054 backwards compatible behavior which reports skipped tests as part
2059 backwards compatible behavior which reports skipped tests as part
2055 of the results.
2060 of the results.
2056
2061
2057 retest denotes whether to retest failed tests. This arguably belongs
2062 retest denotes whether to retest failed tests. This arguably belongs
2058 outside of TestSuite.
2063 outside of TestSuite.
2059
2064
2060 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
2061 to execute. This arguably belongs outside of TestSuite.
2066 to execute. This arguably belongs outside of TestSuite.
2062
2067
2063 loop denotes whether to loop over tests forever.
2068 loop denotes whether to loop over tests forever.
2064 """
2069 """
2065 super(TestSuite, self).__init__(*args, **kwargs)
2070 super(TestSuite, self).__init__(*args, **kwargs)
2066
2071
2067 self._jobs = jobs
2072 self._jobs = jobs
2068 self._whitelist = whitelist
2073 self._whitelist = whitelist
2069 self._blacklist = blacklist
2074 self._blacklist = blacklist
2070 self._retest = retest
2075 self._retest = retest
2071 self._keywords = keywords
2076 self._keywords = keywords
2072 self._loop = loop
2077 self._loop = loop
2073 self._runs_per_test = runs_per_test
2078 self._runs_per_test = runs_per_test
2074 self._loadtest = loadtest
2079 self._loadtest = loadtest
2075 self._showchannels = showchannels
2080 self._showchannels = showchannels
2076
2081
2077 def run(self, result):
2082 def run(self, result):
2078 # 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
2079 # 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
2080 # Test simpler.
2085 # Test simpler.
2081 tests = []
2086 tests = []
2082 num_tests = [0]
2087 num_tests = [0]
2083 for test in self._tests:
2088 for test in self._tests:
2084 def get():
2089 def get():
2085 num_tests[0] += 1
2090 num_tests[0] += 1
2086 if getattr(test, 'should_reload', False):
2091 if getattr(test, 'should_reload', False):
2087 return self._loadtest(test, num_tests[0])
2092 return self._loadtest(test, num_tests[0])
2088 return test
2093 return test
2089 if not os.path.exists(test.path):
2094 if not os.path.exists(test.path):
2090 result.addSkip(test, "Doesn't exist")
2095 result.addSkip(test, "Doesn't exist")
2091 continue
2096 continue
2092
2097
2093 if not (self._whitelist and test.bname in self._whitelist):
2098 if not (self._whitelist and test.bname in self._whitelist):
2094 if self._blacklist and test.bname in self._blacklist:
2099 if self._blacklist and test.bname in self._blacklist:
2095 result.addSkip(test, 'blacklisted')
2100 result.addSkip(test, 'blacklisted')
2096 continue
2101 continue
2097
2102
2098 if self._retest and not os.path.exists(test.errpath):
2103 if self._retest and not os.path.exists(test.errpath):
2099 result.addIgnore(test, 'not retesting')
2104 result.addIgnore(test, 'not retesting')
2100 continue
2105 continue
2101
2106
2102 if self._keywords:
2107 if self._keywords:
2103 with open(test.path, 'rb') as f:
2108 with open(test.path, 'rb') as f:
2104 t = f.read().lower() + test.bname.lower()
2109 t = f.read().lower() + test.bname.lower()
2105 ignored = False
2110 ignored = False
2106 for k in self._keywords.lower().split():
2111 for k in self._keywords.lower().split():
2107 if k not in t:
2112 if k not in t:
2108 result.addIgnore(test, "doesn't match keyword")
2113 result.addIgnore(test, "doesn't match keyword")
2109 ignored = True
2114 ignored = True
2110 break
2115 break
2111
2116
2112 if ignored:
2117 if ignored:
2113 continue
2118 continue
2114 for _ in xrange(self._runs_per_test):
2119 for _ in xrange(self._runs_per_test):
2115 tests.append(get())
2120 tests.append(get())
2116
2121
2117 runtests = list(tests)
2122 runtests = list(tests)
2118 done = queue.Queue()
2123 done = queue.Queue()
2119 running = 0
2124 running = 0
2120
2125
2121 channels = [""] * self._jobs
2126 channels = [""] * self._jobs
2122
2127
2123 def job(test, result):
2128 def job(test, result):
2124 for n, v in enumerate(channels):
2129 for n, v in enumerate(channels):
2125 if not v:
2130 if not v:
2126 channel = n
2131 channel = n
2127 break
2132 break
2128 else:
2133 else:
2129 raise ValueError('Could not find output channel')
2134 raise ValueError('Could not find output channel')
2130 channels[channel] = "=" + test.name[5:].split(".")[0]
2135 channels[channel] = "=" + test.name[5:].split(".")[0]
2131 try:
2136 try:
2132 test(result)
2137 test(result)
2133 done.put(None)
2138 done.put(None)
2134 except KeyboardInterrupt:
2139 except KeyboardInterrupt:
2135 pass
2140 pass
2136 except: # re-raises
2141 except: # re-raises
2137 done.put(('!', test, 'run-test raised an error, see traceback'))
2142 done.put(('!', test, 'run-test raised an error, see traceback'))
2138 raise
2143 raise
2139 finally:
2144 finally:
2140 try:
2145 try:
2141 channels[channel] = ''
2146 channels[channel] = ''
2142 except IndexError:
2147 except IndexError:
2143 pass
2148 pass
2144
2149
2145 def stat():
2150 def stat():
2146 count = 0
2151 count = 0
2147 while channels:
2152 while channels:
2148 d = '\n%03s ' % count
2153 d = '\n%03s ' % count
2149 for n, v in enumerate(channels):
2154 for n, v in enumerate(channels):
2150 if v:
2155 if v:
2151 d += v[0]
2156 d += v[0]
2152 channels[n] = v[1:] or '.'
2157 channels[n] = v[1:] or '.'
2153 else:
2158 else:
2154 d += ' '
2159 d += ' '
2155 d += ' '
2160 d += ' '
2156 with iolock:
2161 with iolock:
2157 sys.stdout.write(d + ' ')
2162 sys.stdout.write(d + ' ')
2158 sys.stdout.flush()
2163 sys.stdout.flush()
2159 for x in xrange(10):
2164 for x in xrange(10):
2160 if channels:
2165 if channels:
2161 time.sleep(.1)
2166 time.sleep(.1)
2162 count += 1
2167 count += 1
2163
2168
2164 stoppedearly = False
2169 stoppedearly = False
2165
2170
2166 if self._showchannels:
2171 if self._showchannels:
2167 statthread = threading.Thread(target=stat, name="stat")
2172 statthread = threading.Thread(target=stat, name="stat")
2168 statthread.start()
2173 statthread.start()
2169
2174
2170 try:
2175 try:
2171 while tests or running:
2176 while tests or running:
2172 if not done.empty() or running == self._jobs or not tests:
2177 if not done.empty() or running == self._jobs or not tests:
2173 try:
2178 try:
2174 done.get(True, 1)
2179 done.get(True, 1)
2175 running -= 1
2180 running -= 1
2176 if result and result.shouldStop:
2181 if result and result.shouldStop:
2177 stoppedearly = True
2182 stoppedearly = True
2178 break
2183 break
2179 except queue.Empty:
2184 except queue.Empty:
2180 continue
2185 continue
2181 if tests and not running == self._jobs:
2186 if tests and not running == self._jobs:
2182 test = tests.pop(0)
2187 test = tests.pop(0)
2183 if self._loop:
2188 if self._loop:
2184 if getattr(test, 'should_reload', False):
2189 if getattr(test, 'should_reload', False):
2185 num_tests[0] += 1
2190 num_tests[0] += 1
2186 tests.append(
2191 tests.append(
2187 self._loadtest(test, num_tests[0]))
2192 self._loadtest(test, num_tests[0]))
2188 else:
2193 else:
2189 tests.append(test)
2194 tests.append(test)
2190 if self._jobs == 1:
2195 if self._jobs == 1:
2191 job(test, result)
2196 job(test, result)
2192 else:
2197 else:
2193 t = threading.Thread(target=job, name=test.name,
2198 t = threading.Thread(target=job, name=test.name,
2194 args=(test, result))
2199 args=(test, result))
2195 t.start()
2200 t.start()
2196 running += 1
2201 running += 1
2197
2202
2198 # 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
2199 # finish. Otherwise, there is a race between the test completing
2204 # finish. Otherwise, there is a race between the test completing
2200 # 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
2201 # test reporting incorrect.
2206 # test reporting incorrect.
2202 if stoppedearly:
2207 if stoppedearly:
2203 while running:
2208 while running:
2204 try:
2209 try:
2205 done.get(True, 1)
2210 done.get(True, 1)
2206 running -= 1
2211 running -= 1
2207 except queue.Empty:
2212 except queue.Empty:
2208 continue
2213 continue
2209 except KeyboardInterrupt:
2214 except KeyboardInterrupt:
2210 for test in runtests:
2215 for test in runtests:
2211 test.abort()
2216 test.abort()
2212
2217
2213 channels = []
2218 channels = []
2214
2219
2215 return result
2220 return result
2216
2221
2217 # 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
2218 # human-readable text file named .testtimes. Tests are sorted
2223 # human-readable text file named .testtimes. Tests are sorted
2219 # alphabetically, while times for each test are listed from oldest to
2224 # alphabetically, while times for each test are listed from oldest to
2220 # newest.
2225 # newest.
2221
2226
2222 def loadtimes(outputdir):
2227 def loadtimes(outputdir):
2223 times = []
2228 times = []
2224 try:
2229 try:
2225 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2230 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2226 for line in fp:
2231 for line in fp:
2227 m = re.match('(.*?) ([0-9. ]+)', line)
2232 m = re.match('(.*?) ([0-9. ]+)', line)
2228 times.append((m.group(1),
2233 times.append((m.group(1),
2229 [float(t) for t in m.group(2).split()]))
2234 [float(t) for t in m.group(2).split()]))
2230 except IOError as err:
2235 except IOError as err:
2231 if err.errno != errno.ENOENT:
2236 if err.errno != errno.ENOENT:
2232 raise
2237 raise
2233 return times
2238 return times
2234
2239
2235 def savetimes(outputdir, result):
2240 def savetimes(outputdir, result):
2236 saved = dict(loadtimes(outputdir))
2241 saved = dict(loadtimes(outputdir))
2237 maxruns = 5
2242 maxruns = 5
2238 skipped = set([str(t[0]) for t in result.skipped])
2243 skipped = set([str(t[0]) for t in result.skipped])
2239 for tdata in result.times:
2244 for tdata in result.times:
2240 test, real = tdata[0], tdata[3]
2245 test, real = tdata[0], tdata[3]
2241 if test not in skipped:
2246 if test not in skipped:
2242 ts = saved.setdefault(test, [])
2247 ts = saved.setdefault(test, [])
2243 ts.append(real)
2248 ts.append(real)
2244 ts[:] = ts[-maxruns:]
2249 ts[:] = ts[-maxruns:]
2245
2250
2246 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2251 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2247 dir=outputdir, text=True)
2252 dir=outputdir, text=True)
2248 with os.fdopen(fd, 'w') as fp:
2253 with os.fdopen(fd, 'w') as fp:
2249 for name, ts in sorted(saved.items()):
2254 for name, ts in sorted(saved.items()):
2250 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])))
2251 timepath = os.path.join(outputdir, b'.testtimes')
2256 timepath = os.path.join(outputdir, b'.testtimes')
2252 try:
2257 try:
2253 os.unlink(timepath)
2258 os.unlink(timepath)
2254 except OSError:
2259 except OSError:
2255 pass
2260 pass
2256 try:
2261 try:
2257 os.rename(tmpname, timepath)
2262 os.rename(tmpname, timepath)
2258 except OSError:
2263 except OSError:
2259 pass
2264 pass
2260
2265
2261 class TextTestRunner(unittest.TextTestRunner):
2266 class TextTestRunner(unittest.TextTestRunner):
2262 """Custom unittest test runner that uses appropriate settings."""
2267 """Custom unittest test runner that uses appropriate settings."""
2263
2268
2264 def __init__(self, runner, *args, **kwargs):
2269 def __init__(self, runner, *args, **kwargs):
2265 super(TextTestRunner, self).__init__(*args, **kwargs)
2270 super(TextTestRunner, self).__init__(*args, **kwargs)
2266
2271
2267 self._runner = runner
2272 self._runner = runner
2268
2273
2269 self._result = getTestResult()(self._runner.options, self.stream,
2274 self._result = getTestResult()(self._runner.options, self.stream,
2270 self.descriptions, self.verbosity)
2275 self.descriptions, self.verbosity)
2271
2276
2272 def listtests(self, test):
2277 def listtests(self, test):
2273 test = sorted(test, key=lambda t: t.name)
2278 test = sorted(test, key=lambda t: t.name)
2274
2279
2275 self._result.onStart(test)
2280 self._result.onStart(test)
2276
2281
2277 for t in test:
2282 for t in test:
2278 print(t.name)
2283 print(t.name)
2279 self._result.addSuccess(t)
2284 self._result.addSuccess(t)
2280
2285
2281 if self._runner.options.xunit:
2286 if self._runner.options.xunit:
2282 with open(self._runner.options.xunit, "wb") as xuf:
2287 with open(self._runner.options.xunit, "wb") as xuf:
2283 self._writexunit(self._result, xuf)
2288 self._writexunit(self._result, xuf)
2284
2289
2285 if self._runner.options.json:
2290 if self._runner.options.json:
2286 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2291 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2287 with open(jsonpath, 'w') as fp:
2292 with open(jsonpath, 'w') as fp:
2288 self._writejson(self._result, fp)
2293 self._writejson(self._result, fp)
2289
2294
2290 return self._result
2295 return self._result
2291
2296
2292 def run(self, test):
2297 def run(self, test):
2293 self._result.onStart(test)
2298 self._result.onStart(test)
2294 test(self._result)
2299 test(self._result)
2295
2300
2296 failed = len(self._result.failures)
2301 failed = len(self._result.failures)
2297 skipped = len(self._result.skipped)
2302 skipped = len(self._result.skipped)
2298 ignored = len(self._result.ignored)
2303 ignored = len(self._result.ignored)
2299
2304
2300 with iolock:
2305 with iolock:
2301 self.stream.writeln('')
2306 self.stream.writeln('')
2302
2307
2303 if not self._runner.options.noskips:
2308 if not self._runner.options.noskips:
2304 for test, msg in sorted(self._result.skipped,
2309 for test, msg in sorted(self._result.skipped,
2305 key=lambda s: s[0].name):
2310 key=lambda s: s[0].name):
2306 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2311 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2307 msg = highlightmsg(formatted, self._result.color)
2312 msg = highlightmsg(formatted, self._result.color)
2308 self.stream.write(msg)
2313 self.stream.write(msg)
2309 for test, msg in sorted(self._result.failures,
2314 for test, msg in sorted(self._result.failures,
2310 key=lambda f: f[0].name):
2315 key=lambda f: f[0].name):
2311 formatted = 'Failed %s: %s\n' % (test.name, msg)
2316 formatted = 'Failed %s: %s\n' % (test.name, msg)
2312 self.stream.write(highlightmsg(formatted, self._result.color))
2317 self.stream.write(highlightmsg(formatted, self._result.color))
2313 for test, msg in sorted(self._result.errors,
2318 for test, msg in sorted(self._result.errors,
2314 key=lambda e: e[0].name):
2319 key=lambda e: e[0].name):
2315 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2320 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2316
2321
2317 if self._runner.options.xunit:
2322 if self._runner.options.xunit:
2318 with open(self._runner.options.xunit, "wb") as xuf:
2323 with open(self._runner.options.xunit, "wb") as xuf:
2319 self._writexunit(self._result, xuf)
2324 self._writexunit(self._result, xuf)
2320
2325
2321 if self._runner.options.json:
2326 if self._runner.options.json:
2322 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2327 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2323 with open(jsonpath, 'w') as fp:
2328 with open(jsonpath, 'w') as fp:
2324 self._writejson(self._result, fp)
2329 self._writejson(self._result, fp)
2325
2330
2326 self._runner._checkhglib('Tested')
2331 self._runner._checkhglib('Tested')
2327
2332
2328 savetimes(self._runner._outputdir, self._result)
2333 savetimes(self._runner._outputdir, self._result)
2329
2334
2330 if failed and self._runner.options.known_good_rev:
2335 if failed and self._runner.options.known_good_rev:
2331 self._bisecttests(t for t, m in self._result.failures)
2336 self._bisecttests(t for t, m in self._result.failures)
2332 self.stream.writeln(
2337 self.stream.writeln(
2333 '# Ran %d tests, %d skipped, %d failed.'
2338 '# Ran %d tests, %d skipped, %d failed.'
2334 % (self._result.testsRun, skipped + ignored, failed))
2339 % (self._result.testsRun, skipped + ignored, failed))
2335 if failed:
2340 if failed:
2336 self.stream.writeln('python hash seed: %s' %
2341 self.stream.writeln('python hash seed: %s' %
2337 os.environ['PYTHONHASHSEED'])
2342 os.environ['PYTHONHASHSEED'])
2338 if self._runner.options.time:
2343 if self._runner.options.time:
2339 self.printtimes(self._result.times)
2344 self.printtimes(self._result.times)
2340
2345
2341 if self._runner.options.exceptions:
2346 if self._runner.options.exceptions:
2342 exceptions = aggregateexceptions(
2347 exceptions = aggregateexceptions(
2343 os.path.join(self._runner._outputdir, b'exceptions'))
2348 os.path.join(self._runner._outputdir, b'exceptions'))
2344
2349
2345 self.stream.writeln('Exceptions Report:')
2350 self.stream.writeln('Exceptions Report:')
2346 self.stream.writeln('%d total from %d frames' %
2351 self.stream.writeln('%d total from %d frames' %
2347 (exceptions['total'],
2352 (exceptions['total'],
2348 len(exceptions['exceptioncounts'])))
2353 len(exceptions['exceptioncounts'])))
2349 combined = exceptions['combined']
2354 combined = exceptions['combined']
2350 for key in sorted(combined, key=combined.get, reverse=True):
2355 for key in sorted(combined, key=combined.get, reverse=True):
2351 frame, line, exc = key
2356 frame, line, exc = key
2352 totalcount, testcount, leastcount, leasttest = combined[key]
2357 totalcount, testcount, leastcount, leasttest = combined[key]
2353
2358
2354 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)'
2355 % (totalcount,
2360 % (totalcount,
2356 testcount,
2361 testcount,
2357 frame, exc,
2362 frame, exc,
2358 leasttest, leastcount))
2363 leasttest, leastcount))
2359
2364
2360 self.stream.flush()
2365 self.stream.flush()
2361
2366
2362 return self._result
2367 return self._result
2363
2368
2364 def _bisecttests(self, tests):
2369 def _bisecttests(self, tests):
2365 bisectcmd = ['hg', 'bisect']
2370 bisectcmd = ['hg', 'bisect']
2366 bisectrepo = self._runner.options.bisect_repo
2371 bisectrepo = self._runner.options.bisect_repo
2367 if bisectrepo:
2372 if bisectrepo:
2368 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2373 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2369 def pread(args):
2374 def pread(args):
2370 env = os.environ.copy()
2375 env = os.environ.copy()
2371 env['HGPLAIN'] = '1'
2376 env['HGPLAIN'] = '1'
2372 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2377 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2373 stdout=subprocess.PIPE, env=env)
2378 stdout=subprocess.PIPE, env=env)
2374 data = p.stdout.read()
2379 data = p.stdout.read()
2375 p.wait()
2380 p.wait()
2376 return data
2381 return data
2377 for test in tests:
2382 for test in tests:
2378 pread(bisectcmd + ['--reset']),
2383 pread(bisectcmd + ['--reset']),
2379 pread(bisectcmd + ['--bad', '.'])
2384 pread(bisectcmd + ['--bad', '.'])
2380 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2385 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2381 # TODO: we probably need to forward more options
2386 # TODO: we probably need to forward more options
2382 # that alter hg's behavior inside the tests.
2387 # that alter hg's behavior inside the tests.
2383 opts = ''
2388 opts = ''
2384 withhg = self._runner.options.with_hg
2389 withhg = self._runner.options.with_hg
2385 if withhg:
2390 if withhg:
2386 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2391 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2387 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts,
2392 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts,
2388 test)
2393 test)
2389 data = pread(bisectcmd + ['--command', rtc])
2394 data = pread(bisectcmd + ['--command', rtc])
2390 m = re.search(
2395 m = re.search(
2391 (br'\nThe first (?P<goodbad>bad|good) revision '
2396 (br'\nThe first (?P<goodbad>bad|good) revision '
2392 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2397 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2393 br'summary: +(?P<summary>[^\n]+)\n'),
2398 br'summary: +(?P<summary>[^\n]+)\n'),
2394 data, (re.MULTILINE | re.DOTALL))
2399 data, (re.MULTILINE | re.DOTALL))
2395 if m is None:
2400 if m is None:
2396 self.stream.writeln(
2401 self.stream.writeln(
2397 'Failed to identify failure point for %s' % test)
2402 'Failed to identify failure point for %s' % test)
2398 continue
2403 continue
2399 dat = m.groupdict()
2404 dat = m.groupdict()
2400 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2405 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2401 self.stream.writeln(
2406 self.stream.writeln(
2402 '%s %s by %s (%s)' % (
2407 '%s %s by %s (%s)' % (
2403 test, verb, dat['node'].decode('ascii'),
2408 test, verb, dat['node'].decode('ascii'),
2404 dat['summary'].decode('utf8', 'ignore')))
2409 dat['summary'].decode('utf8', 'ignore')))
2405
2410
2406 def printtimes(self, times):
2411 def printtimes(self, times):
2407 # iolock held by run
2412 # iolock held by run
2408 self.stream.writeln('# Producing time report')
2413 self.stream.writeln('# Producing time report')
2409 times.sort(key=lambda t: (t[3]))
2414 times.sort(key=lambda t: (t[3]))
2410 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'
2411 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2416 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2412 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2417 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2413 for tdata in times:
2418 for tdata in times:
2414 test = tdata[0]
2419 test = tdata[0]
2415 cuser, csys, real, start, end = tdata[1:6]
2420 cuser, csys, real, start, end = tdata[1:6]
2416 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2421 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2417
2422
2418 @staticmethod
2423 @staticmethod
2419 def _writexunit(result, outf):
2424 def _writexunit(result, outf):
2420 # See http://llg.cubic.org/docs/junit/ for a reference.
2425 # See http://llg.cubic.org/docs/junit/ for a reference.
2421 timesd = dict((t[0], t[3]) for t in result.times)
2426 timesd = dict((t[0], t[3]) for t in result.times)
2422 doc = minidom.Document()
2427 doc = minidom.Document()
2423 s = doc.createElement('testsuite')
2428 s = doc.createElement('testsuite')
2424 s.setAttribute('errors', "0") # TODO
2429 s.setAttribute('errors', "0") # TODO
2425 s.setAttribute('failures', str(len(result.failures)))
2430 s.setAttribute('failures', str(len(result.failures)))
2426 s.setAttribute('name', 'run-tests')
2431 s.setAttribute('name', 'run-tests')
2427 s.setAttribute('skipped', str(len(result.skipped) +
2432 s.setAttribute('skipped', str(len(result.skipped) +
2428 len(result.ignored)))
2433 len(result.ignored)))
2429 s.setAttribute('tests', str(result.testsRun))
2434 s.setAttribute('tests', str(result.testsRun))
2430 doc.appendChild(s)
2435 doc.appendChild(s)
2431 for tc in result.successes:
2436 for tc in result.successes:
2432 t = doc.createElement('testcase')
2437 t = doc.createElement('testcase')
2433 t.setAttribute('name', tc.name)
2438 t.setAttribute('name', tc.name)
2434 tctime = timesd.get(tc.name)
2439 tctime = timesd.get(tc.name)
2435 if tctime is not None:
2440 if tctime is not None:
2436 t.setAttribute('time', '%.3f' % tctime)
2441 t.setAttribute('time', '%.3f' % tctime)
2437 s.appendChild(t)
2442 s.appendChild(t)
2438 for tc, err in sorted(result.faildata.items()):
2443 for tc, err in sorted(result.faildata.items()):
2439 t = doc.createElement('testcase')
2444 t = doc.createElement('testcase')
2440 t.setAttribute('name', tc)
2445 t.setAttribute('name', tc)
2441 tctime = timesd.get(tc)
2446 tctime = timesd.get(tc)
2442 if tctime is not None:
2447 if tctime is not None:
2443 t.setAttribute('time', '%.3f' % tctime)
2448 t.setAttribute('time', '%.3f' % tctime)
2444 # createCDATASection expects a unicode or it will
2449 # createCDATASection expects a unicode or it will
2445 # convert using default conversion rules, which will
2450 # convert using default conversion rules, which will
2446 # fail if string isn't ASCII.
2451 # fail if string isn't ASCII.
2447 err = cdatasafe(err).decode('utf-8', 'replace')
2452 err = cdatasafe(err).decode('utf-8', 'replace')
2448 cd = doc.createCDATASection(err)
2453 cd = doc.createCDATASection(err)
2449 # Use 'failure' here instead of 'error' to match errors = 0,
2454 # Use 'failure' here instead of 'error' to match errors = 0,
2450 # failures = len(result.failures) in the testsuite element.
2455 # failures = len(result.failures) in the testsuite element.
2451 failelem = doc.createElement('failure')
2456 failelem = doc.createElement('failure')
2452 failelem.setAttribute('message', 'output changed')
2457 failelem.setAttribute('message', 'output changed')
2453 failelem.setAttribute('type', 'output-mismatch')
2458 failelem.setAttribute('type', 'output-mismatch')
2454 failelem.appendChild(cd)
2459 failelem.appendChild(cd)
2455 t.appendChild(failelem)
2460 t.appendChild(failelem)
2456 s.appendChild(t)
2461 s.appendChild(t)
2457 for tc, message in result.skipped:
2462 for tc, message in result.skipped:
2458 # According to the schema, 'skipped' has no attributes. So store
2463 # According to the schema, 'skipped' has no attributes. So store
2459 # the skip message as a text node instead.
2464 # the skip message as a text node instead.
2460 t = doc.createElement('testcase')
2465 t = doc.createElement('testcase')
2461 t.setAttribute('name', tc.name)
2466 t.setAttribute('name', tc.name)
2462 binmessage = message.encode('utf-8')
2467 binmessage = message.encode('utf-8')
2463 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2468 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2464 cd = doc.createCDATASection(message)
2469 cd = doc.createCDATASection(message)
2465 skipelem = doc.createElement('skipped')
2470 skipelem = doc.createElement('skipped')
2466 skipelem.appendChild(cd)
2471 skipelem.appendChild(cd)
2467 t.appendChild(skipelem)
2472 t.appendChild(skipelem)
2468 s.appendChild(t)
2473 s.appendChild(t)
2469 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2474 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2470
2475
2471 @staticmethod
2476 @staticmethod
2472 def _writejson(result, outf):
2477 def _writejson(result, outf):
2473 timesd = {}
2478 timesd = {}
2474 for tdata in result.times:
2479 for tdata in result.times:
2475 test = tdata[0]
2480 test = tdata[0]
2476 timesd[test] = tdata[1:]
2481 timesd[test] = tdata[1:]
2477
2482
2478 outcome = {}
2483 outcome = {}
2479 groups = [('success', ((tc, None)
2484 groups = [('success', ((tc, None)
2480 for tc in result.successes)),
2485 for tc in result.successes)),
2481 ('failure', result.failures),
2486 ('failure', result.failures),
2482 ('skip', result.skipped)]
2487 ('skip', result.skipped)]
2483 for res, testcases in groups:
2488 for res, testcases in groups:
2484 for tc, __ in testcases:
2489 for tc, __ in testcases:
2485 if tc.name in timesd:
2490 if tc.name in timesd:
2486 diff = result.faildata.get(tc.name, b'')
2491 diff = result.faildata.get(tc.name, b'')
2487 try:
2492 try:
2488 diff = diff.decode('unicode_escape')
2493 diff = diff.decode('unicode_escape')
2489 except UnicodeDecodeError as e:
2494 except UnicodeDecodeError as e:
2490 diff = '%r decoding diff, sorry' % e
2495 diff = '%r decoding diff, sorry' % e
2491 tres = {'result': res,
2496 tres = {'result': res,
2492 'time': ('%0.3f' % timesd[tc.name][2]),
2497 'time': ('%0.3f' % timesd[tc.name][2]),
2493 'cuser': ('%0.3f' % timesd[tc.name][0]),
2498 'cuser': ('%0.3f' % timesd[tc.name][0]),
2494 'csys': ('%0.3f' % timesd[tc.name][1]),
2499 'csys': ('%0.3f' % timesd[tc.name][1]),
2495 'start': ('%0.3f' % timesd[tc.name][3]),
2500 'start': ('%0.3f' % timesd[tc.name][3]),
2496 'end': ('%0.3f' % timesd[tc.name][4]),
2501 'end': ('%0.3f' % timesd[tc.name][4]),
2497 'diff': diff,
2502 'diff': diff,
2498 }
2503 }
2499 else:
2504 else:
2500 # blacklisted test
2505 # blacklisted test
2501 tres = {'result': res}
2506 tres = {'result': res}
2502
2507
2503 outcome[tc.name] = tres
2508 outcome[tc.name] = tres
2504 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2509 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2505 separators=(',', ': '))
2510 separators=(',', ': '))
2506 outf.writelines(("testreport =", jsonout))
2511 outf.writelines(("testreport =", jsonout))
2507
2512
2508 def sorttests(testdescs, previoustimes, shuffle=False):
2513 def sorttests(testdescs, previoustimes, shuffle=False):
2509 """Do an in-place sort of tests."""
2514 """Do an in-place sort of tests."""
2510 if shuffle:
2515 if shuffle:
2511 random.shuffle(testdescs)
2516 random.shuffle(testdescs)
2512 return
2517 return
2513
2518
2514 if previoustimes:
2519 if previoustimes:
2515 def sortkey(f):
2520 def sortkey(f):
2516 f = f['path']
2521 f = f['path']
2517 if f in previoustimes:
2522 if f in previoustimes:
2518 # Use most recent time as estimate
2523 # Use most recent time as estimate
2519 return -previoustimes[f][-1]
2524 return -previoustimes[f][-1]
2520 else:
2525 else:
2521 # 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
2522 return -1.0
2527 return -1.0
2523 else:
2528 else:
2524 # keywords for slow tests
2529 # keywords for slow tests
2525 slow = {b'svn': 10,
2530 slow = {b'svn': 10,
2526 b'cvs': 10,
2531 b'cvs': 10,
2527 b'hghave': 10,
2532 b'hghave': 10,
2528 b'largefiles-update': 10,
2533 b'largefiles-update': 10,
2529 b'run-tests': 10,
2534 b'run-tests': 10,
2530 b'corruption': 10,
2535 b'corruption': 10,
2531 b'race': 10,
2536 b'race': 10,
2532 b'i18n': 10,
2537 b'i18n': 10,
2533 b'check': 100,
2538 b'check': 100,
2534 b'gendoc': 100,
2539 b'gendoc': 100,
2535 b'contrib-perf': 200,
2540 b'contrib-perf': 200,
2536 }
2541 }
2537 perf = {}
2542 perf = {}
2538
2543
2539 def sortkey(f):
2544 def sortkey(f):
2540 # run largest tests first, as they tend to take the longest
2545 # run largest tests first, as they tend to take the longest
2541 f = f['path']
2546 f = f['path']
2542 try:
2547 try:
2543 return perf[f]
2548 return perf[f]
2544 except KeyError:
2549 except KeyError:
2545 try:
2550 try:
2546 val = -os.stat(f).st_size
2551 val = -os.stat(f).st_size
2547 except OSError as e:
2552 except OSError as e:
2548 if e.errno != errno.ENOENT:
2553 if e.errno != errno.ENOENT:
2549 raise
2554 raise
2550 perf[f] = -1e9 # file does not exist, tell early
2555 perf[f] = -1e9 # file does not exist, tell early
2551 return -1e9
2556 return -1e9
2552 for kw, mul in slow.items():
2557 for kw, mul in slow.items():
2553 if kw in f:
2558 if kw in f:
2554 val *= mul
2559 val *= mul
2555 if f.endswith(b'.py'):
2560 if f.endswith(b'.py'):
2556 val /= 10.0
2561 val /= 10.0
2557 perf[f] = val / 1000.0
2562 perf[f] = val / 1000.0
2558 return perf[f]
2563 return perf[f]
2559
2564
2560 testdescs.sort(key=sortkey)
2565 testdescs.sort(key=sortkey)
2561
2566
2562 class TestRunner(object):
2567 class TestRunner(object):
2563 """Holds context for executing tests.
2568 """Holds context for executing tests.
2564
2569
2565 Tests rely on a lot of state. This object holds it for them.
2570 Tests rely on a lot of state. This object holds it for them.
2566 """
2571 """
2567
2572
2568 # Programs required to run tests.
2573 # Programs required to run tests.
2569 REQUIREDTOOLS = [
2574 REQUIREDTOOLS = [
2570 b'diff',
2575 b'diff',
2571 b'grep',
2576 b'grep',
2572 b'unzip',
2577 b'unzip',
2573 b'gunzip',
2578 b'gunzip',
2574 b'bunzip2',
2579 b'bunzip2',
2575 b'sed',
2580 b'sed',
2576 ]
2581 ]
2577
2582
2578 # Maps file extensions to test class.
2583 # Maps file extensions to test class.
2579 TESTTYPES = [
2584 TESTTYPES = [
2580 (b'.py', PythonTest),
2585 (b'.py', PythonTest),
2581 (b'.t', TTest),
2586 (b'.t', TTest),
2582 ]
2587 ]
2583
2588
2584 def __init__(self):
2589 def __init__(self):
2585 self.options = None
2590 self.options = None
2586 self._hgroot = None
2591 self._hgroot = None
2587 self._testdir = None
2592 self._testdir = None
2588 self._outputdir = None
2593 self._outputdir = None
2589 self._hgtmp = None
2594 self._hgtmp = None
2590 self._installdir = None
2595 self._installdir = None
2591 self._bindir = None
2596 self._bindir = None
2592 self._tmpbinddir = None
2597 self._tmpbinddir = None
2593 self._pythondir = None
2598 self._pythondir = None
2594 self._coveragefile = None
2599 self._coveragefile = None
2595 self._createdfiles = []
2600 self._createdfiles = []
2596 self._hgcommand = None
2601 self._hgcommand = None
2597 self._hgpath = None
2602 self._hgpath = None
2598 self._portoffset = 0
2603 self._portoffset = 0
2599 self._ports = {}
2604 self._ports = {}
2600
2605
2601 def run(self, args, parser=None):
2606 def run(self, args, parser=None):
2602 """Run the test suite."""
2607 """Run the test suite."""
2603 oldmask = os.umask(0o22)
2608 oldmask = os.umask(0o22)
2604 try:
2609 try:
2605 parser = parser or getparser()
2610 parser = parser or getparser()
2606 options = parseargs(args, parser)
2611 options = parseargs(args, parser)
2607 tests = [_bytespath(a) for a in options.tests]
2612 tests = [_bytespath(a) for a in options.tests]
2608 if options.test_list is not None:
2613 if options.test_list is not None:
2609 for listfile in options.test_list:
2614 for listfile in options.test_list:
2610 with open(listfile, 'rb') as f:
2615 with open(listfile, 'rb') as f:
2611 tests.extend(t for t in f.read().splitlines() if t)
2616 tests.extend(t for t in f.read().splitlines() if t)
2612 self.options = options
2617 self.options = options
2613
2618
2614 self._checktools()
2619 self._checktools()
2615 testdescs = self.findtests(tests)
2620 testdescs = self.findtests(tests)
2616 if options.profile_runner:
2621 if options.profile_runner:
2617 import statprof
2622 import statprof
2618 statprof.start()
2623 statprof.start()
2619 result = self._run(testdescs)
2624 result = self._run(testdescs)
2620 if options.profile_runner:
2625 if options.profile_runner:
2621 statprof.stop()
2626 statprof.stop()
2622 statprof.display()
2627 statprof.display()
2623 return result
2628 return result
2624
2629
2625 finally:
2630 finally:
2626 os.umask(oldmask)
2631 os.umask(oldmask)
2627
2632
2628 def _run(self, testdescs):
2633 def _run(self, testdescs):
2629 testdir = getcwdb()
2634 testdir = getcwdb()
2630 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2635 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2631 # assume all tests in same folder for now
2636 # assume all tests in same folder for now
2632 if testdescs:
2637 if testdescs:
2633 pathname = os.path.dirname(testdescs[0]['path'])
2638 pathname = os.path.dirname(testdescs[0]['path'])
2634 if pathname:
2639 if pathname:
2635 testdir = os.path.join(testdir, pathname)
2640 testdir = os.path.join(testdir, pathname)
2636 self._testdir = osenvironb[b'TESTDIR'] = testdir
2641 self._testdir = osenvironb[b'TESTDIR'] = testdir
2637 if self.options.outputdir:
2642 if self.options.outputdir:
2638 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2643 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2639 else:
2644 else:
2640 self._outputdir = getcwdb()
2645 self._outputdir = getcwdb()
2641 if testdescs and pathname:
2646 if testdescs and pathname:
2642 self._outputdir = os.path.join(self._outputdir, pathname)
2647 self._outputdir = os.path.join(self._outputdir, pathname)
2643 previoustimes = {}
2648 previoustimes = {}
2644 if self.options.order_by_runtime:
2649 if self.options.order_by_runtime:
2645 previoustimes = dict(loadtimes(self._outputdir))
2650 previoustimes = dict(loadtimes(self._outputdir))
2646 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2651 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2647
2652
2648 if 'PYTHONHASHSEED' not in os.environ:
2653 if 'PYTHONHASHSEED' not in os.environ:
2649 # use a random python hash seed all the time
2654 # use a random python hash seed all the time
2650 # we do the randomness ourself to know what seed is used
2655 # we do the randomness ourself to know what seed is used
2651 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2656 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2652
2657
2653 if self.options.tmpdir:
2658 if self.options.tmpdir:
2654 self.options.keep_tmpdir = True
2659 self.options.keep_tmpdir = True
2655 tmpdir = _bytespath(self.options.tmpdir)
2660 tmpdir = _bytespath(self.options.tmpdir)
2656 if os.path.exists(tmpdir):
2661 if os.path.exists(tmpdir):
2657 # Meaning of tmpdir has changed since 1.3: we used to create
2662 # Meaning of tmpdir has changed since 1.3: we used to create
2658 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2663 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2659 # tmpdir already exists.
2664 # tmpdir already exists.
2660 print("error: temp dir %r already exists" % tmpdir)
2665 print("error: temp dir %r already exists" % tmpdir)
2661 return 1
2666 return 1
2662
2667
2663 os.makedirs(tmpdir)
2668 os.makedirs(tmpdir)
2664 else:
2669 else:
2665 d = None
2670 d = None
2666 if os.name == 'nt':
2671 if os.name == 'nt':
2667 # without this, we get the default temp dir location, but
2672 # without this, we get the default temp dir location, but
2668 # in all lowercase, which causes troubles with paths (issue3490)
2673 # in all lowercase, which causes troubles with paths (issue3490)
2669 d = osenvironb.get(b'TMP', None)
2674 d = osenvironb.get(b'TMP', None)
2670 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2675 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2671
2676
2672 self._hgtmp = osenvironb[b'HGTMP'] = (
2677 self._hgtmp = osenvironb[b'HGTMP'] = (
2673 os.path.realpath(tmpdir))
2678 os.path.realpath(tmpdir))
2674
2679
2675 if self.options.with_hg:
2680 if self.options.with_hg:
2676 self._installdir = None
2681 self._installdir = None
2677 whg = self.options.with_hg
2682 whg = self.options.with_hg
2678 self._bindir = os.path.dirname(os.path.realpath(whg))
2683 self._bindir = os.path.dirname(os.path.realpath(whg))
2679 assert isinstance(self._bindir, bytes)
2684 assert isinstance(self._bindir, bytes)
2680 self._hgcommand = os.path.basename(whg)
2685 self._hgcommand = os.path.basename(whg)
2681 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2686 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2682 os.makedirs(self._tmpbindir)
2687 os.makedirs(self._tmpbindir)
2683
2688
2684 normbin = os.path.normpath(os.path.abspath(whg))
2689 normbin = os.path.normpath(os.path.abspath(whg))
2685 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2690 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2686
2691
2687 # Other Python scripts in the test harness need to
2692 # Other Python scripts in the test harness need to
2688 # `import mercurial`. If `hg` is a Python script, we assume
2693 # `import mercurial`. If `hg` is a Python script, we assume
2689 # the Mercurial modules are relative to its path and tell the tests
2694 # the Mercurial modules are relative to its path and tell the tests
2690 # to load Python modules from its directory.
2695 # to load Python modules from its directory.
2691 with open(whg, 'rb') as fh:
2696 with open(whg, 'rb') as fh:
2692 initial = fh.read(1024)
2697 initial = fh.read(1024)
2693
2698
2694 if re.match(b'#!.*python', initial):
2699 if re.match(b'#!.*python', initial):
2695 self._pythondir = self._bindir
2700 self._pythondir = self._bindir
2696 # If it looks like our in-repo Rust binary, use the source root.
2701 # If it looks like our in-repo Rust binary, use the source root.
2697 # This is a bit hacky. But rhg is still not supported outside the
2702 # This is a bit hacky. But rhg is still not supported outside the
2698 # source directory. So until it is, do the simple thing.
2703 # source directory. So until it is, do the simple thing.
2699 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2704 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2700 self._pythondir = os.path.dirname(self._testdir)
2705 self._pythondir = os.path.dirname(self._testdir)
2701 # Fall back to the legacy behavior.
2706 # Fall back to the legacy behavior.
2702 else:
2707 else:
2703 self._pythondir = self._bindir
2708 self._pythondir = self._bindir
2704
2709
2705 else:
2710 else:
2706 self._installdir = os.path.join(self._hgtmp, b"install")
2711 self._installdir = os.path.join(self._hgtmp, b"install")
2707 self._bindir = os.path.join(self._installdir, b"bin")
2712 self._bindir = os.path.join(self._installdir, b"bin")
2708 self._hgcommand = b'hg'
2713 self._hgcommand = b'hg'
2709 self._tmpbindir = self._bindir
2714 self._tmpbindir = self._bindir
2710 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2715 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2711
2716
2712 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
2717 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
2713 # a python script and feed it to python.exe. Legacy stdio is force
2718 # a python script and feed it to python.exe. Legacy stdio is force
2714 # enabled by hg.exe, and this is a more realistic way to launch hg
2719 # enabled by hg.exe, and this is a more realistic way to launch hg
2715 # anyway.
2720 # anyway.
2716 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
2721 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
2717 self._hgcommand += b'.exe'
2722 self._hgcommand += b'.exe'
2718
2723
2719 # set CHGHG, then replace "hg" command by "chg"
2724 # set CHGHG, then replace "hg" command by "chg"
2720 chgbindir = self._bindir
2725 chgbindir = self._bindir
2721 if self.options.chg or self.options.with_chg:
2726 if self.options.chg or self.options.with_chg:
2722 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2727 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2723 else:
2728 else:
2724 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2729 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2725 if self.options.chg:
2730 if self.options.chg:
2726 self._hgcommand = b'chg'
2731 self._hgcommand = b'chg'
2727 elif self.options.with_chg:
2732 elif self.options.with_chg:
2728 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2733 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2729 self._hgcommand = os.path.basename(self.options.with_chg)
2734 self._hgcommand = os.path.basename(self.options.with_chg)
2730
2735
2731 osenvironb[b"BINDIR"] = self._bindir
2736 osenvironb[b"BINDIR"] = self._bindir
2732 osenvironb[b"PYTHON"] = PYTHON
2737 osenvironb[b"PYTHON"] = PYTHON
2733
2738
2734 fileb = _bytespath(__file__)
2739 fileb = _bytespath(__file__)
2735 runtestdir = os.path.abspath(os.path.dirname(fileb))
2740 runtestdir = os.path.abspath(os.path.dirname(fileb))
2736 osenvironb[b'RUNTESTDIR'] = runtestdir
2741 osenvironb[b'RUNTESTDIR'] = runtestdir
2737 if PYTHON3:
2742 if PYTHON3:
2738 sepb = _bytespath(os.pathsep)
2743 sepb = _bytespath(os.pathsep)
2739 else:
2744 else:
2740 sepb = os.pathsep
2745 sepb = os.pathsep
2741 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2746 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2742 if os.path.islink(__file__):
2747 if os.path.islink(__file__):
2743 # test helper will likely be at the end of the symlink
2748 # test helper will likely be at the end of the symlink
2744 realfile = os.path.realpath(fileb)
2749 realfile = os.path.realpath(fileb)
2745 realdir = os.path.abspath(os.path.dirname(realfile))
2750 realdir = os.path.abspath(os.path.dirname(realfile))
2746 path.insert(2, realdir)
2751 path.insert(2, realdir)
2747 if chgbindir != self._bindir:
2752 if chgbindir != self._bindir:
2748 path.insert(1, chgbindir)
2753 path.insert(1, chgbindir)
2749 if self._testdir != runtestdir:
2754 if self._testdir != runtestdir:
2750 path = [self._testdir] + path
2755 path = [self._testdir] + path
2751 if self._tmpbindir != self._bindir:
2756 if self._tmpbindir != self._bindir:
2752 path = [self._tmpbindir] + path
2757 path = [self._tmpbindir] + path
2753 osenvironb[b"PATH"] = sepb.join(path)
2758 osenvironb[b"PATH"] = sepb.join(path)
2754
2759
2755 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2760 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2756 # can run .../tests/run-tests.py test-foo where test-foo
2761 # can run .../tests/run-tests.py test-foo where test-foo
2757 # adds an extension to HGRC. Also include run-test.py directory to
2762 # adds an extension to HGRC. Also include run-test.py directory to
2758 # import modules like heredoctest.
2763 # import modules like heredoctest.
2759 pypath = [self._pythondir, self._testdir, runtestdir]
2764 pypath = [self._pythondir, self._testdir, runtestdir]
2760 # We have to augment PYTHONPATH, rather than simply replacing
2765 # We have to augment PYTHONPATH, rather than simply replacing
2761 # it, in case external libraries are only available via current
2766 # it, in case external libraries are only available via current
2762 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2767 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2763 # are in /opt/subversion.)
2768 # are in /opt/subversion.)
2764 oldpypath = osenvironb.get(IMPL_PATH)
2769 oldpypath = osenvironb.get(IMPL_PATH)
2765 if oldpypath:
2770 if oldpypath:
2766 pypath.append(oldpypath)
2771 pypath.append(oldpypath)
2767 osenvironb[IMPL_PATH] = sepb.join(pypath)
2772 osenvironb[IMPL_PATH] = sepb.join(pypath)
2768
2773
2769 if self.options.pure:
2774 if self.options.pure:
2770 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2775 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2771 os.environ["HGMODULEPOLICY"] = "py"
2776 os.environ["HGMODULEPOLICY"] = "py"
2772
2777
2773 if self.options.allow_slow_tests:
2778 if self.options.allow_slow_tests:
2774 os.environ["HGTEST_SLOW"] = "slow"
2779 os.environ["HGTEST_SLOW"] = "slow"
2775 elif 'HGTEST_SLOW' in os.environ:
2780 elif 'HGTEST_SLOW' in os.environ:
2776 del os.environ['HGTEST_SLOW']
2781 del os.environ['HGTEST_SLOW']
2777
2782
2778 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2783 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2779
2784
2780 if self.options.exceptions:
2785 if self.options.exceptions:
2781 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2786 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2782 try:
2787 try:
2783 os.makedirs(exceptionsdir)
2788 os.makedirs(exceptionsdir)
2784 except OSError as e:
2789 except OSError as e:
2785 if e.errno != errno.EEXIST:
2790 if e.errno != errno.EEXIST:
2786 raise
2791 raise
2787
2792
2788 # Remove all existing exception reports.
2793 # Remove all existing exception reports.
2789 for f in os.listdir(exceptionsdir):
2794 for f in os.listdir(exceptionsdir):
2790 os.unlink(os.path.join(exceptionsdir, f))
2795 os.unlink(os.path.join(exceptionsdir, f))
2791
2796
2792 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2797 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2793 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2798 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2794 self.options.extra_config_opt.append(
2799 self.options.extra_config_opt.append(
2795 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2800 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2796
2801
2797 vlog("# Using TESTDIR", self._testdir)
2802 vlog("# Using TESTDIR", self._testdir)
2798 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2803 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2799 vlog("# Using HGTMP", self._hgtmp)
2804 vlog("# Using HGTMP", self._hgtmp)
2800 vlog("# Using PATH", os.environ["PATH"])
2805 vlog("# Using PATH", os.environ["PATH"])
2801 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2806 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2802 vlog("# Writing to directory", self._outputdir)
2807 vlog("# Writing to directory", self._outputdir)
2803
2808
2804 try:
2809 try:
2805 return self._runtests(testdescs) or 0
2810 return self._runtests(testdescs) or 0
2806 finally:
2811 finally:
2807 time.sleep(.1)
2812 time.sleep(.1)
2808 self._cleanup()
2813 self._cleanup()
2809
2814
2810 def findtests(self, args):
2815 def findtests(self, args):
2811 """Finds possible test files from arguments.
2816 """Finds possible test files from arguments.
2812
2817
2813 If you wish to inject custom tests into the test harness, this would
2818 If you wish to inject custom tests into the test harness, this would
2814 be a good function to monkeypatch or override in a derived class.
2819 be a good function to monkeypatch or override in a derived class.
2815 """
2820 """
2816 if not args:
2821 if not args:
2817 if self.options.changed:
2822 if self.options.changed:
2818 proc = Popen4(b'hg st --rev "%s" -man0 .' %
2823 proc = Popen4(b'hg st --rev "%s" -man0 .' %
2819 _bytespath(self.options.changed), None, 0)
2824 _bytespath(self.options.changed), None, 0)
2820 stdout, stderr = proc.communicate()
2825 stdout, stderr = proc.communicate()
2821 args = stdout.strip(b'\0').split(b'\0')
2826 args = stdout.strip(b'\0').split(b'\0')
2822 else:
2827 else:
2823 args = os.listdir(b'.')
2828 args = os.listdir(b'.')
2824
2829
2825 expanded_args = []
2830 expanded_args = []
2826 for arg in args:
2831 for arg in args:
2827 if os.path.isdir(arg):
2832 if os.path.isdir(arg):
2828 if not arg.endswith(b'/'):
2833 if not arg.endswith(b'/'):
2829 arg += b'/'
2834 arg += b'/'
2830 expanded_args.extend([arg + a for a in os.listdir(arg)])
2835 expanded_args.extend([arg + a for a in os.listdir(arg)])
2831 else:
2836 else:
2832 expanded_args.append(arg)
2837 expanded_args.append(arg)
2833 args = expanded_args
2838 args = expanded_args
2834
2839
2835 testcasepattern = re.compile(
2840 testcasepattern = re.compile(
2836 br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-\.#]+))')
2841 br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-\.#]+))')
2837 tests = []
2842 tests = []
2838 for t in args:
2843 for t in args:
2839 case = []
2844 case = []
2840
2845
2841 if not (os.path.basename(t).startswith(b'test-')
2846 if not (os.path.basename(t).startswith(b'test-')
2842 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2847 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2843
2848
2844 m = testcasepattern.match(os.path.basename(t))
2849 m = testcasepattern.match(os.path.basename(t))
2845 if m is not None:
2850 if m is not None:
2846 t_basename, casestr = m.groups()
2851 t_basename, casestr = m.groups()
2847 t = os.path.join(os.path.dirname(t), t_basename)
2852 t = os.path.join(os.path.dirname(t), t_basename)
2848 if casestr:
2853 if casestr:
2849 case = casestr.split(b'#')
2854 case = casestr.split(b'#')
2850 else:
2855 else:
2851 continue
2856 continue
2852
2857
2853 if t.endswith(b'.t'):
2858 if t.endswith(b'.t'):
2854 # .t file may contain multiple test cases
2859 # .t file may contain multiple test cases
2855 casedimensions = parsettestcases(t)
2860 casedimensions = parsettestcases(t)
2856 if casedimensions:
2861 if casedimensions:
2857 cases = []
2862 cases = []
2858 def addcases(case, casedimensions):
2863 def addcases(case, casedimensions):
2859 if not casedimensions:
2864 if not casedimensions:
2860 cases.append(case)
2865 cases.append(case)
2861 else:
2866 else:
2862 for c in casedimensions[0]:
2867 for c in casedimensions[0]:
2863 addcases(case + [c], casedimensions[1:])
2868 addcases(case + [c], casedimensions[1:])
2864 addcases([], casedimensions)
2869 addcases([], casedimensions)
2865 if case and case in cases:
2870 if case and case in cases:
2866 cases = [case]
2871 cases = [case]
2867 elif case:
2872 elif case:
2868 # Ignore invalid cases
2873 # Ignore invalid cases
2869 cases = []
2874 cases = []
2870 else:
2875 else:
2871 pass
2876 pass
2872 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2877 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2873 else:
2878 else:
2874 tests.append({'path': t})
2879 tests.append({'path': t})
2875 else:
2880 else:
2876 tests.append({'path': t})
2881 tests.append({'path': t})
2877 return tests
2882 return tests
2878
2883
2879 def _runtests(self, testdescs):
2884 def _runtests(self, testdescs):
2880 def _reloadtest(test, i):
2885 def _reloadtest(test, i):
2881 # convert a test back to its description dict
2886 # convert a test back to its description dict
2882 desc = {'path': test.path}
2887 desc = {'path': test.path}
2883 case = getattr(test, '_case', [])
2888 case = getattr(test, '_case', [])
2884 if case:
2889 if case:
2885 desc['case'] = case
2890 desc['case'] = case
2886 return self._gettest(desc, i)
2891 return self._gettest(desc, i)
2887
2892
2888 try:
2893 try:
2889 if self.options.restart:
2894 if self.options.restart:
2890 orig = list(testdescs)
2895 orig = list(testdescs)
2891 while testdescs:
2896 while testdescs:
2892 desc = testdescs[0]
2897 desc = testdescs[0]
2893 # desc['path'] is a relative path
2898 # desc['path'] is a relative path
2894 if 'case' in desc:
2899 if 'case' in desc:
2895 casestr = b'#'.join(desc['case'])
2900 casestr = b'#'.join(desc['case'])
2896 errpath = b'%s#%s.err' % (desc['path'], casestr)
2901 errpath = b'%s#%s.err' % (desc['path'], casestr)
2897 else:
2902 else:
2898 errpath = b'%s.err' % desc['path']
2903 errpath = b'%s.err' % desc['path']
2899 errpath = os.path.join(self._outputdir, errpath)
2904 errpath = os.path.join(self._outputdir, errpath)
2900 if os.path.exists(errpath):
2905 if os.path.exists(errpath):
2901 break
2906 break
2902 testdescs.pop(0)
2907 testdescs.pop(0)
2903 if not testdescs:
2908 if not testdescs:
2904 print("running all tests")
2909 print("running all tests")
2905 testdescs = orig
2910 testdescs = orig
2906
2911
2907 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2912 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2908 num_tests = len(tests) * self.options.runs_per_test
2913 num_tests = len(tests) * self.options.runs_per_test
2909
2914
2910 jobs = min(num_tests, self.options.jobs)
2915 jobs = min(num_tests, self.options.jobs)
2911
2916
2912 failed = False
2917 failed = False
2913 kws = self.options.keywords
2918 kws = self.options.keywords
2914 if kws is not None and PYTHON3:
2919 if kws is not None and PYTHON3:
2915 kws = kws.encode('utf-8')
2920 kws = kws.encode('utf-8')
2916
2921
2917 suite = TestSuite(self._testdir,
2922 suite = TestSuite(self._testdir,
2918 jobs=jobs,
2923 jobs=jobs,
2919 whitelist=self.options.whitelisted,
2924 whitelist=self.options.whitelisted,
2920 blacklist=self.options.blacklist,
2925 blacklist=self.options.blacklist,
2921 retest=self.options.retest,
2926 retest=self.options.retest,
2922 keywords=kws,
2927 keywords=kws,
2923 loop=self.options.loop,
2928 loop=self.options.loop,
2924 runs_per_test=self.options.runs_per_test,
2929 runs_per_test=self.options.runs_per_test,
2925 showchannels=self.options.showchannels,
2930 showchannels=self.options.showchannels,
2926 tests=tests, loadtest=_reloadtest)
2931 tests=tests, loadtest=_reloadtest)
2927 verbosity = 1
2932 verbosity = 1
2928 if self.options.list_tests:
2933 if self.options.list_tests:
2929 verbosity = 0
2934 verbosity = 0
2930 elif self.options.verbose:
2935 elif self.options.verbose:
2931 verbosity = 2
2936 verbosity = 2
2932 runner = TextTestRunner(self, verbosity=verbosity)
2937 runner = TextTestRunner(self, verbosity=verbosity)
2933
2938
2934 if self.options.list_tests:
2939 if self.options.list_tests:
2935 result = runner.listtests(suite)
2940 result = runner.listtests(suite)
2936 else:
2941 else:
2937 if self._installdir:
2942 if self._installdir:
2938 self._installhg()
2943 self._installhg()
2939 self._checkhglib("Testing")
2944 self._checkhglib("Testing")
2940 else:
2945 else:
2941 self._usecorrectpython()
2946 self._usecorrectpython()
2942 if self.options.chg:
2947 if self.options.chg:
2943 assert self._installdir
2948 assert self._installdir
2944 self._installchg()
2949 self._installchg()
2945
2950
2946 log('running %d tests using %d parallel processes' % (
2951 log('running %d tests using %d parallel processes' % (
2947 num_tests, jobs))
2952 num_tests, jobs))
2948
2953
2949 result = runner.run(suite)
2954 result = runner.run(suite)
2950
2955
2951 if result.failures or result.errors:
2956 if result.failures or result.errors:
2952 failed = True
2957 failed = True
2953
2958
2954 result.onEnd()
2959 result.onEnd()
2955
2960
2956 if self.options.anycoverage:
2961 if self.options.anycoverage:
2957 self._outputcoverage()
2962 self._outputcoverage()
2958 except KeyboardInterrupt:
2963 except KeyboardInterrupt:
2959 failed = True
2964 failed = True
2960 print("\ninterrupted!")
2965 print("\ninterrupted!")
2961
2966
2962 if failed:
2967 if failed:
2963 return 1
2968 return 1
2964
2969
2965 def _getport(self, count):
2970 def _getport(self, count):
2966 port = self._ports.get(count) # do we have a cached entry?
2971 port = self._ports.get(count) # do we have a cached entry?
2967 if port is None:
2972 if port is None:
2968 portneeded = 3
2973 portneeded = 3
2969 # above 100 tries we just give up and let test reports failure
2974 # above 100 tries we just give up and let test reports failure
2970 for tries in xrange(100):
2975 for tries in xrange(100):
2971 allfree = True
2976 allfree = True
2972 port = self.options.port + self._portoffset
2977 port = self.options.port + self._portoffset
2973 for idx in xrange(portneeded):
2978 for idx in xrange(portneeded):
2974 if not checkportisavailable(port + idx):
2979 if not checkportisavailable(port + idx):
2975 allfree = False
2980 allfree = False
2976 break
2981 break
2977 self._portoffset += portneeded
2982 self._portoffset += portneeded
2978 if allfree:
2983 if allfree:
2979 break
2984 break
2980 self._ports[count] = port
2985 self._ports[count] = port
2981 return port
2986 return port
2982
2987
2983 def _gettest(self, testdesc, count):
2988 def _gettest(self, testdesc, count):
2984 """Obtain a Test by looking at its filename.
2989 """Obtain a Test by looking at its filename.
2985
2990
2986 Returns a Test instance. The Test may not be runnable if it doesn't
2991 Returns a Test instance. The Test may not be runnable if it doesn't
2987 map to a known type.
2992 map to a known type.
2988 """
2993 """
2989 path = testdesc['path']
2994 path = testdesc['path']
2990 lctest = path.lower()
2995 lctest = path.lower()
2991 testcls = Test
2996 testcls = Test
2992
2997
2993 for ext, cls in self.TESTTYPES:
2998 for ext, cls in self.TESTTYPES:
2994 if lctest.endswith(ext):
2999 if lctest.endswith(ext):
2995 testcls = cls
3000 testcls = cls
2996 break
3001 break
2997
3002
2998 refpath = os.path.join(getcwdb(), path)
3003 refpath = os.path.join(getcwdb(), path)
2999 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3004 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3000
3005
3001 # extra keyword parameters. 'case' is used by .t tests
3006 # extra keyword parameters. 'case' is used by .t tests
3002 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
3007 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
3003
3008
3004 t = testcls(refpath, self._outputdir, tmpdir,
3009 t = testcls(refpath, self._outputdir, tmpdir,
3005 keeptmpdir=self.options.keep_tmpdir,
3010 keeptmpdir=self.options.keep_tmpdir,
3006 debug=self.options.debug,
3011 debug=self.options.debug,
3007 first=self.options.first,
3012 first=self.options.first,
3008 timeout=self.options.timeout,
3013 timeout=self.options.timeout,
3009 startport=self._getport(count),
3014 startport=self._getport(count),
3010 extraconfigopts=self.options.extra_config_opt,
3015 extraconfigopts=self.options.extra_config_opt,
3011 py3warnings=self.options.py3_warnings,
3016 py3warnings=self.options.py3_warnings,
3012 shell=self.options.shell,
3017 shell=self.options.shell,
3013 hgcommand=self._hgcommand,
3018 hgcommand=self._hgcommand,
3014 usechg=bool(self.options.with_chg or self.options.chg),
3019 usechg=bool(self.options.with_chg or self.options.chg),
3015 useipv6=useipv6, **kwds)
3020 useipv6=useipv6, **kwds)
3016 t.should_reload = True
3021 t.should_reload = True
3017 return t
3022 return t
3018
3023
3019 def _cleanup(self):
3024 def _cleanup(self):
3020 """Clean up state from this test invocation."""
3025 """Clean up state from this test invocation."""
3021 if self.options.keep_tmpdir:
3026 if self.options.keep_tmpdir:
3022 return
3027 return
3023
3028
3024 vlog("# Cleaning up HGTMP", self._hgtmp)
3029 vlog("# Cleaning up HGTMP", self._hgtmp)
3025 shutil.rmtree(self._hgtmp, True)
3030 shutil.rmtree(self._hgtmp, True)
3026 for f in self._createdfiles:
3031 for f in self._createdfiles:
3027 try:
3032 try:
3028 os.remove(f)
3033 os.remove(f)
3029 except OSError:
3034 except OSError:
3030 pass
3035 pass
3031
3036
3032 def _usecorrectpython(self):
3037 def _usecorrectpython(self):
3033 """Configure the environment to use the appropriate Python in tests."""
3038 """Configure the environment to use the appropriate Python in tests."""
3034 # Tests must use the same interpreter as us or bad things will happen.
3039 # Tests must use the same interpreter as us or bad things will happen.
3035 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3040 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3036
3041
3037 # os.symlink() is a thing with py3 on Windows, but it requires
3042 # os.symlink() is a thing with py3 on Windows, but it requires
3038 # Administrator rights.
3043 # Administrator rights.
3039 if getattr(os, 'symlink', None) and os.name != 'nt':
3044 if getattr(os, 'symlink', None) and os.name != 'nt':
3040 vlog("# Making python executable in test path a symlink to '%s'" %
3045 vlog("# Making python executable in test path a symlink to '%s'" %
3041 sysexecutable)
3046 sysexecutable)
3042 mypython = os.path.join(self._tmpbindir, pyexename)
3047 mypython = os.path.join(self._tmpbindir, pyexename)
3043 try:
3048 try:
3044 if os.readlink(mypython) == sysexecutable:
3049 if os.readlink(mypython) == sysexecutable:
3045 return
3050 return
3046 os.unlink(mypython)
3051 os.unlink(mypython)
3047 except OSError as err:
3052 except OSError as err:
3048 if err.errno != errno.ENOENT:
3053 if err.errno != errno.ENOENT:
3049 raise
3054 raise
3050 if self._findprogram(pyexename) != sysexecutable:
3055 if self._findprogram(pyexename) != sysexecutable:
3051 try:
3056 try:
3052 os.symlink(sysexecutable, mypython)
3057 os.symlink(sysexecutable, mypython)
3053 self._createdfiles.append(mypython)
3058 self._createdfiles.append(mypython)
3054 except OSError as err:
3059 except OSError as err:
3055 # child processes may race, which is harmless
3060 # child processes may race, which is harmless
3056 if err.errno != errno.EEXIST:
3061 if err.errno != errno.EEXIST:
3057 raise
3062 raise
3058 else:
3063 else:
3059 exedir, exename = os.path.split(sysexecutable)
3064 exedir, exename = os.path.split(sysexecutable)
3060 vlog("# Modifying search path to find %s as %s in '%s'" %
3065 vlog("# Modifying search path to find %s as %s in '%s'" %
3061 (exename, pyexename, exedir))
3066 (exename, pyexename, exedir))
3062 path = os.environ['PATH'].split(os.pathsep)
3067 path = os.environ['PATH'].split(os.pathsep)
3063 while exedir in path:
3068 while exedir in path:
3064 path.remove(exedir)
3069 path.remove(exedir)
3065 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3070 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3066 if not self._findprogram(pyexename):
3071 if not self._findprogram(pyexename):
3067 print("WARNING: Cannot find %s in search path" % pyexename)
3072 print("WARNING: Cannot find %s in search path" % pyexename)
3068
3073
3069 def _installhg(self):
3074 def _installhg(self):
3070 """Install hg into the test environment.
3075 """Install hg into the test environment.
3071
3076
3072 This will also configure hg with the appropriate testing settings.
3077 This will also configure hg with the appropriate testing settings.
3073 """
3078 """
3074 vlog("# Performing temporary installation of HG")
3079 vlog("# Performing temporary installation of HG")
3075 installerrs = os.path.join(self._hgtmp, b"install.err")
3080 installerrs = os.path.join(self._hgtmp, b"install.err")
3076 compiler = ''
3081 compiler = ''
3077 if self.options.compiler:
3082 if self.options.compiler:
3078 compiler = '--compiler ' + self.options.compiler
3083 compiler = '--compiler ' + self.options.compiler
3079 if self.options.pure:
3084 if self.options.pure:
3080 pure = b"--pure"
3085 pure = b"--pure"
3081 else:
3086 else:
3082 pure = b""
3087 pure = b""
3083
3088
3084 # Run installer in hg root
3089 # Run installer in hg root
3085 script = os.path.realpath(sys.argv[0])
3090 script = os.path.realpath(sys.argv[0])
3086 exe = sysexecutable
3091 exe = sysexecutable
3087 if PYTHON3:
3092 if PYTHON3:
3088 compiler = _bytespath(compiler)
3093 compiler = _bytespath(compiler)
3089 script = _bytespath(script)
3094 script = _bytespath(script)
3090 exe = _bytespath(exe)
3095 exe = _bytespath(exe)
3091 hgroot = os.path.dirname(os.path.dirname(script))
3096 hgroot = os.path.dirname(os.path.dirname(script))
3092 self._hgroot = hgroot
3097 self._hgroot = hgroot
3093 os.chdir(hgroot)
3098 os.chdir(hgroot)
3094 nohome = b'--home=""'
3099 nohome = b'--home=""'
3095 if os.name == 'nt':
3100 if os.name == 'nt':
3096 # The --home="" trick works only on OS where os.sep == '/'
3101 # The --home="" trick works only on OS where os.sep == '/'
3097 # because of a distutils convert_path() fast-path. Avoid it at
3102 # because of a distutils convert_path() fast-path. Avoid it at
3098 # least on Windows for now, deal with .pydistutils.cfg bugs
3103 # least on Windows for now, deal with .pydistutils.cfg bugs
3099 # when they happen.
3104 # when they happen.
3100 nohome = b''
3105 nohome = b''
3101 cmd = (b'"%(exe)s" setup.py %(pure)s clean --all'
3106 cmd = (b'"%(exe)s" setup.py %(pure)s clean --all'
3102 b' build %(compiler)s --build-base="%(base)s"'
3107 b' build %(compiler)s --build-base="%(base)s"'
3103 b' install --force --prefix="%(prefix)s"'
3108 b' install --force --prefix="%(prefix)s"'
3104 b' --install-lib="%(libdir)s"'
3109 b' --install-lib="%(libdir)s"'
3105 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3110 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3106 % {b'exe': exe, b'pure': pure,
3111 % {b'exe': exe, b'pure': pure,
3107 b'compiler': compiler,
3112 b'compiler': compiler,
3108 b'base': os.path.join(self._hgtmp, b"build"),
3113 b'base': os.path.join(self._hgtmp, b"build"),
3109 b'prefix': self._installdir, b'libdir': self._pythondir,
3114 b'prefix': self._installdir, b'libdir': self._pythondir,
3110 b'bindir': self._bindir,
3115 b'bindir': self._bindir,
3111 b'nohome': nohome, b'logfile': installerrs})
3116 b'nohome': nohome, b'logfile': installerrs})
3112
3117
3113 # setuptools requires install directories to exist.
3118 # setuptools requires install directories to exist.
3114 def makedirs(p):
3119 def makedirs(p):
3115 try:
3120 try:
3116 os.makedirs(p)
3121 os.makedirs(p)
3117 except OSError as e:
3122 except OSError as e:
3118 if e.errno != errno.EEXIST:
3123 if e.errno != errno.EEXIST:
3119 raise
3124 raise
3120 makedirs(self._pythondir)
3125 makedirs(self._pythondir)
3121 makedirs(self._bindir)
3126 makedirs(self._bindir)
3122
3127
3123 vlog("# Running", cmd)
3128 vlog("# Running", cmd)
3124 if subprocess.call(_strpath(cmd), shell=True) == 0:
3129 if subprocess.call(_strpath(cmd), shell=True) == 0:
3125 if not self.options.verbose:
3130 if not self.options.verbose:
3126 try:
3131 try:
3127 os.remove(installerrs)
3132 os.remove(installerrs)
3128 except OSError as e:
3133 except OSError as e:
3129 if e.errno != errno.ENOENT:
3134 if e.errno != errno.ENOENT:
3130 raise
3135 raise
3131 else:
3136 else:
3132 with open(installerrs, 'rb') as f:
3137 with open(installerrs, 'rb') as f:
3133 for line in f:
3138 for line in f:
3134 if PYTHON3:
3139 if PYTHON3:
3135 sys.stdout.buffer.write(line)
3140 sys.stdout.buffer.write(line)
3136 else:
3141 else:
3137 sys.stdout.write(line)
3142 sys.stdout.write(line)
3138 sys.exit(1)
3143 sys.exit(1)
3139 os.chdir(self._testdir)
3144 os.chdir(self._testdir)
3140
3145
3141 self._usecorrectpython()
3146 self._usecorrectpython()
3142
3147
3143 if self.options.py3_warnings and not self.options.anycoverage:
3148 if self.options.py3_warnings and not self.options.anycoverage:
3144 vlog("# Updating hg command to enable Py3k Warnings switch")
3149 vlog("# Updating hg command to enable Py3k Warnings switch")
3145 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
3150 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
3146 lines = [line.rstrip() for line in f]
3151 lines = [line.rstrip() for line in f]
3147 lines[0] += ' -3'
3152 lines[0] += ' -3'
3148 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
3153 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
3149 for line in lines:
3154 for line in lines:
3150 f.write(line + '\n')
3155 f.write(line + '\n')
3151
3156
3152 hgbat = os.path.join(self._bindir, b'hg.bat')
3157 hgbat = os.path.join(self._bindir, b'hg.bat')
3153 if os.path.isfile(hgbat):
3158 if os.path.isfile(hgbat):
3154 # hg.bat expects to be put in bin/scripts while run-tests.py
3159 # hg.bat expects to be put in bin/scripts while run-tests.py
3155 # installation layout put it in bin/ directly. Fix it
3160 # installation layout put it in bin/ directly. Fix it
3156 with open(hgbat, 'rb') as f:
3161 with open(hgbat, 'rb') as f:
3157 data = f.read()
3162 data = f.read()
3158 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3163 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3159 data = data.replace(br'"%~dp0..\python" "%~dp0hg" %*',
3164 data = data.replace(br'"%~dp0..\python" "%~dp0hg" %*',
3160 b'"%~dp0python" "%~dp0hg" %*')
3165 b'"%~dp0python" "%~dp0hg" %*')
3161 with open(hgbat, 'wb') as f:
3166 with open(hgbat, 'wb') as f:
3162 f.write(data)
3167 f.write(data)
3163 else:
3168 else:
3164 print('WARNING: cannot fix hg.bat reference to python.exe')
3169 print('WARNING: cannot fix hg.bat reference to python.exe')
3165
3170
3166 if self.options.anycoverage:
3171 if self.options.anycoverage:
3167 custom = os.path.join(self._testdir, 'sitecustomize.py')
3172 custom = os.path.join(self._testdir, 'sitecustomize.py')
3168 target = os.path.join(self._pythondir, 'sitecustomize.py')
3173 target = os.path.join(self._pythondir, 'sitecustomize.py')
3169 vlog('# Installing coverage trigger to %s' % target)
3174 vlog('# Installing coverage trigger to %s' % target)
3170 shutil.copyfile(custom, target)
3175 shutil.copyfile(custom, target)
3171 rc = os.path.join(self._testdir, '.coveragerc')
3176 rc = os.path.join(self._testdir, '.coveragerc')
3172 vlog('# Installing coverage rc to %s' % rc)
3177 vlog('# Installing coverage rc to %s' % rc)
3173 os.environ['COVERAGE_PROCESS_START'] = rc
3178 os.environ['COVERAGE_PROCESS_START'] = rc
3174 covdir = os.path.join(self._installdir, '..', 'coverage')
3179 covdir = os.path.join(self._installdir, '..', 'coverage')
3175 try:
3180 try:
3176 os.mkdir(covdir)
3181 os.mkdir(covdir)
3177 except OSError as e:
3182 except OSError as e:
3178 if e.errno != errno.EEXIST:
3183 if e.errno != errno.EEXIST:
3179 raise
3184 raise
3180
3185
3181 os.environ['COVERAGE_DIR'] = covdir
3186 os.environ['COVERAGE_DIR'] = covdir
3182
3187
3183 def _checkhglib(self, verb):
3188 def _checkhglib(self, verb):
3184 """Ensure that the 'mercurial' package imported by python is
3189 """Ensure that the 'mercurial' package imported by python is
3185 the one we expect it to be. If not, print a warning to stderr."""
3190 the one we expect it to be. If not, print a warning to stderr."""
3186 if ((self._bindir == self._pythondir) and
3191 if ((self._bindir == self._pythondir) and
3187 (self._bindir != self._tmpbindir)):
3192 (self._bindir != self._tmpbindir)):
3188 # The pythondir has been inferred from --with-hg flag.
3193 # The pythondir has been inferred from --with-hg flag.
3189 # We cannot expect anything sensible here.
3194 # We cannot expect anything sensible here.
3190 return
3195 return
3191 expecthg = os.path.join(self._pythondir, b'mercurial')
3196 expecthg = os.path.join(self._pythondir, b'mercurial')
3192 actualhg = self._gethgpath()
3197 actualhg = self._gethgpath()
3193 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3198 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3194 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
3199 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
3195 ' (expected %s)\n'
3200 ' (expected %s)\n'
3196 % (verb, actualhg, expecthg))
3201 % (verb, actualhg, expecthg))
3197 def _gethgpath(self):
3202 def _gethgpath(self):
3198 """Return the path to the mercurial package that is actually found by
3203 """Return the path to the mercurial package that is actually found by
3199 the current Python interpreter."""
3204 the current Python interpreter."""
3200 if self._hgpath is not None:
3205 if self._hgpath is not None:
3201 return self._hgpath
3206 return self._hgpath
3202
3207
3203 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3208 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3204 cmd = cmd % PYTHON
3209 cmd = cmd % PYTHON
3205 if PYTHON3:
3210 if PYTHON3:
3206 cmd = _strpath(cmd)
3211 cmd = _strpath(cmd)
3207
3212
3208 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3213 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3209 out, err = p.communicate()
3214 out, err = p.communicate()
3210
3215
3211 self._hgpath = out.strip()
3216 self._hgpath = out.strip()
3212
3217
3213 return self._hgpath
3218 return self._hgpath
3214
3219
3215 def _installchg(self):
3220 def _installchg(self):
3216 """Install chg into the test environment"""
3221 """Install chg into the test environment"""
3217 vlog('# Performing temporary installation of CHG')
3222 vlog('# Performing temporary installation of CHG')
3218 assert os.path.dirname(self._bindir) == self._installdir
3223 assert os.path.dirname(self._bindir) == self._installdir
3219 assert self._hgroot, 'must be called after _installhg()'
3224 assert self._hgroot, 'must be called after _installhg()'
3220 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
3225 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
3221 % {b'make': b'make', # TODO: switch by option or environment?
3226 % {b'make': b'make', # TODO: switch by option or environment?
3222 b'prefix': self._installdir})
3227 b'prefix': self._installdir})
3223 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3228 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3224 vlog("# Running", cmd)
3229 vlog("# Running", cmd)
3225 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
3230 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
3226 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3231 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3227 stderr=subprocess.STDOUT)
3232 stderr=subprocess.STDOUT)
3228 out, _err = proc.communicate()
3233 out, _err = proc.communicate()
3229 if proc.returncode != 0:
3234 if proc.returncode != 0:
3230 if PYTHON3:
3235 if PYTHON3:
3231 sys.stdout.buffer.write(out)
3236 sys.stdout.buffer.write(out)
3232 else:
3237 else:
3233 sys.stdout.write(out)
3238 sys.stdout.write(out)
3234 sys.exit(1)
3239 sys.exit(1)
3235
3240
3236 def _outputcoverage(self):
3241 def _outputcoverage(self):
3237 """Produce code coverage output."""
3242 """Produce code coverage output."""
3238 import coverage
3243 import coverage
3239 coverage = coverage.coverage
3244 coverage = coverage.coverage
3240
3245
3241 vlog('# Producing coverage report')
3246 vlog('# Producing coverage report')
3242 # chdir is the easiest way to get short, relative paths in the
3247 # chdir is the easiest way to get short, relative paths in the
3243 # output.
3248 # output.
3244 os.chdir(self._hgroot)
3249 os.chdir(self._hgroot)
3245 covdir = os.path.join(self._installdir, '..', 'coverage')
3250 covdir = os.path.join(self._installdir, '..', 'coverage')
3246 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3251 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3247
3252
3248 # Map install directory paths back to source directory.
3253 # Map install directory paths back to source directory.
3249 cov.config.paths['srcdir'] = ['.', self._pythondir]
3254 cov.config.paths['srcdir'] = ['.', self._pythondir]
3250
3255
3251 cov.combine()
3256 cov.combine()
3252
3257
3253 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
3258 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
3254 cov.report(ignore_errors=True, omit=omit)
3259 cov.report(ignore_errors=True, omit=omit)
3255
3260
3256 if self.options.htmlcov:
3261 if self.options.htmlcov:
3257 htmldir = os.path.join(self._outputdir, 'htmlcov')
3262 htmldir = os.path.join(self._outputdir, 'htmlcov')
3258 cov.html_report(directory=htmldir, omit=omit)
3263 cov.html_report(directory=htmldir, omit=omit)
3259 if self.options.annotate:
3264 if self.options.annotate:
3260 adir = os.path.join(self._outputdir, 'annotated')
3265 adir = os.path.join(self._outputdir, 'annotated')
3261 if not os.path.isdir(adir):
3266 if not os.path.isdir(adir):
3262 os.mkdir(adir)
3267 os.mkdir(adir)
3263 cov.annotate(directory=adir, omit=omit)
3268 cov.annotate(directory=adir, omit=omit)
3264
3269
3265 def _findprogram(self, program):
3270 def _findprogram(self, program):
3266 """Search PATH for a executable program"""
3271 """Search PATH for a executable program"""
3267 dpb = _bytespath(os.defpath)
3272 dpb = _bytespath(os.defpath)
3268 sepb = _bytespath(os.pathsep)
3273 sepb = _bytespath(os.pathsep)
3269 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3274 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3270 name = os.path.join(p, program)
3275 name = os.path.join(p, program)
3271 if os.name == 'nt' or os.access(name, os.X_OK):
3276 if os.name == 'nt' or os.access(name, os.X_OK):
3272 return name
3277 return name
3273 return None
3278 return None
3274
3279
3275 def _checktools(self):
3280 def _checktools(self):
3276 """Ensure tools required to run tests are present."""
3281 """Ensure tools required to run tests are present."""
3277 for p in self.REQUIREDTOOLS:
3282 for p in self.REQUIREDTOOLS:
3278 if os.name == 'nt' and not p.endswith(b'.exe'):
3283 if os.name == 'nt' and not p.endswith(b'.exe'):
3279 p += b'.exe'
3284 p += b'.exe'
3280 found = self._findprogram(p)
3285 found = self._findprogram(p)
3281 if found:
3286 if found:
3282 vlog("# Found prerequisite", p, "at", found)
3287 vlog("# Found prerequisite", p, "at", found)
3283 else:
3288 else:
3284 print("WARNING: Did not find prerequisite tool: %s " %
3289 print("WARNING: Did not find prerequisite tool: %s " %
3285 p.decode("utf-8"))
3290 p.decode("utf-8"))
3286
3291
3287 def aggregateexceptions(path):
3292 def aggregateexceptions(path):
3288 exceptioncounts = collections.Counter()
3293 exceptioncounts = collections.Counter()
3289 testsbyfailure = collections.defaultdict(set)
3294 testsbyfailure = collections.defaultdict(set)
3290 failuresbytest = collections.defaultdict(set)
3295 failuresbytest = collections.defaultdict(set)
3291
3296
3292 for f in os.listdir(path):
3297 for f in os.listdir(path):
3293 with open(os.path.join(path, f), 'rb') as fh:
3298 with open(os.path.join(path, f), 'rb') as fh:
3294 data = fh.read().split(b'\0')
3299 data = fh.read().split(b'\0')
3295 if len(data) != 5:
3300 if len(data) != 5:
3296 continue
3301 continue
3297
3302
3298 exc, mainframe, hgframe, hgline, testname = data
3303 exc, mainframe, hgframe, hgline, testname = data
3299 exc = exc.decode('utf-8')
3304 exc = exc.decode('utf-8')
3300 mainframe = mainframe.decode('utf-8')
3305 mainframe = mainframe.decode('utf-8')
3301 hgframe = hgframe.decode('utf-8')
3306 hgframe = hgframe.decode('utf-8')
3302 hgline = hgline.decode('utf-8')
3307 hgline = hgline.decode('utf-8')
3303 testname = testname.decode('utf-8')
3308 testname = testname.decode('utf-8')
3304
3309
3305 key = (hgframe, hgline, exc)
3310 key = (hgframe, hgline, exc)
3306 exceptioncounts[key] += 1
3311 exceptioncounts[key] += 1
3307 testsbyfailure[key].add(testname)
3312 testsbyfailure[key].add(testname)
3308 failuresbytest[testname].add(key)
3313 failuresbytest[testname].add(key)
3309
3314
3310 # Find test having fewest failures for each failure.
3315 # Find test having fewest failures for each failure.
3311 leastfailing = {}
3316 leastfailing = {}
3312 for key, tests in testsbyfailure.items():
3317 for key, tests in testsbyfailure.items():
3313 fewesttest = None
3318 fewesttest = None
3314 fewestcount = 99999999
3319 fewestcount = 99999999
3315 for test in sorted(tests):
3320 for test in sorted(tests):
3316 if len(failuresbytest[test]) < fewestcount:
3321 if len(failuresbytest[test]) < fewestcount:
3317 fewesttest = test
3322 fewesttest = test
3318 fewestcount = len(failuresbytest[test])
3323 fewestcount = len(failuresbytest[test])
3319
3324
3320 leastfailing[key] = (fewestcount, fewesttest)
3325 leastfailing[key] = (fewestcount, fewesttest)
3321
3326
3322 # Create a combined counter so we can sort by total occurrences and
3327 # Create a combined counter so we can sort by total occurrences and
3323 # impacted tests.
3328 # impacted tests.
3324 combined = {}
3329 combined = {}
3325 for key in exceptioncounts:
3330 for key in exceptioncounts:
3326 combined[key] = (exceptioncounts[key],
3331 combined[key] = (exceptioncounts[key],
3327 len(testsbyfailure[key]),
3332 len(testsbyfailure[key]),
3328 leastfailing[key][0],
3333 leastfailing[key][0],
3329 leastfailing[key][1])
3334 leastfailing[key][1])
3330
3335
3331 return {
3336 return {
3332 'exceptioncounts': exceptioncounts,
3337 'exceptioncounts': exceptioncounts,
3333 'total': sum(exceptioncounts.values()),
3338 'total': sum(exceptioncounts.values()),
3334 'combined': combined,
3339 'combined': combined,
3335 'leastfailing': leastfailing,
3340 'leastfailing': leastfailing,
3336 'byfailure': testsbyfailure,
3341 'byfailure': testsbyfailure,
3337 'bytest': failuresbytest,
3342 'bytest': failuresbytest,
3338 }
3343 }
3339
3344
3340 if __name__ == '__main__':
3345 if __name__ == '__main__':
3341 runner = TestRunner()
3346 runner = TestRunner()
3342
3347
3343 try:
3348 try:
3344 import msvcrt
3349 import msvcrt
3345 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3350 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3346 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3351 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3347 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3352 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3348 except ImportError:
3353 except ImportError:
3349 pass
3354 pass
3350
3355
3351 sys.exit(runner.run(sys.argv[1:]))
3356 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now