##// END OF EJS Templates
tests: force a stable hostname in patchbomb tests...
Augie Fackler -
r39155:1f987f7c default
parent child Browse files
Show More
@@ -1,3176 +1,3177 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 = []
288 cases = []
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.append(sorted(l[11:].split()))
293 cases.append(sorted(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["HGHOSTNAME"] = "test-hostname"
1071 env['HGIPV6'] = str(int(self._useipv6))
1072 env['HGIPV6'] = str(int(self._useipv6))
1072
1073
1073 extraextensions = []
1074 extraextensions = []
1074 for opt in self._extraconfigopts:
1075 for opt in self._extraconfigopts:
1075 section, key = opt.encode('utf-8').split(b'.', 1)
1076 section, key = opt.encode('utf-8').split(b'.', 1)
1076 if section != 'extensions':
1077 if section != 'extensions':
1077 continue
1078 continue
1078 name = key.split(b'=', 1)[0]
1079 name = key.split(b'=', 1)[0]
1079 extraextensions.append(name)
1080 extraextensions.append(name)
1080
1081
1081 if extraextensions:
1082 if extraextensions:
1082 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1083 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1083
1084
1084 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1085 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1085 # IP addresses.
1086 # IP addresses.
1086 env['LOCALIP'] = self._localip()
1087 env['LOCALIP'] = self._localip()
1087
1088
1088 # Reset some environment variables to well-known values so that
1089 # Reset some environment variables to well-known values so that
1089 # the tests produce repeatable output.
1090 # the tests produce repeatable output.
1090 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1091 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1091 env['TZ'] = 'GMT'
1092 env['TZ'] = 'GMT'
1092 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1093 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1093 env['COLUMNS'] = '80'
1094 env['COLUMNS'] = '80'
1094 env['TERM'] = 'xterm'
1095 env['TERM'] = 'xterm'
1095
1096
1096 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
1097 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
1097 'HGPLAIN HGPLAINEXCEPT EDITOR VISUAL PAGER ' +
1098 'HGPLAIN HGPLAINEXCEPT EDITOR VISUAL PAGER ' +
1098 'NO_PROXY CHGDEBUG').split():
1099 'NO_PROXY CHGDEBUG').split():
1099 if k in env:
1100 if k in env:
1100 del env[k]
1101 del env[k]
1101
1102
1102 # unset env related to hooks
1103 # unset env related to hooks
1103 for k in list(env):
1104 for k in list(env):
1104 if k.startswith('HG_'):
1105 if k.startswith('HG_'):
1105 del env[k]
1106 del env[k]
1106
1107
1107 if self._usechg:
1108 if self._usechg:
1108 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1109 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1109
1110
1110 return env
1111 return env
1111
1112
1112 def _createhgrc(self, path):
1113 def _createhgrc(self, path):
1113 """Create an hgrc file for this test."""
1114 """Create an hgrc file for this test."""
1114 with open(path, 'wb') as hgrc:
1115 with open(path, 'wb') as hgrc:
1115 hgrc.write(b'[ui]\n')
1116 hgrc.write(b'[ui]\n')
1116 hgrc.write(b'slash = True\n')
1117 hgrc.write(b'slash = True\n')
1117 hgrc.write(b'interactive = False\n')
1118 hgrc.write(b'interactive = False\n')
1118 hgrc.write(b'mergemarkers = detailed\n')
1119 hgrc.write(b'mergemarkers = detailed\n')
1119 hgrc.write(b'promptecho = True\n')
1120 hgrc.write(b'promptecho = True\n')
1120 hgrc.write(b'[defaults]\n')
1121 hgrc.write(b'[defaults]\n')
1121 hgrc.write(b'[devel]\n')
1122 hgrc.write(b'[devel]\n')
1122 hgrc.write(b'all-warnings = true\n')
1123 hgrc.write(b'all-warnings = true\n')
1123 hgrc.write(b'default-date = 0 0\n')
1124 hgrc.write(b'default-date = 0 0\n')
1124 hgrc.write(b'[largefiles]\n')
1125 hgrc.write(b'[largefiles]\n')
1125 hgrc.write(b'usercache = %s\n' %
1126 hgrc.write(b'usercache = %s\n' %
1126 (os.path.join(self._testtmp, b'.cache/largefiles')))
1127 (os.path.join(self._testtmp, b'.cache/largefiles')))
1127 hgrc.write(b'[lfs]\n')
1128 hgrc.write(b'[lfs]\n')
1128 hgrc.write(b'usercache = %s\n' %
1129 hgrc.write(b'usercache = %s\n' %
1129 (os.path.join(self._testtmp, b'.cache/lfs')))
1130 (os.path.join(self._testtmp, b'.cache/lfs')))
1130 hgrc.write(b'[web]\n')
1131 hgrc.write(b'[web]\n')
1131 hgrc.write(b'address = localhost\n')
1132 hgrc.write(b'address = localhost\n')
1132 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1133 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1133 hgrc.write(b'server-header = testing stub value\n')
1134 hgrc.write(b'server-header = testing stub value\n')
1134
1135
1135 for opt in self._extraconfigopts:
1136 for opt in self._extraconfigopts:
1136 section, key = opt.encode('utf-8').split(b'.', 1)
1137 section, key = opt.encode('utf-8').split(b'.', 1)
1137 assert b'=' in key, ('extra config opt %s must '
1138 assert b'=' in key, ('extra config opt %s must '
1138 'have an = for assignment' % opt)
1139 'have an = for assignment' % opt)
1139 hgrc.write(b'[%s]\n%s\n' % (section, key))
1140 hgrc.write(b'[%s]\n%s\n' % (section, key))
1140
1141
1141 def fail(self, msg):
1142 def fail(self, msg):
1142 # unittest differentiates between errored and failed.
1143 # unittest differentiates between errored and failed.
1143 # Failed is denoted by AssertionError (by default at least).
1144 # Failed is denoted by AssertionError (by default at least).
1144 raise AssertionError(msg)
1145 raise AssertionError(msg)
1145
1146
1146 def _runcommand(self, cmd, env, normalizenewlines=False):
1147 def _runcommand(self, cmd, env, normalizenewlines=False):
1147 """Run command in a sub-process, capturing the output (stdout and
1148 """Run command in a sub-process, capturing the output (stdout and
1148 stderr).
1149 stderr).
1149
1150
1150 Return a tuple (exitcode, output). output is None in debug mode.
1151 Return a tuple (exitcode, output). output is None in debug mode.
1151 """
1152 """
1152 if self._debug:
1153 if self._debug:
1153 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
1154 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
1154 env=env)
1155 env=env)
1155 ret = proc.wait()
1156 ret = proc.wait()
1156 return (ret, None)
1157 return (ret, None)
1157
1158
1158 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1159 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1159 def cleanup():
1160 def cleanup():
1160 terminate(proc)
1161 terminate(proc)
1161 ret = proc.wait()
1162 ret = proc.wait()
1162 if ret == 0:
1163 if ret == 0:
1163 ret = signal.SIGTERM << 8
1164 ret = signal.SIGTERM << 8
1164 killdaemons(env['DAEMON_PIDS'])
1165 killdaemons(env['DAEMON_PIDS'])
1165 return ret
1166 return ret
1166
1167
1167 output = ''
1168 output = ''
1168 proc.tochild.close()
1169 proc.tochild.close()
1169
1170
1170 try:
1171 try:
1171 output = proc.fromchild.read()
1172 output = proc.fromchild.read()
1172 except KeyboardInterrupt:
1173 except KeyboardInterrupt:
1173 vlog('# Handling keyboard interrupt')
1174 vlog('# Handling keyboard interrupt')
1174 cleanup()
1175 cleanup()
1175 raise
1176 raise
1176
1177
1177 ret = proc.wait()
1178 ret = proc.wait()
1178 if wifexited(ret):
1179 if wifexited(ret):
1179 ret = os.WEXITSTATUS(ret)
1180 ret = os.WEXITSTATUS(ret)
1180
1181
1181 if proc.timeout:
1182 if proc.timeout:
1182 ret = 'timeout'
1183 ret = 'timeout'
1183
1184
1184 if ret:
1185 if ret:
1185 killdaemons(env['DAEMON_PIDS'])
1186 killdaemons(env['DAEMON_PIDS'])
1186
1187
1187 for s, r in self._getreplacements():
1188 for s, r in self._getreplacements():
1188 output = re.sub(s, r, output)
1189 output = re.sub(s, r, output)
1189
1190
1190 if normalizenewlines:
1191 if normalizenewlines:
1191 output = output.replace('\r\n', '\n')
1192 output = output.replace('\r\n', '\n')
1192
1193
1193 return ret, output.splitlines(True)
1194 return ret, output.splitlines(True)
1194
1195
1195 class PythonTest(Test):
1196 class PythonTest(Test):
1196 """A Python-based test."""
1197 """A Python-based test."""
1197
1198
1198 @property
1199 @property
1199 def refpath(self):
1200 def refpath(self):
1200 return os.path.join(self._testdir, b'%s.out' % self.bname)
1201 return os.path.join(self._testdir, b'%s.out' % self.bname)
1201
1202
1202 def _run(self, env):
1203 def _run(self, env):
1203 py3kswitch = self._py3kwarnings and b' -3' or b''
1204 py3kswitch = self._py3kwarnings and b' -3' or b''
1204 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
1205 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
1205 vlog("# Running", cmd)
1206 vlog("# Running", cmd)
1206 normalizenewlines = os.name == 'nt'
1207 normalizenewlines = os.name == 'nt'
1207 result = self._runcommand(cmd, env,
1208 result = self._runcommand(cmd, env,
1208 normalizenewlines=normalizenewlines)
1209 normalizenewlines=normalizenewlines)
1209 if self._aborted:
1210 if self._aborted:
1210 raise KeyboardInterrupt()
1211 raise KeyboardInterrupt()
1211
1212
1212 return result
1213 return result
1213
1214
1214 # Some glob patterns apply only in some circumstances, so the script
1215 # Some glob patterns apply only in some circumstances, so the script
1215 # might want to remove (glob) annotations that otherwise should be
1216 # might want to remove (glob) annotations that otherwise should be
1216 # retained.
1217 # retained.
1217 checkcodeglobpats = [
1218 checkcodeglobpats = [
1218 # On Windows it looks like \ doesn't require a (glob), but we know
1219 # On Windows it looks like \ doesn't require a (glob), but we know
1219 # better.
1220 # better.
1220 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1221 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1221 re.compile(br'^moving \S+/.*[^)]$'),
1222 re.compile(br'^moving \S+/.*[^)]$'),
1222 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1223 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1223 # Not all platforms have 127.0.0.1 as loopback (though most do),
1224 # Not all platforms have 127.0.0.1 as loopback (though most do),
1224 # so we always glob that too.
1225 # so we always glob that too.
1225 re.compile(br'.*\$LOCALIP.*$'),
1226 re.compile(br'.*\$LOCALIP.*$'),
1226 ]
1227 ]
1227
1228
1228 bchr = chr
1229 bchr = chr
1229 if PYTHON3:
1230 if PYTHON3:
1230 bchr = lambda x: bytes([x])
1231 bchr = lambda x: bytes([x])
1231
1232
1232 class TTest(Test):
1233 class TTest(Test):
1233 """A "t test" is a test backed by a .t file."""
1234 """A "t test" is a test backed by a .t file."""
1234
1235
1235 SKIPPED_PREFIX = b'skipped: '
1236 SKIPPED_PREFIX = b'skipped: '
1236 FAILED_PREFIX = b'hghave check failed: '
1237 FAILED_PREFIX = b'hghave check failed: '
1237 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1238 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1238
1239
1239 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1240 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))
1241 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1241 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1242 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1242
1243
1243 def __init__(self, path, *args, **kwds):
1244 def __init__(self, path, *args, **kwds):
1244 # accept an extra "case" parameter
1245 # accept an extra "case" parameter
1245 case = kwds.pop('case', [])
1246 case = kwds.pop('case', [])
1246 self._case = case
1247 self._case = case
1247 self._allcases = {x for y in parsettestcases(path) for x in y}
1248 self._allcases = {x for y in parsettestcases(path) for x in y}
1248 super(TTest, self).__init__(path, *args, **kwds)
1249 super(TTest, self).__init__(path, *args, **kwds)
1249 if case:
1250 if case:
1250 casepath = b'#'.join(case)
1251 casepath = b'#'.join(case)
1251 self.name = '%s#%s' % (self.name, _strpath(casepath))
1252 self.name = '%s#%s' % (self.name, _strpath(casepath))
1252 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1253 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1253 self._tmpname += b'-%s' % casepath
1254 self._tmpname += b'-%s' % casepath
1254 self._have = {}
1255 self._have = {}
1255
1256
1256 @property
1257 @property
1257 def refpath(self):
1258 def refpath(self):
1258 return os.path.join(self._testdir, self.bname)
1259 return os.path.join(self._testdir, self.bname)
1259
1260
1260 def _run(self, env):
1261 def _run(self, env):
1261 with open(self.path, 'rb') as f:
1262 with open(self.path, 'rb') as f:
1262 lines = f.readlines()
1263 lines = f.readlines()
1263
1264
1264 # .t file is both reference output and the test input, keep reference
1265 # .t file is both reference output and the test input, keep reference
1265 # output updated with the the test input. This avoids some race
1266 # output updated with the the test input. This avoids some race
1266 # conditions where the reference output does not match the actual test.
1267 # conditions where the reference output does not match the actual test.
1267 if self._refout is not None:
1268 if self._refout is not None:
1268 self._refout = lines
1269 self._refout = lines
1269
1270
1270 salt, script, after, expected = self._parsetest(lines)
1271 salt, script, after, expected = self._parsetest(lines)
1271
1272
1272 # Write out the generated script.
1273 # Write out the generated script.
1273 fname = b'%s.sh' % self._testtmp
1274 fname = b'%s.sh' % self._testtmp
1274 with open(fname, 'wb') as f:
1275 with open(fname, 'wb') as f:
1275 for l in script:
1276 for l in script:
1276 f.write(l)
1277 f.write(l)
1277
1278
1278 cmd = b'%s "%s"' % (self._shell, fname)
1279 cmd = b'%s "%s"' % (self._shell, fname)
1279 vlog("# Running", cmd)
1280 vlog("# Running", cmd)
1280
1281
1281 exitcode, output = self._runcommand(cmd, env)
1282 exitcode, output = self._runcommand(cmd, env)
1282
1283
1283 if self._aborted:
1284 if self._aborted:
1284 raise KeyboardInterrupt()
1285 raise KeyboardInterrupt()
1285
1286
1286 # Do not merge output if skipped. Return hghave message instead.
1287 # Do not merge output if skipped. Return hghave message instead.
1287 # Similarly, with --debug, output is None.
1288 # Similarly, with --debug, output is None.
1288 if exitcode == self.SKIPPED_STATUS or output is None:
1289 if exitcode == self.SKIPPED_STATUS or output is None:
1289 return exitcode, output
1290 return exitcode, output
1290
1291
1291 return self._processoutput(exitcode, output, salt, after, expected)
1292 return self._processoutput(exitcode, output, salt, after, expected)
1292
1293
1293 def _hghave(self, reqs):
1294 def _hghave(self, reqs):
1294 allreqs = b' '.join(reqs)
1295 allreqs = b' '.join(reqs)
1295 if allreqs in self._have:
1296 if allreqs in self._have:
1296 return self._have.get(allreqs)
1297 return self._have.get(allreqs)
1297
1298
1298 # TODO do something smarter when all other uses of hghave are gone.
1299 # TODO do something smarter when all other uses of hghave are gone.
1299 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1300 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1300 tdir = runtestdir.replace(b'\\', b'/')
1301 tdir = runtestdir.replace(b'\\', b'/')
1301 proc = Popen4(b'%s -c "%s/hghave %s"' %
1302 proc = Popen4(b'%s -c "%s/hghave %s"' %
1302 (self._shell, tdir, allreqs),
1303 (self._shell, tdir, allreqs),
1303 self._testtmp, 0, self._getenv())
1304 self._testtmp, 0, self._getenv())
1304 stdout, stderr = proc.communicate()
1305 stdout, stderr = proc.communicate()
1305 ret = proc.wait()
1306 ret = proc.wait()
1306 if wifexited(ret):
1307 if wifexited(ret):
1307 ret = os.WEXITSTATUS(ret)
1308 ret = os.WEXITSTATUS(ret)
1308 if ret == 2:
1309 if ret == 2:
1309 print(stdout.decode('utf-8'))
1310 print(stdout.decode('utf-8'))
1310 sys.exit(1)
1311 sys.exit(1)
1311
1312
1312 if ret != 0:
1313 if ret != 0:
1313 self._have[allreqs] = (False, stdout)
1314 self._have[allreqs] = (False, stdout)
1314 return False, stdout
1315 return False, stdout
1315
1316
1316 if b'slow' in reqs:
1317 if b'slow' in reqs:
1317 self._timeout = self._slowtimeout
1318 self._timeout = self._slowtimeout
1318
1319
1319 self._have[allreqs] = (True, None)
1320 self._have[allreqs] = (True, None)
1320 return True, None
1321 return True, None
1321
1322
1322 def _iftest(self, args):
1323 def _iftest(self, args):
1323 # implements "#if"
1324 # implements "#if"
1324 reqs = []
1325 reqs = []
1325 for arg in args:
1326 for arg in args:
1326 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1327 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1327 if arg[3:] in self._case:
1328 if arg[3:] in self._case:
1328 return False
1329 return False
1329 elif arg in self._allcases:
1330 elif arg in self._allcases:
1330 if arg not in self._case:
1331 if arg not in self._case:
1331 return False
1332 return False
1332 else:
1333 else:
1333 reqs.append(arg)
1334 reqs.append(arg)
1334 return self._hghave(reqs)[0]
1335 return self._hghave(reqs)[0]
1335
1336
1336 def _parsetest(self, lines):
1337 def _parsetest(self, lines):
1337 # We generate a shell script which outputs unique markers to line
1338 # We generate a shell script which outputs unique markers to line
1338 # up script results with our source. These markers include input
1339 # up script results with our source. These markers include input
1339 # line number and the last return code.
1340 # line number and the last return code.
1340 salt = b"SALT%d" % time.time()
1341 salt = b"SALT%d" % time.time()
1341 def addsalt(line, inpython):
1342 def addsalt(line, inpython):
1342 if inpython:
1343 if inpython:
1343 script.append(b'%s %d 0\n' % (salt, line))
1344 script.append(b'%s %d 0\n' % (salt, line))
1344 else:
1345 else:
1345 script.append(b'echo %s %d $?\n' % (salt, line))
1346 script.append(b'echo %s %d $?\n' % (salt, line))
1346
1347
1347 script = []
1348 script = []
1348
1349
1349 # After we run the shell script, we re-unify the script output
1350 # After we run the shell script, we re-unify the script output
1350 # with non-active parts of the source, with synchronization by our
1351 # with non-active parts of the source, with synchronization by our
1351 # SALT line number markers. The after table contains the non-active
1352 # SALT line number markers. The after table contains the non-active
1352 # components, ordered by line number.
1353 # components, ordered by line number.
1353 after = {}
1354 after = {}
1354
1355
1355 # Expected shell script output.
1356 # Expected shell script output.
1356 expected = {}
1357 expected = {}
1357
1358
1358 pos = prepos = -1
1359 pos = prepos = -1
1359
1360
1360 # True or False when in a true or false conditional section
1361 # True or False when in a true or false conditional section
1361 skipping = None
1362 skipping = None
1362
1363
1363 # We keep track of whether or not we're in a Python block so we
1364 # We keep track of whether or not we're in a Python block so we
1364 # can generate the surrounding doctest magic.
1365 # can generate the surrounding doctest magic.
1365 inpython = False
1366 inpython = False
1366
1367
1367 if self._debug:
1368 if self._debug:
1368 script.append(b'set -x\n')
1369 script.append(b'set -x\n')
1369 if self._hgcommand != b'hg':
1370 if self._hgcommand != b'hg':
1370 script.append(b'alias hg="%s"\n' % self._hgcommand)
1371 script.append(b'alias hg="%s"\n' % self._hgcommand)
1371 if os.getenv('MSYSTEM'):
1372 if os.getenv('MSYSTEM'):
1372 script.append(b'alias pwd="pwd -W"\n')
1373 script.append(b'alias pwd="pwd -W"\n')
1373 if self._case:
1374 if self._case:
1374 casestr = b'#'.join(self._case)
1375 casestr = b'#'.join(self._case)
1375 if isinstance(self._case, str):
1376 if isinstance(self._case, str):
1376 quoted = shellquote(casestr)
1377 quoted = shellquote(casestr)
1377 else:
1378 else:
1378 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1379 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1379 script.append(b'TESTCASE=%s\n' % quoted)
1380 script.append(b'TESTCASE=%s\n' % quoted)
1380 script.append(b'export TESTCASE\n')
1381 script.append(b'export TESTCASE\n')
1381
1382
1382 n = 0
1383 n = 0
1383 for n, l in enumerate(lines):
1384 for n, l in enumerate(lines):
1384 if not l.endswith(b'\n'):
1385 if not l.endswith(b'\n'):
1385 l += b'\n'
1386 l += b'\n'
1386 if l.startswith(b'#require'):
1387 if l.startswith(b'#require'):
1387 lsplit = l.split()
1388 lsplit = l.split()
1388 if len(lsplit) < 2 or lsplit[0] != b'#require':
1389 if len(lsplit) < 2 or lsplit[0] != b'#require':
1389 after.setdefault(pos, []).append(' !!! invalid #require\n')
1390 after.setdefault(pos, []).append(' !!! invalid #require\n')
1390 if not skipping:
1391 if not skipping:
1391 haveresult, message = self._hghave(lsplit[1:])
1392 haveresult, message = self._hghave(lsplit[1:])
1392 if not haveresult:
1393 if not haveresult:
1393 script = [b'echo "%s"\nexit 80\n' % message]
1394 script = [b'echo "%s"\nexit 80\n' % message]
1394 break
1395 break
1395 after.setdefault(pos, []).append(l)
1396 after.setdefault(pos, []).append(l)
1396 elif l.startswith(b'#if'):
1397 elif l.startswith(b'#if'):
1397 lsplit = l.split()
1398 lsplit = l.split()
1398 if len(lsplit) < 2 or lsplit[0] != b'#if':
1399 if len(lsplit) < 2 or lsplit[0] != b'#if':
1399 after.setdefault(pos, []).append(' !!! invalid #if\n')
1400 after.setdefault(pos, []).append(' !!! invalid #if\n')
1400 if skipping is not None:
1401 if skipping is not None:
1401 after.setdefault(pos, []).append(' !!! nested #if\n')
1402 after.setdefault(pos, []).append(' !!! nested #if\n')
1402 skipping = not self._iftest(lsplit[1:])
1403 skipping = not self._iftest(lsplit[1:])
1403 after.setdefault(pos, []).append(l)
1404 after.setdefault(pos, []).append(l)
1404 elif l.startswith(b'#else'):
1405 elif l.startswith(b'#else'):
1405 if skipping is None:
1406 if skipping is None:
1406 after.setdefault(pos, []).append(' !!! missing #if\n')
1407 after.setdefault(pos, []).append(' !!! missing #if\n')
1407 skipping = not skipping
1408 skipping = not skipping
1408 after.setdefault(pos, []).append(l)
1409 after.setdefault(pos, []).append(l)
1409 elif l.startswith(b'#endif'):
1410 elif l.startswith(b'#endif'):
1410 if skipping is None:
1411 if skipping is None:
1411 after.setdefault(pos, []).append(' !!! missing #if\n')
1412 after.setdefault(pos, []).append(' !!! missing #if\n')
1412 skipping = None
1413 skipping = None
1413 after.setdefault(pos, []).append(l)
1414 after.setdefault(pos, []).append(l)
1414 elif skipping:
1415 elif skipping:
1415 after.setdefault(pos, []).append(l)
1416 after.setdefault(pos, []).append(l)
1416 elif l.startswith(b' >>> '): # python inlines
1417 elif l.startswith(b' >>> '): # python inlines
1417 after.setdefault(pos, []).append(l)
1418 after.setdefault(pos, []).append(l)
1418 prepos = pos
1419 prepos = pos
1419 pos = n
1420 pos = n
1420 if not inpython:
1421 if not inpython:
1421 # We've just entered a Python block. Add the header.
1422 # We've just entered a Python block. Add the header.
1422 inpython = True
1423 inpython = True
1423 addsalt(prepos, False) # Make sure we report the exit code.
1424 addsalt(prepos, False) # Make sure we report the exit code.
1424 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1425 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1425 addsalt(n, True)
1426 addsalt(n, True)
1426 script.append(l[2:])
1427 script.append(l[2:])
1427 elif l.startswith(b' ... '): # python inlines
1428 elif l.startswith(b' ... '): # python inlines
1428 after.setdefault(prepos, []).append(l)
1429 after.setdefault(prepos, []).append(l)
1429 script.append(l[2:])
1430 script.append(l[2:])
1430 elif l.startswith(b' $ '): # commands
1431 elif l.startswith(b' $ '): # commands
1431 if inpython:
1432 if inpython:
1432 script.append(b'EOF\n')
1433 script.append(b'EOF\n')
1433 inpython = False
1434 inpython = False
1434 after.setdefault(pos, []).append(l)
1435 after.setdefault(pos, []).append(l)
1435 prepos = pos
1436 prepos = pos
1436 pos = n
1437 pos = n
1437 addsalt(n, False)
1438 addsalt(n, False)
1438 cmd = l[4:].split()
1439 cmd = l[4:].split()
1439 if len(cmd) == 2 and cmd[0] == b'cd':
1440 if len(cmd) == 2 and cmd[0] == b'cd':
1440 l = b' $ cd %s || exit 1\n' % cmd[1]
1441 l = b' $ cd %s || exit 1\n' % cmd[1]
1441 script.append(l[4:])
1442 script.append(l[4:])
1442 elif l.startswith(b' > '): # continuations
1443 elif l.startswith(b' > '): # continuations
1443 after.setdefault(prepos, []).append(l)
1444 after.setdefault(prepos, []).append(l)
1444 script.append(l[4:])
1445 script.append(l[4:])
1445 elif l.startswith(b' '): # results
1446 elif l.startswith(b' '): # results
1446 # Queue up a list of expected results.
1447 # Queue up a list of expected results.
1447 expected.setdefault(pos, []).append(l[2:])
1448 expected.setdefault(pos, []).append(l[2:])
1448 else:
1449 else:
1449 if inpython:
1450 if inpython:
1450 script.append(b'EOF\n')
1451 script.append(b'EOF\n')
1451 inpython = False
1452 inpython = False
1452 # Non-command/result. Queue up for merged output.
1453 # Non-command/result. Queue up for merged output.
1453 after.setdefault(pos, []).append(l)
1454 after.setdefault(pos, []).append(l)
1454
1455
1455 if inpython:
1456 if inpython:
1456 script.append(b'EOF\n')
1457 script.append(b'EOF\n')
1457 if skipping is not None:
1458 if skipping is not None:
1458 after.setdefault(pos, []).append(' !!! missing #endif\n')
1459 after.setdefault(pos, []).append(' !!! missing #endif\n')
1459 addsalt(n + 1, False)
1460 addsalt(n + 1, False)
1460
1461
1461 return salt, script, after, expected
1462 return salt, script, after, expected
1462
1463
1463 def _processoutput(self, exitcode, output, salt, after, expected):
1464 def _processoutput(self, exitcode, output, salt, after, expected):
1464 # Merge the script output back into a unified test.
1465 # Merge the script output back into a unified test.
1465 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1466 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1466 if exitcode != 0:
1467 if exitcode != 0:
1467 warnonly = 3
1468 warnonly = 3
1468
1469
1469 pos = -1
1470 pos = -1
1470 postout = []
1471 postout = []
1471 for l in output:
1472 for l in output:
1472 lout, lcmd = l, None
1473 lout, lcmd = l, None
1473 if salt in l:
1474 if salt in l:
1474 lout, lcmd = l.split(salt, 1)
1475 lout, lcmd = l.split(salt, 1)
1475
1476
1476 while lout:
1477 while lout:
1477 if not lout.endswith(b'\n'):
1478 if not lout.endswith(b'\n'):
1478 lout += b' (no-eol)\n'
1479 lout += b' (no-eol)\n'
1479
1480
1480 # Find the expected output at the current position.
1481 # Find the expected output at the current position.
1481 els = [None]
1482 els = [None]
1482 if expected.get(pos, None):
1483 if expected.get(pos, None):
1483 els = expected[pos]
1484 els = expected[pos]
1484
1485
1485 optional = []
1486 optional = []
1486 for i, el in enumerate(els):
1487 for i, el in enumerate(els):
1487 r = False
1488 r = False
1488 if el:
1489 if el:
1489 r, exact = self.linematch(el, lout)
1490 r, exact = self.linematch(el, lout)
1490 if isinstance(r, str):
1491 if isinstance(r, str):
1491 if r == '-glob':
1492 if r == '-glob':
1492 lout = ''.join(el.rsplit(' (glob)', 1))
1493 lout = ''.join(el.rsplit(' (glob)', 1))
1493 r = '' # Warn only this line.
1494 r = '' # Warn only this line.
1494 elif r == "retry":
1495 elif r == "retry":
1495 postout.append(b' ' + el)
1496 postout.append(b' ' + el)
1496 else:
1497 else:
1497 log('\ninfo, unknown linematch result: %r\n' % r)
1498 log('\ninfo, unknown linematch result: %r\n' % r)
1498 r = False
1499 r = False
1499 if r:
1500 if r:
1500 els.pop(i)
1501 els.pop(i)
1501 break
1502 break
1502 if el:
1503 if el:
1503 if el.endswith(b" (?)\n"):
1504 if el.endswith(b" (?)\n"):
1504 optional.append(i)
1505 optional.append(i)
1505 else:
1506 else:
1506 m = optline.match(el)
1507 m = optline.match(el)
1507 if m:
1508 if m:
1508 conditions = [
1509 conditions = [
1509 c for c in m.group(2).split(b' ')]
1510 c for c in m.group(2).split(b' ')]
1510
1511
1511 if not self._iftest(conditions):
1512 if not self._iftest(conditions):
1512 optional.append(i)
1513 optional.append(i)
1513 if exact:
1514 if exact:
1514 # Don't allow line to be matches against a later
1515 # Don't allow line to be matches against a later
1515 # line in the output
1516 # line in the output
1516 els.pop(i)
1517 els.pop(i)
1517 break
1518 break
1518
1519
1519 if r:
1520 if r:
1520 if r == "retry":
1521 if r == "retry":
1521 continue
1522 continue
1522 # clean up any optional leftovers
1523 # clean up any optional leftovers
1523 for i in optional:
1524 for i in optional:
1524 postout.append(b' ' + els[i])
1525 postout.append(b' ' + els[i])
1525 for i in reversed(optional):
1526 for i in reversed(optional):
1526 del els[i]
1527 del els[i]
1527 postout.append(b' ' + el)
1528 postout.append(b' ' + el)
1528 else:
1529 else:
1529 if self.NEEDESCAPE(lout):
1530 if self.NEEDESCAPE(lout):
1530 lout = TTest._stringescape(b'%s (esc)\n' %
1531 lout = TTest._stringescape(b'%s (esc)\n' %
1531 lout.rstrip(b'\n'))
1532 lout.rstrip(b'\n'))
1532 postout.append(b' ' + lout) # Let diff deal with it.
1533 postout.append(b' ' + lout) # Let diff deal with it.
1533 if r != '': # If line failed.
1534 if r != '': # If line failed.
1534 warnonly = 3 # for sure not
1535 warnonly = 3 # for sure not
1535 elif warnonly == 1: # Is "not yet" and line is warn only.
1536 elif warnonly == 1: # Is "not yet" and line is warn only.
1536 warnonly = 2 # Yes do warn.
1537 warnonly = 2 # Yes do warn.
1537 break
1538 break
1538 else:
1539 else:
1539 # clean up any optional leftovers
1540 # clean up any optional leftovers
1540 while expected.get(pos, None):
1541 while expected.get(pos, None):
1541 el = expected[pos].pop(0)
1542 el = expected[pos].pop(0)
1542 if el:
1543 if el:
1543 if not el.endswith(b" (?)\n"):
1544 if not el.endswith(b" (?)\n"):
1544 m = optline.match(el)
1545 m = optline.match(el)
1545 if m:
1546 if m:
1546 conditions = [c for c in m.group(2).split(b' ')]
1547 conditions = [c for c in m.group(2).split(b' ')]
1547
1548
1548 if self._iftest(conditions):
1549 if self._iftest(conditions):
1549 # Don't append as optional line
1550 # Don't append as optional line
1550 continue
1551 continue
1551 else:
1552 else:
1552 continue
1553 continue
1553 postout.append(b' ' + el)
1554 postout.append(b' ' + el)
1554
1555
1555 if lcmd:
1556 if lcmd:
1556 # Add on last return code.
1557 # Add on last return code.
1557 ret = int(lcmd.split()[1])
1558 ret = int(lcmd.split()[1])
1558 if ret != 0:
1559 if ret != 0:
1559 postout.append(b' [%d]\n' % ret)
1560 postout.append(b' [%d]\n' % ret)
1560 if pos in after:
1561 if pos in after:
1561 # Merge in non-active test bits.
1562 # Merge in non-active test bits.
1562 postout += after.pop(pos)
1563 postout += after.pop(pos)
1563 pos = int(lcmd.split()[0])
1564 pos = int(lcmd.split()[0])
1564
1565
1565 if pos in after:
1566 if pos in after:
1566 postout += after.pop(pos)
1567 postout += after.pop(pos)
1567
1568
1568 if warnonly == 2:
1569 if warnonly == 2:
1569 exitcode = False # Set exitcode to warned.
1570 exitcode = False # Set exitcode to warned.
1570
1571
1571 return exitcode, postout
1572 return exitcode, postout
1572
1573
1573 @staticmethod
1574 @staticmethod
1574 def rematch(el, l):
1575 def rematch(el, l):
1575 try:
1576 try:
1576 el = b'(?:' + el + b')'
1577 el = b'(?:' + el + b')'
1577 # use \Z to ensure that the regex matches to the end of the string
1578 # use \Z to ensure that the regex matches to the end of the string
1578 if os.name == 'nt':
1579 if os.name == 'nt':
1579 return re.match(el + br'\r?\n\Z', l)
1580 return re.match(el + br'\r?\n\Z', l)
1580 return re.match(el + br'\n\Z', l)
1581 return re.match(el + br'\n\Z', l)
1581 except re.error:
1582 except re.error:
1582 # el is an invalid regex
1583 # el is an invalid regex
1583 return False
1584 return False
1584
1585
1585 @staticmethod
1586 @staticmethod
1586 def globmatch(el, l):
1587 def globmatch(el, l):
1587 # The only supported special characters are * and ? plus / which also
1588 # The only supported special characters are * and ? plus / which also
1588 # matches \ on windows. Escaping of these characters is supported.
1589 # matches \ on windows. Escaping of these characters is supported.
1589 if el + b'\n' == l:
1590 if el + b'\n' == l:
1590 if os.altsep:
1591 if os.altsep:
1591 # matching on "/" is not needed for this line
1592 # matching on "/" is not needed for this line
1592 for pat in checkcodeglobpats:
1593 for pat in checkcodeglobpats:
1593 if pat.match(el):
1594 if pat.match(el):
1594 return True
1595 return True
1595 return b'-glob'
1596 return b'-glob'
1596 return True
1597 return True
1597 el = el.replace(b'$LOCALIP', b'*')
1598 el = el.replace(b'$LOCALIP', b'*')
1598 i, n = 0, len(el)
1599 i, n = 0, len(el)
1599 res = b''
1600 res = b''
1600 while i < n:
1601 while i < n:
1601 c = el[i:i + 1]
1602 c = el[i:i + 1]
1602 i += 1
1603 i += 1
1603 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1604 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1604 res += el[i - 1:i + 1]
1605 res += el[i - 1:i + 1]
1605 i += 1
1606 i += 1
1606 elif c == b'*':
1607 elif c == b'*':
1607 res += b'.*'
1608 res += b'.*'
1608 elif c == b'?':
1609 elif c == b'?':
1609 res += b'.'
1610 res += b'.'
1610 elif c == b'/' and os.altsep:
1611 elif c == b'/' and os.altsep:
1611 res += b'[/\\\\]'
1612 res += b'[/\\\\]'
1612 else:
1613 else:
1613 res += re.escape(c)
1614 res += re.escape(c)
1614 return TTest.rematch(res, l)
1615 return TTest.rematch(res, l)
1615
1616
1616 def linematch(self, el, l):
1617 def linematch(self, el, l):
1617 if el == l: # perfect match (fast)
1618 if el == l: # perfect match (fast)
1618 return True, True
1619 return True, True
1619 retry = False
1620 retry = False
1620 if el.endswith(b" (?)\n"):
1621 if el.endswith(b" (?)\n"):
1621 retry = "retry"
1622 retry = "retry"
1622 el = el[:-5] + b"\n"
1623 el = el[:-5] + b"\n"
1623 else:
1624 else:
1624 m = optline.match(el)
1625 m = optline.match(el)
1625 if m:
1626 if m:
1626 conditions = [c for c in m.group(2).split(b' ')]
1627 conditions = [c for c in m.group(2).split(b' ')]
1627
1628
1628 el = m.group(1) + b"\n"
1629 el = m.group(1) + b"\n"
1629 if not self._iftest(conditions):
1630 if not self._iftest(conditions):
1630 retry = "retry" # Not required by listed features
1631 retry = "retry" # Not required by listed features
1631
1632
1632 if el.endswith(b" (esc)\n"):
1633 if el.endswith(b" (esc)\n"):
1633 if PYTHON3:
1634 if PYTHON3:
1634 el = el[:-7].decode('unicode_escape') + '\n'
1635 el = el[:-7].decode('unicode_escape') + '\n'
1635 el = el.encode('utf-8')
1636 el = el.encode('utf-8')
1636 else:
1637 else:
1637 el = el[:-7].decode('string-escape') + '\n'
1638 el = el[:-7].decode('string-escape') + '\n'
1638 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1639 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1639 return True, True
1640 return True, True
1640 if el.endswith(b" (re)\n"):
1641 if el.endswith(b" (re)\n"):
1641 return (TTest.rematch(el[:-6], l) or retry), False
1642 return (TTest.rematch(el[:-6], l) or retry), False
1642 if el.endswith(b" (glob)\n"):
1643 if el.endswith(b" (glob)\n"):
1643 # ignore '(glob)' added to l by 'replacements'
1644 # ignore '(glob)' added to l by 'replacements'
1644 if l.endswith(b" (glob)\n"):
1645 if l.endswith(b" (glob)\n"):
1645 l = l[:-8] + b"\n"
1646 l = l[:-8] + b"\n"
1646 return (TTest.globmatch(el[:-8], l) or retry), False
1647 return (TTest.globmatch(el[:-8], l) or retry), False
1647 if os.altsep:
1648 if os.altsep:
1648 _l = l.replace(b'\\', b'/')
1649 _l = l.replace(b'\\', b'/')
1649 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1650 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1650 return True, True
1651 return True, True
1651 return retry, True
1652 return retry, True
1652
1653
1653 @staticmethod
1654 @staticmethod
1654 def parsehghaveoutput(lines):
1655 def parsehghaveoutput(lines):
1655 '''Parse hghave log lines.
1656 '''Parse hghave log lines.
1656
1657
1657 Return tuple of lists (missing, failed):
1658 Return tuple of lists (missing, failed):
1658 * the missing/unknown features
1659 * the missing/unknown features
1659 * the features for which existence check failed'''
1660 * the features for which existence check failed'''
1660 missing = []
1661 missing = []
1661 failed = []
1662 failed = []
1662 for line in lines:
1663 for line in lines:
1663 if line.startswith(TTest.SKIPPED_PREFIX):
1664 if line.startswith(TTest.SKIPPED_PREFIX):
1664 line = line.splitlines()[0]
1665 line = line.splitlines()[0]
1665 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1666 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1666 elif line.startswith(TTest.FAILED_PREFIX):
1667 elif line.startswith(TTest.FAILED_PREFIX):
1667 line = line.splitlines()[0]
1668 line = line.splitlines()[0]
1668 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1669 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1669
1670
1670 return missing, failed
1671 return missing, failed
1671
1672
1672 @staticmethod
1673 @staticmethod
1673 def _escapef(m):
1674 def _escapef(m):
1674 return TTest.ESCAPEMAP[m.group(0)]
1675 return TTest.ESCAPEMAP[m.group(0)]
1675
1676
1676 @staticmethod
1677 @staticmethod
1677 def _stringescape(s):
1678 def _stringescape(s):
1678 return TTest.ESCAPESUB(TTest._escapef, s)
1679 return TTest.ESCAPESUB(TTest._escapef, s)
1679
1680
1680 iolock = threading.RLock()
1681 iolock = threading.RLock()
1681 firstlock = threading.RLock()
1682 firstlock = threading.RLock()
1682 firsterror = False
1683 firsterror = False
1683
1684
1684 class TestResult(unittest._TextTestResult):
1685 class TestResult(unittest._TextTestResult):
1685 """Holds results when executing via unittest."""
1686 """Holds results when executing via unittest."""
1686 # Don't worry too much about accessing the non-public _TextTestResult.
1687 # Don't worry too much about accessing the non-public _TextTestResult.
1687 # It is relatively common in Python testing tools.
1688 # It is relatively common in Python testing tools.
1688 def __init__(self, options, *args, **kwargs):
1689 def __init__(self, options, *args, **kwargs):
1689 super(TestResult, self).__init__(*args, **kwargs)
1690 super(TestResult, self).__init__(*args, **kwargs)
1690
1691
1691 self._options = options
1692 self._options = options
1692
1693
1693 # unittest.TestResult didn't have skipped until 2.7. We need to
1694 # unittest.TestResult didn't have skipped until 2.7. We need to
1694 # polyfill it.
1695 # polyfill it.
1695 self.skipped = []
1696 self.skipped = []
1696
1697
1697 # We have a custom "ignored" result that isn't present in any Python
1698 # We have a custom "ignored" result that isn't present in any Python
1698 # unittest implementation. It is very similar to skipped. It may make
1699 # unittest implementation. It is very similar to skipped. It may make
1699 # sense to map it into skip some day.
1700 # sense to map it into skip some day.
1700 self.ignored = []
1701 self.ignored = []
1701
1702
1702 self.times = []
1703 self.times = []
1703 self._firststarttime = None
1704 self._firststarttime = None
1704 # Data stored for the benefit of generating xunit reports.
1705 # Data stored for the benefit of generating xunit reports.
1705 self.successes = []
1706 self.successes = []
1706 self.faildata = {}
1707 self.faildata = {}
1707
1708
1708 if options.color == 'auto':
1709 if options.color == 'auto':
1709 self.color = pygmentspresent and self.stream.isatty()
1710 self.color = pygmentspresent and self.stream.isatty()
1710 elif options.color == 'never':
1711 elif options.color == 'never':
1711 self.color = False
1712 self.color = False
1712 else: # 'always', for testing purposes
1713 else: # 'always', for testing purposes
1713 self.color = pygmentspresent
1714 self.color = pygmentspresent
1714
1715
1715 def onStart(self, test):
1716 def onStart(self, test):
1716 """ Can be overriden by custom TestResult
1717 """ Can be overriden by custom TestResult
1717 """
1718 """
1718
1719
1719 def onEnd(self):
1720 def onEnd(self):
1720 """ Can be overriden by custom TestResult
1721 """ Can be overriden by custom TestResult
1721 """
1722 """
1722
1723
1723 def addFailure(self, test, reason):
1724 def addFailure(self, test, reason):
1724 self.failures.append((test, reason))
1725 self.failures.append((test, reason))
1725
1726
1726 if self._options.first:
1727 if self._options.first:
1727 self.stop()
1728 self.stop()
1728 else:
1729 else:
1729 with iolock:
1730 with iolock:
1730 if reason == "timed out":
1731 if reason == "timed out":
1731 self.stream.write('t')
1732 self.stream.write('t')
1732 else:
1733 else:
1733 if not self._options.nodiff:
1734 if not self._options.nodiff:
1734 self.stream.write('\n')
1735 self.stream.write('\n')
1735 # Exclude the '\n' from highlighting to lex correctly
1736 # Exclude the '\n' from highlighting to lex correctly
1736 formatted = 'ERROR: %s output changed\n' % test
1737 formatted = 'ERROR: %s output changed\n' % test
1737 self.stream.write(highlightmsg(formatted, self.color))
1738 self.stream.write(highlightmsg(formatted, self.color))
1738 self.stream.write('!')
1739 self.stream.write('!')
1739
1740
1740 self.stream.flush()
1741 self.stream.flush()
1741
1742
1742 def addSuccess(self, test):
1743 def addSuccess(self, test):
1743 with iolock:
1744 with iolock:
1744 super(TestResult, self).addSuccess(test)
1745 super(TestResult, self).addSuccess(test)
1745 self.successes.append(test)
1746 self.successes.append(test)
1746
1747
1747 def addError(self, test, err):
1748 def addError(self, test, err):
1748 super(TestResult, self).addError(test, err)
1749 super(TestResult, self).addError(test, err)
1749 if self._options.first:
1750 if self._options.first:
1750 self.stop()
1751 self.stop()
1751
1752
1752 # Polyfill.
1753 # Polyfill.
1753 def addSkip(self, test, reason):
1754 def addSkip(self, test, reason):
1754 self.skipped.append((test, reason))
1755 self.skipped.append((test, reason))
1755 with iolock:
1756 with iolock:
1756 if self.showAll:
1757 if self.showAll:
1757 self.stream.writeln('skipped %s' % reason)
1758 self.stream.writeln('skipped %s' % reason)
1758 else:
1759 else:
1759 self.stream.write('s')
1760 self.stream.write('s')
1760 self.stream.flush()
1761 self.stream.flush()
1761
1762
1762 def addIgnore(self, test, reason):
1763 def addIgnore(self, test, reason):
1763 self.ignored.append((test, reason))
1764 self.ignored.append((test, reason))
1764 with iolock:
1765 with iolock:
1765 if self.showAll:
1766 if self.showAll:
1766 self.stream.writeln('ignored %s' % reason)
1767 self.stream.writeln('ignored %s' % reason)
1767 else:
1768 else:
1768 if reason not in ('not retesting', "doesn't match keyword"):
1769 if reason not in ('not retesting', "doesn't match keyword"):
1769 self.stream.write('i')
1770 self.stream.write('i')
1770 else:
1771 else:
1771 self.testsRun += 1
1772 self.testsRun += 1
1772 self.stream.flush()
1773 self.stream.flush()
1773
1774
1774 def addOutputMismatch(self, test, ret, got, expected):
1775 def addOutputMismatch(self, test, ret, got, expected):
1775 """Record a mismatch in test output for a particular test."""
1776 """Record a mismatch in test output for a particular test."""
1776 if self.shouldStop or firsterror:
1777 if self.shouldStop or firsterror:
1777 # don't print, some other test case already failed and
1778 # don't print, some other test case already failed and
1778 # printed, we're just stale and probably failed due to our
1779 # printed, we're just stale and probably failed due to our
1779 # temp dir getting cleaned up.
1780 # temp dir getting cleaned up.
1780 return
1781 return
1781
1782
1782 accepted = False
1783 accepted = False
1783 lines = []
1784 lines = []
1784
1785
1785 with iolock:
1786 with iolock:
1786 if self._options.nodiff:
1787 if self._options.nodiff:
1787 pass
1788 pass
1788 elif self._options.view:
1789 elif self._options.view:
1789 v = self._options.view
1790 v = self._options.view
1790 if PYTHON3:
1791 if PYTHON3:
1791 v = _bytespath(v)
1792 v = _bytespath(v)
1792 os.system(b"%s %s %s" %
1793 os.system(b"%s %s %s" %
1793 (v, test.refpath, test.errpath))
1794 (v, test.refpath, test.errpath))
1794 else:
1795 else:
1795 servefail, lines = getdiff(expected, got,
1796 servefail, lines = getdiff(expected, got,
1796 test.refpath, test.errpath)
1797 test.refpath, test.errpath)
1797 self.stream.write('\n')
1798 self.stream.write('\n')
1798 for line in lines:
1799 for line in lines:
1799 line = highlightdiff(line, self.color)
1800 line = highlightdiff(line, self.color)
1800 if PYTHON3:
1801 if PYTHON3:
1801 self.stream.flush()
1802 self.stream.flush()
1802 self.stream.buffer.write(line)
1803 self.stream.buffer.write(line)
1803 self.stream.buffer.flush()
1804 self.stream.buffer.flush()
1804 else:
1805 else:
1805 self.stream.write(line)
1806 self.stream.write(line)
1806 self.stream.flush()
1807 self.stream.flush()
1807
1808
1808 if servefail:
1809 if servefail:
1809 raise test.failureException(
1810 raise test.failureException(
1810 'server failed to start (HGPORT=%s)' % test._startport)
1811 'server failed to start (HGPORT=%s)' % test._startport)
1811
1812
1812 # handle interactive prompt without releasing iolock
1813 # handle interactive prompt without releasing iolock
1813 if self._options.interactive:
1814 if self._options.interactive:
1814 if test.readrefout() != expected:
1815 if test.readrefout() != expected:
1815 self.stream.write(
1816 self.stream.write(
1816 'Reference output has changed (run again to prompt '
1817 'Reference output has changed (run again to prompt '
1817 'changes)')
1818 'changes)')
1818 else:
1819 else:
1819 self.stream.write('Accept this change? [n] ')
1820 self.stream.write('Accept this change? [n] ')
1820 answer = sys.stdin.readline().strip()
1821 answer = sys.stdin.readline().strip()
1821 if answer.lower() in ('y', 'yes'):
1822 if answer.lower() in ('y', 'yes'):
1822 if test.path.endswith(b'.t'):
1823 if test.path.endswith(b'.t'):
1823 rename(test.errpath, test.path)
1824 rename(test.errpath, test.path)
1824 else:
1825 else:
1825 rename(test.errpath, '%s.out' % test.path)
1826 rename(test.errpath, '%s.out' % test.path)
1826 accepted = True
1827 accepted = True
1827 if not accepted:
1828 if not accepted:
1828 self.faildata[test.name] = b''.join(lines)
1829 self.faildata[test.name] = b''.join(lines)
1829
1830
1830 return accepted
1831 return accepted
1831
1832
1832 def startTest(self, test):
1833 def startTest(self, test):
1833 super(TestResult, self).startTest(test)
1834 super(TestResult, self).startTest(test)
1834
1835
1835 # os.times module computes the user time and system time spent by
1836 # os.times module computes the user time and system time spent by
1836 # child's processes along with real elapsed time taken by a process.
1837 # child's processes along with real elapsed time taken by a process.
1837 # This module has one limitation. It can only work for Linux user
1838 # This module has one limitation. It can only work for Linux user
1838 # and not for Windows.
1839 # and not for Windows.
1839 test.started = os.times()
1840 test.started = os.times()
1840 if self._firststarttime is None: # thread racy but irrelevant
1841 if self._firststarttime is None: # thread racy but irrelevant
1841 self._firststarttime = test.started[4]
1842 self._firststarttime = test.started[4]
1842
1843
1843 def stopTest(self, test, interrupted=False):
1844 def stopTest(self, test, interrupted=False):
1844 super(TestResult, self).stopTest(test)
1845 super(TestResult, self).stopTest(test)
1845
1846
1846 test.stopped = os.times()
1847 test.stopped = os.times()
1847
1848
1848 starttime = test.started
1849 starttime = test.started
1849 endtime = test.stopped
1850 endtime = test.stopped
1850 origin = self._firststarttime
1851 origin = self._firststarttime
1851 self.times.append((test.name,
1852 self.times.append((test.name,
1852 endtime[2] - starttime[2], # user space CPU time
1853 endtime[2] - starttime[2], # user space CPU time
1853 endtime[3] - starttime[3], # sys space CPU time
1854 endtime[3] - starttime[3], # sys space CPU time
1854 endtime[4] - starttime[4], # real time
1855 endtime[4] - starttime[4], # real time
1855 starttime[4] - origin, # start date in run context
1856 starttime[4] - origin, # start date in run context
1856 endtime[4] - origin, # end date in run context
1857 endtime[4] - origin, # end date in run context
1857 ))
1858 ))
1858
1859
1859 if interrupted:
1860 if interrupted:
1860 with iolock:
1861 with iolock:
1861 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1862 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1862 test.name, self.times[-1][3]))
1863 test.name, self.times[-1][3]))
1863
1864
1864 def getTestResult():
1865 def getTestResult():
1865 """
1866 """
1866 Returns the relevant test result
1867 Returns the relevant test result
1867 """
1868 """
1868 if "CUSTOM_TEST_RESULT" in os.environ:
1869 if "CUSTOM_TEST_RESULT" in os.environ:
1869 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
1870 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
1870 return testresultmodule.TestResult
1871 return testresultmodule.TestResult
1871 else:
1872 else:
1872 return TestResult
1873 return TestResult
1873
1874
1874 class TestSuite(unittest.TestSuite):
1875 class TestSuite(unittest.TestSuite):
1875 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1876 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1876
1877
1877 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1878 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1878 retest=False, keywords=None, loop=False, runs_per_test=1,
1879 retest=False, keywords=None, loop=False, runs_per_test=1,
1879 loadtest=None, showchannels=False,
1880 loadtest=None, showchannels=False,
1880 *args, **kwargs):
1881 *args, **kwargs):
1881 """Create a new instance that can run tests with a configuration.
1882 """Create a new instance that can run tests with a configuration.
1882
1883
1883 testdir specifies the directory where tests are executed from. This
1884 testdir specifies the directory where tests are executed from. This
1884 is typically the ``tests`` directory from Mercurial's source
1885 is typically the ``tests`` directory from Mercurial's source
1885 repository.
1886 repository.
1886
1887
1887 jobs specifies the number of jobs to run concurrently. Each test
1888 jobs specifies the number of jobs to run concurrently. Each test
1888 executes on its own thread. Tests actually spawn new processes, so
1889 executes on its own thread. Tests actually spawn new processes, so
1889 state mutation should not be an issue.
1890 state mutation should not be an issue.
1890
1891
1891 If there is only one job, it will use the main thread.
1892 If there is only one job, it will use the main thread.
1892
1893
1893 whitelist and blacklist denote tests that have been whitelisted and
1894 whitelist and blacklist denote tests that have been whitelisted and
1894 blacklisted, respectively. These arguments don't belong in TestSuite.
1895 blacklisted, respectively. These arguments don't belong in TestSuite.
1895 Instead, whitelist and blacklist should be handled by the thing that
1896 Instead, whitelist and blacklist should be handled by the thing that
1896 populates the TestSuite with tests. They are present to preserve
1897 populates the TestSuite with tests. They are present to preserve
1897 backwards compatible behavior which reports skipped tests as part
1898 backwards compatible behavior which reports skipped tests as part
1898 of the results.
1899 of the results.
1899
1900
1900 retest denotes whether to retest failed tests. This arguably belongs
1901 retest denotes whether to retest failed tests. This arguably belongs
1901 outside of TestSuite.
1902 outside of TestSuite.
1902
1903
1903 keywords denotes key words that will be used to filter which tests
1904 keywords denotes key words that will be used to filter which tests
1904 to execute. This arguably belongs outside of TestSuite.
1905 to execute. This arguably belongs outside of TestSuite.
1905
1906
1906 loop denotes whether to loop over tests forever.
1907 loop denotes whether to loop over tests forever.
1907 """
1908 """
1908 super(TestSuite, self).__init__(*args, **kwargs)
1909 super(TestSuite, self).__init__(*args, **kwargs)
1909
1910
1910 self._jobs = jobs
1911 self._jobs = jobs
1911 self._whitelist = whitelist
1912 self._whitelist = whitelist
1912 self._blacklist = blacklist
1913 self._blacklist = blacklist
1913 self._retest = retest
1914 self._retest = retest
1914 self._keywords = keywords
1915 self._keywords = keywords
1915 self._loop = loop
1916 self._loop = loop
1916 self._runs_per_test = runs_per_test
1917 self._runs_per_test = runs_per_test
1917 self._loadtest = loadtest
1918 self._loadtest = loadtest
1918 self._showchannels = showchannels
1919 self._showchannels = showchannels
1919
1920
1920 def run(self, result):
1921 def run(self, result):
1921 # We have a number of filters that need to be applied. We do this
1922 # We have a number of filters that need to be applied. We do this
1922 # here instead of inside Test because it makes the running logic for
1923 # here instead of inside Test because it makes the running logic for
1923 # Test simpler.
1924 # Test simpler.
1924 tests = []
1925 tests = []
1925 num_tests = [0]
1926 num_tests = [0]
1926 for test in self._tests:
1927 for test in self._tests:
1927 def get():
1928 def get():
1928 num_tests[0] += 1
1929 num_tests[0] += 1
1929 if getattr(test, 'should_reload', False):
1930 if getattr(test, 'should_reload', False):
1930 return self._loadtest(test, num_tests[0])
1931 return self._loadtest(test, num_tests[0])
1931 return test
1932 return test
1932 if not os.path.exists(test.path):
1933 if not os.path.exists(test.path):
1933 result.addSkip(test, "Doesn't exist")
1934 result.addSkip(test, "Doesn't exist")
1934 continue
1935 continue
1935
1936
1936 if not (self._whitelist and test.bname in self._whitelist):
1937 if not (self._whitelist and test.bname in self._whitelist):
1937 if self._blacklist and test.bname in self._blacklist:
1938 if self._blacklist and test.bname in self._blacklist:
1938 result.addSkip(test, 'blacklisted')
1939 result.addSkip(test, 'blacklisted')
1939 continue
1940 continue
1940
1941
1941 if self._retest and not os.path.exists(test.errpath):
1942 if self._retest and not os.path.exists(test.errpath):
1942 result.addIgnore(test, 'not retesting')
1943 result.addIgnore(test, 'not retesting')
1943 continue
1944 continue
1944
1945
1945 if self._keywords:
1946 if self._keywords:
1946 with open(test.path, 'rb') as f:
1947 with open(test.path, 'rb') as f:
1947 t = f.read().lower() + test.bname.lower()
1948 t = f.read().lower() + test.bname.lower()
1948 ignored = False
1949 ignored = False
1949 for k in self._keywords.lower().split():
1950 for k in self._keywords.lower().split():
1950 if k not in t:
1951 if k not in t:
1951 result.addIgnore(test, "doesn't match keyword")
1952 result.addIgnore(test, "doesn't match keyword")
1952 ignored = True
1953 ignored = True
1953 break
1954 break
1954
1955
1955 if ignored:
1956 if ignored:
1956 continue
1957 continue
1957 for _ in xrange(self._runs_per_test):
1958 for _ in xrange(self._runs_per_test):
1958 tests.append(get())
1959 tests.append(get())
1959
1960
1960 runtests = list(tests)
1961 runtests = list(tests)
1961 done = queue.Queue()
1962 done = queue.Queue()
1962 running = 0
1963 running = 0
1963
1964
1964 channels = [""] * self._jobs
1965 channels = [""] * self._jobs
1965
1966
1966 def job(test, result):
1967 def job(test, result):
1967 for n, v in enumerate(channels):
1968 for n, v in enumerate(channels):
1968 if not v:
1969 if not v:
1969 channel = n
1970 channel = n
1970 break
1971 break
1971 else:
1972 else:
1972 raise ValueError('Could not find output channel')
1973 raise ValueError('Could not find output channel')
1973 channels[channel] = "=" + test.name[5:].split(".")[0]
1974 channels[channel] = "=" + test.name[5:].split(".")[0]
1974 try:
1975 try:
1975 test(result)
1976 test(result)
1976 done.put(None)
1977 done.put(None)
1977 except KeyboardInterrupt:
1978 except KeyboardInterrupt:
1978 pass
1979 pass
1979 except: # re-raises
1980 except: # re-raises
1980 done.put(('!', test, 'run-test raised an error, see traceback'))
1981 done.put(('!', test, 'run-test raised an error, see traceback'))
1981 raise
1982 raise
1982 finally:
1983 finally:
1983 try:
1984 try:
1984 channels[channel] = ''
1985 channels[channel] = ''
1985 except IndexError:
1986 except IndexError:
1986 pass
1987 pass
1987
1988
1988 def stat():
1989 def stat():
1989 count = 0
1990 count = 0
1990 while channels:
1991 while channels:
1991 d = '\n%03s ' % count
1992 d = '\n%03s ' % count
1992 for n, v in enumerate(channels):
1993 for n, v in enumerate(channels):
1993 if v:
1994 if v:
1994 d += v[0]
1995 d += v[0]
1995 channels[n] = v[1:] or '.'
1996 channels[n] = v[1:] or '.'
1996 else:
1997 else:
1997 d += ' '
1998 d += ' '
1998 d += ' '
1999 d += ' '
1999 with iolock:
2000 with iolock:
2000 sys.stdout.write(d + ' ')
2001 sys.stdout.write(d + ' ')
2001 sys.stdout.flush()
2002 sys.stdout.flush()
2002 for x in xrange(10):
2003 for x in xrange(10):
2003 if channels:
2004 if channels:
2004 time.sleep(.1)
2005 time.sleep(.1)
2005 count += 1
2006 count += 1
2006
2007
2007 stoppedearly = False
2008 stoppedearly = False
2008
2009
2009 if self._showchannels:
2010 if self._showchannels:
2010 statthread = threading.Thread(target=stat, name="stat")
2011 statthread = threading.Thread(target=stat, name="stat")
2011 statthread.start()
2012 statthread.start()
2012
2013
2013 try:
2014 try:
2014 while tests or running:
2015 while tests or running:
2015 if not done.empty() or running == self._jobs or not tests:
2016 if not done.empty() or running == self._jobs or not tests:
2016 try:
2017 try:
2017 done.get(True, 1)
2018 done.get(True, 1)
2018 running -= 1
2019 running -= 1
2019 if result and result.shouldStop:
2020 if result and result.shouldStop:
2020 stoppedearly = True
2021 stoppedearly = True
2021 break
2022 break
2022 except queue.Empty:
2023 except queue.Empty:
2023 continue
2024 continue
2024 if tests and not running == self._jobs:
2025 if tests and not running == self._jobs:
2025 test = tests.pop(0)
2026 test = tests.pop(0)
2026 if self._loop:
2027 if self._loop:
2027 if getattr(test, 'should_reload', False):
2028 if getattr(test, 'should_reload', False):
2028 num_tests[0] += 1
2029 num_tests[0] += 1
2029 tests.append(
2030 tests.append(
2030 self._loadtest(test, num_tests[0]))
2031 self._loadtest(test, num_tests[0]))
2031 else:
2032 else:
2032 tests.append(test)
2033 tests.append(test)
2033 if self._jobs == 1:
2034 if self._jobs == 1:
2034 job(test, result)
2035 job(test, result)
2035 else:
2036 else:
2036 t = threading.Thread(target=job, name=test.name,
2037 t = threading.Thread(target=job, name=test.name,
2037 args=(test, result))
2038 args=(test, result))
2038 t.start()
2039 t.start()
2039 running += 1
2040 running += 1
2040
2041
2041 # If we stop early we still need to wait on started tests to
2042 # If we stop early we still need to wait on started tests to
2042 # finish. Otherwise, there is a race between the test completing
2043 # finish. Otherwise, there is a race between the test completing
2043 # and the test's cleanup code running. This could result in the
2044 # and the test's cleanup code running. This could result in the
2044 # test reporting incorrect.
2045 # test reporting incorrect.
2045 if stoppedearly:
2046 if stoppedearly:
2046 while running:
2047 while running:
2047 try:
2048 try:
2048 done.get(True, 1)
2049 done.get(True, 1)
2049 running -= 1
2050 running -= 1
2050 except queue.Empty:
2051 except queue.Empty:
2051 continue
2052 continue
2052 except KeyboardInterrupt:
2053 except KeyboardInterrupt:
2053 for test in runtests:
2054 for test in runtests:
2054 test.abort()
2055 test.abort()
2055
2056
2056 channels = []
2057 channels = []
2057
2058
2058 return result
2059 return result
2059
2060
2060 # Save the most recent 5 wall-clock runtimes of each test to a
2061 # Save the most recent 5 wall-clock runtimes of each test to a
2061 # human-readable text file named .testtimes. Tests are sorted
2062 # human-readable text file named .testtimes. Tests are sorted
2062 # alphabetically, while times for each test are listed from oldest to
2063 # alphabetically, while times for each test are listed from oldest to
2063 # newest.
2064 # newest.
2064
2065
2065 def loadtimes(outputdir):
2066 def loadtimes(outputdir):
2066 times = []
2067 times = []
2067 try:
2068 try:
2068 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2069 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2069 for line in fp:
2070 for line in fp:
2070 m = re.match('(.*?) ([0-9. ]+)', line)
2071 m = re.match('(.*?) ([0-9. ]+)', line)
2071 times.append((m.group(1),
2072 times.append((m.group(1),
2072 [float(t) for t in m.group(2).split()]))
2073 [float(t) for t in m.group(2).split()]))
2073 except IOError as err:
2074 except IOError as err:
2074 if err.errno != errno.ENOENT:
2075 if err.errno != errno.ENOENT:
2075 raise
2076 raise
2076 return times
2077 return times
2077
2078
2078 def savetimes(outputdir, result):
2079 def savetimes(outputdir, result):
2079 saved = dict(loadtimes(outputdir))
2080 saved = dict(loadtimes(outputdir))
2080 maxruns = 5
2081 maxruns = 5
2081 skipped = set([str(t[0]) for t in result.skipped])
2082 skipped = set([str(t[0]) for t in result.skipped])
2082 for tdata in result.times:
2083 for tdata in result.times:
2083 test, real = tdata[0], tdata[3]
2084 test, real = tdata[0], tdata[3]
2084 if test not in skipped:
2085 if test not in skipped:
2085 ts = saved.setdefault(test, [])
2086 ts = saved.setdefault(test, [])
2086 ts.append(real)
2087 ts.append(real)
2087 ts[:] = ts[-maxruns:]
2088 ts[:] = ts[-maxruns:]
2088
2089
2089 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2090 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2090 dir=outputdir, text=True)
2091 dir=outputdir, text=True)
2091 with os.fdopen(fd, 'w') as fp:
2092 with os.fdopen(fd, 'w') as fp:
2092 for name, ts in sorted(saved.items()):
2093 for name, ts in sorted(saved.items()):
2093 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2094 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2094 timepath = os.path.join(outputdir, b'.testtimes')
2095 timepath = os.path.join(outputdir, b'.testtimes')
2095 try:
2096 try:
2096 os.unlink(timepath)
2097 os.unlink(timepath)
2097 except OSError:
2098 except OSError:
2098 pass
2099 pass
2099 try:
2100 try:
2100 os.rename(tmpname, timepath)
2101 os.rename(tmpname, timepath)
2101 except OSError:
2102 except OSError:
2102 pass
2103 pass
2103
2104
2104 class TextTestRunner(unittest.TextTestRunner):
2105 class TextTestRunner(unittest.TextTestRunner):
2105 """Custom unittest test runner that uses appropriate settings."""
2106 """Custom unittest test runner that uses appropriate settings."""
2106
2107
2107 def __init__(self, runner, *args, **kwargs):
2108 def __init__(self, runner, *args, **kwargs):
2108 super(TextTestRunner, self).__init__(*args, **kwargs)
2109 super(TextTestRunner, self).__init__(*args, **kwargs)
2109
2110
2110 self._runner = runner
2111 self._runner = runner
2111
2112
2112 self._result = getTestResult()(self._runner.options, self.stream,
2113 self._result = getTestResult()(self._runner.options, self.stream,
2113 self.descriptions, self.verbosity)
2114 self.descriptions, self.verbosity)
2114
2115
2115 def listtests(self, test):
2116 def listtests(self, test):
2116 test = sorted(test, key=lambda t: t.name)
2117 test = sorted(test, key=lambda t: t.name)
2117
2118
2118 self._result.onStart(test)
2119 self._result.onStart(test)
2119
2120
2120 for t in test:
2121 for t in test:
2121 print(t.name)
2122 print(t.name)
2122 self._result.addSuccess(t)
2123 self._result.addSuccess(t)
2123
2124
2124 if self._runner.options.xunit:
2125 if self._runner.options.xunit:
2125 with open(self._runner.options.xunit, "wb") as xuf:
2126 with open(self._runner.options.xunit, "wb") as xuf:
2126 self._writexunit(self._result, xuf)
2127 self._writexunit(self._result, xuf)
2127
2128
2128 if self._runner.options.json:
2129 if self._runner.options.json:
2129 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2130 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2130 with open(jsonpath, 'w') as fp:
2131 with open(jsonpath, 'w') as fp:
2131 self._writejson(self._result, fp)
2132 self._writejson(self._result, fp)
2132
2133
2133 return self._result
2134 return self._result
2134
2135
2135 def run(self, test):
2136 def run(self, test):
2136 self._result.onStart(test)
2137 self._result.onStart(test)
2137 test(self._result)
2138 test(self._result)
2138
2139
2139 failed = len(self._result.failures)
2140 failed = len(self._result.failures)
2140 skipped = len(self._result.skipped)
2141 skipped = len(self._result.skipped)
2141 ignored = len(self._result.ignored)
2142 ignored = len(self._result.ignored)
2142
2143
2143 with iolock:
2144 with iolock:
2144 self.stream.writeln('')
2145 self.stream.writeln('')
2145
2146
2146 if not self._runner.options.noskips:
2147 if not self._runner.options.noskips:
2147 for test, msg in self._result.skipped:
2148 for test, msg in self._result.skipped:
2148 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2149 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2149 msg = highlightmsg(formatted, self._result.color)
2150 msg = highlightmsg(formatted, self._result.color)
2150 self.stream.write(msg)
2151 self.stream.write(msg)
2151 for test, msg in self._result.failures:
2152 for test, msg in self._result.failures:
2152 formatted = 'Failed %s: %s\n' % (test.name, msg)
2153 formatted = 'Failed %s: %s\n' % (test.name, msg)
2153 self.stream.write(highlightmsg(formatted, self._result.color))
2154 self.stream.write(highlightmsg(formatted, self._result.color))
2154 for test, msg in self._result.errors:
2155 for test, msg in self._result.errors:
2155 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2156 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2156
2157
2157 if self._runner.options.xunit:
2158 if self._runner.options.xunit:
2158 with open(self._runner.options.xunit, "wb") as xuf:
2159 with open(self._runner.options.xunit, "wb") as xuf:
2159 self._writexunit(self._result, xuf)
2160 self._writexunit(self._result, xuf)
2160
2161
2161 if self._runner.options.json:
2162 if self._runner.options.json:
2162 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2163 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2163 with open(jsonpath, 'w') as fp:
2164 with open(jsonpath, 'w') as fp:
2164 self._writejson(self._result, fp)
2165 self._writejson(self._result, fp)
2165
2166
2166 self._runner._checkhglib('Tested')
2167 self._runner._checkhglib('Tested')
2167
2168
2168 savetimes(self._runner._outputdir, self._result)
2169 savetimes(self._runner._outputdir, self._result)
2169
2170
2170 if failed and self._runner.options.known_good_rev:
2171 if failed and self._runner.options.known_good_rev:
2171 self._bisecttests(t for t, m in self._result.failures)
2172 self._bisecttests(t for t, m in self._result.failures)
2172 self.stream.writeln(
2173 self.stream.writeln(
2173 '# Ran %d tests, %d skipped, %d failed.'
2174 '# Ran %d tests, %d skipped, %d failed.'
2174 % (self._result.testsRun, skipped + ignored, failed))
2175 % (self._result.testsRun, skipped + ignored, failed))
2175 if failed:
2176 if failed:
2176 self.stream.writeln('python hash seed: %s' %
2177 self.stream.writeln('python hash seed: %s' %
2177 os.environ['PYTHONHASHSEED'])
2178 os.environ['PYTHONHASHSEED'])
2178 if self._runner.options.time:
2179 if self._runner.options.time:
2179 self.printtimes(self._result.times)
2180 self.printtimes(self._result.times)
2180
2181
2181 if self._runner.options.exceptions:
2182 if self._runner.options.exceptions:
2182 exceptions = aggregateexceptions(
2183 exceptions = aggregateexceptions(
2183 os.path.join(self._runner._outputdir, b'exceptions'))
2184 os.path.join(self._runner._outputdir, b'exceptions'))
2184
2185
2185 self.stream.writeln('Exceptions Report:')
2186 self.stream.writeln('Exceptions Report:')
2186 self.stream.writeln('%d total from %d frames' %
2187 self.stream.writeln('%d total from %d frames' %
2187 (exceptions['total'],
2188 (exceptions['total'],
2188 len(exceptions['exceptioncounts'])))
2189 len(exceptions['exceptioncounts'])))
2189 combined = exceptions['combined']
2190 combined = exceptions['combined']
2190 for key in sorted(combined, key=combined.get, reverse=True):
2191 for key in sorted(combined, key=combined.get, reverse=True):
2191 frame, line, exc = key
2192 frame, line, exc = key
2192 totalcount, testcount, leastcount, leasttest = combined[key]
2193 totalcount, testcount, leastcount, leasttest = combined[key]
2193
2194
2194 self.stream.writeln('%d (%d tests)\t%s: %s (%s - %d total)'
2195 self.stream.writeln('%d (%d tests)\t%s: %s (%s - %d total)'
2195 % (totalcount,
2196 % (totalcount,
2196 testcount,
2197 testcount,
2197 frame, exc,
2198 frame, exc,
2198 leasttest, leastcount))
2199 leasttest, leastcount))
2199
2200
2200 self.stream.flush()
2201 self.stream.flush()
2201
2202
2202 return self._result
2203 return self._result
2203
2204
2204 def _bisecttests(self, tests):
2205 def _bisecttests(self, tests):
2205 bisectcmd = ['hg', 'bisect']
2206 bisectcmd = ['hg', 'bisect']
2206 bisectrepo = self._runner.options.bisect_repo
2207 bisectrepo = self._runner.options.bisect_repo
2207 if bisectrepo:
2208 if bisectrepo:
2208 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2209 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2209 def pread(args):
2210 def pread(args):
2210 env = os.environ.copy()
2211 env = os.environ.copy()
2211 env['HGPLAIN'] = '1'
2212 env['HGPLAIN'] = '1'
2212 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2213 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2213 stdout=subprocess.PIPE, env=env)
2214 stdout=subprocess.PIPE, env=env)
2214 data = p.stdout.read()
2215 data = p.stdout.read()
2215 p.wait()
2216 p.wait()
2216 return data
2217 return data
2217 for test in tests:
2218 for test in tests:
2218 pread(bisectcmd + ['--reset']),
2219 pread(bisectcmd + ['--reset']),
2219 pread(bisectcmd + ['--bad', '.'])
2220 pread(bisectcmd + ['--bad', '.'])
2220 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2221 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2221 # TODO: we probably need to forward more options
2222 # TODO: we probably need to forward more options
2222 # that alter hg's behavior inside the tests.
2223 # that alter hg's behavior inside the tests.
2223 opts = ''
2224 opts = ''
2224 withhg = self._runner.options.with_hg
2225 withhg = self._runner.options.with_hg
2225 if withhg:
2226 if withhg:
2226 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2227 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2227 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2228 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2228 test)
2229 test)
2229 data = pread(bisectcmd + ['--command', rtc])
2230 data = pread(bisectcmd + ['--command', rtc])
2230 m = re.search(
2231 m = re.search(
2231 (br'\nThe first (?P<goodbad>bad|good) revision '
2232 (br'\nThe first (?P<goodbad>bad|good) revision '
2232 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2233 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2233 br'summary: +(?P<summary>[^\n]+)\n'),
2234 br'summary: +(?P<summary>[^\n]+)\n'),
2234 data, (re.MULTILINE | re.DOTALL))
2235 data, (re.MULTILINE | re.DOTALL))
2235 if m is None:
2236 if m is None:
2236 self.stream.writeln(
2237 self.stream.writeln(
2237 'Failed to identify failure point for %s' % test)
2238 'Failed to identify failure point for %s' % test)
2238 continue
2239 continue
2239 dat = m.groupdict()
2240 dat = m.groupdict()
2240 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2241 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2241 self.stream.writeln(
2242 self.stream.writeln(
2242 '%s %s by %s (%s)' % (
2243 '%s %s by %s (%s)' % (
2243 test, verb, dat['node'].decode('ascii'),
2244 test, verb, dat['node'].decode('ascii'),
2244 dat['summary'].decode('utf8', 'ignore')))
2245 dat['summary'].decode('utf8', 'ignore')))
2245
2246
2246 def printtimes(self, times):
2247 def printtimes(self, times):
2247 # iolock held by run
2248 # iolock held by run
2248 self.stream.writeln('# Producing time report')
2249 self.stream.writeln('# Producing time report')
2249 times.sort(key=lambda t: (t[3]))
2250 times.sort(key=lambda t: (t[3]))
2250 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2251 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2251 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2252 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2252 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2253 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2253 for tdata in times:
2254 for tdata in times:
2254 test = tdata[0]
2255 test = tdata[0]
2255 cuser, csys, real, start, end = tdata[1:6]
2256 cuser, csys, real, start, end = tdata[1:6]
2256 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2257 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2257
2258
2258 @staticmethod
2259 @staticmethod
2259 def _writexunit(result, outf):
2260 def _writexunit(result, outf):
2260 # See http://llg.cubic.org/docs/junit/ for a reference.
2261 # See http://llg.cubic.org/docs/junit/ for a reference.
2261 timesd = dict((t[0], t[3]) for t in result.times)
2262 timesd = dict((t[0], t[3]) for t in result.times)
2262 doc = minidom.Document()
2263 doc = minidom.Document()
2263 s = doc.createElement('testsuite')
2264 s = doc.createElement('testsuite')
2264 s.setAttribute('name', 'run-tests')
2265 s.setAttribute('name', 'run-tests')
2265 s.setAttribute('tests', str(result.testsRun))
2266 s.setAttribute('tests', str(result.testsRun))
2266 s.setAttribute('errors', "0") # TODO
2267 s.setAttribute('errors', "0") # TODO
2267 s.setAttribute('failures', str(len(result.failures)))
2268 s.setAttribute('failures', str(len(result.failures)))
2268 s.setAttribute('skipped', str(len(result.skipped) +
2269 s.setAttribute('skipped', str(len(result.skipped) +
2269 len(result.ignored)))
2270 len(result.ignored)))
2270 doc.appendChild(s)
2271 doc.appendChild(s)
2271 for tc in result.successes:
2272 for tc in result.successes:
2272 t = doc.createElement('testcase')
2273 t = doc.createElement('testcase')
2273 t.setAttribute('name', tc.name)
2274 t.setAttribute('name', tc.name)
2274 tctime = timesd.get(tc.name)
2275 tctime = timesd.get(tc.name)
2275 if tctime is not None:
2276 if tctime is not None:
2276 t.setAttribute('time', '%.3f' % tctime)
2277 t.setAttribute('time', '%.3f' % tctime)
2277 s.appendChild(t)
2278 s.appendChild(t)
2278 for tc, err in sorted(result.faildata.items()):
2279 for tc, err in sorted(result.faildata.items()):
2279 t = doc.createElement('testcase')
2280 t = doc.createElement('testcase')
2280 t.setAttribute('name', tc)
2281 t.setAttribute('name', tc)
2281 tctime = timesd.get(tc)
2282 tctime = timesd.get(tc)
2282 if tctime is not None:
2283 if tctime is not None:
2283 t.setAttribute('time', '%.3f' % tctime)
2284 t.setAttribute('time', '%.3f' % tctime)
2284 # createCDATASection expects a unicode or it will
2285 # createCDATASection expects a unicode or it will
2285 # convert using default conversion rules, which will
2286 # convert using default conversion rules, which will
2286 # fail if string isn't ASCII.
2287 # fail if string isn't ASCII.
2287 err = cdatasafe(err).decode('utf-8', 'replace')
2288 err = cdatasafe(err).decode('utf-8', 'replace')
2288 cd = doc.createCDATASection(err)
2289 cd = doc.createCDATASection(err)
2289 # Use 'failure' here instead of 'error' to match errors = 0,
2290 # Use 'failure' here instead of 'error' to match errors = 0,
2290 # failures = len(result.failures) in the testsuite element.
2291 # failures = len(result.failures) in the testsuite element.
2291 failelem = doc.createElement('failure')
2292 failelem = doc.createElement('failure')
2292 failelem.setAttribute('message', 'output changed')
2293 failelem.setAttribute('message', 'output changed')
2293 failelem.setAttribute('type', 'output-mismatch')
2294 failelem.setAttribute('type', 'output-mismatch')
2294 failelem.appendChild(cd)
2295 failelem.appendChild(cd)
2295 t.appendChild(failelem)
2296 t.appendChild(failelem)
2296 s.appendChild(t)
2297 s.appendChild(t)
2297 for tc, message in result.skipped:
2298 for tc, message in result.skipped:
2298 # According to the schema, 'skipped' has no attributes. So store
2299 # According to the schema, 'skipped' has no attributes. So store
2299 # the skip message as a text node instead.
2300 # the skip message as a text node instead.
2300 t = doc.createElement('testcase')
2301 t = doc.createElement('testcase')
2301 t.setAttribute('name', tc.name)
2302 t.setAttribute('name', tc.name)
2302 binmessage = message.encode('utf-8')
2303 binmessage = message.encode('utf-8')
2303 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2304 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2304 cd = doc.createCDATASection(message)
2305 cd = doc.createCDATASection(message)
2305 skipelem = doc.createElement('skipped')
2306 skipelem = doc.createElement('skipped')
2306 skipelem.appendChild(cd)
2307 skipelem.appendChild(cd)
2307 t.appendChild(skipelem)
2308 t.appendChild(skipelem)
2308 s.appendChild(t)
2309 s.appendChild(t)
2309 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2310 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2310
2311
2311 @staticmethod
2312 @staticmethod
2312 def _writejson(result, outf):
2313 def _writejson(result, outf):
2313 timesd = {}
2314 timesd = {}
2314 for tdata in result.times:
2315 for tdata in result.times:
2315 test = tdata[0]
2316 test = tdata[0]
2316 timesd[test] = tdata[1:]
2317 timesd[test] = tdata[1:]
2317
2318
2318 outcome = {}
2319 outcome = {}
2319 groups = [('success', ((tc, None)
2320 groups = [('success', ((tc, None)
2320 for tc in result.successes)),
2321 for tc in result.successes)),
2321 ('failure', result.failures),
2322 ('failure', result.failures),
2322 ('skip', result.skipped)]
2323 ('skip', result.skipped)]
2323 for res, testcases in groups:
2324 for res, testcases in groups:
2324 for tc, __ in testcases:
2325 for tc, __ in testcases:
2325 if tc.name in timesd:
2326 if tc.name in timesd:
2326 diff = result.faildata.get(tc.name, b'')
2327 diff = result.faildata.get(tc.name, b'')
2327 try:
2328 try:
2328 diff = diff.decode('unicode_escape')
2329 diff = diff.decode('unicode_escape')
2329 except UnicodeDecodeError as e:
2330 except UnicodeDecodeError as e:
2330 diff = '%r decoding diff, sorry' % e
2331 diff = '%r decoding diff, sorry' % e
2331 tres = {'result': res,
2332 tres = {'result': res,
2332 'time': ('%0.3f' % timesd[tc.name][2]),
2333 'time': ('%0.3f' % timesd[tc.name][2]),
2333 'cuser': ('%0.3f' % timesd[tc.name][0]),
2334 'cuser': ('%0.3f' % timesd[tc.name][0]),
2334 'csys': ('%0.3f' % timesd[tc.name][1]),
2335 'csys': ('%0.3f' % timesd[tc.name][1]),
2335 'start': ('%0.3f' % timesd[tc.name][3]),
2336 'start': ('%0.3f' % timesd[tc.name][3]),
2336 'end': ('%0.3f' % timesd[tc.name][4]),
2337 'end': ('%0.3f' % timesd[tc.name][4]),
2337 'diff': diff,
2338 'diff': diff,
2338 }
2339 }
2339 else:
2340 else:
2340 # blacklisted test
2341 # blacklisted test
2341 tres = {'result': res}
2342 tres = {'result': res}
2342
2343
2343 outcome[tc.name] = tres
2344 outcome[tc.name] = tres
2344 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2345 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2345 separators=(',', ': '))
2346 separators=(',', ': '))
2346 outf.writelines(("testreport =", jsonout))
2347 outf.writelines(("testreport =", jsonout))
2347
2348
2348 def sorttests(testdescs, previoustimes, shuffle=False):
2349 def sorttests(testdescs, previoustimes, shuffle=False):
2349 """Do an in-place sort of tests."""
2350 """Do an in-place sort of tests."""
2350 if shuffle:
2351 if shuffle:
2351 random.shuffle(testdescs)
2352 random.shuffle(testdescs)
2352 return
2353 return
2353
2354
2354 if previoustimes:
2355 if previoustimes:
2355 def sortkey(f):
2356 def sortkey(f):
2356 f = f['path']
2357 f = f['path']
2357 if f in previoustimes:
2358 if f in previoustimes:
2358 # Use most recent time as estimate
2359 # Use most recent time as estimate
2359 return -previoustimes[f][-1]
2360 return -previoustimes[f][-1]
2360 else:
2361 else:
2361 # Default to a rather arbitrary value of 1 second for new tests
2362 # Default to a rather arbitrary value of 1 second for new tests
2362 return -1.0
2363 return -1.0
2363 else:
2364 else:
2364 # keywords for slow tests
2365 # keywords for slow tests
2365 slow = {b'svn': 10,
2366 slow = {b'svn': 10,
2366 b'cvs': 10,
2367 b'cvs': 10,
2367 b'hghave': 10,
2368 b'hghave': 10,
2368 b'largefiles-update': 10,
2369 b'largefiles-update': 10,
2369 b'run-tests': 10,
2370 b'run-tests': 10,
2370 b'corruption': 10,
2371 b'corruption': 10,
2371 b'race': 10,
2372 b'race': 10,
2372 b'i18n': 10,
2373 b'i18n': 10,
2373 b'check': 100,
2374 b'check': 100,
2374 b'gendoc': 100,
2375 b'gendoc': 100,
2375 b'contrib-perf': 200,
2376 b'contrib-perf': 200,
2376 }
2377 }
2377 perf = {}
2378 perf = {}
2378
2379
2379 def sortkey(f):
2380 def sortkey(f):
2380 # run largest tests first, as they tend to take the longest
2381 # run largest tests first, as they tend to take the longest
2381 f = f['path']
2382 f = f['path']
2382 try:
2383 try:
2383 return perf[f]
2384 return perf[f]
2384 except KeyError:
2385 except KeyError:
2385 try:
2386 try:
2386 val = -os.stat(f).st_size
2387 val = -os.stat(f).st_size
2387 except OSError as e:
2388 except OSError as e:
2388 if e.errno != errno.ENOENT:
2389 if e.errno != errno.ENOENT:
2389 raise
2390 raise
2390 perf[f] = -1e9 # file does not exist, tell early
2391 perf[f] = -1e9 # file does not exist, tell early
2391 return -1e9
2392 return -1e9
2392 for kw, mul in slow.items():
2393 for kw, mul in slow.items():
2393 if kw in f:
2394 if kw in f:
2394 val *= mul
2395 val *= mul
2395 if f.endswith(b'.py'):
2396 if f.endswith(b'.py'):
2396 val /= 10.0
2397 val /= 10.0
2397 perf[f] = val / 1000.0
2398 perf[f] = val / 1000.0
2398 return perf[f]
2399 return perf[f]
2399
2400
2400 testdescs.sort(key=sortkey)
2401 testdescs.sort(key=sortkey)
2401
2402
2402 class TestRunner(object):
2403 class TestRunner(object):
2403 """Holds context for executing tests.
2404 """Holds context for executing tests.
2404
2405
2405 Tests rely on a lot of state. This object holds it for them.
2406 Tests rely on a lot of state. This object holds it for them.
2406 """
2407 """
2407
2408
2408 # Programs required to run tests.
2409 # Programs required to run tests.
2409 REQUIREDTOOLS = [
2410 REQUIREDTOOLS = [
2410 b'diff',
2411 b'diff',
2411 b'grep',
2412 b'grep',
2412 b'unzip',
2413 b'unzip',
2413 b'gunzip',
2414 b'gunzip',
2414 b'bunzip2',
2415 b'bunzip2',
2415 b'sed',
2416 b'sed',
2416 ]
2417 ]
2417
2418
2418 # Maps file extensions to test class.
2419 # Maps file extensions to test class.
2419 TESTTYPES = [
2420 TESTTYPES = [
2420 (b'.py', PythonTest),
2421 (b'.py', PythonTest),
2421 (b'.t', TTest),
2422 (b'.t', TTest),
2422 ]
2423 ]
2423
2424
2424 def __init__(self):
2425 def __init__(self):
2425 self.options = None
2426 self.options = None
2426 self._hgroot = None
2427 self._hgroot = None
2427 self._testdir = None
2428 self._testdir = None
2428 self._outputdir = None
2429 self._outputdir = None
2429 self._hgtmp = None
2430 self._hgtmp = None
2430 self._installdir = None
2431 self._installdir = None
2431 self._bindir = None
2432 self._bindir = None
2432 self._tmpbinddir = None
2433 self._tmpbinddir = None
2433 self._pythondir = None
2434 self._pythondir = None
2434 self._coveragefile = None
2435 self._coveragefile = None
2435 self._createdfiles = []
2436 self._createdfiles = []
2436 self._hgcommand = None
2437 self._hgcommand = None
2437 self._hgpath = None
2438 self._hgpath = None
2438 self._portoffset = 0
2439 self._portoffset = 0
2439 self._ports = {}
2440 self._ports = {}
2440
2441
2441 def run(self, args, parser=None):
2442 def run(self, args, parser=None):
2442 """Run the test suite."""
2443 """Run the test suite."""
2443 oldmask = os.umask(0o22)
2444 oldmask = os.umask(0o22)
2444 try:
2445 try:
2445 parser = parser or getparser()
2446 parser = parser or getparser()
2446 options = parseargs(args, parser)
2447 options = parseargs(args, parser)
2447 tests = [_bytespath(a) for a in options.tests]
2448 tests = [_bytespath(a) for a in options.tests]
2448 if options.test_list is not None:
2449 if options.test_list is not None:
2449 for listfile in options.test_list:
2450 for listfile in options.test_list:
2450 with open(listfile, 'rb') as f:
2451 with open(listfile, 'rb') as f:
2451 tests.extend(t for t in f.read().splitlines() if t)
2452 tests.extend(t for t in f.read().splitlines() if t)
2452 self.options = options
2453 self.options = options
2453
2454
2454 self._checktools()
2455 self._checktools()
2455 testdescs = self.findtests(tests)
2456 testdescs = self.findtests(tests)
2456 if options.profile_runner:
2457 if options.profile_runner:
2457 import statprof
2458 import statprof
2458 statprof.start()
2459 statprof.start()
2459 result = self._run(testdescs)
2460 result = self._run(testdescs)
2460 if options.profile_runner:
2461 if options.profile_runner:
2461 statprof.stop()
2462 statprof.stop()
2462 statprof.display()
2463 statprof.display()
2463 return result
2464 return result
2464
2465
2465 finally:
2466 finally:
2466 os.umask(oldmask)
2467 os.umask(oldmask)
2467
2468
2468 def _run(self, testdescs):
2469 def _run(self, testdescs):
2469 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2470 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2470 os, 'getcwdb', os.getcwd)()
2471 os, 'getcwdb', os.getcwd)()
2471 # assume all tests in same folder for now
2472 # assume all tests in same folder for now
2472 if testdescs:
2473 if testdescs:
2473 pathname = os.path.dirname(testdescs[0]['path'])
2474 pathname = os.path.dirname(testdescs[0]['path'])
2474 if pathname:
2475 if pathname:
2475 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2476 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2476 pathname)
2477 pathname)
2477 if self.options.outputdir:
2478 if self.options.outputdir:
2478 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2479 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2479 else:
2480 else:
2480 self._outputdir = self._testdir
2481 self._outputdir = self._testdir
2481 if testdescs and pathname:
2482 if testdescs and pathname:
2482 self._outputdir = os.path.join(self._outputdir, pathname)
2483 self._outputdir = os.path.join(self._outputdir, pathname)
2483 previoustimes = {}
2484 previoustimes = {}
2484 if self.options.order_by_runtime:
2485 if self.options.order_by_runtime:
2485 previoustimes = dict(loadtimes(self._outputdir))
2486 previoustimes = dict(loadtimes(self._outputdir))
2486 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2487 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2487
2488
2488 if 'PYTHONHASHSEED' not in os.environ:
2489 if 'PYTHONHASHSEED' not in os.environ:
2489 # use a random python hash seed all the time
2490 # use a random python hash seed all the time
2490 # we do the randomness ourself to know what seed is used
2491 # we do the randomness ourself to know what seed is used
2491 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2492 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2492
2493
2493 if self.options.tmpdir:
2494 if self.options.tmpdir:
2494 self.options.keep_tmpdir = True
2495 self.options.keep_tmpdir = True
2495 tmpdir = _bytespath(self.options.tmpdir)
2496 tmpdir = _bytespath(self.options.tmpdir)
2496 if os.path.exists(tmpdir):
2497 if os.path.exists(tmpdir):
2497 # Meaning of tmpdir has changed since 1.3: we used to create
2498 # Meaning of tmpdir has changed since 1.3: we used to create
2498 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2499 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2499 # tmpdir already exists.
2500 # tmpdir already exists.
2500 print("error: temp dir %r already exists" % tmpdir)
2501 print("error: temp dir %r already exists" % tmpdir)
2501 return 1
2502 return 1
2502
2503
2503 os.makedirs(tmpdir)
2504 os.makedirs(tmpdir)
2504 else:
2505 else:
2505 d = None
2506 d = None
2506 if os.name == 'nt':
2507 if os.name == 'nt':
2507 # without this, we get the default temp dir location, but
2508 # without this, we get the default temp dir location, but
2508 # in all lowercase, which causes troubles with paths (issue3490)
2509 # in all lowercase, which causes troubles with paths (issue3490)
2509 d = osenvironb.get(b'TMP', None)
2510 d = osenvironb.get(b'TMP', None)
2510 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2511 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2511
2512
2512 self._hgtmp = osenvironb[b'HGTMP'] = (
2513 self._hgtmp = osenvironb[b'HGTMP'] = (
2513 os.path.realpath(tmpdir))
2514 os.path.realpath(tmpdir))
2514
2515
2515 if self.options.with_hg:
2516 if self.options.with_hg:
2516 self._installdir = None
2517 self._installdir = None
2517 whg = self.options.with_hg
2518 whg = self.options.with_hg
2518 self._bindir = os.path.dirname(os.path.realpath(whg))
2519 self._bindir = os.path.dirname(os.path.realpath(whg))
2519 assert isinstance(self._bindir, bytes)
2520 assert isinstance(self._bindir, bytes)
2520 self._hgcommand = os.path.basename(whg)
2521 self._hgcommand = os.path.basename(whg)
2521 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2522 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2522 os.makedirs(self._tmpbindir)
2523 os.makedirs(self._tmpbindir)
2523
2524
2524 normbin = os.path.normpath(os.path.abspath(whg))
2525 normbin = os.path.normpath(os.path.abspath(whg))
2525 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2526 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2526
2527
2527 # Other Python scripts in the test harness need to
2528 # Other Python scripts in the test harness need to
2528 # `import mercurial`. If `hg` is a Python script, we assume
2529 # `import mercurial`. If `hg` is a Python script, we assume
2529 # the Mercurial modules are relative to its path and tell the tests
2530 # the Mercurial modules are relative to its path and tell the tests
2530 # to load Python modules from its directory.
2531 # to load Python modules from its directory.
2531 with open(whg, 'rb') as fh:
2532 with open(whg, 'rb') as fh:
2532 initial = fh.read(1024)
2533 initial = fh.read(1024)
2533
2534
2534 if re.match(b'#!.*python', initial):
2535 if re.match(b'#!.*python', initial):
2535 self._pythondir = self._bindir
2536 self._pythondir = self._bindir
2536 # If it looks like our in-repo Rust binary, use the source root.
2537 # If it looks like our in-repo Rust binary, use the source root.
2537 # This is a bit hacky. But rhg is still not supported outside the
2538 # This is a bit hacky. But rhg is still not supported outside the
2538 # source directory. So until it is, do the simple thing.
2539 # source directory. So until it is, do the simple thing.
2539 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2540 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2540 self._pythondir = os.path.dirname(self._testdir)
2541 self._pythondir = os.path.dirname(self._testdir)
2541 # Fall back to the legacy behavior.
2542 # Fall back to the legacy behavior.
2542 else:
2543 else:
2543 self._pythondir = self._bindir
2544 self._pythondir = self._bindir
2544
2545
2545 else:
2546 else:
2546 self._installdir = os.path.join(self._hgtmp, b"install")
2547 self._installdir = os.path.join(self._hgtmp, b"install")
2547 self._bindir = os.path.join(self._installdir, b"bin")
2548 self._bindir = os.path.join(self._installdir, b"bin")
2548 self._hgcommand = b'hg'
2549 self._hgcommand = b'hg'
2549 self._tmpbindir = self._bindir
2550 self._tmpbindir = self._bindir
2550 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2551 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2551
2552
2552 # set CHGHG, then replace "hg" command by "chg"
2553 # set CHGHG, then replace "hg" command by "chg"
2553 chgbindir = self._bindir
2554 chgbindir = self._bindir
2554 if self.options.chg or self.options.with_chg:
2555 if self.options.chg or self.options.with_chg:
2555 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2556 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2556 else:
2557 else:
2557 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2558 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2558 if self.options.chg:
2559 if self.options.chg:
2559 self._hgcommand = b'chg'
2560 self._hgcommand = b'chg'
2560 elif self.options.with_chg:
2561 elif self.options.with_chg:
2561 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2562 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2562 self._hgcommand = os.path.basename(self.options.with_chg)
2563 self._hgcommand = os.path.basename(self.options.with_chg)
2563
2564
2564 osenvironb[b"BINDIR"] = self._bindir
2565 osenvironb[b"BINDIR"] = self._bindir
2565 osenvironb[b"PYTHON"] = PYTHON
2566 osenvironb[b"PYTHON"] = PYTHON
2566
2567
2567 if self.options.with_python3:
2568 if self.options.with_python3:
2568 osenvironb[b'PYTHON3'] = self.options.with_python3
2569 osenvironb[b'PYTHON3'] = self.options.with_python3
2569
2570
2570 fileb = _bytespath(__file__)
2571 fileb = _bytespath(__file__)
2571 runtestdir = os.path.abspath(os.path.dirname(fileb))
2572 runtestdir = os.path.abspath(os.path.dirname(fileb))
2572 osenvironb[b'RUNTESTDIR'] = runtestdir
2573 osenvironb[b'RUNTESTDIR'] = runtestdir
2573 if PYTHON3:
2574 if PYTHON3:
2574 sepb = _bytespath(os.pathsep)
2575 sepb = _bytespath(os.pathsep)
2575 else:
2576 else:
2576 sepb = os.pathsep
2577 sepb = os.pathsep
2577 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2578 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2578 if os.path.islink(__file__):
2579 if os.path.islink(__file__):
2579 # test helper will likely be at the end of the symlink
2580 # test helper will likely be at the end of the symlink
2580 realfile = os.path.realpath(fileb)
2581 realfile = os.path.realpath(fileb)
2581 realdir = os.path.abspath(os.path.dirname(realfile))
2582 realdir = os.path.abspath(os.path.dirname(realfile))
2582 path.insert(2, realdir)
2583 path.insert(2, realdir)
2583 if chgbindir != self._bindir:
2584 if chgbindir != self._bindir:
2584 path.insert(1, chgbindir)
2585 path.insert(1, chgbindir)
2585 if self._testdir != runtestdir:
2586 if self._testdir != runtestdir:
2586 path = [self._testdir] + path
2587 path = [self._testdir] + path
2587 if self._tmpbindir != self._bindir:
2588 if self._tmpbindir != self._bindir:
2588 path = [self._tmpbindir] + path
2589 path = [self._tmpbindir] + path
2589 osenvironb[b"PATH"] = sepb.join(path)
2590 osenvironb[b"PATH"] = sepb.join(path)
2590
2591
2591 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2592 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2592 # can run .../tests/run-tests.py test-foo where test-foo
2593 # can run .../tests/run-tests.py test-foo where test-foo
2593 # adds an extension to HGRC. Also include run-test.py directory to
2594 # adds an extension to HGRC. Also include run-test.py directory to
2594 # import modules like heredoctest.
2595 # import modules like heredoctest.
2595 pypath = [self._pythondir, self._testdir, runtestdir]
2596 pypath = [self._pythondir, self._testdir, runtestdir]
2596 # We have to augment PYTHONPATH, rather than simply replacing
2597 # We have to augment PYTHONPATH, rather than simply replacing
2597 # it, in case external libraries are only available via current
2598 # it, in case external libraries are only available via current
2598 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2599 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2599 # are in /opt/subversion.)
2600 # are in /opt/subversion.)
2600 oldpypath = osenvironb.get(IMPL_PATH)
2601 oldpypath = osenvironb.get(IMPL_PATH)
2601 if oldpypath:
2602 if oldpypath:
2602 pypath.append(oldpypath)
2603 pypath.append(oldpypath)
2603 osenvironb[IMPL_PATH] = sepb.join(pypath)
2604 osenvironb[IMPL_PATH] = sepb.join(pypath)
2604
2605
2605 if self.options.pure:
2606 if self.options.pure:
2606 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2607 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2607 os.environ["HGMODULEPOLICY"] = "py"
2608 os.environ["HGMODULEPOLICY"] = "py"
2608
2609
2609 if self.options.allow_slow_tests:
2610 if self.options.allow_slow_tests:
2610 os.environ["HGTEST_SLOW"] = "slow"
2611 os.environ["HGTEST_SLOW"] = "slow"
2611 elif 'HGTEST_SLOW' in os.environ:
2612 elif 'HGTEST_SLOW' in os.environ:
2612 del os.environ['HGTEST_SLOW']
2613 del os.environ['HGTEST_SLOW']
2613
2614
2614 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2615 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2615
2616
2616 if self.options.exceptions:
2617 if self.options.exceptions:
2617 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2618 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2618 try:
2619 try:
2619 os.makedirs(exceptionsdir)
2620 os.makedirs(exceptionsdir)
2620 except OSError as e:
2621 except OSError as e:
2621 if e.errno != errno.EEXIST:
2622 if e.errno != errno.EEXIST:
2622 raise
2623 raise
2623
2624
2624 # Remove all existing exception reports.
2625 # Remove all existing exception reports.
2625 for f in os.listdir(exceptionsdir):
2626 for f in os.listdir(exceptionsdir):
2626 os.unlink(os.path.join(exceptionsdir, f))
2627 os.unlink(os.path.join(exceptionsdir, f))
2627
2628
2628 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2629 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2629 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2630 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2630 self.options.extra_config_opt.append(
2631 self.options.extra_config_opt.append(
2631 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2632 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2632
2633
2633 vlog("# Using TESTDIR", self._testdir)
2634 vlog("# Using TESTDIR", self._testdir)
2634 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2635 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2635 vlog("# Using HGTMP", self._hgtmp)
2636 vlog("# Using HGTMP", self._hgtmp)
2636 vlog("# Using PATH", os.environ["PATH"])
2637 vlog("# Using PATH", os.environ["PATH"])
2637 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2638 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2638 vlog("# Writing to directory", self._outputdir)
2639 vlog("# Writing to directory", self._outputdir)
2639
2640
2640 try:
2641 try:
2641 return self._runtests(testdescs) or 0
2642 return self._runtests(testdescs) or 0
2642 finally:
2643 finally:
2643 time.sleep(.1)
2644 time.sleep(.1)
2644 self._cleanup()
2645 self._cleanup()
2645
2646
2646 def findtests(self, args):
2647 def findtests(self, args):
2647 """Finds possible test files from arguments.
2648 """Finds possible test files from arguments.
2648
2649
2649 If you wish to inject custom tests into the test harness, this would
2650 If you wish to inject custom tests into the test harness, this would
2650 be a good function to monkeypatch or override in a derived class.
2651 be a good function to monkeypatch or override in a derived class.
2651 """
2652 """
2652 if not args:
2653 if not args:
2653 if self.options.changed:
2654 if self.options.changed:
2654 proc = Popen4('hg st --rev "%s" -man0 .' %
2655 proc = Popen4('hg st --rev "%s" -man0 .' %
2655 self.options.changed, None, 0)
2656 self.options.changed, None, 0)
2656 stdout, stderr = proc.communicate()
2657 stdout, stderr = proc.communicate()
2657 args = stdout.strip(b'\0').split(b'\0')
2658 args = stdout.strip(b'\0').split(b'\0')
2658 else:
2659 else:
2659 args = os.listdir(b'.')
2660 args = os.listdir(b'.')
2660
2661
2661 expanded_args = []
2662 expanded_args = []
2662 for arg in args:
2663 for arg in args:
2663 if os.path.isdir(arg):
2664 if os.path.isdir(arg):
2664 if not arg.endswith(b'/'):
2665 if not arg.endswith(b'/'):
2665 arg += b'/'
2666 arg += b'/'
2666 expanded_args.extend([arg + a for a in os.listdir(arg)])
2667 expanded_args.extend([arg + a for a in os.listdir(arg)])
2667 else:
2668 else:
2668 expanded_args.append(arg)
2669 expanded_args.append(arg)
2669 args = expanded_args
2670 args = expanded_args
2670
2671
2671 testcasepattern = re.compile(br'([\w-]+\.t|py)(#([a-zA-Z0-9_\-\.#]+))')
2672 testcasepattern = re.compile(br'([\w-]+\.t|py)(#([a-zA-Z0-9_\-\.#]+))')
2672 tests = []
2673 tests = []
2673 for t in args:
2674 for t in args:
2674 case = []
2675 case = []
2675
2676
2676 if not (os.path.basename(t).startswith(b'test-')
2677 if not (os.path.basename(t).startswith(b'test-')
2677 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2678 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2678
2679
2679 m = testcasepattern.match(t)
2680 m = testcasepattern.match(t)
2680 if m is not None:
2681 if m is not None:
2681 t, _, casestr = m.groups()
2682 t, _, casestr = m.groups()
2682 if casestr:
2683 if casestr:
2683 case = casestr.split(b'#')
2684 case = casestr.split(b'#')
2684 else:
2685 else:
2685 continue
2686 continue
2686
2687
2687 if t.endswith(b'.t'):
2688 if t.endswith(b'.t'):
2688 # .t file may contain multiple test cases
2689 # .t file may contain multiple test cases
2689 casedimensions = parsettestcases(t)
2690 casedimensions = parsettestcases(t)
2690 if casedimensions:
2691 if casedimensions:
2691 cases = []
2692 cases = []
2692 def addcases(case, casedimensions):
2693 def addcases(case, casedimensions):
2693 if not casedimensions:
2694 if not casedimensions:
2694 cases.append(case)
2695 cases.append(case)
2695 else:
2696 else:
2696 for c in casedimensions[0]:
2697 for c in casedimensions[0]:
2697 addcases(case + [c], casedimensions[1:])
2698 addcases(case + [c], casedimensions[1:])
2698 addcases([], casedimensions)
2699 addcases([], casedimensions)
2699 if case and case in cases:
2700 if case and case in cases:
2700 cases = [case]
2701 cases = [case]
2701 elif case:
2702 elif case:
2702 # Ignore invalid cases
2703 # Ignore invalid cases
2703 cases = []
2704 cases = []
2704 else:
2705 else:
2705 pass
2706 pass
2706 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2707 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2707 else:
2708 else:
2708 tests.append({'path': t})
2709 tests.append({'path': t})
2709 else:
2710 else:
2710 tests.append({'path': t})
2711 tests.append({'path': t})
2711 return tests
2712 return tests
2712
2713
2713 def _runtests(self, testdescs):
2714 def _runtests(self, testdescs):
2714 def _reloadtest(test, i):
2715 def _reloadtest(test, i):
2715 # convert a test back to its description dict
2716 # convert a test back to its description dict
2716 desc = {'path': test.path}
2717 desc = {'path': test.path}
2717 case = getattr(test, '_case', [])
2718 case = getattr(test, '_case', [])
2718 if case:
2719 if case:
2719 desc['case'] = case
2720 desc['case'] = case
2720 return self._gettest(desc, i)
2721 return self._gettest(desc, i)
2721
2722
2722 try:
2723 try:
2723 if self.options.restart:
2724 if self.options.restart:
2724 orig = list(testdescs)
2725 orig = list(testdescs)
2725 while testdescs:
2726 while testdescs:
2726 desc = testdescs[0]
2727 desc = testdescs[0]
2727 # desc['path'] is a relative path
2728 # desc['path'] is a relative path
2728 if 'case' in desc:
2729 if 'case' in desc:
2729 casestr = b'#'.join(desc['case'])
2730 casestr = b'#'.join(desc['case'])
2730 errpath = b'%s#%s.err' % (desc['path'], casestr)
2731 errpath = b'%s#%s.err' % (desc['path'], casestr)
2731 else:
2732 else:
2732 errpath = b'%s.err' % desc['path']
2733 errpath = b'%s.err' % desc['path']
2733 errpath = os.path.join(self._outputdir, errpath)
2734 errpath = os.path.join(self._outputdir, errpath)
2734 if os.path.exists(errpath):
2735 if os.path.exists(errpath):
2735 break
2736 break
2736 testdescs.pop(0)
2737 testdescs.pop(0)
2737 if not testdescs:
2738 if not testdescs:
2738 print("running all tests")
2739 print("running all tests")
2739 testdescs = orig
2740 testdescs = orig
2740
2741
2741 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2742 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2742
2743
2743 failed = False
2744 failed = False
2744 kws = self.options.keywords
2745 kws = self.options.keywords
2745 if kws is not None and PYTHON3:
2746 if kws is not None and PYTHON3:
2746 kws = kws.encode('utf-8')
2747 kws = kws.encode('utf-8')
2747
2748
2748 suite = TestSuite(self._testdir,
2749 suite = TestSuite(self._testdir,
2749 jobs=self.options.jobs,
2750 jobs=self.options.jobs,
2750 whitelist=self.options.whitelisted,
2751 whitelist=self.options.whitelisted,
2751 blacklist=self.options.blacklist,
2752 blacklist=self.options.blacklist,
2752 retest=self.options.retest,
2753 retest=self.options.retest,
2753 keywords=kws,
2754 keywords=kws,
2754 loop=self.options.loop,
2755 loop=self.options.loop,
2755 runs_per_test=self.options.runs_per_test,
2756 runs_per_test=self.options.runs_per_test,
2756 showchannels=self.options.showchannels,
2757 showchannels=self.options.showchannels,
2757 tests=tests, loadtest=_reloadtest)
2758 tests=tests, loadtest=_reloadtest)
2758 verbosity = 1
2759 verbosity = 1
2759 if self.options.list_tests:
2760 if self.options.list_tests:
2760 verbosity = 0
2761 verbosity = 0
2761 elif self.options.verbose:
2762 elif self.options.verbose:
2762 verbosity = 2
2763 verbosity = 2
2763 runner = TextTestRunner(self, verbosity=verbosity)
2764 runner = TextTestRunner(self, verbosity=verbosity)
2764
2765
2765 if self.options.list_tests:
2766 if self.options.list_tests:
2766 result = runner.listtests(suite)
2767 result = runner.listtests(suite)
2767 else:
2768 else:
2768 if self._installdir:
2769 if self._installdir:
2769 self._installhg()
2770 self._installhg()
2770 self._checkhglib("Testing")
2771 self._checkhglib("Testing")
2771 else:
2772 else:
2772 self._usecorrectpython()
2773 self._usecorrectpython()
2773 if self.options.chg:
2774 if self.options.chg:
2774 assert self._installdir
2775 assert self._installdir
2775 self._installchg()
2776 self._installchg()
2776
2777
2777 result = runner.run(suite)
2778 result = runner.run(suite)
2778
2779
2779 if result.failures:
2780 if result.failures:
2780 failed = True
2781 failed = True
2781
2782
2782 result.onEnd()
2783 result.onEnd()
2783
2784
2784 if self.options.anycoverage:
2785 if self.options.anycoverage:
2785 self._outputcoverage()
2786 self._outputcoverage()
2786 except KeyboardInterrupt:
2787 except KeyboardInterrupt:
2787 failed = True
2788 failed = True
2788 print("\ninterrupted!")
2789 print("\ninterrupted!")
2789
2790
2790 if failed:
2791 if failed:
2791 return 1
2792 return 1
2792
2793
2793 def _getport(self, count):
2794 def _getport(self, count):
2794 port = self._ports.get(count) # do we have a cached entry?
2795 port = self._ports.get(count) # do we have a cached entry?
2795 if port is None:
2796 if port is None:
2796 portneeded = 3
2797 portneeded = 3
2797 # above 100 tries we just give up and let test reports failure
2798 # above 100 tries we just give up and let test reports failure
2798 for tries in xrange(100):
2799 for tries in xrange(100):
2799 allfree = True
2800 allfree = True
2800 port = self.options.port + self._portoffset
2801 port = self.options.port + self._portoffset
2801 for idx in xrange(portneeded):
2802 for idx in xrange(portneeded):
2802 if not checkportisavailable(port + idx):
2803 if not checkportisavailable(port + idx):
2803 allfree = False
2804 allfree = False
2804 break
2805 break
2805 self._portoffset += portneeded
2806 self._portoffset += portneeded
2806 if allfree:
2807 if allfree:
2807 break
2808 break
2808 self._ports[count] = port
2809 self._ports[count] = port
2809 return port
2810 return port
2810
2811
2811 def _gettest(self, testdesc, count):
2812 def _gettest(self, testdesc, count):
2812 """Obtain a Test by looking at its filename.
2813 """Obtain a Test by looking at its filename.
2813
2814
2814 Returns a Test instance. The Test may not be runnable if it doesn't
2815 Returns a Test instance. The Test may not be runnable if it doesn't
2815 map to a known type.
2816 map to a known type.
2816 """
2817 """
2817 path = testdesc['path']
2818 path = testdesc['path']
2818 lctest = path.lower()
2819 lctest = path.lower()
2819 testcls = Test
2820 testcls = Test
2820
2821
2821 for ext, cls in self.TESTTYPES:
2822 for ext, cls in self.TESTTYPES:
2822 if lctest.endswith(ext):
2823 if lctest.endswith(ext):
2823 testcls = cls
2824 testcls = cls
2824 break
2825 break
2825
2826
2826 refpath = os.path.join(self._testdir, path)
2827 refpath = os.path.join(self._testdir, path)
2827 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2828 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2828
2829
2829 # extra keyword parameters. 'case' is used by .t tests
2830 # extra keyword parameters. 'case' is used by .t tests
2830 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2831 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2831
2832
2832 t = testcls(refpath, self._outputdir, tmpdir,
2833 t = testcls(refpath, self._outputdir, tmpdir,
2833 keeptmpdir=self.options.keep_tmpdir,
2834 keeptmpdir=self.options.keep_tmpdir,
2834 debug=self.options.debug,
2835 debug=self.options.debug,
2835 first=self.options.first,
2836 first=self.options.first,
2836 timeout=self.options.timeout,
2837 timeout=self.options.timeout,
2837 startport=self._getport(count),
2838 startport=self._getport(count),
2838 extraconfigopts=self.options.extra_config_opt,
2839 extraconfigopts=self.options.extra_config_opt,
2839 py3kwarnings=self.options.py3k_warnings,
2840 py3kwarnings=self.options.py3k_warnings,
2840 shell=self.options.shell,
2841 shell=self.options.shell,
2841 hgcommand=self._hgcommand,
2842 hgcommand=self._hgcommand,
2842 usechg=bool(self.options.with_chg or self.options.chg),
2843 usechg=bool(self.options.with_chg or self.options.chg),
2843 useipv6=useipv6, **kwds)
2844 useipv6=useipv6, **kwds)
2844 t.should_reload = True
2845 t.should_reload = True
2845 return t
2846 return t
2846
2847
2847 def _cleanup(self):
2848 def _cleanup(self):
2848 """Clean up state from this test invocation."""
2849 """Clean up state from this test invocation."""
2849 if self.options.keep_tmpdir:
2850 if self.options.keep_tmpdir:
2850 return
2851 return
2851
2852
2852 vlog("# Cleaning up HGTMP", self._hgtmp)
2853 vlog("# Cleaning up HGTMP", self._hgtmp)
2853 shutil.rmtree(self._hgtmp, True)
2854 shutil.rmtree(self._hgtmp, True)
2854 for f in self._createdfiles:
2855 for f in self._createdfiles:
2855 try:
2856 try:
2856 os.remove(f)
2857 os.remove(f)
2857 except OSError:
2858 except OSError:
2858 pass
2859 pass
2859
2860
2860 def _usecorrectpython(self):
2861 def _usecorrectpython(self):
2861 """Configure the environment to use the appropriate Python in tests."""
2862 """Configure the environment to use the appropriate Python in tests."""
2862 # Tests must use the same interpreter as us or bad things will happen.
2863 # Tests must use the same interpreter as us or bad things will happen.
2863 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2864 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2864 if getattr(os, 'symlink', None):
2865 if getattr(os, 'symlink', None):
2865 vlog("# Making python executable in test path a symlink to '%s'" %
2866 vlog("# Making python executable in test path a symlink to '%s'" %
2866 sys.executable)
2867 sys.executable)
2867 mypython = os.path.join(self._tmpbindir, pyexename)
2868 mypython = os.path.join(self._tmpbindir, pyexename)
2868 try:
2869 try:
2869 if os.readlink(mypython) == sys.executable:
2870 if os.readlink(mypython) == sys.executable:
2870 return
2871 return
2871 os.unlink(mypython)
2872 os.unlink(mypython)
2872 except OSError as err:
2873 except OSError as err:
2873 if err.errno != errno.ENOENT:
2874 if err.errno != errno.ENOENT:
2874 raise
2875 raise
2875 if self._findprogram(pyexename) != sys.executable:
2876 if self._findprogram(pyexename) != sys.executable:
2876 try:
2877 try:
2877 os.symlink(sys.executable, mypython)
2878 os.symlink(sys.executable, mypython)
2878 self._createdfiles.append(mypython)
2879 self._createdfiles.append(mypython)
2879 except OSError as err:
2880 except OSError as err:
2880 # child processes may race, which is harmless
2881 # child processes may race, which is harmless
2881 if err.errno != errno.EEXIST:
2882 if err.errno != errno.EEXIST:
2882 raise
2883 raise
2883 else:
2884 else:
2884 exedir, exename = os.path.split(sys.executable)
2885 exedir, exename = os.path.split(sys.executable)
2885 vlog("# Modifying search path to find %s as %s in '%s'" %
2886 vlog("# Modifying search path to find %s as %s in '%s'" %
2886 (exename, pyexename, exedir))
2887 (exename, pyexename, exedir))
2887 path = os.environ['PATH'].split(os.pathsep)
2888 path = os.environ['PATH'].split(os.pathsep)
2888 while exedir in path:
2889 while exedir in path:
2889 path.remove(exedir)
2890 path.remove(exedir)
2890 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2891 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2891 if not self._findprogram(pyexename):
2892 if not self._findprogram(pyexename):
2892 print("WARNING: Cannot find %s in search path" % pyexename)
2893 print("WARNING: Cannot find %s in search path" % pyexename)
2893
2894
2894 def _installhg(self):
2895 def _installhg(self):
2895 """Install hg into the test environment.
2896 """Install hg into the test environment.
2896
2897
2897 This will also configure hg with the appropriate testing settings.
2898 This will also configure hg with the appropriate testing settings.
2898 """
2899 """
2899 vlog("# Performing temporary installation of HG")
2900 vlog("# Performing temporary installation of HG")
2900 installerrs = os.path.join(self._hgtmp, b"install.err")
2901 installerrs = os.path.join(self._hgtmp, b"install.err")
2901 compiler = ''
2902 compiler = ''
2902 if self.options.compiler:
2903 if self.options.compiler:
2903 compiler = '--compiler ' + self.options.compiler
2904 compiler = '--compiler ' + self.options.compiler
2904 if self.options.pure:
2905 if self.options.pure:
2905 pure = b"--pure"
2906 pure = b"--pure"
2906 else:
2907 else:
2907 pure = b""
2908 pure = b""
2908
2909
2909 # Run installer in hg root
2910 # Run installer in hg root
2910 script = os.path.realpath(sys.argv[0])
2911 script = os.path.realpath(sys.argv[0])
2911 exe = sys.executable
2912 exe = sys.executable
2912 if PYTHON3:
2913 if PYTHON3:
2913 compiler = _bytespath(compiler)
2914 compiler = _bytespath(compiler)
2914 script = _bytespath(script)
2915 script = _bytespath(script)
2915 exe = _bytespath(exe)
2916 exe = _bytespath(exe)
2916 hgroot = os.path.dirname(os.path.dirname(script))
2917 hgroot = os.path.dirname(os.path.dirname(script))
2917 self._hgroot = hgroot
2918 self._hgroot = hgroot
2918 os.chdir(hgroot)
2919 os.chdir(hgroot)
2919 nohome = b'--home=""'
2920 nohome = b'--home=""'
2920 if os.name == 'nt':
2921 if os.name == 'nt':
2921 # The --home="" trick works only on OS where os.sep == '/'
2922 # The --home="" trick works only on OS where os.sep == '/'
2922 # because of a distutils convert_path() fast-path. Avoid it at
2923 # because of a distutils convert_path() fast-path. Avoid it at
2923 # least on Windows for now, deal with .pydistutils.cfg bugs
2924 # least on Windows for now, deal with .pydistutils.cfg bugs
2924 # when they happen.
2925 # when they happen.
2925 nohome = b''
2926 nohome = b''
2926 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2927 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2927 b' build %(compiler)s --build-base="%(base)s"'
2928 b' build %(compiler)s --build-base="%(base)s"'
2928 b' install --force --prefix="%(prefix)s"'
2929 b' install --force --prefix="%(prefix)s"'
2929 b' --install-lib="%(libdir)s"'
2930 b' --install-lib="%(libdir)s"'
2930 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2931 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2931 % {b'exe': exe, b'pure': pure,
2932 % {b'exe': exe, b'pure': pure,
2932 b'compiler': compiler,
2933 b'compiler': compiler,
2933 b'base': os.path.join(self._hgtmp, b"build"),
2934 b'base': os.path.join(self._hgtmp, b"build"),
2934 b'prefix': self._installdir, b'libdir': self._pythondir,
2935 b'prefix': self._installdir, b'libdir': self._pythondir,
2935 b'bindir': self._bindir,
2936 b'bindir': self._bindir,
2936 b'nohome': nohome, b'logfile': installerrs})
2937 b'nohome': nohome, b'logfile': installerrs})
2937
2938
2938 # setuptools requires install directories to exist.
2939 # setuptools requires install directories to exist.
2939 def makedirs(p):
2940 def makedirs(p):
2940 try:
2941 try:
2941 os.makedirs(p)
2942 os.makedirs(p)
2942 except OSError as e:
2943 except OSError as e:
2943 if e.errno != errno.EEXIST:
2944 if e.errno != errno.EEXIST:
2944 raise
2945 raise
2945 makedirs(self._pythondir)
2946 makedirs(self._pythondir)
2946 makedirs(self._bindir)
2947 makedirs(self._bindir)
2947
2948
2948 vlog("# Running", cmd)
2949 vlog("# Running", cmd)
2949 if os.system(cmd) == 0:
2950 if os.system(cmd) == 0:
2950 if not self.options.verbose:
2951 if not self.options.verbose:
2951 try:
2952 try:
2952 os.remove(installerrs)
2953 os.remove(installerrs)
2953 except OSError as e:
2954 except OSError as e:
2954 if e.errno != errno.ENOENT:
2955 if e.errno != errno.ENOENT:
2955 raise
2956 raise
2956 else:
2957 else:
2957 with open(installerrs, 'rb') as f:
2958 with open(installerrs, 'rb') as f:
2958 for line in f:
2959 for line in f:
2959 if PYTHON3:
2960 if PYTHON3:
2960 sys.stdout.buffer.write(line)
2961 sys.stdout.buffer.write(line)
2961 else:
2962 else:
2962 sys.stdout.write(line)
2963 sys.stdout.write(line)
2963 sys.exit(1)
2964 sys.exit(1)
2964 os.chdir(self._testdir)
2965 os.chdir(self._testdir)
2965
2966
2966 self._usecorrectpython()
2967 self._usecorrectpython()
2967
2968
2968 if self.options.py3k_warnings and not self.options.anycoverage:
2969 if self.options.py3k_warnings and not self.options.anycoverage:
2969 vlog("# Updating hg command to enable Py3k Warnings switch")
2970 vlog("# Updating hg command to enable Py3k Warnings switch")
2970 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2971 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2971 lines = [line.rstrip() for line in f]
2972 lines = [line.rstrip() for line in f]
2972 lines[0] += ' -3'
2973 lines[0] += ' -3'
2973 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2974 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2974 for line in lines:
2975 for line in lines:
2975 f.write(line + '\n')
2976 f.write(line + '\n')
2976
2977
2977 hgbat = os.path.join(self._bindir, b'hg.bat')
2978 hgbat = os.path.join(self._bindir, b'hg.bat')
2978 if os.path.isfile(hgbat):
2979 if os.path.isfile(hgbat):
2979 # hg.bat expects to be put in bin/scripts while run-tests.py
2980 # hg.bat expects to be put in bin/scripts while run-tests.py
2980 # installation layout put it in bin/ directly. Fix it
2981 # installation layout put it in bin/ directly. Fix it
2981 with open(hgbat, 'rb') as f:
2982 with open(hgbat, 'rb') as f:
2982 data = f.read()
2983 data = f.read()
2983 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2984 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2984 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2985 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2985 b'"%~dp0python" "%~dp0hg" %*')
2986 b'"%~dp0python" "%~dp0hg" %*')
2986 with open(hgbat, 'wb') as f:
2987 with open(hgbat, 'wb') as f:
2987 f.write(data)
2988 f.write(data)
2988 else:
2989 else:
2989 print('WARNING: cannot fix hg.bat reference to python.exe')
2990 print('WARNING: cannot fix hg.bat reference to python.exe')
2990
2991
2991 if self.options.anycoverage:
2992 if self.options.anycoverage:
2992 custom = os.path.join(self._testdir, 'sitecustomize.py')
2993 custom = os.path.join(self._testdir, 'sitecustomize.py')
2993 target = os.path.join(self._pythondir, 'sitecustomize.py')
2994 target = os.path.join(self._pythondir, 'sitecustomize.py')
2994 vlog('# Installing coverage trigger to %s' % target)
2995 vlog('# Installing coverage trigger to %s' % target)
2995 shutil.copyfile(custom, target)
2996 shutil.copyfile(custom, target)
2996 rc = os.path.join(self._testdir, '.coveragerc')
2997 rc = os.path.join(self._testdir, '.coveragerc')
2997 vlog('# Installing coverage rc to %s' % rc)
2998 vlog('# Installing coverage rc to %s' % rc)
2998 os.environ['COVERAGE_PROCESS_START'] = rc
2999 os.environ['COVERAGE_PROCESS_START'] = rc
2999 covdir = os.path.join(self._installdir, '..', 'coverage')
3000 covdir = os.path.join(self._installdir, '..', 'coverage')
3000 try:
3001 try:
3001 os.mkdir(covdir)
3002 os.mkdir(covdir)
3002 except OSError as e:
3003 except OSError as e:
3003 if e.errno != errno.EEXIST:
3004 if e.errno != errno.EEXIST:
3004 raise
3005 raise
3005
3006
3006 os.environ['COVERAGE_DIR'] = covdir
3007 os.environ['COVERAGE_DIR'] = covdir
3007
3008
3008 def _checkhglib(self, verb):
3009 def _checkhglib(self, verb):
3009 """Ensure that the 'mercurial' package imported by python is
3010 """Ensure that the 'mercurial' package imported by python is
3010 the one we expect it to be. If not, print a warning to stderr."""
3011 the one we expect it to be. If not, print a warning to stderr."""
3011 if ((self._bindir == self._pythondir) and
3012 if ((self._bindir == self._pythondir) and
3012 (self._bindir != self._tmpbindir)):
3013 (self._bindir != self._tmpbindir)):
3013 # The pythondir has been inferred from --with-hg flag.
3014 # The pythondir has been inferred from --with-hg flag.
3014 # We cannot expect anything sensible here.
3015 # We cannot expect anything sensible here.
3015 return
3016 return
3016 expecthg = os.path.join(self._pythondir, b'mercurial')
3017 expecthg = os.path.join(self._pythondir, b'mercurial')
3017 actualhg = self._gethgpath()
3018 actualhg = self._gethgpath()
3018 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3019 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3019 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
3020 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
3020 ' (expected %s)\n'
3021 ' (expected %s)\n'
3021 % (verb, actualhg, expecthg))
3022 % (verb, actualhg, expecthg))
3022 def _gethgpath(self):
3023 def _gethgpath(self):
3023 """Return the path to the mercurial package that is actually found by
3024 """Return the path to the mercurial package that is actually found by
3024 the current Python interpreter."""
3025 the current Python interpreter."""
3025 if self._hgpath is not None:
3026 if self._hgpath is not None:
3026 return self._hgpath
3027 return self._hgpath
3027
3028
3028 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
3029 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
3029 cmd = cmd % PYTHON
3030 cmd = cmd % PYTHON
3030 if PYTHON3:
3031 if PYTHON3:
3031 cmd = _strpath(cmd)
3032 cmd = _strpath(cmd)
3032 pipe = os.popen(cmd)
3033 pipe = os.popen(cmd)
3033 try:
3034 try:
3034 self._hgpath = _bytespath(pipe.read().strip())
3035 self._hgpath = _bytespath(pipe.read().strip())
3035 finally:
3036 finally:
3036 pipe.close()
3037 pipe.close()
3037
3038
3038 return self._hgpath
3039 return self._hgpath
3039
3040
3040 def _installchg(self):
3041 def _installchg(self):
3041 """Install chg into the test environment"""
3042 """Install chg into the test environment"""
3042 vlog('# Performing temporary installation of CHG')
3043 vlog('# Performing temporary installation of CHG')
3043 assert os.path.dirname(self._bindir) == self._installdir
3044 assert os.path.dirname(self._bindir) == self._installdir
3044 assert self._hgroot, 'must be called after _installhg()'
3045 assert self._hgroot, 'must be called after _installhg()'
3045 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
3046 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
3046 % {b'make': 'make', # TODO: switch by option or environment?
3047 % {b'make': 'make', # TODO: switch by option or environment?
3047 b'prefix': self._installdir})
3048 b'prefix': self._installdir})
3048 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3049 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3049 vlog("# Running", cmd)
3050 vlog("# Running", cmd)
3050 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
3051 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
3051 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3052 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3052 stderr=subprocess.STDOUT)
3053 stderr=subprocess.STDOUT)
3053 out, _err = proc.communicate()
3054 out, _err = proc.communicate()
3054 if proc.returncode != 0:
3055 if proc.returncode != 0:
3055 if PYTHON3:
3056 if PYTHON3:
3056 sys.stdout.buffer.write(out)
3057 sys.stdout.buffer.write(out)
3057 else:
3058 else:
3058 sys.stdout.write(out)
3059 sys.stdout.write(out)
3059 sys.exit(1)
3060 sys.exit(1)
3060
3061
3061 def _outputcoverage(self):
3062 def _outputcoverage(self):
3062 """Produce code coverage output."""
3063 """Produce code coverage output."""
3063 import coverage
3064 import coverage
3064 coverage = coverage.coverage
3065 coverage = coverage.coverage
3065
3066
3066 vlog('# Producing coverage report')
3067 vlog('# Producing coverage report')
3067 # chdir is the easiest way to get short, relative paths in the
3068 # chdir is the easiest way to get short, relative paths in the
3068 # output.
3069 # output.
3069 os.chdir(self._hgroot)
3070 os.chdir(self._hgroot)
3070 covdir = os.path.join(self._installdir, '..', 'coverage')
3071 covdir = os.path.join(self._installdir, '..', 'coverage')
3071 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3072 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3072
3073
3073 # Map install directory paths back to source directory.
3074 # Map install directory paths back to source directory.
3074 cov.config.paths['srcdir'] = ['.', self._pythondir]
3075 cov.config.paths['srcdir'] = ['.', self._pythondir]
3075
3076
3076 cov.combine()
3077 cov.combine()
3077
3078
3078 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
3079 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
3079 cov.report(ignore_errors=True, omit=omit)
3080 cov.report(ignore_errors=True, omit=omit)
3080
3081
3081 if self.options.htmlcov:
3082 if self.options.htmlcov:
3082 htmldir = os.path.join(self._outputdir, 'htmlcov')
3083 htmldir = os.path.join(self._outputdir, 'htmlcov')
3083 cov.html_report(directory=htmldir, omit=omit)
3084 cov.html_report(directory=htmldir, omit=omit)
3084 if self.options.annotate:
3085 if self.options.annotate:
3085 adir = os.path.join(self._outputdir, 'annotated')
3086 adir = os.path.join(self._outputdir, 'annotated')
3086 if not os.path.isdir(adir):
3087 if not os.path.isdir(adir):
3087 os.mkdir(adir)
3088 os.mkdir(adir)
3088 cov.annotate(directory=adir, omit=omit)
3089 cov.annotate(directory=adir, omit=omit)
3089
3090
3090 def _findprogram(self, program):
3091 def _findprogram(self, program):
3091 """Search PATH for a executable program"""
3092 """Search PATH for a executable program"""
3092 dpb = _bytespath(os.defpath)
3093 dpb = _bytespath(os.defpath)
3093 sepb = _bytespath(os.pathsep)
3094 sepb = _bytespath(os.pathsep)
3094 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3095 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3095 name = os.path.join(p, program)
3096 name = os.path.join(p, program)
3096 if os.name == 'nt' or os.access(name, os.X_OK):
3097 if os.name == 'nt' or os.access(name, os.X_OK):
3097 return name
3098 return name
3098 return None
3099 return None
3099
3100
3100 def _checktools(self):
3101 def _checktools(self):
3101 """Ensure tools required to run tests are present."""
3102 """Ensure tools required to run tests are present."""
3102 for p in self.REQUIREDTOOLS:
3103 for p in self.REQUIREDTOOLS:
3103 if os.name == 'nt' and not p.endswith('.exe'):
3104 if os.name == 'nt' and not p.endswith('.exe'):
3104 p += '.exe'
3105 p += '.exe'
3105 found = self._findprogram(p)
3106 found = self._findprogram(p)
3106 if found:
3107 if found:
3107 vlog("# Found prerequisite", p, "at", found)
3108 vlog("# Found prerequisite", p, "at", found)
3108 else:
3109 else:
3109 print("WARNING: Did not find prerequisite tool: %s " %
3110 print("WARNING: Did not find prerequisite tool: %s " %
3110 p.decode("utf-8"))
3111 p.decode("utf-8"))
3111
3112
3112 def aggregateexceptions(path):
3113 def aggregateexceptions(path):
3113 exceptioncounts = collections.Counter()
3114 exceptioncounts = collections.Counter()
3114 testsbyfailure = collections.defaultdict(set)
3115 testsbyfailure = collections.defaultdict(set)
3115 failuresbytest = collections.defaultdict(set)
3116 failuresbytest = collections.defaultdict(set)
3116
3117
3117 for f in os.listdir(path):
3118 for f in os.listdir(path):
3118 with open(os.path.join(path, f), 'rb') as fh:
3119 with open(os.path.join(path, f), 'rb') as fh:
3119 data = fh.read().split(b'\0')
3120 data = fh.read().split(b'\0')
3120 if len(data) != 5:
3121 if len(data) != 5:
3121 continue
3122 continue
3122
3123
3123 exc, mainframe, hgframe, hgline, testname = data
3124 exc, mainframe, hgframe, hgline, testname = data
3124 exc = exc.decode('utf-8')
3125 exc = exc.decode('utf-8')
3125 mainframe = mainframe.decode('utf-8')
3126 mainframe = mainframe.decode('utf-8')
3126 hgframe = hgframe.decode('utf-8')
3127 hgframe = hgframe.decode('utf-8')
3127 hgline = hgline.decode('utf-8')
3128 hgline = hgline.decode('utf-8')
3128 testname = testname.decode('utf-8')
3129 testname = testname.decode('utf-8')
3129
3130
3130 key = (hgframe, hgline, exc)
3131 key = (hgframe, hgline, exc)
3131 exceptioncounts[key] += 1
3132 exceptioncounts[key] += 1
3132 testsbyfailure[key].add(testname)
3133 testsbyfailure[key].add(testname)
3133 failuresbytest[testname].add(key)
3134 failuresbytest[testname].add(key)
3134
3135
3135 # Find test having fewest failures for each failure.
3136 # Find test having fewest failures for each failure.
3136 leastfailing = {}
3137 leastfailing = {}
3137 for key, tests in testsbyfailure.items():
3138 for key, tests in testsbyfailure.items():
3138 fewesttest = None
3139 fewesttest = None
3139 fewestcount = 99999999
3140 fewestcount = 99999999
3140 for test in sorted(tests):
3141 for test in sorted(tests):
3141 if len(failuresbytest[test]) < fewestcount:
3142 if len(failuresbytest[test]) < fewestcount:
3142 fewesttest = test
3143 fewesttest = test
3143 fewestcount = len(failuresbytest[test])
3144 fewestcount = len(failuresbytest[test])
3144
3145
3145 leastfailing[key] = (fewestcount, fewesttest)
3146 leastfailing[key] = (fewestcount, fewesttest)
3146
3147
3147 # Create a combined counter so we can sort by total occurrences and
3148 # Create a combined counter so we can sort by total occurrences and
3148 # impacted tests.
3149 # impacted tests.
3149 combined = {}
3150 combined = {}
3150 for key in exceptioncounts:
3151 for key in exceptioncounts:
3151 combined[key] = (exceptioncounts[key],
3152 combined[key] = (exceptioncounts[key],
3152 len(testsbyfailure[key]),
3153 len(testsbyfailure[key]),
3153 leastfailing[key][0],
3154 leastfailing[key][0],
3154 leastfailing[key][1])
3155 leastfailing[key][1])
3155
3156
3156 return {
3157 return {
3157 'exceptioncounts': exceptioncounts,
3158 'exceptioncounts': exceptioncounts,
3158 'total': sum(exceptioncounts.values()),
3159 'total': sum(exceptioncounts.values()),
3159 'combined': combined,
3160 'combined': combined,
3160 'leastfailing': leastfailing,
3161 'leastfailing': leastfailing,
3161 'byfailure': testsbyfailure,
3162 'byfailure': testsbyfailure,
3162 'bytest': failuresbytest,
3163 'bytest': failuresbytest,
3163 }
3164 }
3164
3165
3165 if __name__ == '__main__':
3166 if __name__ == '__main__':
3166 runner = TestRunner()
3167 runner = TestRunner()
3167
3168
3168 try:
3169 try:
3169 import msvcrt
3170 import msvcrt
3170 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3171 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3171 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3172 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3172 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3173 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3173 except ImportError:
3174 except ImportError:
3174 pass
3175 pass
3175
3176
3176 sys.exit(runner.run(sys.argv[1:]))
3177 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now