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