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