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