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