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