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