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