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