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