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