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