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