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