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