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