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