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