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