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