##// END OF EJS Templates
run-tests: extract sorting of tests to own function...
Gregory Szorc -
r35505:212a6e9a default
parent child Browse files
Show More
@@ -1,2998 +1,3005 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # run-tests.py - Run a set of tests on Mercurial
3 # run-tests.py - Run a set of tests on Mercurial
4 #
4 #
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 # Modifying this script is tricky because it has many modes:
10 # Modifying this script is tricky because it has many modes:
11 # - serial (default) vs parallel (-jN, N > 1)
11 # - serial (default) vs parallel (-jN, N > 1)
12 # - no coverage (default) vs coverage (-c, -C, -s)
12 # - no coverage (default) vs coverage (-c, -C, -s)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 # - tests are a mix of shell scripts and Python scripts
14 # - tests are a mix of shell scripts and Python scripts
15 #
15 #
16 # If you change this script, it is recommended that you ensure you
16 # If you change this script, it is recommended that you ensure you
17 # haven't broken it by running it in various modes with a representative
17 # haven't broken it by running it in various modes with a representative
18 # sample of test scripts. For example:
18 # sample of test scripts. For example:
19 #
19 #
20 # 1) serial, no coverage, temp install:
20 # 1) serial, no coverage, temp install:
21 # ./run-tests.py test-s*
21 # ./run-tests.py test-s*
22 # 2) serial, no coverage, local hg:
22 # 2) serial, no coverage, local hg:
23 # ./run-tests.py --local test-s*
23 # ./run-tests.py --local test-s*
24 # 3) serial, coverage, temp install:
24 # 3) serial, coverage, temp install:
25 # ./run-tests.py -c test-s*
25 # ./run-tests.py -c test-s*
26 # 4) serial, coverage, local hg:
26 # 4) serial, coverage, local hg:
27 # ./run-tests.py -c --local test-s* # unsupported
27 # ./run-tests.py -c --local test-s* # unsupported
28 # 5) parallel, no coverage, temp install:
28 # 5) parallel, no coverage, temp install:
29 # ./run-tests.py -j2 test-s*
29 # ./run-tests.py -j2 test-s*
30 # 6) parallel, no coverage, local hg:
30 # 6) parallel, no coverage, local hg:
31 # ./run-tests.py -j2 --local test-s*
31 # ./run-tests.py -j2 --local test-s*
32 # 7) parallel, coverage, temp install:
32 # 7) parallel, coverage, temp install:
33 # ./run-tests.py -j2 -c test-s* # currently broken
33 # ./run-tests.py -j2 -c test-s* # currently broken
34 # 8) parallel, coverage, local install:
34 # 8) parallel, coverage, local install:
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 # 9) parallel, custom tmp dir:
36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 # 10) parallel, pure, tests that call run-tests:
38 # 10) parallel, pure, tests that call run-tests:
39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 #
40 #
41 # (You could use any subset of the tests: test-s* happens to match
41 # (You could use any subset of the tests: test-s* happens to match
42 # enough that it's worth doing parallel runs, few enough that it
42 # enough that it's worth doing parallel runs, few enough that it
43 # completes fairly quickly, includes both shell and Python scripts, and
43 # completes fairly quickly, includes both shell and Python scripts, and
44 # includes some scripts that run daemon processes.)
44 # includes some scripts that run daemon processes.)
45
45
46 from __future__ import absolute_import, print_function
46 from __future__ import absolute_import, print_function
47
47
48 import argparse
48 import argparse
49 import collections
49 import collections
50 import difflib
50 import difflib
51 import distutils.version as version
51 import distutils.version as version
52 import errno
52 import errno
53 import json
53 import json
54 import os
54 import os
55 import random
55 import random
56 import re
56 import re
57 import shutil
57 import shutil
58 import signal
58 import signal
59 import socket
59 import socket
60 import subprocess
60 import subprocess
61 import sys
61 import sys
62 import sysconfig
62 import sysconfig
63 import tempfile
63 import tempfile
64 import threading
64 import threading
65 import time
65 import time
66 import unittest
66 import unittest
67 import xml.dom.minidom as minidom
67 import xml.dom.minidom as minidom
68
68
69 try:
69 try:
70 import Queue as queue
70 import Queue as queue
71 except ImportError:
71 except ImportError:
72 import queue
72 import queue
73
73
74 try:
74 try:
75 import shlex
75 import shlex
76 shellquote = shlex.quote
76 shellquote = shlex.quote
77 except (ImportError, AttributeError):
77 except (ImportError, AttributeError):
78 import pipes
78 import pipes
79 shellquote = pipes.quote
79 shellquote = pipes.quote
80
80
81 if os.environ.get('RTUNICODEPEDANTRY', False):
81 if os.environ.get('RTUNICODEPEDANTRY', False):
82 try:
82 try:
83 reload(sys)
83 reload(sys)
84 sys.setdefaultencoding("undefined")
84 sys.setdefaultencoding("undefined")
85 except NameError:
85 except NameError:
86 pass
86 pass
87
87
88 origenviron = os.environ.copy()
88 origenviron = os.environ.copy()
89 osenvironb = getattr(os, 'environb', os.environ)
89 osenvironb = getattr(os, 'environb', os.environ)
90 processlock = threading.Lock()
90 processlock = threading.Lock()
91
91
92 pygmentspresent = False
92 pygmentspresent = False
93 # ANSI color is unsupported prior to Windows 10
93 # ANSI color is unsupported prior to Windows 10
94 if os.name != 'nt':
94 if os.name != 'nt':
95 try: # is pygments installed
95 try: # is pygments installed
96 import pygments
96 import pygments
97 import pygments.lexers as lexers
97 import pygments.lexers as lexers
98 import pygments.lexer as lexer
98 import pygments.lexer as lexer
99 import pygments.formatters as formatters
99 import pygments.formatters as formatters
100 import pygments.token as token
100 import pygments.token as token
101 import pygments.style as style
101 import pygments.style as style
102 pygmentspresent = True
102 pygmentspresent = True
103 difflexer = lexers.DiffLexer()
103 difflexer = lexers.DiffLexer()
104 terminal256formatter = formatters.Terminal256Formatter()
104 terminal256formatter = formatters.Terminal256Formatter()
105 except ImportError:
105 except ImportError:
106 pass
106 pass
107
107
108 if pygmentspresent:
108 if pygmentspresent:
109 class TestRunnerStyle(style.Style):
109 class TestRunnerStyle(style.Style):
110 default_style = ""
110 default_style = ""
111 skipped = token.string_to_tokentype("Token.Generic.Skipped")
111 skipped = token.string_to_tokentype("Token.Generic.Skipped")
112 failed = token.string_to_tokentype("Token.Generic.Failed")
112 failed = token.string_to_tokentype("Token.Generic.Failed")
113 skippedname = token.string_to_tokentype("Token.Generic.SName")
113 skippedname = token.string_to_tokentype("Token.Generic.SName")
114 failedname = token.string_to_tokentype("Token.Generic.FName")
114 failedname = token.string_to_tokentype("Token.Generic.FName")
115 styles = {
115 styles = {
116 skipped: '#e5e5e5',
116 skipped: '#e5e5e5',
117 skippedname: '#00ffff',
117 skippedname: '#00ffff',
118 failed: '#7f0000',
118 failed: '#7f0000',
119 failedname: '#ff0000',
119 failedname: '#ff0000',
120 }
120 }
121
121
122 class TestRunnerLexer(lexer.RegexLexer):
122 class TestRunnerLexer(lexer.RegexLexer):
123 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
1337
1338 n = 0
1338 n = 0
1339 for n, l in enumerate(lines):
1339 for n, l in enumerate(lines):
1340 if not l.endswith(b'\n'):
1340 if not l.endswith(b'\n'):
1341 l += b'\n'
1341 l += b'\n'
1342 if l.startswith(b'#require'):
1342 if l.startswith(b'#require'):
1343 lsplit = l.split()
1343 lsplit = l.split()
1344 if len(lsplit) < 2 or lsplit[0] != b'#require':
1344 if len(lsplit) < 2 or lsplit[0] != b'#require':
1345 after.setdefault(pos, []).append(' !!! invalid #require\n')
1345 after.setdefault(pos, []).append(' !!! invalid #require\n')
1346 haveresult, message = self._hghave(lsplit[1:])
1346 haveresult, message = self._hghave(lsplit[1:])
1347 if not haveresult:
1347 if not haveresult:
1348 script = [b'echo "%s"\nexit 80\n' % message]
1348 script = [b'echo "%s"\nexit 80\n' % message]
1349 break
1349 break
1350 after.setdefault(pos, []).append(l)
1350 after.setdefault(pos, []).append(l)
1351 elif l.startswith(b'#if'):
1351 elif l.startswith(b'#if'):
1352 lsplit = l.split()
1352 lsplit = l.split()
1353 if len(lsplit) < 2 or lsplit[0] != b'#if':
1353 if len(lsplit) < 2 or lsplit[0] != b'#if':
1354 after.setdefault(pos, []).append(' !!! invalid #if\n')
1354 after.setdefault(pos, []).append(' !!! invalid #if\n')
1355 if skipping is not None:
1355 if skipping is not None:
1356 after.setdefault(pos, []).append(' !!! nested #if\n')
1356 after.setdefault(pos, []).append(' !!! nested #if\n')
1357 skipping = not self._iftest(lsplit[1:])
1357 skipping = not self._iftest(lsplit[1:])
1358 after.setdefault(pos, []).append(l)
1358 after.setdefault(pos, []).append(l)
1359 elif l.startswith(b'#else'):
1359 elif l.startswith(b'#else'):
1360 if skipping is None:
1360 if skipping is None:
1361 after.setdefault(pos, []).append(' !!! missing #if\n')
1361 after.setdefault(pos, []).append(' !!! missing #if\n')
1362 skipping = not skipping
1362 skipping = not skipping
1363 after.setdefault(pos, []).append(l)
1363 after.setdefault(pos, []).append(l)
1364 elif l.startswith(b'#endif'):
1364 elif l.startswith(b'#endif'):
1365 if skipping is None:
1365 if skipping is None:
1366 after.setdefault(pos, []).append(' !!! missing #if\n')
1366 after.setdefault(pos, []).append(' !!! missing #if\n')
1367 skipping = None
1367 skipping = None
1368 after.setdefault(pos, []).append(l)
1368 after.setdefault(pos, []).append(l)
1369 elif skipping:
1369 elif skipping:
1370 after.setdefault(pos, []).append(l)
1370 after.setdefault(pos, []).append(l)
1371 elif l.startswith(b' >>> '): # python inlines
1371 elif l.startswith(b' >>> '): # python inlines
1372 after.setdefault(pos, []).append(l)
1372 after.setdefault(pos, []).append(l)
1373 prepos = pos
1373 prepos = pos
1374 pos = n
1374 pos = n
1375 if not inpython:
1375 if not inpython:
1376 # We've just entered a Python block. Add the header.
1376 # We've just entered a Python block. Add the header.
1377 inpython = True
1377 inpython = True
1378 addsalt(prepos, False) # Make sure we report the exit code.
1378 addsalt(prepos, False) # Make sure we report the exit code.
1379 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1379 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1380 addsalt(n, True)
1380 addsalt(n, True)
1381 script.append(l[2:])
1381 script.append(l[2:])
1382 elif l.startswith(b' ... '): # python inlines
1382 elif l.startswith(b' ... '): # python inlines
1383 after.setdefault(prepos, []).append(l)
1383 after.setdefault(prepos, []).append(l)
1384 script.append(l[2:])
1384 script.append(l[2:])
1385 elif l.startswith(b' $ '): # commands
1385 elif l.startswith(b' $ '): # commands
1386 if inpython:
1386 if inpython:
1387 script.append(b'EOF\n')
1387 script.append(b'EOF\n')
1388 inpython = False
1388 inpython = False
1389 after.setdefault(pos, []).append(l)
1389 after.setdefault(pos, []).append(l)
1390 prepos = pos
1390 prepos = pos
1391 pos = n
1391 pos = n
1392 addsalt(n, False)
1392 addsalt(n, False)
1393 cmd = l[4:].split()
1393 cmd = l[4:].split()
1394 if len(cmd) == 2 and cmd[0] == b'cd':
1394 if len(cmd) == 2 and cmd[0] == b'cd':
1395 l = b' $ cd %s || exit 1\n' % cmd[1]
1395 l = b' $ cd %s || exit 1\n' % cmd[1]
1396 script.append(l[4:])
1396 script.append(l[4:])
1397 elif l.startswith(b' > '): # continuations
1397 elif l.startswith(b' > '): # continuations
1398 after.setdefault(prepos, []).append(l)
1398 after.setdefault(prepos, []).append(l)
1399 script.append(l[4:])
1399 script.append(l[4:])
1400 elif l.startswith(b' '): # results
1400 elif l.startswith(b' '): # results
1401 # Queue up a list of expected results.
1401 # Queue up a list of expected results.
1402 expected.setdefault(pos, []).append(l[2:])
1402 expected.setdefault(pos, []).append(l[2:])
1403 else:
1403 else:
1404 if inpython:
1404 if inpython:
1405 script.append(b'EOF\n')
1405 script.append(b'EOF\n')
1406 inpython = False
1406 inpython = False
1407 # Non-command/result. Queue up for merged output.
1407 # Non-command/result. Queue up for merged output.
1408 after.setdefault(pos, []).append(l)
1408 after.setdefault(pos, []).append(l)
1409
1409
1410 if inpython:
1410 if inpython:
1411 script.append(b'EOF\n')
1411 script.append(b'EOF\n')
1412 if skipping is not None:
1412 if skipping is not None:
1413 after.setdefault(pos, []).append(' !!! missing #endif\n')
1413 after.setdefault(pos, []).append(' !!! missing #endif\n')
1414 addsalt(n + 1, False)
1414 addsalt(n + 1, False)
1415
1415
1416 return salt, script, after, expected
1416 return salt, script, after, expected
1417
1417
1418 def _processoutput(self, exitcode, output, salt, after, expected):
1418 def _processoutput(self, exitcode, output, salt, after, expected):
1419 # Merge the script output back into a unified test.
1419 # Merge the script output back into a unified test.
1420 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1420 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1421 if exitcode != 0:
1421 if exitcode != 0:
1422 warnonly = 3
1422 warnonly = 3
1423
1423
1424 pos = -1
1424 pos = -1
1425 postout = []
1425 postout = []
1426 for l in output:
1426 for l in output:
1427 lout, lcmd = l, None
1427 lout, lcmd = l, None
1428 if salt in l:
1428 if salt in l:
1429 lout, lcmd = l.split(salt, 1)
1429 lout, lcmd = l.split(salt, 1)
1430
1430
1431 while lout:
1431 while lout:
1432 if not lout.endswith(b'\n'):
1432 if not lout.endswith(b'\n'):
1433 lout += b' (no-eol)\n'
1433 lout += b' (no-eol)\n'
1434
1434
1435 # Find the expected output at the current position.
1435 # Find the expected output at the current position.
1436 els = [None]
1436 els = [None]
1437 if expected.get(pos, None):
1437 if expected.get(pos, None):
1438 els = expected[pos]
1438 els = expected[pos]
1439
1439
1440 i = 0
1440 i = 0
1441 optional = []
1441 optional = []
1442 while i < len(els):
1442 while i < len(els):
1443 el = els[i]
1443 el = els[i]
1444
1444
1445 r = self.linematch(el, lout)
1445 r = self.linematch(el, lout)
1446 if isinstance(r, str):
1446 if isinstance(r, str):
1447 if r == '-glob':
1447 if r == '-glob':
1448 lout = ''.join(el.rsplit(' (glob)', 1))
1448 lout = ''.join(el.rsplit(' (glob)', 1))
1449 r = '' # Warn only this line.
1449 r = '' # Warn only this line.
1450 elif r == "retry":
1450 elif r == "retry":
1451 postout.append(b' ' + el)
1451 postout.append(b' ' + el)
1452 els.pop(i)
1452 els.pop(i)
1453 break
1453 break
1454 else:
1454 else:
1455 log('\ninfo, unknown linematch result: %r\n' % r)
1455 log('\ninfo, unknown linematch result: %r\n' % r)
1456 r = False
1456 r = False
1457 if r:
1457 if r:
1458 els.pop(i)
1458 els.pop(i)
1459 break
1459 break
1460 if el:
1460 if el:
1461 if el.endswith(b" (?)\n"):
1461 if el.endswith(b" (?)\n"):
1462 optional.append(i)
1462 optional.append(i)
1463 else:
1463 else:
1464 m = optline.match(el)
1464 m = optline.match(el)
1465 if m:
1465 if m:
1466 conditions = [
1466 conditions = [
1467 c for c in m.group(2).split(b' ')]
1467 c for c in m.group(2).split(b' ')]
1468
1468
1469 if not self._iftest(conditions):
1469 if not self._iftest(conditions):
1470 optional.append(i)
1470 optional.append(i)
1471
1471
1472 i += 1
1472 i += 1
1473
1473
1474 if r:
1474 if r:
1475 if r == "retry":
1475 if r == "retry":
1476 continue
1476 continue
1477 # clean up any optional leftovers
1477 # clean up any optional leftovers
1478 for i in optional:
1478 for i in optional:
1479 postout.append(b' ' + els[i])
1479 postout.append(b' ' + els[i])
1480 for i in reversed(optional):
1480 for i in reversed(optional):
1481 del els[i]
1481 del els[i]
1482 postout.append(b' ' + el)
1482 postout.append(b' ' + el)
1483 else:
1483 else:
1484 if self.NEEDESCAPE(lout):
1484 if self.NEEDESCAPE(lout):
1485 lout = TTest._stringescape(b'%s (esc)\n' %
1485 lout = TTest._stringescape(b'%s (esc)\n' %
1486 lout.rstrip(b'\n'))
1486 lout.rstrip(b'\n'))
1487 postout.append(b' ' + lout) # Let diff deal with it.
1487 postout.append(b' ' + lout) # Let diff deal with it.
1488 if r != '': # If line failed.
1488 if r != '': # If line failed.
1489 warnonly = 3 # for sure not
1489 warnonly = 3 # for sure not
1490 elif warnonly == 1: # Is "not yet" and line is warn only.
1490 elif warnonly == 1: # Is "not yet" and line is warn only.
1491 warnonly = 2 # Yes do warn.
1491 warnonly = 2 # Yes do warn.
1492 break
1492 break
1493 else:
1493 else:
1494 # clean up any optional leftovers
1494 # clean up any optional leftovers
1495 while expected.get(pos, None):
1495 while expected.get(pos, None):
1496 el = expected[pos].pop(0)
1496 el = expected[pos].pop(0)
1497 if el:
1497 if el:
1498 if not el.endswith(b" (?)\n"):
1498 if not el.endswith(b" (?)\n"):
1499 m = optline.match(el)
1499 m = optline.match(el)
1500 if m:
1500 if m:
1501 conditions = [c for c in m.group(2).split(b' ')]
1501 conditions = [c for c in m.group(2).split(b' ')]
1502
1502
1503 if self._iftest(conditions):
1503 if self._iftest(conditions):
1504 # Don't append as optional line
1504 # Don't append as optional line
1505 continue
1505 continue
1506 else:
1506 else:
1507 continue
1507 continue
1508 postout.append(b' ' + el)
1508 postout.append(b' ' + el)
1509
1509
1510 if lcmd:
1510 if lcmd:
1511 # Add on last return code.
1511 # Add on last return code.
1512 ret = int(lcmd.split()[1])
1512 ret = int(lcmd.split()[1])
1513 if ret != 0:
1513 if ret != 0:
1514 postout.append(b' [%d]\n' % ret)
1514 postout.append(b' [%d]\n' % ret)
1515 if pos in after:
1515 if pos in after:
1516 # Merge in non-active test bits.
1516 # Merge in non-active test bits.
1517 postout += after.pop(pos)
1517 postout += after.pop(pos)
1518 pos = int(lcmd.split()[0])
1518 pos = int(lcmd.split()[0])
1519
1519
1520 if pos in after:
1520 if pos in after:
1521 postout += after.pop(pos)
1521 postout += after.pop(pos)
1522
1522
1523 if warnonly == 2:
1523 if warnonly == 2:
1524 exitcode = False # Set exitcode to warned.
1524 exitcode = False # Set exitcode to warned.
1525
1525
1526 return exitcode, postout
1526 return exitcode, postout
1527
1527
1528 @staticmethod
1528 @staticmethod
1529 def rematch(el, l):
1529 def rematch(el, l):
1530 try:
1530 try:
1531 el = b'(?:' + el + b')'
1531 el = b'(?:' + el + b')'
1532 # use \Z to ensure that the regex matches to the end of the string
1532 # use \Z to ensure that the regex matches to the end of the string
1533 if os.name == 'nt':
1533 if os.name == 'nt':
1534 return re.match(el + br'\r?\n\Z', l)
1534 return re.match(el + br'\r?\n\Z', l)
1535 return re.match(el + br'\n\Z', l)
1535 return re.match(el + br'\n\Z', l)
1536 except re.error:
1536 except re.error:
1537 # el is an invalid regex
1537 # el is an invalid regex
1538 return False
1538 return False
1539
1539
1540 @staticmethod
1540 @staticmethod
1541 def globmatch(el, l):
1541 def globmatch(el, l):
1542 # The only supported special characters are * and ? plus / which also
1542 # The only supported special characters are * and ? plus / which also
1543 # matches \ on windows. Escaping of these characters is supported.
1543 # matches \ on windows. Escaping of these characters is supported.
1544 if el + b'\n' == l:
1544 if el + b'\n' == l:
1545 if os.altsep:
1545 if os.altsep:
1546 # matching on "/" is not needed for this line
1546 # matching on "/" is not needed for this line
1547 for pat in checkcodeglobpats:
1547 for pat in checkcodeglobpats:
1548 if pat.match(el):
1548 if pat.match(el):
1549 return True
1549 return True
1550 return b'-glob'
1550 return b'-glob'
1551 return True
1551 return True
1552 el = el.replace(b'$LOCALIP', b'*')
1552 el = el.replace(b'$LOCALIP', b'*')
1553 i, n = 0, len(el)
1553 i, n = 0, len(el)
1554 res = b''
1554 res = b''
1555 while i < n:
1555 while i < n:
1556 c = el[i:i + 1]
1556 c = el[i:i + 1]
1557 i += 1
1557 i += 1
1558 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1558 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1559 res += el[i - 1:i + 1]
1559 res += el[i - 1:i + 1]
1560 i += 1
1560 i += 1
1561 elif c == b'*':
1561 elif c == b'*':
1562 res += b'.*'
1562 res += b'.*'
1563 elif c == b'?':
1563 elif c == b'?':
1564 res += b'.'
1564 res += b'.'
1565 elif c == b'/' and os.altsep:
1565 elif c == b'/' and os.altsep:
1566 res += b'[/\\\\]'
1566 res += b'[/\\\\]'
1567 else:
1567 else:
1568 res += re.escape(c)
1568 res += re.escape(c)
1569 return TTest.rematch(res, l)
1569 return TTest.rematch(res, l)
1570
1570
1571 def linematch(self, el, l):
1571 def linematch(self, el, l):
1572 retry = False
1572 retry = False
1573 if el == l: # perfect match (fast)
1573 if el == l: # perfect match (fast)
1574 return True
1574 return True
1575 if el:
1575 if el:
1576 if el.endswith(b" (?)\n"):
1576 if el.endswith(b" (?)\n"):
1577 retry = "retry"
1577 retry = "retry"
1578 el = el[:-5] + b"\n"
1578 el = el[:-5] + b"\n"
1579 else:
1579 else:
1580 m = optline.match(el)
1580 m = optline.match(el)
1581 if m:
1581 if m:
1582 conditions = [c for c in m.group(2).split(b' ')]
1582 conditions = [c for c in m.group(2).split(b' ')]
1583
1583
1584 el = m.group(1) + b"\n"
1584 el = m.group(1) + b"\n"
1585 if not self._iftest(conditions):
1585 if not self._iftest(conditions):
1586 retry = "retry" # Not required by listed features
1586 retry = "retry" # Not required by listed features
1587
1587
1588 if el.endswith(b" (esc)\n"):
1588 if el.endswith(b" (esc)\n"):
1589 if PYTHON3:
1589 if PYTHON3:
1590 el = el[:-7].decode('unicode_escape') + '\n'
1590 el = el[:-7].decode('unicode_escape') + '\n'
1591 el = el.encode('utf-8')
1591 el = el.encode('utf-8')
1592 else:
1592 else:
1593 el = el[:-7].decode('string-escape') + '\n'
1593 el = el[:-7].decode('string-escape') + '\n'
1594 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1594 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1595 return True
1595 return True
1596 if el.endswith(b" (re)\n"):
1596 if el.endswith(b" (re)\n"):
1597 return TTest.rematch(el[:-6], l) or retry
1597 return TTest.rematch(el[:-6], l) or retry
1598 if el.endswith(b" (glob)\n"):
1598 if el.endswith(b" (glob)\n"):
1599 # ignore '(glob)' added to l by 'replacements'
1599 # ignore '(glob)' added to l by 'replacements'
1600 if l.endswith(b" (glob)\n"):
1600 if l.endswith(b" (glob)\n"):
1601 l = l[:-8] + b"\n"
1601 l = l[:-8] + b"\n"
1602 return TTest.globmatch(el[:-8], l) or retry
1602 return TTest.globmatch(el[:-8], l) or retry
1603 if os.altsep:
1603 if os.altsep:
1604 _l = l.replace(b'\\', b'/')
1604 _l = l.replace(b'\\', b'/')
1605 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1605 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1606 return True
1606 return True
1607 return retry
1607 return retry
1608
1608
1609 @staticmethod
1609 @staticmethod
1610 def parsehghaveoutput(lines):
1610 def parsehghaveoutput(lines):
1611 '''Parse hghave log lines.
1611 '''Parse hghave log lines.
1612
1612
1613 Return tuple of lists (missing, failed):
1613 Return tuple of lists (missing, failed):
1614 * the missing/unknown features
1614 * the missing/unknown features
1615 * the features for which existence check failed'''
1615 * the features for which existence check failed'''
1616 missing = []
1616 missing = []
1617 failed = []
1617 failed = []
1618 for line in lines:
1618 for line in lines:
1619 if line.startswith(TTest.SKIPPED_PREFIX):
1619 if line.startswith(TTest.SKIPPED_PREFIX):
1620 line = line.splitlines()[0]
1620 line = line.splitlines()[0]
1621 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1621 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1622 elif line.startswith(TTest.FAILED_PREFIX):
1622 elif line.startswith(TTest.FAILED_PREFIX):
1623 line = line.splitlines()[0]
1623 line = line.splitlines()[0]
1624 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1624 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1625
1625
1626 return missing, failed
1626 return missing, failed
1627
1627
1628 @staticmethod
1628 @staticmethod
1629 def _escapef(m):
1629 def _escapef(m):
1630 return TTest.ESCAPEMAP[m.group(0)]
1630 return TTest.ESCAPEMAP[m.group(0)]
1631
1631
1632 @staticmethod
1632 @staticmethod
1633 def _stringescape(s):
1633 def _stringescape(s):
1634 return TTest.ESCAPESUB(TTest._escapef, s)
1634 return TTest.ESCAPESUB(TTest._escapef, s)
1635
1635
1636 iolock = threading.RLock()
1636 iolock = threading.RLock()
1637
1637
1638 class TestResult(unittest._TextTestResult):
1638 class TestResult(unittest._TextTestResult):
1639 """Holds results when executing via unittest."""
1639 """Holds results when executing via unittest."""
1640 # Don't worry too much about accessing the non-public _TextTestResult.
1640 # Don't worry too much about accessing the non-public _TextTestResult.
1641 # It is relatively common in Python testing tools.
1641 # It is relatively common in Python testing tools.
1642 def __init__(self, options, *args, **kwargs):
1642 def __init__(self, options, *args, **kwargs):
1643 super(TestResult, self).__init__(*args, **kwargs)
1643 super(TestResult, self).__init__(*args, **kwargs)
1644
1644
1645 self._options = options
1645 self._options = options
1646
1646
1647 # unittest.TestResult didn't have skipped until 2.7. We need to
1647 # unittest.TestResult didn't have skipped until 2.7. We need to
1648 # polyfill it.
1648 # polyfill it.
1649 self.skipped = []
1649 self.skipped = []
1650
1650
1651 # We have a custom "ignored" result that isn't present in any Python
1651 # We have a custom "ignored" result that isn't present in any Python
1652 # unittest implementation. It is very similar to skipped. It may make
1652 # unittest implementation. It is very similar to skipped. It may make
1653 # sense to map it into skip some day.
1653 # sense to map it into skip some day.
1654 self.ignored = []
1654 self.ignored = []
1655
1655
1656 self.times = []
1656 self.times = []
1657 self._firststarttime = None
1657 self._firststarttime = None
1658 # Data stored for the benefit of generating xunit reports.
1658 # Data stored for the benefit of generating xunit reports.
1659 self.successes = []
1659 self.successes = []
1660 self.faildata = {}
1660 self.faildata = {}
1661
1661
1662 if options.color == 'auto':
1662 if options.color == 'auto':
1663 self.color = pygmentspresent and self.stream.isatty()
1663 self.color = pygmentspresent and self.stream.isatty()
1664 elif options.color == 'never':
1664 elif options.color == 'never':
1665 self.color = False
1665 self.color = False
1666 else: # 'always', for testing purposes
1666 else: # 'always', for testing purposes
1667 self.color = pygmentspresent
1667 self.color = pygmentspresent
1668
1668
1669 def addFailure(self, test, reason):
1669 def addFailure(self, test, reason):
1670 self.failures.append((test, reason))
1670 self.failures.append((test, reason))
1671
1671
1672 if self._options.first:
1672 if self._options.first:
1673 self.stop()
1673 self.stop()
1674 else:
1674 else:
1675 with iolock:
1675 with iolock:
1676 if reason == "timed out":
1676 if reason == "timed out":
1677 self.stream.write('t')
1677 self.stream.write('t')
1678 else:
1678 else:
1679 if not self._options.nodiff:
1679 if not self._options.nodiff:
1680 self.stream.write('\n')
1680 self.stream.write('\n')
1681 # Exclude the '\n' from highlighting to lex correctly
1681 # Exclude the '\n' from highlighting to lex correctly
1682 formatted = 'ERROR: %s output changed\n' % test
1682 formatted = 'ERROR: %s output changed\n' % test
1683 self.stream.write(highlightmsg(formatted, self.color))
1683 self.stream.write(highlightmsg(formatted, self.color))
1684 self.stream.write('!')
1684 self.stream.write('!')
1685
1685
1686 self.stream.flush()
1686 self.stream.flush()
1687
1687
1688 def addSuccess(self, test):
1688 def addSuccess(self, test):
1689 with iolock:
1689 with iolock:
1690 super(TestResult, self).addSuccess(test)
1690 super(TestResult, self).addSuccess(test)
1691 self.successes.append(test)
1691 self.successes.append(test)
1692
1692
1693 def addError(self, test, err):
1693 def addError(self, test, err):
1694 super(TestResult, self).addError(test, err)
1694 super(TestResult, self).addError(test, err)
1695 if self._options.first:
1695 if self._options.first:
1696 self.stop()
1696 self.stop()
1697
1697
1698 # Polyfill.
1698 # Polyfill.
1699 def addSkip(self, test, reason):
1699 def addSkip(self, test, reason):
1700 self.skipped.append((test, reason))
1700 self.skipped.append((test, reason))
1701 with iolock:
1701 with iolock:
1702 if self.showAll:
1702 if self.showAll:
1703 self.stream.writeln('skipped %s' % reason)
1703 self.stream.writeln('skipped %s' % reason)
1704 else:
1704 else:
1705 self.stream.write('s')
1705 self.stream.write('s')
1706 self.stream.flush()
1706 self.stream.flush()
1707
1707
1708 def addIgnore(self, test, reason):
1708 def addIgnore(self, test, reason):
1709 self.ignored.append((test, reason))
1709 self.ignored.append((test, reason))
1710 with iolock:
1710 with iolock:
1711 if self.showAll:
1711 if self.showAll:
1712 self.stream.writeln('ignored %s' % reason)
1712 self.stream.writeln('ignored %s' % reason)
1713 else:
1713 else:
1714 if reason not in ('not retesting', "doesn't match keyword"):
1714 if reason not in ('not retesting', "doesn't match keyword"):
1715 self.stream.write('i')
1715 self.stream.write('i')
1716 else:
1716 else:
1717 self.testsRun += 1
1717 self.testsRun += 1
1718 self.stream.flush()
1718 self.stream.flush()
1719
1719
1720 def addOutputMismatch(self, test, ret, got, expected):
1720 def addOutputMismatch(self, test, ret, got, expected):
1721 """Record a mismatch in test output for a particular test."""
1721 """Record a mismatch in test output for a particular test."""
1722 if self.shouldStop:
1722 if self.shouldStop:
1723 # don't print, some other test case already failed and
1723 # don't print, some other test case already failed and
1724 # printed, we're just stale and probably failed due to our
1724 # printed, we're just stale and probably failed due to our
1725 # temp dir getting cleaned up.
1725 # temp dir getting cleaned up.
1726 return
1726 return
1727
1727
1728 accepted = False
1728 accepted = False
1729 lines = []
1729 lines = []
1730
1730
1731 with iolock:
1731 with iolock:
1732 if self._options.nodiff:
1732 if self._options.nodiff:
1733 pass
1733 pass
1734 elif self._options.view:
1734 elif self._options.view:
1735 v = self._options.view
1735 v = self._options.view
1736 if PYTHON3:
1736 if PYTHON3:
1737 v = _bytespath(v)
1737 v = _bytespath(v)
1738 os.system(b"%s %s %s" %
1738 os.system(b"%s %s %s" %
1739 (v, test.refpath, test.errpath))
1739 (v, test.refpath, test.errpath))
1740 else:
1740 else:
1741 servefail, lines = getdiff(expected, got,
1741 servefail, lines = getdiff(expected, got,
1742 test.refpath, test.errpath)
1742 test.refpath, test.errpath)
1743 if servefail:
1743 if servefail:
1744 raise test.failureException(
1744 raise test.failureException(
1745 'server failed to start (HGPORT=%s)' % test._startport)
1745 'server failed to start (HGPORT=%s)' % test._startport)
1746 else:
1746 else:
1747 self.stream.write('\n')
1747 self.stream.write('\n')
1748 for line in lines:
1748 for line in lines:
1749 line = highlightdiff(line, self.color)
1749 line = highlightdiff(line, self.color)
1750 if PYTHON3:
1750 if PYTHON3:
1751 self.stream.flush()
1751 self.stream.flush()
1752 self.stream.buffer.write(line)
1752 self.stream.buffer.write(line)
1753 self.stream.buffer.flush()
1753 self.stream.buffer.flush()
1754 else:
1754 else:
1755 self.stream.write(line)
1755 self.stream.write(line)
1756 self.stream.flush()
1756 self.stream.flush()
1757
1757
1758 # handle interactive prompt without releasing iolock
1758 # handle interactive prompt without releasing iolock
1759 if self._options.interactive:
1759 if self._options.interactive:
1760 if test.readrefout() != expected:
1760 if test.readrefout() != expected:
1761 self.stream.write(
1761 self.stream.write(
1762 'Reference output has changed (run again to prompt '
1762 'Reference output has changed (run again to prompt '
1763 'changes)')
1763 'changes)')
1764 else:
1764 else:
1765 self.stream.write('Accept this change? [n] ')
1765 self.stream.write('Accept this change? [n] ')
1766 answer = sys.stdin.readline().strip()
1766 answer = sys.stdin.readline().strip()
1767 if answer.lower() in ('y', 'yes'):
1767 if answer.lower() in ('y', 'yes'):
1768 if test.path.endswith(b'.t'):
1768 if test.path.endswith(b'.t'):
1769 rename(test.errpath, test.path)
1769 rename(test.errpath, test.path)
1770 else:
1770 else:
1771 rename(test.errpath, '%s.out' % test.path)
1771 rename(test.errpath, '%s.out' % test.path)
1772 accepted = True
1772 accepted = True
1773 if not accepted:
1773 if not accepted:
1774 self.faildata[test.name] = b''.join(lines)
1774 self.faildata[test.name] = b''.join(lines)
1775
1775
1776 return accepted
1776 return accepted
1777
1777
1778 def startTest(self, test):
1778 def startTest(self, test):
1779 super(TestResult, self).startTest(test)
1779 super(TestResult, self).startTest(test)
1780
1780
1781 # os.times module computes the user time and system time spent by
1781 # os.times module computes the user time and system time spent by
1782 # child's processes along with real elapsed time taken by a process.
1782 # child's processes along with real elapsed time taken by a process.
1783 # This module has one limitation. It can only work for Linux user
1783 # This module has one limitation. It can only work for Linux user
1784 # and not for Windows.
1784 # and not for Windows.
1785 test.started = os.times()
1785 test.started = os.times()
1786 if self._firststarttime is None: # thread racy but irrelevant
1786 if self._firststarttime is None: # thread racy but irrelevant
1787 self._firststarttime = test.started[4]
1787 self._firststarttime = test.started[4]
1788
1788
1789 def stopTest(self, test, interrupted=False):
1789 def stopTest(self, test, interrupted=False):
1790 super(TestResult, self).stopTest(test)
1790 super(TestResult, self).stopTest(test)
1791
1791
1792 test.stopped = os.times()
1792 test.stopped = os.times()
1793
1793
1794 starttime = test.started
1794 starttime = test.started
1795 endtime = test.stopped
1795 endtime = test.stopped
1796 origin = self._firststarttime
1796 origin = self._firststarttime
1797 self.times.append((test.name,
1797 self.times.append((test.name,
1798 endtime[2] - starttime[2], # user space CPU time
1798 endtime[2] - starttime[2], # user space CPU time
1799 endtime[3] - starttime[3], # sys space CPU time
1799 endtime[3] - starttime[3], # sys space CPU time
1800 endtime[4] - starttime[4], # real time
1800 endtime[4] - starttime[4], # real time
1801 starttime[4] - origin, # start date in run context
1801 starttime[4] - origin, # start date in run context
1802 endtime[4] - origin, # end date in run context
1802 endtime[4] - origin, # end date in run context
1803 ))
1803 ))
1804
1804
1805 if interrupted:
1805 if interrupted:
1806 with iolock:
1806 with iolock:
1807 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1807 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1808 test.name, self.times[-1][3]))
1808 test.name, self.times[-1][3]))
1809
1809
1810 class TestSuite(unittest.TestSuite):
1810 class TestSuite(unittest.TestSuite):
1811 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1811 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1812
1812
1813 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1813 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1814 retest=False, keywords=None, loop=False, runs_per_test=1,
1814 retest=False, keywords=None, loop=False, runs_per_test=1,
1815 loadtest=None, showchannels=False,
1815 loadtest=None, showchannels=False,
1816 *args, **kwargs):
1816 *args, **kwargs):
1817 """Create a new instance that can run tests with a configuration.
1817 """Create a new instance that can run tests with a configuration.
1818
1818
1819 testdir specifies the directory where tests are executed from. This
1819 testdir specifies the directory where tests are executed from. This
1820 is typically the ``tests`` directory from Mercurial's source
1820 is typically the ``tests`` directory from Mercurial's source
1821 repository.
1821 repository.
1822
1822
1823 jobs specifies the number of jobs to run concurrently. Each test
1823 jobs specifies the number of jobs to run concurrently. Each test
1824 executes on its own thread. Tests actually spawn new processes, so
1824 executes on its own thread. Tests actually spawn new processes, so
1825 state mutation should not be an issue.
1825 state mutation should not be an issue.
1826
1826
1827 If there is only one job, it will use the main thread.
1827 If there is only one job, it will use the main thread.
1828
1828
1829 whitelist and blacklist denote tests that have been whitelisted and
1829 whitelist and blacklist denote tests that have been whitelisted and
1830 blacklisted, respectively. These arguments don't belong in TestSuite.
1830 blacklisted, respectively. These arguments don't belong in TestSuite.
1831 Instead, whitelist and blacklist should be handled by the thing that
1831 Instead, whitelist and blacklist should be handled by the thing that
1832 populates the TestSuite with tests. They are present to preserve
1832 populates the TestSuite with tests. They are present to preserve
1833 backwards compatible behavior which reports skipped tests as part
1833 backwards compatible behavior which reports skipped tests as part
1834 of the results.
1834 of the results.
1835
1835
1836 retest denotes whether to retest failed tests. This arguably belongs
1836 retest denotes whether to retest failed tests. This arguably belongs
1837 outside of TestSuite.
1837 outside of TestSuite.
1838
1838
1839 keywords denotes key words that will be used to filter which tests
1839 keywords denotes key words that will be used to filter which tests
1840 to execute. This arguably belongs outside of TestSuite.
1840 to execute. This arguably belongs outside of TestSuite.
1841
1841
1842 loop denotes whether to loop over tests forever.
1842 loop denotes whether to loop over tests forever.
1843 """
1843 """
1844 super(TestSuite, self).__init__(*args, **kwargs)
1844 super(TestSuite, self).__init__(*args, **kwargs)
1845
1845
1846 self._jobs = jobs
1846 self._jobs = jobs
1847 self._whitelist = whitelist
1847 self._whitelist = whitelist
1848 self._blacklist = blacklist
1848 self._blacklist = blacklist
1849 self._retest = retest
1849 self._retest = retest
1850 self._keywords = keywords
1850 self._keywords = keywords
1851 self._loop = loop
1851 self._loop = loop
1852 self._runs_per_test = runs_per_test
1852 self._runs_per_test = runs_per_test
1853 self._loadtest = loadtest
1853 self._loadtest = loadtest
1854 self._showchannels = showchannels
1854 self._showchannels = showchannels
1855
1855
1856 def run(self, result):
1856 def run(self, result):
1857 # We have a number of filters that need to be applied. We do this
1857 # We have a number of filters that need to be applied. We do this
1858 # here instead of inside Test because it makes the running logic for
1858 # here instead of inside Test because it makes the running logic for
1859 # Test simpler.
1859 # Test simpler.
1860 tests = []
1860 tests = []
1861 num_tests = [0]
1861 num_tests = [0]
1862 for test in self._tests:
1862 for test in self._tests:
1863 def get():
1863 def get():
1864 num_tests[0] += 1
1864 num_tests[0] += 1
1865 if getattr(test, 'should_reload', False):
1865 if getattr(test, 'should_reload', False):
1866 return self._loadtest(test, num_tests[0])
1866 return self._loadtest(test, num_tests[0])
1867 return test
1867 return test
1868 if not os.path.exists(test.path):
1868 if not os.path.exists(test.path):
1869 result.addSkip(test, "Doesn't exist")
1869 result.addSkip(test, "Doesn't exist")
1870 continue
1870 continue
1871
1871
1872 if not (self._whitelist and test.bname in self._whitelist):
1872 if not (self._whitelist and test.bname in self._whitelist):
1873 if self._blacklist and test.bname in self._blacklist:
1873 if self._blacklist and test.bname in self._blacklist:
1874 result.addSkip(test, 'blacklisted')
1874 result.addSkip(test, 'blacklisted')
1875 continue
1875 continue
1876
1876
1877 if self._retest and not os.path.exists(test.errpath):
1877 if self._retest and not os.path.exists(test.errpath):
1878 result.addIgnore(test, 'not retesting')
1878 result.addIgnore(test, 'not retesting')
1879 continue
1879 continue
1880
1880
1881 if self._keywords:
1881 if self._keywords:
1882 with open(test.path, 'rb') as f:
1882 with open(test.path, 'rb') as f:
1883 t = f.read().lower() + test.bname.lower()
1883 t = f.read().lower() + test.bname.lower()
1884 ignored = False
1884 ignored = False
1885 for k in self._keywords.lower().split():
1885 for k in self._keywords.lower().split():
1886 if k not in t:
1886 if k not in t:
1887 result.addIgnore(test, "doesn't match keyword")
1887 result.addIgnore(test, "doesn't match keyword")
1888 ignored = True
1888 ignored = True
1889 break
1889 break
1890
1890
1891 if ignored:
1891 if ignored:
1892 continue
1892 continue
1893 for _ in xrange(self._runs_per_test):
1893 for _ in xrange(self._runs_per_test):
1894 tests.append(get())
1894 tests.append(get())
1895
1895
1896 runtests = list(tests)
1896 runtests = list(tests)
1897 done = queue.Queue()
1897 done = queue.Queue()
1898 running = 0
1898 running = 0
1899
1899
1900 channels = [""] * self._jobs
1900 channels = [""] * self._jobs
1901
1901
1902 def job(test, result):
1902 def job(test, result):
1903 for n, v in enumerate(channels):
1903 for n, v in enumerate(channels):
1904 if not v:
1904 if not v:
1905 channel = n
1905 channel = n
1906 break
1906 break
1907 else:
1907 else:
1908 raise ValueError('Could not find output channel')
1908 raise ValueError('Could not find output channel')
1909 channels[channel] = "=" + test.name[5:].split(".")[0]
1909 channels[channel] = "=" + test.name[5:].split(".")[0]
1910 try:
1910 try:
1911 test(result)
1911 test(result)
1912 done.put(None)
1912 done.put(None)
1913 except KeyboardInterrupt:
1913 except KeyboardInterrupt:
1914 pass
1914 pass
1915 except: # re-raises
1915 except: # re-raises
1916 done.put(('!', test, 'run-test raised an error, see traceback'))
1916 done.put(('!', test, 'run-test raised an error, see traceback'))
1917 raise
1917 raise
1918 finally:
1918 finally:
1919 try:
1919 try:
1920 channels[channel] = ''
1920 channels[channel] = ''
1921 except IndexError:
1921 except IndexError:
1922 pass
1922 pass
1923
1923
1924 def stat():
1924 def stat():
1925 count = 0
1925 count = 0
1926 while channels:
1926 while channels:
1927 d = '\n%03s ' % count
1927 d = '\n%03s ' % count
1928 for n, v in enumerate(channels):
1928 for n, v in enumerate(channels):
1929 if v:
1929 if v:
1930 d += v[0]
1930 d += v[0]
1931 channels[n] = v[1:] or '.'
1931 channels[n] = v[1:] or '.'
1932 else:
1932 else:
1933 d += ' '
1933 d += ' '
1934 d += ' '
1934 d += ' '
1935 with iolock:
1935 with iolock:
1936 sys.stdout.write(d + ' ')
1936 sys.stdout.write(d + ' ')
1937 sys.stdout.flush()
1937 sys.stdout.flush()
1938 for x in xrange(10):
1938 for x in xrange(10):
1939 if channels:
1939 if channels:
1940 time.sleep(.1)
1940 time.sleep(.1)
1941 count += 1
1941 count += 1
1942
1942
1943 stoppedearly = False
1943 stoppedearly = False
1944
1944
1945 if self._showchannels:
1945 if self._showchannels:
1946 statthread = threading.Thread(target=stat, name="stat")
1946 statthread = threading.Thread(target=stat, name="stat")
1947 statthread.start()
1947 statthread.start()
1948
1948
1949 try:
1949 try:
1950 while tests or running:
1950 while tests or running:
1951 if not done.empty() or running == self._jobs or not tests:
1951 if not done.empty() or running == self._jobs or not tests:
1952 try:
1952 try:
1953 done.get(True, 1)
1953 done.get(True, 1)
1954 running -= 1
1954 running -= 1
1955 if result and result.shouldStop:
1955 if result and result.shouldStop:
1956 stoppedearly = True
1956 stoppedearly = True
1957 break
1957 break
1958 except queue.Empty:
1958 except queue.Empty:
1959 continue
1959 continue
1960 if tests and not running == self._jobs:
1960 if tests and not running == self._jobs:
1961 test = tests.pop(0)
1961 test = tests.pop(0)
1962 if self._loop:
1962 if self._loop:
1963 if getattr(test, 'should_reload', False):
1963 if getattr(test, 'should_reload', False):
1964 num_tests[0] += 1
1964 num_tests[0] += 1
1965 tests.append(
1965 tests.append(
1966 self._loadtest(test, num_tests[0]))
1966 self._loadtest(test, num_tests[0]))
1967 else:
1967 else:
1968 tests.append(test)
1968 tests.append(test)
1969 if self._jobs == 1:
1969 if self._jobs == 1:
1970 job(test, result)
1970 job(test, result)
1971 else:
1971 else:
1972 t = threading.Thread(target=job, name=test.name,
1972 t = threading.Thread(target=job, name=test.name,
1973 args=(test, result))
1973 args=(test, result))
1974 t.start()
1974 t.start()
1975 running += 1
1975 running += 1
1976
1976
1977 # If we stop early we still need to wait on started tests to
1977 # If we stop early we still need to wait on started tests to
1978 # finish. Otherwise, there is a race between the test completing
1978 # finish. Otherwise, there is a race between the test completing
1979 # and the test's cleanup code running. This could result in the
1979 # and the test's cleanup code running. This could result in the
1980 # test reporting incorrect.
1980 # test reporting incorrect.
1981 if stoppedearly:
1981 if stoppedearly:
1982 while running:
1982 while running:
1983 try:
1983 try:
1984 done.get(True, 1)
1984 done.get(True, 1)
1985 running -= 1
1985 running -= 1
1986 except queue.Empty:
1986 except queue.Empty:
1987 continue
1987 continue
1988 except KeyboardInterrupt:
1988 except KeyboardInterrupt:
1989 for test in runtests:
1989 for test in runtests:
1990 test.abort()
1990 test.abort()
1991
1991
1992 channels = []
1992 channels = []
1993
1993
1994 return result
1994 return result
1995
1995
1996 # Save the most recent 5 wall-clock runtimes of each test to a
1996 # Save the most recent 5 wall-clock runtimes of each test to a
1997 # human-readable text file named .testtimes. Tests are sorted
1997 # human-readable text file named .testtimes. Tests are sorted
1998 # alphabetically, while times for each test are listed from oldest to
1998 # alphabetically, while times for each test are listed from oldest to
1999 # newest.
1999 # newest.
2000
2000
2001 def loadtimes(outputdir):
2001 def loadtimes(outputdir):
2002 times = []
2002 times = []
2003 try:
2003 try:
2004 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
2004 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
2005 for line in fp:
2005 for line in fp:
2006 ts = line.split()
2006 ts = line.split()
2007 times.append((ts[0], [float(t) for t in ts[1:]]))
2007 times.append((ts[0], [float(t) for t in ts[1:]]))
2008 except IOError as err:
2008 except IOError as err:
2009 if err.errno != errno.ENOENT:
2009 if err.errno != errno.ENOENT:
2010 raise
2010 raise
2011 return times
2011 return times
2012
2012
2013 def savetimes(outputdir, result):
2013 def savetimes(outputdir, result):
2014 saved = dict(loadtimes(outputdir))
2014 saved = dict(loadtimes(outputdir))
2015 maxruns = 5
2015 maxruns = 5
2016 skipped = set([str(t[0]) for t in result.skipped])
2016 skipped = set([str(t[0]) for t in result.skipped])
2017 for tdata in result.times:
2017 for tdata in result.times:
2018 test, real = tdata[0], tdata[3]
2018 test, real = tdata[0], tdata[3]
2019 if test not in skipped:
2019 if test not in skipped:
2020 ts = saved.setdefault(test, [])
2020 ts = saved.setdefault(test, [])
2021 ts.append(real)
2021 ts.append(real)
2022 ts[:] = ts[-maxruns:]
2022 ts[:] = ts[-maxruns:]
2023
2023
2024 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2024 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2025 dir=outputdir, text=True)
2025 dir=outputdir, text=True)
2026 with os.fdopen(fd, 'w') as fp:
2026 with os.fdopen(fd, 'w') as fp:
2027 for name, ts in sorted(saved.items()):
2027 for name, ts in sorted(saved.items()):
2028 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2028 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2029 timepath = os.path.join(outputdir, b'.testtimes')
2029 timepath = os.path.join(outputdir, b'.testtimes')
2030 try:
2030 try:
2031 os.unlink(timepath)
2031 os.unlink(timepath)
2032 except OSError:
2032 except OSError:
2033 pass
2033 pass
2034 try:
2034 try:
2035 os.rename(tmpname, timepath)
2035 os.rename(tmpname, timepath)
2036 except OSError:
2036 except OSError:
2037 pass
2037 pass
2038
2038
2039 class TextTestRunner(unittest.TextTestRunner):
2039 class TextTestRunner(unittest.TextTestRunner):
2040 """Custom unittest test runner that uses appropriate settings."""
2040 """Custom unittest test runner that uses appropriate settings."""
2041
2041
2042 def __init__(self, runner, *args, **kwargs):
2042 def __init__(self, runner, *args, **kwargs):
2043 super(TextTestRunner, self).__init__(*args, **kwargs)
2043 super(TextTestRunner, self).__init__(*args, **kwargs)
2044
2044
2045 self._runner = runner
2045 self._runner = runner
2046
2046
2047 def listtests(self, test):
2047 def listtests(self, test):
2048 result = TestResult(self._runner.options, self.stream,
2048 result = TestResult(self._runner.options, self.stream,
2049 self.descriptions, 0)
2049 self.descriptions, 0)
2050 test = sorted(test, key=lambda t: t.name)
2050 test = sorted(test, key=lambda t: t.name)
2051 for t in test:
2051 for t in test:
2052 print(t.name)
2052 print(t.name)
2053 result.addSuccess(t)
2053 result.addSuccess(t)
2054
2054
2055 if self._runner.options.xunit:
2055 if self._runner.options.xunit:
2056 with open(self._runner.options.xunit, "wb") as xuf:
2056 with open(self._runner.options.xunit, "wb") as xuf:
2057 self._writexunit(result, xuf)
2057 self._writexunit(result, xuf)
2058
2058
2059 if self._runner.options.json:
2059 if self._runner.options.json:
2060 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2060 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2061 with open(jsonpath, 'w') as fp:
2061 with open(jsonpath, 'w') as fp:
2062 self._writejson(result, fp)
2062 self._writejson(result, fp)
2063
2063
2064 return result
2064 return result
2065
2065
2066 def run(self, test):
2066 def run(self, test):
2067 result = TestResult(self._runner.options, self.stream,
2067 result = TestResult(self._runner.options, self.stream,
2068 self.descriptions, self.verbosity)
2068 self.descriptions, self.verbosity)
2069
2069
2070 test(result)
2070 test(result)
2071
2071
2072 failed = len(result.failures)
2072 failed = len(result.failures)
2073 skipped = len(result.skipped)
2073 skipped = len(result.skipped)
2074 ignored = len(result.ignored)
2074 ignored = len(result.ignored)
2075
2075
2076 with iolock:
2076 with iolock:
2077 self.stream.writeln('')
2077 self.stream.writeln('')
2078
2078
2079 if not self._runner.options.noskips:
2079 if not self._runner.options.noskips:
2080 for test, msg in result.skipped:
2080 for test, msg in result.skipped:
2081 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2081 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2082 self.stream.write(highlightmsg(formatted, result.color))
2082 self.stream.write(highlightmsg(formatted, result.color))
2083 for test, msg in result.failures:
2083 for test, msg in result.failures:
2084 formatted = 'Failed %s: %s\n' % (test.name, msg)
2084 formatted = 'Failed %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.errors:
2086 for test, msg in result.errors:
2087 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2087 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2088
2088
2089 if self._runner.options.xunit:
2089 if self._runner.options.xunit:
2090 with open(self._runner.options.xunit, "wb") as xuf:
2090 with open(self._runner.options.xunit, "wb") as xuf:
2091 self._writexunit(result, xuf)
2091 self._writexunit(result, xuf)
2092
2092
2093 if self._runner.options.json:
2093 if self._runner.options.json:
2094 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2094 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2095 with open(jsonpath, 'w') as fp:
2095 with open(jsonpath, 'w') as fp:
2096 self._writejson(result, fp)
2096 self._writejson(result, fp)
2097
2097
2098 self._runner._checkhglib('Tested')
2098 self._runner._checkhglib('Tested')
2099
2099
2100 savetimes(self._runner._outputdir, result)
2100 savetimes(self._runner._outputdir, result)
2101
2101
2102 if failed and self._runner.options.known_good_rev:
2102 if failed and self._runner.options.known_good_rev:
2103 self._bisecttests(t for t, m in result.failures)
2103 self._bisecttests(t for t, m in result.failures)
2104 self.stream.writeln(
2104 self.stream.writeln(
2105 '# Ran %d tests, %d skipped, %d failed.'
2105 '# Ran %d tests, %d skipped, %d failed.'
2106 % (result.testsRun, skipped + ignored, failed))
2106 % (result.testsRun, skipped + ignored, failed))
2107 if failed:
2107 if failed:
2108 self.stream.writeln('python hash seed: %s' %
2108 self.stream.writeln('python hash seed: %s' %
2109 os.environ['PYTHONHASHSEED'])
2109 os.environ['PYTHONHASHSEED'])
2110 if self._runner.options.time:
2110 if self._runner.options.time:
2111 self.printtimes(result.times)
2111 self.printtimes(result.times)
2112
2112
2113 if self._runner.options.exceptions:
2113 if self._runner.options.exceptions:
2114 exceptions = aggregateexceptions(
2114 exceptions = aggregateexceptions(
2115 os.path.join(self._runner._outputdir, b'exceptions'))
2115 os.path.join(self._runner._outputdir, b'exceptions'))
2116 total = sum(exceptions.values())
2116 total = sum(exceptions.values())
2117
2117
2118 self.stream.writeln('Exceptions Report:')
2118 self.stream.writeln('Exceptions Report:')
2119 self.stream.writeln('%d total from %d frames' %
2119 self.stream.writeln('%d total from %d frames' %
2120 (total, len(exceptions)))
2120 (total, len(exceptions)))
2121 for (frame, line, exc), count in exceptions.most_common():
2121 for (frame, line, exc), count in exceptions.most_common():
2122 self.stream.writeln('%d\t%s: %s' % (count, frame, exc))
2122 self.stream.writeln('%d\t%s: %s' % (count, frame, exc))
2123
2123
2124 self.stream.flush()
2124 self.stream.flush()
2125
2125
2126 return result
2126 return result
2127
2127
2128 def _bisecttests(self, tests):
2128 def _bisecttests(self, tests):
2129 bisectcmd = ['hg', 'bisect']
2129 bisectcmd = ['hg', 'bisect']
2130 bisectrepo = self._runner.options.bisect_repo
2130 bisectrepo = self._runner.options.bisect_repo
2131 if bisectrepo:
2131 if bisectrepo:
2132 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2132 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2133 def pread(args):
2133 def pread(args):
2134 env = os.environ.copy()
2134 env = os.environ.copy()
2135 env['HGPLAIN'] = '1'
2135 env['HGPLAIN'] = '1'
2136 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2136 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2137 stdout=subprocess.PIPE, env=env)
2137 stdout=subprocess.PIPE, env=env)
2138 data = p.stdout.read()
2138 data = p.stdout.read()
2139 p.wait()
2139 p.wait()
2140 return data
2140 return data
2141 for test in tests:
2141 for test in tests:
2142 pread(bisectcmd + ['--reset']),
2142 pread(bisectcmd + ['--reset']),
2143 pread(bisectcmd + ['--bad', '.'])
2143 pread(bisectcmd + ['--bad', '.'])
2144 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2144 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2145 # TODO: we probably need to forward more options
2145 # TODO: we probably need to forward more options
2146 # that alter hg's behavior inside the tests.
2146 # that alter hg's behavior inside the tests.
2147 opts = ''
2147 opts = ''
2148 withhg = self._runner.options.with_hg
2148 withhg = self._runner.options.with_hg
2149 if withhg:
2149 if withhg:
2150 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2150 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2151 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2151 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2152 test)
2152 test)
2153 data = pread(bisectcmd + ['--command', rtc])
2153 data = pread(bisectcmd + ['--command', rtc])
2154 m = re.search(
2154 m = re.search(
2155 (br'\nThe first (?P<goodbad>bad|good) revision '
2155 (br'\nThe first (?P<goodbad>bad|good) revision '
2156 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2156 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2157 br'summary: +(?P<summary>[^\n]+)\n'),
2157 br'summary: +(?P<summary>[^\n]+)\n'),
2158 data, (re.MULTILINE | re.DOTALL))
2158 data, (re.MULTILINE | re.DOTALL))
2159 if m is None:
2159 if m is None:
2160 self.stream.writeln(
2160 self.stream.writeln(
2161 'Failed to identify failure point for %s' % test)
2161 'Failed to identify failure point for %s' % test)
2162 continue
2162 continue
2163 dat = m.groupdict()
2163 dat = m.groupdict()
2164 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2164 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2165 self.stream.writeln(
2165 self.stream.writeln(
2166 '%s %s by %s (%s)' % (
2166 '%s %s by %s (%s)' % (
2167 test, verb, dat['node'], dat['summary']))
2167 test, verb, dat['node'], dat['summary']))
2168
2168
2169 def printtimes(self, times):
2169 def printtimes(self, times):
2170 # iolock held by run
2170 # iolock held by run
2171 self.stream.writeln('# Producing time report')
2171 self.stream.writeln('# Producing time report')
2172 times.sort(key=lambda t: (t[3]))
2172 times.sort(key=lambda t: (t[3]))
2173 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2173 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2174 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2174 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2175 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2175 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2176 for tdata in times:
2176 for tdata in times:
2177 test = tdata[0]
2177 test = tdata[0]
2178 cuser, csys, real, start, end = tdata[1:6]
2178 cuser, csys, real, start, end = tdata[1:6]
2179 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2179 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2180
2180
2181 @staticmethod
2181 @staticmethod
2182 def _writexunit(result, outf):
2182 def _writexunit(result, outf):
2183 # See http://llg.cubic.org/docs/junit/ for a reference.
2183 # See http://llg.cubic.org/docs/junit/ for a reference.
2184 timesd = dict((t[0], t[3]) for t in result.times)
2184 timesd = dict((t[0], t[3]) for t in result.times)
2185 doc = minidom.Document()
2185 doc = minidom.Document()
2186 s = doc.createElement('testsuite')
2186 s = doc.createElement('testsuite')
2187 s.setAttribute('name', 'run-tests')
2187 s.setAttribute('name', 'run-tests')
2188 s.setAttribute('tests', str(result.testsRun))
2188 s.setAttribute('tests', str(result.testsRun))
2189 s.setAttribute('errors', "0") # TODO
2189 s.setAttribute('errors', "0") # TODO
2190 s.setAttribute('failures', str(len(result.failures)))
2190 s.setAttribute('failures', str(len(result.failures)))
2191 s.setAttribute('skipped', str(len(result.skipped) +
2191 s.setAttribute('skipped', str(len(result.skipped) +
2192 len(result.ignored)))
2192 len(result.ignored)))
2193 doc.appendChild(s)
2193 doc.appendChild(s)
2194 for tc in result.successes:
2194 for tc in result.successes:
2195 t = doc.createElement('testcase')
2195 t = doc.createElement('testcase')
2196 t.setAttribute('name', tc.name)
2196 t.setAttribute('name', tc.name)
2197 tctime = timesd.get(tc.name)
2197 tctime = timesd.get(tc.name)
2198 if tctime is not None:
2198 if tctime is not None:
2199 t.setAttribute('time', '%.3f' % tctime)
2199 t.setAttribute('time', '%.3f' % tctime)
2200 s.appendChild(t)
2200 s.appendChild(t)
2201 for tc, err in sorted(result.faildata.items()):
2201 for tc, err in sorted(result.faildata.items()):
2202 t = doc.createElement('testcase')
2202 t = doc.createElement('testcase')
2203 t.setAttribute('name', tc)
2203 t.setAttribute('name', tc)
2204 tctime = timesd.get(tc)
2204 tctime = timesd.get(tc)
2205 if tctime is not None:
2205 if tctime is not None:
2206 t.setAttribute('time', '%.3f' % tctime)
2206 t.setAttribute('time', '%.3f' % tctime)
2207 # createCDATASection expects a unicode or it will
2207 # createCDATASection expects a unicode or it will
2208 # convert using default conversion rules, which will
2208 # convert using default conversion rules, which will
2209 # fail if string isn't ASCII.
2209 # fail if string isn't ASCII.
2210 err = cdatasafe(err).decode('utf-8', 'replace')
2210 err = cdatasafe(err).decode('utf-8', 'replace')
2211 cd = doc.createCDATASection(err)
2211 cd = doc.createCDATASection(err)
2212 # Use 'failure' here instead of 'error' to match errors = 0,
2212 # Use 'failure' here instead of 'error' to match errors = 0,
2213 # failures = len(result.failures) in the testsuite element.
2213 # failures = len(result.failures) in the testsuite element.
2214 failelem = doc.createElement('failure')
2214 failelem = doc.createElement('failure')
2215 failelem.setAttribute('message', 'output changed')
2215 failelem.setAttribute('message', 'output changed')
2216 failelem.setAttribute('type', 'output-mismatch')
2216 failelem.setAttribute('type', 'output-mismatch')
2217 failelem.appendChild(cd)
2217 failelem.appendChild(cd)
2218 t.appendChild(failelem)
2218 t.appendChild(failelem)
2219 s.appendChild(t)
2219 s.appendChild(t)
2220 for tc, message in result.skipped:
2220 for tc, message in result.skipped:
2221 # According to the schema, 'skipped' has no attributes. So store
2221 # According to the schema, 'skipped' has no attributes. So store
2222 # the skip message as a text node instead.
2222 # the skip message as a text node instead.
2223 t = doc.createElement('testcase')
2223 t = doc.createElement('testcase')
2224 t.setAttribute('name', tc.name)
2224 t.setAttribute('name', tc.name)
2225 binmessage = message.encode('utf-8')
2225 binmessage = message.encode('utf-8')
2226 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2226 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2227 cd = doc.createCDATASection(message)
2227 cd = doc.createCDATASection(message)
2228 skipelem = doc.createElement('skipped')
2228 skipelem = doc.createElement('skipped')
2229 skipelem.appendChild(cd)
2229 skipelem.appendChild(cd)
2230 t.appendChild(skipelem)
2230 t.appendChild(skipelem)
2231 s.appendChild(t)
2231 s.appendChild(t)
2232 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2232 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2233
2233
2234 @staticmethod
2234 @staticmethod
2235 def _writejson(result, outf):
2235 def _writejson(result, outf):
2236 timesd = {}
2236 timesd = {}
2237 for tdata in result.times:
2237 for tdata in result.times:
2238 test = tdata[0]
2238 test = tdata[0]
2239 timesd[test] = tdata[1:]
2239 timesd[test] = tdata[1:]
2240
2240
2241 outcome = {}
2241 outcome = {}
2242 groups = [('success', ((tc, None)
2242 groups = [('success', ((tc, None)
2243 for tc in result.successes)),
2243 for tc in result.successes)),
2244 ('failure', result.failures),
2244 ('failure', result.failures),
2245 ('skip', result.skipped)]
2245 ('skip', result.skipped)]
2246 for res, testcases in groups:
2246 for res, testcases in groups:
2247 for tc, __ in testcases:
2247 for tc, __ in testcases:
2248 if tc.name in timesd:
2248 if tc.name in timesd:
2249 diff = result.faildata.get(tc.name, b'')
2249 diff = result.faildata.get(tc.name, b'')
2250 try:
2250 try:
2251 diff = diff.decode('unicode_escape')
2251 diff = diff.decode('unicode_escape')
2252 except UnicodeDecodeError as e:
2252 except UnicodeDecodeError as e:
2253 diff = '%r decoding diff, sorry' % e
2253 diff = '%r decoding diff, sorry' % e
2254 tres = {'result': res,
2254 tres = {'result': res,
2255 'time': ('%0.3f' % timesd[tc.name][2]),
2255 'time': ('%0.3f' % timesd[tc.name][2]),
2256 'cuser': ('%0.3f' % timesd[tc.name][0]),
2256 'cuser': ('%0.3f' % timesd[tc.name][0]),
2257 'csys': ('%0.3f' % timesd[tc.name][1]),
2257 'csys': ('%0.3f' % timesd[tc.name][1]),
2258 'start': ('%0.3f' % timesd[tc.name][3]),
2258 'start': ('%0.3f' % timesd[tc.name][3]),
2259 'end': ('%0.3f' % timesd[tc.name][4]),
2259 'end': ('%0.3f' % timesd[tc.name][4]),
2260 'diff': diff,
2260 'diff': diff,
2261 }
2261 }
2262 else:
2262 else:
2263 # blacklisted test
2263 # blacklisted test
2264 tres = {'result': res}
2264 tres = {'result': res}
2265
2265
2266 outcome[tc.name] = tres
2266 outcome[tc.name] = tres
2267 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2267 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2268 separators=(',', ': '))
2268 separators=(',', ': '))
2269 outf.writelines(("testreport =", jsonout))
2269 outf.writelines(("testreport =", jsonout))
2270
2270
2271 def sorttests(testdescs, shuffle=False):
2272 """Do an in-place sort of tests."""
2273 if shuffle:
2274 random.shuffle(testdescs)
2275 return
2276
2277 # keywords for slow tests
2278 slow = {b'svn': 10,
2279 b'cvs': 10,
2280 b'hghave': 10,
2281 b'largefiles-update': 10,
2282 b'run-tests': 10,
2283 b'corruption': 10,
2284 b'race': 10,
2285 b'i18n': 10,
2286 b'check': 100,
2287 b'gendoc': 100,
2288 b'contrib-perf': 200,
2289 }
2290 perf = {}
2291
2292 def sortkey(f):
2293 # run largest tests first, as they tend to take the longest
2294 f = f['path']
2295 try:
2296 return perf[f]
2297 except KeyError:
2298 try:
2299 val = -os.stat(f).st_size
2300 except OSError as e:
2301 if e.errno != errno.ENOENT:
2302 raise
2303 perf[f] = -1e9 # file does not exist, tell early
2304 return -1e9
2305 for kw, mul in slow.items():
2306 if kw in f:
2307 val *= mul
2308 if f.endswith(b'.py'):
2309 val /= 10.0
2310 perf[f] = val / 1000.0
2311 return perf[f]
2312
2313 testdescs.sort(key=sortkey)
2314
2271 class TestRunner(object):
2315 class TestRunner(object):
2272 """Holds context for executing tests.
2316 """Holds context for executing tests.
2273
2317
2274 Tests rely on a lot of state. This object holds it for them.
2318 Tests rely on a lot of state. This object holds it for them.
2275 """
2319 """
2276
2320
2277 # Programs required to run tests.
2321 # Programs required to run tests.
2278 REQUIREDTOOLS = [
2322 REQUIREDTOOLS = [
2279 b'diff',
2323 b'diff',
2280 b'grep',
2324 b'grep',
2281 b'unzip',
2325 b'unzip',
2282 b'gunzip',
2326 b'gunzip',
2283 b'bunzip2',
2327 b'bunzip2',
2284 b'sed',
2328 b'sed',
2285 ]
2329 ]
2286
2330
2287 # Maps file extensions to test class.
2331 # Maps file extensions to test class.
2288 TESTTYPES = [
2332 TESTTYPES = [
2289 (b'.py', PythonTest),
2333 (b'.py', PythonTest),
2290 (b'.t', TTest),
2334 (b'.t', TTest),
2291 ]
2335 ]
2292
2336
2293 def __init__(self):
2337 def __init__(self):
2294 self.options = None
2338 self.options = None
2295 self._hgroot = None
2339 self._hgroot = None
2296 self._testdir = None
2340 self._testdir = None
2297 self._outputdir = None
2341 self._outputdir = None
2298 self._hgtmp = None
2342 self._hgtmp = None
2299 self._installdir = None
2343 self._installdir = None
2300 self._bindir = None
2344 self._bindir = None
2301 self._tmpbinddir = None
2345 self._tmpbinddir = None
2302 self._pythondir = None
2346 self._pythondir = None
2303 self._coveragefile = None
2347 self._coveragefile = None
2304 self._createdfiles = []
2348 self._createdfiles = []
2305 self._hgcommand = None
2349 self._hgcommand = None
2306 self._hgpath = None
2350 self._hgpath = None
2307 self._portoffset = 0
2351 self._portoffset = 0
2308 self._ports = {}
2352 self._ports = {}
2309
2353
2310 def run(self, args, parser=None):
2354 def run(self, args, parser=None):
2311 """Run the test suite."""
2355 """Run the test suite."""
2312 oldmask = os.umask(0o22)
2356 oldmask = os.umask(0o22)
2313 try:
2357 try:
2314 parser = parser or getparser()
2358 parser = parser or getparser()
2315 options = parseargs(args, parser)
2359 options = parseargs(args, parser)
2316 tests = [_bytespath(a) for a in options.tests]
2360 tests = [_bytespath(a) for a in options.tests]
2317 if options.test_list is not None:
2361 if options.test_list is not None:
2318 for listfile in options.test_list:
2362 for listfile in options.test_list:
2319 with open(listfile, 'rb') as f:
2363 with open(listfile, 'rb') as f:
2320 tests.extend(t for t in f.read().splitlines() if t)
2364 tests.extend(t for t in f.read().splitlines() if t)
2321 self.options = options
2365 self.options = options
2322
2366
2323 self._checktools()
2367 self._checktools()
2324 testdescs = self.findtests(tests)
2368 testdescs = self.findtests(tests)
2325 if options.profile_runner:
2369 if options.profile_runner:
2326 import statprof
2370 import statprof
2327 statprof.start()
2371 statprof.start()
2328 result = self._run(testdescs)
2372 result = self._run(testdescs)
2329 if options.profile_runner:
2373 if options.profile_runner:
2330 statprof.stop()
2374 statprof.stop()
2331 statprof.display()
2375 statprof.display()
2332 return result
2376 return result
2333
2377
2334 finally:
2378 finally:
2335 os.umask(oldmask)
2379 os.umask(oldmask)
2336
2380
2337 def _run(self, testdescs):
2381 def _run(self, testdescs):
2338 if self.options.random:
2382 sorttests(testdescs, shuffle=self.options.random)
2339 random.shuffle(testdescs)
2340 else:
2341 # keywords for slow tests
2342 slow = {b'svn': 10,
2343 b'cvs': 10,
2344 b'hghave': 10,
2345 b'largefiles-update': 10,
2346 b'run-tests': 10,
2347 b'corruption': 10,
2348 b'race': 10,
2349 b'i18n': 10,
2350 b'check': 100,
2351 b'gendoc': 100,
2352 b'contrib-perf': 200,
2353 }
2354 perf = {}
2355 def sortkey(f):
2356 # run largest tests first, as they tend to take the longest
2357 f = f['path']
2358 try:
2359 return perf[f]
2360 except KeyError:
2361 try:
2362 val = -os.stat(f).st_size
2363 except OSError as e:
2364 if e.errno != errno.ENOENT:
2365 raise
2366 perf[f] = -1e9 # file does not exist, tell early
2367 return -1e9
2368 for kw, mul in slow.items():
2369 if kw in f:
2370 val *= mul
2371 if f.endswith(b'.py'):
2372 val /= 10.0
2373 perf[f] = val / 1000.0
2374 return perf[f]
2375 testdescs.sort(key=sortkey)
2376
2383
2377 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2384 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2378 os, 'getcwdb', os.getcwd)()
2385 os, 'getcwdb', os.getcwd)()
2379 # assume all tests in same folder for now
2386 # assume all tests in same folder for now
2380 if testdescs:
2387 if testdescs:
2381 pathname = os.path.dirname(testdescs[0]['path'])
2388 pathname = os.path.dirname(testdescs[0]['path'])
2382 if pathname:
2389 if pathname:
2383 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2390 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2384 pathname)
2391 pathname)
2385 if self.options.outputdir:
2392 if self.options.outputdir:
2386 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2393 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2387 else:
2394 else:
2388 self._outputdir = self._testdir
2395 self._outputdir = self._testdir
2389 if testdescs and pathname:
2396 if testdescs and pathname:
2390 self._outputdir = os.path.join(self._outputdir, pathname)
2397 self._outputdir = os.path.join(self._outputdir, pathname)
2391
2398
2392 if 'PYTHONHASHSEED' not in os.environ:
2399 if 'PYTHONHASHSEED' not in os.environ:
2393 # use a random python hash seed all the time
2400 # use a random python hash seed all the time
2394 # we do the randomness ourself to know what seed is used
2401 # we do the randomness ourself to know what seed is used
2395 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2402 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2396
2403
2397 if self.options.tmpdir:
2404 if self.options.tmpdir:
2398 self.options.keep_tmpdir = True
2405 self.options.keep_tmpdir = True
2399 tmpdir = _bytespath(self.options.tmpdir)
2406 tmpdir = _bytespath(self.options.tmpdir)
2400 if os.path.exists(tmpdir):
2407 if os.path.exists(tmpdir):
2401 # Meaning of tmpdir has changed since 1.3: we used to create
2408 # Meaning of tmpdir has changed since 1.3: we used to create
2402 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2409 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2403 # tmpdir already exists.
2410 # tmpdir already exists.
2404 print("error: temp dir %r already exists" % tmpdir)
2411 print("error: temp dir %r already exists" % tmpdir)
2405 return 1
2412 return 1
2406
2413
2407 os.makedirs(tmpdir)
2414 os.makedirs(tmpdir)
2408 else:
2415 else:
2409 d = None
2416 d = None
2410 if os.name == 'nt':
2417 if os.name == 'nt':
2411 # without this, we get the default temp dir location, but
2418 # without this, we get the default temp dir location, but
2412 # in all lowercase, which causes troubles with paths (issue3490)
2419 # in all lowercase, which causes troubles with paths (issue3490)
2413 d = osenvironb.get(b'TMP', None)
2420 d = osenvironb.get(b'TMP', None)
2414 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2421 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2415
2422
2416 self._hgtmp = osenvironb[b'HGTMP'] = (
2423 self._hgtmp = osenvironb[b'HGTMP'] = (
2417 os.path.realpath(tmpdir))
2424 os.path.realpath(tmpdir))
2418
2425
2419 if self.options.with_hg:
2426 if self.options.with_hg:
2420 self._installdir = None
2427 self._installdir = None
2421 whg = self.options.with_hg
2428 whg = self.options.with_hg
2422 self._bindir = os.path.dirname(os.path.realpath(whg))
2429 self._bindir = os.path.dirname(os.path.realpath(whg))
2423 assert isinstance(self._bindir, bytes)
2430 assert isinstance(self._bindir, bytes)
2424 self._hgcommand = os.path.basename(whg)
2431 self._hgcommand = os.path.basename(whg)
2425 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2432 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2426 os.makedirs(self._tmpbindir)
2433 os.makedirs(self._tmpbindir)
2427
2434
2428 # This looks redundant with how Python initializes sys.path from
2435 # This looks redundant with how Python initializes sys.path from
2429 # the location of the script being executed. Needed because the
2436 # the location of the script being executed. Needed because the
2430 # "hg" specified by --with-hg is not the only Python script
2437 # "hg" specified by --with-hg is not the only Python script
2431 # executed in the test suite that needs to import 'mercurial'
2438 # executed in the test suite that needs to import 'mercurial'
2432 # ... which means it's not really redundant at all.
2439 # ... which means it's not really redundant at all.
2433 self._pythondir = self._bindir
2440 self._pythondir = self._bindir
2434 else:
2441 else:
2435 self._installdir = os.path.join(self._hgtmp, b"install")
2442 self._installdir = os.path.join(self._hgtmp, b"install")
2436 self._bindir = os.path.join(self._installdir, b"bin")
2443 self._bindir = os.path.join(self._installdir, b"bin")
2437 self._hgcommand = b'hg'
2444 self._hgcommand = b'hg'
2438 self._tmpbindir = self._bindir
2445 self._tmpbindir = self._bindir
2439 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2446 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2440
2447
2441 # set CHGHG, then replace "hg" command by "chg"
2448 # set CHGHG, then replace "hg" command by "chg"
2442 chgbindir = self._bindir
2449 chgbindir = self._bindir
2443 if self.options.chg or self.options.with_chg:
2450 if self.options.chg or self.options.with_chg:
2444 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2451 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2445 else:
2452 else:
2446 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2453 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2447 if self.options.chg:
2454 if self.options.chg:
2448 self._hgcommand = b'chg'
2455 self._hgcommand = b'chg'
2449 elif self.options.with_chg:
2456 elif self.options.with_chg:
2450 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2457 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2451 self._hgcommand = os.path.basename(self.options.with_chg)
2458 self._hgcommand = os.path.basename(self.options.with_chg)
2452
2459
2453 osenvironb[b"BINDIR"] = self._bindir
2460 osenvironb[b"BINDIR"] = self._bindir
2454 osenvironb[b"PYTHON"] = PYTHON
2461 osenvironb[b"PYTHON"] = PYTHON
2455
2462
2456 if self.options.with_python3:
2463 if self.options.with_python3:
2457 osenvironb[b'PYTHON3'] = self.options.with_python3
2464 osenvironb[b'PYTHON3'] = self.options.with_python3
2458
2465
2459 fileb = _bytespath(__file__)
2466 fileb = _bytespath(__file__)
2460 runtestdir = os.path.abspath(os.path.dirname(fileb))
2467 runtestdir = os.path.abspath(os.path.dirname(fileb))
2461 osenvironb[b'RUNTESTDIR'] = runtestdir
2468 osenvironb[b'RUNTESTDIR'] = runtestdir
2462 if PYTHON3:
2469 if PYTHON3:
2463 sepb = _bytespath(os.pathsep)
2470 sepb = _bytespath(os.pathsep)
2464 else:
2471 else:
2465 sepb = os.pathsep
2472 sepb = os.pathsep
2466 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2473 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2467 if os.path.islink(__file__):
2474 if os.path.islink(__file__):
2468 # test helper will likely be at the end of the symlink
2475 # test helper will likely be at the end of the symlink
2469 realfile = os.path.realpath(fileb)
2476 realfile = os.path.realpath(fileb)
2470 realdir = os.path.abspath(os.path.dirname(realfile))
2477 realdir = os.path.abspath(os.path.dirname(realfile))
2471 path.insert(2, realdir)
2478 path.insert(2, realdir)
2472 if chgbindir != self._bindir:
2479 if chgbindir != self._bindir:
2473 path.insert(1, chgbindir)
2480 path.insert(1, chgbindir)
2474 if self._testdir != runtestdir:
2481 if self._testdir != runtestdir:
2475 path = [self._testdir] + path
2482 path = [self._testdir] + path
2476 if self._tmpbindir != self._bindir:
2483 if self._tmpbindir != self._bindir:
2477 path = [self._tmpbindir] + path
2484 path = [self._tmpbindir] + path
2478 osenvironb[b"PATH"] = sepb.join(path)
2485 osenvironb[b"PATH"] = sepb.join(path)
2479
2486
2480 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2487 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2481 # can run .../tests/run-tests.py test-foo where test-foo
2488 # can run .../tests/run-tests.py test-foo where test-foo
2482 # adds an extension to HGRC. Also include run-test.py directory to
2489 # adds an extension to HGRC. Also include run-test.py directory to
2483 # import modules like heredoctest.
2490 # import modules like heredoctest.
2484 pypath = [self._pythondir, self._testdir, runtestdir]
2491 pypath = [self._pythondir, self._testdir, runtestdir]
2485 # We have to augment PYTHONPATH, rather than simply replacing
2492 # We have to augment PYTHONPATH, rather than simply replacing
2486 # it, in case external libraries are only available via current
2493 # it, in case external libraries are only available via current
2487 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2494 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2488 # are in /opt/subversion.)
2495 # are in /opt/subversion.)
2489 oldpypath = osenvironb.get(IMPL_PATH)
2496 oldpypath = osenvironb.get(IMPL_PATH)
2490 if oldpypath:
2497 if oldpypath:
2491 pypath.append(oldpypath)
2498 pypath.append(oldpypath)
2492 osenvironb[IMPL_PATH] = sepb.join(pypath)
2499 osenvironb[IMPL_PATH] = sepb.join(pypath)
2493
2500
2494 if self.options.pure:
2501 if self.options.pure:
2495 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2502 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2496 os.environ["HGMODULEPOLICY"] = "py"
2503 os.environ["HGMODULEPOLICY"] = "py"
2497
2504
2498 if self.options.allow_slow_tests:
2505 if self.options.allow_slow_tests:
2499 os.environ["HGTEST_SLOW"] = "slow"
2506 os.environ["HGTEST_SLOW"] = "slow"
2500 elif 'HGTEST_SLOW' in os.environ:
2507 elif 'HGTEST_SLOW' in os.environ:
2501 del os.environ['HGTEST_SLOW']
2508 del os.environ['HGTEST_SLOW']
2502
2509
2503 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2510 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2504
2511
2505 if self.options.exceptions:
2512 if self.options.exceptions:
2506 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2513 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2507 try:
2514 try:
2508 os.makedirs(exceptionsdir)
2515 os.makedirs(exceptionsdir)
2509 except OSError as e:
2516 except OSError as e:
2510 if e.errno != errno.EEXIST:
2517 if e.errno != errno.EEXIST:
2511 raise
2518 raise
2512
2519
2513 # Remove all existing exception reports.
2520 # Remove all existing exception reports.
2514 for f in os.listdir(exceptionsdir):
2521 for f in os.listdir(exceptionsdir):
2515 os.unlink(os.path.join(exceptionsdir, f))
2522 os.unlink(os.path.join(exceptionsdir, f))
2516
2523
2517 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2524 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2518 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2525 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2519 self.options.extra_config_opt.append(
2526 self.options.extra_config_opt.append(
2520 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2527 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2521
2528
2522 vlog("# Using TESTDIR", self._testdir)
2529 vlog("# Using TESTDIR", self._testdir)
2523 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2530 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2524 vlog("# Using HGTMP", self._hgtmp)
2531 vlog("# Using HGTMP", self._hgtmp)
2525 vlog("# Using PATH", os.environ["PATH"])
2532 vlog("# Using PATH", os.environ["PATH"])
2526 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2533 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2527 vlog("# Writing to directory", self._outputdir)
2534 vlog("# Writing to directory", self._outputdir)
2528
2535
2529 try:
2536 try:
2530 return self._runtests(testdescs) or 0
2537 return self._runtests(testdescs) or 0
2531 finally:
2538 finally:
2532 time.sleep(.1)
2539 time.sleep(.1)
2533 self._cleanup()
2540 self._cleanup()
2534
2541
2535 def findtests(self, args):
2542 def findtests(self, args):
2536 """Finds possible test files from arguments.
2543 """Finds possible test files from arguments.
2537
2544
2538 If you wish to inject custom tests into the test harness, this would
2545 If you wish to inject custom tests into the test harness, this would
2539 be a good function to monkeypatch or override in a derived class.
2546 be a good function to monkeypatch or override in a derived class.
2540 """
2547 """
2541 if not args:
2548 if not args:
2542 if self.options.changed:
2549 if self.options.changed:
2543 proc = Popen4('hg st --rev "%s" -man0 .' %
2550 proc = Popen4('hg st --rev "%s" -man0 .' %
2544 self.options.changed, None, 0)
2551 self.options.changed, None, 0)
2545 stdout, stderr = proc.communicate()
2552 stdout, stderr = proc.communicate()
2546 args = stdout.strip(b'\0').split(b'\0')
2553 args = stdout.strip(b'\0').split(b'\0')
2547 else:
2554 else:
2548 args = os.listdir(b'.')
2555 args = os.listdir(b'.')
2549
2556
2550 expanded_args = []
2557 expanded_args = []
2551 for arg in args:
2558 for arg in args:
2552 if os.path.isdir(arg):
2559 if os.path.isdir(arg):
2553 if not arg.endswith(b'/'):
2560 if not arg.endswith(b'/'):
2554 arg += b'/'
2561 arg += b'/'
2555 expanded_args.extend([arg + a for a in os.listdir(arg)])
2562 expanded_args.extend([arg + a for a in os.listdir(arg)])
2556 else:
2563 else:
2557 expanded_args.append(arg)
2564 expanded_args.append(arg)
2558 args = expanded_args
2565 args = expanded_args
2559
2566
2560 tests = []
2567 tests = []
2561 for t in args:
2568 for t in args:
2562 if not (os.path.basename(t).startswith(b'test-')
2569 if not (os.path.basename(t).startswith(b'test-')
2563 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2570 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2564 continue
2571 continue
2565 if t.endswith(b'.t'):
2572 if t.endswith(b'.t'):
2566 # .t file may contain multiple test cases
2573 # .t file may contain multiple test cases
2567 cases = sorted(parsettestcases(t))
2574 cases = sorted(parsettestcases(t))
2568 if cases:
2575 if cases:
2569 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2576 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2570 else:
2577 else:
2571 tests.append({'path': t})
2578 tests.append({'path': t})
2572 else:
2579 else:
2573 tests.append({'path': t})
2580 tests.append({'path': t})
2574 return tests
2581 return tests
2575
2582
2576 def _runtests(self, testdescs):
2583 def _runtests(self, testdescs):
2577 def _reloadtest(test, i):
2584 def _reloadtest(test, i):
2578 # convert a test back to its description dict
2585 # convert a test back to its description dict
2579 desc = {'path': test.path}
2586 desc = {'path': test.path}
2580 case = getattr(test, '_case', None)
2587 case = getattr(test, '_case', None)
2581 if case:
2588 if case:
2582 desc['case'] = case
2589 desc['case'] = case
2583 return self._gettest(desc, i)
2590 return self._gettest(desc, i)
2584
2591
2585 try:
2592 try:
2586 if self.options.restart:
2593 if self.options.restart:
2587 orig = list(testdescs)
2594 orig = list(testdescs)
2588 while testdescs:
2595 while testdescs:
2589 desc = testdescs[0]
2596 desc = testdescs[0]
2590 # desc['path'] is a relative path
2597 # desc['path'] is a relative path
2591 if 'case' in desc:
2598 if 'case' in desc:
2592 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2599 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2593 else:
2600 else:
2594 errpath = b'%s.err' % desc['path']
2601 errpath = b'%s.err' % desc['path']
2595 errpath = os.path.join(self._outputdir, errpath)
2602 errpath = os.path.join(self._outputdir, errpath)
2596 if os.path.exists(errpath):
2603 if os.path.exists(errpath):
2597 break
2604 break
2598 testdescs.pop(0)
2605 testdescs.pop(0)
2599 if not testdescs:
2606 if not testdescs:
2600 print("running all tests")
2607 print("running all tests")
2601 testdescs = orig
2608 testdescs = orig
2602
2609
2603 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2610 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2604
2611
2605 failed = False
2612 failed = False
2606 kws = self.options.keywords
2613 kws = self.options.keywords
2607 if kws is not None and PYTHON3:
2614 if kws is not None and PYTHON3:
2608 kws = kws.encode('utf-8')
2615 kws = kws.encode('utf-8')
2609
2616
2610 suite = TestSuite(self._testdir,
2617 suite = TestSuite(self._testdir,
2611 jobs=self.options.jobs,
2618 jobs=self.options.jobs,
2612 whitelist=self.options.whitelisted,
2619 whitelist=self.options.whitelisted,
2613 blacklist=self.options.blacklist,
2620 blacklist=self.options.blacklist,
2614 retest=self.options.retest,
2621 retest=self.options.retest,
2615 keywords=kws,
2622 keywords=kws,
2616 loop=self.options.loop,
2623 loop=self.options.loop,
2617 runs_per_test=self.options.runs_per_test,
2624 runs_per_test=self.options.runs_per_test,
2618 showchannels=self.options.showchannels,
2625 showchannels=self.options.showchannels,
2619 tests=tests, loadtest=_reloadtest)
2626 tests=tests, loadtest=_reloadtest)
2620 verbosity = 1
2627 verbosity = 1
2621 if self.options.verbose:
2628 if self.options.verbose:
2622 verbosity = 2
2629 verbosity = 2
2623 runner = TextTestRunner(self, verbosity=verbosity)
2630 runner = TextTestRunner(self, verbosity=verbosity)
2624
2631
2625 if self.options.list_tests:
2632 if self.options.list_tests:
2626 result = runner.listtests(suite)
2633 result = runner.listtests(suite)
2627 else:
2634 else:
2628 if self._installdir:
2635 if self._installdir:
2629 self._installhg()
2636 self._installhg()
2630 self._checkhglib("Testing")
2637 self._checkhglib("Testing")
2631 else:
2638 else:
2632 self._usecorrectpython()
2639 self._usecorrectpython()
2633 if self.options.chg:
2640 if self.options.chg:
2634 assert self._installdir
2641 assert self._installdir
2635 self._installchg()
2642 self._installchg()
2636
2643
2637 result = runner.run(suite)
2644 result = runner.run(suite)
2638
2645
2639 if result.failures:
2646 if result.failures:
2640 failed = True
2647 failed = True
2641
2648
2642 if self.options.anycoverage:
2649 if self.options.anycoverage:
2643 self._outputcoverage()
2650 self._outputcoverage()
2644 except KeyboardInterrupt:
2651 except KeyboardInterrupt:
2645 failed = True
2652 failed = True
2646 print("\ninterrupted!")
2653 print("\ninterrupted!")
2647
2654
2648 if failed:
2655 if failed:
2649 return 1
2656 return 1
2650
2657
2651 def _getport(self, count):
2658 def _getport(self, count):
2652 port = self._ports.get(count) # do we have a cached entry?
2659 port = self._ports.get(count) # do we have a cached entry?
2653 if port is None:
2660 if port is None:
2654 portneeded = 3
2661 portneeded = 3
2655 # above 100 tries we just give up and let test reports failure
2662 # above 100 tries we just give up and let test reports failure
2656 for tries in xrange(100):
2663 for tries in xrange(100):
2657 allfree = True
2664 allfree = True
2658 port = self.options.port + self._portoffset
2665 port = self.options.port + self._portoffset
2659 for idx in xrange(portneeded):
2666 for idx in xrange(portneeded):
2660 if not checkportisavailable(port + idx):
2667 if not checkportisavailable(port + idx):
2661 allfree = False
2668 allfree = False
2662 break
2669 break
2663 self._portoffset += portneeded
2670 self._portoffset += portneeded
2664 if allfree:
2671 if allfree:
2665 break
2672 break
2666 self._ports[count] = port
2673 self._ports[count] = port
2667 return port
2674 return port
2668
2675
2669 def _gettest(self, testdesc, count):
2676 def _gettest(self, testdesc, count):
2670 """Obtain a Test by looking at its filename.
2677 """Obtain a Test by looking at its filename.
2671
2678
2672 Returns a Test instance. The Test may not be runnable if it doesn't
2679 Returns a Test instance. The Test may not be runnable if it doesn't
2673 map to a known type.
2680 map to a known type.
2674 """
2681 """
2675 path = testdesc['path']
2682 path = testdesc['path']
2676 lctest = path.lower()
2683 lctest = path.lower()
2677 testcls = Test
2684 testcls = Test
2678
2685
2679 for ext, cls in self.TESTTYPES:
2686 for ext, cls in self.TESTTYPES:
2680 if lctest.endswith(ext):
2687 if lctest.endswith(ext):
2681 testcls = cls
2688 testcls = cls
2682 break
2689 break
2683
2690
2684 refpath = os.path.join(self._testdir, path)
2691 refpath = os.path.join(self._testdir, path)
2685 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2692 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2686
2693
2687 # extra keyword parameters. 'case' is used by .t tests
2694 # extra keyword parameters. 'case' is used by .t tests
2688 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2695 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2689
2696
2690 t = testcls(refpath, self._outputdir, tmpdir,
2697 t = testcls(refpath, self._outputdir, tmpdir,
2691 keeptmpdir=self.options.keep_tmpdir,
2698 keeptmpdir=self.options.keep_tmpdir,
2692 debug=self.options.debug,
2699 debug=self.options.debug,
2693 timeout=self.options.timeout,
2700 timeout=self.options.timeout,
2694 startport=self._getport(count),
2701 startport=self._getport(count),
2695 extraconfigopts=self.options.extra_config_opt,
2702 extraconfigopts=self.options.extra_config_opt,
2696 py3kwarnings=self.options.py3k_warnings,
2703 py3kwarnings=self.options.py3k_warnings,
2697 shell=self.options.shell,
2704 shell=self.options.shell,
2698 hgcommand=self._hgcommand,
2705 hgcommand=self._hgcommand,
2699 usechg=bool(self.options.with_chg or self.options.chg),
2706 usechg=bool(self.options.with_chg or self.options.chg),
2700 useipv6=useipv6, **kwds)
2707 useipv6=useipv6, **kwds)
2701 t.should_reload = True
2708 t.should_reload = True
2702 return t
2709 return t
2703
2710
2704 def _cleanup(self):
2711 def _cleanup(self):
2705 """Clean up state from this test invocation."""
2712 """Clean up state from this test invocation."""
2706 if self.options.keep_tmpdir:
2713 if self.options.keep_tmpdir:
2707 return
2714 return
2708
2715
2709 vlog("# Cleaning up HGTMP", self._hgtmp)
2716 vlog("# Cleaning up HGTMP", self._hgtmp)
2710 shutil.rmtree(self._hgtmp, True)
2717 shutil.rmtree(self._hgtmp, True)
2711 for f in self._createdfiles:
2718 for f in self._createdfiles:
2712 try:
2719 try:
2713 os.remove(f)
2720 os.remove(f)
2714 except OSError:
2721 except OSError:
2715 pass
2722 pass
2716
2723
2717 def _usecorrectpython(self):
2724 def _usecorrectpython(self):
2718 """Configure the environment to use the appropriate Python in tests."""
2725 """Configure the environment to use the appropriate Python in tests."""
2719 # Tests must use the same interpreter as us or bad things will happen.
2726 # Tests must use the same interpreter as us or bad things will happen.
2720 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2727 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2721 if getattr(os, 'symlink', None):
2728 if getattr(os, 'symlink', None):
2722 vlog("# Making python executable in test path a symlink to '%s'" %
2729 vlog("# Making python executable in test path a symlink to '%s'" %
2723 sys.executable)
2730 sys.executable)
2724 mypython = os.path.join(self._tmpbindir, pyexename)
2731 mypython = os.path.join(self._tmpbindir, pyexename)
2725 try:
2732 try:
2726 if os.readlink(mypython) == sys.executable:
2733 if os.readlink(mypython) == sys.executable:
2727 return
2734 return
2728 os.unlink(mypython)
2735 os.unlink(mypython)
2729 except OSError as err:
2736 except OSError as err:
2730 if err.errno != errno.ENOENT:
2737 if err.errno != errno.ENOENT:
2731 raise
2738 raise
2732 if self._findprogram(pyexename) != sys.executable:
2739 if self._findprogram(pyexename) != sys.executable:
2733 try:
2740 try:
2734 os.symlink(sys.executable, mypython)
2741 os.symlink(sys.executable, mypython)
2735 self._createdfiles.append(mypython)
2742 self._createdfiles.append(mypython)
2736 except OSError as err:
2743 except OSError as err:
2737 # child processes may race, which is harmless
2744 # child processes may race, which is harmless
2738 if err.errno != errno.EEXIST:
2745 if err.errno != errno.EEXIST:
2739 raise
2746 raise
2740 else:
2747 else:
2741 exedir, exename = os.path.split(sys.executable)
2748 exedir, exename = os.path.split(sys.executable)
2742 vlog("# Modifying search path to find %s as %s in '%s'" %
2749 vlog("# Modifying search path to find %s as %s in '%s'" %
2743 (exename, pyexename, exedir))
2750 (exename, pyexename, exedir))
2744 path = os.environ['PATH'].split(os.pathsep)
2751 path = os.environ['PATH'].split(os.pathsep)
2745 while exedir in path:
2752 while exedir in path:
2746 path.remove(exedir)
2753 path.remove(exedir)
2747 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2754 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2748 if not self._findprogram(pyexename):
2755 if not self._findprogram(pyexename):
2749 print("WARNING: Cannot find %s in search path" % pyexename)
2756 print("WARNING: Cannot find %s in search path" % pyexename)
2750
2757
2751 def _installhg(self):
2758 def _installhg(self):
2752 """Install hg into the test environment.
2759 """Install hg into the test environment.
2753
2760
2754 This will also configure hg with the appropriate testing settings.
2761 This will also configure hg with the appropriate testing settings.
2755 """
2762 """
2756 vlog("# Performing temporary installation of HG")
2763 vlog("# Performing temporary installation of HG")
2757 installerrs = os.path.join(self._hgtmp, b"install.err")
2764 installerrs = os.path.join(self._hgtmp, b"install.err")
2758 compiler = ''
2765 compiler = ''
2759 if self.options.compiler:
2766 if self.options.compiler:
2760 compiler = '--compiler ' + self.options.compiler
2767 compiler = '--compiler ' + self.options.compiler
2761 if self.options.pure:
2768 if self.options.pure:
2762 pure = b"--pure"
2769 pure = b"--pure"
2763 else:
2770 else:
2764 pure = b""
2771 pure = b""
2765
2772
2766 # Run installer in hg root
2773 # Run installer in hg root
2767 script = os.path.realpath(sys.argv[0])
2774 script = os.path.realpath(sys.argv[0])
2768 exe = sys.executable
2775 exe = sys.executable
2769 if PYTHON3:
2776 if PYTHON3:
2770 compiler = _bytespath(compiler)
2777 compiler = _bytespath(compiler)
2771 script = _bytespath(script)
2778 script = _bytespath(script)
2772 exe = _bytespath(exe)
2779 exe = _bytespath(exe)
2773 hgroot = os.path.dirname(os.path.dirname(script))
2780 hgroot = os.path.dirname(os.path.dirname(script))
2774 self._hgroot = hgroot
2781 self._hgroot = hgroot
2775 os.chdir(hgroot)
2782 os.chdir(hgroot)
2776 nohome = b'--home=""'
2783 nohome = b'--home=""'
2777 if os.name == 'nt':
2784 if os.name == 'nt':
2778 # The --home="" trick works only on OS where os.sep == '/'
2785 # The --home="" trick works only on OS where os.sep == '/'
2779 # because of a distutils convert_path() fast-path. Avoid it at
2786 # because of a distutils convert_path() fast-path. Avoid it at
2780 # least on Windows for now, deal with .pydistutils.cfg bugs
2787 # least on Windows for now, deal with .pydistutils.cfg bugs
2781 # when they happen.
2788 # when they happen.
2782 nohome = b''
2789 nohome = b''
2783 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2790 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2784 b' build %(compiler)s --build-base="%(base)s"'
2791 b' build %(compiler)s --build-base="%(base)s"'
2785 b' install --force --prefix="%(prefix)s"'
2792 b' install --force --prefix="%(prefix)s"'
2786 b' --install-lib="%(libdir)s"'
2793 b' --install-lib="%(libdir)s"'
2787 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2794 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2788 % {b'exe': exe, b'pure': pure,
2795 % {b'exe': exe, b'pure': pure,
2789 b'compiler': compiler,
2796 b'compiler': compiler,
2790 b'base': os.path.join(self._hgtmp, b"build"),
2797 b'base': os.path.join(self._hgtmp, b"build"),
2791 b'prefix': self._installdir, b'libdir': self._pythondir,
2798 b'prefix': self._installdir, b'libdir': self._pythondir,
2792 b'bindir': self._bindir,
2799 b'bindir': self._bindir,
2793 b'nohome': nohome, b'logfile': installerrs})
2800 b'nohome': nohome, b'logfile': installerrs})
2794
2801
2795 # setuptools requires install directories to exist.
2802 # setuptools requires install directories to exist.
2796 def makedirs(p):
2803 def makedirs(p):
2797 try:
2804 try:
2798 os.makedirs(p)
2805 os.makedirs(p)
2799 except OSError as e:
2806 except OSError as e:
2800 if e.errno != errno.EEXIST:
2807 if e.errno != errno.EEXIST:
2801 raise
2808 raise
2802 makedirs(self._pythondir)
2809 makedirs(self._pythondir)
2803 makedirs(self._bindir)
2810 makedirs(self._bindir)
2804
2811
2805 vlog("# Running", cmd)
2812 vlog("# Running", cmd)
2806 if os.system(cmd) == 0:
2813 if os.system(cmd) == 0:
2807 if not self.options.verbose:
2814 if not self.options.verbose:
2808 try:
2815 try:
2809 os.remove(installerrs)
2816 os.remove(installerrs)
2810 except OSError as e:
2817 except OSError as e:
2811 if e.errno != errno.ENOENT:
2818 if e.errno != errno.ENOENT:
2812 raise
2819 raise
2813 else:
2820 else:
2814 with open(installerrs, 'rb') as f:
2821 with open(installerrs, 'rb') as f:
2815 for line in f:
2822 for line in f:
2816 if PYTHON3:
2823 if PYTHON3:
2817 sys.stdout.buffer.write(line)
2824 sys.stdout.buffer.write(line)
2818 else:
2825 else:
2819 sys.stdout.write(line)
2826 sys.stdout.write(line)
2820 sys.exit(1)
2827 sys.exit(1)
2821 os.chdir(self._testdir)
2828 os.chdir(self._testdir)
2822
2829
2823 self._usecorrectpython()
2830 self._usecorrectpython()
2824
2831
2825 if self.options.py3k_warnings and not self.options.anycoverage:
2832 if self.options.py3k_warnings and not self.options.anycoverage:
2826 vlog("# Updating hg command to enable Py3k Warnings switch")
2833 vlog("# Updating hg command to enable Py3k Warnings switch")
2827 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2834 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2828 lines = [line.rstrip() for line in f]
2835 lines = [line.rstrip() for line in f]
2829 lines[0] += ' -3'
2836 lines[0] += ' -3'
2830 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2837 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2831 for line in lines:
2838 for line in lines:
2832 f.write(line + '\n')
2839 f.write(line + '\n')
2833
2840
2834 hgbat = os.path.join(self._bindir, b'hg.bat')
2841 hgbat = os.path.join(self._bindir, b'hg.bat')
2835 if os.path.isfile(hgbat):
2842 if os.path.isfile(hgbat):
2836 # hg.bat expects to be put in bin/scripts while run-tests.py
2843 # hg.bat expects to be put in bin/scripts while run-tests.py
2837 # installation layout put it in bin/ directly. Fix it
2844 # installation layout put it in bin/ directly. Fix it
2838 with open(hgbat, 'rb') as f:
2845 with open(hgbat, 'rb') as f:
2839 data = f.read()
2846 data = f.read()
2840 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2847 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2841 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2848 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2842 b'"%~dp0python" "%~dp0hg" %*')
2849 b'"%~dp0python" "%~dp0hg" %*')
2843 with open(hgbat, 'wb') as f:
2850 with open(hgbat, 'wb') as f:
2844 f.write(data)
2851 f.write(data)
2845 else:
2852 else:
2846 print('WARNING: cannot fix hg.bat reference to python.exe')
2853 print('WARNING: cannot fix hg.bat reference to python.exe')
2847
2854
2848 if self.options.anycoverage:
2855 if self.options.anycoverage:
2849 custom = os.path.join(self._testdir, 'sitecustomize.py')
2856 custom = os.path.join(self._testdir, 'sitecustomize.py')
2850 target = os.path.join(self._pythondir, 'sitecustomize.py')
2857 target = os.path.join(self._pythondir, 'sitecustomize.py')
2851 vlog('# Installing coverage trigger to %s' % target)
2858 vlog('# Installing coverage trigger to %s' % target)
2852 shutil.copyfile(custom, target)
2859 shutil.copyfile(custom, target)
2853 rc = os.path.join(self._testdir, '.coveragerc')
2860 rc = os.path.join(self._testdir, '.coveragerc')
2854 vlog('# Installing coverage rc to %s' % rc)
2861 vlog('# Installing coverage rc to %s' % rc)
2855 os.environ['COVERAGE_PROCESS_START'] = rc
2862 os.environ['COVERAGE_PROCESS_START'] = rc
2856 covdir = os.path.join(self._installdir, '..', 'coverage')
2863 covdir = os.path.join(self._installdir, '..', 'coverage')
2857 try:
2864 try:
2858 os.mkdir(covdir)
2865 os.mkdir(covdir)
2859 except OSError as e:
2866 except OSError as e:
2860 if e.errno != errno.EEXIST:
2867 if e.errno != errno.EEXIST:
2861 raise
2868 raise
2862
2869
2863 os.environ['COVERAGE_DIR'] = covdir
2870 os.environ['COVERAGE_DIR'] = covdir
2864
2871
2865 def _checkhglib(self, verb):
2872 def _checkhglib(self, verb):
2866 """Ensure that the 'mercurial' package imported by python is
2873 """Ensure that the 'mercurial' package imported by python is
2867 the one we expect it to be. If not, print a warning to stderr."""
2874 the one we expect it to be. If not, print a warning to stderr."""
2868 if ((self._bindir == self._pythondir) and
2875 if ((self._bindir == self._pythondir) and
2869 (self._bindir != self._tmpbindir)):
2876 (self._bindir != self._tmpbindir)):
2870 # The pythondir has been inferred from --with-hg flag.
2877 # The pythondir has been inferred from --with-hg flag.
2871 # We cannot expect anything sensible here.
2878 # We cannot expect anything sensible here.
2872 return
2879 return
2873 expecthg = os.path.join(self._pythondir, b'mercurial')
2880 expecthg = os.path.join(self._pythondir, b'mercurial')
2874 actualhg = self._gethgpath()
2881 actualhg = self._gethgpath()
2875 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2882 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2876 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2883 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2877 ' (expected %s)\n'
2884 ' (expected %s)\n'
2878 % (verb, actualhg, expecthg))
2885 % (verb, actualhg, expecthg))
2879 def _gethgpath(self):
2886 def _gethgpath(self):
2880 """Return the path to the mercurial package that is actually found by
2887 """Return the path to the mercurial package that is actually found by
2881 the current Python interpreter."""
2888 the current Python interpreter."""
2882 if self._hgpath is not None:
2889 if self._hgpath is not None:
2883 return self._hgpath
2890 return self._hgpath
2884
2891
2885 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2892 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2886 cmd = cmd % PYTHON
2893 cmd = cmd % PYTHON
2887 if PYTHON3:
2894 if PYTHON3:
2888 cmd = _strpath(cmd)
2895 cmd = _strpath(cmd)
2889 pipe = os.popen(cmd)
2896 pipe = os.popen(cmd)
2890 try:
2897 try:
2891 self._hgpath = _bytespath(pipe.read().strip())
2898 self._hgpath = _bytespath(pipe.read().strip())
2892 finally:
2899 finally:
2893 pipe.close()
2900 pipe.close()
2894
2901
2895 return self._hgpath
2902 return self._hgpath
2896
2903
2897 def _installchg(self):
2904 def _installchg(self):
2898 """Install chg into the test environment"""
2905 """Install chg into the test environment"""
2899 vlog('# Performing temporary installation of CHG')
2906 vlog('# Performing temporary installation of CHG')
2900 assert os.path.dirname(self._bindir) == self._installdir
2907 assert os.path.dirname(self._bindir) == self._installdir
2901 assert self._hgroot, 'must be called after _installhg()'
2908 assert self._hgroot, 'must be called after _installhg()'
2902 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2909 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2903 % {b'make': 'make', # TODO: switch by option or environment?
2910 % {b'make': 'make', # TODO: switch by option or environment?
2904 b'prefix': self._installdir})
2911 b'prefix': self._installdir})
2905 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2912 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2906 vlog("# Running", cmd)
2913 vlog("# Running", cmd)
2907 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2914 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2908 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2915 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2909 stderr=subprocess.STDOUT)
2916 stderr=subprocess.STDOUT)
2910 out, _err = proc.communicate()
2917 out, _err = proc.communicate()
2911 if proc.returncode != 0:
2918 if proc.returncode != 0:
2912 if PYTHON3:
2919 if PYTHON3:
2913 sys.stdout.buffer.write(out)
2920 sys.stdout.buffer.write(out)
2914 else:
2921 else:
2915 sys.stdout.write(out)
2922 sys.stdout.write(out)
2916 sys.exit(1)
2923 sys.exit(1)
2917
2924
2918 def _outputcoverage(self):
2925 def _outputcoverage(self):
2919 """Produce code coverage output."""
2926 """Produce code coverage output."""
2920 import coverage
2927 import coverage
2921 coverage = coverage.coverage
2928 coverage = coverage.coverage
2922
2929
2923 vlog('# Producing coverage report')
2930 vlog('# Producing coverage report')
2924 # chdir is the easiest way to get short, relative paths in the
2931 # chdir is the easiest way to get short, relative paths in the
2925 # output.
2932 # output.
2926 os.chdir(self._hgroot)
2933 os.chdir(self._hgroot)
2927 covdir = os.path.join(self._installdir, '..', 'coverage')
2934 covdir = os.path.join(self._installdir, '..', 'coverage')
2928 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2935 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2929
2936
2930 # Map install directory paths back to source directory.
2937 # Map install directory paths back to source directory.
2931 cov.config.paths['srcdir'] = ['.', self._pythondir]
2938 cov.config.paths['srcdir'] = ['.', self._pythondir]
2932
2939
2933 cov.combine()
2940 cov.combine()
2934
2941
2935 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2942 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2936 cov.report(ignore_errors=True, omit=omit)
2943 cov.report(ignore_errors=True, omit=omit)
2937
2944
2938 if self.options.htmlcov:
2945 if self.options.htmlcov:
2939 htmldir = os.path.join(self._outputdir, 'htmlcov')
2946 htmldir = os.path.join(self._outputdir, 'htmlcov')
2940 cov.html_report(directory=htmldir, omit=omit)
2947 cov.html_report(directory=htmldir, omit=omit)
2941 if self.options.annotate:
2948 if self.options.annotate:
2942 adir = os.path.join(self._outputdir, 'annotated')
2949 adir = os.path.join(self._outputdir, 'annotated')
2943 if not os.path.isdir(adir):
2950 if not os.path.isdir(adir):
2944 os.mkdir(adir)
2951 os.mkdir(adir)
2945 cov.annotate(directory=adir, omit=omit)
2952 cov.annotate(directory=adir, omit=omit)
2946
2953
2947 def _findprogram(self, program):
2954 def _findprogram(self, program):
2948 """Search PATH for a executable program"""
2955 """Search PATH for a executable program"""
2949 dpb = _bytespath(os.defpath)
2956 dpb = _bytespath(os.defpath)
2950 sepb = _bytespath(os.pathsep)
2957 sepb = _bytespath(os.pathsep)
2951 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2958 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2952 name = os.path.join(p, program)
2959 name = os.path.join(p, program)
2953 if os.name == 'nt' or os.access(name, os.X_OK):
2960 if os.name == 'nt' or os.access(name, os.X_OK):
2954 return name
2961 return name
2955 return None
2962 return None
2956
2963
2957 def _checktools(self):
2964 def _checktools(self):
2958 """Ensure tools required to run tests are present."""
2965 """Ensure tools required to run tests are present."""
2959 for p in self.REQUIREDTOOLS:
2966 for p in self.REQUIREDTOOLS:
2960 if os.name == 'nt' and not p.endswith('.exe'):
2967 if os.name == 'nt' and not p.endswith('.exe'):
2961 p += '.exe'
2968 p += '.exe'
2962 found = self._findprogram(p)
2969 found = self._findprogram(p)
2963 if found:
2970 if found:
2964 vlog("# Found prerequisite", p, "at", found)
2971 vlog("# Found prerequisite", p, "at", found)
2965 else:
2972 else:
2966 print("WARNING: Did not find prerequisite tool: %s " %
2973 print("WARNING: Did not find prerequisite tool: %s " %
2967 p.decode("utf-8"))
2974 p.decode("utf-8"))
2968
2975
2969 def aggregateexceptions(path):
2976 def aggregateexceptions(path):
2970 exceptions = collections.Counter()
2977 exceptions = collections.Counter()
2971
2978
2972 for f in os.listdir(path):
2979 for f in os.listdir(path):
2973 with open(os.path.join(path, f), 'rb') as fh:
2980 with open(os.path.join(path, f), 'rb') as fh:
2974 data = fh.read().split(b'\0')
2981 data = fh.read().split(b'\0')
2975 if len(data) != 4:
2982 if len(data) != 4:
2976 continue
2983 continue
2977
2984
2978 exc, mainframe, hgframe, hgline = data
2985 exc, mainframe, hgframe, hgline = data
2979 exc = exc.decode('utf-8')
2986 exc = exc.decode('utf-8')
2980 mainframe = mainframe.decode('utf-8')
2987 mainframe = mainframe.decode('utf-8')
2981 hgframe = hgframe.decode('utf-8')
2988 hgframe = hgframe.decode('utf-8')
2982 hgline = hgline.decode('utf-8')
2989 hgline = hgline.decode('utf-8')
2983 exceptions[(hgframe, hgline, exc)] += 1
2990 exceptions[(hgframe, hgline, exc)] += 1
2984
2991
2985 return exceptions
2992 return exceptions
2986
2993
2987 if __name__ == '__main__':
2994 if __name__ == '__main__':
2988 runner = TestRunner()
2995 runner = TestRunner()
2989
2996
2990 try:
2997 try:
2991 import msvcrt
2998 import msvcrt
2992 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2999 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2993 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3000 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2994 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3001 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2995 except ImportError:
3002 except ImportError:
2996 pass
3003 pass
2997
3004
2998 sys.exit(runner.run(sys.argv[1:]))
3005 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now