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