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