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