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