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