##// END OF EJS Templates
run-tests: remove dead code related to temp directory...
Gregory Szorc -
r35504:4f5596e5 default
parent child Browse files
Show More
@@ -1,3003 +1,2998
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 class TestRunner(object):
2271 class TestRunner(object):
2272 """Holds context for executing tests.
2272 """Holds context for executing tests.
2273
2273
2274 Tests rely on a lot of state. This object holds it for them.
2274 Tests rely on a lot of state. This object holds it for them.
2275 """
2275 """
2276
2276
2277 # Programs required to run tests.
2277 # Programs required to run tests.
2278 REQUIREDTOOLS = [
2278 REQUIREDTOOLS = [
2279 b'diff',
2279 b'diff',
2280 b'grep',
2280 b'grep',
2281 b'unzip',
2281 b'unzip',
2282 b'gunzip',
2282 b'gunzip',
2283 b'bunzip2',
2283 b'bunzip2',
2284 b'sed',
2284 b'sed',
2285 ]
2285 ]
2286
2286
2287 # Maps file extensions to test class.
2287 # Maps file extensions to test class.
2288 TESTTYPES = [
2288 TESTTYPES = [
2289 (b'.py', PythonTest),
2289 (b'.py', PythonTest),
2290 (b'.t', TTest),
2290 (b'.t', TTest),
2291 ]
2291 ]
2292
2292
2293 def __init__(self):
2293 def __init__(self):
2294 self.options = None
2294 self.options = None
2295 self._hgroot = None
2295 self._hgroot = None
2296 self._testdir = None
2296 self._testdir = None
2297 self._outputdir = None
2297 self._outputdir = None
2298 self._hgtmp = None
2298 self._hgtmp = None
2299 self._installdir = None
2299 self._installdir = None
2300 self._bindir = None
2300 self._bindir = None
2301 self._tmpbinddir = None
2301 self._tmpbinddir = None
2302 self._pythondir = None
2302 self._pythondir = None
2303 self._coveragefile = None
2303 self._coveragefile = None
2304 self._createdfiles = []
2304 self._createdfiles = []
2305 self._hgcommand = None
2305 self._hgcommand = None
2306 self._hgpath = None
2306 self._hgpath = None
2307 self._portoffset = 0
2307 self._portoffset = 0
2308 self._ports = {}
2308 self._ports = {}
2309
2309
2310 def run(self, args, parser=None):
2310 def run(self, args, parser=None):
2311 """Run the test suite."""
2311 """Run the test suite."""
2312 oldmask = os.umask(0o22)
2312 oldmask = os.umask(0o22)
2313 try:
2313 try:
2314 parser = parser or getparser()
2314 parser = parser or getparser()
2315 options = parseargs(args, parser)
2315 options = parseargs(args, parser)
2316 tests = [_bytespath(a) for a in options.tests]
2316 tests = [_bytespath(a) for a in options.tests]
2317 if options.test_list is not None:
2317 if options.test_list is not None:
2318 for listfile in options.test_list:
2318 for listfile in options.test_list:
2319 with open(listfile, 'rb') as f:
2319 with open(listfile, 'rb') as f:
2320 tests.extend(t for t in f.read().splitlines() if t)
2320 tests.extend(t for t in f.read().splitlines() if t)
2321 self.options = options
2321 self.options = options
2322
2322
2323 self._checktools()
2323 self._checktools()
2324 testdescs = self.findtests(tests)
2324 testdescs = self.findtests(tests)
2325 if options.profile_runner:
2325 if options.profile_runner:
2326 import statprof
2326 import statprof
2327 statprof.start()
2327 statprof.start()
2328 result = self._run(testdescs)
2328 result = self._run(testdescs)
2329 if options.profile_runner:
2329 if options.profile_runner:
2330 statprof.stop()
2330 statprof.stop()
2331 statprof.display()
2331 statprof.display()
2332 return result
2332 return result
2333
2333
2334 finally:
2334 finally:
2335 os.umask(oldmask)
2335 os.umask(oldmask)
2336
2336
2337 def _run(self, testdescs):
2337 def _run(self, testdescs):
2338 if self.options.random:
2338 if self.options.random:
2339 random.shuffle(testdescs)
2339 random.shuffle(testdescs)
2340 else:
2340 else:
2341 # keywords for slow tests
2341 # keywords for slow tests
2342 slow = {b'svn': 10,
2342 slow = {b'svn': 10,
2343 b'cvs': 10,
2343 b'cvs': 10,
2344 b'hghave': 10,
2344 b'hghave': 10,
2345 b'largefiles-update': 10,
2345 b'largefiles-update': 10,
2346 b'run-tests': 10,
2346 b'run-tests': 10,
2347 b'corruption': 10,
2347 b'corruption': 10,
2348 b'race': 10,
2348 b'race': 10,
2349 b'i18n': 10,
2349 b'i18n': 10,
2350 b'check': 100,
2350 b'check': 100,
2351 b'gendoc': 100,
2351 b'gendoc': 100,
2352 b'contrib-perf': 200,
2352 b'contrib-perf': 200,
2353 }
2353 }
2354 perf = {}
2354 perf = {}
2355 def sortkey(f):
2355 def sortkey(f):
2356 # run largest tests first, as they tend to take the longest
2356 # run largest tests first, as they tend to take the longest
2357 f = f['path']
2357 f = f['path']
2358 try:
2358 try:
2359 return perf[f]
2359 return perf[f]
2360 except KeyError:
2360 except KeyError:
2361 try:
2361 try:
2362 val = -os.stat(f).st_size
2362 val = -os.stat(f).st_size
2363 except OSError as e:
2363 except OSError as e:
2364 if e.errno != errno.ENOENT:
2364 if e.errno != errno.ENOENT:
2365 raise
2365 raise
2366 perf[f] = -1e9 # file does not exist, tell early
2366 perf[f] = -1e9 # file does not exist, tell early
2367 return -1e9
2367 return -1e9
2368 for kw, mul in slow.items():
2368 for kw, mul in slow.items():
2369 if kw in f:
2369 if kw in f:
2370 val *= mul
2370 val *= mul
2371 if f.endswith(b'.py'):
2371 if f.endswith(b'.py'):
2372 val /= 10.0
2372 val /= 10.0
2373 perf[f] = val / 1000.0
2373 perf[f] = val / 1000.0
2374 return perf[f]
2374 return perf[f]
2375 testdescs.sort(key=sortkey)
2375 testdescs.sort(key=sortkey)
2376
2376
2377 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2377 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2378 os, 'getcwdb', os.getcwd)()
2378 os, 'getcwdb', os.getcwd)()
2379 # assume all tests in same folder for now
2379 # assume all tests in same folder for now
2380 if testdescs:
2380 if testdescs:
2381 pathname = os.path.dirname(testdescs[0]['path'])
2381 pathname = os.path.dirname(testdescs[0]['path'])
2382 if pathname:
2382 if pathname:
2383 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2383 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2384 pathname)
2384 pathname)
2385 if self.options.outputdir:
2385 if self.options.outputdir:
2386 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2386 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2387 else:
2387 else:
2388 self._outputdir = self._testdir
2388 self._outputdir = self._testdir
2389 if testdescs and pathname:
2389 if testdescs and pathname:
2390 self._outputdir = os.path.join(self._outputdir, pathname)
2390 self._outputdir = os.path.join(self._outputdir, pathname)
2391
2391
2392 if 'PYTHONHASHSEED' not in os.environ:
2392 if 'PYTHONHASHSEED' not in os.environ:
2393 # use a random python hash seed all the time
2393 # use a random python hash seed all the time
2394 # we do the randomness ourself to know what seed is used
2394 # we do the randomness ourself to know what seed is used
2395 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2395 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2396
2396
2397 if self.options.tmpdir:
2397 if self.options.tmpdir:
2398 self.options.keep_tmpdir = True
2398 self.options.keep_tmpdir = True
2399 tmpdir = _bytespath(self.options.tmpdir)
2399 tmpdir = _bytespath(self.options.tmpdir)
2400 if os.path.exists(tmpdir):
2400 if os.path.exists(tmpdir):
2401 # Meaning of tmpdir has changed since 1.3: we used to create
2401 # Meaning of tmpdir has changed since 1.3: we used to create
2402 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2402 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2403 # tmpdir already exists.
2403 # tmpdir already exists.
2404 print("error: temp dir %r already exists" % tmpdir)
2404 print("error: temp dir %r already exists" % tmpdir)
2405 return 1
2405 return 1
2406
2406
2407 # Automatically removing tmpdir sounds convenient, but could
2408 # really annoy anyone in the habit of using "--tmpdir=/tmp"
2409 # or "--tmpdir=$HOME".
2410 #vlog("# Removing temp dir", tmpdir)
2411 #shutil.rmtree(tmpdir)
2412 os.makedirs(tmpdir)
2407 os.makedirs(tmpdir)
2413 else:
2408 else:
2414 d = None
2409 d = None
2415 if os.name == 'nt':
2410 if os.name == 'nt':
2416 # without this, we get the default temp dir location, but
2411 # without this, we get the default temp dir location, but
2417 # in all lowercase, which causes troubles with paths (issue3490)
2412 # in all lowercase, which causes troubles with paths (issue3490)
2418 d = osenvironb.get(b'TMP', None)
2413 d = osenvironb.get(b'TMP', None)
2419 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2414 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2420
2415
2421 self._hgtmp = osenvironb[b'HGTMP'] = (
2416 self._hgtmp = osenvironb[b'HGTMP'] = (
2422 os.path.realpath(tmpdir))
2417 os.path.realpath(tmpdir))
2423
2418
2424 if self.options.with_hg:
2419 if self.options.with_hg:
2425 self._installdir = None
2420 self._installdir = None
2426 whg = self.options.with_hg
2421 whg = self.options.with_hg
2427 self._bindir = os.path.dirname(os.path.realpath(whg))
2422 self._bindir = os.path.dirname(os.path.realpath(whg))
2428 assert isinstance(self._bindir, bytes)
2423 assert isinstance(self._bindir, bytes)
2429 self._hgcommand = os.path.basename(whg)
2424 self._hgcommand = os.path.basename(whg)
2430 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2425 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2431 os.makedirs(self._tmpbindir)
2426 os.makedirs(self._tmpbindir)
2432
2427
2433 # This looks redundant with how Python initializes sys.path from
2428 # This looks redundant with how Python initializes sys.path from
2434 # the location of the script being executed. Needed because the
2429 # the location of the script being executed. Needed because the
2435 # "hg" specified by --with-hg is not the only Python script
2430 # "hg" specified by --with-hg is not the only Python script
2436 # executed in the test suite that needs to import 'mercurial'
2431 # executed in the test suite that needs to import 'mercurial'
2437 # ... which means it's not really redundant at all.
2432 # ... which means it's not really redundant at all.
2438 self._pythondir = self._bindir
2433 self._pythondir = self._bindir
2439 else:
2434 else:
2440 self._installdir = os.path.join(self._hgtmp, b"install")
2435 self._installdir = os.path.join(self._hgtmp, b"install")
2441 self._bindir = os.path.join(self._installdir, b"bin")
2436 self._bindir = os.path.join(self._installdir, b"bin")
2442 self._hgcommand = b'hg'
2437 self._hgcommand = b'hg'
2443 self._tmpbindir = self._bindir
2438 self._tmpbindir = self._bindir
2444 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2439 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2445
2440
2446 # set CHGHG, then replace "hg" command by "chg"
2441 # set CHGHG, then replace "hg" command by "chg"
2447 chgbindir = self._bindir
2442 chgbindir = self._bindir
2448 if self.options.chg or self.options.with_chg:
2443 if self.options.chg or self.options.with_chg:
2449 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2444 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2450 else:
2445 else:
2451 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2446 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2452 if self.options.chg:
2447 if self.options.chg:
2453 self._hgcommand = b'chg'
2448 self._hgcommand = b'chg'
2454 elif self.options.with_chg:
2449 elif self.options.with_chg:
2455 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2450 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2456 self._hgcommand = os.path.basename(self.options.with_chg)
2451 self._hgcommand = os.path.basename(self.options.with_chg)
2457
2452
2458 osenvironb[b"BINDIR"] = self._bindir
2453 osenvironb[b"BINDIR"] = self._bindir
2459 osenvironb[b"PYTHON"] = PYTHON
2454 osenvironb[b"PYTHON"] = PYTHON
2460
2455
2461 if self.options.with_python3:
2456 if self.options.with_python3:
2462 osenvironb[b'PYTHON3'] = self.options.with_python3
2457 osenvironb[b'PYTHON3'] = self.options.with_python3
2463
2458
2464 fileb = _bytespath(__file__)
2459 fileb = _bytespath(__file__)
2465 runtestdir = os.path.abspath(os.path.dirname(fileb))
2460 runtestdir = os.path.abspath(os.path.dirname(fileb))
2466 osenvironb[b'RUNTESTDIR'] = runtestdir
2461 osenvironb[b'RUNTESTDIR'] = runtestdir
2467 if PYTHON3:
2462 if PYTHON3:
2468 sepb = _bytespath(os.pathsep)
2463 sepb = _bytespath(os.pathsep)
2469 else:
2464 else:
2470 sepb = os.pathsep
2465 sepb = os.pathsep
2471 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2466 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2472 if os.path.islink(__file__):
2467 if os.path.islink(__file__):
2473 # test helper will likely be at the end of the symlink
2468 # test helper will likely be at the end of the symlink
2474 realfile = os.path.realpath(fileb)
2469 realfile = os.path.realpath(fileb)
2475 realdir = os.path.abspath(os.path.dirname(realfile))
2470 realdir = os.path.abspath(os.path.dirname(realfile))
2476 path.insert(2, realdir)
2471 path.insert(2, realdir)
2477 if chgbindir != self._bindir:
2472 if chgbindir != self._bindir:
2478 path.insert(1, chgbindir)
2473 path.insert(1, chgbindir)
2479 if self._testdir != runtestdir:
2474 if self._testdir != runtestdir:
2480 path = [self._testdir] + path
2475 path = [self._testdir] + path
2481 if self._tmpbindir != self._bindir:
2476 if self._tmpbindir != self._bindir:
2482 path = [self._tmpbindir] + path
2477 path = [self._tmpbindir] + path
2483 osenvironb[b"PATH"] = sepb.join(path)
2478 osenvironb[b"PATH"] = sepb.join(path)
2484
2479
2485 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2480 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2486 # can run .../tests/run-tests.py test-foo where test-foo
2481 # can run .../tests/run-tests.py test-foo where test-foo
2487 # adds an extension to HGRC. Also include run-test.py directory to
2482 # adds an extension to HGRC. Also include run-test.py directory to
2488 # import modules like heredoctest.
2483 # import modules like heredoctest.
2489 pypath = [self._pythondir, self._testdir, runtestdir]
2484 pypath = [self._pythondir, self._testdir, runtestdir]
2490 # We have to augment PYTHONPATH, rather than simply replacing
2485 # We have to augment PYTHONPATH, rather than simply replacing
2491 # it, in case external libraries are only available via current
2486 # it, in case external libraries are only available via current
2492 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2487 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2493 # are in /opt/subversion.)
2488 # are in /opt/subversion.)
2494 oldpypath = osenvironb.get(IMPL_PATH)
2489 oldpypath = osenvironb.get(IMPL_PATH)
2495 if oldpypath:
2490 if oldpypath:
2496 pypath.append(oldpypath)
2491 pypath.append(oldpypath)
2497 osenvironb[IMPL_PATH] = sepb.join(pypath)
2492 osenvironb[IMPL_PATH] = sepb.join(pypath)
2498
2493
2499 if self.options.pure:
2494 if self.options.pure:
2500 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2495 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2501 os.environ["HGMODULEPOLICY"] = "py"
2496 os.environ["HGMODULEPOLICY"] = "py"
2502
2497
2503 if self.options.allow_slow_tests:
2498 if self.options.allow_slow_tests:
2504 os.environ["HGTEST_SLOW"] = "slow"
2499 os.environ["HGTEST_SLOW"] = "slow"
2505 elif 'HGTEST_SLOW' in os.environ:
2500 elif 'HGTEST_SLOW' in os.environ:
2506 del os.environ['HGTEST_SLOW']
2501 del os.environ['HGTEST_SLOW']
2507
2502
2508 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2503 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2509
2504
2510 if self.options.exceptions:
2505 if self.options.exceptions:
2511 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2506 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2512 try:
2507 try:
2513 os.makedirs(exceptionsdir)
2508 os.makedirs(exceptionsdir)
2514 except OSError as e:
2509 except OSError as e:
2515 if e.errno != errno.EEXIST:
2510 if e.errno != errno.EEXIST:
2516 raise
2511 raise
2517
2512
2518 # Remove all existing exception reports.
2513 # Remove all existing exception reports.
2519 for f in os.listdir(exceptionsdir):
2514 for f in os.listdir(exceptionsdir):
2520 os.unlink(os.path.join(exceptionsdir, f))
2515 os.unlink(os.path.join(exceptionsdir, f))
2521
2516
2522 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2517 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2523 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2518 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2524 self.options.extra_config_opt.append(
2519 self.options.extra_config_opt.append(
2525 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2520 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2526
2521
2527 vlog("# Using TESTDIR", self._testdir)
2522 vlog("# Using TESTDIR", self._testdir)
2528 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2523 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2529 vlog("# Using HGTMP", self._hgtmp)
2524 vlog("# Using HGTMP", self._hgtmp)
2530 vlog("# Using PATH", os.environ["PATH"])
2525 vlog("# Using PATH", os.environ["PATH"])
2531 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2526 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2532 vlog("# Writing to directory", self._outputdir)
2527 vlog("# Writing to directory", self._outputdir)
2533
2528
2534 try:
2529 try:
2535 return self._runtests(testdescs) or 0
2530 return self._runtests(testdescs) or 0
2536 finally:
2531 finally:
2537 time.sleep(.1)
2532 time.sleep(.1)
2538 self._cleanup()
2533 self._cleanup()
2539
2534
2540 def findtests(self, args):
2535 def findtests(self, args):
2541 """Finds possible test files from arguments.
2536 """Finds possible test files from arguments.
2542
2537
2543 If you wish to inject custom tests into the test harness, this would
2538 If you wish to inject custom tests into the test harness, this would
2544 be a good function to monkeypatch or override in a derived class.
2539 be a good function to monkeypatch or override in a derived class.
2545 """
2540 """
2546 if not args:
2541 if not args:
2547 if self.options.changed:
2542 if self.options.changed:
2548 proc = Popen4('hg st --rev "%s" -man0 .' %
2543 proc = Popen4('hg st --rev "%s" -man0 .' %
2549 self.options.changed, None, 0)
2544 self.options.changed, None, 0)
2550 stdout, stderr = proc.communicate()
2545 stdout, stderr = proc.communicate()
2551 args = stdout.strip(b'\0').split(b'\0')
2546 args = stdout.strip(b'\0').split(b'\0')
2552 else:
2547 else:
2553 args = os.listdir(b'.')
2548 args = os.listdir(b'.')
2554
2549
2555 expanded_args = []
2550 expanded_args = []
2556 for arg in args:
2551 for arg in args:
2557 if os.path.isdir(arg):
2552 if os.path.isdir(arg):
2558 if not arg.endswith(b'/'):
2553 if not arg.endswith(b'/'):
2559 arg += b'/'
2554 arg += b'/'
2560 expanded_args.extend([arg + a for a in os.listdir(arg)])
2555 expanded_args.extend([arg + a for a in os.listdir(arg)])
2561 else:
2556 else:
2562 expanded_args.append(arg)
2557 expanded_args.append(arg)
2563 args = expanded_args
2558 args = expanded_args
2564
2559
2565 tests = []
2560 tests = []
2566 for t in args:
2561 for t in args:
2567 if not (os.path.basename(t).startswith(b'test-')
2562 if not (os.path.basename(t).startswith(b'test-')
2568 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2563 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2569 continue
2564 continue
2570 if t.endswith(b'.t'):
2565 if t.endswith(b'.t'):
2571 # .t file may contain multiple test cases
2566 # .t file may contain multiple test cases
2572 cases = sorted(parsettestcases(t))
2567 cases = sorted(parsettestcases(t))
2573 if cases:
2568 if cases:
2574 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2569 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2575 else:
2570 else:
2576 tests.append({'path': t})
2571 tests.append({'path': t})
2577 else:
2572 else:
2578 tests.append({'path': t})
2573 tests.append({'path': t})
2579 return tests
2574 return tests
2580
2575
2581 def _runtests(self, testdescs):
2576 def _runtests(self, testdescs):
2582 def _reloadtest(test, i):
2577 def _reloadtest(test, i):
2583 # convert a test back to its description dict
2578 # convert a test back to its description dict
2584 desc = {'path': test.path}
2579 desc = {'path': test.path}
2585 case = getattr(test, '_case', None)
2580 case = getattr(test, '_case', None)
2586 if case:
2581 if case:
2587 desc['case'] = case
2582 desc['case'] = case
2588 return self._gettest(desc, i)
2583 return self._gettest(desc, i)
2589
2584
2590 try:
2585 try:
2591 if self.options.restart:
2586 if self.options.restart:
2592 orig = list(testdescs)
2587 orig = list(testdescs)
2593 while testdescs:
2588 while testdescs:
2594 desc = testdescs[0]
2589 desc = testdescs[0]
2595 # desc['path'] is a relative path
2590 # desc['path'] is a relative path
2596 if 'case' in desc:
2591 if 'case' in desc:
2597 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2592 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2598 else:
2593 else:
2599 errpath = b'%s.err' % desc['path']
2594 errpath = b'%s.err' % desc['path']
2600 errpath = os.path.join(self._outputdir, errpath)
2595 errpath = os.path.join(self._outputdir, errpath)
2601 if os.path.exists(errpath):
2596 if os.path.exists(errpath):
2602 break
2597 break
2603 testdescs.pop(0)
2598 testdescs.pop(0)
2604 if not testdescs:
2599 if not testdescs:
2605 print("running all tests")
2600 print("running all tests")
2606 testdescs = orig
2601 testdescs = orig
2607
2602
2608 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2603 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2609
2604
2610 failed = False
2605 failed = False
2611 kws = self.options.keywords
2606 kws = self.options.keywords
2612 if kws is not None and PYTHON3:
2607 if kws is not None and PYTHON3:
2613 kws = kws.encode('utf-8')
2608 kws = kws.encode('utf-8')
2614
2609
2615 suite = TestSuite(self._testdir,
2610 suite = TestSuite(self._testdir,
2616 jobs=self.options.jobs,
2611 jobs=self.options.jobs,
2617 whitelist=self.options.whitelisted,
2612 whitelist=self.options.whitelisted,
2618 blacklist=self.options.blacklist,
2613 blacklist=self.options.blacklist,
2619 retest=self.options.retest,
2614 retest=self.options.retest,
2620 keywords=kws,
2615 keywords=kws,
2621 loop=self.options.loop,
2616 loop=self.options.loop,
2622 runs_per_test=self.options.runs_per_test,
2617 runs_per_test=self.options.runs_per_test,
2623 showchannels=self.options.showchannels,
2618 showchannels=self.options.showchannels,
2624 tests=tests, loadtest=_reloadtest)
2619 tests=tests, loadtest=_reloadtest)
2625 verbosity = 1
2620 verbosity = 1
2626 if self.options.verbose:
2621 if self.options.verbose:
2627 verbosity = 2
2622 verbosity = 2
2628 runner = TextTestRunner(self, verbosity=verbosity)
2623 runner = TextTestRunner(self, verbosity=verbosity)
2629
2624
2630 if self.options.list_tests:
2625 if self.options.list_tests:
2631 result = runner.listtests(suite)
2626 result = runner.listtests(suite)
2632 else:
2627 else:
2633 if self._installdir:
2628 if self._installdir:
2634 self._installhg()
2629 self._installhg()
2635 self._checkhglib("Testing")
2630 self._checkhglib("Testing")
2636 else:
2631 else:
2637 self._usecorrectpython()
2632 self._usecorrectpython()
2638 if self.options.chg:
2633 if self.options.chg:
2639 assert self._installdir
2634 assert self._installdir
2640 self._installchg()
2635 self._installchg()
2641
2636
2642 result = runner.run(suite)
2637 result = runner.run(suite)
2643
2638
2644 if result.failures:
2639 if result.failures:
2645 failed = True
2640 failed = True
2646
2641
2647 if self.options.anycoverage:
2642 if self.options.anycoverage:
2648 self._outputcoverage()
2643 self._outputcoverage()
2649 except KeyboardInterrupt:
2644 except KeyboardInterrupt:
2650 failed = True
2645 failed = True
2651 print("\ninterrupted!")
2646 print("\ninterrupted!")
2652
2647
2653 if failed:
2648 if failed:
2654 return 1
2649 return 1
2655
2650
2656 def _getport(self, count):
2651 def _getport(self, count):
2657 port = self._ports.get(count) # do we have a cached entry?
2652 port = self._ports.get(count) # do we have a cached entry?
2658 if port is None:
2653 if port is None:
2659 portneeded = 3
2654 portneeded = 3
2660 # above 100 tries we just give up and let test reports failure
2655 # above 100 tries we just give up and let test reports failure
2661 for tries in xrange(100):
2656 for tries in xrange(100):
2662 allfree = True
2657 allfree = True
2663 port = self.options.port + self._portoffset
2658 port = self.options.port + self._portoffset
2664 for idx in xrange(portneeded):
2659 for idx in xrange(portneeded):
2665 if not checkportisavailable(port + idx):
2660 if not checkportisavailable(port + idx):
2666 allfree = False
2661 allfree = False
2667 break
2662 break
2668 self._portoffset += portneeded
2663 self._portoffset += portneeded
2669 if allfree:
2664 if allfree:
2670 break
2665 break
2671 self._ports[count] = port
2666 self._ports[count] = port
2672 return port
2667 return port
2673
2668
2674 def _gettest(self, testdesc, count):
2669 def _gettest(self, testdesc, count):
2675 """Obtain a Test by looking at its filename.
2670 """Obtain a Test by looking at its filename.
2676
2671
2677 Returns a Test instance. The Test may not be runnable if it doesn't
2672 Returns a Test instance. The Test may not be runnable if it doesn't
2678 map to a known type.
2673 map to a known type.
2679 """
2674 """
2680 path = testdesc['path']
2675 path = testdesc['path']
2681 lctest = path.lower()
2676 lctest = path.lower()
2682 testcls = Test
2677 testcls = Test
2683
2678
2684 for ext, cls in self.TESTTYPES:
2679 for ext, cls in self.TESTTYPES:
2685 if lctest.endswith(ext):
2680 if lctest.endswith(ext):
2686 testcls = cls
2681 testcls = cls
2687 break
2682 break
2688
2683
2689 refpath = os.path.join(self._testdir, path)
2684 refpath = os.path.join(self._testdir, path)
2690 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2685 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2691
2686
2692 # extra keyword parameters. 'case' is used by .t tests
2687 # extra keyword parameters. 'case' is used by .t tests
2693 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2688 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2694
2689
2695 t = testcls(refpath, self._outputdir, tmpdir,
2690 t = testcls(refpath, self._outputdir, tmpdir,
2696 keeptmpdir=self.options.keep_tmpdir,
2691 keeptmpdir=self.options.keep_tmpdir,
2697 debug=self.options.debug,
2692 debug=self.options.debug,
2698 timeout=self.options.timeout,
2693 timeout=self.options.timeout,
2699 startport=self._getport(count),
2694 startport=self._getport(count),
2700 extraconfigopts=self.options.extra_config_opt,
2695 extraconfigopts=self.options.extra_config_opt,
2701 py3kwarnings=self.options.py3k_warnings,
2696 py3kwarnings=self.options.py3k_warnings,
2702 shell=self.options.shell,
2697 shell=self.options.shell,
2703 hgcommand=self._hgcommand,
2698 hgcommand=self._hgcommand,
2704 usechg=bool(self.options.with_chg or self.options.chg),
2699 usechg=bool(self.options.with_chg or self.options.chg),
2705 useipv6=useipv6, **kwds)
2700 useipv6=useipv6, **kwds)
2706 t.should_reload = True
2701 t.should_reload = True
2707 return t
2702 return t
2708
2703
2709 def _cleanup(self):
2704 def _cleanup(self):
2710 """Clean up state from this test invocation."""
2705 """Clean up state from this test invocation."""
2711 if self.options.keep_tmpdir:
2706 if self.options.keep_tmpdir:
2712 return
2707 return
2713
2708
2714 vlog("# Cleaning up HGTMP", self._hgtmp)
2709 vlog("# Cleaning up HGTMP", self._hgtmp)
2715 shutil.rmtree(self._hgtmp, True)
2710 shutil.rmtree(self._hgtmp, True)
2716 for f in self._createdfiles:
2711 for f in self._createdfiles:
2717 try:
2712 try:
2718 os.remove(f)
2713 os.remove(f)
2719 except OSError:
2714 except OSError:
2720 pass
2715 pass
2721
2716
2722 def _usecorrectpython(self):
2717 def _usecorrectpython(self):
2723 """Configure the environment to use the appropriate Python in tests."""
2718 """Configure the environment to use the appropriate Python in tests."""
2724 # Tests must use the same interpreter as us or bad things will happen.
2719 # Tests must use the same interpreter as us or bad things will happen.
2725 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2720 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2726 if getattr(os, 'symlink', None):
2721 if getattr(os, 'symlink', None):
2727 vlog("# Making python executable in test path a symlink to '%s'" %
2722 vlog("# Making python executable in test path a symlink to '%s'" %
2728 sys.executable)
2723 sys.executable)
2729 mypython = os.path.join(self._tmpbindir, pyexename)
2724 mypython = os.path.join(self._tmpbindir, pyexename)
2730 try:
2725 try:
2731 if os.readlink(mypython) == sys.executable:
2726 if os.readlink(mypython) == sys.executable:
2732 return
2727 return
2733 os.unlink(mypython)
2728 os.unlink(mypython)
2734 except OSError as err:
2729 except OSError as err:
2735 if err.errno != errno.ENOENT:
2730 if err.errno != errno.ENOENT:
2736 raise
2731 raise
2737 if self._findprogram(pyexename) != sys.executable:
2732 if self._findprogram(pyexename) != sys.executable:
2738 try:
2733 try:
2739 os.symlink(sys.executable, mypython)
2734 os.symlink(sys.executable, mypython)
2740 self._createdfiles.append(mypython)
2735 self._createdfiles.append(mypython)
2741 except OSError as err:
2736 except OSError as err:
2742 # child processes may race, which is harmless
2737 # child processes may race, which is harmless
2743 if err.errno != errno.EEXIST:
2738 if err.errno != errno.EEXIST:
2744 raise
2739 raise
2745 else:
2740 else:
2746 exedir, exename = os.path.split(sys.executable)
2741 exedir, exename = os.path.split(sys.executable)
2747 vlog("# Modifying search path to find %s as %s in '%s'" %
2742 vlog("# Modifying search path to find %s as %s in '%s'" %
2748 (exename, pyexename, exedir))
2743 (exename, pyexename, exedir))
2749 path = os.environ['PATH'].split(os.pathsep)
2744 path = os.environ['PATH'].split(os.pathsep)
2750 while exedir in path:
2745 while exedir in path:
2751 path.remove(exedir)
2746 path.remove(exedir)
2752 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2747 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2753 if not self._findprogram(pyexename):
2748 if not self._findprogram(pyexename):
2754 print("WARNING: Cannot find %s in search path" % pyexename)
2749 print("WARNING: Cannot find %s in search path" % pyexename)
2755
2750
2756 def _installhg(self):
2751 def _installhg(self):
2757 """Install hg into the test environment.
2752 """Install hg into the test environment.
2758
2753
2759 This will also configure hg with the appropriate testing settings.
2754 This will also configure hg with the appropriate testing settings.
2760 """
2755 """
2761 vlog("# Performing temporary installation of HG")
2756 vlog("# Performing temporary installation of HG")
2762 installerrs = os.path.join(self._hgtmp, b"install.err")
2757 installerrs = os.path.join(self._hgtmp, b"install.err")
2763 compiler = ''
2758 compiler = ''
2764 if self.options.compiler:
2759 if self.options.compiler:
2765 compiler = '--compiler ' + self.options.compiler
2760 compiler = '--compiler ' + self.options.compiler
2766 if self.options.pure:
2761 if self.options.pure:
2767 pure = b"--pure"
2762 pure = b"--pure"
2768 else:
2763 else:
2769 pure = b""
2764 pure = b""
2770
2765
2771 # Run installer in hg root
2766 # Run installer in hg root
2772 script = os.path.realpath(sys.argv[0])
2767 script = os.path.realpath(sys.argv[0])
2773 exe = sys.executable
2768 exe = sys.executable
2774 if PYTHON3:
2769 if PYTHON3:
2775 compiler = _bytespath(compiler)
2770 compiler = _bytespath(compiler)
2776 script = _bytespath(script)
2771 script = _bytespath(script)
2777 exe = _bytespath(exe)
2772 exe = _bytespath(exe)
2778 hgroot = os.path.dirname(os.path.dirname(script))
2773 hgroot = os.path.dirname(os.path.dirname(script))
2779 self._hgroot = hgroot
2774 self._hgroot = hgroot
2780 os.chdir(hgroot)
2775 os.chdir(hgroot)
2781 nohome = b'--home=""'
2776 nohome = b'--home=""'
2782 if os.name == 'nt':
2777 if os.name == 'nt':
2783 # The --home="" trick works only on OS where os.sep == '/'
2778 # The --home="" trick works only on OS where os.sep == '/'
2784 # because of a distutils convert_path() fast-path. Avoid it at
2779 # because of a distutils convert_path() fast-path. Avoid it at
2785 # least on Windows for now, deal with .pydistutils.cfg bugs
2780 # least on Windows for now, deal with .pydistutils.cfg bugs
2786 # when they happen.
2781 # when they happen.
2787 nohome = b''
2782 nohome = b''
2788 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2783 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2789 b' build %(compiler)s --build-base="%(base)s"'
2784 b' build %(compiler)s --build-base="%(base)s"'
2790 b' install --force --prefix="%(prefix)s"'
2785 b' install --force --prefix="%(prefix)s"'
2791 b' --install-lib="%(libdir)s"'
2786 b' --install-lib="%(libdir)s"'
2792 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2787 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2793 % {b'exe': exe, b'pure': pure,
2788 % {b'exe': exe, b'pure': pure,
2794 b'compiler': compiler,
2789 b'compiler': compiler,
2795 b'base': os.path.join(self._hgtmp, b"build"),
2790 b'base': os.path.join(self._hgtmp, b"build"),
2796 b'prefix': self._installdir, b'libdir': self._pythondir,
2791 b'prefix': self._installdir, b'libdir': self._pythondir,
2797 b'bindir': self._bindir,
2792 b'bindir': self._bindir,
2798 b'nohome': nohome, b'logfile': installerrs})
2793 b'nohome': nohome, b'logfile': installerrs})
2799
2794
2800 # setuptools requires install directories to exist.
2795 # setuptools requires install directories to exist.
2801 def makedirs(p):
2796 def makedirs(p):
2802 try:
2797 try:
2803 os.makedirs(p)
2798 os.makedirs(p)
2804 except OSError as e:
2799 except OSError as e:
2805 if e.errno != errno.EEXIST:
2800 if e.errno != errno.EEXIST:
2806 raise
2801 raise
2807 makedirs(self._pythondir)
2802 makedirs(self._pythondir)
2808 makedirs(self._bindir)
2803 makedirs(self._bindir)
2809
2804
2810 vlog("# Running", cmd)
2805 vlog("# Running", cmd)
2811 if os.system(cmd) == 0:
2806 if os.system(cmd) == 0:
2812 if not self.options.verbose:
2807 if not self.options.verbose:
2813 try:
2808 try:
2814 os.remove(installerrs)
2809 os.remove(installerrs)
2815 except OSError as e:
2810 except OSError as e:
2816 if e.errno != errno.ENOENT:
2811 if e.errno != errno.ENOENT:
2817 raise
2812 raise
2818 else:
2813 else:
2819 with open(installerrs, 'rb') as f:
2814 with open(installerrs, 'rb') as f:
2820 for line in f:
2815 for line in f:
2821 if PYTHON3:
2816 if PYTHON3:
2822 sys.stdout.buffer.write(line)
2817 sys.stdout.buffer.write(line)
2823 else:
2818 else:
2824 sys.stdout.write(line)
2819 sys.stdout.write(line)
2825 sys.exit(1)
2820 sys.exit(1)
2826 os.chdir(self._testdir)
2821 os.chdir(self._testdir)
2827
2822
2828 self._usecorrectpython()
2823 self._usecorrectpython()
2829
2824
2830 if self.options.py3k_warnings and not self.options.anycoverage:
2825 if self.options.py3k_warnings and not self.options.anycoverage:
2831 vlog("# Updating hg command to enable Py3k Warnings switch")
2826 vlog("# Updating hg command to enable Py3k Warnings switch")
2832 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2827 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2833 lines = [line.rstrip() for line in f]
2828 lines = [line.rstrip() for line in f]
2834 lines[0] += ' -3'
2829 lines[0] += ' -3'
2835 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2830 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2836 for line in lines:
2831 for line in lines:
2837 f.write(line + '\n')
2832 f.write(line + '\n')
2838
2833
2839 hgbat = os.path.join(self._bindir, b'hg.bat')
2834 hgbat = os.path.join(self._bindir, b'hg.bat')
2840 if os.path.isfile(hgbat):
2835 if os.path.isfile(hgbat):
2841 # hg.bat expects to be put in bin/scripts while run-tests.py
2836 # hg.bat expects to be put in bin/scripts while run-tests.py
2842 # installation layout put it in bin/ directly. Fix it
2837 # installation layout put it in bin/ directly. Fix it
2843 with open(hgbat, 'rb') as f:
2838 with open(hgbat, 'rb') as f:
2844 data = f.read()
2839 data = f.read()
2845 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2840 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2846 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2841 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2847 b'"%~dp0python" "%~dp0hg" %*')
2842 b'"%~dp0python" "%~dp0hg" %*')
2848 with open(hgbat, 'wb') as f:
2843 with open(hgbat, 'wb') as f:
2849 f.write(data)
2844 f.write(data)
2850 else:
2845 else:
2851 print('WARNING: cannot fix hg.bat reference to python.exe')
2846 print('WARNING: cannot fix hg.bat reference to python.exe')
2852
2847
2853 if self.options.anycoverage:
2848 if self.options.anycoverage:
2854 custom = os.path.join(self._testdir, 'sitecustomize.py')
2849 custom = os.path.join(self._testdir, 'sitecustomize.py')
2855 target = os.path.join(self._pythondir, 'sitecustomize.py')
2850 target = os.path.join(self._pythondir, 'sitecustomize.py')
2856 vlog('# Installing coverage trigger to %s' % target)
2851 vlog('# Installing coverage trigger to %s' % target)
2857 shutil.copyfile(custom, target)
2852 shutil.copyfile(custom, target)
2858 rc = os.path.join(self._testdir, '.coveragerc')
2853 rc = os.path.join(self._testdir, '.coveragerc')
2859 vlog('# Installing coverage rc to %s' % rc)
2854 vlog('# Installing coverage rc to %s' % rc)
2860 os.environ['COVERAGE_PROCESS_START'] = rc
2855 os.environ['COVERAGE_PROCESS_START'] = rc
2861 covdir = os.path.join(self._installdir, '..', 'coverage')
2856 covdir = os.path.join(self._installdir, '..', 'coverage')
2862 try:
2857 try:
2863 os.mkdir(covdir)
2858 os.mkdir(covdir)
2864 except OSError as e:
2859 except OSError as e:
2865 if e.errno != errno.EEXIST:
2860 if e.errno != errno.EEXIST:
2866 raise
2861 raise
2867
2862
2868 os.environ['COVERAGE_DIR'] = covdir
2863 os.environ['COVERAGE_DIR'] = covdir
2869
2864
2870 def _checkhglib(self, verb):
2865 def _checkhglib(self, verb):
2871 """Ensure that the 'mercurial' package imported by python is
2866 """Ensure that the 'mercurial' package imported by python is
2872 the one we expect it to be. If not, print a warning to stderr."""
2867 the one we expect it to be. If not, print a warning to stderr."""
2873 if ((self._bindir == self._pythondir) and
2868 if ((self._bindir == self._pythondir) and
2874 (self._bindir != self._tmpbindir)):
2869 (self._bindir != self._tmpbindir)):
2875 # The pythondir has been inferred from --with-hg flag.
2870 # The pythondir has been inferred from --with-hg flag.
2876 # We cannot expect anything sensible here.
2871 # We cannot expect anything sensible here.
2877 return
2872 return
2878 expecthg = os.path.join(self._pythondir, b'mercurial')
2873 expecthg = os.path.join(self._pythondir, b'mercurial')
2879 actualhg = self._gethgpath()
2874 actualhg = self._gethgpath()
2880 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2875 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2881 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2876 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2882 ' (expected %s)\n'
2877 ' (expected %s)\n'
2883 % (verb, actualhg, expecthg))
2878 % (verb, actualhg, expecthg))
2884 def _gethgpath(self):
2879 def _gethgpath(self):
2885 """Return the path to the mercurial package that is actually found by
2880 """Return the path to the mercurial package that is actually found by
2886 the current Python interpreter."""
2881 the current Python interpreter."""
2887 if self._hgpath is not None:
2882 if self._hgpath is not None:
2888 return self._hgpath
2883 return self._hgpath
2889
2884
2890 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2885 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2891 cmd = cmd % PYTHON
2886 cmd = cmd % PYTHON
2892 if PYTHON3:
2887 if PYTHON3:
2893 cmd = _strpath(cmd)
2888 cmd = _strpath(cmd)
2894 pipe = os.popen(cmd)
2889 pipe = os.popen(cmd)
2895 try:
2890 try:
2896 self._hgpath = _bytespath(pipe.read().strip())
2891 self._hgpath = _bytespath(pipe.read().strip())
2897 finally:
2892 finally:
2898 pipe.close()
2893 pipe.close()
2899
2894
2900 return self._hgpath
2895 return self._hgpath
2901
2896
2902 def _installchg(self):
2897 def _installchg(self):
2903 """Install chg into the test environment"""
2898 """Install chg into the test environment"""
2904 vlog('# Performing temporary installation of CHG')
2899 vlog('# Performing temporary installation of CHG')
2905 assert os.path.dirname(self._bindir) == self._installdir
2900 assert os.path.dirname(self._bindir) == self._installdir
2906 assert self._hgroot, 'must be called after _installhg()'
2901 assert self._hgroot, 'must be called after _installhg()'
2907 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2902 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2908 % {b'make': 'make', # TODO: switch by option or environment?
2903 % {b'make': 'make', # TODO: switch by option or environment?
2909 b'prefix': self._installdir})
2904 b'prefix': self._installdir})
2910 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2905 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2911 vlog("# Running", cmd)
2906 vlog("# Running", cmd)
2912 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2907 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2913 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2908 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2914 stderr=subprocess.STDOUT)
2909 stderr=subprocess.STDOUT)
2915 out, _err = proc.communicate()
2910 out, _err = proc.communicate()
2916 if proc.returncode != 0:
2911 if proc.returncode != 0:
2917 if PYTHON3:
2912 if PYTHON3:
2918 sys.stdout.buffer.write(out)
2913 sys.stdout.buffer.write(out)
2919 else:
2914 else:
2920 sys.stdout.write(out)
2915 sys.stdout.write(out)
2921 sys.exit(1)
2916 sys.exit(1)
2922
2917
2923 def _outputcoverage(self):
2918 def _outputcoverage(self):
2924 """Produce code coverage output."""
2919 """Produce code coverage output."""
2925 import coverage
2920 import coverage
2926 coverage = coverage.coverage
2921 coverage = coverage.coverage
2927
2922
2928 vlog('# Producing coverage report')
2923 vlog('# Producing coverage report')
2929 # chdir is the easiest way to get short, relative paths in the
2924 # chdir is the easiest way to get short, relative paths in the
2930 # output.
2925 # output.
2931 os.chdir(self._hgroot)
2926 os.chdir(self._hgroot)
2932 covdir = os.path.join(self._installdir, '..', 'coverage')
2927 covdir = os.path.join(self._installdir, '..', 'coverage')
2933 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2928 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2934
2929
2935 # Map install directory paths back to source directory.
2930 # Map install directory paths back to source directory.
2936 cov.config.paths['srcdir'] = ['.', self._pythondir]
2931 cov.config.paths['srcdir'] = ['.', self._pythondir]
2937
2932
2938 cov.combine()
2933 cov.combine()
2939
2934
2940 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2935 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2941 cov.report(ignore_errors=True, omit=omit)
2936 cov.report(ignore_errors=True, omit=omit)
2942
2937
2943 if self.options.htmlcov:
2938 if self.options.htmlcov:
2944 htmldir = os.path.join(self._outputdir, 'htmlcov')
2939 htmldir = os.path.join(self._outputdir, 'htmlcov')
2945 cov.html_report(directory=htmldir, omit=omit)
2940 cov.html_report(directory=htmldir, omit=omit)
2946 if self.options.annotate:
2941 if self.options.annotate:
2947 adir = os.path.join(self._outputdir, 'annotated')
2942 adir = os.path.join(self._outputdir, 'annotated')
2948 if not os.path.isdir(adir):
2943 if not os.path.isdir(adir):
2949 os.mkdir(adir)
2944 os.mkdir(adir)
2950 cov.annotate(directory=adir, omit=omit)
2945 cov.annotate(directory=adir, omit=omit)
2951
2946
2952 def _findprogram(self, program):
2947 def _findprogram(self, program):
2953 """Search PATH for a executable program"""
2948 """Search PATH for a executable program"""
2954 dpb = _bytespath(os.defpath)
2949 dpb = _bytespath(os.defpath)
2955 sepb = _bytespath(os.pathsep)
2950 sepb = _bytespath(os.pathsep)
2956 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2951 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2957 name = os.path.join(p, program)
2952 name = os.path.join(p, program)
2958 if os.name == 'nt' or os.access(name, os.X_OK):
2953 if os.name == 'nt' or os.access(name, os.X_OK):
2959 return name
2954 return name
2960 return None
2955 return None
2961
2956
2962 def _checktools(self):
2957 def _checktools(self):
2963 """Ensure tools required to run tests are present."""
2958 """Ensure tools required to run tests are present."""
2964 for p in self.REQUIREDTOOLS:
2959 for p in self.REQUIREDTOOLS:
2965 if os.name == 'nt' and not p.endswith('.exe'):
2960 if os.name == 'nt' and not p.endswith('.exe'):
2966 p += '.exe'
2961 p += '.exe'
2967 found = self._findprogram(p)
2962 found = self._findprogram(p)
2968 if found:
2963 if found:
2969 vlog("# Found prerequisite", p, "at", found)
2964 vlog("# Found prerequisite", p, "at", found)
2970 else:
2965 else:
2971 print("WARNING: Did not find prerequisite tool: %s " %
2966 print("WARNING: Did not find prerequisite tool: %s " %
2972 p.decode("utf-8"))
2967 p.decode("utf-8"))
2973
2968
2974 def aggregateexceptions(path):
2969 def aggregateexceptions(path):
2975 exceptions = collections.Counter()
2970 exceptions = collections.Counter()
2976
2971
2977 for f in os.listdir(path):
2972 for f in os.listdir(path):
2978 with open(os.path.join(path, f), 'rb') as fh:
2973 with open(os.path.join(path, f), 'rb') as fh:
2979 data = fh.read().split(b'\0')
2974 data = fh.read().split(b'\0')
2980 if len(data) != 4:
2975 if len(data) != 4:
2981 continue
2976 continue
2982
2977
2983 exc, mainframe, hgframe, hgline = data
2978 exc, mainframe, hgframe, hgline = data
2984 exc = exc.decode('utf-8')
2979 exc = exc.decode('utf-8')
2985 mainframe = mainframe.decode('utf-8')
2980 mainframe = mainframe.decode('utf-8')
2986 hgframe = hgframe.decode('utf-8')
2981 hgframe = hgframe.decode('utf-8')
2987 hgline = hgline.decode('utf-8')
2982 hgline = hgline.decode('utf-8')
2988 exceptions[(hgframe, hgline, exc)] += 1
2983 exceptions[(hgframe, hgline, exc)] += 1
2989
2984
2990 return exceptions
2985 return exceptions
2991
2986
2992 if __name__ == '__main__':
2987 if __name__ == '__main__':
2993 runner = TestRunner()
2988 runner = TestRunner()
2994
2989
2995 try:
2990 try:
2996 import msvcrt
2991 import msvcrt
2997 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2992 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2998 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2993 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2999 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2994 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3000 except ImportError:
2995 except ImportError:
3001 pass
2996 pass
3002
2997
3003 sys.exit(runner.run(sys.argv[1:]))
2998 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now