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