##// END OF EJS Templates
run-tests: cache hghave results...
Matt Harbison -
r36480:5c1cea8a default
parent child Browse files
Show More
@@ -1,3087 +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 env.keys():
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
1240
1240 @property
1241 @property
1241 def refpath(self):
1242 def refpath(self):
1242 return os.path.join(self._testdir, self.bname)
1243 return os.path.join(self._testdir, self.bname)
1243
1244
1244 def _run(self, env):
1245 def _run(self, env):
1245 with open(self.path, 'rb') as f:
1246 with open(self.path, 'rb') as f:
1246 lines = f.readlines()
1247 lines = f.readlines()
1247
1248
1248 # .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
1249 # output updated with the the test input. This avoids some race
1250 # output updated with the the test input. This avoids some race
1250 # conditions where the reference output does not match the actual test.
1251 # conditions where the reference output does not match the actual test.
1251 if self._refout is not None:
1252 if self._refout is not None:
1252 self._refout = lines
1253 self._refout = lines
1253
1254
1254 salt, script, after, expected = self._parsetest(lines)
1255 salt, script, after, expected = self._parsetest(lines)
1255
1256
1256 # Write out the generated script.
1257 # Write out the generated script.
1257 fname = b'%s.sh' % self._testtmp
1258 fname = b'%s.sh' % self._testtmp
1258 with open(fname, 'wb') as f:
1259 with open(fname, 'wb') as f:
1259 for l in script:
1260 for l in script:
1260 f.write(l)
1261 f.write(l)
1261
1262
1262 cmd = b'%s "%s"' % (self._shell, fname)
1263 cmd = b'%s "%s"' % (self._shell, fname)
1263 vlog("# Running", cmd)
1264 vlog("# Running", cmd)
1264
1265
1265 exitcode, output = self._runcommand(cmd, env)
1266 exitcode, output = self._runcommand(cmd, env)
1266
1267
1267 if self._aborted:
1268 if self._aborted:
1268 raise KeyboardInterrupt()
1269 raise KeyboardInterrupt()
1269
1270
1270 # Do not merge output if skipped. Return hghave message instead.
1271 # Do not merge output if skipped. Return hghave message instead.
1271 # Similarly, with --debug, output is None.
1272 # Similarly, with --debug, output is None.
1272 if exitcode == self.SKIPPED_STATUS or output is None:
1273 if exitcode == self.SKIPPED_STATUS or output is None:
1273 return exitcode, output
1274 return exitcode, output
1274
1275
1275 return self._processoutput(exitcode, output, salt, after, expected)
1276 return self._processoutput(exitcode, output, salt, after, expected)
1276
1277
1277 def _hghave(self, reqs):
1278 def _hghave(self, reqs):
1279 allreqs = b' '.join(reqs)
1280 if allreqs in self._have:
1281 return self._have.get(allreqs)
1282
1278 # 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.
1279 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1284 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1280 tdir = runtestdir.replace(b'\\', b'/')
1285 tdir = runtestdir.replace(b'\\', b'/')
1281 proc = Popen4(b'%s -c "%s/hghave %s"' %
1286 proc = Popen4(b'%s -c "%s/hghave %s"' %
1282 (self._shell, tdir, b' '.join(reqs)),
1287 (self._shell, tdir, allreqs),
1283 self._testtmp, 0, self._getenv())
1288 self._testtmp, 0, self._getenv())
1284 stdout, stderr = proc.communicate()
1289 stdout, stderr = proc.communicate()
1285 ret = proc.wait()
1290 ret = proc.wait()
1286 if wifexited(ret):
1291 if wifexited(ret):
1287 ret = os.WEXITSTATUS(ret)
1292 ret = os.WEXITSTATUS(ret)
1288 if ret == 2:
1293 if ret == 2:
1289 print(stdout.decode('utf-8'))
1294 print(stdout.decode('utf-8'))
1290 sys.exit(1)
1295 sys.exit(1)
1291
1296
1292 if ret != 0:
1297 if ret != 0:
1298 self._have[allreqs] = (False, stdout)
1293 return False, stdout
1299 return False, stdout
1294
1300
1295 if b'slow' in reqs:
1301 if b'slow' in reqs:
1296 self._timeout = self._slowtimeout
1302 self._timeout = self._slowtimeout
1303
1304 self._have[allreqs] = (True, None)
1297 return True, None
1305 return True, None
1298
1306
1299 def _iftest(self, args):
1307 def _iftest(self, args):
1300 # implements "#if"
1308 # implements "#if"
1301 reqs = []
1309 reqs = []
1302 for arg in args:
1310 for arg in args:
1303 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1311 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1304 if arg[3:] == self._case:
1312 if arg[3:] == self._case:
1305 return False
1313 return False
1306 elif arg in self._allcases:
1314 elif arg in self._allcases:
1307 if arg != self._case:
1315 if arg != self._case:
1308 return False
1316 return False
1309 else:
1317 else:
1310 reqs.append(arg)
1318 reqs.append(arg)
1311 return self._hghave(reqs)[0]
1319 return self._hghave(reqs)[0]
1312
1320
1313 def _parsetest(self, lines):
1321 def _parsetest(self, lines):
1314 # We generate a shell script which outputs unique markers to line
1322 # We generate a shell script which outputs unique markers to line
1315 # up script results with our source. These markers include input
1323 # up script results with our source. These markers include input
1316 # line number and the last return code.
1324 # line number and the last return code.
1317 salt = b"SALT%d" % time.time()
1325 salt = b"SALT%d" % time.time()
1318 def addsalt(line, inpython):
1326 def addsalt(line, inpython):
1319 if inpython:
1327 if inpython:
1320 script.append(b'%s %d 0\n' % (salt, line))
1328 script.append(b'%s %d 0\n' % (salt, line))
1321 else:
1329 else:
1322 script.append(b'echo %s %d $?\n' % (salt, line))
1330 script.append(b'echo %s %d $?\n' % (salt, line))
1323
1331
1324 script = []
1332 script = []
1325
1333
1326 # 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
1327 # with non-active parts of the source, with synchronization by our
1335 # with non-active parts of the source, with synchronization by our
1328 # SALT line number markers. The after table contains the non-active
1336 # SALT line number markers. The after table contains the non-active
1329 # components, ordered by line number.
1337 # components, ordered by line number.
1330 after = {}
1338 after = {}
1331
1339
1332 # Expected shell script output.
1340 # Expected shell script output.
1333 expected = {}
1341 expected = {}
1334
1342
1335 pos = prepos = -1
1343 pos = prepos = -1
1336
1344
1337 # True or False when in a true or false conditional section
1345 # True or False when in a true or false conditional section
1338 skipping = None
1346 skipping = None
1339
1347
1340 # 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
1341 # can generate the surrounding doctest magic.
1349 # can generate the surrounding doctest magic.
1342 inpython = False
1350 inpython = False
1343
1351
1344 if self._debug:
1352 if self._debug:
1345 script.append(b'set -x\n')
1353 script.append(b'set -x\n')
1346 if self._hgcommand != b'hg':
1354 if self._hgcommand != b'hg':
1347 script.append(b'alias hg="%s"\n' % self._hgcommand)
1355 script.append(b'alias hg="%s"\n' % self._hgcommand)
1348 if os.getenv('MSYSTEM'):
1356 if os.getenv('MSYSTEM'):
1349 script.append(b'alias pwd="pwd -W"\n')
1357 script.append(b'alias pwd="pwd -W"\n')
1350 if self._case:
1358 if self._case:
1351 if isinstance(self._case, str):
1359 if isinstance(self._case, str):
1352 quoted = shellquote(self._case)
1360 quoted = shellquote(self._case)
1353 else:
1361 else:
1354 quoted = shellquote(self._case.decode('utf8')).encode('utf8')
1362 quoted = shellquote(self._case.decode('utf8')).encode('utf8')
1355 script.append(b'TESTCASE=%s\n' % quoted)
1363 script.append(b'TESTCASE=%s\n' % quoted)
1356 script.append(b'export TESTCASE\n')
1364 script.append(b'export TESTCASE\n')
1357
1365
1358 n = 0
1366 n = 0
1359 for n, l in enumerate(lines):
1367 for n, l in enumerate(lines):
1360 if not l.endswith(b'\n'):
1368 if not l.endswith(b'\n'):
1361 l += b'\n'
1369 l += b'\n'
1362 if l.startswith(b'#require'):
1370 if l.startswith(b'#require'):
1363 lsplit = l.split()
1371 lsplit = l.split()
1364 if len(lsplit) < 2 or lsplit[0] != b'#require':
1372 if len(lsplit) < 2 or lsplit[0] != b'#require':
1365 after.setdefault(pos, []).append(' !!! invalid #require\n')
1373 after.setdefault(pos, []).append(' !!! invalid #require\n')
1366 haveresult, message = self._hghave(lsplit[1:])
1374 haveresult, message = self._hghave(lsplit[1:])
1367 if not haveresult:
1375 if not haveresult:
1368 script = [b'echo "%s"\nexit 80\n' % message]
1376 script = [b'echo "%s"\nexit 80\n' % message]
1369 break
1377 break
1370 after.setdefault(pos, []).append(l)
1378 after.setdefault(pos, []).append(l)
1371 elif l.startswith(b'#if'):
1379 elif l.startswith(b'#if'):
1372 lsplit = l.split()
1380 lsplit = l.split()
1373 if len(lsplit) < 2 or lsplit[0] != b'#if':
1381 if len(lsplit) < 2 or lsplit[0] != b'#if':
1374 after.setdefault(pos, []).append(' !!! invalid #if\n')
1382 after.setdefault(pos, []).append(' !!! invalid #if\n')
1375 if skipping is not None:
1383 if skipping is not None:
1376 after.setdefault(pos, []).append(' !!! nested #if\n')
1384 after.setdefault(pos, []).append(' !!! nested #if\n')
1377 skipping = not self._iftest(lsplit[1:])
1385 skipping = not self._iftest(lsplit[1:])
1378 after.setdefault(pos, []).append(l)
1386 after.setdefault(pos, []).append(l)
1379 elif l.startswith(b'#else'):
1387 elif l.startswith(b'#else'):
1380 if skipping is None:
1388 if skipping is None:
1381 after.setdefault(pos, []).append(' !!! missing #if\n')
1389 after.setdefault(pos, []).append(' !!! missing #if\n')
1382 skipping = not skipping
1390 skipping = not skipping
1383 after.setdefault(pos, []).append(l)
1391 after.setdefault(pos, []).append(l)
1384 elif l.startswith(b'#endif'):
1392 elif l.startswith(b'#endif'):
1385 if skipping is None:
1393 if skipping is None:
1386 after.setdefault(pos, []).append(' !!! missing #if\n')
1394 after.setdefault(pos, []).append(' !!! missing #if\n')
1387 skipping = None
1395 skipping = None
1388 after.setdefault(pos, []).append(l)
1396 after.setdefault(pos, []).append(l)
1389 elif skipping:
1397 elif skipping:
1390 after.setdefault(pos, []).append(l)
1398 after.setdefault(pos, []).append(l)
1391 elif l.startswith(b' >>> '): # python inlines
1399 elif l.startswith(b' >>> '): # python inlines
1392 after.setdefault(pos, []).append(l)
1400 after.setdefault(pos, []).append(l)
1393 prepos = pos
1401 prepos = pos
1394 pos = n
1402 pos = n
1395 if not inpython:
1403 if not inpython:
1396 # We've just entered a Python block. Add the header.
1404 # We've just entered a Python block. Add the header.
1397 inpython = True
1405 inpython = True
1398 addsalt(prepos, False) # Make sure we report the exit code.
1406 addsalt(prepos, False) # Make sure we report the exit code.
1399 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1407 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1400 addsalt(n, True)
1408 addsalt(n, True)
1401 script.append(l[2:])
1409 script.append(l[2:])
1402 elif l.startswith(b' ... '): # python inlines
1410 elif l.startswith(b' ... '): # python inlines
1403 after.setdefault(prepos, []).append(l)
1411 after.setdefault(prepos, []).append(l)
1404 script.append(l[2:])
1412 script.append(l[2:])
1405 elif l.startswith(b' $ '): # commands
1413 elif l.startswith(b' $ '): # commands
1406 if inpython:
1414 if inpython:
1407 script.append(b'EOF\n')
1415 script.append(b'EOF\n')
1408 inpython = False
1416 inpython = False
1409 after.setdefault(pos, []).append(l)
1417 after.setdefault(pos, []).append(l)
1410 prepos = pos
1418 prepos = pos
1411 pos = n
1419 pos = n
1412 addsalt(n, False)
1420 addsalt(n, False)
1413 cmd = l[4:].split()
1421 cmd = l[4:].split()
1414 if len(cmd) == 2 and cmd[0] == b'cd':
1422 if len(cmd) == 2 and cmd[0] == b'cd':
1415 l = b' $ cd %s || exit 1\n' % cmd[1]
1423 l = b' $ cd %s || exit 1\n' % cmd[1]
1416 script.append(l[4:])
1424 script.append(l[4:])
1417 elif l.startswith(b' > '): # continuations
1425 elif l.startswith(b' > '): # continuations
1418 after.setdefault(prepos, []).append(l)
1426 after.setdefault(prepos, []).append(l)
1419 script.append(l[4:])
1427 script.append(l[4:])
1420 elif l.startswith(b' '): # results
1428 elif l.startswith(b' '): # results
1421 # Queue up a list of expected results.
1429 # Queue up a list of expected results.
1422 expected.setdefault(pos, []).append(l[2:])
1430 expected.setdefault(pos, []).append(l[2:])
1423 else:
1431 else:
1424 if inpython:
1432 if inpython:
1425 script.append(b'EOF\n')
1433 script.append(b'EOF\n')
1426 inpython = False
1434 inpython = False
1427 # Non-command/result. Queue up for merged output.
1435 # Non-command/result. Queue up for merged output.
1428 after.setdefault(pos, []).append(l)
1436 after.setdefault(pos, []).append(l)
1429
1437
1430 if inpython:
1438 if inpython:
1431 script.append(b'EOF\n')
1439 script.append(b'EOF\n')
1432 if skipping is not None:
1440 if skipping is not None:
1433 after.setdefault(pos, []).append(' !!! missing #endif\n')
1441 after.setdefault(pos, []).append(' !!! missing #endif\n')
1434 addsalt(n + 1, False)
1442 addsalt(n + 1, False)
1435
1443
1436 return salt, script, after, expected
1444 return salt, script, after, expected
1437
1445
1438 def _processoutput(self, exitcode, output, salt, after, expected):
1446 def _processoutput(self, exitcode, output, salt, after, expected):
1439 # Merge the script output back into a unified test.
1447 # Merge the script output back into a unified test.
1440 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1448 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1441 if exitcode != 0:
1449 if exitcode != 0:
1442 warnonly = 3
1450 warnonly = 3
1443
1451
1444 pos = -1
1452 pos = -1
1445 postout = []
1453 postout = []
1446 for l in output:
1454 for l in output:
1447 lout, lcmd = l, None
1455 lout, lcmd = l, None
1448 if salt in l:
1456 if salt in l:
1449 lout, lcmd = l.split(salt, 1)
1457 lout, lcmd = l.split(salt, 1)
1450
1458
1451 while lout:
1459 while lout:
1452 if not lout.endswith(b'\n'):
1460 if not lout.endswith(b'\n'):
1453 lout += b' (no-eol)\n'
1461 lout += b' (no-eol)\n'
1454
1462
1455 # Find the expected output at the current position.
1463 # Find the expected output at the current position.
1456 els = [None]
1464 els = [None]
1457 if expected.get(pos, None):
1465 if expected.get(pos, None):
1458 els = expected[pos]
1466 els = expected[pos]
1459
1467
1460 i = 0
1468 i = 0
1461 optional = []
1469 optional = []
1462 while i < len(els):
1470 while i < len(els):
1463 el = els[i]
1471 el = els[i]
1464
1472
1465 r = self.linematch(el, lout)
1473 r = self.linematch(el, lout)
1466 if isinstance(r, str):
1474 if isinstance(r, str):
1467 if r == '-glob':
1475 if r == '-glob':
1468 lout = ''.join(el.rsplit(' (glob)', 1))
1476 lout = ''.join(el.rsplit(' (glob)', 1))
1469 r = '' # Warn only this line.
1477 r = '' # Warn only this line.
1470 elif r == "retry":
1478 elif r == "retry":
1471 postout.append(b' ' + el)
1479 postout.append(b' ' + el)
1472 els.pop(i)
1480 els.pop(i)
1473 break
1481 break
1474 else:
1482 else:
1475 log('\ninfo, unknown linematch result: %r\n' % r)
1483 log('\ninfo, unknown linematch result: %r\n' % r)
1476 r = False
1484 r = False
1477 if r:
1485 if r:
1478 els.pop(i)
1486 els.pop(i)
1479 break
1487 break
1480 if el:
1488 if el:
1481 if el.endswith(b" (?)\n"):
1489 if el.endswith(b" (?)\n"):
1482 optional.append(i)
1490 optional.append(i)
1483 else:
1491 else:
1484 m = optline.match(el)
1492 m = optline.match(el)
1485 if m:
1493 if m:
1486 conditions = [
1494 conditions = [
1487 c for c in m.group(2).split(b' ')]
1495 c for c in m.group(2).split(b' ')]
1488
1496
1489 if not self._iftest(conditions):
1497 if not self._iftest(conditions):
1490 optional.append(i)
1498 optional.append(i)
1491
1499
1492 i += 1
1500 i += 1
1493
1501
1494 if r:
1502 if r:
1495 if r == "retry":
1503 if r == "retry":
1496 continue
1504 continue
1497 # clean up any optional leftovers
1505 # clean up any optional leftovers
1498 for i in optional:
1506 for i in optional:
1499 postout.append(b' ' + els[i])
1507 postout.append(b' ' + els[i])
1500 for i in reversed(optional):
1508 for i in reversed(optional):
1501 del els[i]
1509 del els[i]
1502 postout.append(b' ' + el)
1510 postout.append(b' ' + el)
1503 else:
1511 else:
1504 if self.NEEDESCAPE(lout):
1512 if self.NEEDESCAPE(lout):
1505 lout = TTest._stringescape(b'%s (esc)\n' %
1513 lout = TTest._stringescape(b'%s (esc)\n' %
1506 lout.rstrip(b'\n'))
1514 lout.rstrip(b'\n'))
1507 postout.append(b' ' + lout) # Let diff deal with it.
1515 postout.append(b' ' + lout) # Let diff deal with it.
1508 if r != '': # If line failed.
1516 if r != '': # If line failed.
1509 warnonly = 3 # for sure not
1517 warnonly = 3 # for sure not
1510 elif warnonly == 1: # Is "not yet" and line is warn only.
1518 elif warnonly == 1: # Is "not yet" and line is warn only.
1511 warnonly = 2 # Yes do warn.
1519 warnonly = 2 # Yes do warn.
1512 break
1520 break
1513 else:
1521 else:
1514 # clean up any optional leftovers
1522 # clean up any optional leftovers
1515 while expected.get(pos, None):
1523 while expected.get(pos, None):
1516 el = expected[pos].pop(0)
1524 el = expected[pos].pop(0)
1517 if el:
1525 if el:
1518 if not el.endswith(b" (?)\n"):
1526 if not el.endswith(b" (?)\n"):
1519 m = optline.match(el)
1527 m = optline.match(el)
1520 if m:
1528 if m:
1521 conditions = [c for c in m.group(2).split(b' ')]
1529 conditions = [c for c in m.group(2).split(b' ')]
1522
1530
1523 if self._iftest(conditions):
1531 if self._iftest(conditions):
1524 # Don't append as optional line
1532 # Don't append as optional line
1525 continue
1533 continue
1526 else:
1534 else:
1527 continue
1535 continue
1528 postout.append(b' ' + el)
1536 postout.append(b' ' + el)
1529
1537
1530 if lcmd:
1538 if lcmd:
1531 # Add on last return code.
1539 # Add on last return code.
1532 ret = int(lcmd.split()[1])
1540 ret = int(lcmd.split()[1])
1533 if ret != 0:
1541 if ret != 0:
1534 postout.append(b' [%d]\n' % ret)
1542 postout.append(b' [%d]\n' % ret)
1535 if pos in after:
1543 if pos in after:
1536 # Merge in non-active test bits.
1544 # Merge in non-active test bits.
1537 postout += after.pop(pos)
1545 postout += after.pop(pos)
1538 pos = int(lcmd.split()[0])
1546 pos = int(lcmd.split()[0])
1539
1547
1540 if pos in after:
1548 if pos in after:
1541 postout += after.pop(pos)
1549 postout += after.pop(pos)
1542
1550
1543 if warnonly == 2:
1551 if warnonly == 2:
1544 exitcode = False # Set exitcode to warned.
1552 exitcode = False # Set exitcode to warned.
1545
1553
1546 return exitcode, postout
1554 return exitcode, postout
1547
1555
1548 @staticmethod
1556 @staticmethod
1549 def rematch(el, l):
1557 def rematch(el, l):
1550 try:
1558 try:
1551 el = b'(?:' + el + b')'
1559 el = b'(?:' + el + b')'
1552 # 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
1553 if os.name == 'nt':
1561 if os.name == 'nt':
1554 return re.match(el + br'\r?\n\Z', l)
1562 return re.match(el + br'\r?\n\Z', l)
1555 return re.match(el + br'\n\Z', l)
1563 return re.match(el + br'\n\Z', l)
1556 except re.error:
1564 except re.error:
1557 # el is an invalid regex
1565 # el is an invalid regex
1558 return False
1566 return False
1559
1567
1560 @staticmethod
1568 @staticmethod
1561 def globmatch(el, l):
1569 def globmatch(el, l):
1562 # The only supported special characters are * and ? plus / which also
1570 # The only supported special characters are * and ? plus / which also
1563 # matches \ on windows. Escaping of these characters is supported.
1571 # matches \ on windows. Escaping of these characters is supported.
1564 if el + b'\n' == l:
1572 if el + b'\n' == l:
1565 if os.altsep:
1573 if os.altsep:
1566 # matching on "/" is not needed for this line
1574 # matching on "/" is not needed for this line
1567 for pat in checkcodeglobpats:
1575 for pat in checkcodeglobpats:
1568 if pat.match(el):
1576 if pat.match(el):
1569 return True
1577 return True
1570 return b'-glob'
1578 return b'-glob'
1571 return True
1579 return True
1572 el = el.replace(b'$LOCALIP', b'*')
1580 el = el.replace(b'$LOCALIP', b'*')
1573 i, n = 0, len(el)
1581 i, n = 0, len(el)
1574 res = b''
1582 res = b''
1575 while i < n:
1583 while i < n:
1576 c = el[i:i + 1]
1584 c = el[i:i + 1]
1577 i += 1
1585 i += 1
1578 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'*?\\/':
1579 res += el[i - 1:i + 1]
1587 res += el[i - 1:i + 1]
1580 i += 1
1588 i += 1
1581 elif c == b'*':
1589 elif c == b'*':
1582 res += b'.*'
1590 res += b'.*'
1583 elif c == b'?':
1591 elif c == b'?':
1584 res += b'.'
1592 res += b'.'
1585 elif c == b'/' and os.altsep:
1593 elif c == b'/' and os.altsep:
1586 res += b'[/\\\\]'
1594 res += b'[/\\\\]'
1587 else:
1595 else:
1588 res += re.escape(c)
1596 res += re.escape(c)
1589 return TTest.rematch(res, l)
1597 return TTest.rematch(res, l)
1590
1598
1591 def linematch(self, el, l):
1599 def linematch(self, el, l):
1592 retry = False
1600 retry = False
1593 if el == l: # perfect match (fast)
1601 if el == l: # perfect match (fast)
1594 return True
1602 return True
1595 if el:
1603 if el:
1596 if el.endswith(b" (?)\n"):
1604 if el.endswith(b" (?)\n"):
1597 retry = "retry"
1605 retry = "retry"
1598 el = el[:-5] + b"\n"
1606 el = el[:-5] + b"\n"
1599 else:
1607 else:
1600 m = optline.match(el)
1608 m = optline.match(el)
1601 if m:
1609 if m:
1602 conditions = [c for c in m.group(2).split(b' ')]
1610 conditions = [c for c in m.group(2).split(b' ')]
1603
1611
1604 el = m.group(1) + b"\n"
1612 el = m.group(1) + b"\n"
1605 if not self._iftest(conditions):
1613 if not self._iftest(conditions):
1606 retry = "retry" # Not required by listed features
1614 retry = "retry" # Not required by listed features
1607
1615
1608 if el.endswith(b" (esc)\n"):
1616 if el.endswith(b" (esc)\n"):
1609 if PYTHON3:
1617 if PYTHON3:
1610 el = el[:-7].decode('unicode_escape') + '\n'
1618 el = el[:-7].decode('unicode_escape') + '\n'
1611 el = el.encode('utf-8')
1619 el = el.encode('utf-8')
1612 else:
1620 else:
1613 el = el[:-7].decode('string-escape') + '\n'
1621 el = el[:-7].decode('string-escape') + '\n'
1614 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:
1615 return True
1623 return True
1616 if el.endswith(b" (re)\n"):
1624 if el.endswith(b" (re)\n"):
1617 return TTest.rematch(el[:-6], l) or retry
1625 return TTest.rematch(el[:-6], l) or retry
1618 if el.endswith(b" (glob)\n"):
1626 if el.endswith(b" (glob)\n"):
1619 # ignore '(glob)' added to l by 'replacements'
1627 # ignore '(glob)' added to l by 'replacements'
1620 if l.endswith(b" (glob)\n"):
1628 if l.endswith(b" (glob)\n"):
1621 l = l[:-8] + b"\n"
1629 l = l[:-8] + b"\n"
1622 return TTest.globmatch(el[:-8], l) or retry
1630 return TTest.globmatch(el[:-8], l) or retry
1623 if os.altsep:
1631 if os.altsep:
1624 _l = l.replace(b'\\', b'/')
1632 _l = l.replace(b'\\', b'/')
1625 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:
1626 return True
1634 return True
1627 return retry
1635 return retry
1628
1636
1629 @staticmethod
1637 @staticmethod
1630 def parsehghaveoutput(lines):
1638 def parsehghaveoutput(lines):
1631 '''Parse hghave log lines.
1639 '''Parse hghave log lines.
1632
1640
1633 Return tuple of lists (missing, failed):
1641 Return tuple of lists (missing, failed):
1634 * the missing/unknown features
1642 * the missing/unknown features
1635 * the features for which existence check failed'''
1643 * the features for which existence check failed'''
1636 missing = []
1644 missing = []
1637 failed = []
1645 failed = []
1638 for line in lines:
1646 for line in lines:
1639 if line.startswith(TTest.SKIPPED_PREFIX):
1647 if line.startswith(TTest.SKIPPED_PREFIX):
1640 line = line.splitlines()[0]
1648 line = line.splitlines()[0]
1641 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1649 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1642 elif line.startswith(TTest.FAILED_PREFIX):
1650 elif line.startswith(TTest.FAILED_PREFIX):
1643 line = line.splitlines()[0]
1651 line = line.splitlines()[0]
1644 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1652 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1645
1653
1646 return missing, failed
1654 return missing, failed
1647
1655
1648 @staticmethod
1656 @staticmethod
1649 def _escapef(m):
1657 def _escapef(m):
1650 return TTest.ESCAPEMAP[m.group(0)]
1658 return TTest.ESCAPEMAP[m.group(0)]
1651
1659
1652 @staticmethod
1660 @staticmethod
1653 def _stringescape(s):
1661 def _stringescape(s):
1654 return TTest.ESCAPESUB(TTest._escapef, s)
1662 return TTest.ESCAPESUB(TTest._escapef, s)
1655
1663
1656 iolock = threading.RLock()
1664 iolock = threading.RLock()
1657 firstlock = threading.RLock()
1665 firstlock = threading.RLock()
1658 firsterror = False
1666 firsterror = False
1659
1667
1660 class TestResult(unittest._TextTestResult):
1668 class TestResult(unittest._TextTestResult):
1661 """Holds results when executing via unittest."""
1669 """Holds results when executing via unittest."""
1662 # Don't worry too much about accessing the non-public _TextTestResult.
1670 # Don't worry too much about accessing the non-public _TextTestResult.
1663 # It is relatively common in Python testing tools.
1671 # It is relatively common in Python testing tools.
1664 def __init__(self, options, *args, **kwargs):
1672 def __init__(self, options, *args, **kwargs):
1665 super(TestResult, self).__init__(*args, **kwargs)
1673 super(TestResult, self).__init__(*args, **kwargs)
1666
1674
1667 self._options = options
1675 self._options = options
1668
1676
1669 # 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
1670 # polyfill it.
1678 # polyfill it.
1671 self.skipped = []
1679 self.skipped = []
1672
1680
1673 # 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
1674 # unittest implementation. It is very similar to skipped. It may make
1682 # unittest implementation. It is very similar to skipped. It may make
1675 # sense to map it into skip some day.
1683 # sense to map it into skip some day.
1676 self.ignored = []
1684 self.ignored = []
1677
1685
1678 self.times = []
1686 self.times = []
1679 self._firststarttime = None
1687 self._firststarttime = None
1680 # Data stored for the benefit of generating xunit reports.
1688 # Data stored for the benefit of generating xunit reports.
1681 self.successes = []
1689 self.successes = []
1682 self.faildata = {}
1690 self.faildata = {}
1683
1691
1684 if options.color == 'auto':
1692 if options.color == 'auto':
1685 self.color = pygmentspresent and self.stream.isatty()
1693 self.color = pygmentspresent and self.stream.isatty()
1686 elif options.color == 'never':
1694 elif options.color == 'never':
1687 self.color = False
1695 self.color = False
1688 else: # 'always', for testing purposes
1696 else: # 'always', for testing purposes
1689 self.color = pygmentspresent
1697 self.color = pygmentspresent
1690
1698
1691 def addFailure(self, test, reason):
1699 def addFailure(self, test, reason):
1692 self.failures.append((test, reason))
1700 self.failures.append((test, reason))
1693
1701
1694 if self._options.first:
1702 if self._options.first:
1695 self.stop()
1703 self.stop()
1696 else:
1704 else:
1697 with iolock:
1705 with iolock:
1698 if reason == "timed out":
1706 if reason == "timed out":
1699 self.stream.write('t')
1707 self.stream.write('t')
1700 else:
1708 else:
1701 if not self._options.nodiff:
1709 if not self._options.nodiff:
1702 self.stream.write('\n')
1710 self.stream.write('\n')
1703 # Exclude the '\n' from highlighting to lex correctly
1711 # Exclude the '\n' from highlighting to lex correctly
1704 formatted = 'ERROR: %s output changed\n' % test
1712 formatted = 'ERROR: %s output changed\n' % test
1705 self.stream.write(highlightmsg(formatted, self.color))
1713 self.stream.write(highlightmsg(formatted, self.color))
1706 self.stream.write('!')
1714 self.stream.write('!')
1707
1715
1708 self.stream.flush()
1716 self.stream.flush()
1709
1717
1710 def addSuccess(self, test):
1718 def addSuccess(self, test):
1711 with iolock:
1719 with iolock:
1712 super(TestResult, self).addSuccess(test)
1720 super(TestResult, self).addSuccess(test)
1713 self.successes.append(test)
1721 self.successes.append(test)
1714
1722
1715 def addError(self, test, err):
1723 def addError(self, test, err):
1716 super(TestResult, self).addError(test, err)
1724 super(TestResult, self).addError(test, err)
1717 if self._options.first:
1725 if self._options.first:
1718 self.stop()
1726 self.stop()
1719
1727
1720 # Polyfill.
1728 # Polyfill.
1721 def addSkip(self, test, reason):
1729 def addSkip(self, test, reason):
1722 self.skipped.append((test, reason))
1730 self.skipped.append((test, reason))
1723 with iolock:
1731 with iolock:
1724 if self.showAll:
1732 if self.showAll:
1725 self.stream.writeln('skipped %s' % reason)
1733 self.stream.writeln('skipped %s' % reason)
1726 else:
1734 else:
1727 self.stream.write('s')
1735 self.stream.write('s')
1728 self.stream.flush()
1736 self.stream.flush()
1729
1737
1730 def addIgnore(self, test, reason):
1738 def addIgnore(self, test, reason):
1731 self.ignored.append((test, reason))
1739 self.ignored.append((test, reason))
1732 with iolock:
1740 with iolock:
1733 if self.showAll:
1741 if self.showAll:
1734 self.stream.writeln('ignored %s' % reason)
1742 self.stream.writeln('ignored %s' % reason)
1735 else:
1743 else:
1736 if reason not in ('not retesting', "doesn't match keyword"):
1744 if reason not in ('not retesting', "doesn't match keyword"):
1737 self.stream.write('i')
1745 self.stream.write('i')
1738 else:
1746 else:
1739 self.testsRun += 1
1747 self.testsRun += 1
1740 self.stream.flush()
1748 self.stream.flush()
1741
1749
1742 def addOutputMismatch(self, test, ret, got, expected):
1750 def addOutputMismatch(self, test, ret, got, expected):
1743 """Record a mismatch in test output for a particular test."""
1751 """Record a mismatch in test output for a particular test."""
1744 if self.shouldStop or firsterror:
1752 if self.shouldStop or firsterror:
1745 # don't print, some other test case already failed and
1753 # don't print, some other test case already failed and
1746 # printed, we're just stale and probably failed due to our
1754 # printed, we're just stale and probably failed due to our
1747 # temp dir getting cleaned up.
1755 # temp dir getting cleaned up.
1748 return
1756 return
1749
1757
1750 accepted = False
1758 accepted = False
1751 lines = []
1759 lines = []
1752
1760
1753 with iolock:
1761 with iolock:
1754 if self._options.nodiff:
1762 if self._options.nodiff:
1755 pass
1763 pass
1756 elif self._options.view:
1764 elif self._options.view:
1757 v = self._options.view
1765 v = self._options.view
1758 if PYTHON3:
1766 if PYTHON3:
1759 v = _bytespath(v)
1767 v = _bytespath(v)
1760 os.system(b"%s %s %s" %
1768 os.system(b"%s %s %s" %
1761 (v, test.refpath, test.errpath))
1769 (v, test.refpath, test.errpath))
1762 else:
1770 else:
1763 servefail, lines = getdiff(expected, got,
1771 servefail, lines = getdiff(expected, got,
1764 test.refpath, test.errpath)
1772 test.refpath, test.errpath)
1765 self.stream.write('\n')
1773 self.stream.write('\n')
1766 for line in lines:
1774 for line in lines:
1767 line = highlightdiff(line, self.color)
1775 line = highlightdiff(line, self.color)
1768 if PYTHON3:
1776 if PYTHON3:
1769 self.stream.flush()
1777 self.stream.flush()
1770 self.stream.buffer.write(line)
1778 self.stream.buffer.write(line)
1771 self.stream.buffer.flush()
1779 self.stream.buffer.flush()
1772 else:
1780 else:
1773 self.stream.write(line)
1781 self.stream.write(line)
1774 self.stream.flush()
1782 self.stream.flush()
1775
1783
1776 if servefail:
1784 if servefail:
1777 raise test.failureException(
1785 raise test.failureException(
1778 'server failed to start (HGPORT=%s)' % test._startport)
1786 'server failed to start (HGPORT=%s)' % test._startport)
1779
1787
1780 # handle interactive prompt without releasing iolock
1788 # handle interactive prompt without releasing iolock
1781 if self._options.interactive:
1789 if self._options.interactive:
1782 if test.readrefout() != expected:
1790 if test.readrefout() != expected:
1783 self.stream.write(
1791 self.stream.write(
1784 'Reference output has changed (run again to prompt '
1792 'Reference output has changed (run again to prompt '
1785 'changes)')
1793 'changes)')
1786 else:
1794 else:
1787 self.stream.write('Accept this change? [n] ')
1795 self.stream.write('Accept this change? [n] ')
1788 answer = sys.stdin.readline().strip()
1796 answer = sys.stdin.readline().strip()
1789 if answer.lower() in ('y', 'yes'):
1797 if answer.lower() in ('y', 'yes'):
1790 if test.path.endswith(b'.t'):
1798 if test.path.endswith(b'.t'):
1791 rename(test.errpath, test.path)
1799 rename(test.errpath, test.path)
1792 else:
1800 else:
1793 rename(test.errpath, '%s.out' % test.path)
1801 rename(test.errpath, '%s.out' % test.path)
1794 accepted = True
1802 accepted = True
1795 if not accepted:
1803 if not accepted:
1796 self.faildata[test.name] = b''.join(lines)
1804 self.faildata[test.name] = b''.join(lines)
1797
1805
1798 return accepted
1806 return accepted
1799
1807
1800 def startTest(self, test):
1808 def startTest(self, test):
1801 super(TestResult, self).startTest(test)
1809 super(TestResult, self).startTest(test)
1802
1810
1803 # 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
1804 # 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.
1805 # 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
1806 # and not for Windows.
1814 # and not for Windows.
1807 test.started = os.times()
1815 test.started = os.times()
1808 if self._firststarttime is None: # thread racy but irrelevant
1816 if self._firststarttime is None: # thread racy but irrelevant
1809 self._firststarttime = test.started[4]
1817 self._firststarttime = test.started[4]
1810
1818
1811 def stopTest(self, test, interrupted=False):
1819 def stopTest(self, test, interrupted=False):
1812 super(TestResult, self).stopTest(test)
1820 super(TestResult, self).stopTest(test)
1813
1821
1814 test.stopped = os.times()
1822 test.stopped = os.times()
1815
1823
1816 starttime = test.started
1824 starttime = test.started
1817 endtime = test.stopped
1825 endtime = test.stopped
1818 origin = self._firststarttime
1826 origin = self._firststarttime
1819 self.times.append((test.name,
1827 self.times.append((test.name,
1820 endtime[2] - starttime[2], # user space CPU time
1828 endtime[2] - starttime[2], # user space CPU time
1821 endtime[3] - starttime[3], # sys space CPU time
1829 endtime[3] - starttime[3], # sys space CPU time
1822 endtime[4] - starttime[4], # real time
1830 endtime[4] - starttime[4], # real time
1823 starttime[4] - origin, # start date in run context
1831 starttime[4] - origin, # start date in run context
1824 endtime[4] - origin, # end date in run context
1832 endtime[4] - origin, # end date in run context
1825 ))
1833 ))
1826
1834
1827 if interrupted:
1835 if interrupted:
1828 with iolock:
1836 with iolock:
1829 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1837 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1830 test.name, self.times[-1][3]))
1838 test.name, self.times[-1][3]))
1831
1839
1832 class TestSuite(unittest.TestSuite):
1840 class TestSuite(unittest.TestSuite):
1833 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1841 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1834
1842
1835 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1843 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1836 retest=False, keywords=None, loop=False, runs_per_test=1,
1844 retest=False, keywords=None, loop=False, runs_per_test=1,
1837 loadtest=None, showchannels=False,
1845 loadtest=None, showchannels=False,
1838 *args, **kwargs):
1846 *args, **kwargs):
1839 """Create a new instance that can run tests with a configuration.
1847 """Create a new instance that can run tests with a configuration.
1840
1848
1841 testdir specifies the directory where tests are executed from. This
1849 testdir specifies the directory where tests are executed from. This
1842 is typically the ``tests`` directory from Mercurial's source
1850 is typically the ``tests`` directory from Mercurial's source
1843 repository.
1851 repository.
1844
1852
1845 jobs specifies the number of jobs to run concurrently. Each test
1853 jobs specifies the number of jobs to run concurrently. Each test
1846 executes on its own thread. Tests actually spawn new processes, so
1854 executes on its own thread. Tests actually spawn new processes, so
1847 state mutation should not be an issue.
1855 state mutation should not be an issue.
1848
1856
1849 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.
1850
1858
1851 whitelist and blacklist denote tests that have been whitelisted and
1859 whitelist and blacklist denote tests that have been whitelisted and
1852 blacklisted, respectively. These arguments don't belong in TestSuite.
1860 blacklisted, respectively. These arguments don't belong in TestSuite.
1853 Instead, whitelist and blacklist should be handled by the thing that
1861 Instead, whitelist and blacklist should be handled by the thing that
1854 populates the TestSuite with tests. They are present to preserve
1862 populates the TestSuite with tests. They are present to preserve
1855 backwards compatible behavior which reports skipped tests as part
1863 backwards compatible behavior which reports skipped tests as part
1856 of the results.
1864 of the results.
1857
1865
1858 retest denotes whether to retest failed tests. This arguably belongs
1866 retest denotes whether to retest failed tests. This arguably belongs
1859 outside of TestSuite.
1867 outside of TestSuite.
1860
1868
1861 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
1862 to execute. This arguably belongs outside of TestSuite.
1870 to execute. This arguably belongs outside of TestSuite.
1863
1871
1864 loop denotes whether to loop over tests forever.
1872 loop denotes whether to loop over tests forever.
1865 """
1873 """
1866 super(TestSuite, self).__init__(*args, **kwargs)
1874 super(TestSuite, self).__init__(*args, **kwargs)
1867
1875
1868 self._jobs = jobs
1876 self._jobs = jobs
1869 self._whitelist = whitelist
1877 self._whitelist = whitelist
1870 self._blacklist = blacklist
1878 self._blacklist = blacklist
1871 self._retest = retest
1879 self._retest = retest
1872 self._keywords = keywords
1880 self._keywords = keywords
1873 self._loop = loop
1881 self._loop = loop
1874 self._runs_per_test = runs_per_test
1882 self._runs_per_test = runs_per_test
1875 self._loadtest = loadtest
1883 self._loadtest = loadtest
1876 self._showchannels = showchannels
1884 self._showchannels = showchannels
1877
1885
1878 def run(self, result):
1886 def run(self, result):
1879 # 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
1880 # 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
1881 # Test simpler.
1889 # Test simpler.
1882 tests = []
1890 tests = []
1883 num_tests = [0]
1891 num_tests = [0]
1884 for test in self._tests:
1892 for test in self._tests:
1885 def get():
1893 def get():
1886 num_tests[0] += 1
1894 num_tests[0] += 1
1887 if getattr(test, 'should_reload', False):
1895 if getattr(test, 'should_reload', False):
1888 return self._loadtest(test, num_tests[0])
1896 return self._loadtest(test, num_tests[0])
1889 return test
1897 return test
1890 if not os.path.exists(test.path):
1898 if not os.path.exists(test.path):
1891 result.addSkip(test, "Doesn't exist")
1899 result.addSkip(test, "Doesn't exist")
1892 continue
1900 continue
1893
1901
1894 if not (self._whitelist and test.bname in self._whitelist):
1902 if not (self._whitelist and test.bname in self._whitelist):
1895 if self._blacklist and test.bname in self._blacklist:
1903 if self._blacklist and test.bname in self._blacklist:
1896 result.addSkip(test, 'blacklisted')
1904 result.addSkip(test, 'blacklisted')
1897 continue
1905 continue
1898
1906
1899 if self._retest and not os.path.exists(test.errpath):
1907 if self._retest and not os.path.exists(test.errpath):
1900 result.addIgnore(test, 'not retesting')
1908 result.addIgnore(test, 'not retesting')
1901 continue
1909 continue
1902
1910
1903 if self._keywords:
1911 if self._keywords:
1904 with open(test.path, 'rb') as f:
1912 with open(test.path, 'rb') as f:
1905 t = f.read().lower() + test.bname.lower()
1913 t = f.read().lower() + test.bname.lower()
1906 ignored = False
1914 ignored = False
1907 for k in self._keywords.lower().split():
1915 for k in self._keywords.lower().split():
1908 if k not in t:
1916 if k not in t:
1909 result.addIgnore(test, "doesn't match keyword")
1917 result.addIgnore(test, "doesn't match keyword")
1910 ignored = True
1918 ignored = True
1911 break
1919 break
1912
1920
1913 if ignored:
1921 if ignored:
1914 continue
1922 continue
1915 for _ in xrange(self._runs_per_test):
1923 for _ in xrange(self._runs_per_test):
1916 tests.append(get())
1924 tests.append(get())
1917
1925
1918 runtests = list(tests)
1926 runtests = list(tests)
1919 done = queue.Queue()
1927 done = queue.Queue()
1920 running = 0
1928 running = 0
1921
1929
1922 channels = [""] * self._jobs
1930 channels = [""] * self._jobs
1923
1931
1924 def job(test, result):
1932 def job(test, result):
1925 for n, v in enumerate(channels):
1933 for n, v in enumerate(channels):
1926 if not v:
1934 if not v:
1927 channel = n
1935 channel = n
1928 break
1936 break
1929 else:
1937 else:
1930 raise ValueError('Could not find output channel')
1938 raise ValueError('Could not find output channel')
1931 channels[channel] = "=" + test.name[5:].split(".")[0]
1939 channels[channel] = "=" + test.name[5:].split(".")[0]
1932 try:
1940 try:
1933 test(result)
1941 test(result)
1934 done.put(None)
1942 done.put(None)
1935 except KeyboardInterrupt:
1943 except KeyboardInterrupt:
1936 pass
1944 pass
1937 except: # re-raises
1945 except: # re-raises
1938 done.put(('!', test, 'run-test raised an error, see traceback'))
1946 done.put(('!', test, 'run-test raised an error, see traceback'))
1939 raise
1947 raise
1940 finally:
1948 finally:
1941 try:
1949 try:
1942 channels[channel] = ''
1950 channels[channel] = ''
1943 except IndexError:
1951 except IndexError:
1944 pass
1952 pass
1945
1953
1946 def stat():
1954 def stat():
1947 count = 0
1955 count = 0
1948 while channels:
1956 while channels:
1949 d = '\n%03s ' % count
1957 d = '\n%03s ' % count
1950 for n, v in enumerate(channels):
1958 for n, v in enumerate(channels):
1951 if v:
1959 if v:
1952 d += v[0]
1960 d += v[0]
1953 channels[n] = v[1:] or '.'
1961 channels[n] = v[1:] or '.'
1954 else:
1962 else:
1955 d += ' '
1963 d += ' '
1956 d += ' '
1964 d += ' '
1957 with iolock:
1965 with iolock:
1958 sys.stdout.write(d + ' ')
1966 sys.stdout.write(d + ' ')
1959 sys.stdout.flush()
1967 sys.stdout.flush()
1960 for x in xrange(10):
1968 for x in xrange(10):
1961 if channels:
1969 if channels:
1962 time.sleep(.1)
1970 time.sleep(.1)
1963 count += 1
1971 count += 1
1964
1972
1965 stoppedearly = False
1973 stoppedearly = False
1966
1974
1967 if self._showchannels:
1975 if self._showchannels:
1968 statthread = threading.Thread(target=stat, name="stat")
1976 statthread = threading.Thread(target=stat, name="stat")
1969 statthread.start()
1977 statthread.start()
1970
1978
1971 try:
1979 try:
1972 while tests or running:
1980 while tests or running:
1973 if not done.empty() or running == self._jobs or not tests:
1981 if not done.empty() or running == self._jobs or not tests:
1974 try:
1982 try:
1975 done.get(True, 1)
1983 done.get(True, 1)
1976 running -= 1
1984 running -= 1
1977 if result and result.shouldStop:
1985 if result and result.shouldStop:
1978 stoppedearly = True
1986 stoppedearly = True
1979 break
1987 break
1980 except queue.Empty:
1988 except queue.Empty:
1981 continue
1989 continue
1982 if tests and not running == self._jobs:
1990 if tests and not running == self._jobs:
1983 test = tests.pop(0)
1991 test = tests.pop(0)
1984 if self._loop:
1992 if self._loop:
1985 if getattr(test, 'should_reload', False):
1993 if getattr(test, 'should_reload', False):
1986 num_tests[0] += 1
1994 num_tests[0] += 1
1987 tests.append(
1995 tests.append(
1988 self._loadtest(test, num_tests[0]))
1996 self._loadtest(test, num_tests[0]))
1989 else:
1997 else:
1990 tests.append(test)
1998 tests.append(test)
1991 if self._jobs == 1:
1999 if self._jobs == 1:
1992 job(test, result)
2000 job(test, result)
1993 else:
2001 else:
1994 t = threading.Thread(target=job, name=test.name,
2002 t = threading.Thread(target=job, name=test.name,
1995 args=(test, result))
2003 args=(test, result))
1996 t.start()
2004 t.start()
1997 running += 1
2005 running += 1
1998
2006
1999 # 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
2000 # finish. Otherwise, there is a race between the test completing
2008 # finish. Otherwise, there is a race between the test completing
2001 # 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
2002 # test reporting incorrect.
2010 # test reporting incorrect.
2003 if stoppedearly:
2011 if stoppedearly:
2004 while running:
2012 while running:
2005 try:
2013 try:
2006 done.get(True, 1)
2014 done.get(True, 1)
2007 running -= 1
2015 running -= 1
2008 except queue.Empty:
2016 except queue.Empty:
2009 continue
2017 continue
2010 except KeyboardInterrupt:
2018 except KeyboardInterrupt:
2011 for test in runtests:
2019 for test in runtests:
2012 test.abort()
2020 test.abort()
2013
2021
2014 channels = []
2022 channels = []
2015
2023
2016 return result
2024 return result
2017
2025
2018 # 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
2019 # human-readable text file named .testtimes. Tests are sorted
2027 # human-readable text file named .testtimes. Tests are sorted
2020 # alphabetically, while times for each test are listed from oldest to
2028 # alphabetically, while times for each test are listed from oldest to
2021 # newest.
2029 # newest.
2022
2030
2023 def loadtimes(outputdir):
2031 def loadtimes(outputdir):
2024 times = []
2032 times = []
2025 try:
2033 try:
2026 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2034 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2027 for line in fp:
2035 for line in fp:
2028 m = re.match('(.*?) ([0-9. ]+)', line)
2036 m = re.match('(.*?) ([0-9. ]+)', line)
2029 times.append((m.group(1),
2037 times.append((m.group(1),
2030 [float(t) for t in m.group(2).split()]))
2038 [float(t) for t in m.group(2).split()]))
2031 except IOError as err:
2039 except IOError as err:
2032 if err.errno != errno.ENOENT:
2040 if err.errno != errno.ENOENT:
2033 raise
2041 raise
2034 return times
2042 return times
2035
2043
2036 def savetimes(outputdir, result):
2044 def savetimes(outputdir, result):
2037 saved = dict(loadtimes(outputdir))
2045 saved = dict(loadtimes(outputdir))
2038 maxruns = 5
2046 maxruns = 5
2039 skipped = set([str(t[0]) for t in result.skipped])
2047 skipped = set([str(t[0]) for t in result.skipped])
2040 for tdata in result.times:
2048 for tdata in result.times:
2041 test, real = tdata[0], tdata[3]
2049 test, real = tdata[0], tdata[3]
2042 if test not in skipped:
2050 if test not in skipped:
2043 ts = saved.setdefault(test, [])
2051 ts = saved.setdefault(test, [])
2044 ts.append(real)
2052 ts.append(real)
2045 ts[:] = ts[-maxruns:]
2053 ts[:] = ts[-maxruns:]
2046
2054
2047 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2055 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2048 dir=outputdir, text=True)
2056 dir=outputdir, text=True)
2049 with os.fdopen(fd, 'w') as fp:
2057 with os.fdopen(fd, 'w') as fp:
2050 for name, ts in sorted(saved.items()):
2058 for name, ts in sorted(saved.items()):
2051 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])))
2052 timepath = os.path.join(outputdir, b'.testtimes')
2060 timepath = os.path.join(outputdir, b'.testtimes')
2053 try:
2061 try:
2054 os.unlink(timepath)
2062 os.unlink(timepath)
2055 except OSError:
2063 except OSError:
2056 pass
2064 pass
2057 try:
2065 try:
2058 os.rename(tmpname, timepath)
2066 os.rename(tmpname, timepath)
2059 except OSError:
2067 except OSError:
2060 pass
2068 pass
2061
2069
2062 class TextTestRunner(unittest.TextTestRunner):
2070 class TextTestRunner(unittest.TextTestRunner):
2063 """Custom unittest test runner that uses appropriate settings."""
2071 """Custom unittest test runner that uses appropriate settings."""
2064
2072
2065 def __init__(self, runner, *args, **kwargs):
2073 def __init__(self, runner, *args, **kwargs):
2066 super(TextTestRunner, self).__init__(*args, **kwargs)
2074 super(TextTestRunner, self).__init__(*args, **kwargs)
2067
2075
2068 self._runner = runner
2076 self._runner = runner
2069
2077
2070 def listtests(self, test):
2078 def listtests(self, test):
2071 result = TestResult(self._runner.options, self.stream,
2079 result = TestResult(self._runner.options, self.stream,
2072 self.descriptions, 0)
2080 self.descriptions, 0)
2073 test = sorted(test, key=lambda t: t.name)
2081 test = sorted(test, key=lambda t: t.name)
2074 for t in test:
2082 for t in test:
2075 print(t.name)
2083 print(t.name)
2076 result.addSuccess(t)
2084 result.addSuccess(t)
2077
2085
2078 if self._runner.options.xunit:
2086 if self._runner.options.xunit:
2079 with open(self._runner.options.xunit, "wb") as xuf:
2087 with open(self._runner.options.xunit, "wb") as xuf:
2080 self._writexunit(result, xuf)
2088 self._writexunit(result, xuf)
2081
2089
2082 if self._runner.options.json:
2090 if self._runner.options.json:
2083 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2091 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2084 with open(jsonpath, 'w') as fp:
2092 with open(jsonpath, 'w') as fp:
2085 self._writejson(result, fp)
2093 self._writejson(result, fp)
2086
2094
2087 return result
2095 return result
2088
2096
2089 def run(self, test):
2097 def run(self, test):
2090 result = TestResult(self._runner.options, self.stream,
2098 result = TestResult(self._runner.options, self.stream,
2091 self.descriptions, self.verbosity)
2099 self.descriptions, self.verbosity)
2092
2100
2093 test(result)
2101 test(result)
2094
2102
2095 failed = len(result.failures)
2103 failed = len(result.failures)
2096 skipped = len(result.skipped)
2104 skipped = len(result.skipped)
2097 ignored = len(result.ignored)
2105 ignored = len(result.ignored)
2098
2106
2099 with iolock:
2107 with iolock:
2100 self.stream.writeln('')
2108 self.stream.writeln('')
2101
2109
2102 if not self._runner.options.noskips:
2110 if not self._runner.options.noskips:
2103 for test, msg in result.skipped:
2111 for test, msg in result.skipped:
2104 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2112 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2105 self.stream.write(highlightmsg(formatted, result.color))
2113 self.stream.write(highlightmsg(formatted, result.color))
2106 for test, msg in result.failures:
2114 for test, msg in result.failures:
2107 formatted = 'Failed %s: %s\n' % (test.name, msg)
2115 formatted = 'Failed %s: %s\n' % (test.name, msg)
2108 self.stream.write(highlightmsg(formatted, result.color))
2116 self.stream.write(highlightmsg(formatted, result.color))
2109 for test, msg in result.errors:
2117 for test, msg in result.errors:
2110 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2118 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2111
2119
2112 if self._runner.options.xunit:
2120 if self._runner.options.xunit:
2113 with open(self._runner.options.xunit, "wb") as xuf:
2121 with open(self._runner.options.xunit, "wb") as xuf:
2114 self._writexunit(result, xuf)
2122 self._writexunit(result, xuf)
2115
2123
2116 if self._runner.options.json:
2124 if self._runner.options.json:
2117 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2125 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2118 with open(jsonpath, 'w') as fp:
2126 with open(jsonpath, 'w') as fp:
2119 self._writejson(result, fp)
2127 self._writejson(result, fp)
2120
2128
2121 self._runner._checkhglib('Tested')
2129 self._runner._checkhglib('Tested')
2122
2130
2123 savetimes(self._runner._outputdir, result)
2131 savetimes(self._runner._outputdir, result)
2124
2132
2125 if failed and self._runner.options.known_good_rev:
2133 if failed and self._runner.options.known_good_rev:
2126 self._bisecttests(t for t, m in result.failures)
2134 self._bisecttests(t for t, m in result.failures)
2127 self.stream.writeln(
2135 self.stream.writeln(
2128 '# Ran %d tests, %d skipped, %d failed.'
2136 '# Ran %d tests, %d skipped, %d failed.'
2129 % (result.testsRun, skipped + ignored, failed))
2137 % (result.testsRun, skipped + ignored, failed))
2130 if failed:
2138 if failed:
2131 self.stream.writeln('python hash seed: %s' %
2139 self.stream.writeln('python hash seed: %s' %
2132 os.environ['PYTHONHASHSEED'])
2140 os.environ['PYTHONHASHSEED'])
2133 if self._runner.options.time:
2141 if self._runner.options.time:
2134 self.printtimes(result.times)
2142 self.printtimes(result.times)
2135
2143
2136 if self._runner.options.exceptions:
2144 if self._runner.options.exceptions:
2137 exceptions = aggregateexceptions(
2145 exceptions = aggregateexceptions(
2138 os.path.join(self._runner._outputdir, b'exceptions'))
2146 os.path.join(self._runner._outputdir, b'exceptions'))
2139
2147
2140 self.stream.writeln('Exceptions Report:')
2148 self.stream.writeln('Exceptions Report:')
2141 self.stream.writeln('%d total from %d frames' %
2149 self.stream.writeln('%d total from %d frames' %
2142 (exceptions['total'],
2150 (exceptions['total'],
2143 len(exceptions['exceptioncounts'])))
2151 len(exceptions['exceptioncounts'])))
2144 combined = exceptions['combined']
2152 combined = exceptions['combined']
2145 for key in sorted(combined, key=combined.get, reverse=True):
2153 for key in sorted(combined, key=combined.get, reverse=True):
2146 frame, line, exc = key
2154 frame, line, exc = key
2147 totalcount, testcount, leastcount, leasttest = combined[key]
2155 totalcount, testcount, leastcount, leasttest = combined[key]
2148
2156
2149 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)'
2150 % (totalcount,
2158 % (totalcount,
2151 testcount,
2159 testcount,
2152 frame, exc,
2160 frame, exc,
2153 leasttest, leastcount))
2161 leasttest, leastcount))
2154
2162
2155 self.stream.flush()
2163 self.stream.flush()
2156
2164
2157 return result
2165 return result
2158
2166
2159 def _bisecttests(self, tests):
2167 def _bisecttests(self, tests):
2160 bisectcmd = ['hg', 'bisect']
2168 bisectcmd = ['hg', 'bisect']
2161 bisectrepo = self._runner.options.bisect_repo
2169 bisectrepo = self._runner.options.bisect_repo
2162 if bisectrepo:
2170 if bisectrepo:
2163 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2171 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2164 def pread(args):
2172 def pread(args):
2165 env = os.environ.copy()
2173 env = os.environ.copy()
2166 env['HGPLAIN'] = '1'
2174 env['HGPLAIN'] = '1'
2167 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2175 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2168 stdout=subprocess.PIPE, env=env)
2176 stdout=subprocess.PIPE, env=env)
2169 data = p.stdout.read()
2177 data = p.stdout.read()
2170 p.wait()
2178 p.wait()
2171 return data
2179 return data
2172 for test in tests:
2180 for test in tests:
2173 pread(bisectcmd + ['--reset']),
2181 pread(bisectcmd + ['--reset']),
2174 pread(bisectcmd + ['--bad', '.'])
2182 pread(bisectcmd + ['--bad', '.'])
2175 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2183 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2176 # TODO: we probably need to forward more options
2184 # TODO: we probably need to forward more options
2177 # that alter hg's behavior inside the tests.
2185 # that alter hg's behavior inside the tests.
2178 opts = ''
2186 opts = ''
2179 withhg = self._runner.options.with_hg
2187 withhg = self._runner.options.with_hg
2180 if withhg:
2188 if withhg:
2181 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2189 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2182 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2190 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2183 test)
2191 test)
2184 data = pread(bisectcmd + ['--command', rtc])
2192 data = pread(bisectcmd + ['--command', rtc])
2185 m = re.search(
2193 m = re.search(
2186 (br'\nThe first (?P<goodbad>bad|good) revision '
2194 (br'\nThe first (?P<goodbad>bad|good) revision '
2187 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2195 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2188 br'summary: +(?P<summary>[^\n]+)\n'),
2196 br'summary: +(?P<summary>[^\n]+)\n'),
2189 data, (re.MULTILINE | re.DOTALL))
2197 data, (re.MULTILINE | re.DOTALL))
2190 if m is None:
2198 if m is None:
2191 self.stream.writeln(
2199 self.stream.writeln(
2192 'Failed to identify failure point for %s' % test)
2200 'Failed to identify failure point for %s' % test)
2193 continue
2201 continue
2194 dat = m.groupdict()
2202 dat = m.groupdict()
2195 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2203 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2196 self.stream.writeln(
2204 self.stream.writeln(
2197 '%s %s by %s (%s)' % (
2205 '%s %s by %s (%s)' % (
2198 test, verb, dat['node'], dat['summary']))
2206 test, verb, dat['node'], dat['summary']))
2199
2207
2200 def printtimes(self, times):
2208 def printtimes(self, times):
2201 # iolock held by run
2209 # iolock held by run
2202 self.stream.writeln('# Producing time report')
2210 self.stream.writeln('# Producing time report')
2203 times.sort(key=lambda t: (t[3]))
2211 times.sort(key=lambda t: (t[3]))
2204 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'
2205 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2213 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2206 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2214 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2207 for tdata in times:
2215 for tdata in times:
2208 test = tdata[0]
2216 test = tdata[0]
2209 cuser, csys, real, start, end = tdata[1:6]
2217 cuser, csys, real, start, end = tdata[1:6]
2210 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2218 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2211
2219
2212 @staticmethod
2220 @staticmethod
2213 def _writexunit(result, outf):
2221 def _writexunit(result, outf):
2214 # See http://llg.cubic.org/docs/junit/ for a reference.
2222 # See http://llg.cubic.org/docs/junit/ for a reference.
2215 timesd = dict((t[0], t[3]) for t in result.times)
2223 timesd = dict((t[0], t[3]) for t in result.times)
2216 doc = minidom.Document()
2224 doc = minidom.Document()
2217 s = doc.createElement('testsuite')
2225 s = doc.createElement('testsuite')
2218 s.setAttribute('name', 'run-tests')
2226 s.setAttribute('name', 'run-tests')
2219 s.setAttribute('tests', str(result.testsRun))
2227 s.setAttribute('tests', str(result.testsRun))
2220 s.setAttribute('errors', "0") # TODO
2228 s.setAttribute('errors', "0") # TODO
2221 s.setAttribute('failures', str(len(result.failures)))
2229 s.setAttribute('failures', str(len(result.failures)))
2222 s.setAttribute('skipped', str(len(result.skipped) +
2230 s.setAttribute('skipped', str(len(result.skipped) +
2223 len(result.ignored)))
2231 len(result.ignored)))
2224 doc.appendChild(s)
2232 doc.appendChild(s)
2225 for tc in result.successes:
2233 for tc in result.successes:
2226 t = doc.createElement('testcase')
2234 t = doc.createElement('testcase')
2227 t.setAttribute('name', tc.name)
2235 t.setAttribute('name', tc.name)
2228 tctime = timesd.get(tc.name)
2236 tctime = timesd.get(tc.name)
2229 if tctime is not None:
2237 if tctime is not None:
2230 t.setAttribute('time', '%.3f' % tctime)
2238 t.setAttribute('time', '%.3f' % tctime)
2231 s.appendChild(t)
2239 s.appendChild(t)
2232 for tc, err in sorted(result.faildata.items()):
2240 for tc, err in sorted(result.faildata.items()):
2233 t = doc.createElement('testcase')
2241 t = doc.createElement('testcase')
2234 t.setAttribute('name', tc)
2242 t.setAttribute('name', tc)
2235 tctime = timesd.get(tc)
2243 tctime = timesd.get(tc)
2236 if tctime is not None:
2244 if tctime is not None:
2237 t.setAttribute('time', '%.3f' % tctime)
2245 t.setAttribute('time', '%.3f' % tctime)
2238 # createCDATASection expects a unicode or it will
2246 # createCDATASection expects a unicode or it will
2239 # convert using default conversion rules, which will
2247 # convert using default conversion rules, which will
2240 # fail if string isn't ASCII.
2248 # fail if string isn't ASCII.
2241 err = cdatasafe(err).decode('utf-8', 'replace')
2249 err = cdatasafe(err).decode('utf-8', 'replace')
2242 cd = doc.createCDATASection(err)
2250 cd = doc.createCDATASection(err)
2243 # Use 'failure' here instead of 'error' to match errors = 0,
2251 # Use 'failure' here instead of 'error' to match errors = 0,
2244 # failures = len(result.failures) in the testsuite element.
2252 # failures = len(result.failures) in the testsuite element.
2245 failelem = doc.createElement('failure')
2253 failelem = doc.createElement('failure')
2246 failelem.setAttribute('message', 'output changed')
2254 failelem.setAttribute('message', 'output changed')
2247 failelem.setAttribute('type', 'output-mismatch')
2255 failelem.setAttribute('type', 'output-mismatch')
2248 failelem.appendChild(cd)
2256 failelem.appendChild(cd)
2249 t.appendChild(failelem)
2257 t.appendChild(failelem)
2250 s.appendChild(t)
2258 s.appendChild(t)
2251 for tc, message in result.skipped:
2259 for tc, message in result.skipped:
2252 # According to the schema, 'skipped' has no attributes. So store
2260 # According to the schema, 'skipped' has no attributes. So store
2253 # the skip message as a text node instead.
2261 # the skip message as a text node instead.
2254 t = doc.createElement('testcase')
2262 t = doc.createElement('testcase')
2255 t.setAttribute('name', tc.name)
2263 t.setAttribute('name', tc.name)
2256 binmessage = message.encode('utf-8')
2264 binmessage = message.encode('utf-8')
2257 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2265 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2258 cd = doc.createCDATASection(message)
2266 cd = doc.createCDATASection(message)
2259 skipelem = doc.createElement('skipped')
2267 skipelem = doc.createElement('skipped')
2260 skipelem.appendChild(cd)
2268 skipelem.appendChild(cd)
2261 t.appendChild(skipelem)
2269 t.appendChild(skipelem)
2262 s.appendChild(t)
2270 s.appendChild(t)
2263 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2271 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2264
2272
2265 @staticmethod
2273 @staticmethod
2266 def _writejson(result, outf):
2274 def _writejson(result, outf):
2267 timesd = {}
2275 timesd = {}
2268 for tdata in result.times:
2276 for tdata in result.times:
2269 test = tdata[0]
2277 test = tdata[0]
2270 timesd[test] = tdata[1:]
2278 timesd[test] = tdata[1:]
2271
2279
2272 outcome = {}
2280 outcome = {}
2273 groups = [('success', ((tc, None)
2281 groups = [('success', ((tc, None)
2274 for tc in result.successes)),
2282 for tc in result.successes)),
2275 ('failure', result.failures),
2283 ('failure', result.failures),
2276 ('skip', result.skipped)]
2284 ('skip', result.skipped)]
2277 for res, testcases in groups:
2285 for res, testcases in groups:
2278 for tc, __ in testcases:
2286 for tc, __ in testcases:
2279 if tc.name in timesd:
2287 if tc.name in timesd:
2280 diff = result.faildata.get(tc.name, b'')
2288 diff = result.faildata.get(tc.name, b'')
2281 try:
2289 try:
2282 diff = diff.decode('unicode_escape')
2290 diff = diff.decode('unicode_escape')
2283 except UnicodeDecodeError as e:
2291 except UnicodeDecodeError as e:
2284 diff = '%r decoding diff, sorry' % e
2292 diff = '%r decoding diff, sorry' % e
2285 tres = {'result': res,
2293 tres = {'result': res,
2286 'time': ('%0.3f' % timesd[tc.name][2]),
2294 'time': ('%0.3f' % timesd[tc.name][2]),
2287 'cuser': ('%0.3f' % timesd[tc.name][0]),
2295 'cuser': ('%0.3f' % timesd[tc.name][0]),
2288 'csys': ('%0.3f' % timesd[tc.name][1]),
2296 'csys': ('%0.3f' % timesd[tc.name][1]),
2289 'start': ('%0.3f' % timesd[tc.name][3]),
2297 'start': ('%0.3f' % timesd[tc.name][3]),
2290 'end': ('%0.3f' % timesd[tc.name][4]),
2298 'end': ('%0.3f' % timesd[tc.name][4]),
2291 'diff': diff,
2299 'diff': diff,
2292 }
2300 }
2293 else:
2301 else:
2294 # blacklisted test
2302 # blacklisted test
2295 tres = {'result': res}
2303 tres = {'result': res}
2296
2304
2297 outcome[tc.name] = tres
2305 outcome[tc.name] = tres
2298 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2306 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2299 separators=(',', ': '))
2307 separators=(',', ': '))
2300 outf.writelines(("testreport =", jsonout))
2308 outf.writelines(("testreport =", jsonout))
2301
2309
2302 def sorttests(testdescs, shuffle=False):
2310 def sorttests(testdescs, shuffle=False):
2303 """Do an in-place sort of tests."""
2311 """Do an in-place sort of tests."""
2304 if shuffle:
2312 if shuffle:
2305 random.shuffle(testdescs)
2313 random.shuffle(testdescs)
2306 return
2314 return
2307
2315
2308 # keywords for slow tests
2316 # keywords for slow tests
2309 slow = {b'svn': 10,
2317 slow = {b'svn': 10,
2310 b'cvs': 10,
2318 b'cvs': 10,
2311 b'hghave': 10,
2319 b'hghave': 10,
2312 b'largefiles-update': 10,
2320 b'largefiles-update': 10,
2313 b'run-tests': 10,
2321 b'run-tests': 10,
2314 b'corruption': 10,
2322 b'corruption': 10,
2315 b'race': 10,
2323 b'race': 10,
2316 b'i18n': 10,
2324 b'i18n': 10,
2317 b'check': 100,
2325 b'check': 100,
2318 b'gendoc': 100,
2326 b'gendoc': 100,
2319 b'contrib-perf': 200,
2327 b'contrib-perf': 200,
2320 }
2328 }
2321 perf = {}
2329 perf = {}
2322
2330
2323 def sortkey(f):
2331 def sortkey(f):
2324 # run largest tests first, as they tend to take the longest
2332 # run largest tests first, as they tend to take the longest
2325 f = f['path']
2333 f = f['path']
2326 try:
2334 try:
2327 return perf[f]
2335 return perf[f]
2328 except KeyError:
2336 except KeyError:
2329 try:
2337 try:
2330 val = -os.stat(f).st_size
2338 val = -os.stat(f).st_size
2331 except OSError as e:
2339 except OSError as e:
2332 if e.errno != errno.ENOENT:
2340 if e.errno != errno.ENOENT:
2333 raise
2341 raise
2334 perf[f] = -1e9 # file does not exist, tell early
2342 perf[f] = -1e9 # file does not exist, tell early
2335 return -1e9
2343 return -1e9
2336 for kw, mul in slow.items():
2344 for kw, mul in slow.items():
2337 if kw in f:
2345 if kw in f:
2338 val *= mul
2346 val *= mul
2339 if f.endswith(b'.py'):
2347 if f.endswith(b'.py'):
2340 val /= 10.0
2348 val /= 10.0
2341 perf[f] = val / 1000.0
2349 perf[f] = val / 1000.0
2342 return perf[f]
2350 return perf[f]
2343
2351
2344 testdescs.sort(key=sortkey)
2352 testdescs.sort(key=sortkey)
2345
2353
2346 class TestRunner(object):
2354 class TestRunner(object):
2347 """Holds context for executing tests.
2355 """Holds context for executing tests.
2348
2356
2349 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.
2350 """
2358 """
2351
2359
2352 # Programs required to run tests.
2360 # Programs required to run tests.
2353 REQUIREDTOOLS = [
2361 REQUIREDTOOLS = [
2354 b'diff',
2362 b'diff',
2355 b'grep',
2363 b'grep',
2356 b'unzip',
2364 b'unzip',
2357 b'gunzip',
2365 b'gunzip',
2358 b'bunzip2',
2366 b'bunzip2',
2359 b'sed',
2367 b'sed',
2360 ]
2368 ]
2361
2369
2362 # Maps file extensions to test class.
2370 # Maps file extensions to test class.
2363 TESTTYPES = [
2371 TESTTYPES = [
2364 (b'.py', PythonTest),
2372 (b'.py', PythonTest),
2365 (b'.t', TTest),
2373 (b'.t', TTest),
2366 ]
2374 ]
2367
2375
2368 def __init__(self):
2376 def __init__(self):
2369 self.options = None
2377 self.options = None
2370 self._hgroot = None
2378 self._hgroot = None
2371 self._testdir = None
2379 self._testdir = None
2372 self._outputdir = None
2380 self._outputdir = None
2373 self._hgtmp = None
2381 self._hgtmp = None
2374 self._installdir = None
2382 self._installdir = None
2375 self._bindir = None
2383 self._bindir = None
2376 self._tmpbinddir = None
2384 self._tmpbinddir = None
2377 self._pythondir = None
2385 self._pythondir = None
2378 self._coveragefile = None
2386 self._coveragefile = None
2379 self._createdfiles = []
2387 self._createdfiles = []
2380 self._hgcommand = None
2388 self._hgcommand = None
2381 self._hgpath = None
2389 self._hgpath = None
2382 self._portoffset = 0
2390 self._portoffset = 0
2383 self._ports = {}
2391 self._ports = {}
2384
2392
2385 def run(self, args, parser=None):
2393 def run(self, args, parser=None):
2386 """Run the test suite."""
2394 """Run the test suite."""
2387 oldmask = os.umask(0o22)
2395 oldmask = os.umask(0o22)
2388 try:
2396 try:
2389 parser = parser or getparser()
2397 parser = parser or getparser()
2390 options = parseargs(args, parser)
2398 options = parseargs(args, parser)
2391 tests = [_bytespath(a) for a in options.tests]
2399 tests = [_bytespath(a) for a in options.tests]
2392 if options.test_list is not None:
2400 if options.test_list is not None:
2393 for listfile in options.test_list:
2401 for listfile in options.test_list:
2394 with open(listfile, 'rb') as f:
2402 with open(listfile, 'rb') as f:
2395 tests.extend(t for t in f.read().splitlines() if t)
2403 tests.extend(t for t in f.read().splitlines() if t)
2396 self.options = options
2404 self.options = options
2397
2405
2398 self._checktools()
2406 self._checktools()
2399 testdescs = self.findtests(tests)
2407 testdescs = self.findtests(tests)
2400 if options.profile_runner:
2408 if options.profile_runner:
2401 import statprof
2409 import statprof
2402 statprof.start()
2410 statprof.start()
2403 result = self._run(testdescs)
2411 result = self._run(testdescs)
2404 if options.profile_runner:
2412 if options.profile_runner:
2405 statprof.stop()
2413 statprof.stop()
2406 statprof.display()
2414 statprof.display()
2407 return result
2415 return result
2408
2416
2409 finally:
2417 finally:
2410 os.umask(oldmask)
2418 os.umask(oldmask)
2411
2419
2412 def _run(self, testdescs):
2420 def _run(self, testdescs):
2413 sorttests(testdescs, shuffle=self.options.random)
2421 sorttests(testdescs, shuffle=self.options.random)
2414
2422
2415 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2423 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2416 os, 'getcwdb', os.getcwd)()
2424 os, 'getcwdb', os.getcwd)()
2417 # assume all tests in same folder for now
2425 # assume all tests in same folder for now
2418 if testdescs:
2426 if testdescs:
2419 pathname = os.path.dirname(testdescs[0]['path'])
2427 pathname = os.path.dirname(testdescs[0]['path'])
2420 if pathname:
2428 if pathname:
2421 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2429 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2422 pathname)
2430 pathname)
2423 if self.options.outputdir:
2431 if self.options.outputdir:
2424 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2432 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2425 else:
2433 else:
2426 self._outputdir = self._testdir
2434 self._outputdir = self._testdir
2427 if testdescs and pathname:
2435 if testdescs and pathname:
2428 self._outputdir = os.path.join(self._outputdir, pathname)
2436 self._outputdir = os.path.join(self._outputdir, pathname)
2429
2437
2430 if 'PYTHONHASHSEED' not in os.environ:
2438 if 'PYTHONHASHSEED' not in os.environ:
2431 # use a random python hash seed all the time
2439 # use a random python hash seed all the time
2432 # we do the randomness ourself to know what seed is used
2440 # we do the randomness ourself to know what seed is used
2433 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2441 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2434
2442
2435 if self.options.tmpdir:
2443 if self.options.tmpdir:
2436 self.options.keep_tmpdir = True
2444 self.options.keep_tmpdir = True
2437 tmpdir = _bytespath(self.options.tmpdir)
2445 tmpdir = _bytespath(self.options.tmpdir)
2438 if os.path.exists(tmpdir):
2446 if os.path.exists(tmpdir):
2439 # 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
2440 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2448 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2441 # tmpdir already exists.
2449 # tmpdir already exists.
2442 print("error: temp dir %r already exists" % tmpdir)
2450 print("error: temp dir %r already exists" % tmpdir)
2443 return 1
2451 return 1
2444
2452
2445 os.makedirs(tmpdir)
2453 os.makedirs(tmpdir)
2446 else:
2454 else:
2447 d = None
2455 d = None
2448 if os.name == 'nt':
2456 if os.name == 'nt':
2449 # without this, we get the default temp dir location, but
2457 # without this, we get the default temp dir location, but
2450 # in all lowercase, which causes troubles with paths (issue3490)
2458 # in all lowercase, which causes troubles with paths (issue3490)
2451 d = osenvironb.get(b'TMP', None)
2459 d = osenvironb.get(b'TMP', None)
2452 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2460 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2453
2461
2454 self._hgtmp = osenvironb[b'HGTMP'] = (
2462 self._hgtmp = osenvironb[b'HGTMP'] = (
2455 os.path.realpath(tmpdir))
2463 os.path.realpath(tmpdir))
2456
2464
2457 if self.options.with_hg:
2465 if self.options.with_hg:
2458 self._installdir = None
2466 self._installdir = None
2459 whg = self.options.with_hg
2467 whg = self.options.with_hg
2460 self._bindir = os.path.dirname(os.path.realpath(whg))
2468 self._bindir = os.path.dirname(os.path.realpath(whg))
2461 assert isinstance(self._bindir, bytes)
2469 assert isinstance(self._bindir, bytes)
2462 self._hgcommand = os.path.basename(whg)
2470 self._hgcommand = os.path.basename(whg)
2463 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2471 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2464 os.makedirs(self._tmpbindir)
2472 os.makedirs(self._tmpbindir)
2465
2473
2466 normbin = os.path.normpath(os.path.abspath(whg))
2474 normbin = os.path.normpath(os.path.abspath(whg))
2467 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2475 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2468
2476
2469 # Other Python scripts in the test harness need to
2477 # Other Python scripts in the test harness need to
2470 # `import mercurial`. If `hg` is a Python script, we assume
2478 # `import mercurial`. If `hg` is a Python script, we assume
2471 # 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
2472 # to load Python modules from its directory.
2480 # to load Python modules from its directory.
2473 with open(whg, 'rb') as fh:
2481 with open(whg, 'rb') as fh:
2474 initial = fh.read(1024)
2482 initial = fh.read(1024)
2475
2483
2476 if re.match(b'#!.*python', initial):
2484 if re.match(b'#!.*python', initial):
2477 self._pythondir = self._bindir
2485 self._pythondir = self._bindir
2478 # 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.
2479 # 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
2480 # source directory. So until it is, do the simple thing.
2488 # source directory. So until it is, do the simple thing.
2481 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2489 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2482 self._pythondir = os.path.dirname(self._testdir)
2490 self._pythondir = os.path.dirname(self._testdir)
2483 # Fall back to the legacy behavior.
2491 # Fall back to the legacy behavior.
2484 else:
2492 else:
2485 self._pythondir = self._bindir
2493 self._pythondir = self._bindir
2486
2494
2487 else:
2495 else:
2488 self._installdir = os.path.join(self._hgtmp, b"install")
2496 self._installdir = os.path.join(self._hgtmp, b"install")
2489 self._bindir = os.path.join(self._installdir, b"bin")
2497 self._bindir = os.path.join(self._installdir, b"bin")
2490 self._hgcommand = b'hg'
2498 self._hgcommand = b'hg'
2491 self._tmpbindir = self._bindir
2499 self._tmpbindir = self._bindir
2492 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2500 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2493
2501
2494 # set CHGHG, then replace "hg" command by "chg"
2502 # set CHGHG, then replace "hg" command by "chg"
2495 chgbindir = self._bindir
2503 chgbindir = self._bindir
2496 if self.options.chg or self.options.with_chg:
2504 if self.options.chg or self.options.with_chg:
2497 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2505 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2498 else:
2506 else:
2499 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2507 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2500 if self.options.chg:
2508 if self.options.chg:
2501 self._hgcommand = b'chg'
2509 self._hgcommand = b'chg'
2502 elif self.options.with_chg:
2510 elif self.options.with_chg:
2503 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2511 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2504 self._hgcommand = os.path.basename(self.options.with_chg)
2512 self._hgcommand = os.path.basename(self.options.with_chg)
2505
2513
2506 osenvironb[b"BINDIR"] = self._bindir
2514 osenvironb[b"BINDIR"] = self._bindir
2507 osenvironb[b"PYTHON"] = PYTHON
2515 osenvironb[b"PYTHON"] = PYTHON
2508
2516
2509 if self.options.with_python3:
2517 if self.options.with_python3:
2510 osenvironb[b'PYTHON3'] = self.options.with_python3
2518 osenvironb[b'PYTHON3'] = self.options.with_python3
2511
2519
2512 fileb = _bytespath(__file__)
2520 fileb = _bytespath(__file__)
2513 runtestdir = os.path.abspath(os.path.dirname(fileb))
2521 runtestdir = os.path.abspath(os.path.dirname(fileb))
2514 osenvironb[b'RUNTESTDIR'] = runtestdir
2522 osenvironb[b'RUNTESTDIR'] = runtestdir
2515 if PYTHON3:
2523 if PYTHON3:
2516 sepb = _bytespath(os.pathsep)
2524 sepb = _bytespath(os.pathsep)
2517 else:
2525 else:
2518 sepb = os.pathsep
2526 sepb = os.pathsep
2519 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2527 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2520 if os.path.islink(__file__):
2528 if os.path.islink(__file__):
2521 # test helper will likely be at the end of the symlink
2529 # test helper will likely be at the end of the symlink
2522 realfile = os.path.realpath(fileb)
2530 realfile = os.path.realpath(fileb)
2523 realdir = os.path.abspath(os.path.dirname(realfile))
2531 realdir = os.path.abspath(os.path.dirname(realfile))
2524 path.insert(2, realdir)
2532 path.insert(2, realdir)
2525 if chgbindir != self._bindir:
2533 if chgbindir != self._bindir:
2526 path.insert(1, chgbindir)
2534 path.insert(1, chgbindir)
2527 if self._testdir != runtestdir:
2535 if self._testdir != runtestdir:
2528 path = [self._testdir] + path
2536 path = [self._testdir] + path
2529 if self._tmpbindir != self._bindir:
2537 if self._tmpbindir != self._bindir:
2530 path = [self._tmpbindir] + path
2538 path = [self._tmpbindir] + path
2531 osenvironb[b"PATH"] = sepb.join(path)
2539 osenvironb[b"PATH"] = sepb.join(path)
2532
2540
2533 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2541 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2534 # can run .../tests/run-tests.py test-foo where test-foo
2542 # can run .../tests/run-tests.py test-foo where test-foo
2535 # 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
2536 # import modules like heredoctest.
2544 # import modules like heredoctest.
2537 pypath = [self._pythondir, self._testdir, runtestdir]
2545 pypath = [self._pythondir, self._testdir, runtestdir]
2538 # We have to augment PYTHONPATH, rather than simply replacing
2546 # We have to augment PYTHONPATH, rather than simply replacing
2539 # it, in case external libraries are only available via current
2547 # it, in case external libraries are only available via current
2540 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2548 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2541 # are in /opt/subversion.)
2549 # are in /opt/subversion.)
2542 oldpypath = osenvironb.get(IMPL_PATH)
2550 oldpypath = osenvironb.get(IMPL_PATH)
2543 if oldpypath:
2551 if oldpypath:
2544 pypath.append(oldpypath)
2552 pypath.append(oldpypath)
2545 osenvironb[IMPL_PATH] = sepb.join(pypath)
2553 osenvironb[IMPL_PATH] = sepb.join(pypath)
2546
2554
2547 if self.options.pure:
2555 if self.options.pure:
2548 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2556 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2549 os.environ["HGMODULEPOLICY"] = "py"
2557 os.environ["HGMODULEPOLICY"] = "py"
2550
2558
2551 if self.options.allow_slow_tests:
2559 if self.options.allow_slow_tests:
2552 os.environ["HGTEST_SLOW"] = "slow"
2560 os.environ["HGTEST_SLOW"] = "slow"
2553 elif 'HGTEST_SLOW' in os.environ:
2561 elif 'HGTEST_SLOW' in os.environ:
2554 del os.environ['HGTEST_SLOW']
2562 del os.environ['HGTEST_SLOW']
2555
2563
2556 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2564 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2557
2565
2558 if self.options.exceptions:
2566 if self.options.exceptions:
2559 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2567 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2560 try:
2568 try:
2561 os.makedirs(exceptionsdir)
2569 os.makedirs(exceptionsdir)
2562 except OSError as e:
2570 except OSError as e:
2563 if e.errno != errno.EEXIST:
2571 if e.errno != errno.EEXIST:
2564 raise
2572 raise
2565
2573
2566 # Remove all existing exception reports.
2574 # Remove all existing exception reports.
2567 for f in os.listdir(exceptionsdir):
2575 for f in os.listdir(exceptionsdir):
2568 os.unlink(os.path.join(exceptionsdir, f))
2576 os.unlink(os.path.join(exceptionsdir, f))
2569
2577
2570 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2578 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2571 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2579 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2572 self.options.extra_config_opt.append(
2580 self.options.extra_config_opt.append(
2573 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2581 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2574
2582
2575 vlog("# Using TESTDIR", self._testdir)
2583 vlog("# Using TESTDIR", self._testdir)
2576 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2584 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2577 vlog("# Using HGTMP", self._hgtmp)
2585 vlog("# Using HGTMP", self._hgtmp)
2578 vlog("# Using PATH", os.environ["PATH"])
2586 vlog("# Using PATH", os.environ["PATH"])
2579 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2587 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2580 vlog("# Writing to directory", self._outputdir)
2588 vlog("# Writing to directory", self._outputdir)
2581
2589
2582 try:
2590 try:
2583 return self._runtests(testdescs) or 0
2591 return self._runtests(testdescs) or 0
2584 finally:
2592 finally:
2585 time.sleep(.1)
2593 time.sleep(.1)
2586 self._cleanup()
2594 self._cleanup()
2587
2595
2588 def findtests(self, args):
2596 def findtests(self, args):
2589 """Finds possible test files from arguments.
2597 """Finds possible test files from arguments.
2590
2598
2591 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
2592 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.
2593 """
2601 """
2594 if not args:
2602 if not args:
2595 if self.options.changed:
2603 if self.options.changed:
2596 proc = Popen4('hg st --rev "%s" -man0 .' %
2604 proc = Popen4('hg st --rev "%s" -man0 .' %
2597 self.options.changed, None, 0)
2605 self.options.changed, None, 0)
2598 stdout, stderr = proc.communicate()
2606 stdout, stderr = proc.communicate()
2599 args = stdout.strip(b'\0').split(b'\0')
2607 args = stdout.strip(b'\0').split(b'\0')
2600 else:
2608 else:
2601 args = os.listdir(b'.')
2609 args = os.listdir(b'.')
2602
2610
2603 expanded_args = []
2611 expanded_args = []
2604 for arg in args:
2612 for arg in args:
2605 if os.path.isdir(arg):
2613 if os.path.isdir(arg):
2606 if not arg.endswith(b'/'):
2614 if not arg.endswith(b'/'):
2607 arg += b'/'
2615 arg += b'/'
2608 expanded_args.extend([arg + a for a in os.listdir(arg)])
2616 expanded_args.extend([arg + a for a in os.listdir(arg)])
2609 else:
2617 else:
2610 expanded_args.append(arg)
2618 expanded_args.append(arg)
2611 args = expanded_args
2619 args = expanded_args
2612
2620
2613 tests = []
2621 tests = []
2614 for t in args:
2622 for t in args:
2615 if not (os.path.basename(t).startswith(b'test-')
2623 if not (os.path.basename(t).startswith(b'test-')
2616 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2624 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2617 continue
2625 continue
2618 if t.endswith(b'.t'):
2626 if t.endswith(b'.t'):
2619 # .t file may contain multiple test cases
2627 # .t file may contain multiple test cases
2620 cases = sorted(parsettestcases(t))
2628 cases = sorted(parsettestcases(t))
2621 if cases:
2629 if cases:
2622 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2630 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2623 else:
2631 else:
2624 tests.append({'path': t})
2632 tests.append({'path': t})
2625 else:
2633 else:
2626 tests.append({'path': t})
2634 tests.append({'path': t})
2627 return tests
2635 return tests
2628
2636
2629 def _runtests(self, testdescs):
2637 def _runtests(self, testdescs):
2630 def _reloadtest(test, i):
2638 def _reloadtest(test, i):
2631 # convert a test back to its description dict
2639 # convert a test back to its description dict
2632 desc = {'path': test.path}
2640 desc = {'path': test.path}
2633 case = getattr(test, '_case', None)
2641 case = getattr(test, '_case', None)
2634 if case:
2642 if case:
2635 desc['case'] = case
2643 desc['case'] = case
2636 return self._gettest(desc, i)
2644 return self._gettest(desc, i)
2637
2645
2638 try:
2646 try:
2639 if self.options.restart:
2647 if self.options.restart:
2640 orig = list(testdescs)
2648 orig = list(testdescs)
2641 while testdescs:
2649 while testdescs:
2642 desc = testdescs[0]
2650 desc = testdescs[0]
2643 # desc['path'] is a relative path
2651 # desc['path'] is a relative path
2644 if 'case' in desc:
2652 if 'case' in desc:
2645 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2653 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2646 else:
2654 else:
2647 errpath = b'%s.err' % desc['path']
2655 errpath = b'%s.err' % desc['path']
2648 errpath = os.path.join(self._outputdir, errpath)
2656 errpath = os.path.join(self._outputdir, errpath)
2649 if os.path.exists(errpath):
2657 if os.path.exists(errpath):
2650 break
2658 break
2651 testdescs.pop(0)
2659 testdescs.pop(0)
2652 if not testdescs:
2660 if not testdescs:
2653 print("running all tests")
2661 print("running all tests")
2654 testdescs = orig
2662 testdescs = orig
2655
2663
2656 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2664 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2657
2665
2658 failed = False
2666 failed = False
2659 kws = self.options.keywords
2667 kws = self.options.keywords
2660 if kws is not None and PYTHON3:
2668 if kws is not None and PYTHON3:
2661 kws = kws.encode('utf-8')
2669 kws = kws.encode('utf-8')
2662
2670
2663 suite = TestSuite(self._testdir,
2671 suite = TestSuite(self._testdir,
2664 jobs=self.options.jobs,
2672 jobs=self.options.jobs,
2665 whitelist=self.options.whitelisted,
2673 whitelist=self.options.whitelisted,
2666 blacklist=self.options.blacklist,
2674 blacklist=self.options.blacklist,
2667 retest=self.options.retest,
2675 retest=self.options.retest,
2668 keywords=kws,
2676 keywords=kws,
2669 loop=self.options.loop,
2677 loop=self.options.loop,
2670 runs_per_test=self.options.runs_per_test,
2678 runs_per_test=self.options.runs_per_test,
2671 showchannels=self.options.showchannels,
2679 showchannels=self.options.showchannels,
2672 tests=tests, loadtest=_reloadtest)
2680 tests=tests, loadtest=_reloadtest)
2673 verbosity = 1
2681 verbosity = 1
2674 if self.options.verbose:
2682 if self.options.verbose:
2675 verbosity = 2
2683 verbosity = 2
2676 runner = TextTestRunner(self, verbosity=verbosity)
2684 runner = TextTestRunner(self, verbosity=verbosity)
2677
2685
2678 if self.options.list_tests:
2686 if self.options.list_tests:
2679 result = runner.listtests(suite)
2687 result = runner.listtests(suite)
2680 else:
2688 else:
2681 if self._installdir:
2689 if self._installdir:
2682 self._installhg()
2690 self._installhg()
2683 self._checkhglib("Testing")
2691 self._checkhglib("Testing")
2684 else:
2692 else:
2685 self._usecorrectpython()
2693 self._usecorrectpython()
2686 if self.options.chg:
2694 if self.options.chg:
2687 assert self._installdir
2695 assert self._installdir
2688 self._installchg()
2696 self._installchg()
2689
2697
2690 result = runner.run(suite)
2698 result = runner.run(suite)
2691
2699
2692 if result.failures:
2700 if result.failures:
2693 failed = True
2701 failed = True
2694
2702
2695 if self.options.anycoverage:
2703 if self.options.anycoverage:
2696 self._outputcoverage()
2704 self._outputcoverage()
2697 except KeyboardInterrupt:
2705 except KeyboardInterrupt:
2698 failed = True
2706 failed = True
2699 print("\ninterrupted!")
2707 print("\ninterrupted!")
2700
2708
2701 if failed:
2709 if failed:
2702 return 1
2710 return 1
2703
2711
2704 def _getport(self, count):
2712 def _getport(self, count):
2705 port = self._ports.get(count) # do we have a cached entry?
2713 port = self._ports.get(count) # do we have a cached entry?
2706 if port is None:
2714 if port is None:
2707 portneeded = 3
2715 portneeded = 3
2708 # 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
2709 for tries in xrange(100):
2717 for tries in xrange(100):
2710 allfree = True
2718 allfree = True
2711 port = self.options.port + self._portoffset
2719 port = self.options.port + self._portoffset
2712 for idx in xrange(portneeded):
2720 for idx in xrange(portneeded):
2713 if not checkportisavailable(port + idx):
2721 if not checkportisavailable(port + idx):
2714 allfree = False
2722 allfree = False
2715 break
2723 break
2716 self._portoffset += portneeded
2724 self._portoffset += portneeded
2717 if allfree:
2725 if allfree:
2718 break
2726 break
2719 self._ports[count] = port
2727 self._ports[count] = port
2720 return port
2728 return port
2721
2729
2722 def _gettest(self, testdesc, count):
2730 def _gettest(self, testdesc, count):
2723 """Obtain a Test by looking at its filename.
2731 """Obtain a Test by looking at its filename.
2724
2732
2725 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
2726 map to a known type.
2734 map to a known type.
2727 """
2735 """
2728 path = testdesc['path']
2736 path = testdesc['path']
2729 lctest = path.lower()
2737 lctest = path.lower()
2730 testcls = Test
2738 testcls = Test
2731
2739
2732 for ext, cls in self.TESTTYPES:
2740 for ext, cls in self.TESTTYPES:
2733 if lctest.endswith(ext):
2741 if lctest.endswith(ext):
2734 testcls = cls
2742 testcls = cls
2735 break
2743 break
2736
2744
2737 refpath = os.path.join(self._testdir, path)
2745 refpath = os.path.join(self._testdir, path)
2738 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2746 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2739
2747
2740 # extra keyword parameters. 'case' is used by .t tests
2748 # extra keyword parameters. 'case' is used by .t tests
2741 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)
2742
2750
2743 t = testcls(refpath, self._outputdir, tmpdir,
2751 t = testcls(refpath, self._outputdir, tmpdir,
2744 keeptmpdir=self.options.keep_tmpdir,
2752 keeptmpdir=self.options.keep_tmpdir,
2745 debug=self.options.debug,
2753 debug=self.options.debug,
2746 first=self.options.first,
2754 first=self.options.first,
2747 timeout=self.options.timeout,
2755 timeout=self.options.timeout,
2748 startport=self._getport(count),
2756 startport=self._getport(count),
2749 extraconfigopts=self.options.extra_config_opt,
2757 extraconfigopts=self.options.extra_config_opt,
2750 py3kwarnings=self.options.py3k_warnings,
2758 py3kwarnings=self.options.py3k_warnings,
2751 shell=self.options.shell,
2759 shell=self.options.shell,
2752 hgcommand=self._hgcommand,
2760 hgcommand=self._hgcommand,
2753 usechg=bool(self.options.with_chg or self.options.chg),
2761 usechg=bool(self.options.with_chg or self.options.chg),
2754 useipv6=useipv6, **kwds)
2762 useipv6=useipv6, **kwds)
2755 t.should_reload = True
2763 t.should_reload = True
2756 return t
2764 return t
2757
2765
2758 def _cleanup(self):
2766 def _cleanup(self):
2759 """Clean up state from this test invocation."""
2767 """Clean up state from this test invocation."""
2760 if self.options.keep_tmpdir:
2768 if self.options.keep_tmpdir:
2761 return
2769 return
2762
2770
2763 vlog("# Cleaning up HGTMP", self._hgtmp)
2771 vlog("# Cleaning up HGTMP", self._hgtmp)
2764 shutil.rmtree(self._hgtmp, True)
2772 shutil.rmtree(self._hgtmp, True)
2765 for f in self._createdfiles:
2773 for f in self._createdfiles:
2766 try:
2774 try:
2767 os.remove(f)
2775 os.remove(f)
2768 except OSError:
2776 except OSError:
2769 pass
2777 pass
2770
2778
2771 def _usecorrectpython(self):
2779 def _usecorrectpython(self):
2772 """Configure the environment to use the appropriate Python in tests."""
2780 """Configure the environment to use the appropriate Python in tests."""
2773 # 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.
2774 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2782 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2775 if getattr(os, 'symlink', None):
2783 if getattr(os, 'symlink', None):
2776 vlog("# Making python executable in test path a symlink to '%s'" %
2784 vlog("# Making python executable in test path a symlink to '%s'" %
2777 sys.executable)
2785 sys.executable)
2778 mypython = os.path.join(self._tmpbindir, pyexename)
2786 mypython = os.path.join(self._tmpbindir, pyexename)
2779 try:
2787 try:
2780 if os.readlink(mypython) == sys.executable:
2788 if os.readlink(mypython) == sys.executable:
2781 return
2789 return
2782 os.unlink(mypython)
2790 os.unlink(mypython)
2783 except OSError as err:
2791 except OSError as err:
2784 if err.errno != errno.ENOENT:
2792 if err.errno != errno.ENOENT:
2785 raise
2793 raise
2786 if self._findprogram(pyexename) != sys.executable:
2794 if self._findprogram(pyexename) != sys.executable:
2787 try:
2795 try:
2788 os.symlink(sys.executable, mypython)
2796 os.symlink(sys.executable, mypython)
2789 self._createdfiles.append(mypython)
2797 self._createdfiles.append(mypython)
2790 except OSError as err:
2798 except OSError as err:
2791 # child processes may race, which is harmless
2799 # child processes may race, which is harmless
2792 if err.errno != errno.EEXIST:
2800 if err.errno != errno.EEXIST:
2793 raise
2801 raise
2794 else:
2802 else:
2795 exedir, exename = os.path.split(sys.executable)
2803 exedir, exename = os.path.split(sys.executable)
2796 vlog("# Modifying search path to find %s as %s in '%s'" %
2804 vlog("# Modifying search path to find %s as %s in '%s'" %
2797 (exename, pyexename, exedir))
2805 (exename, pyexename, exedir))
2798 path = os.environ['PATH'].split(os.pathsep)
2806 path = os.environ['PATH'].split(os.pathsep)
2799 while exedir in path:
2807 while exedir in path:
2800 path.remove(exedir)
2808 path.remove(exedir)
2801 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2809 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2802 if not self._findprogram(pyexename):
2810 if not self._findprogram(pyexename):
2803 print("WARNING: Cannot find %s in search path" % pyexename)
2811 print("WARNING: Cannot find %s in search path" % pyexename)
2804
2812
2805 def _installhg(self):
2813 def _installhg(self):
2806 """Install hg into the test environment.
2814 """Install hg into the test environment.
2807
2815
2808 This will also configure hg with the appropriate testing settings.
2816 This will also configure hg with the appropriate testing settings.
2809 """
2817 """
2810 vlog("# Performing temporary installation of HG")
2818 vlog("# Performing temporary installation of HG")
2811 installerrs = os.path.join(self._hgtmp, b"install.err")
2819 installerrs = os.path.join(self._hgtmp, b"install.err")
2812 compiler = ''
2820 compiler = ''
2813 if self.options.compiler:
2821 if self.options.compiler:
2814 compiler = '--compiler ' + self.options.compiler
2822 compiler = '--compiler ' + self.options.compiler
2815 if self.options.pure:
2823 if self.options.pure:
2816 pure = b"--pure"
2824 pure = b"--pure"
2817 else:
2825 else:
2818 pure = b""
2826 pure = b""
2819
2827
2820 # Run installer in hg root
2828 # Run installer in hg root
2821 script = os.path.realpath(sys.argv[0])
2829 script = os.path.realpath(sys.argv[0])
2822 exe = sys.executable
2830 exe = sys.executable
2823 if PYTHON3:
2831 if PYTHON3:
2824 compiler = _bytespath(compiler)
2832 compiler = _bytespath(compiler)
2825 script = _bytespath(script)
2833 script = _bytespath(script)
2826 exe = _bytespath(exe)
2834 exe = _bytespath(exe)
2827 hgroot = os.path.dirname(os.path.dirname(script))
2835 hgroot = os.path.dirname(os.path.dirname(script))
2828 self._hgroot = hgroot
2836 self._hgroot = hgroot
2829 os.chdir(hgroot)
2837 os.chdir(hgroot)
2830 nohome = b'--home=""'
2838 nohome = b'--home=""'
2831 if os.name == 'nt':
2839 if os.name == 'nt':
2832 # The --home="" trick works only on OS where os.sep == '/'
2840 # The --home="" trick works only on OS where os.sep == '/'
2833 # because of a distutils convert_path() fast-path. Avoid it at
2841 # because of a distutils convert_path() fast-path. Avoid it at
2834 # least on Windows for now, deal with .pydistutils.cfg bugs
2842 # least on Windows for now, deal with .pydistutils.cfg bugs
2835 # when they happen.
2843 # when they happen.
2836 nohome = b''
2844 nohome = b''
2837 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2845 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2838 b' build %(compiler)s --build-base="%(base)s"'
2846 b' build %(compiler)s --build-base="%(base)s"'
2839 b' install --force --prefix="%(prefix)s"'
2847 b' install --force --prefix="%(prefix)s"'
2840 b' --install-lib="%(libdir)s"'
2848 b' --install-lib="%(libdir)s"'
2841 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2849 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2842 % {b'exe': exe, b'pure': pure,
2850 % {b'exe': exe, b'pure': pure,
2843 b'compiler': compiler,
2851 b'compiler': compiler,
2844 b'base': os.path.join(self._hgtmp, b"build"),
2852 b'base': os.path.join(self._hgtmp, b"build"),
2845 b'prefix': self._installdir, b'libdir': self._pythondir,
2853 b'prefix': self._installdir, b'libdir': self._pythondir,
2846 b'bindir': self._bindir,
2854 b'bindir': self._bindir,
2847 b'nohome': nohome, b'logfile': installerrs})
2855 b'nohome': nohome, b'logfile': installerrs})
2848
2856
2849 # setuptools requires install directories to exist.
2857 # setuptools requires install directories to exist.
2850 def makedirs(p):
2858 def makedirs(p):
2851 try:
2859 try:
2852 os.makedirs(p)
2860 os.makedirs(p)
2853 except OSError as e:
2861 except OSError as e:
2854 if e.errno != errno.EEXIST:
2862 if e.errno != errno.EEXIST:
2855 raise
2863 raise
2856 makedirs(self._pythondir)
2864 makedirs(self._pythondir)
2857 makedirs(self._bindir)
2865 makedirs(self._bindir)
2858
2866
2859 vlog("# Running", cmd)
2867 vlog("# Running", cmd)
2860 if os.system(cmd) == 0:
2868 if os.system(cmd) == 0:
2861 if not self.options.verbose:
2869 if not self.options.verbose:
2862 try:
2870 try:
2863 os.remove(installerrs)
2871 os.remove(installerrs)
2864 except OSError as e:
2872 except OSError as e:
2865 if e.errno != errno.ENOENT:
2873 if e.errno != errno.ENOENT:
2866 raise
2874 raise
2867 else:
2875 else:
2868 with open(installerrs, 'rb') as f:
2876 with open(installerrs, 'rb') as f:
2869 for line in f:
2877 for line in f:
2870 if PYTHON3:
2878 if PYTHON3:
2871 sys.stdout.buffer.write(line)
2879 sys.stdout.buffer.write(line)
2872 else:
2880 else:
2873 sys.stdout.write(line)
2881 sys.stdout.write(line)
2874 sys.exit(1)
2882 sys.exit(1)
2875 os.chdir(self._testdir)
2883 os.chdir(self._testdir)
2876
2884
2877 self._usecorrectpython()
2885 self._usecorrectpython()
2878
2886
2879 if self.options.py3k_warnings and not self.options.anycoverage:
2887 if self.options.py3k_warnings and not self.options.anycoverage:
2880 vlog("# Updating hg command to enable Py3k Warnings switch")
2888 vlog("# Updating hg command to enable Py3k Warnings switch")
2881 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2889 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2882 lines = [line.rstrip() for line in f]
2890 lines = [line.rstrip() for line in f]
2883 lines[0] += ' -3'
2891 lines[0] += ' -3'
2884 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2892 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2885 for line in lines:
2893 for line in lines:
2886 f.write(line + '\n')
2894 f.write(line + '\n')
2887
2895
2888 hgbat = os.path.join(self._bindir, b'hg.bat')
2896 hgbat = os.path.join(self._bindir, b'hg.bat')
2889 if os.path.isfile(hgbat):
2897 if os.path.isfile(hgbat):
2890 # 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
2891 # installation layout put it in bin/ directly. Fix it
2899 # installation layout put it in bin/ directly. Fix it
2892 with open(hgbat, 'rb') as f:
2900 with open(hgbat, 'rb') as f:
2893 data = f.read()
2901 data = f.read()
2894 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2902 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2895 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2903 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2896 b'"%~dp0python" "%~dp0hg" %*')
2904 b'"%~dp0python" "%~dp0hg" %*')
2897 with open(hgbat, 'wb') as f:
2905 with open(hgbat, 'wb') as f:
2898 f.write(data)
2906 f.write(data)
2899 else:
2907 else:
2900 print('WARNING: cannot fix hg.bat reference to python.exe')
2908 print('WARNING: cannot fix hg.bat reference to python.exe')
2901
2909
2902 if self.options.anycoverage:
2910 if self.options.anycoverage:
2903 custom = os.path.join(self._testdir, 'sitecustomize.py')
2911 custom = os.path.join(self._testdir, 'sitecustomize.py')
2904 target = os.path.join(self._pythondir, 'sitecustomize.py')
2912 target = os.path.join(self._pythondir, 'sitecustomize.py')
2905 vlog('# Installing coverage trigger to %s' % target)
2913 vlog('# Installing coverage trigger to %s' % target)
2906 shutil.copyfile(custom, target)
2914 shutil.copyfile(custom, target)
2907 rc = os.path.join(self._testdir, '.coveragerc')
2915 rc = os.path.join(self._testdir, '.coveragerc')
2908 vlog('# Installing coverage rc to %s' % rc)
2916 vlog('# Installing coverage rc to %s' % rc)
2909 os.environ['COVERAGE_PROCESS_START'] = rc
2917 os.environ['COVERAGE_PROCESS_START'] = rc
2910 covdir = os.path.join(self._installdir, '..', 'coverage')
2918 covdir = os.path.join(self._installdir, '..', 'coverage')
2911 try:
2919 try:
2912 os.mkdir(covdir)
2920 os.mkdir(covdir)
2913 except OSError as e:
2921 except OSError as e:
2914 if e.errno != errno.EEXIST:
2922 if e.errno != errno.EEXIST:
2915 raise
2923 raise
2916
2924
2917 os.environ['COVERAGE_DIR'] = covdir
2925 os.environ['COVERAGE_DIR'] = covdir
2918
2926
2919 def _checkhglib(self, verb):
2927 def _checkhglib(self, verb):
2920 """Ensure that the 'mercurial' package imported by python is
2928 """Ensure that the 'mercurial' package imported by python is
2921 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."""
2922 if ((self._bindir == self._pythondir) and
2930 if ((self._bindir == self._pythondir) and
2923 (self._bindir != self._tmpbindir)):
2931 (self._bindir != self._tmpbindir)):
2924 # The pythondir has been inferred from --with-hg flag.
2932 # The pythondir has been inferred from --with-hg flag.
2925 # We cannot expect anything sensible here.
2933 # We cannot expect anything sensible here.
2926 return
2934 return
2927 expecthg = os.path.join(self._pythondir, b'mercurial')
2935 expecthg = os.path.join(self._pythondir, b'mercurial')
2928 actualhg = self._gethgpath()
2936 actualhg = self._gethgpath()
2929 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2937 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2930 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2938 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2931 ' (expected %s)\n'
2939 ' (expected %s)\n'
2932 % (verb, actualhg, expecthg))
2940 % (verb, actualhg, expecthg))
2933 def _gethgpath(self):
2941 def _gethgpath(self):
2934 """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
2935 the current Python interpreter."""
2943 the current Python interpreter."""
2936 if self._hgpath is not None:
2944 if self._hgpath is not None:
2937 return self._hgpath
2945 return self._hgpath
2938
2946
2939 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2947 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2940 cmd = cmd % PYTHON
2948 cmd = cmd % PYTHON
2941 if PYTHON3:
2949 if PYTHON3:
2942 cmd = _strpath(cmd)
2950 cmd = _strpath(cmd)
2943 pipe = os.popen(cmd)
2951 pipe = os.popen(cmd)
2944 try:
2952 try:
2945 self._hgpath = _bytespath(pipe.read().strip())
2953 self._hgpath = _bytespath(pipe.read().strip())
2946 finally:
2954 finally:
2947 pipe.close()
2955 pipe.close()
2948
2956
2949 return self._hgpath
2957 return self._hgpath
2950
2958
2951 def _installchg(self):
2959 def _installchg(self):
2952 """Install chg into the test environment"""
2960 """Install chg into the test environment"""
2953 vlog('# Performing temporary installation of CHG')
2961 vlog('# Performing temporary installation of CHG')
2954 assert os.path.dirname(self._bindir) == self._installdir
2962 assert os.path.dirname(self._bindir) == self._installdir
2955 assert self._hgroot, 'must be called after _installhg()'
2963 assert self._hgroot, 'must be called after _installhg()'
2956 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2964 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2957 % {b'make': 'make', # TODO: switch by option or environment?
2965 % {b'make': 'make', # TODO: switch by option or environment?
2958 b'prefix': self._installdir})
2966 b'prefix': self._installdir})
2959 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2967 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2960 vlog("# Running", cmd)
2968 vlog("# Running", cmd)
2961 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2969 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2962 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2970 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2963 stderr=subprocess.STDOUT)
2971 stderr=subprocess.STDOUT)
2964 out, _err = proc.communicate()
2972 out, _err = proc.communicate()
2965 if proc.returncode != 0:
2973 if proc.returncode != 0:
2966 if PYTHON3:
2974 if PYTHON3:
2967 sys.stdout.buffer.write(out)
2975 sys.stdout.buffer.write(out)
2968 else:
2976 else:
2969 sys.stdout.write(out)
2977 sys.stdout.write(out)
2970 sys.exit(1)
2978 sys.exit(1)
2971
2979
2972 def _outputcoverage(self):
2980 def _outputcoverage(self):
2973 """Produce code coverage output."""
2981 """Produce code coverage output."""
2974 import coverage
2982 import coverage
2975 coverage = coverage.coverage
2983 coverage = coverage.coverage
2976
2984
2977 vlog('# Producing coverage report')
2985 vlog('# Producing coverage report')
2978 # 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
2979 # output.
2987 # output.
2980 os.chdir(self._hgroot)
2988 os.chdir(self._hgroot)
2981 covdir = os.path.join(self._installdir, '..', 'coverage')
2989 covdir = os.path.join(self._installdir, '..', 'coverage')
2982 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2990 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2983
2991
2984 # Map install directory paths back to source directory.
2992 # Map install directory paths back to source directory.
2985 cov.config.paths['srcdir'] = ['.', self._pythondir]
2993 cov.config.paths['srcdir'] = ['.', self._pythondir]
2986
2994
2987 cov.combine()
2995 cov.combine()
2988
2996
2989 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]]
2990 cov.report(ignore_errors=True, omit=omit)
2998 cov.report(ignore_errors=True, omit=omit)
2991
2999
2992 if self.options.htmlcov:
3000 if self.options.htmlcov:
2993 htmldir = os.path.join(self._outputdir, 'htmlcov')
3001 htmldir = os.path.join(self._outputdir, 'htmlcov')
2994 cov.html_report(directory=htmldir, omit=omit)
3002 cov.html_report(directory=htmldir, omit=omit)
2995 if self.options.annotate:
3003 if self.options.annotate:
2996 adir = os.path.join(self._outputdir, 'annotated')
3004 adir = os.path.join(self._outputdir, 'annotated')
2997 if not os.path.isdir(adir):
3005 if not os.path.isdir(adir):
2998 os.mkdir(adir)
3006 os.mkdir(adir)
2999 cov.annotate(directory=adir, omit=omit)
3007 cov.annotate(directory=adir, omit=omit)
3000
3008
3001 def _findprogram(self, program):
3009 def _findprogram(self, program):
3002 """Search PATH for a executable program"""
3010 """Search PATH for a executable program"""
3003 dpb = _bytespath(os.defpath)
3011 dpb = _bytespath(os.defpath)
3004 sepb = _bytespath(os.pathsep)
3012 sepb = _bytespath(os.pathsep)
3005 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3013 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3006 name = os.path.join(p, program)
3014 name = os.path.join(p, program)
3007 if os.name == 'nt' or os.access(name, os.X_OK):
3015 if os.name == 'nt' or os.access(name, os.X_OK):
3008 return name
3016 return name
3009 return None
3017 return None
3010
3018
3011 def _checktools(self):
3019 def _checktools(self):
3012 """Ensure tools required to run tests are present."""
3020 """Ensure tools required to run tests are present."""
3013 for p in self.REQUIREDTOOLS:
3021 for p in self.REQUIREDTOOLS:
3014 if os.name == 'nt' and not p.endswith('.exe'):
3022 if os.name == 'nt' and not p.endswith('.exe'):
3015 p += '.exe'
3023 p += '.exe'
3016 found = self._findprogram(p)
3024 found = self._findprogram(p)
3017 if found:
3025 if found:
3018 vlog("# Found prerequisite", p, "at", found)
3026 vlog("# Found prerequisite", p, "at", found)
3019 else:
3027 else:
3020 print("WARNING: Did not find prerequisite tool: %s " %
3028 print("WARNING: Did not find prerequisite tool: %s " %
3021 p.decode("utf-8"))
3029 p.decode("utf-8"))
3022
3030
3023 def aggregateexceptions(path):
3031 def aggregateexceptions(path):
3024 exceptioncounts = collections.Counter()
3032 exceptioncounts = collections.Counter()
3025 testsbyfailure = collections.defaultdict(set)
3033 testsbyfailure = collections.defaultdict(set)
3026 failuresbytest = collections.defaultdict(set)
3034 failuresbytest = collections.defaultdict(set)
3027
3035
3028 for f in os.listdir(path):
3036 for f in os.listdir(path):
3029 with open(os.path.join(path, f), 'rb') as fh:
3037 with open(os.path.join(path, f), 'rb') as fh:
3030 data = fh.read().split(b'\0')
3038 data = fh.read().split(b'\0')
3031 if len(data) != 5:
3039 if len(data) != 5:
3032 continue
3040 continue
3033
3041
3034 exc, mainframe, hgframe, hgline, testname = data
3042 exc, mainframe, hgframe, hgline, testname = data
3035 exc = exc.decode('utf-8')
3043 exc = exc.decode('utf-8')
3036 mainframe = mainframe.decode('utf-8')
3044 mainframe = mainframe.decode('utf-8')
3037 hgframe = hgframe.decode('utf-8')
3045 hgframe = hgframe.decode('utf-8')
3038 hgline = hgline.decode('utf-8')
3046 hgline = hgline.decode('utf-8')
3039 testname = testname.decode('utf-8')
3047 testname = testname.decode('utf-8')
3040
3048
3041 key = (hgframe, hgline, exc)
3049 key = (hgframe, hgline, exc)
3042 exceptioncounts[key] += 1
3050 exceptioncounts[key] += 1
3043 testsbyfailure[key].add(testname)
3051 testsbyfailure[key].add(testname)
3044 failuresbytest[testname].add(key)
3052 failuresbytest[testname].add(key)
3045
3053
3046 # Find test having fewest failures for each failure.
3054 # Find test having fewest failures for each failure.
3047 leastfailing = {}
3055 leastfailing = {}
3048 for key, tests in testsbyfailure.items():
3056 for key, tests in testsbyfailure.items():
3049 fewesttest = None
3057 fewesttest = None
3050 fewestcount = 99999999
3058 fewestcount = 99999999
3051 for test in sorted(tests):
3059 for test in sorted(tests):
3052 if len(failuresbytest[test]) < fewestcount:
3060 if len(failuresbytest[test]) < fewestcount:
3053 fewesttest = test
3061 fewesttest = test
3054 fewestcount = len(failuresbytest[test])
3062 fewestcount = len(failuresbytest[test])
3055
3063
3056 leastfailing[key] = (fewestcount, fewesttest)
3064 leastfailing[key] = (fewestcount, fewesttest)
3057
3065
3058 # 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
3059 # impacted tests.
3067 # impacted tests.
3060 combined = {}
3068 combined = {}
3061 for key in exceptioncounts:
3069 for key in exceptioncounts:
3062 combined[key] = (exceptioncounts[key],
3070 combined[key] = (exceptioncounts[key],
3063 len(testsbyfailure[key]),
3071 len(testsbyfailure[key]),
3064 leastfailing[key][0],
3072 leastfailing[key][0],
3065 leastfailing[key][1])
3073 leastfailing[key][1])
3066
3074
3067 return {
3075 return {
3068 'exceptioncounts': exceptioncounts,
3076 'exceptioncounts': exceptioncounts,
3069 'total': sum(exceptioncounts.values()),
3077 'total': sum(exceptioncounts.values()),
3070 'combined': combined,
3078 'combined': combined,
3071 'leastfailing': leastfailing,
3079 'leastfailing': leastfailing,
3072 'byfailure': testsbyfailure,
3080 'byfailure': testsbyfailure,
3073 'bytest': failuresbytest,
3081 'bytest': failuresbytest,
3074 }
3082 }
3075
3083
3076 if __name__ == '__main__':
3084 if __name__ == '__main__':
3077 runner = TestRunner()
3085 runner = TestRunner()
3078
3086
3079 try:
3087 try:
3080 import msvcrt
3088 import msvcrt
3081 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3089 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3082 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3090 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3083 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3091 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3084 except ImportError:
3092 except ImportError:
3085 pass
3093 pass
3086
3094
3087 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