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