##// END OF EJS Templates
run-tests: pre instantiate pygments objects...
Matthieu Laneuville -
r33868:4e8a46c2 default
parent child Browse files
Show More
@@ -1,2906 +1,2906
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', 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', 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)
140 runnerlexer = TestRunnerLexer()
141
139 if sys.version_info > (3, 5, 0):
142 if sys.version_info > (3, 5, 0):
140 PYTHON3 = True
143 PYTHON3 = True
141 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
142 def _bytespath(p):
145 def _bytespath(p):
143 if p is None:
146 if p is None:
144 return p
147 return p
145 return p.encode('utf-8')
148 return p.encode('utf-8')
146
149
147 def _strpath(p):
150 def _strpath(p):
148 if p is None:
151 if p is None:
149 return p
152 return p
150 return p.decode('utf-8')
153 return p.decode('utf-8')
151
154
152 elif sys.version_info >= (3, 0, 0):
155 elif sys.version_info >= (3, 0, 0):
153 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' %
154 (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])))
155 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
158 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
156 else:
159 else:
157 PYTHON3 = False
160 PYTHON3 = False
158
161
159 # In python 2.x, path operations are generally done using
162 # In python 2.x, path operations are generally done using
160 # 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
161 # fiddling there. We define the wrapper functions anyway just to
164 # fiddling there. We define the wrapper functions anyway just to
162 # help keep code consistent between platforms.
165 # help keep code consistent between platforms.
163 def _bytespath(p):
166 def _bytespath(p):
164 return p
167 return p
165
168
166 _strpath = _bytespath
169 _strpath = _bytespath
167
170
168 # For Windows support
171 # For Windows support
169 wifexited = getattr(os, "WIFEXITED", lambda x: False)
172 wifexited = getattr(os, "WIFEXITED", lambda x: False)
170
173
171 # Whether to use IPv6
174 # Whether to use IPv6
172 def checksocketfamily(name, port=20058):
175 def checksocketfamily(name, port=20058):
173 """return true if we can listen on localhost using family=name
176 """return true if we can listen on localhost using family=name
174
177
175 name should be either 'AF_INET', or 'AF_INET6'.
178 name should be either 'AF_INET', or 'AF_INET6'.
176 port being used is okay - EADDRINUSE is considered as successful.
179 port being used is okay - EADDRINUSE is considered as successful.
177 """
180 """
178 family = getattr(socket, name, None)
181 family = getattr(socket, name, None)
179 if family is None:
182 if family is None:
180 return False
183 return False
181 try:
184 try:
182 s = socket.socket(family, socket.SOCK_STREAM)
185 s = socket.socket(family, socket.SOCK_STREAM)
183 s.bind(('localhost', port))
186 s.bind(('localhost', port))
184 s.close()
187 s.close()
185 return True
188 return True
186 except socket.error as exc:
189 except socket.error as exc:
187 if exc.errno == errno.EADDRINUSE:
190 if exc.errno == errno.EADDRINUSE:
188 return True
191 return True
189 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
192 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
190 return False
193 return False
191 else:
194 else:
192 raise
195 raise
193 else:
196 else:
194 return False
197 return False
195
198
196 # useipv6 will be set by parseargs
199 # useipv6 will be set by parseargs
197 useipv6 = None
200 useipv6 = None
198
201
199 def checkportisavailable(port):
202 def checkportisavailable(port):
200 """return true if a port seems free to bind on localhost"""
203 """return true if a port seems free to bind on localhost"""
201 if useipv6:
204 if useipv6:
202 family = socket.AF_INET6
205 family = socket.AF_INET6
203 else:
206 else:
204 family = socket.AF_INET
207 family = socket.AF_INET
205 try:
208 try:
206 s = socket.socket(family, socket.SOCK_STREAM)
209 s = socket.socket(family, socket.SOCK_STREAM)
207 s.bind(('localhost', port))
210 s.bind(('localhost', port))
208 s.close()
211 s.close()
209 return True
212 return True
210 except socket.error as exc:
213 except socket.error as exc:
211 if exc.errno not in (errno.EADDRINUSE, errno.EADDRNOTAVAIL,
214 if exc.errno not in (errno.EADDRINUSE, errno.EADDRNOTAVAIL,
212 errno.EPROTONOSUPPORT):
215 errno.EPROTONOSUPPORT):
213 raise
216 raise
214 return False
217 return False
215
218
216 closefds = os.name == 'posix'
219 closefds = os.name == 'posix'
217 def Popen4(cmd, wd, timeout, env=None):
220 def Popen4(cmd, wd, timeout, env=None):
218 processlock.acquire()
221 processlock.acquire()
219 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,
220 close_fds=closefds,
223 close_fds=closefds,
221 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
224 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
222 stderr=subprocess.STDOUT)
225 stderr=subprocess.STDOUT)
223 processlock.release()
226 processlock.release()
224
227
225 p.fromchild = p.stdout
228 p.fromchild = p.stdout
226 p.tochild = p.stdin
229 p.tochild = p.stdin
227 p.childerr = p.stderr
230 p.childerr = p.stderr
228
231
229 p.timeout = False
232 p.timeout = False
230 if timeout:
233 if timeout:
231 def t():
234 def t():
232 start = time.time()
235 start = time.time()
233 while time.time() - start < timeout and p.returncode is None:
236 while time.time() - start < timeout and p.returncode is None:
234 time.sleep(.1)
237 time.sleep(.1)
235 p.timeout = True
238 p.timeout = True
236 if p.returncode is None:
239 if p.returncode is None:
237 terminate(p)
240 terminate(p)
238 threading.Thread(target=t).start()
241 threading.Thread(target=t).start()
239
242
240 return p
243 return p
241
244
242 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
245 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
243 IMPL_PATH = b'PYTHONPATH'
246 IMPL_PATH = b'PYTHONPATH'
244 if 'java' in sys.platform:
247 if 'java' in sys.platform:
245 IMPL_PATH = b'JYTHONPATH'
248 IMPL_PATH = b'JYTHONPATH'
246
249
247 defaults = {
250 defaults = {
248 'jobs': ('HGTEST_JOBS', 1),
251 'jobs': ('HGTEST_JOBS', 1),
249 'timeout': ('HGTEST_TIMEOUT', 180),
252 'timeout': ('HGTEST_TIMEOUT', 180),
250 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
253 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
251 'port': ('HGTEST_PORT', 20059),
254 'port': ('HGTEST_PORT', 20059),
252 'shell': ('HGTEST_SHELL', 'sh'),
255 'shell': ('HGTEST_SHELL', 'sh'),
253 }
256 }
254
257
255 def canonpath(path):
258 def canonpath(path):
256 return os.path.realpath(os.path.expanduser(path))
259 return os.path.realpath(os.path.expanduser(path))
257
260
258 def parselistfiles(files, listtype, warn=True):
261 def parselistfiles(files, listtype, warn=True):
259 entries = dict()
262 entries = dict()
260 for filename in files:
263 for filename in files:
261 try:
264 try:
262 path = os.path.expanduser(os.path.expandvars(filename))
265 path = os.path.expanduser(os.path.expandvars(filename))
263 f = open(path, "rb")
266 f = open(path, "rb")
264 except IOError as err:
267 except IOError as err:
265 if err.errno != errno.ENOENT:
268 if err.errno != errno.ENOENT:
266 raise
269 raise
267 if warn:
270 if warn:
268 print("warning: no such %s file: %s" % (listtype, filename))
271 print("warning: no such %s file: %s" % (listtype, filename))
269 continue
272 continue
270
273
271 for line in f.readlines():
274 for line in f.readlines():
272 line = line.split(b'#', 1)[0].strip()
275 line = line.split(b'#', 1)[0].strip()
273 if line:
276 if line:
274 entries[line] = filename
277 entries[line] = filename
275
278
276 f.close()
279 f.close()
277 return entries
280 return entries
278
281
279 def parsettestcases(path):
282 def parsettestcases(path):
280 """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
281
284
282 If path does not exist, return an empty set.
285 If path does not exist, return an empty set.
283 """
286 """
284 cases = set()
287 cases = set()
285 try:
288 try:
286 with open(path, 'rb') as f:
289 with open(path, 'rb') as f:
287 for l in f:
290 for l in f:
288 if l.startswith(b'#testcases '):
291 if l.startswith(b'#testcases '):
289 cases.update(l[11:].split())
292 cases.update(l[11:].split())
290 except IOError as ex:
293 except IOError as ex:
291 if ex.errno != errno.ENOENT:
294 if ex.errno != errno.ENOENT:
292 raise
295 raise
293 return cases
296 return cases
294
297
295 def getparser():
298 def getparser():
296 """Obtain the OptionParser used by the CLI."""
299 """Obtain the OptionParser used by the CLI."""
297 parser = optparse.OptionParser("%prog [options] [tests]")
300 parser = optparse.OptionParser("%prog [options] [tests]")
298
301
299 # keep these sorted
302 # keep these sorted
300 parser.add_option("--blacklist", action="append",
303 parser.add_option("--blacklist", action="append",
301 help="skip tests listed in the specified blacklist file")
304 help="skip tests listed in the specified blacklist file")
302 parser.add_option("--whitelist", action="append",
305 parser.add_option("--whitelist", action="append",
303 help="always run tests listed in the specified whitelist file")
306 help="always run tests listed in the specified whitelist file")
304 parser.add_option("--changed", type="string",
307 parser.add_option("--changed", type="string",
305 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")
306 parser.add_option("-C", "--annotate", action="store_true",
309 parser.add_option("-C", "--annotate", action="store_true",
307 help="output files annotated with coverage")
310 help="output files annotated with coverage")
308 parser.add_option("-c", "--cover", action="store_true",
311 parser.add_option("-c", "--cover", action="store_true",
309 help="print a test coverage report")
312 help="print a test coverage report")
310 parser.add_option("--color", choices=["always", "auto", "never"],
313 parser.add_option("--color", choices=["always", "auto", "never"],
311 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
314 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
312 help="colorisation: always|auto|never (default: auto)")
315 help="colorisation: always|auto|never (default: auto)")
313 parser.add_option("-d", "--debug", action="store_true",
316 parser.add_option("-d", "--debug", action="store_true",
314 help="debug mode: write output of test scripts to console"
317 help="debug mode: write output of test scripts to console"
315 " rather than capturing and diffing it (disables timeout)")
318 " rather than capturing and diffing it (disables timeout)")
316 parser.add_option("-f", "--first", action="store_true",
319 parser.add_option("-f", "--first", action="store_true",
317 help="exit on the first test failure")
320 help="exit on the first test failure")
318 parser.add_option("-H", "--htmlcov", action="store_true",
321 parser.add_option("-H", "--htmlcov", action="store_true",
319 help="create an HTML report of the coverage of the files")
322 help="create an HTML report of the coverage of the files")
320 parser.add_option("-i", "--interactive", action="store_true",
323 parser.add_option("-i", "--interactive", action="store_true",
321 help="prompt to accept changed output")
324 help="prompt to accept changed output")
322 parser.add_option("-j", "--jobs", type="int",
325 parser.add_option("-j", "--jobs", type="int",
323 help="number of jobs to run in parallel"
326 help="number of jobs to run in parallel"
324 " (default: $%s or %d)" % defaults['jobs'])
327 " (default: $%s or %d)" % defaults['jobs'])
325 parser.add_option("--keep-tmpdir", action="store_true",
328 parser.add_option("--keep-tmpdir", action="store_true",
326 help="keep temporary directory after running tests")
329 help="keep temporary directory after running tests")
327 parser.add_option("-k", "--keywords",
330 parser.add_option("-k", "--keywords",
328 help="run tests matching keywords")
331 help="run tests matching keywords")
329 parser.add_option("--list-tests", action="store_true",
332 parser.add_option("--list-tests", action="store_true",
330 help="list tests instead of running them")
333 help="list tests instead of running them")
331 parser.add_option("-l", "--local", action="store_true",
334 parser.add_option("-l", "--local", action="store_true",
332 help="shortcut for --with-hg=<testdir>/../hg, "
335 help="shortcut for --with-hg=<testdir>/../hg, "
333 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
336 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
334 parser.add_option("--loop", action="store_true",
337 parser.add_option("--loop", action="store_true",
335 help="loop tests repeatedly")
338 help="loop tests repeatedly")
336 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",
337 help="run each test N times (default=1)", default=1)
340 help="run each test N times (default=1)", default=1)
338 parser.add_option("-n", "--nodiff", action="store_true",
341 parser.add_option("-n", "--nodiff", action="store_true",
339 help="skip showing test changes")
342 help="skip showing test changes")
340 parser.add_option("--outputdir", type="string",
343 parser.add_option("--outputdir", type="string",
341 help="directory to write error logs to (default=test directory)")
344 help="directory to write error logs to (default=test directory)")
342 parser.add_option("-p", "--port", type="int",
345 parser.add_option("-p", "--port", type="int",
343 help="port on which servers should listen"
346 help="port on which servers should listen"
344 " (default: $%s or %d)" % defaults['port'])
347 " (default: $%s or %d)" % defaults['port'])
345 parser.add_option("--compiler", type="string",
348 parser.add_option("--compiler", type="string",
346 help="compiler to build with")
349 help="compiler to build with")
347 parser.add_option("--pure", action="store_true",
350 parser.add_option("--pure", action="store_true",
348 help="use pure Python code instead of C extensions")
351 help="use pure Python code instead of C extensions")
349 parser.add_option("-R", "--restart", action="store_true",
352 parser.add_option("-R", "--restart", action="store_true",
350 help="restart at last error")
353 help="restart at last error")
351 parser.add_option("-r", "--retest", action="store_true",
354 parser.add_option("-r", "--retest", action="store_true",
352 help="retest failed tests")
355 help="retest failed tests")
353 parser.add_option("-S", "--noskips", action="store_true",
356 parser.add_option("-S", "--noskips", action="store_true",
354 help="don't report skip tests verbosely")
357 help="don't report skip tests verbosely")
355 parser.add_option("--shell", type="string",
358 parser.add_option("--shell", type="string",
356 help="shell to use (default: $%s or %s)" % defaults['shell'])
359 help="shell to use (default: $%s or %s)" % defaults['shell'])
357 parser.add_option("-t", "--timeout", type="int",
360 parser.add_option("-t", "--timeout", type="int",
358 help="kill errant tests after TIMEOUT seconds"
361 help="kill errant tests after TIMEOUT seconds"
359 " (default: $%s or %d)" % defaults['timeout'])
362 " (default: $%s or %d)" % defaults['timeout'])
360 parser.add_option("--slowtimeout", type="int",
363 parser.add_option("--slowtimeout", type="int",
361 help="kill errant slow tests after SLOWTIMEOUT seconds"
364 help="kill errant slow tests after SLOWTIMEOUT seconds"
362 " (default: $%s or %d)" % defaults['slowtimeout'])
365 " (default: $%s or %d)" % defaults['slowtimeout'])
363 parser.add_option("--time", action="store_true",
366 parser.add_option("--time", action="store_true",
364 help="time how long each test takes")
367 help="time how long each test takes")
365 parser.add_option("--json", action="store_true",
368 parser.add_option("--json", action="store_true",
366 help="store test result data in 'report.json' file")
369 help="store test result data in 'report.json' file")
367 parser.add_option("--tmpdir", type="string",
370 parser.add_option("--tmpdir", type="string",
368 help="run tests in the given temporary directory"
371 help="run tests in the given temporary directory"
369 " (implies --keep-tmpdir)")
372 " (implies --keep-tmpdir)")
370 parser.add_option("-v", "--verbose", action="store_true",
373 parser.add_option("-v", "--verbose", action="store_true",
371 help="output verbose messages")
374 help="output verbose messages")
372 parser.add_option("--xunit", type="string",
375 parser.add_option("--xunit", type="string",
373 help="record xunit results at specified path")
376 help="record xunit results at specified path")
374 parser.add_option("--view", type="string",
377 parser.add_option("--view", type="string",
375 help="external diff viewer")
378 help="external diff viewer")
376 parser.add_option("--with-hg", type="string",
379 parser.add_option("--with-hg", type="string",
377 metavar="HG",
380 metavar="HG",
378 help="test using specified hg script rather than a "
381 help="test using specified hg script rather than a "
379 "temporary installation")
382 "temporary installation")
380 parser.add_option("--chg", action="store_true",
383 parser.add_option("--chg", action="store_true",
381 help="install and use chg wrapper in place of hg")
384 help="install and use chg wrapper in place of hg")
382 parser.add_option("--with-chg", metavar="CHG",
385 parser.add_option("--with-chg", metavar="CHG",
383 help="use specified chg wrapper in place of hg")
386 help="use specified chg wrapper in place of hg")
384 parser.add_option("--ipv6", action="store_true",
387 parser.add_option("--ipv6", action="store_true",
385 help="prefer IPv6 to IPv4 for network related tests")
388 help="prefer IPv6 to IPv4 for network related tests")
386 parser.add_option("-3", "--py3k-warnings", action="store_true",
389 parser.add_option("-3", "--py3k-warnings", action="store_true",
387 help="enable Py3k warnings on Python 2.7+")
390 help="enable Py3k warnings on Python 2.7+")
388 # 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
389 # Python 3 tests run with Python 3.
392 # Python 3 tests run with Python 3.
390 parser.add_option("--with-python3", metavar="PYTHON3",
393 parser.add_option("--with-python3", metavar="PYTHON3",
391 help="Python 3 interpreter (if running under Python 2)"
394 help="Python 3 interpreter (if running under Python 2)"
392 " (TEMPORARY)")
395 " (TEMPORARY)")
393 parser.add_option('--extra-config-opt', action="append",
396 parser.add_option('--extra-config-opt', action="append",
394 help='set the given config opt in the test hgrc')
397 help='set the given config opt in the test hgrc')
395 parser.add_option('--random', action="store_true",
398 parser.add_option('--random', action="store_true",
396 help='run tests in random order')
399 help='run tests in random order')
397 parser.add_option('--profile-runner', action='store_true',
400 parser.add_option('--profile-runner', action='store_true',
398 help='run statprof on run-tests')
401 help='run statprof on run-tests')
399 parser.add_option('--allow-slow-tests', action='store_true',
402 parser.add_option('--allow-slow-tests', action='store_true',
400 help='allow extremely slow tests')
403 help='allow extremely slow tests')
401 parser.add_option('--showchannels', action='store_true',
404 parser.add_option('--showchannels', action='store_true',
402 help='show scheduling channels')
405 help='show scheduling channels')
403 parser.add_option('--known-good-rev', type="string",
406 parser.add_option('--known-good-rev', type="string",
404 metavar="known_good_rev",
407 metavar="known_good_rev",
405 help=("Automatically bisect any failures using this "
408 help=("Automatically bisect any failures using this "
406 "revision as a known-good revision."))
409 "revision as a known-good revision."))
407
410
408 for option, (envvar, default) in defaults.items():
411 for option, (envvar, default) in defaults.items():
409 defaults[option] = type(default)(os.environ.get(envvar, default))
412 defaults[option] = type(default)(os.environ.get(envvar, default))
410 parser.set_defaults(**defaults)
413 parser.set_defaults(**defaults)
411
414
412 return parser
415 return parser
413
416
414 def parseargs(args, parser):
417 def parseargs(args, parser):
415 """Parse arguments with our OptionParser and validate results."""
418 """Parse arguments with our OptionParser and validate results."""
416 (options, args) = parser.parse_args(args)
419 (options, args) = parser.parse_args(args)
417
420
418 # jython is always pure
421 # jython is always pure
419 if 'java' in sys.platform or '__pypy__' in sys.modules:
422 if 'java' in sys.platform or '__pypy__' in sys.modules:
420 options.pure = True
423 options.pure = True
421
424
422 if options.with_hg:
425 if options.with_hg:
423 options.with_hg = canonpath(_bytespath(options.with_hg))
426 options.with_hg = canonpath(_bytespath(options.with_hg))
424 if not (os.path.isfile(options.with_hg) and
427 if not (os.path.isfile(options.with_hg) and
425 os.access(options.with_hg, os.X_OK)):
428 os.access(options.with_hg, os.X_OK)):
426 parser.error('--with-hg must specify an executable hg script')
429 parser.error('--with-hg must specify an executable hg script')
427 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']:
428 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')
429 if options.local:
432 if options.local:
430 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
433 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
431 reporootdir = os.path.dirname(testdir)
434 reporootdir = os.path.dirname(testdir)
432 pathandattrs = [(b'hg', 'with_hg')]
435 pathandattrs = [(b'hg', 'with_hg')]
433 if options.chg:
436 if options.chg:
434 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
437 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
435 for relpath, attr in pathandattrs:
438 for relpath, attr in pathandattrs:
436 binpath = os.path.join(reporootdir, relpath)
439 binpath = os.path.join(reporootdir, relpath)
437 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):
438 parser.error('--local specified, but %r not found or '
441 parser.error('--local specified, but %r not found or '
439 'not executable' % binpath)
442 'not executable' % binpath)
440 setattr(options, attr, binpath)
443 setattr(options, attr, binpath)
441
444
442 if (options.chg or options.with_chg) and os.name == 'nt':
445 if (options.chg or options.with_chg) and os.name == 'nt':
443 parser.error('chg does not work on %s' % os.name)
446 parser.error('chg does not work on %s' % os.name)
444 if options.with_chg:
447 if options.with_chg:
445 options.chg = False # no installation to temporary location
448 options.chg = False # no installation to temporary location
446 options.with_chg = canonpath(_bytespath(options.with_chg))
449 options.with_chg = canonpath(_bytespath(options.with_chg))
447 if not (os.path.isfile(options.with_chg) and
450 if not (os.path.isfile(options.with_chg) and
448 os.access(options.with_chg, os.X_OK)):
451 os.access(options.with_chg, os.X_OK)):
449 parser.error('--with-chg must specify a chg executable')
452 parser.error('--with-chg must specify a chg executable')
450 if options.chg and options.with_hg:
453 if options.chg and options.with_hg:
451 # chg shares installation location with hg
454 # chg shares installation location with hg
452 parser.error('--chg does not work when --with-hg is specified '
455 parser.error('--chg does not work when --with-hg is specified '
453 '(use --with-chg instead)')
456 '(use --with-chg instead)')
454
457
455 if options.color == 'always' and not pygmentspresent:
458 if options.color == 'always' and not pygmentspresent:
456 sys.stderr.write('warning: --color=always ignored because '
459 sys.stderr.write('warning: --color=always ignored because '
457 'pygments is not installed\n')
460 'pygments is not installed\n')
458
461
459 global useipv6
462 global useipv6
460 if options.ipv6:
463 if options.ipv6:
461 useipv6 = checksocketfamily('AF_INET6')
464 useipv6 = checksocketfamily('AF_INET6')
462 else:
465 else:
463 # only use IPv6 if IPv4 is unavailable and IPv6 is available
466 # only use IPv6 if IPv4 is unavailable and IPv6 is available
464 useipv6 = ((not checksocketfamily('AF_INET'))
467 useipv6 = ((not checksocketfamily('AF_INET'))
465 and checksocketfamily('AF_INET6'))
468 and checksocketfamily('AF_INET6'))
466
469
467 options.anycoverage = options.cover or options.annotate or options.htmlcov
470 options.anycoverage = options.cover or options.annotate or options.htmlcov
468 if options.anycoverage:
471 if options.anycoverage:
469 try:
472 try:
470 import coverage
473 import coverage
471 covver = version.StrictVersion(coverage.__version__).version
474 covver = version.StrictVersion(coverage.__version__).version
472 if covver < (3, 3):
475 if covver < (3, 3):
473 parser.error('coverage options require coverage 3.3 or later')
476 parser.error('coverage options require coverage 3.3 or later')
474 except ImportError:
477 except ImportError:
475 parser.error('coverage options now require the coverage package')
478 parser.error('coverage options now require the coverage package')
476
479
477 if options.anycoverage and options.local:
480 if options.anycoverage and options.local:
478 # this needs some path mangling somewhere, I guess
481 # this needs some path mangling somewhere, I guess
479 parser.error("sorry, coverage options do not work when --local "
482 parser.error("sorry, coverage options do not work when --local "
480 "is specified")
483 "is specified")
481
484
482 if options.anycoverage and options.with_hg:
485 if options.anycoverage and options.with_hg:
483 parser.error("sorry, coverage options do not work when --with-hg "
486 parser.error("sorry, coverage options do not work when --with-hg "
484 "is specified")
487 "is specified")
485
488
486 global verbose
489 global verbose
487 if options.verbose:
490 if options.verbose:
488 verbose = ''
491 verbose = ''
489
492
490 if options.tmpdir:
493 if options.tmpdir:
491 options.tmpdir = canonpath(options.tmpdir)
494 options.tmpdir = canonpath(options.tmpdir)
492
495
493 if options.jobs < 1:
496 if options.jobs < 1:
494 parser.error('--jobs must be positive')
497 parser.error('--jobs must be positive')
495 if options.interactive and options.debug:
498 if options.interactive and options.debug:
496 parser.error("-i/--interactive and -d/--debug are incompatible")
499 parser.error("-i/--interactive and -d/--debug are incompatible")
497 if options.debug:
500 if options.debug:
498 if options.timeout != defaults['timeout']:
501 if options.timeout != defaults['timeout']:
499 sys.stderr.write(
502 sys.stderr.write(
500 'warning: --timeout option ignored with --debug\n')
503 'warning: --timeout option ignored with --debug\n')
501 if options.slowtimeout != defaults['slowtimeout']:
504 if options.slowtimeout != defaults['slowtimeout']:
502 sys.stderr.write(
505 sys.stderr.write(
503 'warning: --slowtimeout option ignored with --debug\n')
506 'warning: --slowtimeout option ignored with --debug\n')
504 options.timeout = 0
507 options.timeout = 0
505 options.slowtimeout = 0
508 options.slowtimeout = 0
506 if options.py3k_warnings:
509 if options.py3k_warnings:
507 if PYTHON3:
510 if PYTHON3:
508 parser.error(
511 parser.error(
509 '--py3k-warnings can only be used on Python 2.7')
512 '--py3k-warnings can only be used on Python 2.7')
510 if options.with_python3:
513 if options.with_python3:
511 if PYTHON3:
514 if PYTHON3:
512 parser.error('--with-python3 cannot be used when executing with '
515 parser.error('--with-python3 cannot be used when executing with '
513 'Python 3')
516 'Python 3')
514
517
515 options.with_python3 = canonpath(options.with_python3)
518 options.with_python3 = canonpath(options.with_python3)
516 # Verify Python3 executable is acceptable.
519 # Verify Python3 executable is acceptable.
517 proc = subprocess.Popen([options.with_python3, b'--version'],
520 proc = subprocess.Popen([options.with_python3, b'--version'],
518 stdout=subprocess.PIPE,
521 stdout=subprocess.PIPE,
519 stderr=subprocess.STDOUT)
522 stderr=subprocess.STDOUT)
520 out, _err = proc.communicate()
523 out, _err = proc.communicate()
521 ret = proc.wait()
524 ret = proc.wait()
522 if ret != 0:
525 if ret != 0:
523 parser.error('could not determine version of python 3')
526 parser.error('could not determine version of python 3')
524 if not out.startswith('Python '):
527 if not out.startswith('Python '):
525 parser.error('unexpected output from python3 --version: %s' %
528 parser.error('unexpected output from python3 --version: %s' %
526 out)
529 out)
527 vers = version.LooseVersion(out[len('Python '):])
530 vers = version.LooseVersion(out[len('Python '):])
528 if vers < version.LooseVersion('3.5.0'):
531 if vers < version.LooseVersion('3.5.0'):
529 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; '
530 'got %s' % out)
533 'got %s' % out)
531
534
532 if options.blacklist:
535 if options.blacklist:
533 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
536 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
534 if options.whitelist:
537 if options.whitelist:
535 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
538 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
536 else:
539 else:
537 options.whitelisted = {}
540 options.whitelisted = {}
538
541
539 if options.showchannels:
542 if options.showchannels:
540 options.nodiff = True
543 options.nodiff = True
541
544
542 return (options, args)
545 return (options, args)
543
546
544 def rename(src, dst):
547 def rename(src, dst):
545 """Like os.rename(), trade atomicity and opened files friendliness
548 """Like os.rename(), trade atomicity and opened files friendliness
546 for existing destination support.
549 for existing destination support.
547 """
550 """
548 shutil.copy(src, dst)
551 shutil.copy(src, dst)
549 os.remove(src)
552 os.remove(src)
550
553
551 _unified_diff = difflib.unified_diff
554 _unified_diff = difflib.unified_diff
552 if PYTHON3:
555 if PYTHON3:
553 import functools
556 import functools
554 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
557 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
555
558
556 def getdiff(expected, output, ref, err):
559 def getdiff(expected, output, ref, err):
557 servefail = False
560 servefail = False
558 lines = []
561 lines = []
559 for line in _unified_diff(expected, output, ref, err):
562 for line in _unified_diff(expected, output, ref, err):
560 if line.startswith(b'+++') or line.startswith(b'---'):
563 if line.startswith(b'+++') or line.startswith(b'---'):
561 line = line.replace(b'\\', b'/')
564 line = line.replace(b'\\', b'/')
562 if line.endswith(b' \n'):
565 if line.endswith(b' \n'):
563 line = line[:-2] + b'\n'
566 line = line[:-2] + b'\n'
564 lines.append(line)
567 lines.append(line)
565 if not servefail and line.startswith(
568 if not servefail and line.startswith(
566 b'+ abort: child process failed to start'):
569 b'+ abort: child process failed to start'):
567 servefail = True
570 servefail = True
568
571
569 return servefail, lines
572 return servefail, lines
570
573
571 verbose = False
574 verbose = False
572 def vlog(*msg):
575 def vlog(*msg):
573 """Log only when in verbose mode."""
576 """Log only when in verbose mode."""
574 if verbose is False:
577 if verbose is False:
575 return
578 return
576
579
577 return log(*msg)
580 return log(*msg)
578
581
579 # 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
580 # sans \t, \n and \r
583 # sans \t, \n and \r
581 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
584 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
582
585
583 # Match feature conditionalized output lines in the form, capturing the feature
586 # Match feature conditionalized output lines in the form, capturing the feature
584 # 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:
585 #
588 #
586 # output..output (feature !)\n
589 # output..output (feature !)\n
587 optline = re.compile(b'(.+) \((.+?) !\)\n$')
590 optline = re.compile(b'(.+) \((.+?) !\)\n$')
588
591
589 def cdatasafe(data):
592 def cdatasafe(data):
590 """Make a string safe to include in a CDATA block.
593 """Make a string safe to include in a CDATA block.
591
594
592 Certain control characters are illegal in a CDATA block, and
595 Certain control characters are illegal in a CDATA block, and
593 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
594 replaces illegal bytes with ? and adds a space between the ]] so
597 replaces illegal bytes with ? and adds a space between the ]] so
595 that it won't break the CDATA block.
598 that it won't break the CDATA block.
596 """
599 """
597 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
600 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
598
601
599 def log(*msg):
602 def log(*msg):
600 """Log something to stdout.
603 """Log something to stdout.
601
604
602 Arguments are strings to print.
605 Arguments are strings to print.
603 """
606 """
604 with iolock:
607 with iolock:
605 if verbose:
608 if verbose:
606 print(verbose, end=' ')
609 print(verbose, end=' ')
607 for m in msg:
610 for m in msg:
608 print(m, end=' ')
611 print(m, end=' ')
609 print()
612 print()
610 sys.stdout.flush()
613 sys.stdout.flush()
611
614
612 def terminate(proc):
615 def terminate(proc):
613 """Terminate subprocess"""
616 """Terminate subprocess"""
614 vlog('# Terminating process %d' % proc.pid)
617 vlog('# Terminating process %d' % proc.pid)
615 try:
618 try:
616 proc.terminate()
619 proc.terminate()
617 except OSError:
620 except OSError:
618 pass
621 pass
619
622
620 def killdaemons(pidfile):
623 def killdaemons(pidfile):
621 import killdaemons as killmod
624 import killdaemons as killmod
622 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
625 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
623 logfn=vlog)
626 logfn=vlog)
624
627
625 class Test(unittest.TestCase):
628 class Test(unittest.TestCase):
626 """Encapsulates a single, runnable test.
629 """Encapsulates a single, runnable test.
627
630
628 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
629 instances need to be instantiated manually. (Typically, unittest.TestCase
632 instances need to be instantiated manually. (Typically, unittest.TestCase
630 classes are instantiated automatically by scanning modules.)
633 classes are instantiated automatically by scanning modules.)
631 """
634 """
632
635
633 # Status code reserved for skipped tests (used by hghave).
636 # Status code reserved for skipped tests (used by hghave).
634 SKIPPED_STATUS = 80
637 SKIPPED_STATUS = 80
635
638
636 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False,
639 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False,
637 debug=False,
640 debug=False,
638 timeout=defaults['timeout'],
641 timeout=defaults['timeout'],
639 startport=defaults['port'], extraconfigopts=None,
642 startport=defaults['port'], extraconfigopts=None,
640 py3kwarnings=False, shell=None, hgcommand=None,
643 py3kwarnings=False, shell=None, hgcommand=None,
641 slowtimeout=defaults['slowtimeout'], usechg=False,
644 slowtimeout=defaults['slowtimeout'], usechg=False,
642 useipv6=False):
645 useipv6=False):
643 """Create a test from parameters.
646 """Create a test from parameters.
644
647
645 path is the full path to the file defining the test.
648 path is the full path to the file defining the test.
646
649
647 tmpdir is the main temporary directory to use for this test.
650 tmpdir is the main temporary directory to use for this test.
648
651
649 keeptmpdir determines whether to keep the test's temporary directory
652 keeptmpdir determines whether to keep the test's temporary directory
650 after execution. It defaults to removal (False).
653 after execution. It defaults to removal (False).
651
654
652 debug mode will make the test execute verbosely, with unfiltered
655 debug mode will make the test execute verbosely, with unfiltered
653 output.
656 output.
654
657
655 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
656 debug is True. See slowtimeout for tests with #require slow.
659 debug is True. See slowtimeout for tests with #require slow.
657
660
658 slowtimeout overrides timeout if the test has #require slow.
661 slowtimeout overrides timeout if the test has #require slow.
659
662
660 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
661 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
662 responsibility to allocate a non-overlapping port range to Test
665 responsibility to allocate a non-overlapping port range to Test
663 instances.
666 instances.
664
667
665 extraconfigopts is an iterable of extra hgrc config options. Values
668 extraconfigopts is an iterable of extra hgrc config options. Values
666 must have the form "key=value" (something understood by hgrc). Values
669 must have the form "key=value" (something understood by hgrc). Values
667 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".
668
671
669 py3kwarnings enables Py3k warnings.
672 py3kwarnings enables Py3k warnings.
670
673
671 shell is the shell to execute tests in.
674 shell is the shell to execute tests in.
672 """
675 """
673 self.path = path
676 self.path = path
674 self.bname = os.path.basename(path)
677 self.bname = os.path.basename(path)
675 self.name = _strpath(self.bname)
678 self.name = _strpath(self.bname)
676 self._testdir = os.path.dirname(path)
679 self._testdir = os.path.dirname(path)
677 self._outputdir = outputdir
680 self._outputdir = outputdir
678 self._tmpname = os.path.basename(path)
681 self._tmpname = os.path.basename(path)
679 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)
680
683
681 self._threadtmp = tmpdir
684 self._threadtmp = tmpdir
682 self._keeptmpdir = keeptmpdir
685 self._keeptmpdir = keeptmpdir
683 self._debug = debug
686 self._debug = debug
684 self._timeout = timeout
687 self._timeout = timeout
685 self._slowtimeout = slowtimeout
688 self._slowtimeout = slowtimeout
686 self._startport = startport
689 self._startport = startport
687 self._extraconfigopts = extraconfigopts or []
690 self._extraconfigopts = extraconfigopts or []
688 self._py3kwarnings = py3kwarnings
691 self._py3kwarnings = py3kwarnings
689 self._shell = _bytespath(shell)
692 self._shell = _bytespath(shell)
690 self._hgcommand = hgcommand or b'hg'
693 self._hgcommand = hgcommand or b'hg'
691 self._usechg = usechg
694 self._usechg = usechg
692 self._useipv6 = useipv6
695 self._useipv6 = useipv6
693
696
694 self._aborted = False
697 self._aborted = False
695 self._daemonpids = []
698 self._daemonpids = []
696 self._finished = None
699 self._finished = None
697 self._ret = None
700 self._ret = None
698 self._out = None
701 self._out = None
699 self._skipped = None
702 self._skipped = None
700 self._testtmp = None
703 self._testtmp = None
701 self._chgsockdir = None
704 self._chgsockdir = None
702
705
703 self._refout = self.readrefout()
706 self._refout = self.readrefout()
704
707
705 def readrefout(self):
708 def readrefout(self):
706 """read reference output"""
709 """read reference output"""
707 # 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,
708 # check test output against it.
711 # check test output against it.
709 if self._debug:
712 if self._debug:
710 return None # to match "out is None"
713 return None # to match "out is None"
711 elif os.path.exists(self.refpath):
714 elif os.path.exists(self.refpath):
712 with open(self.refpath, 'rb') as f:
715 with open(self.refpath, 'rb') as f:
713 return f.read().splitlines(True)
716 return f.read().splitlines(True)
714 else:
717 else:
715 return []
718 return []
716
719
717 # needed to get base class __repr__ running
720 # needed to get base class __repr__ running
718 @property
721 @property
719 def _testMethodName(self):
722 def _testMethodName(self):
720 return self.name
723 return self.name
721
724
722 def __str__(self):
725 def __str__(self):
723 return self.name
726 return self.name
724
727
725 def shortDescription(self):
728 def shortDescription(self):
726 return self.name
729 return self.name
727
730
728 def setUp(self):
731 def setUp(self):
729 """Tasks to perform before run()."""
732 """Tasks to perform before run()."""
730 self._finished = False
733 self._finished = False
731 self._ret = None
734 self._ret = None
732 self._out = None
735 self._out = None
733 self._skipped = None
736 self._skipped = None
734
737
735 try:
738 try:
736 os.mkdir(self._threadtmp)
739 os.mkdir(self._threadtmp)
737 except OSError as e:
740 except OSError as e:
738 if e.errno != errno.EEXIST:
741 if e.errno != errno.EEXIST:
739 raise
742 raise
740
743
741 name = self._tmpname
744 name = self._tmpname
742 self._testtmp = os.path.join(self._threadtmp, name)
745 self._testtmp = os.path.join(self._threadtmp, name)
743 os.mkdir(self._testtmp)
746 os.mkdir(self._testtmp)
744
747
745 # Remove any previous output files.
748 # Remove any previous output files.
746 if os.path.exists(self.errpath):
749 if os.path.exists(self.errpath):
747 try:
750 try:
748 os.remove(self.errpath)
751 os.remove(self.errpath)
749 except OSError as e:
752 except OSError as e:
750 # We might have raced another test to clean up a .err
753 # We might have raced another test to clean up a .err
751 # file, so ignore ENOENT when removing a previous .err
754 # file, so ignore ENOENT when removing a previous .err
752 # file.
755 # file.
753 if e.errno != errno.ENOENT:
756 if e.errno != errno.ENOENT:
754 raise
757 raise
755
758
756 if self._usechg:
759 if self._usechg:
757 self._chgsockdir = os.path.join(self._threadtmp,
760 self._chgsockdir = os.path.join(self._threadtmp,
758 b'%s.chgsock' % name)
761 b'%s.chgsock' % name)
759 os.mkdir(self._chgsockdir)
762 os.mkdir(self._chgsockdir)
760
763
761 def run(self, result):
764 def run(self, result):
762 """Run this test and report results against a TestResult instance."""
765 """Run this test and report results against a TestResult instance."""
763 # This function is extremely similar to unittest.TestCase.run(). Once
766 # This function is extremely similar to unittest.TestCase.run(). Once
764 # 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
765 # function can largely go away.
768 # function can largely go away.
766 self._result = result
769 self._result = result
767 result.startTest(self)
770 result.startTest(self)
768 try:
771 try:
769 try:
772 try:
770 self.setUp()
773 self.setUp()
771 except (KeyboardInterrupt, SystemExit):
774 except (KeyboardInterrupt, SystemExit):
772 self._aborted = True
775 self._aborted = True
773 raise
776 raise
774 except Exception:
777 except Exception:
775 result.addError(self, sys.exc_info())
778 result.addError(self, sys.exc_info())
776 return
779 return
777
780
778 success = False
781 success = False
779 try:
782 try:
780 self.runTest()
783 self.runTest()
781 except KeyboardInterrupt:
784 except KeyboardInterrupt:
782 self._aborted = True
785 self._aborted = True
783 raise
786 raise
784 except unittest.SkipTest as e:
787 except unittest.SkipTest as e:
785 result.addSkip(self, str(e))
788 result.addSkip(self, str(e))
786 # The base class will have already counted this as a
789 # The base class will have already counted this as a
787 # test we "ran", but we want to exclude skipped tests
790 # test we "ran", but we want to exclude skipped tests
788 # from those we count towards those run.
791 # from those we count towards those run.
789 result.testsRun -= 1
792 result.testsRun -= 1
790 except self.failureException as e:
793 except self.failureException as e:
791 # This differs from unittest in that we don't capture
794 # This differs from unittest in that we don't capture
792 # the stack trace. This is for historical reasons and
795 # the stack trace. This is for historical reasons and
793 # this decision could be revisited in the future,
796 # this decision could be revisited in the future,
794 # especially for PythonTest instances.
797 # especially for PythonTest instances.
795 if result.addFailure(self, str(e)):
798 if result.addFailure(self, str(e)):
796 success = True
799 success = True
797 except Exception:
800 except Exception:
798 result.addError(self, sys.exc_info())
801 result.addError(self, sys.exc_info())
799 else:
802 else:
800 success = True
803 success = True
801
804
802 try:
805 try:
803 self.tearDown()
806 self.tearDown()
804 except (KeyboardInterrupt, SystemExit):
807 except (KeyboardInterrupt, SystemExit):
805 self._aborted = True
808 self._aborted = True
806 raise
809 raise
807 except Exception:
810 except Exception:
808 result.addError(self, sys.exc_info())
811 result.addError(self, sys.exc_info())
809 success = False
812 success = False
810
813
811 if success:
814 if success:
812 result.addSuccess(self)
815 result.addSuccess(self)
813 finally:
816 finally:
814 result.stopTest(self, interrupted=self._aborted)
817 result.stopTest(self, interrupted=self._aborted)
815
818
816 def runTest(self):
819 def runTest(self):
817 """Run this test instance.
820 """Run this test instance.
818
821
819 This will return a tuple describing the result of the test.
822 This will return a tuple describing the result of the test.
820 """
823 """
821 env = self._getenv()
824 env = self._getenv()
822 self._genrestoreenv(env)
825 self._genrestoreenv(env)
823 self._daemonpids.append(env['DAEMON_PIDS'])
826 self._daemonpids.append(env['DAEMON_PIDS'])
824 self._createhgrc(env['HGRCPATH'])
827 self._createhgrc(env['HGRCPATH'])
825
828
826 vlog('# Test', self.name)
829 vlog('# Test', self.name)
827
830
828 ret, out = self._run(env)
831 ret, out = self._run(env)
829 self._finished = True
832 self._finished = True
830 self._ret = ret
833 self._ret = ret
831 self._out = out
834 self._out = out
832
835
833 def describe(ret):
836 def describe(ret):
834 if ret < 0:
837 if ret < 0:
835 return 'killed by signal: %d' % -ret
838 return 'killed by signal: %d' % -ret
836 return 'returned error code %d' % ret
839 return 'returned error code %d' % ret
837
840
838 self._skipped = False
841 self._skipped = False
839
842
840 if ret == self.SKIPPED_STATUS:
843 if ret == self.SKIPPED_STATUS:
841 if out is None: # Debug mode, nothing to parse.
844 if out is None: # Debug mode, nothing to parse.
842 missing = ['unknown']
845 missing = ['unknown']
843 failed = None
846 failed = None
844 else:
847 else:
845 missing, failed = TTest.parsehghaveoutput(out)
848 missing, failed = TTest.parsehghaveoutput(out)
846
849
847 if not missing:
850 if not missing:
848 missing = ['skipped']
851 missing = ['skipped']
849
852
850 if failed:
853 if failed:
851 self.fail('hg have failed checking for %s' % failed[-1])
854 self.fail('hg have failed checking for %s' % failed[-1])
852 else:
855 else:
853 self._skipped = True
856 self._skipped = True
854 raise unittest.SkipTest(missing[-1])
857 raise unittest.SkipTest(missing[-1])
855 elif ret == 'timeout':
858 elif ret == 'timeout':
856 self.fail('timed out')
859 self.fail('timed out')
857 elif ret is False:
860 elif ret is False:
858 self.fail('no result code from test')
861 self.fail('no result code from test')
859 elif out != self._refout:
862 elif out != self._refout:
860 # Diff generation may rely on written .err file.
863 # Diff generation may rely on written .err file.
861 if (ret != 0 or out != self._refout) and not self._skipped \
864 if (ret != 0 or out != self._refout) and not self._skipped \
862 and not self._debug:
865 and not self._debug:
863 f = open(self.errpath, 'wb')
866 f = open(self.errpath, 'wb')
864 for line in out:
867 for line in out:
865 f.write(line)
868 f.write(line)
866 f.close()
869 f.close()
867
870
868 # The result object handles diff calculation for us.
871 # The result object handles diff calculation for us.
869 if self._result.addOutputMismatch(self, ret, out, self._refout):
872 if self._result.addOutputMismatch(self, ret, out, self._refout):
870 # change was accepted, skip failing
873 # change was accepted, skip failing
871 return
874 return
872
875
873 if ret:
876 if ret:
874 msg = 'output changed and ' + describe(ret)
877 msg = 'output changed and ' + describe(ret)
875 else:
878 else:
876 msg = 'output changed'
879 msg = 'output changed'
877
880
878 self.fail(msg)
881 self.fail(msg)
879 elif ret:
882 elif ret:
880 self.fail(describe(ret))
883 self.fail(describe(ret))
881
884
882 def tearDown(self):
885 def tearDown(self):
883 """Tasks to perform after run()."""
886 """Tasks to perform after run()."""
884 for entry in self._daemonpids:
887 for entry in self._daemonpids:
885 killdaemons(entry)
888 killdaemons(entry)
886 self._daemonpids = []
889 self._daemonpids = []
887
890
888 if self._keeptmpdir:
891 if self._keeptmpdir:
889 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
892 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
890 (self._testtmp.decode('utf-8'),
893 (self._testtmp.decode('utf-8'),
891 self._threadtmp.decode('utf-8')))
894 self._threadtmp.decode('utf-8')))
892 else:
895 else:
893 shutil.rmtree(self._testtmp, True)
896 shutil.rmtree(self._testtmp, True)
894 shutil.rmtree(self._threadtmp, True)
897 shutil.rmtree(self._threadtmp, True)
895
898
896 if self._usechg:
899 if self._usechg:
897 # chgservers will stop automatically after they find the socket
900 # chgservers will stop automatically after they find the socket
898 # files are deleted
901 # files are deleted
899 shutil.rmtree(self._chgsockdir, True)
902 shutil.rmtree(self._chgsockdir, True)
900
903
901 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 \
902 and not self._debug and self._out:
905 and not self._debug and self._out:
903 f = open(self.errpath, 'wb')
906 f = open(self.errpath, 'wb')
904 for line in self._out:
907 for line in self._out:
905 f.write(line)
908 f.write(line)
906 f.close()
909 f.close()
907
910
908 vlog("# Ret was:", self._ret, '(%s)' % self.name)
911 vlog("# Ret was:", self._ret, '(%s)' % self.name)
909
912
910 def _run(self, env):
913 def _run(self, env):
911 # This should be implemented in child classes to run tests.
914 # This should be implemented in child classes to run tests.
912 raise unittest.SkipTest('unknown test type')
915 raise unittest.SkipTest('unknown test type')
913
916
914 def abort(self):
917 def abort(self):
915 """Terminate execution of this test."""
918 """Terminate execution of this test."""
916 self._aborted = True
919 self._aborted = True
917
920
918 def _portmap(self, i):
921 def _portmap(self, i):
919 offset = b'' if i == 0 else b'%d' % i
922 offset = b'' if i == 0 else b'%d' % i
920 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
923 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
921
924
922 def _getreplacements(self):
925 def _getreplacements(self):
923 """Obtain a mapping of text replacements to apply to test output.
926 """Obtain a mapping of text replacements to apply to test output.
924
927
925 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
926 output. This function defines how some of that normalization will
929 output. This function defines how some of that normalization will
927 occur.
930 occur.
928 """
931 """
929 r = [
932 r = [
930 # This list should be parallel to defineport in _getenv
933 # This list should be parallel to defineport in _getenv
931 self._portmap(0),
934 self._portmap(0),
932 self._portmap(1),
935 self._portmap(1),
933 self._portmap(2),
936 self._portmap(2),
934 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
937 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
935 br'\1 (glob)'),
938 br'\1 (glob)'),
936 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
939 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
937 (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$'),
938 ]
941 ]
939 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
942 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
940
943
941 return r
944 return r
942
945
943 def _escapepath(self, p):
946 def _escapepath(self, p):
944 if os.name == 'nt':
947 if os.name == 'nt':
945 return (
948 return (
946 (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
947 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
948 for c in p))
951 for c in p))
949 )
952 )
950 else:
953 else:
951 return re.escape(p)
954 return re.escape(p)
952
955
953 def _localip(self):
956 def _localip(self):
954 if self._useipv6:
957 if self._useipv6:
955 return b'::1'
958 return b'::1'
956 else:
959 else:
957 return b'127.0.0.1'
960 return b'127.0.0.1'
958
961
959 def _genrestoreenv(self, testenv):
962 def _genrestoreenv(self, testenv):
960 """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
961 environment."""
964 environment."""
962 # Put the restoreenv script inside self._threadtmp
965 # Put the restoreenv script inside self._threadtmp
963 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
966 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
964 testenv['HGTEST_RESTOREENV'] = scriptpath
967 testenv['HGTEST_RESTOREENV'] = scriptpath
965
968
966 # Only restore environment variable names that the shell allows
969 # Only restore environment variable names that the shell allows
967 # us to export.
970 # us to export.
968 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
971 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
969
972
970 # Do not restore these variables; otherwise tests would fail.
973 # Do not restore these variables; otherwise tests would fail.
971 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
974 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
972
975
973 with open(scriptpath, 'w') as envf:
976 with open(scriptpath, 'w') as envf:
974 for name, value in origenviron.items():
977 for name, value in origenviron.items():
975 if not name_regex.match(name):
978 if not name_regex.match(name):
976 # Skip environment variables with unusual names not
979 # Skip environment variables with unusual names not
977 # allowed by most shells.
980 # allowed by most shells.
978 continue
981 continue
979 if name in reqnames:
982 if name in reqnames:
980 continue
983 continue
981 envf.write('%s=%s\n' % (name, shellquote(value)))
984 envf.write('%s=%s\n' % (name, shellquote(value)))
982
985
983 for name in testenv:
986 for name in testenv:
984 if name in origenviron or name in reqnames:
987 if name in origenviron or name in reqnames:
985 continue
988 continue
986 envf.write('unset %s\n' % (name,))
989 envf.write('unset %s\n' % (name,))
987
990
988 def _getenv(self):
991 def _getenv(self):
989 """Obtain environment variables to use during test execution."""
992 """Obtain environment variables to use during test execution."""
990 def defineport(i):
993 def defineport(i):
991 offset = '' if i == 0 else '%s' % i
994 offset = '' if i == 0 else '%s' % i
992 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
995 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
993 env = os.environ.copy()
996 env = os.environ.copy()
994 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase')
997 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase')
995 env['HGEMITWARNINGS'] = '1'
998 env['HGEMITWARNINGS'] = '1'
996 env['TESTTMP'] = self._testtmp
999 env['TESTTMP'] = self._testtmp
997 env['HOME'] = self._testtmp
1000 env['HOME'] = self._testtmp
998 # This number should match portneeded in _getport
1001 # This number should match portneeded in _getport
999 for port in xrange(3):
1002 for port in xrange(3):
1000 # This list should be parallel to _portmap in _getreplacements
1003 # This list should be parallel to _portmap in _getreplacements
1001 defineport(port)
1004 defineport(port)
1002 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
1005 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
1003 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
1006 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
1004 env["HGEDITOR"] = ('"' + sys.executable + '"'
1007 env["HGEDITOR"] = ('"' + sys.executable + '"'
1005 + ' -c "import sys; sys.exit(0)"')
1008 + ' -c "import sys; sys.exit(0)"')
1006 env["HGMERGE"] = "internal:merge"
1009 env["HGMERGE"] = "internal:merge"
1007 env["HGUSER"] = "test"
1010 env["HGUSER"] = "test"
1008 env["HGENCODING"] = "ascii"
1011 env["HGENCODING"] = "ascii"
1009 env["HGENCODINGMODE"] = "strict"
1012 env["HGENCODINGMODE"] = "strict"
1010 env['HGIPV6'] = str(int(self._useipv6))
1013 env['HGIPV6'] = str(int(self._useipv6))
1011
1014
1012 # 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
1013 # IP addresses.
1016 # IP addresses.
1014 env['LOCALIP'] = self._localip()
1017 env['LOCALIP'] = self._localip()
1015
1018
1016 # Reset some environment variables to well-known values so that
1019 # Reset some environment variables to well-known values so that
1017 # the tests produce repeatable output.
1020 # the tests produce repeatable output.
1018 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1021 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1019 env['TZ'] = 'GMT'
1022 env['TZ'] = 'GMT'
1020 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1023 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1021 env['COLUMNS'] = '80'
1024 env['COLUMNS'] = '80'
1022 env['TERM'] = 'xterm'
1025 env['TERM'] = 'xterm'
1023
1026
1024 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 ' +
1025 'HGPLAIN HGPLAINEXCEPT EDITOR VISUAL PAGER ' +
1028 'HGPLAIN HGPLAINEXCEPT EDITOR VISUAL PAGER ' +
1026 'NO_PROXY CHGDEBUG').split():
1029 'NO_PROXY CHGDEBUG').split():
1027 if k in env:
1030 if k in env:
1028 del env[k]
1031 del env[k]
1029
1032
1030 # unset env related to hooks
1033 # unset env related to hooks
1031 for k in env.keys():
1034 for k in env.keys():
1032 if k.startswith('HG_'):
1035 if k.startswith('HG_'):
1033 del env[k]
1036 del env[k]
1034
1037
1035 if self._usechg:
1038 if self._usechg:
1036 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1039 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1037
1040
1038 return env
1041 return env
1039
1042
1040 def _createhgrc(self, path):
1043 def _createhgrc(self, path):
1041 """Create an hgrc file for this test."""
1044 """Create an hgrc file for this test."""
1042 hgrc = open(path, 'wb')
1045 hgrc = open(path, 'wb')
1043 hgrc.write(b'[ui]\n')
1046 hgrc.write(b'[ui]\n')
1044 hgrc.write(b'slash = True\n')
1047 hgrc.write(b'slash = True\n')
1045 hgrc.write(b'interactive = False\n')
1048 hgrc.write(b'interactive = False\n')
1046 hgrc.write(b'mergemarkers = detailed\n')
1049 hgrc.write(b'mergemarkers = detailed\n')
1047 hgrc.write(b'promptecho = True\n')
1050 hgrc.write(b'promptecho = True\n')
1048 hgrc.write(b'[defaults]\n')
1051 hgrc.write(b'[defaults]\n')
1049 hgrc.write(b'[devel]\n')
1052 hgrc.write(b'[devel]\n')
1050 hgrc.write(b'all-warnings = true\n')
1053 hgrc.write(b'all-warnings = true\n')
1051 hgrc.write(b'default-date = 0 0\n')
1054 hgrc.write(b'default-date = 0 0\n')
1052 hgrc.write(b'[largefiles]\n')
1055 hgrc.write(b'[largefiles]\n')
1053 hgrc.write(b'usercache = %s\n' %
1056 hgrc.write(b'usercache = %s\n' %
1054 (os.path.join(self._testtmp, b'.cache/largefiles')))
1057 (os.path.join(self._testtmp, b'.cache/largefiles')))
1055 hgrc.write(b'[web]\n')
1058 hgrc.write(b'[web]\n')
1056 hgrc.write(b'address = localhost\n')
1059 hgrc.write(b'address = localhost\n')
1057 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1060 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1058
1061
1059 for opt in self._extraconfigopts:
1062 for opt in self._extraconfigopts:
1060 section, key = opt.split('.', 1)
1063 section, key = opt.split('.', 1)
1061 assert '=' in key, ('extra config opt %s must '
1064 assert '=' in key, ('extra config opt %s must '
1062 'have an = for assignment' % opt)
1065 'have an = for assignment' % opt)
1063 hgrc.write(b'[%s]\n%s\n' % (section, key))
1066 hgrc.write(b'[%s]\n%s\n' % (section, key))
1064 hgrc.close()
1067 hgrc.close()
1065
1068
1066 def fail(self, msg):
1069 def fail(self, msg):
1067 # unittest differentiates between errored and failed.
1070 # unittest differentiates between errored and failed.
1068 # Failed is denoted by AssertionError (by default at least).
1071 # Failed is denoted by AssertionError (by default at least).
1069 raise AssertionError(msg)
1072 raise AssertionError(msg)
1070
1073
1071 def _runcommand(self, cmd, env, normalizenewlines=False):
1074 def _runcommand(self, cmd, env, normalizenewlines=False):
1072 """Run command in a sub-process, capturing the output (stdout and
1075 """Run command in a sub-process, capturing the output (stdout and
1073 stderr).
1076 stderr).
1074
1077
1075 Return a tuple (exitcode, output). output is None in debug mode.
1078 Return a tuple (exitcode, output). output is None in debug mode.
1076 """
1079 """
1077 if self._debug:
1080 if self._debug:
1078 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
1081 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
1079 env=env)
1082 env=env)
1080 ret = proc.wait()
1083 ret = proc.wait()
1081 return (ret, None)
1084 return (ret, None)
1082
1085
1083 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1086 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1084 def cleanup():
1087 def cleanup():
1085 terminate(proc)
1088 terminate(proc)
1086 ret = proc.wait()
1089 ret = proc.wait()
1087 if ret == 0:
1090 if ret == 0:
1088 ret = signal.SIGTERM << 8
1091 ret = signal.SIGTERM << 8
1089 killdaemons(env['DAEMON_PIDS'])
1092 killdaemons(env['DAEMON_PIDS'])
1090 return ret
1093 return ret
1091
1094
1092 output = ''
1095 output = ''
1093 proc.tochild.close()
1096 proc.tochild.close()
1094
1097
1095 try:
1098 try:
1096 output = proc.fromchild.read()
1099 output = proc.fromchild.read()
1097 except KeyboardInterrupt:
1100 except KeyboardInterrupt:
1098 vlog('# Handling keyboard interrupt')
1101 vlog('# Handling keyboard interrupt')
1099 cleanup()
1102 cleanup()
1100 raise
1103 raise
1101
1104
1102 ret = proc.wait()
1105 ret = proc.wait()
1103 if wifexited(ret):
1106 if wifexited(ret):
1104 ret = os.WEXITSTATUS(ret)
1107 ret = os.WEXITSTATUS(ret)
1105
1108
1106 if proc.timeout:
1109 if proc.timeout:
1107 ret = 'timeout'
1110 ret = 'timeout'
1108
1111
1109 if ret:
1112 if ret:
1110 killdaemons(env['DAEMON_PIDS'])
1113 killdaemons(env['DAEMON_PIDS'])
1111
1114
1112 for s, r in self._getreplacements():
1115 for s, r in self._getreplacements():
1113 output = re.sub(s, r, output)
1116 output = re.sub(s, r, output)
1114
1117
1115 if normalizenewlines:
1118 if normalizenewlines:
1116 output = output.replace('\r\n', '\n')
1119 output = output.replace('\r\n', '\n')
1117
1120
1118 return ret, output.splitlines(True)
1121 return ret, output.splitlines(True)
1119
1122
1120 class PythonTest(Test):
1123 class PythonTest(Test):
1121 """A Python-based test."""
1124 """A Python-based test."""
1122
1125
1123 @property
1126 @property
1124 def refpath(self):
1127 def refpath(self):
1125 return os.path.join(self._testdir, b'%s.out' % self.bname)
1128 return os.path.join(self._testdir, b'%s.out' % self.bname)
1126
1129
1127 def _run(self, env):
1130 def _run(self, env):
1128 py3kswitch = self._py3kwarnings and b' -3' or b''
1131 py3kswitch = self._py3kwarnings and b' -3' or b''
1129 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
1132 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
1130 vlog("# Running", cmd)
1133 vlog("# Running", cmd)
1131 normalizenewlines = os.name == 'nt'
1134 normalizenewlines = os.name == 'nt'
1132 result = self._runcommand(cmd, env,
1135 result = self._runcommand(cmd, env,
1133 normalizenewlines=normalizenewlines)
1136 normalizenewlines=normalizenewlines)
1134 if self._aborted:
1137 if self._aborted:
1135 raise KeyboardInterrupt()
1138 raise KeyboardInterrupt()
1136
1139
1137 return result
1140 return result
1138
1141
1139 # Some glob patterns apply only in some circumstances, so the script
1142 # Some glob patterns apply only in some circumstances, so the script
1140 # might want to remove (glob) annotations that otherwise should be
1143 # might want to remove (glob) annotations that otherwise should be
1141 # retained.
1144 # retained.
1142 checkcodeglobpats = [
1145 checkcodeglobpats = [
1143 # 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
1144 # better.
1147 # better.
1145 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1148 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1146 re.compile(br'^moving \S+/.*[^)]$'),
1149 re.compile(br'^moving \S+/.*[^)]$'),
1147 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1150 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1148 # 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),
1149 # so we always glob that too.
1152 # so we always glob that too.
1150 re.compile(br'.*\$LOCALIP.*$'),
1153 re.compile(br'.*\$LOCALIP.*$'),
1151 ]
1154 ]
1152
1155
1153 bchr = chr
1156 bchr = chr
1154 if PYTHON3:
1157 if PYTHON3:
1155 bchr = lambda x: bytes([x])
1158 bchr = lambda x: bytes([x])
1156
1159
1157 class TTest(Test):
1160 class TTest(Test):
1158 """A "t test" is a test backed by a .t file."""
1161 """A "t test" is a test backed by a .t file."""
1159
1162
1160 SKIPPED_PREFIX = b'skipped: '
1163 SKIPPED_PREFIX = b'skipped: '
1161 FAILED_PREFIX = b'hghave check failed: '
1164 FAILED_PREFIX = b'hghave check failed: '
1162 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1165 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1163
1166
1164 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1167 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1165 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))
1166 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1169 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1167
1170
1168 def __init__(self, path, *args, **kwds):
1171 def __init__(self, path, *args, **kwds):
1169 # accept an extra "case" parameter
1172 # accept an extra "case" parameter
1170 case = None
1173 case = None
1171 if 'case' in kwds:
1174 if 'case' in kwds:
1172 case = kwds.pop('case')
1175 case = kwds.pop('case')
1173 self._case = case
1176 self._case = case
1174 self._allcases = parsettestcases(path)
1177 self._allcases = parsettestcases(path)
1175 super(TTest, self).__init__(path, *args, **kwds)
1178 super(TTest, self).__init__(path, *args, **kwds)
1176 if case:
1179 if case:
1177 self.name = '%s (case %s)' % (self.name, _strpath(case))
1180 self.name = '%s (case %s)' % (self.name, _strpath(case))
1178 self.errpath = b'%s.%s.err' % (self.errpath[:-4], case)
1181 self.errpath = b'%s.%s.err' % (self.errpath[:-4], case)
1179 self._tmpname += b'-%s' % case
1182 self._tmpname += b'-%s' % case
1180
1183
1181 @property
1184 @property
1182 def refpath(self):
1185 def refpath(self):
1183 return os.path.join(self._testdir, self.bname)
1186 return os.path.join(self._testdir, self.bname)
1184
1187
1185 def _run(self, env):
1188 def _run(self, env):
1186 f = open(self.path, 'rb')
1189 f = open(self.path, 'rb')
1187 lines = f.readlines()
1190 lines = f.readlines()
1188 f.close()
1191 f.close()
1189
1192
1190 # .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
1191 # output updated with the the test input. This avoids some race
1194 # output updated with the the test input. This avoids some race
1192 # conditions where the reference output does not match the actual test.
1195 # conditions where the reference output does not match the actual test.
1193 if self._refout is not None:
1196 if self._refout is not None:
1194 self._refout = lines
1197 self._refout = lines
1195
1198
1196 salt, script, after, expected = self._parsetest(lines)
1199 salt, script, after, expected = self._parsetest(lines)
1197
1200
1198 # Write out the generated script.
1201 # Write out the generated script.
1199 fname = b'%s.sh' % self._testtmp
1202 fname = b'%s.sh' % self._testtmp
1200 f = open(fname, 'wb')
1203 f = open(fname, 'wb')
1201 for l in script:
1204 for l in script:
1202 f.write(l)
1205 f.write(l)
1203 f.close()
1206 f.close()
1204
1207
1205 cmd = b'%s "%s"' % (self._shell, fname)
1208 cmd = b'%s "%s"' % (self._shell, fname)
1206 vlog("# Running", cmd)
1209 vlog("# Running", cmd)
1207
1210
1208 exitcode, output = self._runcommand(cmd, env)
1211 exitcode, output = self._runcommand(cmd, env)
1209
1212
1210 if self._aborted:
1213 if self._aborted:
1211 raise KeyboardInterrupt()
1214 raise KeyboardInterrupt()
1212
1215
1213 # Do not merge output if skipped. Return hghave message instead.
1216 # Do not merge output if skipped. Return hghave message instead.
1214 # Similarly, with --debug, output is None.
1217 # Similarly, with --debug, output is None.
1215 if exitcode == self.SKIPPED_STATUS or output is None:
1218 if exitcode == self.SKIPPED_STATUS or output is None:
1216 return exitcode, output
1219 return exitcode, output
1217
1220
1218 return self._processoutput(exitcode, output, salt, after, expected)
1221 return self._processoutput(exitcode, output, salt, after, expected)
1219
1222
1220 def _hghave(self, reqs):
1223 def _hghave(self, reqs):
1221 # 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.
1222 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1225 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1223 tdir = runtestdir.replace(b'\\', b'/')
1226 tdir = runtestdir.replace(b'\\', b'/')
1224 proc = Popen4(b'%s -c "%s/hghave %s"' %
1227 proc = Popen4(b'%s -c "%s/hghave %s"' %
1225 (self._shell, tdir, b' '.join(reqs)),
1228 (self._shell, tdir, b' '.join(reqs)),
1226 self._testtmp, 0, self._getenv())
1229 self._testtmp, 0, self._getenv())
1227 stdout, stderr = proc.communicate()
1230 stdout, stderr = proc.communicate()
1228 ret = proc.wait()
1231 ret = proc.wait()
1229 if wifexited(ret):
1232 if wifexited(ret):
1230 ret = os.WEXITSTATUS(ret)
1233 ret = os.WEXITSTATUS(ret)
1231 if ret == 2:
1234 if ret == 2:
1232 print(stdout.decode('utf-8'))
1235 print(stdout.decode('utf-8'))
1233 sys.exit(1)
1236 sys.exit(1)
1234
1237
1235 if ret != 0:
1238 if ret != 0:
1236 return False, stdout
1239 return False, stdout
1237
1240
1238 if 'slow' in reqs:
1241 if 'slow' in reqs:
1239 self._timeout = self._slowtimeout
1242 self._timeout = self._slowtimeout
1240 return True, None
1243 return True, None
1241
1244
1242 def _iftest(self, args):
1245 def _iftest(self, args):
1243 # implements "#if"
1246 # implements "#if"
1244 reqs = []
1247 reqs = []
1245 for arg in args:
1248 for arg in args:
1246 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1249 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1247 if arg[3:] == self._case:
1250 if arg[3:] == self._case:
1248 return False
1251 return False
1249 elif arg in self._allcases:
1252 elif arg in self._allcases:
1250 if arg != self._case:
1253 if arg != self._case:
1251 return False
1254 return False
1252 else:
1255 else:
1253 reqs.append(arg)
1256 reqs.append(arg)
1254 return self._hghave(reqs)[0]
1257 return self._hghave(reqs)[0]
1255
1258
1256 def _parsetest(self, lines):
1259 def _parsetest(self, lines):
1257 # We generate a shell script which outputs unique markers to line
1260 # We generate a shell script which outputs unique markers to line
1258 # up script results with our source. These markers include input
1261 # up script results with our source. These markers include input
1259 # line number and the last return code.
1262 # line number and the last return code.
1260 salt = b"SALT%d" % time.time()
1263 salt = b"SALT%d" % time.time()
1261 def addsalt(line, inpython):
1264 def addsalt(line, inpython):
1262 if inpython:
1265 if inpython:
1263 script.append(b'%s %d 0\n' % (salt, line))
1266 script.append(b'%s %d 0\n' % (salt, line))
1264 else:
1267 else:
1265 script.append(b'echo %s %d $?\n' % (salt, line))
1268 script.append(b'echo %s %d $?\n' % (salt, line))
1266
1269
1267 script = []
1270 script = []
1268
1271
1269 # 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
1270 # with non-active parts of the source, with synchronization by our
1273 # with non-active parts of the source, with synchronization by our
1271 # SALT line number markers. The after table contains the non-active
1274 # SALT line number markers. The after table contains the non-active
1272 # components, ordered by line number.
1275 # components, ordered by line number.
1273 after = {}
1276 after = {}
1274
1277
1275 # Expected shell script output.
1278 # Expected shell script output.
1276 expected = {}
1279 expected = {}
1277
1280
1278 pos = prepos = -1
1281 pos = prepos = -1
1279
1282
1280 # True or False when in a true or false conditional section
1283 # True or False when in a true or false conditional section
1281 skipping = None
1284 skipping = None
1282
1285
1283 # 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
1284 # can generate the surrounding doctest magic.
1287 # can generate the surrounding doctest magic.
1285 inpython = False
1288 inpython = False
1286
1289
1287 if self._debug:
1290 if self._debug:
1288 script.append(b'set -x\n')
1291 script.append(b'set -x\n')
1289 if self._hgcommand != b'hg':
1292 if self._hgcommand != b'hg':
1290 script.append(b'alias hg="%s"\n' % self._hgcommand)
1293 script.append(b'alias hg="%s"\n' % self._hgcommand)
1291 if os.getenv('MSYSTEM'):
1294 if os.getenv('MSYSTEM'):
1292 script.append(b'alias pwd="pwd -W"\n')
1295 script.append(b'alias pwd="pwd -W"\n')
1293
1296
1294 n = 0
1297 n = 0
1295 for n, l in enumerate(lines):
1298 for n, l in enumerate(lines):
1296 if not l.endswith(b'\n'):
1299 if not l.endswith(b'\n'):
1297 l += b'\n'
1300 l += b'\n'
1298 if l.startswith(b'#require'):
1301 if l.startswith(b'#require'):
1299 lsplit = l.split()
1302 lsplit = l.split()
1300 if len(lsplit) < 2 or lsplit[0] != b'#require':
1303 if len(lsplit) < 2 or lsplit[0] != b'#require':
1301 after.setdefault(pos, []).append(' !!! invalid #require\n')
1304 after.setdefault(pos, []).append(' !!! invalid #require\n')
1302 haveresult, message = self._hghave(lsplit[1:])
1305 haveresult, message = self._hghave(lsplit[1:])
1303 if not haveresult:
1306 if not haveresult:
1304 script = [b'echo "%s"\nexit 80\n' % message]
1307 script = [b'echo "%s"\nexit 80\n' % message]
1305 break
1308 break
1306 after.setdefault(pos, []).append(l)
1309 after.setdefault(pos, []).append(l)
1307 elif l.startswith(b'#if'):
1310 elif l.startswith(b'#if'):
1308 lsplit = l.split()
1311 lsplit = l.split()
1309 if len(lsplit) < 2 or lsplit[0] != b'#if':
1312 if len(lsplit) < 2 or lsplit[0] != b'#if':
1310 after.setdefault(pos, []).append(' !!! invalid #if\n')
1313 after.setdefault(pos, []).append(' !!! invalid #if\n')
1311 if skipping is not None:
1314 if skipping is not None:
1312 after.setdefault(pos, []).append(' !!! nested #if\n')
1315 after.setdefault(pos, []).append(' !!! nested #if\n')
1313 skipping = not self._iftest(lsplit[1:])
1316 skipping = not self._iftest(lsplit[1:])
1314 after.setdefault(pos, []).append(l)
1317 after.setdefault(pos, []).append(l)
1315 elif l.startswith(b'#else'):
1318 elif l.startswith(b'#else'):
1316 if skipping is None:
1319 if skipping is None:
1317 after.setdefault(pos, []).append(' !!! missing #if\n')
1320 after.setdefault(pos, []).append(' !!! missing #if\n')
1318 skipping = not skipping
1321 skipping = not skipping
1319 after.setdefault(pos, []).append(l)
1322 after.setdefault(pos, []).append(l)
1320 elif l.startswith(b'#endif'):
1323 elif l.startswith(b'#endif'):
1321 if skipping is None:
1324 if skipping is None:
1322 after.setdefault(pos, []).append(' !!! missing #if\n')
1325 after.setdefault(pos, []).append(' !!! missing #if\n')
1323 skipping = None
1326 skipping = None
1324 after.setdefault(pos, []).append(l)
1327 after.setdefault(pos, []).append(l)
1325 elif skipping:
1328 elif skipping:
1326 after.setdefault(pos, []).append(l)
1329 after.setdefault(pos, []).append(l)
1327 elif l.startswith(b' >>> '): # python inlines
1330 elif l.startswith(b' >>> '): # python inlines
1328 after.setdefault(pos, []).append(l)
1331 after.setdefault(pos, []).append(l)
1329 prepos = pos
1332 prepos = pos
1330 pos = n
1333 pos = n
1331 if not inpython:
1334 if not inpython:
1332 # We've just entered a Python block. Add the header.
1335 # We've just entered a Python block. Add the header.
1333 inpython = True
1336 inpython = True
1334 addsalt(prepos, False) # Make sure we report the exit code.
1337 addsalt(prepos, False) # Make sure we report the exit code.
1335 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1338 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1336 addsalt(n, True)
1339 addsalt(n, True)
1337 script.append(l[2:])
1340 script.append(l[2:])
1338 elif l.startswith(b' ... '): # python inlines
1341 elif l.startswith(b' ... '): # python inlines
1339 after.setdefault(prepos, []).append(l)
1342 after.setdefault(prepos, []).append(l)
1340 script.append(l[2:])
1343 script.append(l[2:])
1341 elif l.startswith(b' $ '): # commands
1344 elif l.startswith(b' $ '): # commands
1342 if inpython:
1345 if inpython:
1343 script.append(b'EOF\n')
1346 script.append(b'EOF\n')
1344 inpython = False
1347 inpython = False
1345 after.setdefault(pos, []).append(l)
1348 after.setdefault(pos, []).append(l)
1346 prepos = pos
1349 prepos = pos
1347 pos = n
1350 pos = n
1348 addsalt(n, False)
1351 addsalt(n, False)
1349 cmd = l[4:].split()
1352 cmd = l[4:].split()
1350 if len(cmd) == 2 and cmd[0] == b'cd':
1353 if len(cmd) == 2 and cmd[0] == b'cd':
1351 l = b' $ cd %s || exit 1\n' % cmd[1]
1354 l = b' $ cd %s || exit 1\n' % cmd[1]
1352 script.append(l[4:])
1355 script.append(l[4:])
1353 elif l.startswith(b' > '): # continuations
1356 elif l.startswith(b' > '): # continuations
1354 after.setdefault(prepos, []).append(l)
1357 after.setdefault(prepos, []).append(l)
1355 script.append(l[4:])
1358 script.append(l[4:])
1356 elif l.startswith(b' '): # results
1359 elif l.startswith(b' '): # results
1357 # Queue up a list of expected results.
1360 # Queue up a list of expected results.
1358 expected.setdefault(pos, []).append(l[2:])
1361 expected.setdefault(pos, []).append(l[2:])
1359 else:
1362 else:
1360 if inpython:
1363 if inpython:
1361 script.append(b'EOF\n')
1364 script.append(b'EOF\n')
1362 inpython = False
1365 inpython = False
1363 # Non-command/result. Queue up for merged output.
1366 # Non-command/result. Queue up for merged output.
1364 after.setdefault(pos, []).append(l)
1367 after.setdefault(pos, []).append(l)
1365
1368
1366 if inpython:
1369 if inpython:
1367 script.append(b'EOF\n')
1370 script.append(b'EOF\n')
1368 if skipping is not None:
1371 if skipping is not None:
1369 after.setdefault(pos, []).append(' !!! missing #endif\n')
1372 after.setdefault(pos, []).append(' !!! missing #endif\n')
1370 addsalt(n + 1, False)
1373 addsalt(n + 1, False)
1371
1374
1372 return salt, script, after, expected
1375 return salt, script, after, expected
1373
1376
1374 def _processoutput(self, exitcode, output, salt, after, expected):
1377 def _processoutput(self, exitcode, output, salt, after, expected):
1375 # Merge the script output back into a unified test.
1378 # Merge the script output back into a unified test.
1376 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1379 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1377 if exitcode != 0:
1380 if exitcode != 0:
1378 warnonly = 3
1381 warnonly = 3
1379
1382
1380 pos = -1
1383 pos = -1
1381 postout = []
1384 postout = []
1382 for l in output:
1385 for l in output:
1383 lout, lcmd = l, None
1386 lout, lcmd = l, None
1384 if salt in l:
1387 if salt in l:
1385 lout, lcmd = l.split(salt, 1)
1388 lout, lcmd = l.split(salt, 1)
1386
1389
1387 while lout:
1390 while lout:
1388 if not lout.endswith(b'\n'):
1391 if not lout.endswith(b'\n'):
1389 lout += b' (no-eol)\n'
1392 lout += b' (no-eol)\n'
1390
1393
1391 # Find the expected output at the current position.
1394 # Find the expected output at the current position.
1392 els = [None]
1395 els = [None]
1393 if expected.get(pos, None):
1396 if expected.get(pos, None):
1394 els = expected[pos]
1397 els = expected[pos]
1395
1398
1396 i = 0
1399 i = 0
1397 optional = []
1400 optional = []
1398 while i < len(els):
1401 while i < len(els):
1399 el = els[i]
1402 el = els[i]
1400
1403
1401 r = self.linematch(el, lout)
1404 r = self.linematch(el, lout)
1402 if isinstance(r, str):
1405 if isinstance(r, str):
1403 if r == '+glob':
1406 if r == '+glob':
1404 lout = el[:-1] + ' (glob)\n'
1407 lout = el[:-1] + ' (glob)\n'
1405 r = '' # Warn only this line.
1408 r = '' # Warn only this line.
1406 elif r == '-glob':
1409 elif r == '-glob':
1407 lout = ''.join(el.rsplit(' (glob)', 1))
1410 lout = ''.join(el.rsplit(' (glob)', 1))
1408 r = '' # Warn only this line.
1411 r = '' # Warn only this line.
1409 elif r == "retry":
1412 elif r == "retry":
1410 postout.append(b' ' + el)
1413 postout.append(b' ' + el)
1411 els.pop(i)
1414 els.pop(i)
1412 break
1415 break
1413 else:
1416 else:
1414 log('\ninfo, unknown linematch result: %r\n' % r)
1417 log('\ninfo, unknown linematch result: %r\n' % r)
1415 r = False
1418 r = False
1416 if r:
1419 if r:
1417 els.pop(i)
1420 els.pop(i)
1418 break
1421 break
1419 if el:
1422 if el:
1420 if el.endswith(b" (?)\n"):
1423 if el.endswith(b" (?)\n"):
1421 optional.append(i)
1424 optional.append(i)
1422 else:
1425 else:
1423 m = optline.match(el)
1426 m = optline.match(el)
1424 if m:
1427 if m:
1425 conditions = [
1428 conditions = [
1426 c for c in m.group(2).split(b' ')]
1429 c for c in m.group(2).split(b' ')]
1427
1430
1428 if not self._hghave(conditions)[0]:
1431 if not self._hghave(conditions)[0]:
1429 optional.append(i)
1432 optional.append(i)
1430
1433
1431 i += 1
1434 i += 1
1432
1435
1433 if r:
1436 if r:
1434 if r == "retry":
1437 if r == "retry":
1435 continue
1438 continue
1436 # clean up any optional leftovers
1439 # clean up any optional leftovers
1437 for i in optional:
1440 for i in optional:
1438 postout.append(b' ' + els[i])
1441 postout.append(b' ' + els[i])
1439 for i in reversed(optional):
1442 for i in reversed(optional):
1440 del els[i]
1443 del els[i]
1441 postout.append(b' ' + el)
1444 postout.append(b' ' + el)
1442 else:
1445 else:
1443 if self.NEEDESCAPE(lout):
1446 if self.NEEDESCAPE(lout):
1444 lout = TTest._stringescape(b'%s (esc)\n' %
1447 lout = TTest._stringescape(b'%s (esc)\n' %
1445 lout.rstrip(b'\n'))
1448 lout.rstrip(b'\n'))
1446 postout.append(b' ' + lout) # Let diff deal with it.
1449 postout.append(b' ' + lout) # Let diff deal with it.
1447 if r != '': # If line failed.
1450 if r != '': # If line failed.
1448 warnonly = 3 # for sure not
1451 warnonly = 3 # for sure not
1449 elif warnonly == 1: # Is "not yet" and line is warn only.
1452 elif warnonly == 1: # Is "not yet" and line is warn only.
1450 warnonly = 2 # Yes do warn.
1453 warnonly = 2 # Yes do warn.
1451 break
1454 break
1452 else:
1455 else:
1453 # clean up any optional leftovers
1456 # clean up any optional leftovers
1454 while expected.get(pos, None):
1457 while expected.get(pos, None):
1455 el = expected[pos].pop(0)
1458 el = expected[pos].pop(0)
1456 if el:
1459 if el:
1457 if not el.endswith(b" (?)\n"):
1460 if not el.endswith(b" (?)\n"):
1458 m = optline.match(el)
1461 m = optline.match(el)
1459 if m:
1462 if m:
1460 conditions = [c for c in m.group(2).split(' ')]
1463 conditions = [c for c in m.group(2).split(' ')]
1461
1464
1462 if self._hghave(conditions)[0]:
1465 if self._hghave(conditions)[0]:
1463 # Don't append as optional line
1466 # Don't append as optional line
1464 continue
1467 continue
1465 else:
1468 else:
1466 continue
1469 continue
1467 postout.append(b' ' + el)
1470 postout.append(b' ' + el)
1468
1471
1469 if lcmd:
1472 if lcmd:
1470 # Add on last return code.
1473 # Add on last return code.
1471 ret = int(lcmd.split()[1])
1474 ret = int(lcmd.split()[1])
1472 if ret != 0:
1475 if ret != 0:
1473 postout.append(b' [%d]\n' % ret)
1476 postout.append(b' [%d]\n' % ret)
1474 if pos in after:
1477 if pos in after:
1475 # Merge in non-active test bits.
1478 # Merge in non-active test bits.
1476 postout += after.pop(pos)
1479 postout += after.pop(pos)
1477 pos = int(lcmd.split()[0])
1480 pos = int(lcmd.split()[0])
1478
1481
1479 if pos in after:
1482 if pos in after:
1480 postout += after.pop(pos)
1483 postout += after.pop(pos)
1481
1484
1482 if warnonly == 2:
1485 if warnonly == 2:
1483 exitcode = False # Set exitcode to warned.
1486 exitcode = False # Set exitcode to warned.
1484
1487
1485 return exitcode, postout
1488 return exitcode, postout
1486
1489
1487 @staticmethod
1490 @staticmethod
1488 def rematch(el, l):
1491 def rematch(el, l):
1489 try:
1492 try:
1490 # 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
1491 if os.name == 'nt':
1494 if os.name == 'nt':
1492 return re.match(el + br'\r?\n\Z', l)
1495 return re.match(el + br'\r?\n\Z', l)
1493 return re.match(el + br'\n\Z', l)
1496 return re.match(el + br'\n\Z', l)
1494 except re.error:
1497 except re.error:
1495 # el is an invalid regex
1498 # el is an invalid regex
1496 return False
1499 return False
1497
1500
1498 @staticmethod
1501 @staticmethod
1499 def globmatch(el, l):
1502 def globmatch(el, l):
1500 # The only supported special characters are * and ? plus / which also
1503 # The only supported special characters are * and ? plus / which also
1501 # matches \ on windows. Escaping of these characters is supported.
1504 # matches \ on windows. Escaping of these characters is supported.
1502 if el + b'\n' == l:
1505 if el + b'\n' == l:
1503 if os.altsep:
1506 if os.altsep:
1504 # matching on "/" is not needed for this line
1507 # matching on "/" is not needed for this line
1505 for pat in checkcodeglobpats:
1508 for pat in checkcodeglobpats:
1506 if pat.match(el):
1509 if pat.match(el):
1507 return True
1510 return True
1508 return b'-glob'
1511 return b'-glob'
1509 return True
1512 return True
1510 el = el.replace(b'$LOCALIP', b'*')
1513 el = el.replace(b'$LOCALIP', b'*')
1511 i, n = 0, len(el)
1514 i, n = 0, len(el)
1512 res = b''
1515 res = b''
1513 while i < n:
1516 while i < n:
1514 c = el[i:i + 1]
1517 c = el[i:i + 1]
1515 i += 1
1518 i += 1
1516 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'*?\\/':
1517 res += el[i - 1:i + 1]
1520 res += el[i - 1:i + 1]
1518 i += 1
1521 i += 1
1519 elif c == b'*':
1522 elif c == b'*':
1520 res += b'.*'
1523 res += b'.*'
1521 elif c == b'?':
1524 elif c == b'?':
1522 res += b'.'
1525 res += b'.'
1523 elif c == b'/' and os.altsep:
1526 elif c == b'/' and os.altsep:
1524 res += b'[/\\\\]'
1527 res += b'[/\\\\]'
1525 else:
1528 else:
1526 res += re.escape(c)
1529 res += re.escape(c)
1527 return TTest.rematch(res, l)
1530 return TTest.rematch(res, l)
1528
1531
1529 def linematch(self, el, l):
1532 def linematch(self, el, l):
1530 retry = False
1533 retry = False
1531 if el == l: # perfect match (fast)
1534 if el == l: # perfect match (fast)
1532 return True
1535 return True
1533 if el:
1536 if el:
1534 if el.endswith(b" (?)\n"):
1537 if el.endswith(b" (?)\n"):
1535 retry = "retry"
1538 retry = "retry"
1536 el = el[:-5] + b"\n"
1539 el = el[:-5] + b"\n"
1537 else:
1540 else:
1538 m = optline.match(el)
1541 m = optline.match(el)
1539 if m:
1542 if m:
1540 conditions = [c for c in m.group(2).split(b' ')]
1543 conditions = [c for c in m.group(2).split(b' ')]
1541
1544
1542 el = m.group(1) + b"\n"
1545 el = m.group(1) + b"\n"
1543 if not self._hghave(conditions)[0]:
1546 if not self._hghave(conditions)[0]:
1544 retry = "retry" # Not required by listed features
1547 retry = "retry" # Not required by listed features
1545
1548
1546 if el.endswith(b" (esc)\n"):
1549 if el.endswith(b" (esc)\n"):
1547 if PYTHON3:
1550 if PYTHON3:
1548 el = el[:-7].decode('unicode_escape') + '\n'
1551 el = el[:-7].decode('unicode_escape') + '\n'
1549 el = el.encode('utf-8')
1552 el = el.encode('utf-8')
1550 else:
1553 else:
1551 el = el[:-7].decode('string-escape') + '\n'
1554 el = el[:-7].decode('string-escape') + '\n'
1552 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:
1553 return True
1556 return True
1554 if el.endswith(b" (re)\n"):
1557 if el.endswith(b" (re)\n"):
1555 return TTest.rematch(el[:-6], l) or retry
1558 return TTest.rematch(el[:-6], l) or retry
1556 if el.endswith(b" (glob)\n"):
1559 if el.endswith(b" (glob)\n"):
1557 # ignore '(glob)' added to l by 'replacements'
1560 # ignore '(glob)' added to l by 'replacements'
1558 if l.endswith(b" (glob)\n"):
1561 if l.endswith(b" (glob)\n"):
1559 l = l[:-8] + b"\n"
1562 l = l[:-8] + b"\n"
1560 return TTest.globmatch(el[:-8], l) or retry
1563 return TTest.globmatch(el[:-8], l) or retry
1561 if os.altsep and l.replace(b'\\', b'/') == el:
1564 if os.altsep and l.replace(b'\\', b'/') == el:
1562 return b'+glob'
1565 return b'+glob'
1563 return retry
1566 return retry
1564
1567
1565 @staticmethod
1568 @staticmethod
1566 def parsehghaveoutput(lines):
1569 def parsehghaveoutput(lines):
1567 '''Parse hghave log lines.
1570 '''Parse hghave log lines.
1568
1571
1569 Return tuple of lists (missing, failed):
1572 Return tuple of lists (missing, failed):
1570 * the missing/unknown features
1573 * the missing/unknown features
1571 * the features for which existence check failed'''
1574 * the features for which existence check failed'''
1572 missing = []
1575 missing = []
1573 failed = []
1576 failed = []
1574 for line in lines:
1577 for line in lines:
1575 if line.startswith(TTest.SKIPPED_PREFIX):
1578 if line.startswith(TTest.SKIPPED_PREFIX):
1576 line = line.splitlines()[0]
1579 line = line.splitlines()[0]
1577 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1580 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1578 elif line.startswith(TTest.FAILED_PREFIX):
1581 elif line.startswith(TTest.FAILED_PREFIX):
1579 line = line.splitlines()[0]
1582 line = line.splitlines()[0]
1580 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1583 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1581
1584
1582 return missing, failed
1585 return missing, failed
1583
1586
1584 @staticmethod
1587 @staticmethod
1585 def _escapef(m):
1588 def _escapef(m):
1586 return TTest.ESCAPEMAP[m.group(0)]
1589 return TTest.ESCAPEMAP[m.group(0)]
1587
1590
1588 @staticmethod
1591 @staticmethod
1589 def _stringescape(s):
1592 def _stringescape(s):
1590 return TTest.ESCAPESUB(TTest._escapef, s)
1593 return TTest.ESCAPESUB(TTest._escapef, s)
1591
1594
1592 iolock = threading.RLock()
1595 iolock = threading.RLock()
1593
1596
1594 class TestResult(unittest._TextTestResult):
1597 class TestResult(unittest._TextTestResult):
1595 """Holds results when executing via unittest."""
1598 """Holds results when executing via unittest."""
1596 # Don't worry too much about accessing the non-public _TextTestResult.
1599 # Don't worry too much about accessing the non-public _TextTestResult.
1597 # It is relatively common in Python testing tools.
1600 # It is relatively common in Python testing tools.
1598 def __init__(self, options, *args, **kwargs):
1601 def __init__(self, options, *args, **kwargs):
1599 super(TestResult, self).__init__(*args, **kwargs)
1602 super(TestResult, self).__init__(*args, **kwargs)
1600
1603
1601 self._options = options
1604 self._options = options
1602
1605
1603 # 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
1604 # polyfill it.
1607 # polyfill it.
1605 self.skipped = []
1608 self.skipped = []
1606
1609
1607 # 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
1608 # unittest implementation. It is very similar to skipped. It may make
1611 # unittest implementation. It is very similar to skipped. It may make
1609 # sense to map it into skip some day.
1612 # sense to map it into skip some day.
1610 self.ignored = []
1613 self.ignored = []
1611
1614
1612 self.times = []
1615 self.times = []
1613 self._firststarttime = None
1616 self._firststarttime = None
1614 # Data stored for the benefit of generating xunit reports.
1617 # Data stored for the benefit of generating xunit reports.
1615 self.successes = []
1618 self.successes = []
1616 self.faildata = {}
1619 self.faildata = {}
1617
1620
1618 if options.color == 'auto':
1621 if options.color == 'auto':
1619 self.color = pygmentspresent and self.stream.isatty()
1622 self.color = pygmentspresent and self.stream.isatty()
1620 elif options.color == 'never':
1623 elif options.color == 'never':
1621 self.color = False
1624 self.color = False
1622 else: # 'always', for testing purposes
1625 else: # 'always', for testing purposes
1623 self.color = pygmentspresent
1626 self.color = pygmentspresent
1624
1627
1625 def addFailure(self, test, reason):
1628 def addFailure(self, test, reason):
1626 self.failures.append((test, reason))
1629 self.failures.append((test, reason))
1627
1630
1628 if self._options.first:
1631 if self._options.first:
1629 self.stop()
1632 self.stop()
1630 else:
1633 else:
1631 with iolock:
1634 with iolock:
1632 if reason == "timed out":
1635 if reason == "timed out":
1633 self.stream.write('t')
1636 self.stream.write('t')
1634 else:
1637 else:
1635 if not self._options.nodiff:
1638 if not self._options.nodiff:
1636 formatted = '\nERROR: %s output changed\n' % test
1639 formatted = '\nERROR: %s output changed\n' % test
1637 if self.color:
1640 if self.color:
1638 formatted = pygments.highlight(
1641 formatted = pygments.highlight(
1639 formatted,
1642 formatted,
1640 TestRunnerLexer(),
1643 runnerlexer,
1641 formatters.Terminal256Formatter(
1644 runnerformatter)
1642 style=TestRunnerStyle))
1643 self.stream.write(formatted)
1645 self.stream.write(formatted)
1644 self.stream.write('!')
1646 self.stream.write('!')
1645
1647
1646 self.stream.flush()
1648 self.stream.flush()
1647
1649
1648 def addSuccess(self, test):
1650 def addSuccess(self, test):
1649 with iolock:
1651 with iolock:
1650 super(TestResult, self).addSuccess(test)
1652 super(TestResult, self).addSuccess(test)
1651 self.successes.append(test)
1653 self.successes.append(test)
1652
1654
1653 def addError(self, test, err):
1655 def addError(self, test, err):
1654 super(TestResult, self).addError(test, err)
1656 super(TestResult, self).addError(test, err)
1655 if self._options.first:
1657 if self._options.first:
1656 self.stop()
1658 self.stop()
1657
1659
1658 # Polyfill.
1660 # Polyfill.
1659 def addSkip(self, test, reason):
1661 def addSkip(self, test, reason):
1660 self.skipped.append((test, reason))
1662 self.skipped.append((test, reason))
1661 with iolock:
1663 with iolock:
1662 if self.showAll:
1664 if self.showAll:
1663 self.stream.writeln('skipped %s' % reason)
1665 self.stream.writeln('skipped %s' % reason)
1664 else:
1666 else:
1665 self.stream.write('s')
1667 self.stream.write('s')
1666 self.stream.flush()
1668 self.stream.flush()
1667
1669
1668 def addIgnore(self, test, reason):
1670 def addIgnore(self, test, reason):
1669 self.ignored.append((test, reason))
1671 self.ignored.append((test, reason))
1670 with iolock:
1672 with iolock:
1671 if self.showAll:
1673 if self.showAll:
1672 self.stream.writeln('ignored %s' % reason)
1674 self.stream.writeln('ignored %s' % reason)
1673 else:
1675 else:
1674 if reason not in ('not retesting', "doesn't match keyword"):
1676 if reason not in ('not retesting', "doesn't match keyword"):
1675 self.stream.write('i')
1677 self.stream.write('i')
1676 else:
1678 else:
1677 self.testsRun += 1
1679 self.testsRun += 1
1678 self.stream.flush()
1680 self.stream.flush()
1679
1681
1680 def addOutputMismatch(self, test, ret, got, expected):
1682 def addOutputMismatch(self, test, ret, got, expected):
1681 """Record a mismatch in test output for a particular test."""
1683 """Record a mismatch in test output for a particular test."""
1682 if self.shouldStop:
1684 if self.shouldStop:
1683 # don't print, some other test case already failed and
1685 # don't print, some other test case already failed and
1684 # printed, we're just stale and probably failed due to our
1686 # printed, we're just stale and probably failed due to our
1685 # temp dir getting cleaned up.
1687 # temp dir getting cleaned up.
1686 return
1688 return
1687
1689
1688 accepted = False
1690 accepted = False
1689 lines = []
1691 lines = []
1690
1692
1691 with iolock:
1693 with iolock:
1692 if self._options.nodiff:
1694 if self._options.nodiff:
1693 pass
1695 pass
1694 elif self._options.view:
1696 elif self._options.view:
1695 v = self._options.view
1697 v = self._options.view
1696 if PYTHON3:
1698 if PYTHON3:
1697 v = _bytespath(v)
1699 v = _bytespath(v)
1698 os.system(b"%s %s %s" %
1700 os.system(b"%s %s %s" %
1699 (v, test.refpath, test.errpath))
1701 (v, test.refpath, test.errpath))
1700 else:
1702 else:
1701 servefail, lines = getdiff(expected, got,
1703 servefail, lines = getdiff(expected, got,
1702 test.refpath, test.errpath)
1704 test.refpath, test.errpath)
1703 if servefail:
1705 if servefail:
1704 raise test.failureException(
1706 raise test.failureException(
1705 'server failed to start (HGPORT=%s)' % test._startport)
1707 'server failed to start (HGPORT=%s)' % test._startport)
1706 else:
1708 else:
1707 self.stream.write('\n')
1709 self.stream.write('\n')
1708 for line in lines:
1710 for line in lines:
1709 if self.color:
1711 if self.color:
1710 line = pygments.highlight(line,
1712 line = pygments.highlight(line,
1711 difflexer,
1713 difflexer,
1712 terminal256formatter)
1714 terminal256formatter)
1713 if PYTHON3:
1715 if PYTHON3:
1714 self.stream.flush()
1716 self.stream.flush()
1715 self.stream.buffer.write(line)
1717 self.stream.buffer.write(line)
1716 self.stream.buffer.flush()
1718 self.stream.buffer.flush()
1717 else:
1719 else:
1718 self.stream.write(line)
1720 self.stream.write(line)
1719 self.stream.flush()
1721 self.stream.flush()
1720
1722
1721 # handle interactive prompt without releasing iolock
1723 # handle interactive prompt without releasing iolock
1722 if self._options.interactive:
1724 if self._options.interactive:
1723 if test.readrefout() != expected:
1725 if test.readrefout() != expected:
1724 self.stream.write(
1726 self.stream.write(
1725 'Reference output has changed (run again to prompt '
1727 'Reference output has changed (run again to prompt '
1726 'changes)')
1728 'changes)')
1727 else:
1729 else:
1728 self.stream.write('Accept this change? [n] ')
1730 self.stream.write('Accept this change? [n] ')
1729 answer = sys.stdin.readline().strip()
1731 answer = sys.stdin.readline().strip()
1730 if answer.lower() in ('y', 'yes'):
1732 if answer.lower() in ('y', 'yes'):
1731 if test.path.endswith(b'.t'):
1733 if test.path.endswith(b'.t'):
1732 rename(test.errpath, test.path)
1734 rename(test.errpath, test.path)
1733 else:
1735 else:
1734 rename(test.errpath, '%s.out' % test.path)
1736 rename(test.errpath, '%s.out' % test.path)
1735 accepted = True
1737 accepted = True
1736 if not accepted:
1738 if not accepted:
1737 self.faildata[test.name] = b''.join(lines)
1739 self.faildata[test.name] = b''.join(lines)
1738
1740
1739 return accepted
1741 return accepted
1740
1742
1741 def startTest(self, test):
1743 def startTest(self, test):
1742 super(TestResult, self).startTest(test)
1744 super(TestResult, self).startTest(test)
1743
1745
1744 # 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
1745 # 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.
1746 # 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
1747 # and not for Windows.
1749 # and not for Windows.
1748 test.started = os.times()
1750 test.started = os.times()
1749 if self._firststarttime is None: # thread racy but irrelevant
1751 if self._firststarttime is None: # thread racy but irrelevant
1750 self._firststarttime = test.started[4]
1752 self._firststarttime = test.started[4]
1751
1753
1752 def stopTest(self, test, interrupted=False):
1754 def stopTest(self, test, interrupted=False):
1753 super(TestResult, self).stopTest(test)
1755 super(TestResult, self).stopTest(test)
1754
1756
1755 test.stopped = os.times()
1757 test.stopped = os.times()
1756
1758
1757 starttime = test.started
1759 starttime = test.started
1758 endtime = test.stopped
1760 endtime = test.stopped
1759 origin = self._firststarttime
1761 origin = self._firststarttime
1760 self.times.append((test.name,
1762 self.times.append((test.name,
1761 endtime[2] - starttime[2], # user space CPU time
1763 endtime[2] - starttime[2], # user space CPU time
1762 endtime[3] - starttime[3], # sys space CPU time
1764 endtime[3] - starttime[3], # sys space CPU time
1763 endtime[4] - starttime[4], # real time
1765 endtime[4] - starttime[4], # real time
1764 starttime[4] - origin, # start date in run context
1766 starttime[4] - origin, # start date in run context
1765 endtime[4] - origin, # end date in run context
1767 endtime[4] - origin, # end date in run context
1766 ))
1768 ))
1767
1769
1768 if interrupted:
1770 if interrupted:
1769 with iolock:
1771 with iolock:
1770 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1772 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1771 test.name, self.times[-1][3]))
1773 test.name, self.times[-1][3]))
1772
1774
1773 class TestSuite(unittest.TestSuite):
1775 class TestSuite(unittest.TestSuite):
1774 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1776 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1775
1777
1776 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1778 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1777 retest=False, keywords=None, loop=False, runs_per_test=1,
1779 retest=False, keywords=None, loop=False, runs_per_test=1,
1778 loadtest=None, showchannels=False,
1780 loadtest=None, showchannels=False,
1779 *args, **kwargs):
1781 *args, **kwargs):
1780 """Create a new instance that can run tests with a configuration.
1782 """Create a new instance that can run tests with a configuration.
1781
1783
1782 testdir specifies the directory where tests are executed from. This
1784 testdir specifies the directory where tests are executed from. This
1783 is typically the ``tests`` directory from Mercurial's source
1785 is typically the ``tests`` directory from Mercurial's source
1784 repository.
1786 repository.
1785
1787
1786 jobs specifies the number of jobs to run concurrently. Each test
1788 jobs specifies the number of jobs to run concurrently. Each test
1787 executes on its own thread. Tests actually spawn new processes, so
1789 executes on its own thread. Tests actually spawn new processes, so
1788 state mutation should not be an issue.
1790 state mutation should not be an issue.
1789
1791
1790 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.
1791
1793
1792 whitelist and blacklist denote tests that have been whitelisted and
1794 whitelist and blacklist denote tests that have been whitelisted and
1793 blacklisted, respectively. These arguments don't belong in TestSuite.
1795 blacklisted, respectively. These arguments don't belong in TestSuite.
1794 Instead, whitelist and blacklist should be handled by the thing that
1796 Instead, whitelist and blacklist should be handled by the thing that
1795 populates the TestSuite with tests. They are present to preserve
1797 populates the TestSuite with tests. They are present to preserve
1796 backwards compatible behavior which reports skipped tests as part
1798 backwards compatible behavior which reports skipped tests as part
1797 of the results.
1799 of the results.
1798
1800
1799 retest denotes whether to retest failed tests. This arguably belongs
1801 retest denotes whether to retest failed tests. This arguably belongs
1800 outside of TestSuite.
1802 outside of TestSuite.
1801
1803
1802 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
1803 to execute. This arguably belongs outside of TestSuite.
1805 to execute. This arguably belongs outside of TestSuite.
1804
1806
1805 loop denotes whether to loop over tests forever.
1807 loop denotes whether to loop over tests forever.
1806 """
1808 """
1807 super(TestSuite, self).__init__(*args, **kwargs)
1809 super(TestSuite, self).__init__(*args, **kwargs)
1808
1810
1809 self._jobs = jobs
1811 self._jobs = jobs
1810 self._whitelist = whitelist
1812 self._whitelist = whitelist
1811 self._blacklist = blacklist
1813 self._blacklist = blacklist
1812 self._retest = retest
1814 self._retest = retest
1813 self._keywords = keywords
1815 self._keywords = keywords
1814 self._loop = loop
1816 self._loop = loop
1815 self._runs_per_test = runs_per_test
1817 self._runs_per_test = runs_per_test
1816 self._loadtest = loadtest
1818 self._loadtest = loadtest
1817 self._showchannels = showchannels
1819 self._showchannels = showchannels
1818
1820
1819 def run(self, result):
1821 def run(self, result):
1820 # 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
1821 # 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
1822 # Test simpler.
1824 # Test simpler.
1823 tests = []
1825 tests = []
1824 num_tests = [0]
1826 num_tests = [0]
1825 for test in self._tests:
1827 for test in self._tests:
1826 def get():
1828 def get():
1827 num_tests[0] += 1
1829 num_tests[0] += 1
1828 if getattr(test, 'should_reload', False):
1830 if getattr(test, 'should_reload', False):
1829 return self._loadtest(test, num_tests[0])
1831 return self._loadtest(test, num_tests[0])
1830 return test
1832 return test
1831 if not os.path.exists(test.path):
1833 if not os.path.exists(test.path):
1832 result.addSkip(test, "Doesn't exist")
1834 result.addSkip(test, "Doesn't exist")
1833 continue
1835 continue
1834
1836
1835 if not (self._whitelist and test.name in self._whitelist):
1837 if not (self._whitelist and test.name in self._whitelist):
1836 if self._blacklist and test.bname in self._blacklist:
1838 if self._blacklist and test.bname in self._blacklist:
1837 result.addSkip(test, 'blacklisted')
1839 result.addSkip(test, 'blacklisted')
1838 continue
1840 continue
1839
1841
1840 if self._retest and not os.path.exists(test.errpath):
1842 if self._retest and not os.path.exists(test.errpath):
1841 result.addIgnore(test, 'not retesting')
1843 result.addIgnore(test, 'not retesting')
1842 continue
1844 continue
1843
1845
1844 if self._keywords:
1846 if self._keywords:
1845 f = open(test.path, 'rb')
1847 f = open(test.path, 'rb')
1846 t = f.read().lower() + test.bname.lower()
1848 t = f.read().lower() + test.bname.lower()
1847 f.close()
1849 f.close()
1848 ignored = False
1850 ignored = False
1849 for k in self._keywords.lower().split():
1851 for k in self._keywords.lower().split():
1850 if k not in t:
1852 if k not in t:
1851 result.addIgnore(test, "doesn't match keyword")
1853 result.addIgnore(test, "doesn't match keyword")
1852 ignored = True
1854 ignored = True
1853 break
1855 break
1854
1856
1855 if ignored:
1857 if ignored:
1856 continue
1858 continue
1857 for _ in xrange(self._runs_per_test):
1859 for _ in xrange(self._runs_per_test):
1858 tests.append(get())
1860 tests.append(get())
1859
1861
1860 runtests = list(tests)
1862 runtests = list(tests)
1861 done = queue.Queue()
1863 done = queue.Queue()
1862 running = 0
1864 running = 0
1863
1865
1864 channels = [""] * self._jobs
1866 channels = [""] * self._jobs
1865
1867
1866 def job(test, result):
1868 def job(test, result):
1867 for n, v in enumerate(channels):
1869 for n, v in enumerate(channels):
1868 if not v:
1870 if not v:
1869 channel = n
1871 channel = n
1870 break
1872 break
1871 else:
1873 else:
1872 raise ValueError('Could not find output channel')
1874 raise ValueError('Could not find output channel')
1873 channels[channel] = "=" + test.name[5:].split(".")[0]
1875 channels[channel] = "=" + test.name[5:].split(".")[0]
1874 try:
1876 try:
1875 test(result)
1877 test(result)
1876 done.put(None)
1878 done.put(None)
1877 except KeyboardInterrupt:
1879 except KeyboardInterrupt:
1878 pass
1880 pass
1879 except: # re-raises
1881 except: # re-raises
1880 done.put(('!', test, 'run-test raised an error, see traceback'))
1882 done.put(('!', test, 'run-test raised an error, see traceback'))
1881 raise
1883 raise
1882 finally:
1884 finally:
1883 try:
1885 try:
1884 channels[channel] = ''
1886 channels[channel] = ''
1885 except IndexError:
1887 except IndexError:
1886 pass
1888 pass
1887
1889
1888 def stat():
1890 def stat():
1889 count = 0
1891 count = 0
1890 while channels:
1892 while channels:
1891 d = '\n%03s ' % count
1893 d = '\n%03s ' % count
1892 for n, v in enumerate(channels):
1894 for n, v in enumerate(channels):
1893 if v:
1895 if v:
1894 d += v[0]
1896 d += v[0]
1895 channels[n] = v[1:] or '.'
1897 channels[n] = v[1:] or '.'
1896 else:
1898 else:
1897 d += ' '
1899 d += ' '
1898 d += ' '
1900 d += ' '
1899 with iolock:
1901 with iolock:
1900 sys.stdout.write(d + ' ')
1902 sys.stdout.write(d + ' ')
1901 sys.stdout.flush()
1903 sys.stdout.flush()
1902 for x in xrange(10):
1904 for x in xrange(10):
1903 if channels:
1905 if channels:
1904 time.sleep(.1)
1906 time.sleep(.1)
1905 count += 1
1907 count += 1
1906
1908
1907 stoppedearly = False
1909 stoppedearly = False
1908
1910
1909 if self._showchannels:
1911 if self._showchannels:
1910 statthread = threading.Thread(target=stat, name="stat")
1912 statthread = threading.Thread(target=stat, name="stat")
1911 statthread.start()
1913 statthread.start()
1912
1914
1913 try:
1915 try:
1914 while tests or running:
1916 while tests or running:
1915 if not done.empty() or running == self._jobs or not tests:
1917 if not done.empty() or running == self._jobs or not tests:
1916 try:
1918 try:
1917 done.get(True, 1)
1919 done.get(True, 1)
1918 running -= 1
1920 running -= 1
1919 if result and result.shouldStop:
1921 if result and result.shouldStop:
1920 stoppedearly = True
1922 stoppedearly = True
1921 break
1923 break
1922 except queue.Empty:
1924 except queue.Empty:
1923 continue
1925 continue
1924 if tests and not running == self._jobs:
1926 if tests and not running == self._jobs:
1925 test = tests.pop(0)
1927 test = tests.pop(0)
1926 if self._loop:
1928 if self._loop:
1927 if getattr(test, 'should_reload', False):
1929 if getattr(test, 'should_reload', False):
1928 num_tests[0] += 1
1930 num_tests[0] += 1
1929 tests.append(
1931 tests.append(
1930 self._loadtest(test, num_tests[0]))
1932 self._loadtest(test, num_tests[0]))
1931 else:
1933 else:
1932 tests.append(test)
1934 tests.append(test)
1933 if self._jobs == 1:
1935 if self._jobs == 1:
1934 job(test, result)
1936 job(test, result)
1935 else:
1937 else:
1936 t = threading.Thread(target=job, name=test.name,
1938 t = threading.Thread(target=job, name=test.name,
1937 args=(test, result))
1939 args=(test, result))
1938 t.start()
1940 t.start()
1939 running += 1
1941 running += 1
1940
1942
1941 # 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
1942 # finish. Otherwise, there is a race between the test completing
1944 # finish. Otherwise, there is a race between the test completing
1943 # 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
1944 # test reporting incorrect.
1946 # test reporting incorrect.
1945 if stoppedearly:
1947 if stoppedearly:
1946 while running:
1948 while running:
1947 try:
1949 try:
1948 done.get(True, 1)
1950 done.get(True, 1)
1949 running -= 1
1951 running -= 1
1950 except queue.Empty:
1952 except queue.Empty:
1951 continue
1953 continue
1952 except KeyboardInterrupt:
1954 except KeyboardInterrupt:
1953 for test in runtests:
1955 for test in runtests:
1954 test.abort()
1956 test.abort()
1955
1957
1956 channels = []
1958 channels = []
1957
1959
1958 return result
1960 return result
1959
1961
1960 # 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
1961 # human-readable text file named .testtimes. Tests are sorted
1963 # human-readable text file named .testtimes. Tests are sorted
1962 # alphabetically, while times for each test are listed from oldest to
1964 # alphabetically, while times for each test are listed from oldest to
1963 # newest.
1965 # newest.
1964
1966
1965 def loadtimes(outputdir):
1967 def loadtimes(outputdir):
1966 times = []
1968 times = []
1967 try:
1969 try:
1968 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
1970 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
1969 for line in fp:
1971 for line in fp:
1970 ts = line.split()
1972 ts = line.split()
1971 times.append((ts[0], [float(t) for t in ts[1:]]))
1973 times.append((ts[0], [float(t) for t in ts[1:]]))
1972 except IOError as err:
1974 except IOError as err:
1973 if err.errno != errno.ENOENT:
1975 if err.errno != errno.ENOENT:
1974 raise
1976 raise
1975 return times
1977 return times
1976
1978
1977 def savetimes(outputdir, result):
1979 def savetimes(outputdir, result):
1978 saved = dict(loadtimes(outputdir))
1980 saved = dict(loadtimes(outputdir))
1979 maxruns = 5
1981 maxruns = 5
1980 skipped = set([str(t[0]) for t in result.skipped])
1982 skipped = set([str(t[0]) for t in result.skipped])
1981 for tdata in result.times:
1983 for tdata in result.times:
1982 test, real = tdata[0], tdata[3]
1984 test, real = tdata[0], tdata[3]
1983 if test not in skipped:
1985 if test not in skipped:
1984 ts = saved.setdefault(test, [])
1986 ts = saved.setdefault(test, [])
1985 ts.append(real)
1987 ts.append(real)
1986 ts[:] = ts[-maxruns:]
1988 ts[:] = ts[-maxruns:]
1987
1989
1988 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
1990 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
1989 dir=outputdir, text=True)
1991 dir=outputdir, text=True)
1990 with os.fdopen(fd, 'w') as fp:
1992 with os.fdopen(fd, 'w') as fp:
1991 for name, ts in sorted(saved.items()):
1993 for name, ts in sorted(saved.items()):
1992 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])))
1993 timepath = os.path.join(outputdir, b'.testtimes')
1995 timepath = os.path.join(outputdir, b'.testtimes')
1994 try:
1996 try:
1995 os.unlink(timepath)
1997 os.unlink(timepath)
1996 except OSError:
1998 except OSError:
1997 pass
1999 pass
1998 try:
2000 try:
1999 os.rename(tmpname, timepath)
2001 os.rename(tmpname, timepath)
2000 except OSError:
2002 except OSError:
2001 pass
2003 pass
2002
2004
2003 class TextTestRunner(unittest.TextTestRunner):
2005 class TextTestRunner(unittest.TextTestRunner):
2004 """Custom unittest test runner that uses appropriate settings."""
2006 """Custom unittest test runner that uses appropriate settings."""
2005
2007
2006 def __init__(self, runner, *args, **kwargs):
2008 def __init__(self, runner, *args, **kwargs):
2007 super(TextTestRunner, self).__init__(*args, **kwargs)
2009 super(TextTestRunner, self).__init__(*args, **kwargs)
2008
2010
2009 self._runner = runner
2011 self._runner = runner
2010
2012
2011 def listtests(self, test):
2013 def listtests(self, test):
2012 result = TestResult(self._runner.options, self.stream,
2014 result = TestResult(self._runner.options, self.stream,
2013 self.descriptions, 0)
2015 self.descriptions, 0)
2014 test = sorted(test, key=lambda t: t.name)
2016 test = sorted(test, key=lambda t: t.name)
2015 for t in test:
2017 for t in test:
2016 print(t.name)
2018 print(t.name)
2017 result.addSuccess(t)
2019 result.addSuccess(t)
2018
2020
2019 if self._runner.options.xunit:
2021 if self._runner.options.xunit:
2020 with open(self._runner.options.xunit, "wb") as xuf:
2022 with open(self._runner.options.xunit, "wb") as xuf:
2021 self._writexunit(result, xuf)
2023 self._writexunit(result, xuf)
2022
2024
2023 if self._runner.options.json:
2025 if self._runner.options.json:
2024 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2026 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2025 with open(jsonpath, 'w') as fp:
2027 with open(jsonpath, 'w') as fp:
2026 self._writejson(result, fp)
2028 self._writejson(result, fp)
2027
2029
2028 return result
2030 return result
2029
2031
2030 def run(self, test):
2032 def run(self, test):
2031 result = TestResult(self._runner.options, self.stream,
2033 result = TestResult(self._runner.options, self.stream,
2032 self.descriptions, self.verbosity)
2034 self.descriptions, self.verbosity)
2033
2035
2034 test(result)
2036 test(result)
2035
2037
2036 failed = len(result.failures)
2038 failed = len(result.failures)
2037 skipped = len(result.skipped)
2039 skipped = len(result.skipped)
2038 ignored = len(result.ignored)
2040 ignored = len(result.ignored)
2039
2041
2040 with iolock:
2042 with iolock:
2041 self.stream.writeln('')
2043 self.stream.writeln('')
2042
2044
2043 if not self._runner.options.noskips:
2045 if not self._runner.options.noskips:
2044 for test, msg in result.skipped:
2046 for test, msg in result.skipped:
2045 formatted = 'Skipped %s: %s' % (test.name, msg)
2047 formatted = 'Skipped %s: %s' % (test.name, msg)
2046 if result.color:
2048 if result.color:
2047 formatted = pygments.highlight(
2049 formatted = pygments.highlight(
2048 formatted,
2050 formatted,
2049 TestRunnerLexer(),
2051 runnerlexer,
2050 formatters.Terminal256Formatter(
2052 runnerformatter).strip("\n")
2051 style=TestRunnerStyle)).strip("\n")
2052 self.stream.writeln(formatted)
2053 self.stream.writeln(formatted)
2053 for test, msg in result.failures:
2054 for test, msg in result.failures:
2054 formatted = 'Failed %s: %s' % (test.name, msg)
2055 formatted = 'Failed %s: %s' % (test.name, msg)
2055 if result.color:
2056 if result.color:
2056 formatted = pygments.highlight(
2057 formatted = pygments.highlight(
2057 formatted,
2058 formatted,
2058 TestRunnerLexer(),
2059 runnerlexer,
2059 formatters.Terminal256Formatter(
2060 runnerformatter).strip("\n")
2060 style=TestRunnerStyle)).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