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