##// END OF EJS Templates
merge with stable
Yuya Nishihara -
r45068:f8427841 merge default
parent child Browse files
Show More
@@ -1,345 +1,346 b''
1 #!/usr/bin/env python3
1 #!/usr/bin/env python3
2 #
2 #
3 # byteify-strings.py - transform string literals to be Python 3 safe
3 # byteify-strings.py - transform string literals to be Python 3 safe
4 #
4 #
5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.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 from __future__ import absolute_import, print_function
10 from __future__ import absolute_import, print_function
11
11
12 import argparse
12 import argparse
13 import contextlib
13 import contextlib
14 import errno
14 import errno
15 import os
15 import os
16 import sys
16 import sys
17 import tempfile
17 import tempfile
18 import token
18 import token
19 import tokenize
19 import tokenize
20
20
21
21
22 def adjusttokenpos(t, ofs):
22 def adjusttokenpos(t, ofs):
23 """Adjust start/end column of the given token"""
23 """Adjust start/end column of the given token"""
24 return t._replace(
24 return t._replace(
25 start=(t.start[0], t.start[1] + ofs), end=(t.end[0], t.end[1] + ofs)
25 start=(t.start[0], t.start[1] + ofs), end=(t.end[0], t.end[1] + ofs)
26 )
26 )
27
27
28
28
29 def replacetokens(tokens, opts):
29 def replacetokens(tokens, opts):
30 """Transform a stream of tokens from raw to Python 3.
30 """Transform a stream of tokens from raw to Python 3.
31
31
32 Returns a generator of possibly rewritten tokens.
32 Returns a generator of possibly rewritten tokens.
33
33
34 The input token list may be mutated as part of processing. However,
34 The input token list may be mutated as part of processing. However,
35 its changes do not necessarily match the output token stream.
35 its changes do not necessarily match the output token stream.
36 """
36 """
37 sysstrtokens = set()
37 sysstrtokens = set()
38
38
39 # The following utility functions access the tokens list and i index of
39 # The following utility functions access the tokens list and i index of
40 # the for i, t enumerate(tokens) loop below
40 # the for i, t enumerate(tokens) loop below
41 def _isop(j, *o):
41 def _isop(j, *o):
42 """Assert that tokens[j] is an OP with one of the given values"""
42 """Assert that tokens[j] is an OP with one of the given values"""
43 try:
43 try:
44 return tokens[j].type == token.OP and tokens[j].string in o
44 return tokens[j].type == token.OP and tokens[j].string in o
45 except IndexError:
45 except IndexError:
46 return False
46 return False
47
47
48 def _findargnofcall(n):
48 def _findargnofcall(n):
49 """Find arg n of a call expression (start at 0)
49 """Find arg n of a call expression (start at 0)
50
50
51 Returns index of the first token of that argument, or None if
51 Returns index of the first token of that argument, or None if
52 there is not that many arguments.
52 there is not that many arguments.
53
53
54 Assumes that token[i + 1] is '('.
54 Assumes that token[i + 1] is '('.
55
55
56 """
56 """
57 nested = 0
57 nested = 0
58 for j in range(i + 2, len(tokens)):
58 for j in range(i + 2, len(tokens)):
59 if _isop(j, ')', ']', '}'):
59 if _isop(j, ')', ']', '}'):
60 # end of call, tuple, subscription or dict / set
60 # end of call, tuple, subscription or dict / set
61 nested -= 1
61 nested -= 1
62 if nested < 0:
62 if nested < 0:
63 return None
63 return None
64 elif n == 0:
64 elif n == 0:
65 # this is the starting position of arg
65 # this is the starting position of arg
66 return j
66 return j
67 elif _isop(j, '(', '[', '{'):
67 elif _isop(j, '(', '[', '{'):
68 nested += 1
68 nested += 1
69 elif _isop(j, ',') and nested == 0:
69 elif _isop(j, ',') and nested == 0:
70 n -= 1
70 n -= 1
71
71
72 return None
72 return None
73
73
74 def _ensuresysstr(j):
74 def _ensuresysstr(j):
75 """Make sure the token at j is a system string
75 """Make sure the token at j is a system string
76
76
77 Remember the given token so the string transformer won't add
77 Remember the given token so the string transformer won't add
78 the byte prefix.
78 the byte prefix.
79
79
80 Ignores tokens that are not strings. Assumes bounds checking has
80 Ignores tokens that are not strings. Assumes bounds checking has
81 already been done.
81 already been done.
82
82
83 """
83 """
84 k = j
84 k = j
85 currtoken = tokens[k]
85 currtoken = tokens[k]
86 while currtoken.type in (token.STRING, token.NEWLINE, tokenize.NL):
86 while currtoken.type in (token.STRING, token.NEWLINE, tokenize.NL):
87 k += 1
87 k += 1
88 if currtoken.type == token.STRING and currtoken.string.startswith(
88 if currtoken.type == token.STRING and currtoken.string.startswith(
89 ("'", '"')
89 ("'", '"')
90 ):
90 ):
91 sysstrtokens.add(currtoken)
91 sysstrtokens.add(currtoken)
92 try:
92 try:
93 currtoken = tokens[k]
93 currtoken = tokens[k]
94 except IndexError:
94 except IndexError:
95 break
95 break
96
96
97 def _isitemaccess(j):
97 def _isitemaccess(j):
98 """Assert the next tokens form an item access on `tokens[j]` and that
98 """Assert the next tokens form an item access on `tokens[j]` and that
99 `tokens[j]` is a name.
99 `tokens[j]` is a name.
100 """
100 """
101 try:
101 try:
102 return (
102 return (
103 tokens[j].type == token.NAME
103 tokens[j].type == token.NAME
104 and _isop(j + 1, '[')
104 and _isop(j + 1, '[')
105 and tokens[j + 2].type == token.STRING
105 and tokens[j + 2].type == token.STRING
106 and _isop(j + 3, ']')
106 and _isop(j + 3, ']')
107 )
107 )
108 except IndexError:
108 except IndexError:
109 return False
109 return False
110
110
111 def _ismethodcall(j, *methodnames):
111 def _ismethodcall(j, *methodnames):
112 """Assert the next tokens form a call to `methodname` with a string
112 """Assert the next tokens form a call to `methodname` with a string
113 as first argument on `tokens[j]` and that `tokens[j]` is a name.
113 as first argument on `tokens[j]` and that `tokens[j]` is a name.
114 """
114 """
115 try:
115 try:
116 return (
116 return (
117 tokens[j].type == token.NAME
117 tokens[j].type == token.NAME
118 and _isop(j + 1, '.')
118 and _isop(j + 1, '.')
119 and tokens[j + 2].type == token.NAME
119 and tokens[j + 2].type == token.NAME
120 and tokens[j + 2].string in methodnames
120 and tokens[j + 2].string in methodnames
121 and _isop(j + 3, '(')
121 and _isop(j + 3, '(')
122 and tokens[j + 4].type == token.STRING
122 and tokens[j + 4].type == token.STRING
123 )
123 )
124 except IndexError:
124 except IndexError:
125 return False
125 return False
126
126
127 coldelta = 0 # column increment for new opening parens
127 coldelta = 0 # column increment for new opening parens
128 coloffset = -1 # column offset for the current line (-1: TBD)
128 coloffset = -1 # column offset for the current line (-1: TBD)
129 parens = [(0, 0, 0, -1)] # stack of (line, end-column, column-offset, type)
129 parens = [(0, 0, 0, -1)] # stack of (line, end-column, column-offset, type)
130 ignorenextline = False # don't transform the next line
130 ignorenextline = False # don't transform the next line
131 insideignoreblock = False # don't transform until turned off
131 insideignoreblock = False # don't transform until turned off
132 for i, t in enumerate(tokens):
132 for i, t in enumerate(tokens):
133 # Compute the column offset for the current line, such that
133 # Compute the column offset for the current line, such that
134 # the current line will be aligned to the last opening paren
134 # the current line will be aligned to the last opening paren
135 # as before.
135 # as before.
136 if coloffset < 0:
136 if coloffset < 0:
137 lastparen = parens[-1]
137 lastparen = parens[-1]
138 if t.start[1] == lastparen[1]:
138 if t.start[1] == lastparen[1]:
139 coloffset = lastparen[2]
139 coloffset = lastparen[2]
140 elif t.start[1] + 1 == lastparen[1] and lastparen[3] not in (
140 elif t.start[1] + 1 == lastparen[1] and lastparen[3] not in (
141 token.NEWLINE,
141 token.NEWLINE,
142 tokenize.NL,
142 tokenize.NL,
143 ):
143 ):
144 # fix misaligned indent of s/util.Abort/error.Abort/
144 # fix misaligned indent of s/util.Abort/error.Abort/
145 coloffset = lastparen[2] + (lastparen[1] - t.start[1])
145 coloffset = lastparen[2] + (lastparen[1] - t.start[1])
146 else:
146 else:
147 coloffset = 0
147 coloffset = 0
148
148
149 # Reset per-line attributes at EOL.
149 # Reset per-line attributes at EOL.
150 if t.type in (token.NEWLINE, tokenize.NL):
150 if t.type in (token.NEWLINE, tokenize.NL):
151 yield adjusttokenpos(t, coloffset)
151 yield adjusttokenpos(t, coloffset)
152 coldelta = 0
152 coldelta = 0
153 coloffset = -1
153 coloffset = -1
154 if not insideignoreblock:
154 if not insideignoreblock:
155 ignorenextline = (
155 ignorenextline = (
156 tokens[i - 1].type == token.COMMENT
156 tokens[i - 1].type == token.COMMENT
157 and tokens[i - 1].string == "# no-py3-transform"
157 and tokens[i - 1].string == "# no-py3-transform"
158 )
158 )
159 continue
159 continue
160
160
161 if t.type == token.COMMENT:
161 if t.type == token.COMMENT:
162 if t.string == "# py3-transform: off":
162 if t.string == "# py3-transform: off":
163 insideignoreblock = True
163 insideignoreblock = True
164 if t.string == "# py3-transform: on":
164 if t.string == "# py3-transform: on":
165 insideignoreblock = False
165 insideignoreblock = False
166
166
167 if ignorenextline or insideignoreblock:
167 if ignorenextline or insideignoreblock:
168 yield adjusttokenpos(t, coloffset)
168 yield adjusttokenpos(t, coloffset)
169 continue
169 continue
170
170
171 # Remember the last paren position.
171 # Remember the last paren position.
172 if _isop(i, '(', '[', '{'):
172 if _isop(i, '(', '[', '{'):
173 parens.append(t.end + (coloffset + coldelta, tokens[i + 1].type))
173 parens.append(t.end + (coloffset + coldelta, tokens[i + 1].type))
174 elif _isop(i, ')', ']', '}'):
174 elif _isop(i, ')', ']', '}'):
175 parens.pop()
175 parens.pop()
176
176
177 # Convert most string literals to byte literals. String literals
177 # Convert most string literals to byte literals. String literals
178 # in Python 2 are bytes. String literals in Python 3 are unicode.
178 # in Python 2 are bytes. String literals in Python 3 are unicode.
179 # Most strings in Mercurial are bytes and unicode strings are rare.
179 # Most strings in Mercurial are bytes and unicode strings are rare.
180 # Rather than rewrite all string literals to use ``b''`` to indicate
180 # Rather than rewrite all string literals to use ``b''`` to indicate
181 # byte strings, we apply this token transformer to insert the ``b``
181 # byte strings, we apply this token transformer to insert the ``b``
182 # prefix nearly everywhere.
182 # prefix nearly everywhere.
183 if t.type == token.STRING and t not in sysstrtokens:
183 if t.type == token.STRING and t not in sysstrtokens:
184 s = t.string
184 s = t.string
185
185
186 # Preserve docstrings as string literals. This is inconsistent
186 # Preserve docstrings as string literals. This is inconsistent
187 # with regular unprefixed strings. However, the
187 # with regular unprefixed strings. However, the
188 # "from __future__" parsing (which allows a module docstring to
188 # "from __future__" parsing (which allows a module docstring to
189 # exist before it) doesn't properly handle the docstring if it
189 # exist before it) doesn't properly handle the docstring if it
190 # is b''' prefixed, leading to a SyntaxError. We leave all
190 # is b''' prefixed, leading to a SyntaxError. We leave all
191 # docstrings as unprefixed to avoid this. This means Mercurial
191 # docstrings as unprefixed to avoid this. This means Mercurial
192 # components touching docstrings need to handle unicode,
192 # components touching docstrings need to handle unicode,
193 # unfortunately.
193 # unfortunately.
194 if s[0:3] in ("'''", '"""'):
194 if s[0:3] in ("'''", '"""'):
195 # If it's assigned to something, it's not a docstring
195 # If it's assigned to something, it's not a docstring
196 if not _isop(i - 1, '='):
196 if not _isop(i - 1, '='):
197 yield adjusttokenpos(t, coloffset)
197 yield adjusttokenpos(t, coloffset)
198 continue
198 continue
199
199
200 # If the first character isn't a quote, it is likely a string
200 # If the first character isn't a quote, it is likely a string
201 # prefixing character (such as 'b', 'u', or 'r'. Ignore.
201 # prefixing character (such as 'b', 'u', or 'r'. Ignore.
202 if s[0] not in ("'", '"'):
202 if s[0] not in ("'", '"'):
203 yield adjusttokenpos(t, coloffset)
203 yield adjusttokenpos(t, coloffset)
204 continue
204 continue
205
205
206 # String literal. Prefix to make a b'' string.
206 # String literal. Prefix to make a b'' string.
207 yield adjusttokenpos(t._replace(string='b%s' % t.string), coloffset)
207 yield adjusttokenpos(t._replace(string='b%s' % t.string), coloffset)
208 coldelta += 1
208 coldelta += 1
209 continue
209 continue
210
210
211 # This looks like a function call.
211 # This looks like a function call.
212 if t.type == token.NAME and _isop(i + 1, '('):
212 if t.type == token.NAME and _isop(i + 1, '('):
213 fn = t.string
213 fn = t.string
214
214
215 # *attr() builtins don't accept byte strings to 2nd argument.
215 # *attr() builtins don't accept byte strings to 2nd argument.
216 if fn in (
216 if fn in (
217 'getattr',
217 'getattr',
218 'setattr',
218 'setattr',
219 'hasattr',
219 'hasattr',
220 'safehasattr',
220 'safehasattr',
221 'wrapfunction',
221 'wrapfunction',
222 'wrapclass',
222 'wrapclass',
223 'addattr',
223 'addattr',
224 ) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')):
224 ) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')):
225 arg1idx = _findargnofcall(1)
225 arg1idx = _findargnofcall(1)
226 if arg1idx is not None:
226 if arg1idx is not None:
227 _ensuresysstr(arg1idx)
227 _ensuresysstr(arg1idx)
228
228
229 # .encode() and .decode() on str/bytes/unicode don't accept
229 # .encode() and .decode() on str/bytes/unicode don't accept
230 # byte strings on Python 3.
230 # byte strings on Python 3.
231 elif fn in ('encode', 'decode') and _isop(i - 1, '.'):
231 elif fn in ('encode', 'decode') and _isop(i - 1, '.'):
232 for argn in range(2):
232 for argn in range(2):
233 argidx = _findargnofcall(argn)
233 argidx = _findargnofcall(argn)
234 if argidx is not None:
234 if argidx is not None:
235 _ensuresysstr(argidx)
235 _ensuresysstr(argidx)
236
236
237 # It changes iteritems/values to items/values as they are not
237 # It changes iteritems/values to items/values as they are not
238 # present in Python 3 world.
238 # present in Python 3 world.
239 elif opts['dictiter'] and fn in ('iteritems', 'itervalues'):
239 elif opts['dictiter'] and fn in ('iteritems', 'itervalues'):
240 yield adjusttokenpos(t._replace(string=fn[4:]), coloffset)
240 yield adjusttokenpos(t._replace(string=fn[4:]), coloffset)
241 continue
241 continue
242
242
243 if t.type == token.NAME and t.string in opts['treat-as-kwargs']:
243 if t.type == token.NAME and t.string in opts['treat-as-kwargs']:
244 if _isitemaccess(i):
244 if _isitemaccess(i):
245 _ensuresysstr(i + 2)
245 _ensuresysstr(i + 2)
246 if _ismethodcall(i, 'get', 'pop', 'setdefault', 'popitem'):
246 if _ismethodcall(i, 'get', 'pop', 'setdefault', 'popitem'):
247 _ensuresysstr(i + 4)
247 _ensuresysstr(i + 4)
248
248
249 # Looks like "if __name__ == '__main__'".
249 # Looks like "if __name__ == '__main__'".
250 if (
250 if (
251 t.type == token.NAME
251 t.type == token.NAME
252 and t.string == '__name__'
252 and t.string == '__name__'
253 and _isop(i + 1, '==')
253 and _isop(i + 1, '==')
254 ):
254 ):
255 _ensuresysstr(i + 2)
255 _ensuresysstr(i + 2)
256
256
257 # Emit unmodified token.
257 # Emit unmodified token.
258 yield adjusttokenpos(t, coloffset)
258 yield adjusttokenpos(t, coloffset)
259
259
260
260
261 def process(fin, fout, opts):
261 def process(fin, fout, opts):
262 tokens = tokenize.tokenize(fin.readline)
262 tokens = tokenize.tokenize(fin.readline)
263 tokens = replacetokens(list(tokens), opts)
263 tokens = replacetokens(list(tokens), opts)
264 fout.write(tokenize.untokenize(tokens))
264 fout.write(tokenize.untokenize(tokens))
265
265
266
266
267 def tryunlink(fname):
267 def tryunlink(fname):
268 try:
268 try:
269 os.unlink(fname)
269 os.unlink(fname)
270 except OSError as err:
270 except OSError as err:
271 if err.errno != errno.ENOENT:
271 if err.errno != errno.ENOENT:
272 raise
272 raise
273
273
274
274
275 @contextlib.contextmanager
275 @contextlib.contextmanager
276 def editinplace(fname):
276 def editinplace(fname):
277 n = os.path.basename(fname)
277 n = os.path.basename(fname)
278 d = os.path.dirname(fname)
278 d = os.path.dirname(fname)
279 fp = tempfile.NamedTemporaryFile(
279 fp = tempfile.NamedTemporaryFile(
280 prefix='.%s-' % n, suffix='~', dir=d, delete=False
280 prefix='.%s-' % n, suffix='~', dir=d, delete=False
281 )
281 )
282 try:
282 try:
283 yield fp
283 yield fp
284 fp.close()
284 fp.close()
285 if os.name == 'nt':
285 if os.name == 'nt':
286 tryunlink(fname)
286 tryunlink(fname)
287 os.rename(fp.name, fname)
287 os.rename(fp.name, fname)
288 finally:
288 finally:
289 fp.close()
289 fp.close()
290 tryunlink(fp.name)
290 tryunlink(fp.name)
291
291
292
292
293 def main():
293 def main():
294 ap = argparse.ArgumentParser()
294 ap = argparse.ArgumentParser()
295 ap.add_argument(
295 ap.add_argument(
296 '--version', action='version', version='Byteify strings 1.0'
296 '--version', action='version', version='Byteify strings 1.0'
297 )
297 )
298 ap.add_argument(
298 ap.add_argument(
299 '-i',
299 '-i',
300 '--inplace',
300 '--inplace',
301 action='store_true',
301 action='store_true',
302 default=False,
302 default=False,
303 help='edit files in place',
303 help='edit files in place',
304 )
304 )
305 ap.add_argument(
305 ap.add_argument(
306 '--dictiter',
306 '--dictiter',
307 action='store_true',
307 action='store_true',
308 default=False,
308 default=False,
309 help='rewrite iteritems() and itervalues()',
309 help='rewrite iteritems() and itervalues()',
310 ),
310 ),
311 ap.add_argument(
311 ap.add_argument(
312 '--allow-attr-methods',
312 '--allow-attr-methods',
313 action='store_true',
313 action='store_true',
314 default=False,
314 default=False,
315 help='also handle attr*() when they are methods',
315 help='also handle attr*() when they are methods',
316 ),
316 ),
317 ap.add_argument(
317 ap.add_argument(
318 '--treat-as-kwargs',
318 '--treat-as-kwargs',
319 nargs="+",
319 nargs="+",
320 default=[],
320 default=[],
321 help="ignore kwargs-like objects",
321 help="ignore kwargs-like objects",
322 ),
322 ),
323 ap.add_argument('files', metavar='FILE', nargs='+', help='source file')
323 ap.add_argument('files', metavar='FILE', nargs='+', help='source file')
324 args = ap.parse_args()
324 args = ap.parse_args()
325 opts = {
325 opts = {
326 'dictiter': args.dictiter,
326 'dictiter': args.dictiter,
327 'treat-as-kwargs': set(args.treat_as_kwargs),
327 'treat-as-kwargs': set(args.treat_as_kwargs),
328 'allow-attr-methods': args.allow_attr_methods,
328 'allow-attr-methods': args.allow_attr_methods,
329 }
329 }
330 for fname in args.files:
330 for fname in args.files:
331 fname = os.path.realpath(fname)
331 if args.inplace:
332 if args.inplace:
332 with editinplace(fname) as fout:
333 with editinplace(fname) as fout:
333 with open(fname, 'rb') as fin:
334 with open(fname, 'rb') as fin:
334 process(fin, fout, opts)
335 process(fin, fout, opts)
335 else:
336 else:
336 with open(fname, 'rb') as fin:
337 with open(fname, 'rb') as fin:
337 fout = sys.stdout.buffer
338 fout = sys.stdout.buffer
338 process(fin, fout, opts)
339 process(fin, fout, opts)
339
340
340
341
341 if __name__ == '__main__':
342 if __name__ == '__main__':
342 if sys.version_info[0:2] < (3, 7):
343 if sys.version_info[0:2] < (3, 7):
343 print('This script must be run under Python 3.7+')
344 print('This script must be run under Python 3.7+')
344 sys.exit(3)
345 sys.exit(3)
345 main()
346 main()
@@ -1,82 +1,81 b''
1 image: octobus/ci-mercurial-core
1 image: octobus/ci-mercurial-core
2
2
3 # The runner made a clone as root.
3 # The runner made a clone as root.
4 # We make a new clone owned by user used to run the step.
4 # We make a new clone owned by user used to run the step.
5 before_script:
5 before_script:
6 - hg clone . /tmp/mercurial-ci/ --noupdate
6 - hg clone . /tmp/mercurial-ci/ --noupdate
7 - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
7 - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
8 - cd /tmp/mercurial-ci/
8 - cd /tmp/mercurial-ci/
9 - (cd tests; ls -1 test-check-*.*) > /tmp/check-tests.txt
9 - ls -1 tests/test-check-*.* > /tmp/check-tests.txt
10
10
11 variables:
11 variables:
12 PYTHON: python
12 PYTHON: python
13 TEST_HGMODULEPOLICY: "allow"
13 TEST_HGMODULEPOLICY: "allow"
14
14
15 .runtests_template: &runtests
15 .runtests_template: &runtests
16 script:
16 script:
17 - cd tests/
18 - echo "python used, $PYTHON"
17 - echo "python used, $PYTHON"
19 - echo "$RUNTEST_ARGS"
18 - echo "$RUNTEST_ARGS"
20 - HGMODULEPOLICY="$TEST_HGMODULEPOLICY" "$PYTHON" run-tests.py --color=always $RUNTEST_ARGS
19 - HGMODULEPOLICY="$TEST_HGMODULEPOLICY" "$PYTHON" tests/run-tests.py --color=always $RUNTEST_ARGS
21
20
22 checks-py2:
21 checks-py2:
23 <<: *runtests
22 <<: *runtests
24 variables:
23 variables:
25 RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
24 RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
26
25
27 checks-py3:
26 checks-py3:
28 <<: *runtests
27 <<: *runtests
29 variables:
28 variables:
30 RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
29 RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
31 PYTHON: python3
30 PYTHON: python3
32
31
33 rust-cargo-test-py2: &rust_cargo_test
32 rust-cargo-test-py2: &rust_cargo_test
34 script:
33 script:
35 - echo "python used, $PYTHON"
34 - echo "python used, $PYTHON"
36 - make rust-tests
35 - make rust-tests
37
36
38 rust-cargo-test-py3:
37 rust-cargo-test-py3:
39 <<: *rust_cargo_test
38 <<: *rust_cargo_test
40 variables:
39 variables:
41 PYTHON: python3
40 PYTHON: python3
42
41
43 test-py2:
42 test-py2:
44 <<: *runtests
43 <<: *runtests
45 variables:
44 variables:
46 RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
45 RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
47 TEST_HGMODULEPOLICY: "c"
46 TEST_HGMODULEPOLICY: "c"
48
47
49 test-py3:
48 test-py3:
50 <<: *runtests
49 <<: *runtests
51 variables:
50 variables:
52 RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
51 RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
53 PYTHON: python3
52 PYTHON: python3
54 TEST_HGMODULEPOLICY: "c"
53 TEST_HGMODULEPOLICY: "c"
55
54
56 test-py2-pure:
55 test-py2-pure:
57 <<: *runtests
56 <<: *runtests
58 variables:
57 variables:
59 RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
58 RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
60 TEST_HGMODULEPOLICY: "py"
59 TEST_HGMODULEPOLICY: "py"
61
60
62 test-py3-pure:
61 test-py3-pure:
63 <<: *runtests
62 <<: *runtests
64 variables:
63 variables:
65 RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
64 RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
66 PYTHON: python3
65 PYTHON: python3
67 TEST_HGMODULEPOLICY: "py"
66 TEST_HGMODULEPOLICY: "py"
68
67
69 test-py2-rust:
68 test-py2-rust:
70 <<: *runtests
69 <<: *runtests
71 variables:
70 variables:
72 HGWITHRUSTEXT: cpython
71 HGWITHRUSTEXT: cpython
73 RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
72 RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
74 TEST_HGMODULEPOLICY: "rust+c"
73 TEST_HGMODULEPOLICY: "rust+c"
75
74
76 test-py3-rust:
75 test-py3-rust:
77 <<: *runtests
76 <<: *runtests
78 variables:
77 variables:
79 HGWITHRUSTEXT: cpython
78 HGWITHRUSTEXT: cpython
80 RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
79 RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
81 PYTHON: python3
80 PYTHON: python3
82 TEST_HGMODULEPOLICY: "rust+c"
81 TEST_HGMODULEPOLICY: "rust+c"
@@ -1,386 +1,390 b''
1 # archival.py - revision archival for mercurial
1 # archival.py - revision archival for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import gzip
10 import gzip
11 import os
11 import os
12 import struct
12 import struct
13 import tarfile
13 import tarfile
14 import time
14 import time
15 import zipfile
15 import zipfile
16 import zlib
16 import zlib
17
17
18 from .i18n import _
18 from .i18n import _
19 from .node import nullrev
19 from .node import nullrev
20 from .pycompat import open
20 from .pycompat import open
21
21
22 from . import (
22 from . import (
23 error,
23 error,
24 formatter,
24 formatter,
25 match as matchmod,
25 match as matchmod,
26 pycompat,
26 pycompat,
27 scmutil,
27 scmutil,
28 util,
28 util,
29 vfs as vfsmod,
29 vfs as vfsmod,
30 )
30 )
31
31
32 stringio = util.stringio
32 stringio = util.stringio
33
33
34 # from unzip source code:
34 # from unzip source code:
35 _UNX_IFREG = 0x8000
35 _UNX_IFREG = 0x8000
36 _UNX_IFLNK = 0xA000
36 _UNX_IFLNK = 0xA000
37
37
38
38
39 def tidyprefix(dest, kind, prefix):
39 def tidyprefix(dest, kind, prefix):
40 '''choose prefix to use for names in archive. make sure prefix is
40 '''choose prefix to use for names in archive. make sure prefix is
41 safe for consumers.'''
41 safe for consumers.'''
42
42
43 if prefix:
43 if prefix:
44 prefix = util.normpath(prefix)
44 prefix = util.normpath(prefix)
45 else:
45 else:
46 if not isinstance(dest, bytes):
46 if not isinstance(dest, bytes):
47 raise ValueError(b'dest must be string if no prefix')
47 raise ValueError(b'dest must be string if no prefix')
48 prefix = os.path.basename(dest)
48 prefix = os.path.basename(dest)
49 lower = prefix.lower()
49 lower = prefix.lower()
50 for sfx in exts.get(kind, []):
50 for sfx in exts.get(kind, []):
51 if lower.endswith(sfx):
51 if lower.endswith(sfx):
52 prefix = prefix[: -len(sfx)]
52 prefix = prefix[: -len(sfx)]
53 break
53 break
54 lpfx = os.path.normpath(util.localpath(prefix))
54 lpfx = os.path.normpath(util.localpath(prefix))
55 prefix = util.pconvert(lpfx)
55 prefix = util.pconvert(lpfx)
56 if not prefix.endswith(b'/'):
56 if not prefix.endswith(b'/'):
57 prefix += b'/'
57 prefix += b'/'
58 # Drop the leading '.' path component if present, so Windows can read the
58 # Drop the leading '.' path component if present, so Windows can read the
59 # zip files (issue4634)
59 # zip files (issue4634)
60 if prefix.startswith(b'./'):
60 if prefix.startswith(b'./'):
61 prefix = prefix[2:]
61 prefix = prefix[2:]
62 if prefix.startswith(b'../') or os.path.isabs(lpfx) or b'/../' in prefix:
62 if prefix.startswith(b'../') or os.path.isabs(lpfx) or b'/../' in prefix:
63 raise error.Abort(_(b'archive prefix contains illegal components'))
63 raise error.Abort(_(b'archive prefix contains illegal components'))
64 return prefix
64 return prefix
65
65
66
66
67 exts = {
67 exts = {
68 b'tar': [b'.tar'],
68 b'tar': [b'.tar'],
69 b'tbz2': [b'.tbz2', b'.tar.bz2'],
69 b'tbz2': [b'.tbz2', b'.tar.bz2'],
70 b'tgz': [b'.tgz', b'.tar.gz'],
70 b'tgz': [b'.tgz', b'.tar.gz'],
71 b'zip': [b'.zip'],
71 b'zip': [b'.zip'],
72 b'txz': [b'.txz', b'.tar.xz'],
72 b'txz': [b'.txz', b'.tar.xz'],
73 }
73 }
74
74
75
75
76 def guesskind(dest):
76 def guesskind(dest):
77 for kind, extensions in pycompat.iteritems(exts):
77 for kind, extensions in pycompat.iteritems(exts):
78 if any(dest.endswith(ext) for ext in extensions):
78 if any(dest.endswith(ext) for ext in extensions):
79 return kind
79 return kind
80 return None
80 return None
81
81
82
82
83 def _rootctx(repo):
83 def _rootctx(repo):
84 # repo[0] may be hidden
84 # repo[0] may be hidden
85 for rev in repo:
85 for rev in repo:
86 return repo[rev]
86 return repo[rev]
87 return repo[nullrev]
87 return repo[nullrev]
88
88
89
89
90 # {tags} on ctx includes local tags and 'tip', with no current way to limit
90 # {tags} on ctx includes local tags and 'tip', with no current way to limit
91 # that to global tags. Therefore, use {latesttag} as a substitute when
91 # that to global tags. Therefore, use {latesttag} as a substitute when
92 # the distance is 0, since that will be the list of global tags on ctx.
92 # the distance is 0, since that will be the list of global tags on ctx.
93 _defaultmetatemplate = br'''
93 _defaultmetatemplate = br'''
94 repo: {root}
94 repo: {root}
95 node: {ifcontains(rev, revset("wdir()"), "{p1node}{dirty}", "{node}")}
95 node: {ifcontains(rev, revset("wdir()"), "{p1node}{dirty}", "{node}")}
96 branch: {branch|utf8}
96 branch: {branch|utf8}
97 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
97 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
98 separate("\n",
98 separate("\n",
99 join(latesttag % "latesttag: {tag}", "\n"),
99 join(latesttag % "latesttag: {tag}", "\n"),
100 "latesttagdistance: {latesttagdistance}",
100 "latesttagdistance: {latesttagdistance}",
101 "changessincelatesttag: {changessincelatesttag}"))}
101 "changessincelatesttag: {changessincelatesttag}"))}
102 '''[
102 '''[
103 1:
103 1:
104 ] # drop leading '\n'
104 ] # drop leading '\n'
105
105
106
106
107 def buildmetadata(ctx):
107 def buildmetadata(ctx):
108 '''build content of .hg_archival.txt'''
108 '''build content of .hg_archival.txt'''
109 repo = ctx.repo()
109 repo = ctx.repo()
110
110
111 opts = {
111 opts = {
112 b'template': repo.ui.config(
112 b'template': repo.ui.config(
113 b'experimental', b'archivemetatemplate', _defaultmetatemplate
113 b'experimental', b'archivemetatemplate', _defaultmetatemplate
114 )
114 )
115 }
115 }
116
116
117 out = util.stringio()
117 out = util.stringio()
118
118
119 fm = formatter.formatter(repo.ui, out, b'archive', opts)
119 fm = formatter.formatter(repo.ui, out, b'archive', opts)
120 fm.startitem()
120 fm.startitem()
121 fm.context(ctx=ctx)
121 fm.context(ctx=ctx)
122 fm.data(root=_rootctx(repo).hex())
122 fm.data(root=_rootctx(repo).hex())
123
123
124 if ctx.rev() is None:
124 if ctx.rev() is None:
125 dirty = b''
125 dirty = b''
126 if ctx.dirty(missing=True):
126 if ctx.dirty(missing=True):
127 dirty = b'+'
127 dirty = b'+'
128 fm.data(dirty=dirty)
128 fm.data(dirty=dirty)
129 fm.end()
129 fm.end()
130
130
131 return out.getvalue()
131 return out.getvalue()
132
132
133
133
134 class tarit(object):
134 class tarit(object):
135 '''write archive to tar file or stream. can write uncompressed,
135 '''write archive to tar file or stream. can write uncompressed,
136 or compress with gzip or bzip2.'''
136 or compress with gzip or bzip2.'''
137
137
138 class GzipFileWithTime(gzip.GzipFile):
138 if pycompat.ispy3:
139 def __init__(self, *args, **kw):
139 GzipFileWithTime = gzip.GzipFile # camelcase-required
140 timestamp = None
140 else:
141 if 'timestamp' in kw:
141
142 timestamp = kw.pop('timestamp')
142 class GzipFileWithTime(gzip.GzipFile):
143 if timestamp is None:
143 def __init__(self, *args, **kw):
144 self.timestamp = time.time()
144 timestamp = None
145 else:
145 if 'mtime' in kw:
146 self.timestamp = timestamp
146 timestamp = kw.pop('mtime')
147 gzip.GzipFile.__init__(self, *args, **kw)
147 if timestamp is None:
148 self.timestamp = time.time()
149 else:
150 self.timestamp = timestamp
151 gzip.GzipFile.__init__(self, *args, **kw)
148
152
149 def _write_gzip_header(self):
153 def _write_gzip_header(self):
150 self.fileobj.write(b'\037\213') # magic header
154 self.fileobj.write(b'\037\213') # magic header
151 self.fileobj.write(b'\010') # compression method
155 self.fileobj.write(b'\010') # compression method
152 fname = self.name
156 fname = self.name
153 if fname and fname.endswith(b'.gz'):
157 if fname and fname.endswith(b'.gz'):
154 fname = fname[:-3]
158 fname = fname[:-3]
155 flags = 0
159 flags = 0
156 if fname:
160 if fname:
157 flags = gzip.FNAME # pytype: disable=module-attr
161 flags = gzip.FNAME # pytype: disable=module-attr
158 self.fileobj.write(pycompat.bytechr(flags))
162 self.fileobj.write(pycompat.bytechr(flags))
159 gzip.write32u( # pytype: disable=module-attr
163 gzip.write32u( # pytype: disable=module-attr
160 self.fileobj, int(self.timestamp)
164 self.fileobj, int(self.timestamp)
161 )
165 )
162 self.fileobj.write(b'\002')
166 self.fileobj.write(b'\002')
163 self.fileobj.write(b'\377')
167 self.fileobj.write(b'\377')
164 if fname:
168 if fname:
165 self.fileobj.write(fname + b'\000')
169 self.fileobj.write(fname + b'\000')
166
170
167 def __init__(self, dest, mtime, kind=b''):
171 def __init__(self, dest, mtime, kind=b''):
168 self.mtime = mtime
172 self.mtime = mtime
169 self.fileobj = None
173 self.fileobj = None
170
174
171 def taropen(mode, name=b'', fileobj=None):
175 def taropen(mode, name=b'', fileobj=None):
172 if kind == b'gz':
176 if kind == b'gz':
173 mode = mode[0:1]
177 mode = mode[0:1]
174 if not fileobj:
178 if not fileobj:
175 fileobj = open(name, mode + b'b')
179 fileobj = open(name, mode + b'b')
176 gzfileobj = self.GzipFileWithTime(
180 gzfileobj = self.GzipFileWithTime(
177 name,
181 name,
178 pycompat.sysstr(mode + b'b'),
182 pycompat.sysstr(mode + b'b'),
179 zlib.Z_BEST_COMPRESSION,
183 zlib.Z_BEST_COMPRESSION,
180 fileobj,
184 fileobj,
181 timestamp=mtime,
185 mtime=mtime,
182 )
186 )
183 self.fileobj = gzfileobj
187 self.fileobj = gzfileobj
184 return tarfile.TarFile.taropen( # pytype: disable=attribute-error
188 return tarfile.TarFile.taropen( # pytype: disable=attribute-error
185 name, pycompat.sysstr(mode), gzfileobj
189 name, pycompat.sysstr(mode), gzfileobj
186 )
190 )
187 else:
191 else:
188 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj)
192 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj)
189
193
190 if isinstance(dest, bytes):
194 if isinstance(dest, bytes):
191 self.z = taropen(b'w:', name=dest)
195 self.z = taropen(b'w:', name=dest)
192 else:
196 else:
193 self.z = taropen(b'w|', fileobj=dest)
197 self.z = taropen(b'w|', fileobj=dest)
194
198
195 def addfile(self, name, mode, islink, data):
199 def addfile(self, name, mode, islink, data):
196 name = pycompat.fsdecode(name)
200 name = pycompat.fsdecode(name)
197 i = tarfile.TarInfo(name)
201 i = tarfile.TarInfo(name)
198 i.mtime = self.mtime
202 i.mtime = self.mtime
199 i.size = len(data)
203 i.size = len(data)
200 if islink:
204 if islink:
201 i.type = tarfile.SYMTYPE
205 i.type = tarfile.SYMTYPE
202 i.mode = 0o777
206 i.mode = 0o777
203 i.linkname = pycompat.fsdecode(data)
207 i.linkname = pycompat.fsdecode(data)
204 data = None
208 data = None
205 i.size = 0
209 i.size = 0
206 else:
210 else:
207 i.mode = mode
211 i.mode = mode
208 data = stringio(data)
212 data = stringio(data)
209 self.z.addfile(i, data)
213 self.z.addfile(i, data)
210
214
211 def done(self):
215 def done(self):
212 self.z.close()
216 self.z.close()
213 if self.fileobj:
217 if self.fileobj:
214 self.fileobj.close()
218 self.fileobj.close()
215
219
216
220
217 class zipit(object):
221 class zipit(object):
218 '''write archive to zip file or stream. can write uncompressed,
222 '''write archive to zip file or stream. can write uncompressed,
219 or compressed with deflate.'''
223 or compressed with deflate.'''
220
224
221 def __init__(self, dest, mtime, compress=True):
225 def __init__(self, dest, mtime, compress=True):
222 if isinstance(dest, bytes):
226 if isinstance(dest, bytes):
223 dest = pycompat.fsdecode(dest)
227 dest = pycompat.fsdecode(dest)
224 self.z = zipfile.ZipFile(
228 self.z = zipfile.ZipFile(
225 dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED
229 dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED
226 )
230 )
227
231
228 # Python's zipfile module emits deprecation warnings if we try
232 # Python's zipfile module emits deprecation warnings if we try
229 # to store files with a date before 1980.
233 # to store files with a date before 1980.
230 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
234 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
231 if mtime < epoch:
235 if mtime < epoch:
232 mtime = epoch
236 mtime = epoch
233
237
234 self.mtime = mtime
238 self.mtime = mtime
235 self.date_time = time.gmtime(mtime)[:6]
239 self.date_time = time.gmtime(mtime)[:6]
236
240
237 def addfile(self, name, mode, islink, data):
241 def addfile(self, name, mode, islink, data):
238 i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time)
242 i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time)
239 i.compress_type = self.z.compression # pytype: disable=attribute-error
243 i.compress_type = self.z.compression # pytype: disable=attribute-error
240 # unzip will not honor unix file modes unless file creator is
244 # unzip will not honor unix file modes unless file creator is
241 # set to unix (id 3).
245 # set to unix (id 3).
242 i.create_system = 3
246 i.create_system = 3
243 ftype = _UNX_IFREG
247 ftype = _UNX_IFREG
244 if islink:
248 if islink:
245 mode = 0o777
249 mode = 0o777
246 ftype = _UNX_IFLNK
250 ftype = _UNX_IFLNK
247 i.external_attr = (mode | ftype) << 16
251 i.external_attr = (mode | ftype) << 16
248 # add "extended-timestamp" extra block, because zip archives
252 # add "extended-timestamp" extra block, because zip archives
249 # without this will be extracted with unexpected timestamp,
253 # without this will be extracted with unexpected timestamp,
250 # if TZ is not configured as GMT
254 # if TZ is not configured as GMT
251 i.extra += struct.pack(
255 i.extra += struct.pack(
252 b'<hhBl',
256 b'<hhBl',
253 0x5455, # block type: "extended-timestamp"
257 0x5455, # block type: "extended-timestamp"
254 1 + 4, # size of this block
258 1 + 4, # size of this block
255 1, # "modification time is present"
259 1, # "modification time is present"
256 int(self.mtime),
260 int(self.mtime),
257 ) # last modification (UTC)
261 ) # last modification (UTC)
258 self.z.writestr(i, data)
262 self.z.writestr(i, data)
259
263
260 def done(self):
264 def done(self):
261 self.z.close()
265 self.z.close()
262
266
263
267
264 class fileit(object):
268 class fileit(object):
265 '''write archive as files in directory.'''
269 '''write archive as files in directory.'''
266
270
267 def __init__(self, name, mtime):
271 def __init__(self, name, mtime):
268 self.basedir = name
272 self.basedir = name
269 self.opener = vfsmod.vfs(self.basedir)
273 self.opener = vfsmod.vfs(self.basedir)
270 self.mtime = mtime
274 self.mtime = mtime
271
275
272 def addfile(self, name, mode, islink, data):
276 def addfile(self, name, mode, islink, data):
273 if islink:
277 if islink:
274 self.opener.symlink(data, name)
278 self.opener.symlink(data, name)
275 return
279 return
276 f = self.opener(name, b"w", atomictemp=False)
280 f = self.opener(name, b"w", atomictemp=False)
277 f.write(data)
281 f.write(data)
278 f.close()
282 f.close()
279 destfile = os.path.join(self.basedir, name)
283 destfile = os.path.join(self.basedir, name)
280 os.chmod(destfile, mode)
284 os.chmod(destfile, mode)
281 if self.mtime is not None:
285 if self.mtime is not None:
282 os.utime(destfile, (self.mtime, self.mtime))
286 os.utime(destfile, (self.mtime, self.mtime))
283
287
284 def done(self):
288 def done(self):
285 pass
289 pass
286
290
287
291
288 archivers = {
292 archivers = {
289 b'files': fileit,
293 b'files': fileit,
290 b'tar': tarit,
294 b'tar': tarit,
291 b'tbz2': lambda name, mtime: tarit(name, mtime, b'bz2'),
295 b'tbz2': lambda name, mtime: tarit(name, mtime, b'bz2'),
292 b'tgz': lambda name, mtime: tarit(name, mtime, b'gz'),
296 b'tgz': lambda name, mtime: tarit(name, mtime, b'gz'),
293 b'txz': lambda name, mtime: tarit(name, mtime, b'xz'),
297 b'txz': lambda name, mtime: tarit(name, mtime, b'xz'),
294 b'uzip': lambda name, mtime: zipit(name, mtime, False),
298 b'uzip': lambda name, mtime: zipit(name, mtime, False),
295 b'zip': zipit,
299 b'zip': zipit,
296 }
300 }
297
301
298
302
299 def archive(
303 def archive(
300 repo,
304 repo,
301 dest,
305 dest,
302 node,
306 node,
303 kind,
307 kind,
304 decode=True,
308 decode=True,
305 match=None,
309 match=None,
306 prefix=b'',
310 prefix=b'',
307 mtime=None,
311 mtime=None,
308 subrepos=False,
312 subrepos=False,
309 ):
313 ):
310 '''create archive of repo as it was at node.
314 '''create archive of repo as it was at node.
311
315
312 dest can be name of directory, name of archive file, or file
316 dest can be name of directory, name of archive file, or file
313 object to write archive to.
317 object to write archive to.
314
318
315 kind is type of archive to create.
319 kind is type of archive to create.
316
320
317 decode tells whether to put files through decode filters from
321 decode tells whether to put files through decode filters from
318 hgrc.
322 hgrc.
319
323
320 match is a matcher to filter names of files to write to archive.
324 match is a matcher to filter names of files to write to archive.
321
325
322 prefix is name of path to put before every archive member.
326 prefix is name of path to put before every archive member.
323
327
324 mtime is the modified time, in seconds, or None to use the changeset time.
328 mtime is the modified time, in seconds, or None to use the changeset time.
325
329
326 subrepos tells whether to include subrepos.
330 subrepos tells whether to include subrepos.
327 '''
331 '''
328
332
329 if kind == b'txz' and not pycompat.ispy3:
333 if kind == b'txz' and not pycompat.ispy3:
330 raise error.Abort(_(b'xz compression is only available in Python 3'))
334 raise error.Abort(_(b'xz compression is only available in Python 3'))
331
335
332 if kind == b'files':
336 if kind == b'files':
333 if prefix:
337 if prefix:
334 raise error.Abort(_(b'cannot give prefix when archiving to files'))
338 raise error.Abort(_(b'cannot give prefix when archiving to files'))
335 else:
339 else:
336 prefix = tidyprefix(dest, kind, prefix)
340 prefix = tidyprefix(dest, kind, prefix)
337
341
338 def write(name, mode, islink, getdata):
342 def write(name, mode, islink, getdata):
339 data = getdata()
343 data = getdata()
340 if decode:
344 if decode:
341 data = repo.wwritedata(name, data)
345 data = repo.wwritedata(name, data)
342 archiver.addfile(prefix + name, mode, islink, data)
346 archiver.addfile(prefix + name, mode, islink, data)
343
347
344 if kind not in archivers:
348 if kind not in archivers:
345 raise error.Abort(_(b"unknown archive type '%s'") % kind)
349 raise error.Abort(_(b"unknown archive type '%s'") % kind)
346
350
347 ctx = repo[node]
351 ctx = repo[node]
348 archiver = archivers[kind](dest, mtime or ctx.date()[0])
352 archiver = archivers[kind](dest, mtime or ctx.date()[0])
349
353
350 if not match:
354 if not match:
351 match = scmutil.matchall(repo)
355 match = scmutil.matchall(repo)
352
356
353 if repo.ui.configbool(b"ui", b"archivemeta"):
357 if repo.ui.configbool(b"ui", b"archivemeta"):
354 name = b'.hg_archival.txt'
358 name = b'.hg_archival.txt'
355 if match(name):
359 if match(name):
356 write(name, 0o644, False, lambda: buildmetadata(ctx))
360 write(name, 0o644, False, lambda: buildmetadata(ctx))
357
361
358 files = list(ctx.manifest().walk(match))
362 files = list(ctx.manifest().walk(match))
359 total = len(files)
363 total = len(files)
360 if total:
364 if total:
361 files.sort()
365 files.sort()
362 scmutil.prefetchfiles(
366 scmutil.prefetchfiles(
363 repo, [ctx.rev()], scmutil.matchfiles(repo, files)
367 repo, [ctx.rev()], scmutil.matchfiles(repo, files)
364 )
368 )
365 progress = repo.ui.makeprogress(
369 progress = repo.ui.makeprogress(
366 _(b'archiving'), unit=_(b'files'), total=total
370 _(b'archiving'), unit=_(b'files'), total=total
367 )
371 )
368 progress.update(0)
372 progress.update(0)
369 for f in files:
373 for f in files:
370 ff = ctx.flags(f)
374 ff = ctx.flags(f)
371 write(f, b'x' in ff and 0o755 or 0o644, b'l' in ff, ctx[f].data)
375 write(f, b'x' in ff and 0o755 or 0o644, b'l' in ff, ctx[f].data)
372 progress.increment(item=f)
376 progress.increment(item=f)
373 progress.complete()
377 progress.complete()
374
378
375 if subrepos:
379 if subrepos:
376 for subpath in sorted(ctx.substate):
380 for subpath in sorted(ctx.substate):
377 sub = ctx.workingsub(subpath)
381 sub = ctx.workingsub(subpath)
378 submatch = matchmod.subdirmatcher(subpath, match)
382 submatch = matchmod.subdirmatcher(subpath, match)
379 subprefix = prefix + subpath + b'/'
383 subprefix = prefix + subpath + b'/'
380 total += sub.archive(archiver, subprefix, submatch, decode)
384 total += sub.archive(archiver, subprefix, submatch, decode)
381
385
382 if total == 0:
386 if total == 0:
383 raise error.Abort(_(b'no files match the archive pattern'))
387 raise error.Abort(_(b'no files match the archive pattern'))
384
388
385 archiver.done()
389 archiver.done()
386 return total
390 return total
@@ -1,2914 +1,2917 b''
1 /*
1 /*
2 parsers.c - efficient content parsing
2 parsers.c - efficient content parsing
3
3
4 Copyright 2008 Matt Mackall <mpm@selenic.com> and others
4 Copyright 2008 Matt Mackall <mpm@selenic.com> and others
5
5
6 This software may be used and distributed according to the terms of
6 This software may be used and distributed according to the terms of
7 the GNU General Public License, incorporated herein by reference.
7 the GNU General Public License, incorporated herein by reference.
8 */
8 */
9
9
10 #define PY_SSIZE_T_CLEAN
10 #define PY_SSIZE_T_CLEAN
11 #include <Python.h>
11 #include <Python.h>
12 #include <assert.h>
12 #include <assert.h>
13 #include <ctype.h>
13 #include <ctype.h>
14 #include <limits.h>
14 #include <limits.h>
15 #include <stddef.h>
15 #include <stddef.h>
16 #include <stdlib.h>
16 #include <stdlib.h>
17 #include <string.h>
17 #include <string.h>
18
18
19 #include "bitmanipulation.h"
19 #include "bitmanipulation.h"
20 #include "charencode.h"
20 #include "charencode.h"
21 #include "revlog.h"
21 #include "revlog.h"
22 #include "util.h"
22 #include "util.h"
23
23
24 #ifdef IS_PY3K
24 #ifdef IS_PY3K
25 /* The mapping of Python types is meant to be temporary to get Python
25 /* The mapping of Python types is meant to be temporary to get Python
26 * 3 to compile. We should remove this once Python 3 support is fully
26 * 3 to compile. We should remove this once Python 3 support is fully
27 * supported and proper types are used in the extensions themselves. */
27 * supported and proper types are used in the extensions themselves. */
28 #define PyInt_Check PyLong_Check
28 #define PyInt_Check PyLong_Check
29 #define PyInt_FromLong PyLong_FromLong
29 #define PyInt_FromLong PyLong_FromLong
30 #define PyInt_FromSsize_t PyLong_FromSsize_t
30 #define PyInt_FromSsize_t PyLong_FromSsize_t
31 #define PyInt_AsLong PyLong_AsLong
31 #define PyInt_AsLong PyLong_AsLong
32 #endif
32 #endif
33
33
34 typedef struct indexObjectStruct indexObject;
34 typedef struct indexObjectStruct indexObject;
35
35
36 typedef struct {
36 typedef struct {
37 int children[16];
37 int children[16];
38 } nodetreenode;
38 } nodetreenode;
39
39
40 typedef struct {
40 typedef struct {
41 int abi_version;
41 int abi_version;
42 Py_ssize_t (*index_length)(const indexObject *);
42 Py_ssize_t (*index_length)(const indexObject *);
43 const char *(*index_node)(indexObject *, Py_ssize_t);
43 const char *(*index_node)(indexObject *, Py_ssize_t);
44 int (*index_parents)(PyObject *, int, int *);
44 int (*index_parents)(PyObject *, int, int *);
45 } Revlog_CAPI;
45 } Revlog_CAPI;
46
46
47 /*
47 /*
48 * A base-16 trie for fast node->rev mapping.
48 * A base-16 trie for fast node->rev mapping.
49 *
49 *
50 * Positive value is index of the next node in the trie
50 * Positive value is index of the next node in the trie
51 * Negative value is a leaf: -(rev + 2)
51 * Negative value is a leaf: -(rev + 2)
52 * Zero is empty
52 * Zero is empty
53 */
53 */
54 typedef struct {
54 typedef struct {
55 indexObject *index;
55 indexObject *index;
56 nodetreenode *nodes;
56 nodetreenode *nodes;
57 unsigned length; /* # nodes in use */
57 unsigned length; /* # nodes in use */
58 unsigned capacity; /* # nodes allocated */
58 unsigned capacity; /* # nodes allocated */
59 int depth; /* maximum depth of tree */
59 int depth; /* maximum depth of tree */
60 int splits; /* # splits performed */
60 int splits; /* # splits performed */
61 } nodetree;
61 } nodetree;
62
62
63 typedef struct {
63 typedef struct {
64 PyObject_HEAD /* ; */
64 PyObject_HEAD /* ; */
65 nodetree nt;
65 nodetree nt;
66 } nodetreeObject;
66 } nodetreeObject;
67
67
68 /*
68 /*
69 * This class has two behaviors.
69 * This class has two behaviors.
70 *
70 *
71 * When used in a list-like way (with integer keys), we decode an
71 * When used in a list-like way (with integer keys), we decode an
72 * entry in a RevlogNG index file on demand. We have limited support for
72 * entry in a RevlogNG index file on demand. We have limited support for
73 * integer-keyed insert and delete, only at elements right before the
73 * integer-keyed insert and delete, only at elements right before the
74 * end.
74 * end.
75 *
75 *
76 * With string keys, we lazily perform a reverse mapping from node to
76 * With string keys, we lazily perform a reverse mapping from node to
77 * rev, using a base-16 trie.
77 * rev, using a base-16 trie.
78 */
78 */
79 struct indexObjectStruct {
79 struct indexObjectStruct {
80 PyObject_HEAD
80 PyObject_HEAD
81 /* Type-specific fields go here. */
81 /* Type-specific fields go here. */
82 PyObject *data; /* raw bytes of index */
82 PyObject *data; /* raw bytes of index */
83 Py_buffer buf; /* buffer of data */
83 Py_buffer buf; /* buffer of data */
84 PyObject **cache; /* cached tuples */
84 PyObject **cache; /* cached tuples */
85 const char **offsets; /* populated on demand */
85 const char **offsets; /* populated on demand */
86 Py_ssize_t raw_length; /* original number of elements */
86 Py_ssize_t raw_length; /* original number of elements */
87 Py_ssize_t length; /* current number of elements */
87 Py_ssize_t length; /* current number of elements */
88 PyObject *added; /* populated on demand */
88 PyObject *added; /* populated on demand */
89 PyObject *headrevs; /* cache, invalidated on changes */
89 PyObject *headrevs; /* cache, invalidated on changes */
90 PyObject *filteredrevs; /* filtered revs set */
90 PyObject *filteredrevs; /* filtered revs set */
91 nodetree nt; /* base-16 trie */
91 nodetree nt; /* base-16 trie */
92 int ntinitialized; /* 0 or 1 */
92 int ntinitialized; /* 0 or 1 */
93 int ntrev; /* last rev scanned */
93 int ntrev; /* last rev scanned */
94 int ntlookups; /* # lookups */
94 int ntlookups; /* # lookups */
95 int ntmisses; /* # lookups that miss the cache */
95 int ntmisses; /* # lookups that miss the cache */
96 int inlined;
96 int inlined;
97 };
97 };
98
98
99 static Py_ssize_t index_length(const indexObject *self)
99 static Py_ssize_t index_length(const indexObject *self)
100 {
100 {
101 if (self->added == NULL)
101 if (self->added == NULL)
102 return self->length;
102 return self->length;
103 return self->length + PyList_GET_SIZE(self->added);
103 return self->length + PyList_GET_SIZE(self->added);
104 }
104 }
105
105
106 static PyObject *nullentry = NULL;
106 static PyObject *nullentry = NULL;
107 static const char nullid[20] = {0};
107 static const char nullid[20] = {0};
108 static const Py_ssize_t nullrev = -1;
108 static const Py_ssize_t nullrev = -1;
109
109
110 static Py_ssize_t inline_scan(indexObject *self, const char **offsets);
110 static Py_ssize_t inline_scan(indexObject *self, const char **offsets);
111
111
112 #if LONG_MAX == 0x7fffffffL
112 #if LONG_MAX == 0x7fffffffL
113 static const char *const tuple_format = PY23("Kiiiiiis#", "Kiiiiiiy#");
113 static const char *const tuple_format = PY23("Kiiiiiis#", "Kiiiiiiy#");
114 #else
114 #else
115 static const char *const tuple_format = PY23("kiiiiiis#", "kiiiiiiy#");
115 static const char *const tuple_format = PY23("kiiiiiis#", "kiiiiiiy#");
116 #endif
116 #endif
117
117
118 /* A RevlogNG v1 index entry is 64 bytes long. */
118 /* A RevlogNG v1 index entry is 64 bytes long. */
119 static const long v1_hdrsize = 64;
119 static const long v1_hdrsize = 64;
120
120
121 static void raise_revlog_error(void)
121 static void raise_revlog_error(void)
122 {
122 {
123 PyObject *mod = NULL, *dict = NULL, *errclass = NULL;
123 PyObject *mod = NULL, *dict = NULL, *errclass = NULL;
124
124
125 mod = PyImport_ImportModule("mercurial.error");
125 mod = PyImport_ImportModule("mercurial.error");
126 if (mod == NULL) {
126 if (mod == NULL) {
127 goto cleanup;
127 goto cleanup;
128 }
128 }
129
129
130 dict = PyModule_GetDict(mod);
130 dict = PyModule_GetDict(mod);
131 if (dict == NULL) {
131 if (dict == NULL) {
132 goto cleanup;
132 goto cleanup;
133 }
133 }
134 Py_INCREF(dict);
134 Py_INCREF(dict);
135
135
136 errclass = PyDict_GetItemString(dict, "RevlogError");
136 errclass = PyDict_GetItemString(dict, "RevlogError");
137 if (errclass == NULL) {
137 if (errclass == NULL) {
138 PyErr_SetString(PyExc_SystemError,
138 PyErr_SetString(PyExc_SystemError,
139 "could not find RevlogError");
139 "could not find RevlogError");
140 goto cleanup;
140 goto cleanup;
141 }
141 }
142
142
143 /* value of exception is ignored by callers */
143 /* value of exception is ignored by callers */
144 PyErr_SetString(errclass, "RevlogError");
144 PyErr_SetString(errclass, "RevlogError");
145
145
146 cleanup:
146 cleanup:
147 Py_XDECREF(dict);
147 Py_XDECREF(dict);
148 Py_XDECREF(mod);
148 Py_XDECREF(mod);
149 }
149 }
150
150
151 /*
151 /*
152 * Return a pointer to the beginning of a RevlogNG record.
152 * Return a pointer to the beginning of a RevlogNG record.
153 */
153 */
154 static const char *index_deref(indexObject *self, Py_ssize_t pos)
154 static const char *index_deref(indexObject *self, Py_ssize_t pos)
155 {
155 {
156 if (self->inlined && pos > 0) {
156 if (self->inlined && pos > 0) {
157 if (self->offsets == NULL) {
157 if (self->offsets == NULL) {
158 self->offsets = PyMem_Malloc(self->raw_length *
158 self->offsets = PyMem_Malloc(self->raw_length *
159 sizeof(*self->offsets));
159 sizeof(*self->offsets));
160 if (self->offsets == NULL)
160 if (self->offsets == NULL)
161 return (const char *)PyErr_NoMemory();
161 return (const char *)PyErr_NoMemory();
162 inline_scan(self, self->offsets);
162 Py_ssize_t ret = inline_scan(self, self->offsets);
163 if (ret == -1) {
164 return NULL;
165 };
163 }
166 }
164 return self->offsets[pos];
167 return self->offsets[pos];
165 }
168 }
166
169
167 return (const char *)(self->buf.buf) + pos * v1_hdrsize;
170 return (const char *)(self->buf.buf) + pos * v1_hdrsize;
168 }
171 }
169
172
170 /*
173 /*
171 * Get parents of the given rev.
174 * Get parents of the given rev.
172 *
175 *
173 * The specified rev must be valid and must not be nullrev. A returned
176 * The specified rev must be valid and must not be nullrev. A returned
174 * parent revision may be nullrev, but is guaranteed to be in valid range.
177 * parent revision may be nullrev, but is guaranteed to be in valid range.
175 */
178 */
176 static inline int index_get_parents(indexObject *self, Py_ssize_t rev, int *ps,
179 static inline int index_get_parents(indexObject *self, Py_ssize_t rev, int *ps,
177 int maxrev)
180 int maxrev)
178 {
181 {
179 if (rev >= self->length) {
182 if (rev >= self->length) {
180 long tmp;
183 long tmp;
181 PyObject *tuple =
184 PyObject *tuple =
182 PyList_GET_ITEM(self->added, rev - self->length);
185 PyList_GET_ITEM(self->added, rev - self->length);
183 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 5), &tmp)) {
186 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 5), &tmp)) {
184 return -1;
187 return -1;
185 }
188 }
186 ps[0] = (int)tmp;
189 ps[0] = (int)tmp;
187 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 6), &tmp)) {
190 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 6), &tmp)) {
188 return -1;
191 return -1;
189 }
192 }
190 ps[1] = (int)tmp;
193 ps[1] = (int)tmp;
191 } else {
194 } else {
192 const char *data = index_deref(self, rev);
195 const char *data = index_deref(self, rev);
193 ps[0] = getbe32(data + 24);
196 ps[0] = getbe32(data + 24);
194 ps[1] = getbe32(data + 28);
197 ps[1] = getbe32(data + 28);
195 }
198 }
196 /* If index file is corrupted, ps[] may point to invalid revisions. So
199 /* If index file is corrupted, ps[] may point to invalid revisions. So
197 * there is a risk of buffer overflow to trust them unconditionally. */
200 * there is a risk of buffer overflow to trust them unconditionally. */
198 if (ps[0] < -1 || ps[0] > maxrev || ps[1] < -1 || ps[1] > maxrev) {
201 if (ps[0] < -1 || ps[0] > maxrev || ps[1] < -1 || ps[1] > maxrev) {
199 PyErr_SetString(PyExc_ValueError, "parent out of range");
202 PyErr_SetString(PyExc_ValueError, "parent out of range");
200 return -1;
203 return -1;
201 }
204 }
202 return 0;
205 return 0;
203 }
206 }
204
207
205 /*
208 /*
206 * Get parents of the given rev.
209 * Get parents of the given rev.
207 *
210 *
208 * If the specified rev is out of range, IndexError will be raised. If the
211 * If the specified rev is out of range, IndexError will be raised. If the
209 * revlog entry is corrupted, ValueError may be raised.
212 * revlog entry is corrupted, ValueError may be raised.
210 *
213 *
211 * Returns 0 on success or -1 on failure.
214 * Returns 0 on success or -1 on failure.
212 */
215 */
213 static int HgRevlogIndex_GetParents(PyObject *op, int rev, int *ps)
216 static int HgRevlogIndex_GetParents(PyObject *op, int rev, int *ps)
214 {
217 {
215 int tiprev;
218 int tiprev;
216 if (!op || !HgRevlogIndex_Check(op) || !ps) {
219 if (!op || !HgRevlogIndex_Check(op) || !ps) {
217 PyErr_BadInternalCall();
220 PyErr_BadInternalCall();
218 return -1;
221 return -1;
219 }
222 }
220 tiprev = (int)index_length((indexObject *)op) - 1;
223 tiprev = (int)index_length((indexObject *)op) - 1;
221 if (rev < -1 || rev > tiprev) {
224 if (rev < -1 || rev > tiprev) {
222 PyErr_Format(PyExc_IndexError, "rev out of range: %d", rev);
225 PyErr_Format(PyExc_IndexError, "rev out of range: %d", rev);
223 return -1;
226 return -1;
224 } else if (rev == -1) {
227 } else if (rev == -1) {
225 ps[0] = ps[1] = -1;
228 ps[0] = ps[1] = -1;
226 return 0;
229 return 0;
227 } else {
230 } else {
228 return index_get_parents((indexObject *)op, rev, ps, tiprev);
231 return index_get_parents((indexObject *)op, rev, ps, tiprev);
229 }
232 }
230 }
233 }
231
234
232 static inline int64_t index_get_start(indexObject *self, Py_ssize_t rev)
235 static inline int64_t index_get_start(indexObject *self, Py_ssize_t rev)
233 {
236 {
234 uint64_t offset;
237 uint64_t offset;
235 if (rev == nullrev) {
238 if (rev == nullrev) {
236 return 0;
239 return 0;
237 }
240 }
238 if (rev >= self->length) {
241 if (rev >= self->length) {
239 PyObject *tuple;
242 PyObject *tuple;
240 PyObject *pylong;
243 PyObject *pylong;
241 PY_LONG_LONG tmp;
244 PY_LONG_LONG tmp;
242 tuple = PyList_GET_ITEM(self->added, rev - self->length);
245 tuple = PyList_GET_ITEM(self->added, rev - self->length);
243 pylong = PyTuple_GET_ITEM(tuple, 0);
246 pylong = PyTuple_GET_ITEM(tuple, 0);
244 tmp = PyLong_AsLongLong(pylong);
247 tmp = PyLong_AsLongLong(pylong);
245 if (tmp == -1 && PyErr_Occurred()) {
248 if (tmp == -1 && PyErr_Occurred()) {
246 return -1;
249 return -1;
247 }
250 }
248 if (tmp < 0) {
251 if (tmp < 0) {
249 PyErr_Format(PyExc_OverflowError,
252 PyErr_Format(PyExc_OverflowError,
250 "revlog entry size out of bound (%lld)",
253 "revlog entry size out of bound (%lld)",
251 (long long)tmp);
254 (long long)tmp);
252 return -1;
255 return -1;
253 }
256 }
254 offset = (uint64_t)tmp;
257 offset = (uint64_t)tmp;
255 } else {
258 } else {
256 const char *data = index_deref(self, rev);
259 const char *data = index_deref(self, rev);
257 offset = getbe32(data + 4);
260 offset = getbe32(data + 4);
258 if (rev == 0) {
261 if (rev == 0) {
259 /* mask out version number for the first entry */
262 /* mask out version number for the first entry */
260 offset &= 0xFFFF;
263 offset &= 0xFFFF;
261 } else {
264 } else {
262 uint32_t offset_high = getbe32(data);
265 uint32_t offset_high = getbe32(data);
263 offset |= ((uint64_t)offset_high) << 32;
266 offset |= ((uint64_t)offset_high) << 32;
264 }
267 }
265 }
268 }
266 return (int64_t)(offset >> 16);
269 return (int64_t)(offset >> 16);
267 }
270 }
268
271
269 static inline int index_get_length(indexObject *self, Py_ssize_t rev)
272 static inline int index_get_length(indexObject *self, Py_ssize_t rev)
270 {
273 {
271 if (rev == nullrev) {
274 if (rev == nullrev) {
272 return 0;
275 return 0;
273 }
276 }
274 if (rev >= self->length) {
277 if (rev >= self->length) {
275 PyObject *tuple;
278 PyObject *tuple;
276 PyObject *pylong;
279 PyObject *pylong;
277 long ret;
280 long ret;
278 tuple = PyList_GET_ITEM(self->added, rev - self->length);
281 tuple = PyList_GET_ITEM(self->added, rev - self->length);
279 pylong = PyTuple_GET_ITEM(tuple, 1);
282 pylong = PyTuple_GET_ITEM(tuple, 1);
280 ret = PyInt_AsLong(pylong);
283 ret = PyInt_AsLong(pylong);
281 if (ret == -1 && PyErr_Occurred()) {
284 if (ret == -1 && PyErr_Occurred()) {
282 return -1;
285 return -1;
283 }
286 }
284 if (ret < 0 || ret > (long)INT_MAX) {
287 if (ret < 0 || ret > (long)INT_MAX) {
285 PyErr_Format(PyExc_OverflowError,
288 PyErr_Format(PyExc_OverflowError,
286 "revlog entry size out of bound (%ld)",
289 "revlog entry size out of bound (%ld)",
287 ret);
290 ret);
288 return -1;
291 return -1;
289 }
292 }
290 return (int)ret;
293 return (int)ret;
291 } else {
294 } else {
292 const char *data = index_deref(self, rev);
295 const char *data = index_deref(self, rev);
293 int tmp = (int)getbe32(data + 8);
296 int tmp = (int)getbe32(data + 8);
294 if (tmp < 0) {
297 if (tmp < 0) {
295 PyErr_Format(PyExc_OverflowError,
298 PyErr_Format(PyExc_OverflowError,
296 "revlog entry size out of bound (%d)",
299 "revlog entry size out of bound (%d)",
297 tmp);
300 tmp);
298 return -1;
301 return -1;
299 }
302 }
300 return tmp;
303 return tmp;
301 }
304 }
302 }
305 }
303
306
304 /*
307 /*
305 * RevlogNG format (all in big endian, data may be inlined):
308 * RevlogNG format (all in big endian, data may be inlined):
306 * 6 bytes: offset
309 * 6 bytes: offset
307 * 2 bytes: flags
310 * 2 bytes: flags
308 * 4 bytes: compressed length
311 * 4 bytes: compressed length
309 * 4 bytes: uncompressed length
312 * 4 bytes: uncompressed length
310 * 4 bytes: base revision
313 * 4 bytes: base revision
311 * 4 bytes: link revision
314 * 4 bytes: link revision
312 * 4 bytes: parent 1 revision
315 * 4 bytes: parent 1 revision
313 * 4 bytes: parent 2 revision
316 * 4 bytes: parent 2 revision
314 * 32 bytes: nodeid (only 20 bytes used)
317 * 32 bytes: nodeid (only 20 bytes used)
315 */
318 */
316 static PyObject *index_get(indexObject *self, Py_ssize_t pos)
319 static PyObject *index_get(indexObject *self, Py_ssize_t pos)
317 {
320 {
318 uint64_t offset_flags;
321 uint64_t offset_flags;
319 int comp_len, uncomp_len, base_rev, link_rev, parent_1, parent_2;
322 int comp_len, uncomp_len, base_rev, link_rev, parent_1, parent_2;
320 const char *c_node_id;
323 const char *c_node_id;
321 const char *data;
324 const char *data;
322 Py_ssize_t length = index_length(self);
325 Py_ssize_t length = index_length(self);
323 PyObject *entry;
326 PyObject *entry;
324
327
325 if (pos == nullrev) {
328 if (pos == nullrev) {
326 Py_INCREF(nullentry);
329 Py_INCREF(nullentry);
327 return nullentry;
330 return nullentry;
328 }
331 }
329
332
330 if (pos < 0 || pos >= length) {
333 if (pos < 0 || pos >= length) {
331 PyErr_SetString(PyExc_IndexError, "revlog index out of range");
334 PyErr_SetString(PyExc_IndexError, "revlog index out of range");
332 return NULL;
335 return NULL;
333 }
336 }
334
337
335 if (pos >= self->length) {
338 if (pos >= self->length) {
336 PyObject *obj;
339 PyObject *obj;
337 obj = PyList_GET_ITEM(self->added, pos - self->length);
340 obj = PyList_GET_ITEM(self->added, pos - self->length);
338 Py_INCREF(obj);
341 Py_INCREF(obj);
339 return obj;
342 return obj;
340 }
343 }
341
344
342 if (self->cache) {
345 if (self->cache) {
343 if (self->cache[pos]) {
346 if (self->cache[pos]) {
344 Py_INCREF(self->cache[pos]);
347 Py_INCREF(self->cache[pos]);
345 return self->cache[pos];
348 return self->cache[pos];
346 }
349 }
347 } else {
350 } else {
348 self->cache = calloc(self->raw_length, sizeof(PyObject *));
351 self->cache = calloc(self->raw_length, sizeof(PyObject *));
349 if (self->cache == NULL)
352 if (self->cache == NULL)
350 return PyErr_NoMemory();
353 return PyErr_NoMemory();
351 }
354 }
352
355
353 data = index_deref(self, pos);
356 data = index_deref(self, pos);
354 if (data == NULL)
357 if (data == NULL)
355 return NULL;
358 return NULL;
356
359
357 offset_flags = getbe32(data + 4);
360 offset_flags = getbe32(data + 4);
358 if (pos == 0) /* mask out version number for the first entry */
361 if (pos == 0) /* mask out version number for the first entry */
359 offset_flags &= 0xFFFF;
362 offset_flags &= 0xFFFF;
360 else {
363 else {
361 uint32_t offset_high = getbe32(data);
364 uint32_t offset_high = getbe32(data);
362 offset_flags |= ((uint64_t)offset_high) << 32;
365 offset_flags |= ((uint64_t)offset_high) << 32;
363 }
366 }
364
367
365 comp_len = getbe32(data + 8);
368 comp_len = getbe32(data + 8);
366 uncomp_len = getbe32(data + 12);
369 uncomp_len = getbe32(data + 12);
367 base_rev = getbe32(data + 16);
370 base_rev = getbe32(data + 16);
368 link_rev = getbe32(data + 20);
371 link_rev = getbe32(data + 20);
369 parent_1 = getbe32(data + 24);
372 parent_1 = getbe32(data + 24);
370 parent_2 = getbe32(data + 28);
373 parent_2 = getbe32(data + 28);
371 c_node_id = data + 32;
374 c_node_id = data + 32;
372
375
373 entry = Py_BuildValue(tuple_format, offset_flags, comp_len, uncomp_len,
376 entry = Py_BuildValue(tuple_format, offset_flags, comp_len, uncomp_len,
374 base_rev, link_rev, parent_1, parent_2, c_node_id,
377 base_rev, link_rev, parent_1, parent_2, c_node_id,
375 (Py_ssize_t)20);
378 (Py_ssize_t)20);
376
379
377 if (entry) {
380 if (entry) {
378 PyObject_GC_UnTrack(entry);
381 PyObject_GC_UnTrack(entry);
379 Py_INCREF(entry);
382 Py_INCREF(entry);
380 }
383 }
381
384
382 self->cache[pos] = entry;
385 self->cache[pos] = entry;
383
386
384 return entry;
387 return entry;
385 }
388 }
386
389
387 /*
390 /*
388 * Return the 20-byte SHA of the node corresponding to the given rev.
391 * Return the 20-byte SHA of the node corresponding to the given rev.
389 */
392 */
390 static const char *index_node(indexObject *self, Py_ssize_t pos)
393 static const char *index_node(indexObject *self, Py_ssize_t pos)
391 {
394 {
392 Py_ssize_t length = index_length(self);
395 Py_ssize_t length = index_length(self);
393 const char *data;
396 const char *data;
394
397
395 if (pos == nullrev)
398 if (pos == nullrev)
396 return nullid;
399 return nullid;
397
400
398 if (pos >= length)
401 if (pos >= length)
399 return NULL;
402 return NULL;
400
403
401 if (pos >= self->length) {
404 if (pos >= self->length) {
402 PyObject *tuple, *str;
405 PyObject *tuple, *str;
403 tuple = PyList_GET_ITEM(self->added, pos - self->length);
406 tuple = PyList_GET_ITEM(self->added, pos - self->length);
404 str = PyTuple_GetItem(tuple, 7);
407 str = PyTuple_GetItem(tuple, 7);
405 return str ? PyBytes_AS_STRING(str) : NULL;
408 return str ? PyBytes_AS_STRING(str) : NULL;
406 }
409 }
407
410
408 data = index_deref(self, pos);
411 data = index_deref(self, pos);
409 return data ? data + 32 : NULL;
412 return data ? data + 32 : NULL;
410 }
413 }
411
414
412 /*
415 /*
413 * Return the 20-byte SHA of the node corresponding to the given rev. The
416 * Return the 20-byte SHA of the node corresponding to the given rev. The
414 * rev is assumed to be existing. If not, an exception is set.
417 * rev is assumed to be existing. If not, an exception is set.
415 */
418 */
416 static const char *index_node_existing(indexObject *self, Py_ssize_t pos)
419 static const char *index_node_existing(indexObject *self, Py_ssize_t pos)
417 {
420 {
418 const char *node = index_node(self, pos);
421 const char *node = index_node(self, pos);
419 if (node == NULL) {
422 if (node == NULL) {
420 PyErr_Format(PyExc_IndexError, "could not access rev %d",
423 PyErr_Format(PyExc_IndexError, "could not access rev %d",
421 (int)pos);
424 (int)pos);
422 }
425 }
423 return node;
426 return node;
424 }
427 }
425
428
426 static int nt_insert(nodetree *self, const char *node, int rev);
429 static int nt_insert(nodetree *self, const char *node, int rev);
427
430
428 static int node_check(PyObject *obj, char **node)
431 static int node_check(PyObject *obj, char **node)
429 {
432 {
430 Py_ssize_t nodelen;
433 Py_ssize_t nodelen;
431 if (PyBytes_AsStringAndSize(obj, node, &nodelen) == -1)
434 if (PyBytes_AsStringAndSize(obj, node, &nodelen) == -1)
432 return -1;
435 return -1;
433 if (nodelen == 20)
436 if (nodelen == 20)
434 return 0;
437 return 0;
435 PyErr_SetString(PyExc_ValueError, "20-byte hash required");
438 PyErr_SetString(PyExc_ValueError, "20-byte hash required");
436 return -1;
439 return -1;
437 }
440 }
438
441
439 static PyObject *index_append(indexObject *self, PyObject *obj)
442 static PyObject *index_append(indexObject *self, PyObject *obj)
440 {
443 {
441 char *node;
444 char *node;
442 Py_ssize_t len;
445 Py_ssize_t len;
443
446
444 if (!PyTuple_Check(obj) || PyTuple_GET_SIZE(obj) != 8) {
447 if (!PyTuple_Check(obj) || PyTuple_GET_SIZE(obj) != 8) {
445 PyErr_SetString(PyExc_TypeError, "8-tuple required");
448 PyErr_SetString(PyExc_TypeError, "8-tuple required");
446 return NULL;
449 return NULL;
447 }
450 }
448
451
449 if (node_check(PyTuple_GET_ITEM(obj, 7), &node) == -1)
452 if (node_check(PyTuple_GET_ITEM(obj, 7), &node) == -1)
450 return NULL;
453 return NULL;
451
454
452 len = index_length(self);
455 len = index_length(self);
453
456
454 if (self->added == NULL) {
457 if (self->added == NULL) {
455 self->added = PyList_New(0);
458 self->added = PyList_New(0);
456 if (self->added == NULL)
459 if (self->added == NULL)
457 return NULL;
460 return NULL;
458 }
461 }
459
462
460 if (PyList_Append(self->added, obj) == -1)
463 if (PyList_Append(self->added, obj) == -1)
461 return NULL;
464 return NULL;
462
465
463 if (self->ntinitialized)
466 if (self->ntinitialized)
464 nt_insert(&self->nt, node, (int)len);
467 nt_insert(&self->nt, node, (int)len);
465
468
466 Py_CLEAR(self->headrevs);
469 Py_CLEAR(self->headrevs);
467 Py_RETURN_NONE;
470 Py_RETURN_NONE;
468 }
471 }
469
472
470 static PyObject *index_stats(indexObject *self)
473 static PyObject *index_stats(indexObject *self)
471 {
474 {
472 PyObject *obj = PyDict_New();
475 PyObject *obj = PyDict_New();
473 PyObject *s = NULL;
476 PyObject *s = NULL;
474 PyObject *t = NULL;
477 PyObject *t = NULL;
475
478
476 if (obj == NULL)
479 if (obj == NULL)
477 return NULL;
480 return NULL;
478
481
479 #define istat(__n, __d) \
482 #define istat(__n, __d) \
480 do { \
483 do { \
481 s = PyBytes_FromString(__d); \
484 s = PyBytes_FromString(__d); \
482 t = PyInt_FromSsize_t(self->__n); \
485 t = PyInt_FromSsize_t(self->__n); \
483 if (!s || !t) \
486 if (!s || !t) \
484 goto bail; \
487 goto bail; \
485 if (PyDict_SetItem(obj, s, t) == -1) \
488 if (PyDict_SetItem(obj, s, t) == -1) \
486 goto bail; \
489 goto bail; \
487 Py_CLEAR(s); \
490 Py_CLEAR(s); \
488 Py_CLEAR(t); \
491 Py_CLEAR(t); \
489 } while (0)
492 } while (0)
490
493
491 if (self->added) {
494 if (self->added) {
492 Py_ssize_t len = PyList_GET_SIZE(self->added);
495 Py_ssize_t len = PyList_GET_SIZE(self->added);
493 s = PyBytes_FromString("index entries added");
496 s = PyBytes_FromString("index entries added");
494 t = PyInt_FromSsize_t(len);
497 t = PyInt_FromSsize_t(len);
495 if (!s || !t)
498 if (!s || !t)
496 goto bail;
499 goto bail;
497 if (PyDict_SetItem(obj, s, t) == -1)
500 if (PyDict_SetItem(obj, s, t) == -1)
498 goto bail;
501 goto bail;
499 Py_CLEAR(s);
502 Py_CLEAR(s);
500 Py_CLEAR(t);
503 Py_CLEAR(t);
501 }
504 }
502
505
503 if (self->raw_length != self->length)
506 if (self->raw_length != self->length)
504 istat(raw_length, "revs on disk");
507 istat(raw_length, "revs on disk");
505 istat(length, "revs in memory");
508 istat(length, "revs in memory");
506 istat(ntlookups, "node trie lookups");
509 istat(ntlookups, "node trie lookups");
507 istat(ntmisses, "node trie misses");
510 istat(ntmisses, "node trie misses");
508 istat(ntrev, "node trie last rev scanned");
511 istat(ntrev, "node trie last rev scanned");
509 if (self->ntinitialized) {
512 if (self->ntinitialized) {
510 istat(nt.capacity, "node trie capacity");
513 istat(nt.capacity, "node trie capacity");
511 istat(nt.depth, "node trie depth");
514 istat(nt.depth, "node trie depth");
512 istat(nt.length, "node trie count");
515 istat(nt.length, "node trie count");
513 istat(nt.splits, "node trie splits");
516 istat(nt.splits, "node trie splits");
514 }
517 }
515
518
516 #undef istat
519 #undef istat
517
520
518 return obj;
521 return obj;
519
522
520 bail:
523 bail:
521 Py_XDECREF(obj);
524 Py_XDECREF(obj);
522 Py_XDECREF(s);
525 Py_XDECREF(s);
523 Py_XDECREF(t);
526 Py_XDECREF(t);
524 return NULL;
527 return NULL;
525 }
528 }
526
529
527 /*
530 /*
528 * When we cache a list, we want to be sure the caller can't mutate
531 * When we cache a list, we want to be sure the caller can't mutate
529 * the cached copy.
532 * the cached copy.
530 */
533 */
531 static PyObject *list_copy(PyObject *list)
534 static PyObject *list_copy(PyObject *list)
532 {
535 {
533 Py_ssize_t len = PyList_GET_SIZE(list);
536 Py_ssize_t len = PyList_GET_SIZE(list);
534 PyObject *newlist = PyList_New(len);
537 PyObject *newlist = PyList_New(len);
535 Py_ssize_t i;
538 Py_ssize_t i;
536
539
537 if (newlist == NULL)
540 if (newlist == NULL)
538 return NULL;
541 return NULL;
539
542
540 for (i = 0; i < len; i++) {
543 for (i = 0; i < len; i++) {
541 PyObject *obj = PyList_GET_ITEM(list, i);
544 PyObject *obj = PyList_GET_ITEM(list, i);
542 Py_INCREF(obj);
545 Py_INCREF(obj);
543 PyList_SET_ITEM(newlist, i, obj);
546 PyList_SET_ITEM(newlist, i, obj);
544 }
547 }
545
548
546 return newlist;
549 return newlist;
547 }
550 }
548
551
549 static int check_filter(PyObject *filter, Py_ssize_t arg)
552 static int check_filter(PyObject *filter, Py_ssize_t arg)
550 {
553 {
551 if (filter) {
554 if (filter) {
552 PyObject *arglist, *result;
555 PyObject *arglist, *result;
553 int isfiltered;
556 int isfiltered;
554
557
555 arglist = Py_BuildValue("(n)", arg);
558 arglist = Py_BuildValue("(n)", arg);
556 if (!arglist) {
559 if (!arglist) {
557 return -1;
560 return -1;
558 }
561 }
559
562
560 result = PyEval_CallObject(filter, arglist);
563 result = PyEval_CallObject(filter, arglist);
561 Py_DECREF(arglist);
564 Py_DECREF(arglist);
562 if (!result) {
565 if (!result) {
563 return -1;
566 return -1;
564 }
567 }
565
568
566 /* PyObject_IsTrue returns 1 if true, 0 if false, -1 if error,
569 /* PyObject_IsTrue returns 1 if true, 0 if false, -1 if error,
567 * same as this function, so we can just return it directly.*/
570 * same as this function, so we can just return it directly.*/
568 isfiltered = PyObject_IsTrue(result);
571 isfiltered = PyObject_IsTrue(result);
569 Py_DECREF(result);
572 Py_DECREF(result);
570 return isfiltered;
573 return isfiltered;
571 } else {
574 } else {
572 return 0;
575 return 0;
573 }
576 }
574 }
577 }
575
578
576 static Py_ssize_t add_roots_get_min(indexObject *self, PyObject *list,
579 static Py_ssize_t add_roots_get_min(indexObject *self, PyObject *list,
577 Py_ssize_t marker, char *phases)
580 Py_ssize_t marker, char *phases)
578 {
581 {
579 PyObject *iter = NULL;
582 PyObject *iter = NULL;
580 PyObject *iter_item = NULL;
583 PyObject *iter_item = NULL;
581 Py_ssize_t min_idx = index_length(self) + 2;
584 Py_ssize_t min_idx = index_length(self) + 2;
582 long iter_item_long;
585 long iter_item_long;
583
586
584 if (PyList_GET_SIZE(list) != 0) {
587 if (PyList_GET_SIZE(list) != 0) {
585 iter = PyObject_GetIter(list);
588 iter = PyObject_GetIter(list);
586 if (iter == NULL)
589 if (iter == NULL)
587 return -2;
590 return -2;
588 while ((iter_item = PyIter_Next(iter))) {
591 while ((iter_item = PyIter_Next(iter))) {
589 if (!pylong_to_long(iter_item, &iter_item_long)) {
592 if (!pylong_to_long(iter_item, &iter_item_long)) {
590 Py_DECREF(iter_item);
593 Py_DECREF(iter_item);
591 return -2;
594 return -2;
592 }
595 }
593 Py_DECREF(iter_item);
596 Py_DECREF(iter_item);
594 if (iter_item_long < min_idx)
597 if (iter_item_long < min_idx)
595 min_idx = iter_item_long;
598 min_idx = iter_item_long;
596 phases[iter_item_long] = (char)marker;
599 phases[iter_item_long] = (char)marker;
597 }
600 }
598 Py_DECREF(iter);
601 Py_DECREF(iter);
599 }
602 }
600
603
601 return min_idx;
604 return min_idx;
602 }
605 }
603
606
604 static inline void set_phase_from_parents(char *phases, int parent_1,
607 static inline void set_phase_from_parents(char *phases, int parent_1,
605 int parent_2, Py_ssize_t i)
608 int parent_2, Py_ssize_t i)
606 {
609 {
607 if (parent_1 >= 0 && phases[parent_1] > phases[i])
610 if (parent_1 >= 0 && phases[parent_1] > phases[i])
608 phases[i] = phases[parent_1];
611 phases[i] = phases[parent_1];
609 if (parent_2 >= 0 && phases[parent_2] > phases[i])
612 if (parent_2 >= 0 && phases[parent_2] > phases[i])
610 phases[i] = phases[parent_2];
613 phases[i] = phases[parent_2];
611 }
614 }
612
615
613 static PyObject *reachableroots2(indexObject *self, PyObject *args)
616 static PyObject *reachableroots2(indexObject *self, PyObject *args)
614 {
617 {
615
618
616 /* Input */
619 /* Input */
617 long minroot;
620 long minroot;
618 PyObject *includepatharg = NULL;
621 PyObject *includepatharg = NULL;
619 int includepath = 0;
622 int includepath = 0;
620 /* heads and roots are lists */
623 /* heads and roots are lists */
621 PyObject *heads = NULL;
624 PyObject *heads = NULL;
622 PyObject *roots = NULL;
625 PyObject *roots = NULL;
623 PyObject *reachable = NULL;
626 PyObject *reachable = NULL;
624
627
625 PyObject *val;
628 PyObject *val;
626 Py_ssize_t len = index_length(self);
629 Py_ssize_t len = index_length(self);
627 long revnum;
630 long revnum;
628 Py_ssize_t k;
631 Py_ssize_t k;
629 Py_ssize_t i;
632 Py_ssize_t i;
630 Py_ssize_t l;
633 Py_ssize_t l;
631 int r;
634 int r;
632 int parents[2];
635 int parents[2];
633
636
634 /* Internal data structure:
637 /* Internal data structure:
635 * tovisit: array of length len+1 (all revs + nullrev), filled upto
638 * tovisit: array of length len+1 (all revs + nullrev), filled upto
636 * lentovisit
639 * lentovisit
637 *
640 *
638 * revstates: array of length len+1 (all revs + nullrev) */
641 * revstates: array of length len+1 (all revs + nullrev) */
639 int *tovisit = NULL;
642 int *tovisit = NULL;
640 long lentovisit = 0;
643 long lentovisit = 0;
641 enum { RS_SEEN = 1, RS_ROOT = 2, RS_REACHABLE = 4 };
644 enum { RS_SEEN = 1, RS_ROOT = 2, RS_REACHABLE = 4 };
642 char *revstates = NULL;
645 char *revstates = NULL;
643
646
644 /* Get arguments */
647 /* Get arguments */
645 if (!PyArg_ParseTuple(args, "lO!O!O!", &minroot, &PyList_Type, &heads,
648 if (!PyArg_ParseTuple(args, "lO!O!O!", &minroot, &PyList_Type, &heads,
646 &PyList_Type, &roots, &PyBool_Type,
649 &PyList_Type, &roots, &PyBool_Type,
647 &includepatharg))
650 &includepatharg))
648 goto bail;
651 goto bail;
649
652
650 if (includepatharg == Py_True)
653 if (includepatharg == Py_True)
651 includepath = 1;
654 includepath = 1;
652
655
653 /* Initialize return set */
656 /* Initialize return set */
654 reachable = PyList_New(0);
657 reachable = PyList_New(0);
655 if (reachable == NULL)
658 if (reachable == NULL)
656 goto bail;
659 goto bail;
657
660
658 /* Initialize internal datastructures */
661 /* Initialize internal datastructures */
659 tovisit = (int *)malloc((len + 1) * sizeof(int));
662 tovisit = (int *)malloc((len + 1) * sizeof(int));
660 if (tovisit == NULL) {
663 if (tovisit == NULL) {
661 PyErr_NoMemory();
664 PyErr_NoMemory();
662 goto bail;
665 goto bail;
663 }
666 }
664
667
665 revstates = (char *)calloc(len + 1, 1);
668 revstates = (char *)calloc(len + 1, 1);
666 if (revstates == NULL) {
669 if (revstates == NULL) {
667 PyErr_NoMemory();
670 PyErr_NoMemory();
668 goto bail;
671 goto bail;
669 }
672 }
670
673
671 l = PyList_GET_SIZE(roots);
674 l = PyList_GET_SIZE(roots);
672 for (i = 0; i < l; i++) {
675 for (i = 0; i < l; i++) {
673 revnum = PyInt_AsLong(PyList_GET_ITEM(roots, i));
676 revnum = PyInt_AsLong(PyList_GET_ITEM(roots, i));
674 if (revnum == -1 && PyErr_Occurred())
677 if (revnum == -1 && PyErr_Occurred())
675 goto bail;
678 goto bail;
676 /* If root is out of range, e.g. wdir(), it must be unreachable
679 /* If root is out of range, e.g. wdir(), it must be unreachable
677 * from heads. So we can just ignore it. */
680 * from heads. So we can just ignore it. */
678 if (revnum + 1 < 0 || revnum + 1 >= len + 1)
681 if (revnum + 1 < 0 || revnum + 1 >= len + 1)
679 continue;
682 continue;
680 revstates[revnum + 1] |= RS_ROOT;
683 revstates[revnum + 1] |= RS_ROOT;
681 }
684 }
682
685
683 /* Populate tovisit with all the heads */
686 /* Populate tovisit with all the heads */
684 l = PyList_GET_SIZE(heads);
687 l = PyList_GET_SIZE(heads);
685 for (i = 0; i < l; i++) {
688 for (i = 0; i < l; i++) {
686 revnum = PyInt_AsLong(PyList_GET_ITEM(heads, i));
689 revnum = PyInt_AsLong(PyList_GET_ITEM(heads, i));
687 if (revnum == -1 && PyErr_Occurred())
690 if (revnum == -1 && PyErr_Occurred())
688 goto bail;
691 goto bail;
689 if (revnum + 1 < 0 || revnum + 1 >= len + 1) {
692 if (revnum + 1 < 0 || revnum + 1 >= len + 1) {
690 PyErr_SetString(PyExc_IndexError, "head out of range");
693 PyErr_SetString(PyExc_IndexError, "head out of range");
691 goto bail;
694 goto bail;
692 }
695 }
693 if (!(revstates[revnum + 1] & RS_SEEN)) {
696 if (!(revstates[revnum + 1] & RS_SEEN)) {
694 tovisit[lentovisit++] = (int)revnum;
697 tovisit[lentovisit++] = (int)revnum;
695 revstates[revnum + 1] |= RS_SEEN;
698 revstates[revnum + 1] |= RS_SEEN;
696 }
699 }
697 }
700 }
698
701
699 /* Visit the tovisit list and find the reachable roots */
702 /* Visit the tovisit list and find the reachable roots */
700 k = 0;
703 k = 0;
701 while (k < lentovisit) {
704 while (k < lentovisit) {
702 /* Add the node to reachable if it is a root*/
705 /* Add the node to reachable if it is a root*/
703 revnum = tovisit[k++];
706 revnum = tovisit[k++];
704 if (revstates[revnum + 1] & RS_ROOT) {
707 if (revstates[revnum + 1] & RS_ROOT) {
705 revstates[revnum + 1] |= RS_REACHABLE;
708 revstates[revnum + 1] |= RS_REACHABLE;
706 val = PyInt_FromLong(revnum);
709 val = PyInt_FromLong(revnum);
707 if (val == NULL)
710 if (val == NULL)
708 goto bail;
711 goto bail;
709 r = PyList_Append(reachable, val);
712 r = PyList_Append(reachable, val);
710 Py_DECREF(val);
713 Py_DECREF(val);
711 if (r < 0)
714 if (r < 0)
712 goto bail;
715 goto bail;
713 if (includepath == 0)
716 if (includepath == 0)
714 continue;
717 continue;
715 }
718 }
716
719
717 /* Add its parents to the list of nodes to visit */
720 /* Add its parents to the list of nodes to visit */
718 if (revnum == nullrev)
721 if (revnum == nullrev)
719 continue;
722 continue;
720 r = index_get_parents(self, revnum, parents, (int)len - 1);
723 r = index_get_parents(self, revnum, parents, (int)len - 1);
721 if (r < 0)
724 if (r < 0)
722 goto bail;
725 goto bail;
723 for (i = 0; i < 2; i++) {
726 for (i = 0; i < 2; i++) {
724 if (!(revstates[parents[i] + 1] & RS_SEEN) &&
727 if (!(revstates[parents[i] + 1] & RS_SEEN) &&
725 parents[i] >= minroot) {
728 parents[i] >= minroot) {
726 tovisit[lentovisit++] = parents[i];
729 tovisit[lentovisit++] = parents[i];
727 revstates[parents[i] + 1] |= RS_SEEN;
730 revstates[parents[i] + 1] |= RS_SEEN;
728 }
731 }
729 }
732 }
730 }
733 }
731
734
732 /* Find all the nodes in between the roots we found and the heads
735 /* Find all the nodes in between the roots we found and the heads
733 * and add them to the reachable set */
736 * and add them to the reachable set */
734 if (includepath == 1) {
737 if (includepath == 1) {
735 long minidx = minroot;
738 long minidx = minroot;
736 if (minidx < 0)
739 if (minidx < 0)
737 minidx = 0;
740 minidx = 0;
738 for (i = minidx; i < len; i++) {
741 for (i = minidx; i < len; i++) {
739 if (!(revstates[i + 1] & RS_SEEN))
742 if (!(revstates[i + 1] & RS_SEEN))
740 continue;
743 continue;
741 r = index_get_parents(self, i, parents, (int)len - 1);
744 r = index_get_parents(self, i, parents, (int)len - 1);
742 /* Corrupted index file, error is set from
745 /* Corrupted index file, error is set from
743 * index_get_parents */
746 * index_get_parents */
744 if (r < 0)
747 if (r < 0)
745 goto bail;
748 goto bail;
746 if (((revstates[parents[0] + 1] |
749 if (((revstates[parents[0] + 1] |
747 revstates[parents[1] + 1]) &
750 revstates[parents[1] + 1]) &
748 RS_REACHABLE) &&
751 RS_REACHABLE) &&
749 !(revstates[i + 1] & RS_REACHABLE)) {
752 !(revstates[i + 1] & RS_REACHABLE)) {
750 revstates[i + 1] |= RS_REACHABLE;
753 revstates[i + 1] |= RS_REACHABLE;
751 val = PyInt_FromSsize_t(i);
754 val = PyInt_FromSsize_t(i);
752 if (val == NULL)
755 if (val == NULL)
753 goto bail;
756 goto bail;
754 r = PyList_Append(reachable, val);
757 r = PyList_Append(reachable, val);
755 Py_DECREF(val);
758 Py_DECREF(val);
756 if (r < 0)
759 if (r < 0)
757 goto bail;
760 goto bail;
758 }
761 }
759 }
762 }
760 }
763 }
761
764
762 free(revstates);
765 free(revstates);
763 free(tovisit);
766 free(tovisit);
764 return reachable;
767 return reachable;
765 bail:
768 bail:
766 Py_XDECREF(reachable);
769 Py_XDECREF(reachable);
767 free(revstates);
770 free(revstates);
768 free(tovisit);
771 free(tovisit);
769 return NULL;
772 return NULL;
770 }
773 }
771
774
772 static PyObject *compute_phases_map_sets(indexObject *self, PyObject *args)
775 static PyObject *compute_phases_map_sets(indexObject *self, PyObject *args)
773 {
776 {
774 PyObject *roots = Py_None;
777 PyObject *roots = Py_None;
775 PyObject *ret = NULL;
778 PyObject *ret = NULL;
776 PyObject *phasessize = NULL;
779 PyObject *phasessize = NULL;
777 PyObject *phaseroots = NULL;
780 PyObject *phaseroots = NULL;
778 PyObject *phaseset = NULL;
781 PyObject *phaseset = NULL;
779 PyObject *phasessetlist = NULL;
782 PyObject *phasessetlist = NULL;
780 PyObject *rev = NULL;
783 PyObject *rev = NULL;
781 Py_ssize_t len = index_length(self);
784 Py_ssize_t len = index_length(self);
782 Py_ssize_t numphase = 0;
785 Py_ssize_t numphase = 0;
783 Py_ssize_t minrevallphases = 0;
786 Py_ssize_t minrevallphases = 0;
784 Py_ssize_t minrevphase = 0;
787 Py_ssize_t minrevphase = 0;
785 Py_ssize_t i = 0;
788 Py_ssize_t i = 0;
786 char *phases = NULL;
789 char *phases = NULL;
787 long phase;
790 long phase;
788
791
789 if (!PyArg_ParseTuple(args, "O", &roots))
792 if (!PyArg_ParseTuple(args, "O", &roots))
790 goto done;
793 goto done;
791 if (roots == NULL || !PyList_Check(roots)) {
794 if (roots == NULL || !PyList_Check(roots)) {
792 PyErr_SetString(PyExc_TypeError, "roots must be a list");
795 PyErr_SetString(PyExc_TypeError, "roots must be a list");
793 goto done;
796 goto done;
794 }
797 }
795
798
796 phases = calloc(
799 phases = calloc(
797 len, 1); /* phase per rev: {0: public, 1: draft, 2: secret} */
800 len, 1); /* phase per rev: {0: public, 1: draft, 2: secret} */
798 if (phases == NULL) {
801 if (phases == NULL) {
799 PyErr_NoMemory();
802 PyErr_NoMemory();
800 goto done;
803 goto done;
801 }
804 }
802 /* Put the phase information of all the roots in phases */
805 /* Put the phase information of all the roots in phases */
803 numphase = PyList_GET_SIZE(roots) + 1;
806 numphase = PyList_GET_SIZE(roots) + 1;
804 minrevallphases = len + 1;
807 minrevallphases = len + 1;
805 phasessetlist = PyList_New(numphase);
808 phasessetlist = PyList_New(numphase);
806 if (phasessetlist == NULL)
809 if (phasessetlist == NULL)
807 goto done;
810 goto done;
808
811
809 PyList_SET_ITEM(phasessetlist, 0, Py_None);
812 PyList_SET_ITEM(phasessetlist, 0, Py_None);
810 Py_INCREF(Py_None);
813 Py_INCREF(Py_None);
811
814
812 for (i = 0; i < numphase - 1; i++) {
815 for (i = 0; i < numphase - 1; i++) {
813 phaseroots = PyList_GET_ITEM(roots, i);
816 phaseroots = PyList_GET_ITEM(roots, i);
814 phaseset = PySet_New(NULL);
817 phaseset = PySet_New(NULL);
815 if (phaseset == NULL)
818 if (phaseset == NULL)
816 goto release;
819 goto release;
817 PyList_SET_ITEM(phasessetlist, i + 1, phaseset);
820 PyList_SET_ITEM(phasessetlist, i + 1, phaseset);
818 if (!PyList_Check(phaseroots)) {
821 if (!PyList_Check(phaseroots)) {
819 PyErr_SetString(PyExc_TypeError,
822 PyErr_SetString(PyExc_TypeError,
820 "roots item must be a list");
823 "roots item must be a list");
821 goto release;
824 goto release;
822 }
825 }
823 minrevphase =
826 minrevphase =
824 add_roots_get_min(self, phaseroots, i + 1, phases);
827 add_roots_get_min(self, phaseroots, i + 1, phases);
825 if (minrevphase == -2) /* Error from add_roots_get_min */
828 if (minrevphase == -2) /* Error from add_roots_get_min */
826 goto release;
829 goto release;
827 minrevallphases = MIN(minrevallphases, minrevphase);
830 minrevallphases = MIN(minrevallphases, minrevphase);
828 }
831 }
829 /* Propagate the phase information from the roots to the revs */
832 /* Propagate the phase information from the roots to the revs */
830 if (minrevallphases != -1) {
833 if (minrevallphases != -1) {
831 int parents[2];
834 int parents[2];
832 for (i = minrevallphases; i < len; i++) {
835 for (i = minrevallphases; i < len; i++) {
833 if (index_get_parents(self, i, parents, (int)len - 1) <
836 if (index_get_parents(self, i, parents, (int)len - 1) <
834 0)
837 0)
835 goto release;
838 goto release;
836 set_phase_from_parents(phases, parents[0], parents[1],
839 set_phase_from_parents(phases, parents[0], parents[1],
837 i);
840 i);
838 }
841 }
839 }
842 }
840 /* Transform phase list to a python list */
843 /* Transform phase list to a python list */
841 phasessize = PyInt_FromSsize_t(len);
844 phasessize = PyInt_FromSsize_t(len);
842 if (phasessize == NULL)
845 if (phasessize == NULL)
843 goto release;
846 goto release;
844 for (i = 0; i < len; i++) {
847 for (i = 0; i < len; i++) {
845 phase = phases[i];
848 phase = phases[i];
846 /* We only store the sets of phase for non public phase, the
849 /* We only store the sets of phase for non public phase, the
847 * public phase is computed as a difference */
850 * public phase is computed as a difference */
848 if (phase != 0) {
851 if (phase != 0) {
849 phaseset = PyList_GET_ITEM(phasessetlist, phase);
852 phaseset = PyList_GET_ITEM(phasessetlist, phase);
850 rev = PyInt_FromSsize_t(i);
853 rev = PyInt_FromSsize_t(i);
851 if (rev == NULL)
854 if (rev == NULL)
852 goto release;
855 goto release;
853 PySet_Add(phaseset, rev);
856 PySet_Add(phaseset, rev);
854 Py_XDECREF(rev);
857 Py_XDECREF(rev);
855 }
858 }
856 }
859 }
857 ret = PyTuple_Pack(2, phasessize, phasessetlist);
860 ret = PyTuple_Pack(2, phasessize, phasessetlist);
858
861
859 release:
862 release:
860 Py_XDECREF(phasessize);
863 Py_XDECREF(phasessize);
861 Py_XDECREF(phasessetlist);
864 Py_XDECREF(phasessetlist);
862 done:
865 done:
863 free(phases);
866 free(phases);
864 return ret;
867 return ret;
865 }
868 }
866
869
867 static PyObject *index_headrevs(indexObject *self, PyObject *args)
870 static PyObject *index_headrevs(indexObject *self, PyObject *args)
868 {
871 {
869 Py_ssize_t i, j, len;
872 Py_ssize_t i, j, len;
870 char *nothead = NULL;
873 char *nothead = NULL;
871 PyObject *heads = NULL;
874 PyObject *heads = NULL;
872 PyObject *filter = NULL;
875 PyObject *filter = NULL;
873 PyObject *filteredrevs = Py_None;
876 PyObject *filteredrevs = Py_None;
874
877
875 if (!PyArg_ParseTuple(args, "|O", &filteredrevs)) {
878 if (!PyArg_ParseTuple(args, "|O", &filteredrevs)) {
876 return NULL;
879 return NULL;
877 }
880 }
878
881
879 if (self->headrevs && filteredrevs == self->filteredrevs)
882 if (self->headrevs && filteredrevs == self->filteredrevs)
880 return list_copy(self->headrevs);
883 return list_copy(self->headrevs);
881
884
882 Py_DECREF(self->filteredrevs);
885 Py_DECREF(self->filteredrevs);
883 self->filteredrevs = filteredrevs;
886 self->filteredrevs = filteredrevs;
884 Py_INCREF(filteredrevs);
887 Py_INCREF(filteredrevs);
885
888
886 if (filteredrevs != Py_None) {
889 if (filteredrevs != Py_None) {
887 filter = PyObject_GetAttrString(filteredrevs, "__contains__");
890 filter = PyObject_GetAttrString(filteredrevs, "__contains__");
888 if (!filter) {
891 if (!filter) {
889 PyErr_SetString(
892 PyErr_SetString(
890 PyExc_TypeError,
893 PyExc_TypeError,
891 "filteredrevs has no attribute __contains__");
894 "filteredrevs has no attribute __contains__");
892 goto bail;
895 goto bail;
893 }
896 }
894 }
897 }
895
898
896 len = index_length(self);
899 len = index_length(self);
897 heads = PyList_New(0);
900 heads = PyList_New(0);
898 if (heads == NULL)
901 if (heads == NULL)
899 goto bail;
902 goto bail;
900 if (len == 0) {
903 if (len == 0) {
901 PyObject *nullid = PyInt_FromLong(-1);
904 PyObject *nullid = PyInt_FromLong(-1);
902 if (nullid == NULL || PyList_Append(heads, nullid) == -1) {
905 if (nullid == NULL || PyList_Append(heads, nullid) == -1) {
903 Py_XDECREF(nullid);
906 Py_XDECREF(nullid);
904 goto bail;
907 goto bail;
905 }
908 }
906 goto done;
909 goto done;
907 }
910 }
908
911
909 nothead = calloc(len, 1);
912 nothead = calloc(len, 1);
910 if (nothead == NULL) {
913 if (nothead == NULL) {
911 PyErr_NoMemory();
914 PyErr_NoMemory();
912 goto bail;
915 goto bail;
913 }
916 }
914
917
915 for (i = len - 1; i >= 0; i--) {
918 for (i = len - 1; i >= 0; i--) {
916 int isfiltered;
919 int isfiltered;
917 int parents[2];
920 int parents[2];
918
921
919 /* If nothead[i] == 1, it means we've seen an unfiltered child
922 /* If nothead[i] == 1, it means we've seen an unfiltered child
920 * of this node already, and therefore this node is not
923 * of this node already, and therefore this node is not
921 * filtered. So we can skip the expensive check_filter step.
924 * filtered. So we can skip the expensive check_filter step.
922 */
925 */
923 if (nothead[i] != 1) {
926 if (nothead[i] != 1) {
924 isfiltered = check_filter(filter, i);
927 isfiltered = check_filter(filter, i);
925 if (isfiltered == -1) {
928 if (isfiltered == -1) {
926 PyErr_SetString(PyExc_TypeError,
929 PyErr_SetString(PyExc_TypeError,
927 "unable to check filter");
930 "unable to check filter");
928 goto bail;
931 goto bail;
929 }
932 }
930
933
931 if (isfiltered) {
934 if (isfiltered) {
932 nothead[i] = 1;
935 nothead[i] = 1;
933 continue;
936 continue;
934 }
937 }
935 }
938 }
936
939
937 if (index_get_parents(self, i, parents, (int)len - 1) < 0)
940 if (index_get_parents(self, i, parents, (int)len - 1) < 0)
938 goto bail;
941 goto bail;
939 for (j = 0; j < 2; j++) {
942 for (j = 0; j < 2; j++) {
940 if (parents[j] >= 0)
943 if (parents[j] >= 0)
941 nothead[parents[j]] = 1;
944 nothead[parents[j]] = 1;
942 }
945 }
943 }
946 }
944
947
945 for (i = 0; i < len; i++) {
948 for (i = 0; i < len; i++) {
946 PyObject *head;
949 PyObject *head;
947
950
948 if (nothead[i])
951 if (nothead[i])
949 continue;
952 continue;
950 head = PyInt_FromSsize_t(i);
953 head = PyInt_FromSsize_t(i);
951 if (head == NULL || PyList_Append(heads, head) == -1) {
954 if (head == NULL || PyList_Append(heads, head) == -1) {
952 Py_XDECREF(head);
955 Py_XDECREF(head);
953 goto bail;
956 goto bail;
954 }
957 }
955 }
958 }
956
959
957 done:
960 done:
958 self->headrevs = heads;
961 self->headrevs = heads;
959 Py_XDECREF(filter);
962 Py_XDECREF(filter);
960 free(nothead);
963 free(nothead);
961 return list_copy(self->headrevs);
964 return list_copy(self->headrevs);
962 bail:
965 bail:
963 Py_XDECREF(filter);
966 Py_XDECREF(filter);
964 Py_XDECREF(heads);
967 Py_XDECREF(heads);
965 free(nothead);
968 free(nothead);
966 return NULL;
969 return NULL;
967 }
970 }
968
971
969 /**
972 /**
970 * Obtain the base revision index entry.
973 * Obtain the base revision index entry.
971 *
974 *
972 * Callers must ensure that rev >= 0 or illegal memory access may occur.
975 * Callers must ensure that rev >= 0 or illegal memory access may occur.
973 */
976 */
974 static inline int index_baserev(indexObject *self, int rev)
977 static inline int index_baserev(indexObject *self, int rev)
975 {
978 {
976 const char *data;
979 const char *data;
977 int result;
980 int result;
978
981
979 if (rev >= self->length) {
982 if (rev >= self->length) {
980 PyObject *tuple =
983 PyObject *tuple =
981 PyList_GET_ITEM(self->added, rev - self->length);
984 PyList_GET_ITEM(self->added, rev - self->length);
982 long ret;
985 long ret;
983 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 3), &ret)) {
986 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 3), &ret)) {
984 return -2;
987 return -2;
985 }
988 }
986 result = (int)ret;
989 result = (int)ret;
987 } else {
990 } else {
988 data = index_deref(self, rev);
991 data = index_deref(self, rev);
989 if (data == NULL) {
992 if (data == NULL) {
990 return -2;
993 return -2;
991 }
994 }
992
995
993 result = getbe32(data + 16);
996 result = getbe32(data + 16);
994 }
997 }
995 if (result > rev) {
998 if (result > rev) {
996 PyErr_Format(
999 PyErr_Format(
997 PyExc_ValueError,
1000 PyExc_ValueError,
998 "corrupted revlog, revision base above revision: %d, %d",
1001 "corrupted revlog, revision base above revision: %d, %d",
999 rev, result);
1002 rev, result);
1000 return -2;
1003 return -2;
1001 }
1004 }
1002 if (result < -1) {
1005 if (result < -1) {
1003 PyErr_Format(
1006 PyErr_Format(
1004 PyExc_ValueError,
1007 PyExc_ValueError,
1005 "corrupted revlog, revision base out of range: %d, %d", rev,
1008 "corrupted revlog, revision base out of range: %d, %d", rev,
1006 result);
1009 result);
1007 return -2;
1010 return -2;
1008 }
1011 }
1009 return result;
1012 return result;
1010 }
1013 }
1011
1014
1012 /**
1015 /**
1013 * Find if a revision is a snapshot or not
1016 * Find if a revision is a snapshot or not
1014 *
1017 *
1015 * Only relevant for sparse-revlog case.
1018 * Only relevant for sparse-revlog case.
1016 * Callers must ensure that rev is in a valid range.
1019 * Callers must ensure that rev is in a valid range.
1017 */
1020 */
1018 static int index_issnapshotrev(indexObject *self, Py_ssize_t rev)
1021 static int index_issnapshotrev(indexObject *self, Py_ssize_t rev)
1019 {
1022 {
1020 int ps[2];
1023 int ps[2];
1021 Py_ssize_t base;
1024 Py_ssize_t base;
1022 while (rev >= 0) {
1025 while (rev >= 0) {
1023 base = (Py_ssize_t)index_baserev(self, rev);
1026 base = (Py_ssize_t)index_baserev(self, rev);
1024 if (base == rev) {
1027 if (base == rev) {
1025 base = -1;
1028 base = -1;
1026 }
1029 }
1027 if (base == -2) {
1030 if (base == -2) {
1028 assert(PyErr_Occurred());
1031 assert(PyErr_Occurred());
1029 return -1;
1032 return -1;
1030 }
1033 }
1031 if (base == -1) {
1034 if (base == -1) {
1032 return 1;
1035 return 1;
1033 }
1036 }
1034 if (index_get_parents(self, rev, ps, (int)rev) < 0) {
1037 if (index_get_parents(self, rev, ps, (int)rev) < 0) {
1035 assert(PyErr_Occurred());
1038 assert(PyErr_Occurred());
1036 return -1;
1039 return -1;
1037 };
1040 };
1038 if (base == ps[0] || base == ps[1]) {
1041 if (base == ps[0] || base == ps[1]) {
1039 return 0;
1042 return 0;
1040 }
1043 }
1041 rev = base;
1044 rev = base;
1042 }
1045 }
1043 return rev == -1;
1046 return rev == -1;
1044 }
1047 }
1045
1048
1046 static PyObject *index_issnapshot(indexObject *self, PyObject *value)
1049 static PyObject *index_issnapshot(indexObject *self, PyObject *value)
1047 {
1050 {
1048 long rev;
1051 long rev;
1049 int issnap;
1052 int issnap;
1050 Py_ssize_t length = index_length(self);
1053 Py_ssize_t length = index_length(self);
1051
1054
1052 if (!pylong_to_long(value, &rev)) {
1055 if (!pylong_to_long(value, &rev)) {
1053 return NULL;
1056 return NULL;
1054 }
1057 }
1055 if (rev < -1 || rev >= length) {
1058 if (rev < -1 || rev >= length) {
1056 PyErr_Format(PyExc_ValueError, "revlog index out of range: %ld",
1059 PyErr_Format(PyExc_ValueError, "revlog index out of range: %ld",
1057 rev);
1060 rev);
1058 return NULL;
1061 return NULL;
1059 };
1062 };
1060 issnap = index_issnapshotrev(self, (Py_ssize_t)rev);
1063 issnap = index_issnapshotrev(self, (Py_ssize_t)rev);
1061 if (issnap < 0) {
1064 if (issnap < 0) {
1062 return NULL;
1065 return NULL;
1063 };
1066 };
1064 return PyBool_FromLong((long)issnap);
1067 return PyBool_FromLong((long)issnap);
1065 }
1068 }
1066
1069
1067 static PyObject *index_findsnapshots(indexObject *self, PyObject *args)
1070 static PyObject *index_findsnapshots(indexObject *self, PyObject *args)
1068 {
1071 {
1069 Py_ssize_t start_rev;
1072 Py_ssize_t start_rev;
1070 PyObject *cache;
1073 PyObject *cache;
1071 Py_ssize_t base;
1074 Py_ssize_t base;
1072 Py_ssize_t rev;
1075 Py_ssize_t rev;
1073 PyObject *key = NULL;
1076 PyObject *key = NULL;
1074 PyObject *value = NULL;
1077 PyObject *value = NULL;
1075 const Py_ssize_t length = index_length(self);
1078 const Py_ssize_t length = index_length(self);
1076 if (!PyArg_ParseTuple(args, "O!n", &PyDict_Type, &cache, &start_rev)) {
1079 if (!PyArg_ParseTuple(args, "O!n", &PyDict_Type, &cache, &start_rev)) {
1077 return NULL;
1080 return NULL;
1078 }
1081 }
1079 for (rev = start_rev; rev < length; rev++) {
1082 for (rev = start_rev; rev < length; rev++) {
1080 int issnap;
1083 int issnap;
1081 PyObject *allvalues = NULL;
1084 PyObject *allvalues = NULL;
1082 issnap = index_issnapshotrev(self, rev);
1085 issnap = index_issnapshotrev(self, rev);
1083 if (issnap < 0) {
1086 if (issnap < 0) {
1084 goto bail;
1087 goto bail;
1085 }
1088 }
1086 if (issnap == 0) {
1089 if (issnap == 0) {
1087 continue;
1090 continue;
1088 }
1091 }
1089 base = (Py_ssize_t)index_baserev(self, rev);
1092 base = (Py_ssize_t)index_baserev(self, rev);
1090 if (base == rev) {
1093 if (base == rev) {
1091 base = -1;
1094 base = -1;
1092 }
1095 }
1093 if (base == -2) {
1096 if (base == -2) {
1094 assert(PyErr_Occurred());
1097 assert(PyErr_Occurred());
1095 goto bail;
1098 goto bail;
1096 }
1099 }
1097 key = PyInt_FromSsize_t(base);
1100 key = PyInt_FromSsize_t(base);
1098 allvalues = PyDict_GetItem(cache, key);
1101 allvalues = PyDict_GetItem(cache, key);
1099 if (allvalues == NULL && PyErr_Occurred()) {
1102 if (allvalues == NULL && PyErr_Occurred()) {
1100 goto bail;
1103 goto bail;
1101 }
1104 }
1102 if (allvalues == NULL) {
1105 if (allvalues == NULL) {
1103 int r;
1106 int r;
1104 allvalues = PyList_New(0);
1107 allvalues = PyList_New(0);
1105 if (!allvalues) {
1108 if (!allvalues) {
1106 goto bail;
1109 goto bail;
1107 }
1110 }
1108 r = PyDict_SetItem(cache, key, allvalues);
1111 r = PyDict_SetItem(cache, key, allvalues);
1109 Py_DECREF(allvalues);
1112 Py_DECREF(allvalues);
1110 if (r < 0) {
1113 if (r < 0) {
1111 goto bail;
1114 goto bail;
1112 }
1115 }
1113 }
1116 }
1114 value = PyInt_FromSsize_t(rev);
1117 value = PyInt_FromSsize_t(rev);
1115 if (PyList_Append(allvalues, value)) {
1118 if (PyList_Append(allvalues, value)) {
1116 goto bail;
1119 goto bail;
1117 }
1120 }
1118 Py_CLEAR(key);
1121 Py_CLEAR(key);
1119 Py_CLEAR(value);
1122 Py_CLEAR(value);
1120 }
1123 }
1121 Py_RETURN_NONE;
1124 Py_RETURN_NONE;
1122 bail:
1125 bail:
1123 Py_XDECREF(key);
1126 Py_XDECREF(key);
1124 Py_XDECREF(value);
1127 Py_XDECREF(value);
1125 return NULL;
1128 return NULL;
1126 }
1129 }
1127
1130
1128 static PyObject *index_deltachain(indexObject *self, PyObject *args)
1131 static PyObject *index_deltachain(indexObject *self, PyObject *args)
1129 {
1132 {
1130 int rev, generaldelta;
1133 int rev, generaldelta;
1131 PyObject *stoparg;
1134 PyObject *stoparg;
1132 int stoprev, iterrev, baserev = -1;
1135 int stoprev, iterrev, baserev = -1;
1133 int stopped;
1136 int stopped;
1134 PyObject *chain = NULL, *result = NULL;
1137 PyObject *chain = NULL, *result = NULL;
1135 const Py_ssize_t length = index_length(self);
1138 const Py_ssize_t length = index_length(self);
1136
1139
1137 if (!PyArg_ParseTuple(args, "iOi", &rev, &stoparg, &generaldelta)) {
1140 if (!PyArg_ParseTuple(args, "iOi", &rev, &stoparg, &generaldelta)) {
1138 return NULL;
1141 return NULL;
1139 }
1142 }
1140
1143
1141 if (PyInt_Check(stoparg)) {
1144 if (PyInt_Check(stoparg)) {
1142 stoprev = (int)PyInt_AsLong(stoparg);
1145 stoprev = (int)PyInt_AsLong(stoparg);
1143 if (stoprev == -1 && PyErr_Occurred()) {
1146 if (stoprev == -1 && PyErr_Occurred()) {
1144 return NULL;
1147 return NULL;
1145 }
1148 }
1146 } else if (stoparg == Py_None) {
1149 } else if (stoparg == Py_None) {
1147 stoprev = -2;
1150 stoprev = -2;
1148 } else {
1151 } else {
1149 PyErr_SetString(PyExc_ValueError,
1152 PyErr_SetString(PyExc_ValueError,
1150 "stoprev must be integer or None");
1153 "stoprev must be integer or None");
1151 return NULL;
1154 return NULL;
1152 }
1155 }
1153
1156
1154 if (rev < 0 || rev >= length) {
1157 if (rev < 0 || rev >= length) {
1155 PyErr_SetString(PyExc_ValueError, "revlog index out of range");
1158 PyErr_SetString(PyExc_ValueError, "revlog index out of range");
1156 return NULL;
1159 return NULL;
1157 }
1160 }
1158
1161
1159 chain = PyList_New(0);
1162 chain = PyList_New(0);
1160 if (chain == NULL) {
1163 if (chain == NULL) {
1161 return NULL;
1164 return NULL;
1162 }
1165 }
1163
1166
1164 baserev = index_baserev(self, rev);
1167 baserev = index_baserev(self, rev);
1165
1168
1166 /* This should never happen. */
1169 /* This should never happen. */
1167 if (baserev <= -2) {
1170 if (baserev <= -2) {
1168 /* Error should be set by index_deref() */
1171 /* Error should be set by index_deref() */
1169 assert(PyErr_Occurred());
1172 assert(PyErr_Occurred());
1170 goto bail;
1173 goto bail;
1171 }
1174 }
1172
1175
1173 iterrev = rev;
1176 iterrev = rev;
1174
1177
1175 while (iterrev != baserev && iterrev != stoprev) {
1178 while (iterrev != baserev && iterrev != stoprev) {
1176 PyObject *value = PyInt_FromLong(iterrev);
1179 PyObject *value = PyInt_FromLong(iterrev);
1177 if (value == NULL) {
1180 if (value == NULL) {
1178 goto bail;
1181 goto bail;
1179 }
1182 }
1180 if (PyList_Append(chain, value)) {
1183 if (PyList_Append(chain, value)) {
1181 Py_DECREF(value);
1184 Py_DECREF(value);
1182 goto bail;
1185 goto bail;
1183 }
1186 }
1184 Py_DECREF(value);
1187 Py_DECREF(value);
1185
1188
1186 if (generaldelta) {
1189 if (generaldelta) {
1187 iterrev = baserev;
1190 iterrev = baserev;
1188 } else {
1191 } else {
1189 iterrev--;
1192 iterrev--;
1190 }
1193 }
1191
1194
1192 if (iterrev < 0) {
1195 if (iterrev < 0) {
1193 break;
1196 break;
1194 }
1197 }
1195
1198
1196 if (iterrev >= length) {
1199 if (iterrev >= length) {
1197 PyErr_SetString(PyExc_IndexError,
1200 PyErr_SetString(PyExc_IndexError,
1198 "revision outside index");
1201 "revision outside index");
1199 return NULL;
1202 return NULL;
1200 }
1203 }
1201
1204
1202 baserev = index_baserev(self, iterrev);
1205 baserev = index_baserev(self, iterrev);
1203
1206
1204 /* This should never happen. */
1207 /* This should never happen. */
1205 if (baserev <= -2) {
1208 if (baserev <= -2) {
1206 /* Error should be set by index_deref() */
1209 /* Error should be set by index_deref() */
1207 assert(PyErr_Occurred());
1210 assert(PyErr_Occurred());
1208 goto bail;
1211 goto bail;
1209 }
1212 }
1210 }
1213 }
1211
1214
1212 if (iterrev == stoprev) {
1215 if (iterrev == stoprev) {
1213 stopped = 1;
1216 stopped = 1;
1214 } else {
1217 } else {
1215 PyObject *value = PyInt_FromLong(iterrev);
1218 PyObject *value = PyInt_FromLong(iterrev);
1216 if (value == NULL) {
1219 if (value == NULL) {
1217 goto bail;
1220 goto bail;
1218 }
1221 }
1219 if (PyList_Append(chain, value)) {
1222 if (PyList_Append(chain, value)) {
1220 Py_DECREF(value);
1223 Py_DECREF(value);
1221 goto bail;
1224 goto bail;
1222 }
1225 }
1223 Py_DECREF(value);
1226 Py_DECREF(value);
1224
1227
1225 stopped = 0;
1228 stopped = 0;
1226 }
1229 }
1227
1230
1228 if (PyList_Reverse(chain)) {
1231 if (PyList_Reverse(chain)) {
1229 goto bail;
1232 goto bail;
1230 }
1233 }
1231
1234
1232 result = Py_BuildValue("OO", chain, stopped ? Py_True : Py_False);
1235 result = Py_BuildValue("OO", chain, stopped ? Py_True : Py_False);
1233 Py_DECREF(chain);
1236 Py_DECREF(chain);
1234 return result;
1237 return result;
1235
1238
1236 bail:
1239 bail:
1237 Py_DECREF(chain);
1240 Py_DECREF(chain);
1238 return NULL;
1241 return NULL;
1239 }
1242 }
1240
1243
1241 static inline int64_t
1244 static inline int64_t
1242 index_segment_span(indexObject *self, Py_ssize_t start_rev, Py_ssize_t end_rev)
1245 index_segment_span(indexObject *self, Py_ssize_t start_rev, Py_ssize_t end_rev)
1243 {
1246 {
1244 int64_t start_offset;
1247 int64_t start_offset;
1245 int64_t end_offset;
1248 int64_t end_offset;
1246 int end_size;
1249 int end_size;
1247 start_offset = index_get_start(self, start_rev);
1250 start_offset = index_get_start(self, start_rev);
1248 if (start_offset < 0) {
1251 if (start_offset < 0) {
1249 return -1;
1252 return -1;
1250 }
1253 }
1251 end_offset = index_get_start(self, end_rev);
1254 end_offset = index_get_start(self, end_rev);
1252 if (end_offset < 0) {
1255 if (end_offset < 0) {
1253 return -1;
1256 return -1;
1254 }
1257 }
1255 end_size = index_get_length(self, end_rev);
1258 end_size = index_get_length(self, end_rev);
1256 if (end_size < 0) {
1259 if (end_size < 0) {
1257 return -1;
1260 return -1;
1258 }
1261 }
1259 if (end_offset < start_offset) {
1262 if (end_offset < start_offset) {
1260 PyErr_Format(PyExc_ValueError,
1263 PyErr_Format(PyExc_ValueError,
1261 "corrupted revlog index: inconsistent offset "
1264 "corrupted revlog index: inconsistent offset "
1262 "between revisions (%zd) and (%zd)",
1265 "between revisions (%zd) and (%zd)",
1263 start_rev, end_rev);
1266 start_rev, end_rev);
1264 return -1;
1267 return -1;
1265 }
1268 }
1266 return (end_offset - start_offset) + (int64_t)end_size;
1269 return (end_offset - start_offset) + (int64_t)end_size;
1267 }
1270 }
1268
1271
1269 /* returns endidx so that revs[startidx:endidx] has no empty trailing revs */
1272 /* returns endidx so that revs[startidx:endidx] has no empty trailing revs */
1270 static Py_ssize_t trim_endidx(indexObject *self, const Py_ssize_t *revs,
1273 static Py_ssize_t trim_endidx(indexObject *self, const Py_ssize_t *revs,
1271 Py_ssize_t startidx, Py_ssize_t endidx)
1274 Py_ssize_t startidx, Py_ssize_t endidx)
1272 {
1275 {
1273 int length;
1276 int length;
1274 while (endidx > 1 && endidx > startidx) {
1277 while (endidx > 1 && endidx > startidx) {
1275 length = index_get_length(self, revs[endidx - 1]);
1278 length = index_get_length(self, revs[endidx - 1]);
1276 if (length < 0) {
1279 if (length < 0) {
1277 return -1;
1280 return -1;
1278 }
1281 }
1279 if (length != 0) {
1282 if (length != 0) {
1280 break;
1283 break;
1281 }
1284 }
1282 endidx -= 1;
1285 endidx -= 1;
1283 }
1286 }
1284 return endidx;
1287 return endidx;
1285 }
1288 }
1286
1289
1287 struct Gap {
1290 struct Gap {
1288 int64_t size;
1291 int64_t size;
1289 Py_ssize_t idx;
1292 Py_ssize_t idx;
1290 };
1293 };
1291
1294
1292 static int gap_compare(const void *left, const void *right)
1295 static int gap_compare(const void *left, const void *right)
1293 {
1296 {
1294 const struct Gap *l_left = ((const struct Gap *)left);
1297 const struct Gap *l_left = ((const struct Gap *)left);
1295 const struct Gap *l_right = ((const struct Gap *)right);
1298 const struct Gap *l_right = ((const struct Gap *)right);
1296 if (l_left->size < l_right->size) {
1299 if (l_left->size < l_right->size) {
1297 return -1;
1300 return -1;
1298 } else if (l_left->size > l_right->size) {
1301 } else if (l_left->size > l_right->size) {
1299 return 1;
1302 return 1;
1300 }
1303 }
1301 return 0;
1304 return 0;
1302 }
1305 }
1303 static int Py_ssize_t_compare(const void *left, const void *right)
1306 static int Py_ssize_t_compare(const void *left, const void *right)
1304 {
1307 {
1305 const Py_ssize_t l_left = *(const Py_ssize_t *)left;
1308 const Py_ssize_t l_left = *(const Py_ssize_t *)left;
1306 const Py_ssize_t l_right = *(const Py_ssize_t *)right;
1309 const Py_ssize_t l_right = *(const Py_ssize_t *)right;
1307 if (l_left < l_right) {
1310 if (l_left < l_right) {
1308 return -1;
1311 return -1;
1309 } else if (l_left > l_right) {
1312 } else if (l_left > l_right) {
1310 return 1;
1313 return 1;
1311 }
1314 }
1312 return 0;
1315 return 0;
1313 }
1316 }
1314
1317
1315 static PyObject *index_slicechunktodensity(indexObject *self, PyObject *args)
1318 static PyObject *index_slicechunktodensity(indexObject *self, PyObject *args)
1316 {
1319 {
1317 /* method arguments */
1320 /* method arguments */
1318 PyObject *list_revs = NULL; /* revisions in the chain */
1321 PyObject *list_revs = NULL; /* revisions in the chain */
1319 double targetdensity = 0; /* min density to achieve */
1322 double targetdensity = 0; /* min density to achieve */
1320 Py_ssize_t mingapsize = 0; /* threshold to ignore gaps */
1323 Py_ssize_t mingapsize = 0; /* threshold to ignore gaps */
1321
1324
1322 /* other core variables */
1325 /* other core variables */
1323 Py_ssize_t idxlen = index_length(self);
1326 Py_ssize_t idxlen = index_length(self);
1324 Py_ssize_t i; /* used for various iteration */
1327 Py_ssize_t i; /* used for various iteration */
1325 PyObject *result = NULL; /* the final return of the function */
1328 PyObject *result = NULL; /* the final return of the function */
1326
1329
1327 /* generic information about the delta chain being slice */
1330 /* generic information about the delta chain being slice */
1328 Py_ssize_t num_revs = 0; /* size of the full delta chain */
1331 Py_ssize_t num_revs = 0; /* size of the full delta chain */
1329 Py_ssize_t *revs = NULL; /* native array of revision in the chain */
1332 Py_ssize_t *revs = NULL; /* native array of revision in the chain */
1330 int64_t chainpayload = 0; /* sum of all delta in the chain */
1333 int64_t chainpayload = 0; /* sum of all delta in the chain */
1331 int64_t deltachainspan = 0; /* distance from first byte to last byte */
1334 int64_t deltachainspan = 0; /* distance from first byte to last byte */
1332
1335
1333 /* variable used for slicing the delta chain */
1336 /* variable used for slicing the delta chain */
1334 int64_t readdata = 0; /* amount of data currently planned to be read */
1337 int64_t readdata = 0; /* amount of data currently planned to be read */
1335 double density = 0; /* ration of payload data compared to read ones */
1338 double density = 0; /* ration of payload data compared to read ones */
1336 int64_t previous_end;
1339 int64_t previous_end;
1337 struct Gap *gaps = NULL; /* array of notable gap in the chain */
1340 struct Gap *gaps = NULL; /* array of notable gap in the chain */
1338 Py_ssize_t num_gaps =
1341 Py_ssize_t num_gaps =
1339 0; /* total number of notable gap recorded so far */
1342 0; /* total number of notable gap recorded so far */
1340 Py_ssize_t *selected_indices = NULL; /* indices of gap skipped over */
1343 Py_ssize_t *selected_indices = NULL; /* indices of gap skipped over */
1341 Py_ssize_t num_selected = 0; /* number of gaps skipped */
1344 Py_ssize_t num_selected = 0; /* number of gaps skipped */
1342 PyObject *chunk = NULL; /* individual slice */
1345 PyObject *chunk = NULL; /* individual slice */
1343 PyObject *allchunks = NULL; /* all slices */
1346 PyObject *allchunks = NULL; /* all slices */
1344 Py_ssize_t previdx;
1347 Py_ssize_t previdx;
1345
1348
1346 /* parsing argument */
1349 /* parsing argument */
1347 if (!PyArg_ParseTuple(args, "O!dn", &PyList_Type, &list_revs,
1350 if (!PyArg_ParseTuple(args, "O!dn", &PyList_Type, &list_revs,
1348 &targetdensity, &mingapsize)) {
1351 &targetdensity, &mingapsize)) {
1349 goto bail;
1352 goto bail;
1350 }
1353 }
1351
1354
1352 /* If the delta chain contains a single element, we do not need slicing
1355 /* If the delta chain contains a single element, we do not need slicing
1353 */
1356 */
1354 num_revs = PyList_GET_SIZE(list_revs);
1357 num_revs = PyList_GET_SIZE(list_revs);
1355 if (num_revs <= 1) {
1358 if (num_revs <= 1) {
1356 result = PyTuple_Pack(1, list_revs);
1359 result = PyTuple_Pack(1, list_revs);
1357 goto done;
1360 goto done;
1358 }
1361 }
1359
1362
1360 /* Turn the python list into a native integer array (for efficiency) */
1363 /* Turn the python list into a native integer array (for efficiency) */
1361 revs = (Py_ssize_t *)calloc(num_revs, sizeof(Py_ssize_t));
1364 revs = (Py_ssize_t *)calloc(num_revs, sizeof(Py_ssize_t));
1362 if (revs == NULL) {
1365 if (revs == NULL) {
1363 PyErr_NoMemory();
1366 PyErr_NoMemory();
1364 goto bail;
1367 goto bail;
1365 }
1368 }
1366 for (i = 0; i < num_revs; i++) {
1369 for (i = 0; i < num_revs; i++) {
1367 Py_ssize_t revnum = PyInt_AsLong(PyList_GET_ITEM(list_revs, i));
1370 Py_ssize_t revnum = PyInt_AsLong(PyList_GET_ITEM(list_revs, i));
1368 if (revnum == -1 && PyErr_Occurred()) {
1371 if (revnum == -1 && PyErr_Occurred()) {
1369 goto bail;
1372 goto bail;
1370 }
1373 }
1371 if (revnum < nullrev || revnum >= idxlen) {
1374 if (revnum < nullrev || revnum >= idxlen) {
1372 PyErr_Format(PyExc_IndexError,
1375 PyErr_Format(PyExc_IndexError,
1373 "index out of range: %zd", revnum);
1376 "index out of range: %zd", revnum);
1374 goto bail;
1377 goto bail;
1375 }
1378 }
1376 revs[i] = revnum;
1379 revs[i] = revnum;
1377 }
1380 }
1378
1381
1379 /* Compute and check various property of the unsliced delta chain */
1382 /* Compute and check various property of the unsliced delta chain */
1380 deltachainspan = index_segment_span(self, revs[0], revs[num_revs - 1]);
1383 deltachainspan = index_segment_span(self, revs[0], revs[num_revs - 1]);
1381 if (deltachainspan < 0) {
1384 if (deltachainspan < 0) {
1382 goto bail;
1385 goto bail;
1383 }
1386 }
1384
1387
1385 if (deltachainspan <= mingapsize) {
1388 if (deltachainspan <= mingapsize) {
1386 result = PyTuple_Pack(1, list_revs);
1389 result = PyTuple_Pack(1, list_revs);
1387 goto done;
1390 goto done;
1388 }
1391 }
1389 chainpayload = 0;
1392 chainpayload = 0;
1390 for (i = 0; i < num_revs; i++) {
1393 for (i = 0; i < num_revs; i++) {
1391 int tmp = index_get_length(self, revs[i]);
1394 int tmp = index_get_length(self, revs[i]);
1392 if (tmp < 0) {
1395 if (tmp < 0) {
1393 goto bail;
1396 goto bail;
1394 }
1397 }
1395 chainpayload += tmp;
1398 chainpayload += tmp;
1396 }
1399 }
1397
1400
1398 readdata = deltachainspan;
1401 readdata = deltachainspan;
1399 density = 1.0;
1402 density = 1.0;
1400
1403
1401 if (0 < deltachainspan) {
1404 if (0 < deltachainspan) {
1402 density = (double)chainpayload / (double)deltachainspan;
1405 density = (double)chainpayload / (double)deltachainspan;
1403 }
1406 }
1404
1407
1405 if (density >= targetdensity) {
1408 if (density >= targetdensity) {
1406 result = PyTuple_Pack(1, list_revs);
1409 result = PyTuple_Pack(1, list_revs);
1407 goto done;
1410 goto done;
1408 }
1411 }
1409
1412
1410 /* if chain is too sparse, look for relevant gaps */
1413 /* if chain is too sparse, look for relevant gaps */
1411 gaps = (struct Gap *)calloc(num_revs, sizeof(struct Gap));
1414 gaps = (struct Gap *)calloc(num_revs, sizeof(struct Gap));
1412 if (gaps == NULL) {
1415 if (gaps == NULL) {
1413 PyErr_NoMemory();
1416 PyErr_NoMemory();
1414 goto bail;
1417 goto bail;
1415 }
1418 }
1416
1419
1417 previous_end = -1;
1420 previous_end = -1;
1418 for (i = 0; i < num_revs; i++) {
1421 for (i = 0; i < num_revs; i++) {
1419 int64_t revstart;
1422 int64_t revstart;
1420 int revsize;
1423 int revsize;
1421 revstart = index_get_start(self, revs[i]);
1424 revstart = index_get_start(self, revs[i]);
1422 if (revstart < 0) {
1425 if (revstart < 0) {
1423 goto bail;
1426 goto bail;
1424 };
1427 };
1425 revsize = index_get_length(self, revs[i]);
1428 revsize = index_get_length(self, revs[i]);
1426 if (revsize < 0) {
1429 if (revsize < 0) {
1427 goto bail;
1430 goto bail;
1428 };
1431 };
1429 if (revsize == 0) {
1432 if (revsize == 0) {
1430 continue;
1433 continue;
1431 }
1434 }
1432 if (previous_end >= 0) {
1435 if (previous_end >= 0) {
1433 int64_t gapsize = revstart - previous_end;
1436 int64_t gapsize = revstart - previous_end;
1434 if (gapsize > mingapsize) {
1437 if (gapsize > mingapsize) {
1435 gaps[num_gaps].size = gapsize;
1438 gaps[num_gaps].size = gapsize;
1436 gaps[num_gaps].idx = i;
1439 gaps[num_gaps].idx = i;
1437 num_gaps += 1;
1440 num_gaps += 1;
1438 }
1441 }
1439 }
1442 }
1440 previous_end = revstart + revsize;
1443 previous_end = revstart + revsize;
1441 }
1444 }
1442 if (num_gaps == 0) {
1445 if (num_gaps == 0) {
1443 result = PyTuple_Pack(1, list_revs);
1446 result = PyTuple_Pack(1, list_revs);
1444 goto done;
1447 goto done;
1445 }
1448 }
1446 qsort(gaps, num_gaps, sizeof(struct Gap), &gap_compare);
1449 qsort(gaps, num_gaps, sizeof(struct Gap), &gap_compare);
1447
1450
1448 /* Slice the largest gap first, they improve the density the most */
1451 /* Slice the largest gap first, they improve the density the most */
1449 selected_indices =
1452 selected_indices =
1450 (Py_ssize_t *)malloc((num_gaps + 1) * sizeof(Py_ssize_t));
1453 (Py_ssize_t *)malloc((num_gaps + 1) * sizeof(Py_ssize_t));
1451 if (selected_indices == NULL) {
1454 if (selected_indices == NULL) {
1452 PyErr_NoMemory();
1455 PyErr_NoMemory();
1453 goto bail;
1456 goto bail;
1454 }
1457 }
1455
1458
1456 for (i = num_gaps - 1; i >= 0; i--) {
1459 for (i = num_gaps - 1; i >= 0; i--) {
1457 selected_indices[num_selected] = gaps[i].idx;
1460 selected_indices[num_selected] = gaps[i].idx;
1458 readdata -= gaps[i].size;
1461 readdata -= gaps[i].size;
1459 num_selected += 1;
1462 num_selected += 1;
1460 if (readdata <= 0) {
1463 if (readdata <= 0) {
1461 density = 1.0;
1464 density = 1.0;
1462 } else {
1465 } else {
1463 density = (double)chainpayload / (double)readdata;
1466 density = (double)chainpayload / (double)readdata;
1464 }
1467 }
1465 if (density >= targetdensity) {
1468 if (density >= targetdensity) {
1466 break;
1469 break;
1467 }
1470 }
1468 }
1471 }
1469 qsort(selected_indices, num_selected, sizeof(Py_ssize_t),
1472 qsort(selected_indices, num_selected, sizeof(Py_ssize_t),
1470 &Py_ssize_t_compare);
1473 &Py_ssize_t_compare);
1471
1474
1472 /* create the resulting slice */
1475 /* create the resulting slice */
1473 allchunks = PyList_New(0);
1476 allchunks = PyList_New(0);
1474 if (allchunks == NULL) {
1477 if (allchunks == NULL) {
1475 goto bail;
1478 goto bail;
1476 }
1479 }
1477 previdx = 0;
1480 previdx = 0;
1478 selected_indices[num_selected] = num_revs;
1481 selected_indices[num_selected] = num_revs;
1479 for (i = 0; i <= num_selected; i++) {
1482 for (i = 0; i <= num_selected; i++) {
1480 Py_ssize_t idx = selected_indices[i];
1483 Py_ssize_t idx = selected_indices[i];
1481 Py_ssize_t endidx = trim_endidx(self, revs, previdx, idx);
1484 Py_ssize_t endidx = trim_endidx(self, revs, previdx, idx);
1482 if (endidx < 0) {
1485 if (endidx < 0) {
1483 goto bail;
1486 goto bail;
1484 }
1487 }
1485 if (previdx < endidx) {
1488 if (previdx < endidx) {
1486 chunk = PyList_GetSlice(list_revs, previdx, endidx);
1489 chunk = PyList_GetSlice(list_revs, previdx, endidx);
1487 if (chunk == NULL) {
1490 if (chunk == NULL) {
1488 goto bail;
1491 goto bail;
1489 }
1492 }
1490 if (PyList_Append(allchunks, chunk) == -1) {
1493 if (PyList_Append(allchunks, chunk) == -1) {
1491 goto bail;
1494 goto bail;
1492 }
1495 }
1493 Py_DECREF(chunk);
1496 Py_DECREF(chunk);
1494 chunk = NULL;
1497 chunk = NULL;
1495 }
1498 }
1496 previdx = idx;
1499 previdx = idx;
1497 }
1500 }
1498 result = allchunks;
1501 result = allchunks;
1499 goto done;
1502 goto done;
1500
1503
1501 bail:
1504 bail:
1502 Py_XDECREF(allchunks);
1505 Py_XDECREF(allchunks);
1503 Py_XDECREF(chunk);
1506 Py_XDECREF(chunk);
1504 done:
1507 done:
1505 free(revs);
1508 free(revs);
1506 free(gaps);
1509 free(gaps);
1507 free(selected_indices);
1510 free(selected_indices);
1508 return result;
1511 return result;
1509 }
1512 }
1510
1513
1511 static inline int nt_level(const char *node, Py_ssize_t level)
1514 static inline int nt_level(const char *node, Py_ssize_t level)
1512 {
1515 {
1513 int v = node[level >> 1];
1516 int v = node[level >> 1];
1514 if (!(level & 1))
1517 if (!(level & 1))
1515 v >>= 4;
1518 v >>= 4;
1516 return v & 0xf;
1519 return v & 0xf;
1517 }
1520 }
1518
1521
1519 /*
1522 /*
1520 * Return values:
1523 * Return values:
1521 *
1524 *
1522 * -4: match is ambiguous (multiple candidates)
1525 * -4: match is ambiguous (multiple candidates)
1523 * -2: not found
1526 * -2: not found
1524 * rest: valid rev
1527 * rest: valid rev
1525 */
1528 */
1526 static int nt_find(nodetree *self, const char *node, Py_ssize_t nodelen,
1529 static int nt_find(nodetree *self, const char *node, Py_ssize_t nodelen,
1527 int hex)
1530 int hex)
1528 {
1531 {
1529 int (*getnybble)(const char *, Py_ssize_t) = hex ? hexdigit : nt_level;
1532 int (*getnybble)(const char *, Py_ssize_t) = hex ? hexdigit : nt_level;
1530 int level, maxlevel, off;
1533 int level, maxlevel, off;
1531
1534
1532 if (nodelen == 20 && node[0] == '\0' && memcmp(node, nullid, 20) == 0)
1535 if (nodelen == 20 && node[0] == '\0' && memcmp(node, nullid, 20) == 0)
1533 return -1;
1536 return -1;
1534
1537
1535 if (hex)
1538 if (hex)
1536 maxlevel = nodelen > 40 ? 40 : (int)nodelen;
1539 maxlevel = nodelen > 40 ? 40 : (int)nodelen;
1537 else
1540 else
1538 maxlevel = nodelen > 20 ? 40 : ((int)nodelen * 2);
1541 maxlevel = nodelen > 20 ? 40 : ((int)nodelen * 2);
1539
1542
1540 for (level = off = 0; level < maxlevel; level++) {
1543 for (level = off = 0; level < maxlevel; level++) {
1541 int k = getnybble(node, level);
1544 int k = getnybble(node, level);
1542 nodetreenode *n = &self->nodes[off];
1545 nodetreenode *n = &self->nodes[off];
1543 int v = n->children[k];
1546 int v = n->children[k];
1544
1547
1545 if (v < 0) {
1548 if (v < 0) {
1546 const char *n;
1549 const char *n;
1547 Py_ssize_t i;
1550 Py_ssize_t i;
1548
1551
1549 v = -(v + 2);
1552 v = -(v + 2);
1550 n = index_node(self->index, v);
1553 n = index_node(self->index, v);
1551 if (n == NULL)
1554 if (n == NULL)
1552 return -2;
1555 return -2;
1553 for (i = level; i < maxlevel; i++)
1556 for (i = level; i < maxlevel; i++)
1554 if (getnybble(node, i) != nt_level(n, i))
1557 if (getnybble(node, i) != nt_level(n, i))
1555 return -2;
1558 return -2;
1556 return v;
1559 return v;
1557 }
1560 }
1558 if (v == 0)
1561 if (v == 0)
1559 return -2;
1562 return -2;
1560 off = v;
1563 off = v;
1561 }
1564 }
1562 /* multiple matches against an ambiguous prefix */
1565 /* multiple matches against an ambiguous prefix */
1563 return -4;
1566 return -4;
1564 }
1567 }
1565
1568
1566 static int nt_new(nodetree *self)
1569 static int nt_new(nodetree *self)
1567 {
1570 {
1568 if (self->length == self->capacity) {
1571 if (self->length == self->capacity) {
1569 unsigned newcapacity;
1572 unsigned newcapacity;
1570 nodetreenode *newnodes;
1573 nodetreenode *newnodes;
1571 newcapacity = self->capacity * 2;
1574 newcapacity = self->capacity * 2;
1572 if (newcapacity >= INT_MAX / sizeof(nodetreenode)) {
1575 if (newcapacity >= INT_MAX / sizeof(nodetreenode)) {
1573 PyErr_SetString(PyExc_MemoryError,
1576 PyErr_SetString(PyExc_MemoryError,
1574 "overflow in nt_new");
1577 "overflow in nt_new");
1575 return -1;
1578 return -1;
1576 }
1579 }
1577 newnodes =
1580 newnodes =
1578 realloc(self->nodes, newcapacity * sizeof(nodetreenode));
1581 realloc(self->nodes, newcapacity * sizeof(nodetreenode));
1579 if (newnodes == NULL) {
1582 if (newnodes == NULL) {
1580 PyErr_SetString(PyExc_MemoryError, "out of memory");
1583 PyErr_SetString(PyExc_MemoryError, "out of memory");
1581 return -1;
1584 return -1;
1582 }
1585 }
1583 self->capacity = newcapacity;
1586 self->capacity = newcapacity;
1584 self->nodes = newnodes;
1587 self->nodes = newnodes;
1585 memset(&self->nodes[self->length], 0,
1588 memset(&self->nodes[self->length], 0,
1586 sizeof(nodetreenode) * (self->capacity - self->length));
1589 sizeof(nodetreenode) * (self->capacity - self->length));
1587 }
1590 }
1588 return self->length++;
1591 return self->length++;
1589 }
1592 }
1590
1593
1591 static int nt_insert(nodetree *self, const char *node, int rev)
1594 static int nt_insert(nodetree *self, const char *node, int rev)
1592 {
1595 {
1593 int level = 0;
1596 int level = 0;
1594 int off = 0;
1597 int off = 0;
1595
1598
1596 while (level < 40) {
1599 while (level < 40) {
1597 int k = nt_level(node, level);
1600 int k = nt_level(node, level);
1598 nodetreenode *n;
1601 nodetreenode *n;
1599 int v;
1602 int v;
1600
1603
1601 n = &self->nodes[off];
1604 n = &self->nodes[off];
1602 v = n->children[k];
1605 v = n->children[k];
1603
1606
1604 if (v == 0) {
1607 if (v == 0) {
1605 n->children[k] = -rev - 2;
1608 n->children[k] = -rev - 2;
1606 return 0;
1609 return 0;
1607 }
1610 }
1608 if (v < 0) {
1611 if (v < 0) {
1609 const char *oldnode =
1612 const char *oldnode =
1610 index_node_existing(self->index, -(v + 2));
1613 index_node_existing(self->index, -(v + 2));
1611 int noff;
1614 int noff;
1612
1615
1613 if (oldnode == NULL)
1616 if (oldnode == NULL)
1614 return -1;
1617 return -1;
1615 if (!memcmp(oldnode, node, 20)) {
1618 if (!memcmp(oldnode, node, 20)) {
1616 n->children[k] = -rev - 2;
1619 n->children[k] = -rev - 2;
1617 return 0;
1620 return 0;
1618 }
1621 }
1619 noff = nt_new(self);
1622 noff = nt_new(self);
1620 if (noff == -1)
1623 if (noff == -1)
1621 return -1;
1624 return -1;
1622 /* self->nodes may have been changed by realloc */
1625 /* self->nodes may have been changed by realloc */
1623 self->nodes[off].children[k] = noff;
1626 self->nodes[off].children[k] = noff;
1624 off = noff;
1627 off = noff;
1625 n = &self->nodes[off];
1628 n = &self->nodes[off];
1626 n->children[nt_level(oldnode, ++level)] = v;
1629 n->children[nt_level(oldnode, ++level)] = v;
1627 if (level > self->depth)
1630 if (level > self->depth)
1628 self->depth = level;
1631 self->depth = level;
1629 self->splits += 1;
1632 self->splits += 1;
1630 } else {
1633 } else {
1631 level += 1;
1634 level += 1;
1632 off = v;
1635 off = v;
1633 }
1636 }
1634 }
1637 }
1635
1638
1636 return -1;
1639 return -1;
1637 }
1640 }
1638
1641
1639 static PyObject *ntobj_insert(nodetreeObject *self, PyObject *args)
1642 static PyObject *ntobj_insert(nodetreeObject *self, PyObject *args)
1640 {
1643 {
1641 Py_ssize_t rev;
1644 Py_ssize_t rev;
1642 const char *node;
1645 const char *node;
1643 Py_ssize_t length;
1646 Py_ssize_t length;
1644 if (!PyArg_ParseTuple(args, "n", &rev))
1647 if (!PyArg_ParseTuple(args, "n", &rev))
1645 return NULL;
1648 return NULL;
1646 length = index_length(self->nt.index);
1649 length = index_length(self->nt.index);
1647 if (rev < 0 || rev >= length) {
1650 if (rev < 0 || rev >= length) {
1648 PyErr_SetString(PyExc_ValueError, "revlog index out of range");
1651 PyErr_SetString(PyExc_ValueError, "revlog index out of range");
1649 return NULL;
1652 return NULL;
1650 }
1653 }
1651 node = index_node_existing(self->nt.index, rev);
1654 node = index_node_existing(self->nt.index, rev);
1652 if (nt_insert(&self->nt, node, (int)rev) == -1)
1655 if (nt_insert(&self->nt, node, (int)rev) == -1)
1653 return NULL;
1656 return NULL;
1654 Py_RETURN_NONE;
1657 Py_RETURN_NONE;
1655 }
1658 }
1656
1659
1657 static int nt_delete_node(nodetree *self, const char *node)
1660 static int nt_delete_node(nodetree *self, const char *node)
1658 {
1661 {
1659 /* rev==-2 happens to get encoded as 0, which is interpreted as not set
1662 /* rev==-2 happens to get encoded as 0, which is interpreted as not set
1660 */
1663 */
1661 return nt_insert(self, node, -2);
1664 return nt_insert(self, node, -2);
1662 }
1665 }
1663
1666
1664 static int nt_init(nodetree *self, indexObject *index, unsigned capacity)
1667 static int nt_init(nodetree *self, indexObject *index, unsigned capacity)
1665 {
1668 {
1666 /* Initialize before overflow-checking to avoid nt_dealloc() crash. */
1669 /* Initialize before overflow-checking to avoid nt_dealloc() crash. */
1667 self->nodes = NULL;
1670 self->nodes = NULL;
1668
1671
1669 self->index = index;
1672 self->index = index;
1670 /* The input capacity is in terms of revisions, while the field is in
1673 /* The input capacity is in terms of revisions, while the field is in
1671 * terms of nodetree nodes. */
1674 * terms of nodetree nodes. */
1672 self->capacity = (capacity < 4 ? 4 : capacity / 2);
1675 self->capacity = (capacity < 4 ? 4 : capacity / 2);
1673 self->depth = 0;
1676 self->depth = 0;
1674 self->splits = 0;
1677 self->splits = 0;
1675 if ((size_t)self->capacity > INT_MAX / sizeof(nodetreenode)) {
1678 if ((size_t)self->capacity > INT_MAX / sizeof(nodetreenode)) {
1676 PyErr_SetString(PyExc_ValueError, "overflow in init_nt");
1679 PyErr_SetString(PyExc_ValueError, "overflow in init_nt");
1677 return -1;
1680 return -1;
1678 }
1681 }
1679 self->nodes = calloc(self->capacity, sizeof(nodetreenode));
1682 self->nodes = calloc(self->capacity, sizeof(nodetreenode));
1680 if (self->nodes == NULL) {
1683 if (self->nodes == NULL) {
1681 PyErr_NoMemory();
1684 PyErr_NoMemory();
1682 return -1;
1685 return -1;
1683 }
1686 }
1684 self->length = 1;
1687 self->length = 1;
1685 return 0;
1688 return 0;
1686 }
1689 }
1687
1690
1688 static int ntobj_init(nodetreeObject *self, PyObject *args)
1691 static int ntobj_init(nodetreeObject *self, PyObject *args)
1689 {
1692 {
1690 PyObject *index;
1693 PyObject *index;
1691 unsigned capacity;
1694 unsigned capacity;
1692 if (!PyArg_ParseTuple(args, "O!I", &HgRevlogIndex_Type, &index,
1695 if (!PyArg_ParseTuple(args, "O!I", &HgRevlogIndex_Type, &index,
1693 &capacity))
1696 &capacity))
1694 return -1;
1697 return -1;
1695 Py_INCREF(index);
1698 Py_INCREF(index);
1696 return nt_init(&self->nt, (indexObject *)index, capacity);
1699 return nt_init(&self->nt, (indexObject *)index, capacity);
1697 }
1700 }
1698
1701
1699 static int nt_partialmatch(nodetree *self, const char *node, Py_ssize_t nodelen)
1702 static int nt_partialmatch(nodetree *self, const char *node, Py_ssize_t nodelen)
1700 {
1703 {
1701 return nt_find(self, node, nodelen, 1);
1704 return nt_find(self, node, nodelen, 1);
1702 }
1705 }
1703
1706
1704 /*
1707 /*
1705 * Find the length of the shortest unique prefix of node.
1708 * Find the length of the shortest unique prefix of node.
1706 *
1709 *
1707 * Return values:
1710 * Return values:
1708 *
1711 *
1709 * -3: error (exception set)
1712 * -3: error (exception set)
1710 * -2: not found (no exception set)
1713 * -2: not found (no exception set)
1711 * rest: length of shortest prefix
1714 * rest: length of shortest prefix
1712 */
1715 */
1713 static int nt_shortest(nodetree *self, const char *node)
1716 static int nt_shortest(nodetree *self, const char *node)
1714 {
1717 {
1715 int level, off;
1718 int level, off;
1716
1719
1717 for (level = off = 0; level < 40; level++) {
1720 for (level = off = 0; level < 40; level++) {
1718 int k, v;
1721 int k, v;
1719 nodetreenode *n = &self->nodes[off];
1722 nodetreenode *n = &self->nodes[off];
1720 k = nt_level(node, level);
1723 k = nt_level(node, level);
1721 v = n->children[k];
1724 v = n->children[k];
1722 if (v < 0) {
1725 if (v < 0) {
1723 const char *n;
1726 const char *n;
1724 v = -(v + 2);
1727 v = -(v + 2);
1725 n = index_node_existing(self->index, v);
1728 n = index_node_existing(self->index, v);
1726 if (n == NULL)
1729 if (n == NULL)
1727 return -3;
1730 return -3;
1728 if (memcmp(node, n, 20) != 0)
1731 if (memcmp(node, n, 20) != 0)
1729 /*
1732 /*
1730 * Found a unique prefix, but it wasn't for the
1733 * Found a unique prefix, but it wasn't for the
1731 * requested node (i.e the requested node does
1734 * requested node (i.e the requested node does
1732 * not exist).
1735 * not exist).
1733 */
1736 */
1734 return -2;
1737 return -2;
1735 return level + 1;
1738 return level + 1;
1736 }
1739 }
1737 if (v == 0)
1740 if (v == 0)
1738 return -2;
1741 return -2;
1739 off = v;
1742 off = v;
1740 }
1743 }
1741 /*
1744 /*
1742 * The node was still not unique after 40 hex digits, so this won't
1745 * The node was still not unique after 40 hex digits, so this won't
1743 * happen. Also, if we get here, then there's a programming error in
1746 * happen. Also, if we get here, then there's a programming error in
1744 * this file that made us insert a node longer than 40 hex digits.
1747 * this file that made us insert a node longer than 40 hex digits.
1745 */
1748 */
1746 PyErr_SetString(PyExc_Exception, "broken node tree");
1749 PyErr_SetString(PyExc_Exception, "broken node tree");
1747 return -3;
1750 return -3;
1748 }
1751 }
1749
1752
1750 static PyObject *ntobj_shortest(nodetreeObject *self, PyObject *args)
1753 static PyObject *ntobj_shortest(nodetreeObject *self, PyObject *args)
1751 {
1754 {
1752 PyObject *val;
1755 PyObject *val;
1753 char *node;
1756 char *node;
1754 int length;
1757 int length;
1755
1758
1756 if (!PyArg_ParseTuple(args, "O", &val))
1759 if (!PyArg_ParseTuple(args, "O", &val))
1757 return NULL;
1760 return NULL;
1758 if (node_check(val, &node) == -1)
1761 if (node_check(val, &node) == -1)
1759 return NULL;
1762 return NULL;
1760
1763
1761 length = nt_shortest(&self->nt, node);
1764 length = nt_shortest(&self->nt, node);
1762 if (length == -3)
1765 if (length == -3)
1763 return NULL;
1766 return NULL;
1764 if (length == -2) {
1767 if (length == -2) {
1765 raise_revlog_error();
1768 raise_revlog_error();
1766 return NULL;
1769 return NULL;
1767 }
1770 }
1768 return PyInt_FromLong(length);
1771 return PyInt_FromLong(length);
1769 }
1772 }
1770
1773
1771 static void nt_dealloc(nodetree *self)
1774 static void nt_dealloc(nodetree *self)
1772 {
1775 {
1773 free(self->nodes);
1776 free(self->nodes);
1774 self->nodes = NULL;
1777 self->nodes = NULL;
1775 }
1778 }
1776
1779
1777 static void ntobj_dealloc(nodetreeObject *self)
1780 static void ntobj_dealloc(nodetreeObject *self)
1778 {
1781 {
1779 Py_XDECREF(self->nt.index);
1782 Py_XDECREF(self->nt.index);
1780 nt_dealloc(&self->nt);
1783 nt_dealloc(&self->nt);
1781 PyObject_Del(self);
1784 PyObject_Del(self);
1782 }
1785 }
1783
1786
1784 static PyMethodDef ntobj_methods[] = {
1787 static PyMethodDef ntobj_methods[] = {
1785 {"insert", (PyCFunction)ntobj_insert, METH_VARARGS,
1788 {"insert", (PyCFunction)ntobj_insert, METH_VARARGS,
1786 "insert an index entry"},
1789 "insert an index entry"},
1787 {"shortest", (PyCFunction)ntobj_shortest, METH_VARARGS,
1790 {"shortest", (PyCFunction)ntobj_shortest, METH_VARARGS,
1788 "find length of shortest hex nodeid of a binary ID"},
1791 "find length of shortest hex nodeid of a binary ID"},
1789 {NULL} /* Sentinel */
1792 {NULL} /* Sentinel */
1790 };
1793 };
1791
1794
1792 static PyTypeObject nodetreeType = {
1795 static PyTypeObject nodetreeType = {
1793 PyVarObject_HEAD_INIT(NULL, 0) /* header */
1796 PyVarObject_HEAD_INIT(NULL, 0) /* header */
1794 "parsers.nodetree", /* tp_name */
1797 "parsers.nodetree", /* tp_name */
1795 sizeof(nodetreeObject), /* tp_basicsize */
1798 sizeof(nodetreeObject), /* tp_basicsize */
1796 0, /* tp_itemsize */
1799 0, /* tp_itemsize */
1797 (destructor)ntobj_dealloc, /* tp_dealloc */
1800 (destructor)ntobj_dealloc, /* tp_dealloc */
1798 0, /* tp_print */
1801 0, /* tp_print */
1799 0, /* tp_getattr */
1802 0, /* tp_getattr */
1800 0, /* tp_setattr */
1803 0, /* tp_setattr */
1801 0, /* tp_compare */
1804 0, /* tp_compare */
1802 0, /* tp_repr */
1805 0, /* tp_repr */
1803 0, /* tp_as_number */
1806 0, /* tp_as_number */
1804 0, /* tp_as_sequence */
1807 0, /* tp_as_sequence */
1805 0, /* tp_as_mapping */
1808 0, /* tp_as_mapping */
1806 0, /* tp_hash */
1809 0, /* tp_hash */
1807 0, /* tp_call */
1810 0, /* tp_call */
1808 0, /* tp_str */
1811 0, /* tp_str */
1809 0, /* tp_getattro */
1812 0, /* tp_getattro */
1810 0, /* tp_setattro */
1813 0, /* tp_setattro */
1811 0, /* tp_as_buffer */
1814 0, /* tp_as_buffer */
1812 Py_TPFLAGS_DEFAULT, /* tp_flags */
1815 Py_TPFLAGS_DEFAULT, /* tp_flags */
1813 "nodetree", /* tp_doc */
1816 "nodetree", /* tp_doc */
1814 0, /* tp_traverse */
1817 0, /* tp_traverse */
1815 0, /* tp_clear */
1818 0, /* tp_clear */
1816 0, /* tp_richcompare */
1819 0, /* tp_richcompare */
1817 0, /* tp_weaklistoffset */
1820 0, /* tp_weaklistoffset */
1818 0, /* tp_iter */
1821 0, /* tp_iter */
1819 0, /* tp_iternext */
1822 0, /* tp_iternext */
1820 ntobj_methods, /* tp_methods */
1823 ntobj_methods, /* tp_methods */
1821 0, /* tp_members */
1824 0, /* tp_members */
1822 0, /* tp_getset */
1825 0, /* tp_getset */
1823 0, /* tp_base */
1826 0, /* tp_base */
1824 0, /* tp_dict */
1827 0, /* tp_dict */
1825 0, /* tp_descr_get */
1828 0, /* tp_descr_get */
1826 0, /* tp_descr_set */
1829 0, /* tp_descr_set */
1827 0, /* tp_dictoffset */
1830 0, /* tp_dictoffset */
1828 (initproc)ntobj_init, /* tp_init */
1831 (initproc)ntobj_init, /* tp_init */
1829 0, /* tp_alloc */
1832 0, /* tp_alloc */
1830 };
1833 };
1831
1834
1832 static int index_init_nt(indexObject *self)
1835 static int index_init_nt(indexObject *self)
1833 {
1836 {
1834 if (!self->ntinitialized) {
1837 if (!self->ntinitialized) {
1835 if (nt_init(&self->nt, self, (int)self->raw_length) == -1) {
1838 if (nt_init(&self->nt, self, (int)self->raw_length) == -1) {
1836 nt_dealloc(&self->nt);
1839 nt_dealloc(&self->nt);
1837 return -1;
1840 return -1;
1838 }
1841 }
1839 if (nt_insert(&self->nt, nullid, -1) == -1) {
1842 if (nt_insert(&self->nt, nullid, -1) == -1) {
1840 nt_dealloc(&self->nt);
1843 nt_dealloc(&self->nt);
1841 return -1;
1844 return -1;
1842 }
1845 }
1843 self->ntinitialized = 1;
1846 self->ntinitialized = 1;
1844 self->ntrev = (int)index_length(self);
1847 self->ntrev = (int)index_length(self);
1845 self->ntlookups = 1;
1848 self->ntlookups = 1;
1846 self->ntmisses = 0;
1849 self->ntmisses = 0;
1847 }
1850 }
1848 return 0;
1851 return 0;
1849 }
1852 }
1850
1853
1851 /*
1854 /*
1852 * Return values:
1855 * Return values:
1853 *
1856 *
1854 * -3: error (exception set)
1857 * -3: error (exception set)
1855 * -2: not found (no exception set)
1858 * -2: not found (no exception set)
1856 * rest: valid rev
1859 * rest: valid rev
1857 */
1860 */
1858 static int index_find_node(indexObject *self, const char *node,
1861 static int index_find_node(indexObject *self, const char *node,
1859 Py_ssize_t nodelen)
1862 Py_ssize_t nodelen)
1860 {
1863 {
1861 int rev;
1864 int rev;
1862
1865
1863 if (index_init_nt(self) == -1)
1866 if (index_init_nt(self) == -1)
1864 return -3;
1867 return -3;
1865
1868
1866 self->ntlookups++;
1869 self->ntlookups++;
1867 rev = nt_find(&self->nt, node, nodelen, 0);
1870 rev = nt_find(&self->nt, node, nodelen, 0);
1868 if (rev >= -1)
1871 if (rev >= -1)
1869 return rev;
1872 return rev;
1870
1873
1871 /*
1874 /*
1872 * For the first handful of lookups, we scan the entire index,
1875 * For the first handful of lookups, we scan the entire index,
1873 * and cache only the matching nodes. This optimizes for cases
1876 * and cache only the matching nodes. This optimizes for cases
1874 * like "hg tip", where only a few nodes are accessed.
1877 * like "hg tip", where only a few nodes are accessed.
1875 *
1878 *
1876 * After that, we cache every node we visit, using a single
1879 * After that, we cache every node we visit, using a single
1877 * scan amortized over multiple lookups. This gives the best
1880 * scan amortized over multiple lookups. This gives the best
1878 * bulk performance, e.g. for "hg log".
1881 * bulk performance, e.g. for "hg log".
1879 */
1882 */
1880 if (self->ntmisses++ < 4) {
1883 if (self->ntmisses++ < 4) {
1881 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1884 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1882 const char *n = index_node_existing(self, rev);
1885 const char *n = index_node_existing(self, rev);
1883 if (n == NULL)
1886 if (n == NULL)
1884 return -3;
1887 return -3;
1885 if (memcmp(node, n, nodelen > 20 ? 20 : nodelen) == 0) {
1888 if (memcmp(node, n, nodelen > 20 ? 20 : nodelen) == 0) {
1886 if (nt_insert(&self->nt, n, rev) == -1)
1889 if (nt_insert(&self->nt, n, rev) == -1)
1887 return -3;
1890 return -3;
1888 break;
1891 break;
1889 }
1892 }
1890 }
1893 }
1891 } else {
1894 } else {
1892 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1895 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1893 const char *n = index_node_existing(self, rev);
1896 const char *n = index_node_existing(self, rev);
1894 if (n == NULL)
1897 if (n == NULL)
1895 return -3;
1898 return -3;
1896 if (nt_insert(&self->nt, n, rev) == -1) {
1899 if (nt_insert(&self->nt, n, rev) == -1) {
1897 self->ntrev = rev + 1;
1900 self->ntrev = rev + 1;
1898 return -3;
1901 return -3;
1899 }
1902 }
1900 if (memcmp(node, n, nodelen > 20 ? 20 : nodelen) == 0) {
1903 if (memcmp(node, n, nodelen > 20 ? 20 : nodelen) == 0) {
1901 break;
1904 break;
1902 }
1905 }
1903 }
1906 }
1904 self->ntrev = rev;
1907 self->ntrev = rev;
1905 }
1908 }
1906
1909
1907 if (rev >= 0)
1910 if (rev >= 0)
1908 return rev;
1911 return rev;
1909 return -2;
1912 return -2;
1910 }
1913 }
1911
1914
1912 static PyObject *index_getitem(indexObject *self, PyObject *value)
1915 static PyObject *index_getitem(indexObject *self, PyObject *value)
1913 {
1916 {
1914 char *node;
1917 char *node;
1915 int rev;
1918 int rev;
1916
1919
1917 if (PyInt_Check(value)) {
1920 if (PyInt_Check(value)) {
1918 long idx;
1921 long idx;
1919 if (!pylong_to_long(value, &idx)) {
1922 if (!pylong_to_long(value, &idx)) {
1920 return NULL;
1923 return NULL;
1921 }
1924 }
1922 return index_get(self, idx);
1925 return index_get(self, idx);
1923 }
1926 }
1924
1927
1925 if (node_check(value, &node) == -1)
1928 if (node_check(value, &node) == -1)
1926 return NULL;
1929 return NULL;
1927 rev = index_find_node(self, node, 20);
1930 rev = index_find_node(self, node, 20);
1928 if (rev >= -1)
1931 if (rev >= -1)
1929 return PyInt_FromLong(rev);
1932 return PyInt_FromLong(rev);
1930 if (rev == -2)
1933 if (rev == -2)
1931 raise_revlog_error();
1934 raise_revlog_error();
1932 return NULL;
1935 return NULL;
1933 }
1936 }
1934
1937
1935 /*
1938 /*
1936 * Fully populate the radix tree.
1939 * Fully populate the radix tree.
1937 */
1940 */
1938 static int index_populate_nt(indexObject *self)
1941 static int index_populate_nt(indexObject *self)
1939 {
1942 {
1940 int rev;
1943 int rev;
1941 if (self->ntrev > 0) {
1944 if (self->ntrev > 0) {
1942 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1945 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1943 const char *n = index_node_existing(self, rev);
1946 const char *n = index_node_existing(self, rev);
1944 if (n == NULL)
1947 if (n == NULL)
1945 return -1;
1948 return -1;
1946 if (nt_insert(&self->nt, n, rev) == -1)
1949 if (nt_insert(&self->nt, n, rev) == -1)
1947 return -1;
1950 return -1;
1948 }
1951 }
1949 self->ntrev = -1;
1952 self->ntrev = -1;
1950 }
1953 }
1951 return 0;
1954 return 0;
1952 }
1955 }
1953
1956
1954 static PyObject *index_partialmatch(indexObject *self, PyObject *args)
1957 static PyObject *index_partialmatch(indexObject *self, PyObject *args)
1955 {
1958 {
1956 const char *fullnode;
1959 const char *fullnode;
1957 Py_ssize_t nodelen;
1960 Py_ssize_t nodelen;
1958 char *node;
1961 char *node;
1959 int rev, i;
1962 int rev, i;
1960
1963
1961 if (!PyArg_ParseTuple(args, PY23("s#", "y#"), &node, &nodelen))
1964 if (!PyArg_ParseTuple(args, PY23("s#", "y#"), &node, &nodelen))
1962 return NULL;
1965 return NULL;
1963
1966
1964 if (nodelen < 1) {
1967 if (nodelen < 1) {
1965 PyErr_SetString(PyExc_ValueError, "key too short");
1968 PyErr_SetString(PyExc_ValueError, "key too short");
1966 return NULL;
1969 return NULL;
1967 }
1970 }
1968
1971
1969 if (nodelen > 40) {
1972 if (nodelen > 40) {
1970 PyErr_SetString(PyExc_ValueError, "key too long");
1973 PyErr_SetString(PyExc_ValueError, "key too long");
1971 return NULL;
1974 return NULL;
1972 }
1975 }
1973
1976
1974 for (i = 0; i < nodelen; i++)
1977 for (i = 0; i < nodelen; i++)
1975 hexdigit(node, i);
1978 hexdigit(node, i);
1976 if (PyErr_Occurred()) {
1979 if (PyErr_Occurred()) {
1977 /* input contains non-hex characters */
1980 /* input contains non-hex characters */
1978 PyErr_Clear();
1981 PyErr_Clear();
1979 Py_RETURN_NONE;
1982 Py_RETURN_NONE;
1980 }
1983 }
1981
1984
1982 if (index_init_nt(self) == -1)
1985 if (index_init_nt(self) == -1)
1983 return NULL;
1986 return NULL;
1984 if (index_populate_nt(self) == -1)
1987 if (index_populate_nt(self) == -1)
1985 return NULL;
1988 return NULL;
1986 rev = nt_partialmatch(&self->nt, node, nodelen);
1989 rev = nt_partialmatch(&self->nt, node, nodelen);
1987
1990
1988 switch (rev) {
1991 switch (rev) {
1989 case -4:
1992 case -4:
1990 raise_revlog_error();
1993 raise_revlog_error();
1991 return NULL;
1994 return NULL;
1992 case -2:
1995 case -2:
1993 Py_RETURN_NONE;
1996 Py_RETURN_NONE;
1994 case -1:
1997 case -1:
1995 return PyBytes_FromStringAndSize(nullid, 20);
1998 return PyBytes_FromStringAndSize(nullid, 20);
1996 }
1999 }
1997
2000
1998 fullnode = index_node_existing(self, rev);
2001 fullnode = index_node_existing(self, rev);
1999 if (fullnode == NULL) {
2002 if (fullnode == NULL) {
2000 return NULL;
2003 return NULL;
2001 }
2004 }
2002 return PyBytes_FromStringAndSize(fullnode, 20);
2005 return PyBytes_FromStringAndSize(fullnode, 20);
2003 }
2006 }
2004
2007
2005 static PyObject *index_shortest(indexObject *self, PyObject *args)
2008 static PyObject *index_shortest(indexObject *self, PyObject *args)
2006 {
2009 {
2007 PyObject *val;
2010 PyObject *val;
2008 char *node;
2011 char *node;
2009 int length;
2012 int length;
2010
2013
2011 if (!PyArg_ParseTuple(args, "O", &val))
2014 if (!PyArg_ParseTuple(args, "O", &val))
2012 return NULL;
2015 return NULL;
2013 if (node_check(val, &node) == -1)
2016 if (node_check(val, &node) == -1)
2014 return NULL;
2017 return NULL;
2015
2018
2016 self->ntlookups++;
2019 self->ntlookups++;
2017 if (index_init_nt(self) == -1)
2020 if (index_init_nt(self) == -1)
2018 return NULL;
2021 return NULL;
2019 if (index_populate_nt(self) == -1)
2022 if (index_populate_nt(self) == -1)
2020 return NULL;
2023 return NULL;
2021 length = nt_shortest(&self->nt, node);
2024 length = nt_shortest(&self->nt, node);
2022 if (length == -3)
2025 if (length == -3)
2023 return NULL;
2026 return NULL;
2024 if (length == -2) {
2027 if (length == -2) {
2025 raise_revlog_error();
2028 raise_revlog_error();
2026 return NULL;
2029 return NULL;
2027 }
2030 }
2028 return PyInt_FromLong(length);
2031 return PyInt_FromLong(length);
2029 }
2032 }
2030
2033
2031 static PyObject *index_m_get(indexObject *self, PyObject *args)
2034 static PyObject *index_m_get(indexObject *self, PyObject *args)
2032 {
2035 {
2033 PyObject *val;
2036 PyObject *val;
2034 char *node;
2037 char *node;
2035 int rev;
2038 int rev;
2036
2039
2037 if (!PyArg_ParseTuple(args, "O", &val))
2040 if (!PyArg_ParseTuple(args, "O", &val))
2038 return NULL;
2041 return NULL;
2039 if (node_check(val, &node) == -1)
2042 if (node_check(val, &node) == -1)
2040 return NULL;
2043 return NULL;
2041 rev = index_find_node(self, node, 20);
2044 rev = index_find_node(self, node, 20);
2042 if (rev == -3)
2045 if (rev == -3)
2043 return NULL;
2046 return NULL;
2044 if (rev == -2)
2047 if (rev == -2)
2045 Py_RETURN_NONE;
2048 Py_RETURN_NONE;
2046 return PyInt_FromLong(rev);
2049 return PyInt_FromLong(rev);
2047 }
2050 }
2048
2051
2049 static int index_contains(indexObject *self, PyObject *value)
2052 static int index_contains(indexObject *self, PyObject *value)
2050 {
2053 {
2051 char *node;
2054 char *node;
2052
2055
2053 if (PyInt_Check(value)) {
2056 if (PyInt_Check(value)) {
2054 long rev;
2057 long rev;
2055 if (!pylong_to_long(value, &rev)) {
2058 if (!pylong_to_long(value, &rev)) {
2056 return -1;
2059 return -1;
2057 }
2060 }
2058 return rev >= -1 && rev < index_length(self);
2061 return rev >= -1 && rev < index_length(self);
2059 }
2062 }
2060
2063
2061 if (node_check(value, &node) == -1)
2064 if (node_check(value, &node) == -1)
2062 return -1;
2065 return -1;
2063
2066
2064 switch (index_find_node(self, node, 20)) {
2067 switch (index_find_node(self, node, 20)) {
2065 case -3:
2068 case -3:
2066 return -1;
2069 return -1;
2067 case -2:
2070 case -2:
2068 return 0;
2071 return 0;
2069 default:
2072 default:
2070 return 1;
2073 return 1;
2071 }
2074 }
2072 }
2075 }
2073
2076
2074 static PyObject *index_m_has_node(indexObject *self, PyObject *args)
2077 static PyObject *index_m_has_node(indexObject *self, PyObject *args)
2075 {
2078 {
2076 int ret = index_contains(self, args);
2079 int ret = index_contains(self, args);
2077 if (ret < 0)
2080 if (ret < 0)
2078 return NULL;
2081 return NULL;
2079 return PyBool_FromLong((long)ret);
2082 return PyBool_FromLong((long)ret);
2080 }
2083 }
2081
2084
2082 static PyObject *index_m_rev(indexObject *self, PyObject *val)
2085 static PyObject *index_m_rev(indexObject *self, PyObject *val)
2083 {
2086 {
2084 char *node;
2087 char *node;
2085 int rev;
2088 int rev;
2086
2089
2087 if (node_check(val, &node) == -1)
2090 if (node_check(val, &node) == -1)
2088 return NULL;
2091 return NULL;
2089 rev = index_find_node(self, node, 20);
2092 rev = index_find_node(self, node, 20);
2090 if (rev >= -1)
2093 if (rev >= -1)
2091 return PyInt_FromLong(rev);
2094 return PyInt_FromLong(rev);
2092 if (rev == -2)
2095 if (rev == -2)
2093 raise_revlog_error();
2096 raise_revlog_error();
2094 return NULL;
2097 return NULL;
2095 }
2098 }
2096
2099
2097 typedef uint64_t bitmask;
2100 typedef uint64_t bitmask;
2098
2101
2099 /*
2102 /*
2100 * Given a disjoint set of revs, return all candidates for the
2103 * Given a disjoint set of revs, return all candidates for the
2101 * greatest common ancestor. In revset notation, this is the set
2104 * greatest common ancestor. In revset notation, this is the set
2102 * "heads(::a and ::b and ...)"
2105 * "heads(::a and ::b and ...)"
2103 */
2106 */
2104 static PyObject *find_gca_candidates(indexObject *self, const int *revs,
2107 static PyObject *find_gca_candidates(indexObject *self, const int *revs,
2105 int revcount)
2108 int revcount)
2106 {
2109 {
2107 const bitmask allseen = (1ull << revcount) - 1;
2110 const bitmask allseen = (1ull << revcount) - 1;
2108 const bitmask poison = 1ull << revcount;
2111 const bitmask poison = 1ull << revcount;
2109 PyObject *gca = PyList_New(0);
2112 PyObject *gca = PyList_New(0);
2110 int i, v, interesting;
2113 int i, v, interesting;
2111 int maxrev = -1;
2114 int maxrev = -1;
2112 bitmask sp;
2115 bitmask sp;
2113 bitmask *seen;
2116 bitmask *seen;
2114
2117
2115 if (gca == NULL)
2118 if (gca == NULL)
2116 return PyErr_NoMemory();
2119 return PyErr_NoMemory();
2117
2120
2118 for (i = 0; i < revcount; i++) {
2121 for (i = 0; i < revcount; i++) {
2119 if (revs[i] > maxrev)
2122 if (revs[i] > maxrev)
2120 maxrev = revs[i];
2123 maxrev = revs[i];
2121 }
2124 }
2122
2125
2123 seen = calloc(sizeof(*seen), maxrev + 1);
2126 seen = calloc(sizeof(*seen), maxrev + 1);
2124 if (seen == NULL) {
2127 if (seen == NULL) {
2125 Py_DECREF(gca);
2128 Py_DECREF(gca);
2126 return PyErr_NoMemory();
2129 return PyErr_NoMemory();
2127 }
2130 }
2128
2131
2129 for (i = 0; i < revcount; i++)
2132 for (i = 0; i < revcount; i++)
2130 seen[revs[i]] = 1ull << i;
2133 seen[revs[i]] = 1ull << i;
2131
2134
2132 interesting = revcount;
2135 interesting = revcount;
2133
2136
2134 for (v = maxrev; v >= 0 && interesting; v--) {
2137 for (v = maxrev; v >= 0 && interesting; v--) {
2135 bitmask sv = seen[v];
2138 bitmask sv = seen[v];
2136 int parents[2];
2139 int parents[2];
2137
2140
2138 if (!sv)
2141 if (!sv)
2139 continue;
2142 continue;
2140
2143
2141 if (sv < poison) {
2144 if (sv < poison) {
2142 interesting -= 1;
2145 interesting -= 1;
2143 if (sv == allseen) {
2146 if (sv == allseen) {
2144 PyObject *obj = PyInt_FromLong(v);
2147 PyObject *obj = PyInt_FromLong(v);
2145 if (obj == NULL)
2148 if (obj == NULL)
2146 goto bail;
2149 goto bail;
2147 if (PyList_Append(gca, obj) == -1) {
2150 if (PyList_Append(gca, obj) == -1) {
2148 Py_DECREF(obj);
2151 Py_DECREF(obj);
2149 goto bail;
2152 goto bail;
2150 }
2153 }
2151 sv |= poison;
2154 sv |= poison;
2152 for (i = 0; i < revcount; i++) {
2155 for (i = 0; i < revcount; i++) {
2153 if (revs[i] == v)
2156 if (revs[i] == v)
2154 goto done;
2157 goto done;
2155 }
2158 }
2156 }
2159 }
2157 }
2160 }
2158 if (index_get_parents(self, v, parents, maxrev) < 0)
2161 if (index_get_parents(self, v, parents, maxrev) < 0)
2159 goto bail;
2162 goto bail;
2160
2163
2161 for (i = 0; i < 2; i++) {
2164 for (i = 0; i < 2; i++) {
2162 int p = parents[i];
2165 int p = parents[i];
2163 if (p == -1)
2166 if (p == -1)
2164 continue;
2167 continue;
2165 sp = seen[p];
2168 sp = seen[p];
2166 if (sv < poison) {
2169 if (sv < poison) {
2167 if (sp == 0) {
2170 if (sp == 0) {
2168 seen[p] = sv;
2171 seen[p] = sv;
2169 interesting++;
2172 interesting++;
2170 } else if (sp != sv)
2173 } else if (sp != sv)
2171 seen[p] |= sv;
2174 seen[p] |= sv;
2172 } else {
2175 } else {
2173 if (sp && sp < poison)
2176 if (sp && sp < poison)
2174 interesting--;
2177 interesting--;
2175 seen[p] = sv;
2178 seen[p] = sv;
2176 }
2179 }
2177 }
2180 }
2178 }
2181 }
2179
2182
2180 done:
2183 done:
2181 free(seen);
2184 free(seen);
2182 return gca;
2185 return gca;
2183 bail:
2186 bail:
2184 free(seen);
2187 free(seen);
2185 Py_XDECREF(gca);
2188 Py_XDECREF(gca);
2186 return NULL;
2189 return NULL;
2187 }
2190 }
2188
2191
2189 /*
2192 /*
2190 * Given a disjoint set of revs, return the subset with the longest
2193 * Given a disjoint set of revs, return the subset with the longest
2191 * path to the root.
2194 * path to the root.
2192 */
2195 */
2193 static PyObject *find_deepest(indexObject *self, PyObject *revs)
2196 static PyObject *find_deepest(indexObject *self, PyObject *revs)
2194 {
2197 {
2195 const Py_ssize_t revcount = PyList_GET_SIZE(revs);
2198 const Py_ssize_t revcount = PyList_GET_SIZE(revs);
2196 static const Py_ssize_t capacity = 24;
2199 static const Py_ssize_t capacity = 24;
2197 int *depth, *interesting = NULL;
2200 int *depth, *interesting = NULL;
2198 int i, j, v, ninteresting;
2201 int i, j, v, ninteresting;
2199 PyObject *dict = NULL, *keys = NULL;
2202 PyObject *dict = NULL, *keys = NULL;
2200 long *seen = NULL;
2203 long *seen = NULL;
2201 int maxrev = -1;
2204 int maxrev = -1;
2202 long final;
2205 long final;
2203
2206
2204 if (revcount > capacity) {
2207 if (revcount > capacity) {
2205 PyErr_Format(PyExc_OverflowError,
2208 PyErr_Format(PyExc_OverflowError,
2206 "bitset size (%ld) > capacity (%ld)",
2209 "bitset size (%ld) > capacity (%ld)",
2207 (long)revcount, (long)capacity);
2210 (long)revcount, (long)capacity);
2208 return NULL;
2211 return NULL;
2209 }
2212 }
2210
2213
2211 for (i = 0; i < revcount; i++) {
2214 for (i = 0; i < revcount; i++) {
2212 int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
2215 int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
2213 if (n > maxrev)
2216 if (n > maxrev)
2214 maxrev = n;
2217 maxrev = n;
2215 }
2218 }
2216
2219
2217 depth = calloc(sizeof(*depth), maxrev + 1);
2220 depth = calloc(sizeof(*depth), maxrev + 1);
2218 if (depth == NULL)
2221 if (depth == NULL)
2219 return PyErr_NoMemory();
2222 return PyErr_NoMemory();
2220
2223
2221 seen = calloc(sizeof(*seen), maxrev + 1);
2224 seen = calloc(sizeof(*seen), maxrev + 1);
2222 if (seen == NULL) {
2225 if (seen == NULL) {
2223 PyErr_NoMemory();
2226 PyErr_NoMemory();
2224 goto bail;
2227 goto bail;
2225 }
2228 }
2226
2229
2227 interesting = calloc(sizeof(*interesting), ((size_t)1) << revcount);
2230 interesting = calloc(sizeof(*interesting), ((size_t)1) << revcount);
2228 if (interesting == NULL) {
2231 if (interesting == NULL) {
2229 PyErr_NoMemory();
2232 PyErr_NoMemory();
2230 goto bail;
2233 goto bail;
2231 }
2234 }
2232
2235
2233 if (PyList_Sort(revs) == -1)
2236 if (PyList_Sort(revs) == -1)
2234 goto bail;
2237 goto bail;
2235
2238
2236 for (i = 0; i < revcount; i++) {
2239 for (i = 0; i < revcount; i++) {
2237 int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
2240 int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
2238 long b = 1l << i;
2241 long b = 1l << i;
2239 depth[n] = 1;
2242 depth[n] = 1;
2240 seen[n] = b;
2243 seen[n] = b;
2241 interesting[b] = 1;
2244 interesting[b] = 1;
2242 }
2245 }
2243
2246
2244 /* invariant: ninteresting is the number of non-zero entries in
2247 /* invariant: ninteresting is the number of non-zero entries in
2245 * interesting. */
2248 * interesting. */
2246 ninteresting = (int)revcount;
2249 ninteresting = (int)revcount;
2247
2250
2248 for (v = maxrev; v >= 0 && ninteresting > 1; v--) {
2251 for (v = maxrev; v >= 0 && ninteresting > 1; v--) {
2249 int dv = depth[v];
2252 int dv = depth[v];
2250 int parents[2];
2253 int parents[2];
2251 long sv;
2254 long sv;
2252
2255
2253 if (dv == 0)
2256 if (dv == 0)
2254 continue;
2257 continue;
2255
2258
2256 sv = seen[v];
2259 sv = seen[v];
2257 if (index_get_parents(self, v, parents, maxrev) < 0)
2260 if (index_get_parents(self, v, parents, maxrev) < 0)
2258 goto bail;
2261 goto bail;
2259
2262
2260 for (i = 0; i < 2; i++) {
2263 for (i = 0; i < 2; i++) {
2261 int p = parents[i];
2264 int p = parents[i];
2262 long sp;
2265 long sp;
2263 int dp;
2266 int dp;
2264
2267
2265 if (p == -1)
2268 if (p == -1)
2266 continue;
2269 continue;
2267
2270
2268 dp = depth[p];
2271 dp = depth[p];
2269 sp = seen[p];
2272 sp = seen[p];
2270 if (dp <= dv) {
2273 if (dp <= dv) {
2271 depth[p] = dv + 1;
2274 depth[p] = dv + 1;
2272 if (sp != sv) {
2275 if (sp != sv) {
2273 interesting[sv] += 1;
2276 interesting[sv] += 1;
2274 seen[p] = sv;
2277 seen[p] = sv;
2275 if (sp) {
2278 if (sp) {
2276 interesting[sp] -= 1;
2279 interesting[sp] -= 1;
2277 if (interesting[sp] == 0)
2280 if (interesting[sp] == 0)
2278 ninteresting -= 1;
2281 ninteresting -= 1;
2279 }
2282 }
2280 }
2283 }
2281 } else if (dv == dp - 1) {
2284 } else if (dv == dp - 1) {
2282 long nsp = sp | sv;
2285 long nsp = sp | sv;
2283 if (nsp == sp)
2286 if (nsp == sp)
2284 continue;
2287 continue;
2285 seen[p] = nsp;
2288 seen[p] = nsp;
2286 interesting[sp] -= 1;
2289 interesting[sp] -= 1;
2287 if (interesting[sp] == 0)
2290 if (interesting[sp] == 0)
2288 ninteresting -= 1;
2291 ninteresting -= 1;
2289 if (interesting[nsp] == 0)
2292 if (interesting[nsp] == 0)
2290 ninteresting += 1;
2293 ninteresting += 1;
2291 interesting[nsp] += 1;
2294 interesting[nsp] += 1;
2292 }
2295 }
2293 }
2296 }
2294 interesting[sv] -= 1;
2297 interesting[sv] -= 1;
2295 if (interesting[sv] == 0)
2298 if (interesting[sv] == 0)
2296 ninteresting -= 1;
2299 ninteresting -= 1;
2297 }
2300 }
2298
2301
2299 final = 0;
2302 final = 0;
2300 j = ninteresting;
2303 j = ninteresting;
2301 for (i = 0; i < (int)(2 << revcount) && j > 0; i++) {
2304 for (i = 0; i < (int)(2 << revcount) && j > 0; i++) {
2302 if (interesting[i] == 0)
2305 if (interesting[i] == 0)
2303 continue;
2306 continue;
2304 final |= i;
2307 final |= i;
2305 j -= 1;
2308 j -= 1;
2306 }
2309 }
2307 if (final == 0) {
2310 if (final == 0) {
2308 keys = PyList_New(0);
2311 keys = PyList_New(0);
2309 goto bail;
2312 goto bail;
2310 }
2313 }
2311
2314
2312 dict = PyDict_New();
2315 dict = PyDict_New();
2313 if (dict == NULL)
2316 if (dict == NULL)
2314 goto bail;
2317 goto bail;
2315
2318
2316 for (i = 0; i < revcount; i++) {
2319 for (i = 0; i < revcount; i++) {
2317 PyObject *key;
2320 PyObject *key;
2318
2321
2319 if ((final & (1 << i)) == 0)
2322 if ((final & (1 << i)) == 0)
2320 continue;
2323 continue;
2321
2324
2322 key = PyList_GET_ITEM(revs, i);
2325 key = PyList_GET_ITEM(revs, i);
2323 Py_INCREF(key);
2326 Py_INCREF(key);
2324 Py_INCREF(Py_None);
2327 Py_INCREF(Py_None);
2325 if (PyDict_SetItem(dict, key, Py_None) == -1) {
2328 if (PyDict_SetItem(dict, key, Py_None) == -1) {
2326 Py_DECREF(key);
2329 Py_DECREF(key);
2327 Py_DECREF(Py_None);
2330 Py_DECREF(Py_None);
2328 goto bail;
2331 goto bail;
2329 }
2332 }
2330 }
2333 }
2331
2334
2332 keys = PyDict_Keys(dict);
2335 keys = PyDict_Keys(dict);
2333
2336
2334 bail:
2337 bail:
2335 free(depth);
2338 free(depth);
2336 free(seen);
2339 free(seen);
2337 free(interesting);
2340 free(interesting);
2338 Py_XDECREF(dict);
2341 Py_XDECREF(dict);
2339
2342
2340 return keys;
2343 return keys;
2341 }
2344 }
2342
2345
2343 /*
2346 /*
2344 * Given a (possibly overlapping) set of revs, return all the
2347 * Given a (possibly overlapping) set of revs, return all the
2345 * common ancestors heads: heads(::args[0] and ::a[1] and ...)
2348 * common ancestors heads: heads(::args[0] and ::a[1] and ...)
2346 */
2349 */
2347 static PyObject *index_commonancestorsheads(indexObject *self, PyObject *args)
2350 static PyObject *index_commonancestorsheads(indexObject *self, PyObject *args)
2348 {
2351 {
2349 PyObject *ret = NULL;
2352 PyObject *ret = NULL;
2350 Py_ssize_t argcount, i, len;
2353 Py_ssize_t argcount, i, len;
2351 bitmask repeat = 0;
2354 bitmask repeat = 0;
2352 int revcount = 0;
2355 int revcount = 0;
2353 int *revs;
2356 int *revs;
2354
2357
2355 argcount = PySequence_Length(args);
2358 argcount = PySequence_Length(args);
2356 revs = PyMem_Malloc(argcount * sizeof(*revs));
2359 revs = PyMem_Malloc(argcount * sizeof(*revs));
2357 if (argcount > 0 && revs == NULL)
2360 if (argcount > 0 && revs == NULL)
2358 return PyErr_NoMemory();
2361 return PyErr_NoMemory();
2359 len = index_length(self);
2362 len = index_length(self);
2360
2363
2361 for (i = 0; i < argcount; i++) {
2364 for (i = 0; i < argcount; i++) {
2362 static const int capacity = 24;
2365 static const int capacity = 24;
2363 PyObject *obj = PySequence_GetItem(args, i);
2366 PyObject *obj = PySequence_GetItem(args, i);
2364 bitmask x;
2367 bitmask x;
2365 long val;
2368 long val;
2366
2369
2367 if (!PyInt_Check(obj)) {
2370 if (!PyInt_Check(obj)) {
2368 PyErr_SetString(PyExc_TypeError,
2371 PyErr_SetString(PyExc_TypeError,
2369 "arguments must all be ints");
2372 "arguments must all be ints");
2370 Py_DECREF(obj);
2373 Py_DECREF(obj);
2371 goto bail;
2374 goto bail;
2372 }
2375 }
2373 val = PyInt_AsLong(obj);
2376 val = PyInt_AsLong(obj);
2374 Py_DECREF(obj);
2377 Py_DECREF(obj);
2375 if (val == -1) {
2378 if (val == -1) {
2376 ret = PyList_New(0);
2379 ret = PyList_New(0);
2377 goto done;
2380 goto done;
2378 }
2381 }
2379 if (val < 0 || val >= len) {
2382 if (val < 0 || val >= len) {
2380 PyErr_SetString(PyExc_IndexError, "index out of range");
2383 PyErr_SetString(PyExc_IndexError, "index out of range");
2381 goto bail;
2384 goto bail;
2382 }
2385 }
2383 /* this cheesy bloom filter lets us avoid some more
2386 /* this cheesy bloom filter lets us avoid some more
2384 * expensive duplicate checks in the common set-is-disjoint
2387 * expensive duplicate checks in the common set-is-disjoint
2385 * case */
2388 * case */
2386 x = 1ull << (val & 0x3f);
2389 x = 1ull << (val & 0x3f);
2387 if (repeat & x) {
2390 if (repeat & x) {
2388 int k;
2391 int k;
2389 for (k = 0; k < revcount; k++) {
2392 for (k = 0; k < revcount; k++) {
2390 if (val == revs[k])
2393 if (val == revs[k])
2391 goto duplicate;
2394 goto duplicate;
2392 }
2395 }
2393 } else
2396 } else
2394 repeat |= x;
2397 repeat |= x;
2395 if (revcount >= capacity) {
2398 if (revcount >= capacity) {
2396 PyErr_Format(PyExc_OverflowError,
2399 PyErr_Format(PyExc_OverflowError,
2397 "bitset size (%d) > capacity (%d)",
2400 "bitset size (%d) > capacity (%d)",
2398 revcount, capacity);
2401 revcount, capacity);
2399 goto bail;
2402 goto bail;
2400 }
2403 }
2401 revs[revcount++] = (int)val;
2404 revs[revcount++] = (int)val;
2402 duplicate:;
2405 duplicate:;
2403 }
2406 }
2404
2407
2405 if (revcount == 0) {
2408 if (revcount == 0) {
2406 ret = PyList_New(0);
2409 ret = PyList_New(0);
2407 goto done;
2410 goto done;
2408 }
2411 }
2409 if (revcount == 1) {
2412 if (revcount == 1) {
2410 PyObject *obj;
2413 PyObject *obj;
2411 ret = PyList_New(1);
2414 ret = PyList_New(1);
2412 if (ret == NULL)
2415 if (ret == NULL)
2413 goto bail;
2416 goto bail;
2414 obj = PyInt_FromLong(revs[0]);
2417 obj = PyInt_FromLong(revs[0]);
2415 if (obj == NULL)
2418 if (obj == NULL)
2416 goto bail;
2419 goto bail;
2417 PyList_SET_ITEM(ret, 0, obj);
2420 PyList_SET_ITEM(ret, 0, obj);
2418 goto done;
2421 goto done;
2419 }
2422 }
2420
2423
2421 ret = find_gca_candidates(self, revs, revcount);
2424 ret = find_gca_candidates(self, revs, revcount);
2422 if (ret == NULL)
2425 if (ret == NULL)
2423 goto bail;
2426 goto bail;
2424
2427
2425 done:
2428 done:
2426 PyMem_Free(revs);
2429 PyMem_Free(revs);
2427 return ret;
2430 return ret;
2428
2431
2429 bail:
2432 bail:
2430 PyMem_Free(revs);
2433 PyMem_Free(revs);
2431 Py_XDECREF(ret);
2434 Py_XDECREF(ret);
2432 return NULL;
2435 return NULL;
2433 }
2436 }
2434
2437
2435 /*
2438 /*
2436 * Given a (possibly overlapping) set of revs, return the greatest
2439 * Given a (possibly overlapping) set of revs, return the greatest
2437 * common ancestors: those with the longest path to the root.
2440 * common ancestors: those with the longest path to the root.
2438 */
2441 */
2439 static PyObject *index_ancestors(indexObject *self, PyObject *args)
2442 static PyObject *index_ancestors(indexObject *self, PyObject *args)
2440 {
2443 {
2441 PyObject *ret;
2444 PyObject *ret;
2442 PyObject *gca = index_commonancestorsheads(self, args);
2445 PyObject *gca = index_commonancestorsheads(self, args);
2443 if (gca == NULL)
2446 if (gca == NULL)
2444 return NULL;
2447 return NULL;
2445
2448
2446 if (PyList_GET_SIZE(gca) <= 1) {
2449 if (PyList_GET_SIZE(gca) <= 1) {
2447 return gca;
2450 return gca;
2448 }
2451 }
2449
2452
2450 ret = find_deepest(self, gca);
2453 ret = find_deepest(self, gca);
2451 Py_DECREF(gca);
2454 Py_DECREF(gca);
2452 return ret;
2455 return ret;
2453 }
2456 }
2454
2457
2455 /*
2458 /*
2456 * Invalidate any trie entries introduced by added revs.
2459 * Invalidate any trie entries introduced by added revs.
2457 */
2460 */
2458 static void index_invalidate_added(indexObject *self, Py_ssize_t start)
2461 static void index_invalidate_added(indexObject *self, Py_ssize_t start)
2459 {
2462 {
2460 Py_ssize_t i, len = PyList_GET_SIZE(self->added);
2463 Py_ssize_t i, len = PyList_GET_SIZE(self->added);
2461
2464
2462 for (i = start; i < len; i++) {
2465 for (i = start; i < len; i++) {
2463 PyObject *tuple = PyList_GET_ITEM(self->added, i);
2466 PyObject *tuple = PyList_GET_ITEM(self->added, i);
2464 PyObject *node = PyTuple_GET_ITEM(tuple, 7);
2467 PyObject *node = PyTuple_GET_ITEM(tuple, 7);
2465
2468
2466 nt_delete_node(&self->nt, PyBytes_AS_STRING(node));
2469 nt_delete_node(&self->nt, PyBytes_AS_STRING(node));
2467 }
2470 }
2468
2471
2469 if (start == 0)
2472 if (start == 0)
2470 Py_CLEAR(self->added);
2473 Py_CLEAR(self->added);
2471 }
2474 }
2472
2475
2473 /*
2476 /*
2474 * Delete a numeric range of revs, which must be at the end of the
2477 * Delete a numeric range of revs, which must be at the end of the
2475 * range.
2478 * range.
2476 */
2479 */
2477 static int index_slice_del(indexObject *self, PyObject *item)
2480 static int index_slice_del(indexObject *self, PyObject *item)
2478 {
2481 {
2479 Py_ssize_t start, stop, step, slicelength;
2482 Py_ssize_t start, stop, step, slicelength;
2480 Py_ssize_t length = index_length(self) + 1;
2483 Py_ssize_t length = index_length(self) + 1;
2481 int ret = 0;
2484 int ret = 0;
2482
2485
2483 /* Argument changed from PySliceObject* to PyObject* in Python 3. */
2486 /* Argument changed from PySliceObject* to PyObject* in Python 3. */
2484 #ifdef IS_PY3K
2487 #ifdef IS_PY3K
2485 if (PySlice_GetIndicesEx(item, length, &start, &stop, &step,
2488 if (PySlice_GetIndicesEx(item, length, &start, &stop, &step,
2486 &slicelength) < 0)
2489 &slicelength) < 0)
2487 #else
2490 #else
2488 if (PySlice_GetIndicesEx((PySliceObject *)item, length, &start, &stop,
2491 if (PySlice_GetIndicesEx((PySliceObject *)item, length, &start, &stop,
2489 &step, &slicelength) < 0)
2492 &step, &slicelength) < 0)
2490 #endif
2493 #endif
2491 return -1;
2494 return -1;
2492
2495
2493 if (slicelength <= 0)
2496 if (slicelength <= 0)
2494 return 0;
2497 return 0;
2495
2498
2496 if ((step < 0 && start < stop) || (step > 0 && start > stop))
2499 if ((step < 0 && start < stop) || (step > 0 && start > stop))
2497 stop = start;
2500 stop = start;
2498
2501
2499 if (step < 0) {
2502 if (step < 0) {
2500 stop = start + 1;
2503 stop = start + 1;
2501 start = stop + step * (slicelength - 1) - 1;
2504 start = stop + step * (slicelength - 1) - 1;
2502 step = -step;
2505 step = -step;
2503 }
2506 }
2504
2507
2505 if (step != 1) {
2508 if (step != 1) {
2506 PyErr_SetString(PyExc_ValueError,
2509 PyErr_SetString(PyExc_ValueError,
2507 "revlog index delete requires step size of 1");
2510 "revlog index delete requires step size of 1");
2508 return -1;
2511 return -1;
2509 }
2512 }
2510
2513
2511 if (stop != length - 1) {
2514 if (stop != length - 1) {
2512 PyErr_SetString(PyExc_IndexError,
2515 PyErr_SetString(PyExc_IndexError,
2513 "revlog index deletion indices are invalid");
2516 "revlog index deletion indices are invalid");
2514 return -1;
2517 return -1;
2515 }
2518 }
2516
2519
2517 if (start < self->length) {
2520 if (start < self->length) {
2518 if (self->ntinitialized) {
2521 if (self->ntinitialized) {
2519 Py_ssize_t i;
2522 Py_ssize_t i;
2520
2523
2521 for (i = start; i < self->length; i++) {
2524 for (i = start; i < self->length; i++) {
2522 const char *node = index_node_existing(self, i);
2525 const char *node = index_node_existing(self, i);
2523 if (node == NULL)
2526 if (node == NULL)
2524 return -1;
2527 return -1;
2525
2528
2526 nt_delete_node(&self->nt, node);
2529 nt_delete_node(&self->nt, node);
2527 }
2530 }
2528 if (self->added)
2531 if (self->added)
2529 index_invalidate_added(self, 0);
2532 index_invalidate_added(self, 0);
2530 if (self->ntrev > start)
2533 if (self->ntrev > start)
2531 self->ntrev = (int)start;
2534 self->ntrev = (int)start;
2532 } else if (self->added) {
2535 } else if (self->added) {
2533 Py_CLEAR(self->added);
2536 Py_CLEAR(self->added);
2534 }
2537 }
2535
2538
2536 self->length = start;
2539 self->length = start;
2537 if (start < self->raw_length) {
2540 if (start < self->raw_length) {
2538 if (self->cache) {
2541 if (self->cache) {
2539 Py_ssize_t i;
2542 Py_ssize_t i;
2540 for (i = start; i < self->raw_length; i++)
2543 for (i = start; i < self->raw_length; i++)
2541 Py_CLEAR(self->cache[i]);
2544 Py_CLEAR(self->cache[i]);
2542 }
2545 }
2543 self->raw_length = start;
2546 self->raw_length = start;
2544 }
2547 }
2545 goto done;
2548 goto done;
2546 }
2549 }
2547
2550
2548 if (self->ntinitialized) {
2551 if (self->ntinitialized) {
2549 index_invalidate_added(self, start - self->length);
2552 index_invalidate_added(self, start - self->length);
2550 if (self->ntrev > start)
2553 if (self->ntrev > start)
2551 self->ntrev = (int)start;
2554 self->ntrev = (int)start;
2552 }
2555 }
2553 if (self->added)
2556 if (self->added)
2554 ret = PyList_SetSlice(self->added, start - self->length,
2557 ret = PyList_SetSlice(self->added, start - self->length,
2555 PyList_GET_SIZE(self->added), NULL);
2558 PyList_GET_SIZE(self->added), NULL);
2556 done:
2559 done:
2557 Py_CLEAR(self->headrevs);
2560 Py_CLEAR(self->headrevs);
2558 return ret;
2561 return ret;
2559 }
2562 }
2560
2563
2561 /*
2564 /*
2562 * Supported ops:
2565 * Supported ops:
2563 *
2566 *
2564 * slice deletion
2567 * slice deletion
2565 * string assignment (extend node->rev mapping)
2568 * string assignment (extend node->rev mapping)
2566 * string deletion (shrink node->rev mapping)
2569 * string deletion (shrink node->rev mapping)
2567 */
2570 */
2568 static int index_assign_subscript(indexObject *self, PyObject *item,
2571 static int index_assign_subscript(indexObject *self, PyObject *item,
2569 PyObject *value)
2572 PyObject *value)
2570 {
2573 {
2571 char *node;
2574 char *node;
2572 long rev;
2575 long rev;
2573
2576
2574 if (PySlice_Check(item) && value == NULL)
2577 if (PySlice_Check(item) && value == NULL)
2575 return index_slice_del(self, item);
2578 return index_slice_del(self, item);
2576
2579
2577 if (node_check(item, &node) == -1)
2580 if (node_check(item, &node) == -1)
2578 return -1;
2581 return -1;
2579
2582
2580 if (value == NULL)
2583 if (value == NULL)
2581 return self->ntinitialized ? nt_delete_node(&self->nt, node)
2584 return self->ntinitialized ? nt_delete_node(&self->nt, node)
2582 : 0;
2585 : 0;
2583 rev = PyInt_AsLong(value);
2586 rev = PyInt_AsLong(value);
2584 if (rev > INT_MAX || rev < 0) {
2587 if (rev > INT_MAX || rev < 0) {
2585 if (!PyErr_Occurred())
2588 if (!PyErr_Occurred())
2586 PyErr_SetString(PyExc_ValueError, "rev out of range");
2589 PyErr_SetString(PyExc_ValueError, "rev out of range");
2587 return -1;
2590 return -1;
2588 }
2591 }
2589
2592
2590 if (index_init_nt(self) == -1)
2593 if (index_init_nt(self) == -1)
2591 return -1;
2594 return -1;
2592 return nt_insert(&self->nt, node, (int)rev);
2595 return nt_insert(&self->nt, node, (int)rev);
2593 }
2596 }
2594
2597
2595 /*
2598 /*
2596 * Find all RevlogNG entries in an index that has inline data. Update
2599 * Find all RevlogNG entries in an index that has inline data. Update
2597 * the optional "offsets" table with those entries.
2600 * the optional "offsets" table with those entries.
2598 */
2601 */
2599 static Py_ssize_t inline_scan(indexObject *self, const char **offsets)
2602 static Py_ssize_t inline_scan(indexObject *self, const char **offsets)
2600 {
2603 {
2601 const char *data = (const char *)self->buf.buf;
2604 const char *data = (const char *)self->buf.buf;
2602 Py_ssize_t pos = 0;
2605 Py_ssize_t pos = 0;
2603 Py_ssize_t end = self->buf.len;
2606 Py_ssize_t end = self->buf.len;
2604 long incr = v1_hdrsize;
2607 long incr = v1_hdrsize;
2605 Py_ssize_t len = 0;
2608 Py_ssize_t len = 0;
2606
2609
2607 while (pos + v1_hdrsize <= end && pos >= 0) {
2610 while (pos + v1_hdrsize <= end && pos >= 0) {
2608 uint32_t comp_len;
2611 uint32_t comp_len;
2609 /* 3rd element of header is length of compressed inline data */
2612 /* 3rd element of header is length of compressed inline data */
2610 comp_len = getbe32(data + pos + 8);
2613 comp_len = getbe32(data + pos + 8);
2611 incr = v1_hdrsize + comp_len;
2614 incr = v1_hdrsize + comp_len;
2612 if (offsets)
2615 if (offsets)
2613 offsets[len] = data + pos;
2616 offsets[len] = data + pos;
2614 len++;
2617 len++;
2615 pos += incr;
2618 pos += incr;
2616 }
2619 }
2617
2620
2618 if (pos != end) {
2621 if (pos != end) {
2619 if (!PyErr_Occurred())
2622 if (!PyErr_Occurred())
2620 PyErr_SetString(PyExc_ValueError, "corrupt index file");
2623 PyErr_SetString(PyExc_ValueError, "corrupt index file");
2621 return -1;
2624 return -1;
2622 }
2625 }
2623
2626
2624 return len;
2627 return len;
2625 }
2628 }
2626
2629
2627 static int index_init(indexObject *self, PyObject *args)
2630 static int index_init(indexObject *self, PyObject *args)
2628 {
2631 {
2629 PyObject *data_obj, *inlined_obj;
2632 PyObject *data_obj, *inlined_obj;
2630 Py_ssize_t size;
2633 Py_ssize_t size;
2631
2634
2632 /* Initialize before argument-checking to avoid index_dealloc() crash.
2635 /* Initialize before argument-checking to avoid index_dealloc() crash.
2633 */
2636 */
2634 self->raw_length = 0;
2637 self->raw_length = 0;
2635 self->added = NULL;
2638 self->added = NULL;
2636 self->cache = NULL;
2639 self->cache = NULL;
2637 self->data = NULL;
2640 self->data = NULL;
2638 memset(&self->buf, 0, sizeof(self->buf));
2641 memset(&self->buf, 0, sizeof(self->buf));
2639 self->headrevs = NULL;
2642 self->headrevs = NULL;
2640 self->filteredrevs = Py_None;
2643 self->filteredrevs = Py_None;
2641 Py_INCREF(Py_None);
2644 Py_INCREF(Py_None);
2642 self->ntinitialized = 0;
2645 self->ntinitialized = 0;
2643 self->offsets = NULL;
2646 self->offsets = NULL;
2644
2647
2645 if (!PyArg_ParseTuple(args, "OO", &data_obj, &inlined_obj))
2648 if (!PyArg_ParseTuple(args, "OO", &data_obj, &inlined_obj))
2646 return -1;
2649 return -1;
2647 if (!PyObject_CheckBuffer(data_obj)) {
2650 if (!PyObject_CheckBuffer(data_obj)) {
2648 PyErr_SetString(PyExc_TypeError,
2651 PyErr_SetString(PyExc_TypeError,
2649 "data does not support buffer interface");
2652 "data does not support buffer interface");
2650 return -1;
2653 return -1;
2651 }
2654 }
2652
2655
2653 if (PyObject_GetBuffer(data_obj, &self->buf, PyBUF_SIMPLE) == -1)
2656 if (PyObject_GetBuffer(data_obj, &self->buf, PyBUF_SIMPLE) == -1)
2654 return -1;
2657 return -1;
2655 size = self->buf.len;
2658 size = self->buf.len;
2656
2659
2657 self->inlined = inlined_obj && PyObject_IsTrue(inlined_obj);
2660 self->inlined = inlined_obj && PyObject_IsTrue(inlined_obj);
2658 self->data = data_obj;
2661 self->data = data_obj;
2659
2662
2660 self->ntlookups = self->ntmisses = 0;
2663 self->ntlookups = self->ntmisses = 0;
2661 self->ntrev = -1;
2664 self->ntrev = -1;
2662 Py_INCREF(self->data);
2665 Py_INCREF(self->data);
2663
2666
2664 if (self->inlined) {
2667 if (self->inlined) {
2665 Py_ssize_t len = inline_scan(self, NULL);
2668 Py_ssize_t len = inline_scan(self, NULL);
2666 if (len == -1)
2669 if (len == -1)
2667 goto bail;
2670 goto bail;
2668 self->raw_length = len;
2671 self->raw_length = len;
2669 self->length = len;
2672 self->length = len;
2670 } else {
2673 } else {
2671 if (size % v1_hdrsize) {
2674 if (size % v1_hdrsize) {
2672 PyErr_SetString(PyExc_ValueError, "corrupt index file");
2675 PyErr_SetString(PyExc_ValueError, "corrupt index file");
2673 goto bail;
2676 goto bail;
2674 }
2677 }
2675 self->raw_length = size / v1_hdrsize;
2678 self->raw_length = size / v1_hdrsize;
2676 self->length = self->raw_length;
2679 self->length = self->raw_length;
2677 }
2680 }
2678
2681
2679 return 0;
2682 return 0;
2680 bail:
2683 bail:
2681 return -1;
2684 return -1;
2682 }
2685 }
2683
2686
2684 static PyObject *index_nodemap(indexObject *self)
2687 static PyObject *index_nodemap(indexObject *self)
2685 {
2688 {
2686 Py_INCREF(self);
2689 Py_INCREF(self);
2687 return (PyObject *)self;
2690 return (PyObject *)self;
2688 }
2691 }
2689
2692
2690 static void _index_clearcaches(indexObject *self)
2693 static void _index_clearcaches(indexObject *self)
2691 {
2694 {
2692 if (self->cache) {
2695 if (self->cache) {
2693 Py_ssize_t i;
2696 Py_ssize_t i;
2694
2697
2695 for (i = 0; i < self->raw_length; i++)
2698 for (i = 0; i < self->raw_length; i++)
2696 Py_CLEAR(self->cache[i]);
2699 Py_CLEAR(self->cache[i]);
2697 free(self->cache);
2700 free(self->cache);
2698 self->cache = NULL;
2701 self->cache = NULL;
2699 }
2702 }
2700 if (self->offsets) {
2703 if (self->offsets) {
2701 PyMem_Free((void *)self->offsets);
2704 PyMem_Free((void *)self->offsets);
2702 self->offsets = NULL;
2705 self->offsets = NULL;
2703 }
2706 }
2704 if (self->ntinitialized) {
2707 if (self->ntinitialized) {
2705 nt_dealloc(&self->nt);
2708 nt_dealloc(&self->nt);
2706 }
2709 }
2707 self->ntinitialized = 0;
2710 self->ntinitialized = 0;
2708 Py_CLEAR(self->headrevs);
2711 Py_CLEAR(self->headrevs);
2709 }
2712 }
2710
2713
2711 static PyObject *index_clearcaches(indexObject *self)
2714 static PyObject *index_clearcaches(indexObject *self)
2712 {
2715 {
2713 _index_clearcaches(self);
2716 _index_clearcaches(self);
2714 self->ntrev = -1;
2717 self->ntrev = -1;
2715 self->ntlookups = self->ntmisses = 0;
2718 self->ntlookups = self->ntmisses = 0;
2716 Py_RETURN_NONE;
2719 Py_RETURN_NONE;
2717 }
2720 }
2718
2721
2719 static void index_dealloc(indexObject *self)
2722 static void index_dealloc(indexObject *self)
2720 {
2723 {
2721 _index_clearcaches(self);
2724 _index_clearcaches(self);
2722 Py_XDECREF(self->filteredrevs);
2725 Py_XDECREF(self->filteredrevs);
2723 if (self->buf.buf) {
2726 if (self->buf.buf) {
2724 PyBuffer_Release(&self->buf);
2727 PyBuffer_Release(&self->buf);
2725 memset(&self->buf, 0, sizeof(self->buf));
2728 memset(&self->buf, 0, sizeof(self->buf));
2726 }
2729 }
2727 Py_XDECREF(self->data);
2730 Py_XDECREF(self->data);
2728 Py_XDECREF(self->added);
2731 Py_XDECREF(self->added);
2729 PyObject_Del(self);
2732 PyObject_Del(self);
2730 }
2733 }
2731
2734
2732 static PySequenceMethods index_sequence_methods = {
2735 static PySequenceMethods index_sequence_methods = {
2733 (lenfunc)index_length, /* sq_length */
2736 (lenfunc)index_length, /* sq_length */
2734 0, /* sq_concat */
2737 0, /* sq_concat */
2735 0, /* sq_repeat */
2738 0, /* sq_repeat */
2736 (ssizeargfunc)index_get, /* sq_item */
2739 (ssizeargfunc)index_get, /* sq_item */
2737 0, /* sq_slice */
2740 0, /* sq_slice */
2738 0, /* sq_ass_item */
2741 0, /* sq_ass_item */
2739 0, /* sq_ass_slice */
2742 0, /* sq_ass_slice */
2740 (objobjproc)index_contains, /* sq_contains */
2743 (objobjproc)index_contains, /* sq_contains */
2741 };
2744 };
2742
2745
2743 static PyMappingMethods index_mapping_methods = {
2746 static PyMappingMethods index_mapping_methods = {
2744 (lenfunc)index_length, /* mp_length */
2747 (lenfunc)index_length, /* mp_length */
2745 (binaryfunc)index_getitem, /* mp_subscript */
2748 (binaryfunc)index_getitem, /* mp_subscript */
2746 (objobjargproc)index_assign_subscript, /* mp_ass_subscript */
2749 (objobjargproc)index_assign_subscript, /* mp_ass_subscript */
2747 };
2750 };
2748
2751
2749 static PyMethodDef index_methods[] = {
2752 static PyMethodDef index_methods[] = {
2750 {"ancestors", (PyCFunction)index_ancestors, METH_VARARGS,
2753 {"ancestors", (PyCFunction)index_ancestors, METH_VARARGS,
2751 "return the gca set of the given revs"},
2754 "return the gca set of the given revs"},
2752 {"commonancestorsheads", (PyCFunction)index_commonancestorsheads,
2755 {"commonancestorsheads", (PyCFunction)index_commonancestorsheads,
2753 METH_VARARGS,
2756 METH_VARARGS,
2754 "return the heads of the common ancestors of the given revs"},
2757 "return the heads of the common ancestors of the given revs"},
2755 {"clearcaches", (PyCFunction)index_clearcaches, METH_NOARGS,
2758 {"clearcaches", (PyCFunction)index_clearcaches, METH_NOARGS,
2756 "clear the index caches"},
2759 "clear the index caches"},
2757 {"get", (PyCFunction)index_m_get, METH_VARARGS, "get an index entry"},
2760 {"get", (PyCFunction)index_m_get, METH_VARARGS, "get an index entry"},
2758 {"get_rev", (PyCFunction)index_m_get, METH_VARARGS,
2761 {"get_rev", (PyCFunction)index_m_get, METH_VARARGS,
2759 "return `rev` associated with a node or None"},
2762 "return `rev` associated with a node or None"},
2760 {"has_node", (PyCFunction)index_m_has_node, METH_O,
2763 {"has_node", (PyCFunction)index_m_has_node, METH_O,
2761 "return True if the node exist in the index"},
2764 "return True if the node exist in the index"},
2762 {"rev", (PyCFunction)index_m_rev, METH_O,
2765 {"rev", (PyCFunction)index_m_rev, METH_O,
2763 "return `rev` associated with a node or raise RevlogError"},
2766 "return `rev` associated with a node or raise RevlogError"},
2764 {"computephasesmapsets", (PyCFunction)compute_phases_map_sets, METH_VARARGS,
2767 {"computephasesmapsets", (PyCFunction)compute_phases_map_sets, METH_VARARGS,
2765 "compute phases"},
2768 "compute phases"},
2766 {"reachableroots2", (PyCFunction)reachableroots2, METH_VARARGS,
2769 {"reachableroots2", (PyCFunction)reachableroots2, METH_VARARGS,
2767 "reachableroots"},
2770 "reachableroots"},
2768 {"headrevs", (PyCFunction)index_headrevs, METH_VARARGS,
2771 {"headrevs", (PyCFunction)index_headrevs, METH_VARARGS,
2769 "get head revisions"}, /* Can do filtering since 3.2 */
2772 "get head revisions"}, /* Can do filtering since 3.2 */
2770 {"headrevsfiltered", (PyCFunction)index_headrevs, METH_VARARGS,
2773 {"headrevsfiltered", (PyCFunction)index_headrevs, METH_VARARGS,
2771 "get filtered head revisions"}, /* Can always do filtering */
2774 "get filtered head revisions"}, /* Can always do filtering */
2772 {"issnapshot", (PyCFunction)index_issnapshot, METH_O,
2775 {"issnapshot", (PyCFunction)index_issnapshot, METH_O,
2773 "True if the object is a snapshot"},
2776 "True if the object is a snapshot"},
2774 {"findsnapshots", (PyCFunction)index_findsnapshots, METH_VARARGS,
2777 {"findsnapshots", (PyCFunction)index_findsnapshots, METH_VARARGS,
2775 "Gather snapshot data in a cache dict"},
2778 "Gather snapshot data in a cache dict"},
2776 {"deltachain", (PyCFunction)index_deltachain, METH_VARARGS,
2779 {"deltachain", (PyCFunction)index_deltachain, METH_VARARGS,
2777 "determine revisions with deltas to reconstruct fulltext"},
2780 "determine revisions with deltas to reconstruct fulltext"},
2778 {"slicechunktodensity", (PyCFunction)index_slicechunktodensity,
2781 {"slicechunktodensity", (PyCFunction)index_slicechunktodensity,
2779 METH_VARARGS, "determine revisions with deltas to reconstruct fulltext"},
2782 METH_VARARGS, "determine revisions with deltas to reconstruct fulltext"},
2780 {"append", (PyCFunction)index_append, METH_O, "append an index entry"},
2783 {"append", (PyCFunction)index_append, METH_O, "append an index entry"},
2781 {"partialmatch", (PyCFunction)index_partialmatch, METH_VARARGS,
2784 {"partialmatch", (PyCFunction)index_partialmatch, METH_VARARGS,
2782 "match a potentially ambiguous node ID"},
2785 "match a potentially ambiguous node ID"},
2783 {"shortest", (PyCFunction)index_shortest, METH_VARARGS,
2786 {"shortest", (PyCFunction)index_shortest, METH_VARARGS,
2784 "find length of shortest hex nodeid of a binary ID"},
2787 "find length of shortest hex nodeid of a binary ID"},
2785 {"stats", (PyCFunction)index_stats, METH_NOARGS, "stats for the index"},
2788 {"stats", (PyCFunction)index_stats, METH_NOARGS, "stats for the index"},
2786 {NULL} /* Sentinel */
2789 {NULL} /* Sentinel */
2787 };
2790 };
2788
2791
2789 static PyGetSetDef index_getset[] = {
2792 static PyGetSetDef index_getset[] = {
2790 {"nodemap", (getter)index_nodemap, NULL, "nodemap", NULL},
2793 {"nodemap", (getter)index_nodemap, NULL, "nodemap", NULL},
2791 {NULL} /* Sentinel */
2794 {NULL} /* Sentinel */
2792 };
2795 };
2793
2796
2794 PyTypeObject HgRevlogIndex_Type = {
2797 PyTypeObject HgRevlogIndex_Type = {
2795 PyVarObject_HEAD_INIT(NULL, 0) /* header */
2798 PyVarObject_HEAD_INIT(NULL, 0) /* header */
2796 "parsers.index", /* tp_name */
2799 "parsers.index", /* tp_name */
2797 sizeof(indexObject), /* tp_basicsize */
2800 sizeof(indexObject), /* tp_basicsize */
2798 0, /* tp_itemsize */
2801 0, /* tp_itemsize */
2799 (destructor)index_dealloc, /* tp_dealloc */
2802 (destructor)index_dealloc, /* tp_dealloc */
2800 0, /* tp_print */
2803 0, /* tp_print */
2801 0, /* tp_getattr */
2804 0, /* tp_getattr */
2802 0, /* tp_setattr */
2805 0, /* tp_setattr */
2803 0, /* tp_compare */
2806 0, /* tp_compare */
2804 0, /* tp_repr */
2807 0, /* tp_repr */
2805 0, /* tp_as_number */
2808 0, /* tp_as_number */
2806 &index_sequence_methods, /* tp_as_sequence */
2809 &index_sequence_methods, /* tp_as_sequence */
2807 &index_mapping_methods, /* tp_as_mapping */
2810 &index_mapping_methods, /* tp_as_mapping */
2808 0, /* tp_hash */
2811 0, /* tp_hash */
2809 0, /* tp_call */
2812 0, /* tp_call */
2810 0, /* tp_str */
2813 0, /* tp_str */
2811 0, /* tp_getattro */
2814 0, /* tp_getattro */
2812 0, /* tp_setattro */
2815 0, /* tp_setattro */
2813 0, /* tp_as_buffer */
2816 0, /* tp_as_buffer */
2814 Py_TPFLAGS_DEFAULT, /* tp_flags */
2817 Py_TPFLAGS_DEFAULT, /* tp_flags */
2815 "revlog index", /* tp_doc */
2818 "revlog index", /* tp_doc */
2816 0, /* tp_traverse */
2819 0, /* tp_traverse */
2817 0, /* tp_clear */
2820 0, /* tp_clear */
2818 0, /* tp_richcompare */
2821 0, /* tp_richcompare */
2819 0, /* tp_weaklistoffset */
2822 0, /* tp_weaklistoffset */
2820 0, /* tp_iter */
2823 0, /* tp_iter */
2821 0, /* tp_iternext */
2824 0, /* tp_iternext */
2822 index_methods, /* tp_methods */
2825 index_methods, /* tp_methods */
2823 0, /* tp_members */
2826 0, /* tp_members */
2824 index_getset, /* tp_getset */
2827 index_getset, /* tp_getset */
2825 0, /* tp_base */
2828 0, /* tp_base */
2826 0, /* tp_dict */
2829 0, /* tp_dict */
2827 0, /* tp_descr_get */
2830 0, /* tp_descr_get */
2828 0, /* tp_descr_set */
2831 0, /* tp_descr_set */
2829 0, /* tp_dictoffset */
2832 0, /* tp_dictoffset */
2830 (initproc)index_init, /* tp_init */
2833 (initproc)index_init, /* tp_init */
2831 0, /* tp_alloc */
2834 0, /* tp_alloc */
2832 };
2835 };
2833
2836
2834 /*
2837 /*
2835 * returns a tuple of the form (index, index, cache) with elements as
2838 * returns a tuple of the form (index, index, cache) with elements as
2836 * follows:
2839 * follows:
2837 *
2840 *
2838 * index: an index object that lazily parses RevlogNG records
2841 * index: an index object that lazily parses RevlogNG records
2839 * cache: if data is inlined, a tuple (0, index_file_content), else None
2842 * cache: if data is inlined, a tuple (0, index_file_content), else None
2840 * index_file_content could be a string, or a buffer
2843 * index_file_content could be a string, or a buffer
2841 *
2844 *
2842 * added complications are for backwards compatibility
2845 * added complications are for backwards compatibility
2843 */
2846 */
2844 PyObject *parse_index2(PyObject *self, PyObject *args)
2847 PyObject *parse_index2(PyObject *self, PyObject *args)
2845 {
2848 {
2846 PyObject *tuple = NULL, *cache = NULL;
2849 PyObject *tuple = NULL, *cache = NULL;
2847 indexObject *idx;
2850 indexObject *idx;
2848 int ret;
2851 int ret;
2849
2852
2850 idx = PyObject_New(indexObject, &HgRevlogIndex_Type);
2853 idx = PyObject_New(indexObject, &HgRevlogIndex_Type);
2851 if (idx == NULL)
2854 if (idx == NULL)
2852 goto bail;
2855 goto bail;
2853
2856
2854 ret = index_init(idx, args);
2857 ret = index_init(idx, args);
2855 if (ret == -1)
2858 if (ret == -1)
2856 goto bail;
2859 goto bail;
2857
2860
2858 if (idx->inlined) {
2861 if (idx->inlined) {
2859 cache = Py_BuildValue("iO", 0, idx->data);
2862 cache = Py_BuildValue("iO", 0, idx->data);
2860 if (cache == NULL)
2863 if (cache == NULL)
2861 goto bail;
2864 goto bail;
2862 } else {
2865 } else {
2863 cache = Py_None;
2866 cache = Py_None;
2864 Py_INCREF(cache);
2867 Py_INCREF(cache);
2865 }
2868 }
2866
2869
2867 tuple = Py_BuildValue("NN", idx, cache);
2870 tuple = Py_BuildValue("NN", idx, cache);
2868 if (!tuple)
2871 if (!tuple)
2869 goto bail;
2872 goto bail;
2870 return tuple;
2873 return tuple;
2871
2874
2872 bail:
2875 bail:
2873 Py_XDECREF(idx);
2876 Py_XDECREF(idx);
2874 Py_XDECREF(cache);
2877 Py_XDECREF(cache);
2875 Py_XDECREF(tuple);
2878 Py_XDECREF(tuple);
2876 return NULL;
2879 return NULL;
2877 }
2880 }
2878
2881
2879 static Revlog_CAPI CAPI = {
2882 static Revlog_CAPI CAPI = {
2880 /* increment the abi_version field upon each change in the Revlog_CAPI
2883 /* increment the abi_version field upon each change in the Revlog_CAPI
2881 struct or in the ABI of the listed functions */
2884 struct or in the ABI of the listed functions */
2882 2,
2885 2,
2883 index_length,
2886 index_length,
2884 index_node,
2887 index_node,
2885 HgRevlogIndex_GetParents,
2888 HgRevlogIndex_GetParents,
2886 };
2889 };
2887
2890
2888 void revlog_module_init(PyObject *mod)
2891 void revlog_module_init(PyObject *mod)
2889 {
2892 {
2890 PyObject *caps = NULL;
2893 PyObject *caps = NULL;
2891 HgRevlogIndex_Type.tp_new = PyType_GenericNew;
2894 HgRevlogIndex_Type.tp_new = PyType_GenericNew;
2892 if (PyType_Ready(&HgRevlogIndex_Type) < 0)
2895 if (PyType_Ready(&HgRevlogIndex_Type) < 0)
2893 return;
2896 return;
2894 Py_INCREF(&HgRevlogIndex_Type);
2897 Py_INCREF(&HgRevlogIndex_Type);
2895 PyModule_AddObject(mod, "index", (PyObject *)&HgRevlogIndex_Type);
2898 PyModule_AddObject(mod, "index", (PyObject *)&HgRevlogIndex_Type);
2896
2899
2897 nodetreeType.tp_new = PyType_GenericNew;
2900 nodetreeType.tp_new = PyType_GenericNew;
2898 if (PyType_Ready(&nodetreeType) < 0)
2901 if (PyType_Ready(&nodetreeType) < 0)
2899 return;
2902 return;
2900 Py_INCREF(&nodetreeType);
2903 Py_INCREF(&nodetreeType);
2901 PyModule_AddObject(mod, "nodetree", (PyObject *)&nodetreeType);
2904 PyModule_AddObject(mod, "nodetree", (PyObject *)&nodetreeType);
2902
2905
2903 if (!nullentry) {
2906 if (!nullentry) {
2904 nullentry =
2907 nullentry =
2905 Py_BuildValue(PY23("iiiiiiis#", "iiiiiiiy#"), 0, 0, 0, -1,
2908 Py_BuildValue(PY23("iiiiiiis#", "iiiiiiiy#"), 0, 0, 0, -1,
2906 -1, -1, -1, nullid, (Py_ssize_t)20);
2909 -1, -1, -1, nullid, (Py_ssize_t)20);
2907 }
2910 }
2908 if (nullentry)
2911 if (nullentry)
2909 PyObject_GC_UnTrack(nullentry);
2912 PyObject_GC_UnTrack(nullentry);
2910
2913
2911 caps = PyCapsule_New(&CAPI, "mercurial.cext.parsers.revlog_CAPI", NULL);
2914 caps = PyCapsule_New(&CAPI, "mercurial.cext.parsers.revlog_CAPI", NULL);
2912 if (caps != NULL)
2915 if (caps != NULL)
2913 PyModule_AddObject(mod, "revlog_CAPI", caps);
2916 PyModule_AddObject(mod, "revlog_CAPI", caps);
2914 }
2917 }
@@ -1,191 +1,199 b''
1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
2 // and Mercurial contributors
2 // and Mercurial contributors
3 //
3 //
4 // This software may be used and distributed according to the terms of the
4 // This software may be used and distributed according to the terms of the
5 // GNU General Public License version 2 or any later version.
5 // GNU General Public License version 2 or any later version.
6 mod ancestors;
6 mod ancestors;
7 pub mod dagops;
7 pub mod dagops;
8 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
8 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
9 mod dirstate;
9 mod dirstate;
10 pub mod discovery;
10 pub mod discovery;
11 pub mod testing; // unconditionally built, for use from integration tests
11 pub mod testing; // unconditionally built, for use from integration tests
12 pub use dirstate::{
12 pub use dirstate::{
13 dirs_multiset::{DirsMultiset, DirsMultisetIter},
13 dirs_multiset::{DirsMultiset, DirsMultisetIter},
14 dirstate_map::DirstateMap,
14 dirstate_map::DirstateMap,
15 parsers::{pack_dirstate, parse_dirstate, PARENT_SIZE},
15 parsers::{pack_dirstate, parse_dirstate, PARENT_SIZE},
16 status::{
16 status::{
17 status, BadMatch, BadType, DirstateStatus, StatusError, StatusOptions,
17 status, BadMatch, BadType, DirstateStatus, StatusError, StatusOptions,
18 },
18 },
19 CopyMap, CopyMapIter, DirstateEntry, DirstateParents, EntryState,
19 CopyMap, CopyMapIter, DirstateEntry, DirstateParents, EntryState,
20 StateMap, StateMapIter,
20 StateMap, StateMapIter,
21 };
21 };
22 mod filepatterns;
22 mod filepatterns;
23 pub mod matchers;
23 pub mod matchers;
24 pub mod revlog;
24 pub mod revlog;
25 pub use revlog::*;
25 pub use revlog::*;
26 #[cfg(feature = "with-re2")]
26 #[cfg(feature = "with-re2")]
27 pub mod re2;
27 pub mod re2;
28 pub mod utils;
28 pub mod utils;
29
29
30 // Remove this to see (potential) non-artificial compile failures. MacOS
31 // *should* compile, but fail to compile tests for example as of 2020-03-06
32 #[cfg(not(target_os = "linux"))]
33 compile_error!(
34 "`hg-core` has only been tested on Linux and will most \
35 likely not behave correctly on other platforms."
36 );
37
30 use crate::utils::hg_path::{HgPathBuf, HgPathError};
38 use crate::utils::hg_path::{HgPathBuf, HgPathError};
31 pub use filepatterns::{
39 pub use filepatterns::{
32 parse_pattern_syntax, read_pattern_file, IgnorePattern,
40 parse_pattern_syntax, read_pattern_file, IgnorePattern,
33 PatternFileWarning, PatternSyntax,
41 PatternFileWarning, PatternSyntax,
34 };
42 };
35 use std::collections::HashMap;
43 use std::collections::HashMap;
36 use twox_hash::RandomXxHashBuilder64;
44 use twox_hash::RandomXxHashBuilder64;
37
45
38 /// This is a contract between the `micro-timer` crate and us, to expose
46 /// This is a contract between the `micro-timer` crate and us, to expose
39 /// the `log` crate as `crate::log`.
47 /// the `log` crate as `crate::log`.
40 use log;
48 use log;
41
49
42 pub type LineNumber = usize;
50 pub type LineNumber = usize;
43
51
44 /// Rust's default hasher is too slow because it tries to prevent collision
52 /// Rust's default hasher is too slow because it tries to prevent collision
45 /// attacks. We are not concerned about those: if an ill-minded person has
53 /// attacks. We are not concerned about those: if an ill-minded person has
46 /// write access to your repository, you have other issues.
54 /// write access to your repository, you have other issues.
47 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
55 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
48
56
49 #[derive(Clone, Debug, PartialEq)]
57 #[derive(Clone, Debug, PartialEq)]
50 pub enum DirstateParseError {
58 pub enum DirstateParseError {
51 TooLittleData,
59 TooLittleData,
52 Overflow,
60 Overflow,
53 CorruptedEntry(String),
61 CorruptedEntry(String),
54 Damaged,
62 Damaged,
55 }
63 }
56
64
57 impl From<std::io::Error> for DirstateParseError {
65 impl From<std::io::Error> for DirstateParseError {
58 fn from(e: std::io::Error) -> Self {
66 fn from(e: std::io::Error) -> Self {
59 DirstateParseError::CorruptedEntry(e.to_string())
67 DirstateParseError::CorruptedEntry(e.to_string())
60 }
68 }
61 }
69 }
62
70
63 impl ToString for DirstateParseError {
71 impl ToString for DirstateParseError {
64 fn to_string(&self) -> String {
72 fn to_string(&self) -> String {
65 use crate::DirstateParseError::*;
73 use crate::DirstateParseError::*;
66 match self {
74 match self {
67 TooLittleData => "Too little data for dirstate.".to_string(),
75 TooLittleData => "Too little data for dirstate.".to_string(),
68 Overflow => "Overflow in dirstate.".to_string(),
76 Overflow => "Overflow in dirstate.".to_string(),
69 CorruptedEntry(e) => format!("Corrupted entry: {:?}.", e),
77 CorruptedEntry(e) => format!("Corrupted entry: {:?}.", e),
70 Damaged => "Dirstate appears to be damaged.".to_string(),
78 Damaged => "Dirstate appears to be damaged.".to_string(),
71 }
79 }
72 }
80 }
73 }
81 }
74
82
75 #[derive(Debug, PartialEq)]
83 #[derive(Debug, PartialEq)]
76 pub enum DirstatePackError {
84 pub enum DirstatePackError {
77 CorruptedEntry(String),
85 CorruptedEntry(String),
78 CorruptedParent,
86 CorruptedParent,
79 BadSize(usize, usize),
87 BadSize(usize, usize),
80 }
88 }
81
89
82 impl From<std::io::Error> for DirstatePackError {
90 impl From<std::io::Error> for DirstatePackError {
83 fn from(e: std::io::Error) -> Self {
91 fn from(e: std::io::Error) -> Self {
84 DirstatePackError::CorruptedEntry(e.to_string())
92 DirstatePackError::CorruptedEntry(e.to_string())
85 }
93 }
86 }
94 }
87 #[derive(Debug, PartialEq)]
95 #[derive(Debug, PartialEq)]
88 pub enum DirstateMapError {
96 pub enum DirstateMapError {
89 PathNotFound(HgPathBuf),
97 PathNotFound(HgPathBuf),
90 EmptyPath,
98 EmptyPath,
91 InvalidPath(HgPathError),
99 InvalidPath(HgPathError),
92 }
100 }
93
101
94 impl ToString for DirstateMapError {
102 impl ToString for DirstateMapError {
95 fn to_string(&self) -> String {
103 fn to_string(&self) -> String {
96 match self {
104 match self {
97 DirstateMapError::PathNotFound(_) => {
105 DirstateMapError::PathNotFound(_) => {
98 "expected a value, found none".to_string()
106 "expected a value, found none".to_string()
99 }
107 }
100 DirstateMapError::EmptyPath => "Overflow in dirstate.".to_string(),
108 DirstateMapError::EmptyPath => "Overflow in dirstate.".to_string(),
101 DirstateMapError::InvalidPath(e) => e.to_string(),
109 DirstateMapError::InvalidPath(e) => e.to_string(),
102 }
110 }
103 }
111 }
104 }
112 }
105
113
106 #[derive(Debug)]
114 #[derive(Debug)]
107 pub enum DirstateError {
115 pub enum DirstateError {
108 Parse(DirstateParseError),
116 Parse(DirstateParseError),
109 Pack(DirstatePackError),
117 Pack(DirstatePackError),
110 Map(DirstateMapError),
118 Map(DirstateMapError),
111 IO(std::io::Error),
119 IO(std::io::Error),
112 }
120 }
113
121
114 impl From<DirstateParseError> for DirstateError {
122 impl From<DirstateParseError> for DirstateError {
115 fn from(e: DirstateParseError) -> Self {
123 fn from(e: DirstateParseError) -> Self {
116 DirstateError::Parse(e)
124 DirstateError::Parse(e)
117 }
125 }
118 }
126 }
119
127
120 impl From<DirstatePackError> for DirstateError {
128 impl From<DirstatePackError> for DirstateError {
121 fn from(e: DirstatePackError) -> Self {
129 fn from(e: DirstatePackError) -> Self {
122 DirstateError::Pack(e)
130 DirstateError::Pack(e)
123 }
131 }
124 }
132 }
125
133
126 #[derive(Debug)]
134 #[derive(Debug)]
127 pub enum PatternError {
135 pub enum PatternError {
128 Path(HgPathError),
136 Path(HgPathError),
129 UnsupportedSyntax(String),
137 UnsupportedSyntax(String),
130 UnsupportedSyntaxInFile(String, String, usize),
138 UnsupportedSyntaxInFile(String, String, usize),
131 TooLong(usize),
139 TooLong(usize),
132 IO(std::io::Error),
140 IO(std::io::Error),
133 /// Needed a pattern that can be turned into a regex but got one that
141 /// Needed a pattern that can be turned into a regex but got one that
134 /// can't. This should only happen through programmer error.
142 /// can't. This should only happen through programmer error.
135 NonRegexPattern(IgnorePattern),
143 NonRegexPattern(IgnorePattern),
136 /// This is temporary, see `re2/mod.rs`.
144 /// This is temporary, see `re2/mod.rs`.
137 /// This will cause a fallback to Python.
145 /// This will cause a fallback to Python.
138 Re2NotInstalled,
146 Re2NotInstalled,
139 }
147 }
140
148
141 impl ToString for PatternError {
149 impl ToString for PatternError {
142 fn to_string(&self) -> String {
150 fn to_string(&self) -> String {
143 match self {
151 match self {
144 PatternError::UnsupportedSyntax(syntax) => {
152 PatternError::UnsupportedSyntax(syntax) => {
145 format!("Unsupported syntax {}", syntax)
153 format!("Unsupported syntax {}", syntax)
146 }
154 }
147 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
155 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
148 format!(
156 format!(
149 "{}:{}: unsupported syntax {}",
157 "{}:{}: unsupported syntax {}",
150 file_path, line, syntax
158 file_path, line, syntax
151 )
159 )
152 }
160 }
153 PatternError::TooLong(size) => {
161 PatternError::TooLong(size) => {
154 format!("matcher pattern is too long ({} bytes)", size)
162 format!("matcher pattern is too long ({} bytes)", size)
155 }
163 }
156 PatternError::IO(e) => e.to_string(),
164 PatternError::IO(e) => e.to_string(),
157 PatternError::Path(e) => e.to_string(),
165 PatternError::Path(e) => e.to_string(),
158 PatternError::NonRegexPattern(pattern) => {
166 PatternError::NonRegexPattern(pattern) => {
159 format!("'{:?}' cannot be turned into a regex", pattern)
167 format!("'{:?}' cannot be turned into a regex", pattern)
160 }
168 }
161 PatternError::Re2NotInstalled => {
169 PatternError::Re2NotInstalled => {
162 "Re2 is not installed, cannot use regex functionality."
170 "Re2 is not installed, cannot use regex functionality."
163 .to_string()
171 .to_string()
164 }
172 }
165 }
173 }
166 }
174 }
167 }
175 }
168
176
169 impl From<DirstateMapError> for DirstateError {
177 impl From<DirstateMapError> for DirstateError {
170 fn from(e: DirstateMapError) -> Self {
178 fn from(e: DirstateMapError) -> Self {
171 DirstateError::Map(e)
179 DirstateError::Map(e)
172 }
180 }
173 }
181 }
174
182
175 impl From<std::io::Error> for DirstateError {
183 impl From<std::io::Error> for DirstateError {
176 fn from(e: std::io::Error) -> Self {
184 fn from(e: std::io::Error) -> Self {
177 DirstateError::IO(e)
185 DirstateError::IO(e)
178 }
186 }
179 }
187 }
180
188
181 impl From<std::io::Error> for PatternError {
189 impl From<std::io::Error> for PatternError {
182 fn from(e: std::io::Error) -> Self {
190 fn from(e: std::io::Error) -> Self {
183 PatternError::IO(e)
191 PatternError::IO(e)
184 }
192 }
185 }
193 }
186
194
187 impl From<HgPathError> for PatternError {
195 impl From<HgPathError> for PatternError {
188 fn from(e: HgPathError) -> Self {
196 fn from(e: HgPathError) -> Self {
189 PatternError::Path(e)
197 PatternError::Path(e)
190 }
198 }
191 }
199 }
@@ -1,3755 +1,3755 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 platform
56 import platform
57 import random
57 import random
58 import re
58 import re
59 import shutil
59 import shutil
60 import signal
60 import signal
61 import socket
61 import socket
62 import subprocess
62 import subprocess
63 import sys
63 import sys
64 import sysconfig
64 import sysconfig
65 import tempfile
65 import tempfile
66 import threading
66 import threading
67 import time
67 import time
68 import unittest
68 import unittest
69 import uuid
69 import uuid
70 import xml.dom.minidom as minidom
70 import xml.dom.minidom as minidom
71
71
72 try:
72 try:
73 import Queue as queue
73 import Queue as queue
74 except ImportError:
74 except ImportError:
75 import queue
75 import queue
76
76
77 try:
77 try:
78 import shlex
78 import shlex
79
79
80 shellquote = shlex.quote
80 shellquote = shlex.quote
81 except (ImportError, AttributeError):
81 except (ImportError, AttributeError):
82 import pipes
82 import pipes
83
83
84 shellquote = pipes.quote
84 shellquote = pipes.quote
85
85
86 processlock = threading.Lock()
86 processlock = threading.Lock()
87
87
88 pygmentspresent = False
88 pygmentspresent = False
89 # ANSI color is unsupported prior to Windows 10
89 # ANSI color is unsupported prior to Windows 10
90 if os.name != 'nt':
90 if os.name != 'nt':
91 try: # is pygments installed
91 try: # is pygments installed
92 import pygments
92 import pygments
93 import pygments.lexers as lexers
93 import pygments.lexers as lexers
94 import pygments.lexer as lexer
94 import pygments.lexer as lexer
95 import pygments.formatters as formatters
95 import pygments.formatters as formatters
96 import pygments.token as token
96 import pygments.token as token
97 import pygments.style as style
97 import pygments.style as style
98
98
99 pygmentspresent = True
99 pygmentspresent = True
100 difflexer = lexers.DiffLexer()
100 difflexer = lexers.DiffLexer()
101 terminal256formatter = formatters.Terminal256Formatter()
101 terminal256formatter = formatters.Terminal256Formatter()
102 except ImportError:
102 except ImportError:
103 pass
103 pass
104
104
105 if pygmentspresent:
105 if pygmentspresent:
106
106
107 class TestRunnerStyle(style.Style):
107 class TestRunnerStyle(style.Style):
108 default_style = ""
108 default_style = ""
109 skipped = token.string_to_tokentype("Token.Generic.Skipped")
109 skipped = token.string_to_tokentype("Token.Generic.Skipped")
110 failed = token.string_to_tokentype("Token.Generic.Failed")
110 failed = token.string_to_tokentype("Token.Generic.Failed")
111 skippedname = token.string_to_tokentype("Token.Generic.SName")
111 skippedname = token.string_to_tokentype("Token.Generic.SName")
112 failedname = token.string_to_tokentype("Token.Generic.FName")
112 failedname = token.string_to_tokentype("Token.Generic.FName")
113 styles = {
113 styles = {
114 skipped: '#e5e5e5',
114 skipped: '#e5e5e5',
115 skippedname: '#00ffff',
115 skippedname: '#00ffff',
116 failed: '#7f0000',
116 failed: '#7f0000',
117 failedname: '#ff0000',
117 failedname: '#ff0000',
118 }
118 }
119
119
120 class TestRunnerLexer(lexer.RegexLexer):
120 class TestRunnerLexer(lexer.RegexLexer):
121 testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?'
121 testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?'
122 tokens = {
122 tokens = {
123 'root': [
123 'root': [
124 (r'^Skipped', token.Generic.Skipped, 'skipped'),
124 (r'^Skipped', token.Generic.Skipped, 'skipped'),
125 (r'^Failed ', token.Generic.Failed, 'failed'),
125 (r'^Failed ', token.Generic.Failed, 'failed'),
126 (r'^ERROR: ', token.Generic.Failed, 'failed'),
126 (r'^ERROR: ', token.Generic.Failed, 'failed'),
127 ],
127 ],
128 'skipped': [
128 'skipped': [
129 (testpattern, token.Generic.SName),
129 (testpattern, token.Generic.SName),
130 (r':.*', token.Generic.Skipped),
130 (r':.*', token.Generic.Skipped),
131 ],
131 ],
132 'failed': [
132 'failed': [
133 (testpattern, token.Generic.FName),
133 (testpattern, token.Generic.FName),
134 (r'(:| ).*', token.Generic.Failed),
134 (r'(:| ).*', token.Generic.Failed),
135 ],
135 ],
136 }
136 }
137
137
138 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
138 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
139 runnerlexer = TestRunnerLexer()
139 runnerlexer = TestRunnerLexer()
140
140
141 origenviron = os.environ.copy()
141 origenviron = os.environ.copy()
142
142
143 if sys.version_info > (3, 5, 0):
143 if sys.version_info > (3, 5, 0):
144 PYTHON3 = True
144 PYTHON3 = True
145 xrange = range # we use xrange in one place, and we'd rather not use range
145 xrange = range # we use xrange in one place, and we'd rather not use range
146
146
147 def _sys2bytes(p):
147 def _sys2bytes(p):
148 if p is None:
148 if p is None:
149 return p
149 return p
150 return p.encode('utf-8')
150 return p.encode('utf-8')
151
151
152 def _bytes2sys(p):
152 def _bytes2sys(p):
153 if p is None:
153 if p is None:
154 return p
154 return p
155 return p.decode('utf-8')
155 return p.decode('utf-8')
156
156
157 osenvironb = getattr(os, 'environb', None)
157 osenvironb = getattr(os, 'environb', None)
158 if osenvironb is None:
158 if osenvironb is None:
159 # Windows lacks os.environb, for instance. A proxy over the real thing
159 # Windows lacks os.environb, for instance. A proxy over the real thing
160 # instead of a copy allows the environment to be updated via bytes on
160 # instead of a copy allows the environment to be updated via bytes on
161 # all platforms.
161 # all platforms.
162 class environbytes(object):
162 class environbytes(object):
163 def __init__(self, strenv):
163 def __init__(self, strenv):
164 self.__len__ = strenv.__len__
164 self.__len__ = strenv.__len__
165 self.clear = strenv.clear
165 self.clear = strenv.clear
166 self._strenv = strenv
166 self._strenv = strenv
167
167
168 def __getitem__(self, k):
168 def __getitem__(self, k):
169 v = self._strenv.__getitem__(_bytes2sys(k))
169 v = self._strenv.__getitem__(_bytes2sys(k))
170 return _sys2bytes(v)
170 return _sys2bytes(v)
171
171
172 def __setitem__(self, k, v):
172 def __setitem__(self, k, v):
173 self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
173 self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
174
174
175 def __delitem__(self, k):
175 def __delitem__(self, k):
176 self._strenv.__delitem__(_bytes2sys(k))
176 self._strenv.__delitem__(_bytes2sys(k))
177
177
178 def __contains__(self, k):
178 def __contains__(self, k):
179 return self._strenv.__contains__(_bytes2sys(k))
179 return self._strenv.__contains__(_bytes2sys(k))
180
180
181 def __iter__(self):
181 def __iter__(self):
182 return iter([_sys2bytes(k) for k in iter(self._strenv)])
182 return iter([_sys2bytes(k) for k in iter(self._strenv)])
183
183
184 def get(self, k, default=None):
184 def get(self, k, default=None):
185 v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
185 v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
186 return _sys2bytes(v)
186 return _sys2bytes(v)
187
187
188 def pop(self, k, default=None):
188 def pop(self, k, default=None):
189 v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
189 v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
190 return _sys2bytes(v)
190 return _sys2bytes(v)
191
191
192 osenvironb = environbytes(os.environ)
192 osenvironb = environbytes(os.environ)
193
193
194 getcwdb = getattr(os, 'getcwdb')
194 getcwdb = getattr(os, 'getcwdb')
195 if not getcwdb or os.name == 'nt':
195 if not getcwdb or os.name == 'nt':
196 getcwdb = lambda: _sys2bytes(os.getcwd())
196 getcwdb = lambda: _sys2bytes(os.getcwd())
197
197
198 elif sys.version_info >= (3, 0, 0):
198 elif sys.version_info >= (3, 0, 0):
199 print(
199 print(
200 '%s is only supported on Python 3.5+ and 2.7, not %s'
200 '%s is only supported on Python 3.5+ and 2.7, not %s'
201 % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
201 % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
202 )
202 )
203 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
203 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
204 else:
204 else:
205 PYTHON3 = False
205 PYTHON3 = False
206
206
207 # In python 2.x, path operations are generally done using
207 # In python 2.x, path operations are generally done using
208 # bytestrings by default, so we don't have to do any extra
208 # bytestrings by default, so we don't have to do any extra
209 # fiddling there. We define the wrapper functions anyway just to
209 # fiddling there. We define the wrapper functions anyway just to
210 # help keep code consistent between platforms.
210 # help keep code consistent between platforms.
211 def _sys2bytes(p):
211 def _sys2bytes(p):
212 return p
212 return p
213
213
214 _bytes2sys = _sys2bytes
214 _bytes2sys = _sys2bytes
215 osenvironb = os.environ
215 osenvironb = os.environ
216 getcwdb = os.getcwd
216 getcwdb = os.getcwd
217
217
218 # For Windows support
218 # For Windows support
219 wifexited = getattr(os, "WIFEXITED", lambda x: False)
219 wifexited = getattr(os, "WIFEXITED", lambda x: False)
220
220
221 # Whether to use IPv6
221 # Whether to use IPv6
222 def checksocketfamily(name, port=20058):
222 def checksocketfamily(name, port=20058):
223 """return true if we can listen on localhost using family=name
223 """return true if we can listen on localhost using family=name
224
224
225 name should be either 'AF_INET', or 'AF_INET6'.
225 name should be either 'AF_INET', or 'AF_INET6'.
226 port being used is okay - EADDRINUSE is considered as successful.
226 port being used is okay - EADDRINUSE is considered as successful.
227 """
227 """
228 family = getattr(socket, name, None)
228 family = getattr(socket, name, None)
229 if family is None:
229 if family is None:
230 return False
230 return False
231 try:
231 try:
232 s = socket.socket(family, socket.SOCK_STREAM)
232 s = socket.socket(family, socket.SOCK_STREAM)
233 s.bind(('localhost', port))
233 s.bind(('localhost', port))
234 s.close()
234 s.close()
235 return True
235 return True
236 except socket.error as exc:
236 except socket.error as exc:
237 if exc.errno == errno.EADDRINUSE:
237 if exc.errno == errno.EADDRINUSE:
238 return True
238 return True
239 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
239 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
240 return False
240 return False
241 else:
241 else:
242 raise
242 raise
243 else:
243 else:
244 return False
244 return False
245
245
246
246
247 # useipv6 will be set by parseargs
247 # useipv6 will be set by parseargs
248 useipv6 = None
248 useipv6 = None
249
249
250
250
251 def checkportisavailable(port):
251 def checkportisavailable(port):
252 """return true if a port seems free to bind on localhost"""
252 """return true if a port seems free to bind on localhost"""
253 if useipv6:
253 if useipv6:
254 family = socket.AF_INET6
254 family = socket.AF_INET6
255 else:
255 else:
256 family = socket.AF_INET
256 family = socket.AF_INET
257 try:
257 try:
258 s = socket.socket(family, socket.SOCK_STREAM)
258 s = socket.socket(family, socket.SOCK_STREAM)
259 s.bind(('localhost', port))
259 s.bind(('localhost', port))
260 s.close()
260 s.close()
261 return True
261 return True
262 except socket.error as exc:
262 except socket.error as exc:
263 if exc.errno not in (
263 if exc.errno not in (
264 errno.EADDRINUSE,
264 errno.EADDRINUSE,
265 errno.EADDRNOTAVAIL,
265 errno.EADDRNOTAVAIL,
266 errno.EPROTONOSUPPORT,
266 errno.EPROTONOSUPPORT,
267 ):
267 ):
268 raise
268 raise
269 return False
269 return False
270
270
271
271
272 closefds = os.name == 'posix'
272 closefds = os.name == 'posix'
273
273
274
274
275 def Popen4(cmd, wd, timeout, env=None):
275 def Popen4(cmd, wd, timeout, env=None):
276 processlock.acquire()
276 processlock.acquire()
277 p = subprocess.Popen(
277 p = subprocess.Popen(
278 _bytes2sys(cmd),
278 _bytes2sys(cmd),
279 shell=True,
279 shell=True,
280 bufsize=-1,
280 bufsize=-1,
281 cwd=_bytes2sys(wd),
281 cwd=_bytes2sys(wd),
282 env=env,
282 env=env,
283 close_fds=closefds,
283 close_fds=closefds,
284 stdin=subprocess.PIPE,
284 stdin=subprocess.PIPE,
285 stdout=subprocess.PIPE,
285 stdout=subprocess.PIPE,
286 stderr=subprocess.STDOUT,
286 stderr=subprocess.STDOUT,
287 )
287 )
288 processlock.release()
288 processlock.release()
289
289
290 p.fromchild = p.stdout
290 p.fromchild = p.stdout
291 p.tochild = p.stdin
291 p.tochild = p.stdin
292 p.childerr = p.stderr
292 p.childerr = p.stderr
293
293
294 p.timeout = False
294 p.timeout = False
295 if timeout:
295 if timeout:
296
296
297 def t():
297 def t():
298 start = time.time()
298 start = time.time()
299 while time.time() - start < timeout and p.returncode is None:
299 while time.time() - start < timeout and p.returncode is None:
300 time.sleep(0.1)
300 time.sleep(0.1)
301 p.timeout = True
301 p.timeout = True
302 if p.returncode is None:
302 if p.returncode is None:
303 terminate(p)
303 terminate(p)
304
304
305 threading.Thread(target=t).start()
305 threading.Thread(target=t).start()
306
306
307 return p
307 return p
308
308
309
309
310 if sys.executable:
310 if sys.executable:
311 sysexecutable = sys.executable
311 sysexecutable = sys.executable
312 elif os.environ.get('PYTHONEXECUTABLE'):
312 elif os.environ.get('PYTHONEXECUTABLE'):
313 sysexecutable = os.environ['PYTHONEXECUTABLE']
313 sysexecutable = os.environ['PYTHONEXECUTABLE']
314 elif os.environ.get('PYTHON'):
314 elif os.environ.get('PYTHON'):
315 sysexecutable = os.environ['PYTHON']
315 sysexecutable = os.environ['PYTHON']
316 else:
316 else:
317 raise AssertionError('Could not find Python interpreter')
317 raise AssertionError('Could not find Python interpreter')
318
318
319 PYTHON = _sys2bytes(sysexecutable.replace('\\', '/'))
319 PYTHON = _sys2bytes(sysexecutable.replace('\\', '/'))
320 IMPL_PATH = b'PYTHONPATH'
320 IMPL_PATH = b'PYTHONPATH'
321 if 'java' in sys.platform:
321 if 'java' in sys.platform:
322 IMPL_PATH = b'JYTHONPATH'
322 IMPL_PATH = b'JYTHONPATH'
323
323
324 defaults = {
324 defaults = {
325 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()),
325 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()),
326 'timeout': ('HGTEST_TIMEOUT', 180),
326 'timeout': ('HGTEST_TIMEOUT', 180),
327 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500),
327 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500),
328 'port': ('HGTEST_PORT', 20059),
328 'port': ('HGTEST_PORT', 20059),
329 'shell': ('HGTEST_SHELL', 'sh'),
329 'shell': ('HGTEST_SHELL', 'sh'),
330 }
330 }
331
331
332
332
333 def canonpath(path):
333 def canonpath(path):
334 return os.path.realpath(os.path.expanduser(path))
334 return os.path.realpath(os.path.expanduser(path))
335
335
336
336
337 def parselistfiles(files, listtype, warn=True):
337 def parselistfiles(files, listtype, warn=True):
338 entries = dict()
338 entries = dict()
339 for filename in files:
339 for filename in files:
340 try:
340 try:
341 path = os.path.expanduser(os.path.expandvars(filename))
341 path = os.path.expanduser(os.path.expandvars(filename))
342 f = open(path, "rb")
342 f = open(path, "rb")
343 except IOError as err:
343 except IOError as err:
344 if err.errno != errno.ENOENT:
344 if err.errno != errno.ENOENT:
345 raise
345 raise
346 if warn:
346 if warn:
347 print("warning: no such %s file: %s" % (listtype, filename))
347 print("warning: no such %s file: %s" % (listtype, filename))
348 continue
348 continue
349
349
350 for line in f.readlines():
350 for line in f.readlines():
351 line = line.split(b'#', 1)[0].strip()
351 line = line.split(b'#', 1)[0].strip()
352 if line:
352 if line:
353 entries[line] = filename
353 entries[line] = filename
354
354
355 f.close()
355 f.close()
356 return entries
356 return entries
357
357
358
358
359 def parsettestcases(path):
359 def parsettestcases(path):
360 """read a .t test file, return a set of test case names
360 """read a .t test file, return a set of test case names
361
361
362 If path does not exist, return an empty set.
362 If path does not exist, return an empty set.
363 """
363 """
364 cases = []
364 cases = []
365 try:
365 try:
366 with open(path, 'rb') as f:
366 with open(path, 'rb') as f:
367 for l in f:
367 for l in f:
368 if l.startswith(b'#testcases '):
368 if l.startswith(b'#testcases '):
369 cases.append(sorted(l[11:].split()))
369 cases.append(sorted(l[11:].split()))
370 except IOError as ex:
370 except IOError as ex:
371 if ex.errno != errno.ENOENT:
371 if ex.errno != errno.ENOENT:
372 raise
372 raise
373 return cases
373 return cases
374
374
375
375
376 def getparser():
376 def getparser():
377 """Obtain the OptionParser used by the CLI."""
377 """Obtain the OptionParser used by the CLI."""
378 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
378 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
379
379
380 selection = parser.add_argument_group('Test Selection')
380 selection = parser.add_argument_group('Test Selection')
381 selection.add_argument(
381 selection.add_argument(
382 '--allow-slow-tests',
382 '--allow-slow-tests',
383 action='store_true',
383 action='store_true',
384 help='allow extremely slow tests',
384 help='allow extremely slow tests',
385 )
385 )
386 selection.add_argument(
386 selection.add_argument(
387 "--blacklist",
387 "--blacklist",
388 action="append",
388 action="append",
389 help="skip tests listed in the specified blacklist file",
389 help="skip tests listed in the specified blacklist file",
390 )
390 )
391 selection.add_argument(
391 selection.add_argument(
392 "--changed",
392 "--changed",
393 help="run tests that are changed in parent rev or working directory",
393 help="run tests that are changed in parent rev or working directory",
394 )
394 )
395 selection.add_argument(
395 selection.add_argument(
396 "-k", "--keywords", help="run tests matching keywords"
396 "-k", "--keywords", help="run tests matching keywords"
397 )
397 )
398 selection.add_argument(
398 selection.add_argument(
399 "-r", "--retest", action="store_true", help="retest failed tests"
399 "-r", "--retest", action="store_true", help="retest failed tests"
400 )
400 )
401 selection.add_argument(
401 selection.add_argument(
402 "--test-list",
402 "--test-list",
403 action="append",
403 action="append",
404 help="read tests to run from the specified file",
404 help="read tests to run from the specified file",
405 )
405 )
406 selection.add_argument(
406 selection.add_argument(
407 "--whitelist",
407 "--whitelist",
408 action="append",
408 action="append",
409 help="always run tests listed in the specified whitelist file",
409 help="always run tests listed in the specified whitelist file",
410 )
410 )
411 selection.add_argument(
411 selection.add_argument(
412 'tests', metavar='TESTS', nargs='*', help='Tests to run'
412 'tests', metavar='TESTS', nargs='*', help='Tests to run'
413 )
413 )
414
414
415 harness = parser.add_argument_group('Test Harness Behavior')
415 harness = parser.add_argument_group('Test Harness Behavior')
416 harness.add_argument(
416 harness.add_argument(
417 '--bisect-repo',
417 '--bisect-repo',
418 metavar='bisect_repo',
418 metavar='bisect_repo',
419 help=(
419 help=(
420 "Path of a repo to bisect. Use together with " "--known-good-rev"
420 "Path of a repo to bisect. Use together with " "--known-good-rev"
421 ),
421 ),
422 )
422 )
423 harness.add_argument(
423 harness.add_argument(
424 "-d",
424 "-d",
425 "--debug",
425 "--debug",
426 action="store_true",
426 action="store_true",
427 help="debug mode: write output of test scripts to console"
427 help="debug mode: write output of test scripts to console"
428 " rather than capturing and diffing it (disables timeout)",
428 " rather than capturing and diffing it (disables timeout)",
429 )
429 )
430 harness.add_argument(
430 harness.add_argument(
431 "-f",
431 "-f",
432 "--first",
432 "--first",
433 action="store_true",
433 action="store_true",
434 help="exit on the first test failure",
434 help="exit on the first test failure",
435 )
435 )
436 harness.add_argument(
436 harness.add_argument(
437 "-i",
437 "-i",
438 "--interactive",
438 "--interactive",
439 action="store_true",
439 action="store_true",
440 help="prompt to accept changed output",
440 help="prompt to accept changed output",
441 )
441 )
442 harness.add_argument(
442 harness.add_argument(
443 "-j",
443 "-j",
444 "--jobs",
444 "--jobs",
445 type=int,
445 type=int,
446 help="number of jobs to run in parallel"
446 help="number of jobs to run in parallel"
447 " (default: $%s or %d)" % defaults['jobs'],
447 " (default: $%s or %d)" % defaults['jobs'],
448 )
448 )
449 harness.add_argument(
449 harness.add_argument(
450 "--keep-tmpdir",
450 "--keep-tmpdir",
451 action="store_true",
451 action="store_true",
452 help="keep temporary directory after running tests",
452 help="keep temporary directory after running tests",
453 )
453 )
454 harness.add_argument(
454 harness.add_argument(
455 '--known-good-rev',
455 '--known-good-rev',
456 metavar="known_good_rev",
456 metavar="known_good_rev",
457 help=(
457 help=(
458 "Automatically bisect any failures using this "
458 "Automatically bisect any failures using this "
459 "revision as a known-good revision."
459 "revision as a known-good revision."
460 ),
460 ),
461 )
461 )
462 harness.add_argument(
462 harness.add_argument(
463 "--list-tests",
463 "--list-tests",
464 action="store_true",
464 action="store_true",
465 help="list tests instead of running them",
465 help="list tests instead of running them",
466 )
466 )
467 harness.add_argument(
467 harness.add_argument(
468 "--loop", action="store_true", help="loop tests repeatedly"
468 "--loop", action="store_true", help="loop tests repeatedly"
469 )
469 )
470 harness.add_argument(
470 harness.add_argument(
471 '--random', action="store_true", help='run tests in random order'
471 '--random', action="store_true", help='run tests in random order'
472 )
472 )
473 harness.add_argument(
473 harness.add_argument(
474 '--order-by-runtime',
474 '--order-by-runtime',
475 action="store_true",
475 action="store_true",
476 help='run slowest tests first, according to .testtimes',
476 help='run slowest tests first, according to .testtimes',
477 )
477 )
478 harness.add_argument(
478 harness.add_argument(
479 "-p",
479 "-p",
480 "--port",
480 "--port",
481 type=int,
481 type=int,
482 help="port on which servers should listen"
482 help="port on which servers should listen"
483 " (default: $%s or %d)" % defaults['port'],
483 " (default: $%s or %d)" % defaults['port'],
484 )
484 )
485 harness.add_argument(
485 harness.add_argument(
486 '--profile-runner',
486 '--profile-runner',
487 action='store_true',
487 action='store_true',
488 help='run statprof on run-tests',
488 help='run statprof on run-tests',
489 )
489 )
490 harness.add_argument(
490 harness.add_argument(
491 "-R", "--restart", action="store_true", help="restart at last error"
491 "-R", "--restart", action="store_true", help="restart at last error"
492 )
492 )
493 harness.add_argument(
493 harness.add_argument(
494 "--runs-per-test",
494 "--runs-per-test",
495 type=int,
495 type=int,
496 dest="runs_per_test",
496 dest="runs_per_test",
497 help="run each test N times (default=1)",
497 help="run each test N times (default=1)",
498 default=1,
498 default=1,
499 )
499 )
500 harness.add_argument(
500 harness.add_argument(
501 "--shell", help="shell to use (default: $%s or %s)" % defaults['shell']
501 "--shell", help="shell to use (default: $%s or %s)" % defaults['shell']
502 )
502 )
503 harness.add_argument(
503 harness.add_argument(
504 '--showchannels', action='store_true', help='show scheduling channels'
504 '--showchannels', action='store_true', help='show scheduling channels'
505 )
505 )
506 harness.add_argument(
506 harness.add_argument(
507 "--slowtimeout",
507 "--slowtimeout",
508 type=int,
508 type=int,
509 help="kill errant slow tests after SLOWTIMEOUT seconds"
509 help="kill errant slow tests after SLOWTIMEOUT seconds"
510 " (default: $%s or %d)" % defaults['slowtimeout'],
510 " (default: $%s or %d)" % defaults['slowtimeout'],
511 )
511 )
512 harness.add_argument(
512 harness.add_argument(
513 "-t",
513 "-t",
514 "--timeout",
514 "--timeout",
515 type=int,
515 type=int,
516 help="kill errant tests after TIMEOUT seconds"
516 help="kill errant tests after TIMEOUT seconds"
517 " (default: $%s or %d)" % defaults['timeout'],
517 " (default: $%s or %d)" % defaults['timeout'],
518 )
518 )
519 harness.add_argument(
519 harness.add_argument(
520 "--tmpdir",
520 "--tmpdir",
521 help="run tests in the given temporary directory"
521 help="run tests in the given temporary directory"
522 " (implies --keep-tmpdir)",
522 " (implies --keep-tmpdir)",
523 )
523 )
524 harness.add_argument(
524 harness.add_argument(
525 "-v", "--verbose", action="store_true", help="output verbose messages"
525 "-v", "--verbose", action="store_true", help="output verbose messages"
526 )
526 )
527
527
528 hgconf = parser.add_argument_group('Mercurial Configuration')
528 hgconf = parser.add_argument_group('Mercurial Configuration')
529 hgconf.add_argument(
529 hgconf.add_argument(
530 "--chg",
530 "--chg",
531 action="store_true",
531 action="store_true",
532 help="install and use chg wrapper in place of hg",
532 help="install and use chg wrapper in place of hg",
533 )
533 )
534 hgconf.add_argument("--compiler", help="compiler to build with")
534 hgconf.add_argument("--compiler", help="compiler to build with")
535 hgconf.add_argument(
535 hgconf.add_argument(
536 '--extra-config-opt',
536 '--extra-config-opt',
537 action="append",
537 action="append",
538 default=[],
538 default=[],
539 help='set the given config opt in the test hgrc',
539 help='set the given config opt in the test hgrc',
540 )
540 )
541 hgconf.add_argument(
541 hgconf.add_argument(
542 "-l",
542 "-l",
543 "--local",
543 "--local",
544 action="store_true",
544 action="store_true",
545 help="shortcut for --with-hg=<testdir>/../hg, "
545 help="shortcut for --with-hg=<testdir>/../hg, "
546 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set",
546 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set",
547 )
547 )
548 hgconf.add_argument(
548 hgconf.add_argument(
549 "--ipv6",
549 "--ipv6",
550 action="store_true",
550 action="store_true",
551 help="prefer IPv6 to IPv4 for network related tests",
551 help="prefer IPv6 to IPv4 for network related tests",
552 )
552 )
553 hgconf.add_argument(
553 hgconf.add_argument(
554 "--pure",
554 "--pure",
555 action="store_true",
555 action="store_true",
556 help="use pure Python code instead of C extensions",
556 help="use pure Python code instead of C extensions",
557 )
557 )
558 hgconf.add_argument(
558 hgconf.add_argument(
559 "--rust",
559 "--rust",
560 action="store_true",
560 action="store_true",
561 help="use Rust code alongside C extensions",
561 help="use Rust code alongside C extensions",
562 )
562 )
563 hgconf.add_argument(
563 hgconf.add_argument(
564 "--no-rust",
564 "--no-rust",
565 action="store_true",
565 action="store_true",
566 help="do not use Rust code even if compiled",
566 help="do not use Rust code even if compiled",
567 )
567 )
568 hgconf.add_argument(
568 hgconf.add_argument(
569 "--with-chg",
569 "--with-chg",
570 metavar="CHG",
570 metavar="CHG",
571 help="use specified chg wrapper in place of hg",
571 help="use specified chg wrapper in place of hg",
572 )
572 )
573 hgconf.add_argument(
573 hgconf.add_argument(
574 "--with-hg",
574 "--with-hg",
575 metavar="HG",
575 metavar="HG",
576 help="test using specified hg script rather than a "
576 help="test using specified hg script rather than a "
577 "temporary installation",
577 "temporary installation",
578 )
578 )
579
579
580 reporting = parser.add_argument_group('Results Reporting')
580 reporting = parser.add_argument_group('Results Reporting')
581 reporting.add_argument(
581 reporting.add_argument(
582 "-C",
582 "-C",
583 "--annotate",
583 "--annotate",
584 action="store_true",
584 action="store_true",
585 help="output files annotated with coverage",
585 help="output files annotated with coverage",
586 )
586 )
587 reporting.add_argument(
587 reporting.add_argument(
588 "--color",
588 "--color",
589 choices=["always", "auto", "never"],
589 choices=["always", "auto", "never"],
590 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
590 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
591 help="colorisation: always|auto|never (default: auto)",
591 help="colorisation: always|auto|never (default: auto)",
592 )
592 )
593 reporting.add_argument(
593 reporting.add_argument(
594 "-c",
594 "-c",
595 "--cover",
595 "--cover",
596 action="store_true",
596 action="store_true",
597 help="print a test coverage report",
597 help="print a test coverage report",
598 )
598 )
599 reporting.add_argument(
599 reporting.add_argument(
600 '--exceptions',
600 '--exceptions',
601 action='store_true',
601 action='store_true',
602 help='log all exceptions and generate an exception report',
602 help='log all exceptions and generate an exception report',
603 )
603 )
604 reporting.add_argument(
604 reporting.add_argument(
605 "-H",
605 "-H",
606 "--htmlcov",
606 "--htmlcov",
607 action="store_true",
607 action="store_true",
608 help="create an HTML report of the coverage of the files",
608 help="create an HTML report of the coverage of the files",
609 )
609 )
610 reporting.add_argument(
610 reporting.add_argument(
611 "--json",
611 "--json",
612 action="store_true",
612 action="store_true",
613 help="store test result data in 'report.json' file",
613 help="store test result data in 'report.json' file",
614 )
614 )
615 reporting.add_argument(
615 reporting.add_argument(
616 "--outputdir",
616 "--outputdir",
617 help="directory to write error logs to (default=test directory)",
617 help="directory to write error logs to (default=test directory)",
618 )
618 )
619 reporting.add_argument(
619 reporting.add_argument(
620 "-n", "--nodiff", action="store_true", help="skip showing test changes"
620 "-n", "--nodiff", action="store_true", help="skip showing test changes"
621 )
621 )
622 reporting.add_argument(
622 reporting.add_argument(
623 "-S",
623 "-S",
624 "--noskips",
624 "--noskips",
625 action="store_true",
625 action="store_true",
626 help="don't report skip tests verbosely",
626 help="don't report skip tests verbosely",
627 )
627 )
628 reporting.add_argument(
628 reporting.add_argument(
629 "--time", action="store_true", help="time how long each test takes"
629 "--time", action="store_true", help="time how long each test takes"
630 )
630 )
631 reporting.add_argument("--view", help="external diff viewer")
631 reporting.add_argument("--view", help="external diff viewer")
632 reporting.add_argument(
632 reporting.add_argument(
633 "--xunit", help="record xunit results at specified path"
633 "--xunit", help="record xunit results at specified path"
634 )
634 )
635
635
636 for option, (envvar, default) in defaults.items():
636 for option, (envvar, default) in defaults.items():
637 defaults[option] = type(default)(os.environ.get(envvar, default))
637 defaults[option] = type(default)(os.environ.get(envvar, default))
638 parser.set_defaults(**defaults)
638 parser.set_defaults(**defaults)
639
639
640 return parser
640 return parser
641
641
642
642
643 def parseargs(args, parser):
643 def parseargs(args, parser):
644 """Parse arguments with our OptionParser and validate results."""
644 """Parse arguments with our OptionParser and validate results."""
645 options = parser.parse_args(args)
645 options = parser.parse_args(args)
646
646
647 # jython is always pure
647 # jython is always pure
648 if 'java' in sys.platform or '__pypy__' in sys.modules:
648 if 'java' in sys.platform or '__pypy__' in sys.modules:
649 options.pure = True
649 options.pure = True
650
650
651 if platform.python_implementation() != 'CPython' and options.rust:
651 if platform.python_implementation() != 'CPython' and options.rust:
652 parser.error('Rust extensions are only available with CPython')
652 parser.error('Rust extensions are only available with CPython')
653
653
654 if options.pure and options.rust:
654 if options.pure and options.rust:
655 parser.error('--rust cannot be used with --pure')
655 parser.error('--rust cannot be used with --pure')
656
656
657 if options.rust and options.no_rust:
657 if options.rust and options.no_rust:
658 parser.error('--rust cannot be used with --no-rust')
658 parser.error('--rust cannot be used with --no-rust')
659
659
660 if options.local:
660 if options.local:
661 if options.with_hg or options.with_chg:
661 if options.with_hg or options.with_chg:
662 parser.error('--local cannot be used with --with-hg or --with-chg')
662 parser.error('--local cannot be used with --with-hg or --with-chg')
663 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
663 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
664 reporootdir = os.path.dirname(testdir)
664 reporootdir = os.path.dirname(testdir)
665 pathandattrs = [(b'hg', 'with_hg')]
665 pathandattrs = [(b'hg', 'with_hg')]
666 if options.chg:
666 if options.chg:
667 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
667 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
668 for relpath, attr in pathandattrs:
668 for relpath, attr in pathandattrs:
669 binpath = os.path.join(reporootdir, relpath)
669 binpath = os.path.join(reporootdir, relpath)
670 if os.name != 'nt' and not os.access(binpath, os.X_OK):
670 if os.name != 'nt' and not os.access(binpath, os.X_OK):
671 parser.error(
671 parser.error(
672 '--local specified, but %r not found or '
672 '--local specified, but %r not found or '
673 'not executable' % binpath
673 'not executable' % binpath
674 )
674 )
675 setattr(options, attr, _bytes2sys(binpath))
675 setattr(options, attr, _bytes2sys(binpath))
676
676
677 if options.with_hg:
677 if options.with_hg:
678 options.with_hg = canonpath(_sys2bytes(options.with_hg))
678 options.with_hg = canonpath(_sys2bytes(options.with_hg))
679 if not (
679 if not (
680 os.path.isfile(options.with_hg)
680 os.path.isfile(options.with_hg)
681 and os.access(options.with_hg, os.X_OK)
681 and os.access(options.with_hg, os.X_OK)
682 ):
682 ):
683 parser.error('--with-hg must specify an executable hg script')
683 parser.error('--with-hg must specify an executable hg script')
684 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
684 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
685 sys.stderr.write('warning: --with-hg should specify an hg script\n')
685 sys.stderr.write('warning: --with-hg should specify an hg script\n')
686 sys.stderr.flush()
686 sys.stderr.flush()
687
687
688 if (options.chg or options.with_chg) and os.name == 'nt':
688 if (options.chg or options.with_chg) and os.name == 'nt':
689 parser.error('chg does not work on %s' % os.name)
689 parser.error('chg does not work on %s' % os.name)
690 if options.with_chg:
690 if options.with_chg:
691 options.chg = False # no installation to temporary location
691 options.chg = False # no installation to temporary location
692 options.with_chg = canonpath(_sys2bytes(options.with_chg))
692 options.with_chg = canonpath(_sys2bytes(options.with_chg))
693 if not (
693 if not (
694 os.path.isfile(options.with_chg)
694 os.path.isfile(options.with_chg)
695 and os.access(options.with_chg, os.X_OK)
695 and os.access(options.with_chg, os.X_OK)
696 ):
696 ):
697 parser.error('--with-chg must specify a chg executable')
697 parser.error('--with-chg must specify a chg executable')
698 if options.chg and options.with_hg:
698 if options.chg and options.with_hg:
699 # chg shares installation location with hg
699 # chg shares installation location with hg
700 parser.error(
700 parser.error(
701 '--chg does not work when --with-hg is specified '
701 '--chg does not work when --with-hg is specified '
702 '(use --with-chg instead)'
702 '(use --with-chg instead)'
703 )
703 )
704
704
705 if options.color == 'always' and not pygmentspresent:
705 if options.color == 'always' and not pygmentspresent:
706 sys.stderr.write(
706 sys.stderr.write(
707 'warning: --color=always ignored because '
707 'warning: --color=always ignored because '
708 'pygments is not installed\n'
708 'pygments is not installed\n'
709 )
709 )
710
710
711 if options.bisect_repo and not options.known_good_rev:
711 if options.bisect_repo and not options.known_good_rev:
712 parser.error("--bisect-repo cannot be used without --known-good-rev")
712 parser.error("--bisect-repo cannot be used without --known-good-rev")
713
713
714 global useipv6
714 global useipv6
715 if options.ipv6:
715 if options.ipv6:
716 useipv6 = checksocketfamily('AF_INET6')
716 useipv6 = checksocketfamily('AF_INET6')
717 else:
717 else:
718 # only use IPv6 if IPv4 is unavailable and IPv6 is available
718 # only use IPv6 if IPv4 is unavailable and IPv6 is available
719 useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily(
719 useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily(
720 'AF_INET6'
720 'AF_INET6'
721 )
721 )
722
722
723 options.anycoverage = options.cover or options.annotate or options.htmlcov
723 options.anycoverage = options.cover or options.annotate or options.htmlcov
724 if options.anycoverage:
724 if options.anycoverage:
725 try:
725 try:
726 import coverage
726 import coverage
727
727
728 covver = version.StrictVersion(coverage.__version__).version
728 covver = version.StrictVersion(coverage.__version__).version
729 if covver < (3, 3):
729 if covver < (3, 3):
730 parser.error('coverage options require coverage 3.3 or later')
730 parser.error('coverage options require coverage 3.3 or later')
731 except ImportError:
731 except ImportError:
732 parser.error('coverage options now require the coverage package')
732 parser.error('coverage options now require the coverage package')
733
733
734 if options.anycoverage and options.local:
734 if options.anycoverage and options.local:
735 # this needs some path mangling somewhere, I guess
735 # this needs some path mangling somewhere, I guess
736 parser.error(
736 parser.error(
737 "sorry, coverage options do not work when --local " "is specified"
737 "sorry, coverage options do not work when --local " "is specified"
738 )
738 )
739
739
740 if options.anycoverage and options.with_hg:
740 if options.anycoverage and options.with_hg:
741 parser.error(
741 parser.error(
742 "sorry, coverage options do not work when --with-hg " "is specified"
742 "sorry, coverage options do not work when --with-hg " "is specified"
743 )
743 )
744
744
745 global verbose
745 global verbose
746 if options.verbose:
746 if options.verbose:
747 verbose = ''
747 verbose = ''
748
748
749 if options.tmpdir:
749 if options.tmpdir:
750 options.tmpdir = canonpath(options.tmpdir)
750 options.tmpdir = canonpath(options.tmpdir)
751
751
752 if options.jobs < 1:
752 if options.jobs < 1:
753 parser.error('--jobs must be positive')
753 parser.error('--jobs must be positive')
754 if options.interactive and options.debug:
754 if options.interactive and options.debug:
755 parser.error("-i/--interactive and -d/--debug are incompatible")
755 parser.error("-i/--interactive and -d/--debug are incompatible")
756 if options.debug:
756 if options.debug:
757 if options.timeout != defaults['timeout']:
757 if options.timeout != defaults['timeout']:
758 sys.stderr.write('warning: --timeout option ignored with --debug\n')
758 sys.stderr.write('warning: --timeout option ignored with --debug\n')
759 if options.slowtimeout != defaults['slowtimeout']:
759 if options.slowtimeout != defaults['slowtimeout']:
760 sys.stderr.write(
760 sys.stderr.write(
761 'warning: --slowtimeout option ignored with --debug\n'
761 'warning: --slowtimeout option ignored with --debug\n'
762 )
762 )
763 options.timeout = 0
763 options.timeout = 0
764 options.slowtimeout = 0
764 options.slowtimeout = 0
765
765
766 if options.blacklist:
766 if options.blacklist:
767 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
767 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
768 if options.whitelist:
768 if options.whitelist:
769 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
769 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
770 else:
770 else:
771 options.whitelisted = {}
771 options.whitelisted = {}
772
772
773 if options.showchannels:
773 if options.showchannels:
774 options.nodiff = True
774 options.nodiff = True
775
775
776 return options
776 return options
777
777
778
778
779 def rename(src, dst):
779 def rename(src, dst):
780 """Like os.rename(), trade atomicity and opened files friendliness
780 """Like os.rename(), trade atomicity and opened files friendliness
781 for existing destination support.
781 for existing destination support.
782 """
782 """
783 shutil.copy(src, dst)
783 shutil.copy(src, dst)
784 os.remove(src)
784 os.remove(src)
785
785
786
786
787 def makecleanable(path):
787 def makecleanable(path):
788 """Try to fix directory permission recursively so that the entire tree
788 """Try to fix directory permission recursively so that the entire tree
789 can be deleted"""
789 can be deleted"""
790 for dirpath, dirnames, _filenames in os.walk(path, topdown=True):
790 for dirpath, dirnames, _filenames in os.walk(path, topdown=True):
791 for d in dirnames:
791 for d in dirnames:
792 p = os.path.join(dirpath, d)
792 p = os.path.join(dirpath, d)
793 try:
793 try:
794 os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx
794 os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx
795 except OSError:
795 except OSError:
796 pass
796 pass
797
797
798
798
799 _unified_diff = difflib.unified_diff
799 _unified_diff = difflib.unified_diff
800 if PYTHON3:
800 if PYTHON3:
801 import functools
801 import functools
802
802
803 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
803 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
804
804
805
805
806 def getdiff(expected, output, ref, err):
806 def getdiff(expected, output, ref, err):
807 servefail = False
807 servefail = False
808 lines = []
808 lines = []
809 for line in _unified_diff(expected, output, ref, err):
809 for line in _unified_diff(expected, output, ref, err):
810 if line.startswith(b'+++') or line.startswith(b'---'):
810 if line.startswith(b'+++') or line.startswith(b'---'):
811 line = line.replace(b'\\', b'/')
811 line = line.replace(b'\\', b'/')
812 if line.endswith(b' \n'):
812 if line.endswith(b' \n'):
813 line = line[:-2] + b'\n'
813 line = line[:-2] + b'\n'
814 lines.append(line)
814 lines.append(line)
815 if not servefail and line.startswith(
815 if not servefail and line.startswith(
816 b'+ abort: child process failed to start'
816 b'+ abort: child process failed to start'
817 ):
817 ):
818 servefail = True
818 servefail = True
819
819
820 return servefail, lines
820 return servefail, lines
821
821
822
822
823 verbose = False
823 verbose = False
824
824
825
825
826 def vlog(*msg):
826 def vlog(*msg):
827 """Log only when in verbose mode."""
827 """Log only when in verbose mode."""
828 if verbose is False:
828 if verbose is False:
829 return
829 return
830
830
831 return log(*msg)
831 return log(*msg)
832
832
833
833
834 # Bytes that break XML even in a CDATA block: control characters 0-31
834 # Bytes that break XML even in a CDATA block: control characters 0-31
835 # sans \t, \n and \r
835 # sans \t, \n and \r
836 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
836 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
837
837
838 # Match feature conditionalized output lines in the form, capturing the feature
838 # Match feature conditionalized output lines in the form, capturing the feature
839 # list in group 2, and the preceeding line output in group 1:
839 # list in group 2, and the preceeding line output in group 1:
840 #
840 #
841 # output..output (feature !)\n
841 # output..output (feature !)\n
842 optline = re.compile(br'(.*) \((.+?) !\)\n$')
842 optline = re.compile(br'(.*) \((.+?) !\)\n$')
843
843
844
844
845 def cdatasafe(data):
845 def cdatasafe(data):
846 """Make a string safe to include in a CDATA block.
846 """Make a string safe to include in a CDATA block.
847
847
848 Certain control characters are illegal in a CDATA block, and
848 Certain control characters are illegal in a CDATA block, and
849 there's no way to include a ]]> in a CDATA either. This function
849 there's no way to include a ]]> in a CDATA either. This function
850 replaces illegal bytes with ? and adds a space between the ]] so
850 replaces illegal bytes with ? and adds a space between the ]] so
851 that it won't break the CDATA block.
851 that it won't break the CDATA block.
852 """
852 """
853 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
853 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
854
854
855
855
856 def log(*msg):
856 def log(*msg):
857 """Log something to stdout.
857 """Log something to stdout.
858
858
859 Arguments are strings to print.
859 Arguments are strings to print.
860 """
860 """
861 with iolock:
861 with iolock:
862 if verbose:
862 if verbose:
863 print(verbose, end=' ')
863 print(verbose, end=' ')
864 for m in msg:
864 for m in msg:
865 print(m, end=' ')
865 print(m, end=' ')
866 print()
866 print()
867 sys.stdout.flush()
867 sys.stdout.flush()
868
868
869
869
870 def highlightdiff(line, color):
870 def highlightdiff(line, color):
871 if not color:
871 if not color:
872 return line
872 return line
873 assert pygmentspresent
873 assert pygmentspresent
874 return pygments.highlight(
874 return pygments.highlight(
875 line.decode('latin1'), difflexer, terminal256formatter
875 line.decode('latin1'), difflexer, terminal256formatter
876 ).encode('latin1')
876 ).encode('latin1')
877
877
878
878
879 def highlightmsg(msg, color):
879 def highlightmsg(msg, color):
880 if not color:
880 if not color:
881 return msg
881 return msg
882 assert pygmentspresent
882 assert pygmentspresent
883 return pygments.highlight(msg, runnerlexer, runnerformatter)
883 return pygments.highlight(msg, runnerlexer, runnerformatter)
884
884
885
885
886 def terminate(proc):
886 def terminate(proc):
887 """Terminate subprocess"""
887 """Terminate subprocess"""
888 vlog('# Terminating process %d' % proc.pid)
888 vlog('# Terminating process %d' % proc.pid)
889 try:
889 try:
890 proc.terminate()
890 proc.terminate()
891 except OSError:
891 except OSError:
892 pass
892 pass
893
893
894
894
895 def killdaemons(pidfile):
895 def killdaemons(pidfile):
896 import killdaemons as killmod
896 import killdaemons as killmod
897
897
898 return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog)
898 return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog)
899
899
900
900
901 class Test(unittest.TestCase):
901 class Test(unittest.TestCase):
902 """Encapsulates a single, runnable test.
902 """Encapsulates a single, runnable test.
903
903
904 While this class conforms to the unittest.TestCase API, it differs in that
904 While this class conforms to the unittest.TestCase API, it differs in that
905 instances need to be instantiated manually. (Typically, unittest.TestCase
905 instances need to be instantiated manually. (Typically, unittest.TestCase
906 classes are instantiated automatically by scanning modules.)
906 classes are instantiated automatically by scanning modules.)
907 """
907 """
908
908
909 # Status code reserved for skipped tests (used by hghave).
909 # Status code reserved for skipped tests (used by hghave).
910 SKIPPED_STATUS = 80
910 SKIPPED_STATUS = 80
911
911
912 def __init__(
912 def __init__(
913 self,
913 self,
914 path,
914 path,
915 outputdir,
915 outputdir,
916 tmpdir,
916 tmpdir,
917 keeptmpdir=False,
917 keeptmpdir=False,
918 debug=False,
918 debug=False,
919 first=False,
919 first=False,
920 timeout=None,
920 timeout=None,
921 startport=None,
921 startport=None,
922 extraconfigopts=None,
922 extraconfigopts=None,
923 shell=None,
923 shell=None,
924 hgcommand=None,
924 hgcommand=None,
925 slowtimeout=None,
925 slowtimeout=None,
926 usechg=False,
926 usechg=False,
927 useipv6=False,
927 useipv6=False,
928 ):
928 ):
929 """Create a test from parameters.
929 """Create a test from parameters.
930
930
931 path is the full path to the file defining the test.
931 path is the full path to the file defining the test.
932
932
933 tmpdir is the main temporary directory to use for this test.
933 tmpdir is the main temporary directory to use for this test.
934
934
935 keeptmpdir determines whether to keep the test's temporary directory
935 keeptmpdir determines whether to keep the test's temporary directory
936 after execution. It defaults to removal (False).
936 after execution. It defaults to removal (False).
937
937
938 debug mode will make the test execute verbosely, with unfiltered
938 debug mode will make the test execute verbosely, with unfiltered
939 output.
939 output.
940
940
941 timeout controls the maximum run time of the test. It is ignored when
941 timeout controls the maximum run time of the test. It is ignored when
942 debug is True. See slowtimeout for tests with #require slow.
942 debug is True. See slowtimeout for tests with #require slow.
943
943
944 slowtimeout overrides timeout if the test has #require slow.
944 slowtimeout overrides timeout if the test has #require slow.
945
945
946 startport controls the starting port number to use for this test. Each
946 startport controls the starting port number to use for this test. Each
947 test will reserve 3 port numbers for execution. It is the caller's
947 test will reserve 3 port numbers for execution. It is the caller's
948 responsibility to allocate a non-overlapping port range to Test
948 responsibility to allocate a non-overlapping port range to Test
949 instances.
949 instances.
950
950
951 extraconfigopts is an iterable of extra hgrc config options. Values
951 extraconfigopts is an iterable of extra hgrc config options. Values
952 must have the form "key=value" (something understood by hgrc). Values
952 must have the form "key=value" (something understood by hgrc). Values
953 of the form "foo.key=value" will result in "[foo] key=value".
953 of the form "foo.key=value" will result in "[foo] key=value".
954
954
955 shell is the shell to execute tests in.
955 shell is the shell to execute tests in.
956 """
956 """
957 if timeout is None:
957 if timeout is None:
958 timeout = defaults['timeout']
958 timeout = defaults['timeout']
959 if startport is None:
959 if startport is None:
960 startport = defaults['port']
960 startport = defaults['port']
961 if slowtimeout is None:
961 if slowtimeout is None:
962 slowtimeout = defaults['slowtimeout']
962 slowtimeout = defaults['slowtimeout']
963 self.path = path
963 self.path = path
964 self.bname = os.path.basename(path)
964 self.bname = os.path.basename(path)
965 self.name = _bytes2sys(self.bname)
965 self.name = _bytes2sys(self.bname)
966 self._testdir = os.path.dirname(path)
966 self._testdir = os.path.dirname(path)
967 self._outputdir = outputdir
967 self._outputdir = outputdir
968 self._tmpname = os.path.basename(path)
968 self._tmpname = os.path.basename(path)
969 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
969 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
970
970
971 self._threadtmp = tmpdir
971 self._threadtmp = tmpdir
972 self._keeptmpdir = keeptmpdir
972 self._keeptmpdir = keeptmpdir
973 self._debug = debug
973 self._debug = debug
974 self._first = first
974 self._first = first
975 self._timeout = timeout
975 self._timeout = timeout
976 self._slowtimeout = slowtimeout
976 self._slowtimeout = slowtimeout
977 self._startport = startport
977 self._startport = startport
978 self._extraconfigopts = extraconfigopts or []
978 self._extraconfigopts = extraconfigopts or []
979 self._shell = _sys2bytes(shell)
979 self._shell = _sys2bytes(shell)
980 self._hgcommand = hgcommand or b'hg'
980 self._hgcommand = hgcommand or b'hg'
981 self._usechg = usechg
981 self._usechg = usechg
982 self._useipv6 = useipv6
982 self._useipv6 = useipv6
983
983
984 self._aborted = False
984 self._aborted = False
985 self._daemonpids = []
985 self._daemonpids = []
986 self._finished = None
986 self._finished = None
987 self._ret = None
987 self._ret = None
988 self._out = None
988 self._out = None
989 self._skipped = None
989 self._skipped = None
990 self._testtmp = None
990 self._testtmp = None
991 self._chgsockdir = None
991 self._chgsockdir = None
992
992
993 self._refout = self.readrefout()
993 self._refout = self.readrefout()
994
994
995 def readrefout(self):
995 def readrefout(self):
996 """read reference output"""
996 """read reference output"""
997 # If we're not in --debug mode and reference output file exists,
997 # If we're not in --debug mode and reference output file exists,
998 # check test output against it.
998 # check test output against it.
999 if self._debug:
999 if self._debug:
1000 return None # to match "out is None"
1000 return None # to match "out is None"
1001 elif os.path.exists(self.refpath):
1001 elif os.path.exists(self.refpath):
1002 with open(self.refpath, 'rb') as f:
1002 with open(self.refpath, 'rb') as f:
1003 return f.read().splitlines(True)
1003 return f.read().splitlines(True)
1004 else:
1004 else:
1005 return []
1005 return []
1006
1006
1007 # needed to get base class __repr__ running
1007 # needed to get base class __repr__ running
1008 @property
1008 @property
1009 def _testMethodName(self):
1009 def _testMethodName(self):
1010 return self.name
1010 return self.name
1011
1011
1012 def __str__(self):
1012 def __str__(self):
1013 return self.name
1013 return self.name
1014
1014
1015 def shortDescription(self):
1015 def shortDescription(self):
1016 return self.name
1016 return self.name
1017
1017
1018 def setUp(self):
1018 def setUp(self):
1019 """Tasks to perform before run()."""
1019 """Tasks to perform before run()."""
1020 self._finished = False
1020 self._finished = False
1021 self._ret = None
1021 self._ret = None
1022 self._out = None
1022 self._out = None
1023 self._skipped = None
1023 self._skipped = None
1024
1024
1025 try:
1025 try:
1026 os.mkdir(self._threadtmp)
1026 os.mkdir(self._threadtmp)
1027 except OSError as e:
1027 except OSError as e:
1028 if e.errno != errno.EEXIST:
1028 if e.errno != errno.EEXIST:
1029 raise
1029 raise
1030
1030
1031 name = self._tmpname
1031 name = self._tmpname
1032 self._testtmp = os.path.join(self._threadtmp, name)
1032 self._testtmp = os.path.join(self._threadtmp, name)
1033 os.mkdir(self._testtmp)
1033 os.mkdir(self._testtmp)
1034
1034
1035 # Remove any previous output files.
1035 # Remove any previous output files.
1036 if os.path.exists(self.errpath):
1036 if os.path.exists(self.errpath):
1037 try:
1037 try:
1038 os.remove(self.errpath)
1038 os.remove(self.errpath)
1039 except OSError as e:
1039 except OSError as e:
1040 # We might have raced another test to clean up a .err
1040 # We might have raced another test to clean up a .err
1041 # file, so ignore ENOENT when removing a previous .err
1041 # file, so ignore ENOENT when removing a previous .err
1042 # file.
1042 # file.
1043 if e.errno != errno.ENOENT:
1043 if e.errno != errno.ENOENT:
1044 raise
1044 raise
1045
1045
1046 if self._usechg:
1046 if self._usechg:
1047 self._chgsockdir = os.path.join(
1047 self._chgsockdir = os.path.join(
1048 self._threadtmp, b'%s.chgsock' % name
1048 self._threadtmp, b'%s.chgsock' % name
1049 )
1049 )
1050 os.mkdir(self._chgsockdir)
1050 os.mkdir(self._chgsockdir)
1051
1051
1052 def run(self, result):
1052 def run(self, result):
1053 """Run this test and report results against a TestResult instance."""
1053 """Run this test and report results against a TestResult instance."""
1054 # This function is extremely similar to unittest.TestCase.run(). Once
1054 # This function is extremely similar to unittest.TestCase.run(). Once
1055 # we require Python 2.7 (or at least its version of unittest), this
1055 # we require Python 2.7 (or at least its version of unittest), this
1056 # function can largely go away.
1056 # function can largely go away.
1057 self._result = result
1057 self._result = result
1058 result.startTest(self)
1058 result.startTest(self)
1059 try:
1059 try:
1060 try:
1060 try:
1061 self.setUp()
1061 self.setUp()
1062 except (KeyboardInterrupt, SystemExit):
1062 except (KeyboardInterrupt, SystemExit):
1063 self._aborted = True
1063 self._aborted = True
1064 raise
1064 raise
1065 except Exception:
1065 except Exception:
1066 result.addError(self, sys.exc_info())
1066 result.addError(self, sys.exc_info())
1067 return
1067 return
1068
1068
1069 success = False
1069 success = False
1070 try:
1070 try:
1071 self.runTest()
1071 self.runTest()
1072 except KeyboardInterrupt:
1072 except KeyboardInterrupt:
1073 self._aborted = True
1073 self._aborted = True
1074 raise
1074 raise
1075 except unittest.SkipTest as e:
1075 except unittest.SkipTest as e:
1076 result.addSkip(self, str(e))
1076 result.addSkip(self, str(e))
1077 # The base class will have already counted this as a
1077 # The base class will have already counted this as a
1078 # test we "ran", but we want to exclude skipped tests
1078 # test we "ran", but we want to exclude skipped tests
1079 # from those we count towards those run.
1079 # from those we count towards those run.
1080 result.testsRun -= 1
1080 result.testsRun -= 1
1081 except self.failureException as e:
1081 except self.failureException as e:
1082 # This differs from unittest in that we don't capture
1082 # This differs from unittest in that we don't capture
1083 # the stack trace. This is for historical reasons and
1083 # the stack trace. This is for historical reasons and
1084 # this decision could be revisited in the future,
1084 # this decision could be revisited in the future,
1085 # especially for PythonTest instances.
1085 # especially for PythonTest instances.
1086 if result.addFailure(self, str(e)):
1086 if result.addFailure(self, str(e)):
1087 success = True
1087 success = True
1088 except Exception:
1088 except Exception:
1089 result.addError(self, sys.exc_info())
1089 result.addError(self, sys.exc_info())
1090 else:
1090 else:
1091 success = True
1091 success = True
1092
1092
1093 try:
1093 try:
1094 self.tearDown()
1094 self.tearDown()
1095 except (KeyboardInterrupt, SystemExit):
1095 except (KeyboardInterrupt, SystemExit):
1096 self._aborted = True
1096 self._aborted = True
1097 raise
1097 raise
1098 except Exception:
1098 except Exception:
1099 result.addError(self, sys.exc_info())
1099 result.addError(self, sys.exc_info())
1100 success = False
1100 success = False
1101
1101
1102 if success:
1102 if success:
1103 result.addSuccess(self)
1103 result.addSuccess(self)
1104 finally:
1104 finally:
1105 result.stopTest(self, interrupted=self._aborted)
1105 result.stopTest(self, interrupted=self._aborted)
1106
1106
1107 def runTest(self):
1107 def runTest(self):
1108 """Run this test instance.
1108 """Run this test instance.
1109
1109
1110 This will return a tuple describing the result of the test.
1110 This will return a tuple describing the result of the test.
1111 """
1111 """
1112 env = self._getenv()
1112 env = self._getenv()
1113 self._genrestoreenv(env)
1113 self._genrestoreenv(env)
1114 self._daemonpids.append(env['DAEMON_PIDS'])
1114 self._daemonpids.append(env['DAEMON_PIDS'])
1115 self._createhgrc(env['HGRCPATH'])
1115 self._createhgrc(env['HGRCPATH'])
1116
1116
1117 vlog('# Test', self.name)
1117 vlog('# Test', self.name)
1118
1118
1119 ret, out = self._run(env)
1119 ret, out = self._run(env)
1120 self._finished = True
1120 self._finished = True
1121 self._ret = ret
1121 self._ret = ret
1122 self._out = out
1122 self._out = out
1123
1123
1124 def describe(ret):
1124 def describe(ret):
1125 if ret < 0:
1125 if ret < 0:
1126 return 'killed by signal: %d' % -ret
1126 return 'killed by signal: %d' % -ret
1127 return 'returned error code %d' % ret
1127 return 'returned error code %d' % ret
1128
1128
1129 self._skipped = False
1129 self._skipped = False
1130
1130
1131 if ret == self.SKIPPED_STATUS:
1131 if ret == self.SKIPPED_STATUS:
1132 if out is None: # Debug mode, nothing to parse.
1132 if out is None: # Debug mode, nothing to parse.
1133 missing = ['unknown']
1133 missing = ['unknown']
1134 failed = None
1134 failed = None
1135 else:
1135 else:
1136 missing, failed = TTest.parsehghaveoutput(out)
1136 missing, failed = TTest.parsehghaveoutput(out)
1137
1137
1138 if not missing:
1138 if not missing:
1139 missing = ['skipped']
1139 missing = ['skipped']
1140
1140
1141 if failed:
1141 if failed:
1142 self.fail('hg have failed checking for %s' % failed[-1])
1142 self.fail('hg have failed checking for %s' % failed[-1])
1143 else:
1143 else:
1144 self._skipped = True
1144 self._skipped = True
1145 raise unittest.SkipTest(missing[-1])
1145 raise unittest.SkipTest(missing[-1])
1146 elif ret == 'timeout':
1146 elif ret == 'timeout':
1147 self.fail('timed out')
1147 self.fail('timed out')
1148 elif ret is False:
1148 elif ret is False:
1149 self.fail('no result code from test')
1149 self.fail('no result code from test')
1150 elif out != self._refout:
1150 elif out != self._refout:
1151 # Diff generation may rely on written .err file.
1151 # Diff generation may rely on written .err file.
1152 if (
1152 if (
1153 (ret != 0 or out != self._refout)
1153 (ret != 0 or out != self._refout)
1154 and not self._skipped
1154 and not self._skipped
1155 and not self._debug
1155 and not self._debug
1156 ):
1156 ):
1157 with open(self.errpath, 'wb') as f:
1157 with open(self.errpath, 'wb') as f:
1158 for line in out:
1158 for line in out:
1159 f.write(line)
1159 f.write(line)
1160
1160
1161 # The result object handles diff calculation for us.
1161 # The result object handles diff calculation for us.
1162 with firstlock:
1162 with firstlock:
1163 if self._result.addOutputMismatch(self, ret, out, self._refout):
1163 if self._result.addOutputMismatch(self, ret, out, self._refout):
1164 # change was accepted, skip failing
1164 # change was accepted, skip failing
1165 return
1165 return
1166 if self._first:
1166 if self._first:
1167 global firsterror
1167 global firsterror
1168 firsterror = True
1168 firsterror = True
1169
1169
1170 if ret:
1170 if ret:
1171 msg = 'output changed and ' + describe(ret)
1171 msg = 'output changed and ' + describe(ret)
1172 else:
1172 else:
1173 msg = 'output changed'
1173 msg = 'output changed'
1174
1174
1175 self.fail(msg)
1175 self.fail(msg)
1176 elif ret:
1176 elif ret:
1177 self.fail(describe(ret))
1177 self.fail(describe(ret))
1178
1178
1179 def tearDown(self):
1179 def tearDown(self):
1180 """Tasks to perform after run()."""
1180 """Tasks to perform after run()."""
1181 for entry in self._daemonpids:
1181 for entry in self._daemonpids:
1182 killdaemons(entry)
1182 killdaemons(entry)
1183 self._daemonpids = []
1183 self._daemonpids = []
1184
1184
1185 if self._keeptmpdir:
1185 if self._keeptmpdir:
1186 log(
1186 log(
1187 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1187 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1188 % (_bytes2sys(self._testtmp), _bytes2sys(self._threadtmp),)
1188 % (_bytes2sys(self._testtmp), _bytes2sys(self._threadtmp),)
1189 )
1189 )
1190 else:
1190 else:
1191 try:
1191 try:
1192 shutil.rmtree(self._testtmp)
1192 shutil.rmtree(self._testtmp)
1193 except OSError:
1193 except OSError:
1194 # unreadable directory may be left in $TESTTMP; fix permission
1194 # unreadable directory may be left in $TESTTMP; fix permission
1195 # and try again
1195 # and try again
1196 makecleanable(self._testtmp)
1196 makecleanable(self._testtmp)
1197 shutil.rmtree(self._testtmp, True)
1197 shutil.rmtree(self._testtmp, True)
1198 shutil.rmtree(self._threadtmp, True)
1198 shutil.rmtree(self._threadtmp, True)
1199
1199
1200 if self._usechg:
1200 if self._usechg:
1201 # chgservers will stop automatically after they find the socket
1201 # chgservers will stop automatically after they find the socket
1202 # files are deleted
1202 # files are deleted
1203 shutil.rmtree(self._chgsockdir, True)
1203 shutil.rmtree(self._chgsockdir, True)
1204
1204
1205 if (
1205 if (
1206 (self._ret != 0 or self._out != self._refout)
1206 (self._ret != 0 or self._out != self._refout)
1207 and not self._skipped
1207 and not self._skipped
1208 and not self._debug
1208 and not self._debug
1209 and self._out
1209 and self._out
1210 ):
1210 ):
1211 with open(self.errpath, 'wb') as f:
1211 with open(self.errpath, 'wb') as f:
1212 for line in self._out:
1212 for line in self._out:
1213 f.write(line)
1213 f.write(line)
1214
1214
1215 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1215 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1216
1216
1217 def _run(self, env):
1217 def _run(self, env):
1218 # This should be implemented in child classes to run tests.
1218 # This should be implemented in child classes to run tests.
1219 raise unittest.SkipTest('unknown test type')
1219 raise unittest.SkipTest('unknown test type')
1220
1220
1221 def abort(self):
1221 def abort(self):
1222 """Terminate execution of this test."""
1222 """Terminate execution of this test."""
1223 self._aborted = True
1223 self._aborted = True
1224
1224
1225 def _portmap(self, i):
1225 def _portmap(self, i):
1226 offset = b'' if i == 0 else b'%d' % i
1226 offset = b'' if i == 0 else b'%d' % i
1227 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1227 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1228
1228
1229 def _getreplacements(self):
1229 def _getreplacements(self):
1230 """Obtain a mapping of text replacements to apply to test output.
1230 """Obtain a mapping of text replacements to apply to test output.
1231
1231
1232 Test output needs to be normalized so it can be compared to expected
1232 Test output needs to be normalized so it can be compared to expected
1233 output. This function defines how some of that normalization will
1233 output. This function defines how some of that normalization will
1234 occur.
1234 occur.
1235 """
1235 """
1236 r = [
1236 r = [
1237 # This list should be parallel to defineport in _getenv
1237 # This list should be parallel to defineport in _getenv
1238 self._portmap(0),
1238 self._portmap(0),
1239 self._portmap(1),
1239 self._portmap(1),
1240 self._portmap(2),
1240 self._portmap(2),
1241 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1241 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1242 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1242 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1243 ]
1243 ]
1244 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1244 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1245
1245
1246 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1246 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1247
1247
1248 if os.path.exists(replacementfile):
1248 if os.path.exists(replacementfile):
1249 data = {}
1249 data = {}
1250 with open(replacementfile, mode='rb') as source:
1250 with open(replacementfile, mode='rb') as source:
1251 # the intermediate 'compile' step help with debugging
1251 # the intermediate 'compile' step help with debugging
1252 code = compile(source.read(), replacementfile, 'exec')
1252 code = compile(source.read(), replacementfile, 'exec')
1253 exec(code, data)
1253 exec(code, data)
1254 for value in data.get('substitutions', ()):
1254 for value in data.get('substitutions', ()):
1255 if len(value) != 2:
1255 if len(value) != 2:
1256 msg = 'malformatted substitution in %s: %r'
1256 msg = 'malformatted substitution in %s: %r'
1257 msg %= (replacementfile, value)
1257 msg %= (replacementfile, value)
1258 raise ValueError(msg)
1258 raise ValueError(msg)
1259 r.append(value)
1259 r.append(value)
1260 return r
1260 return r
1261
1261
1262 def _escapepath(self, p):
1262 def _escapepath(self, p):
1263 if os.name == 'nt':
1263 if os.name == 'nt':
1264 return b''.join(
1264 return b''.join(
1265 c.isalpha()
1265 c.isalpha()
1266 and b'[%s%s]' % (c.lower(), c.upper())
1266 and b'[%s%s]' % (c.lower(), c.upper())
1267 or c in b'/\\'
1267 or c in b'/\\'
1268 and br'[/\\]'
1268 and br'[/\\]'
1269 or c.isdigit()
1269 or c.isdigit()
1270 and c
1270 and c
1271 or b'\\' + c
1271 or b'\\' + c
1272 for c in [p[i : i + 1] for i in range(len(p))]
1272 for c in [p[i : i + 1] for i in range(len(p))]
1273 )
1273 )
1274 else:
1274 else:
1275 return re.escape(p)
1275 return re.escape(p)
1276
1276
1277 def _localip(self):
1277 def _localip(self):
1278 if self._useipv6:
1278 if self._useipv6:
1279 return b'::1'
1279 return b'::1'
1280 else:
1280 else:
1281 return b'127.0.0.1'
1281 return b'127.0.0.1'
1282
1282
1283 def _genrestoreenv(self, testenv):
1283 def _genrestoreenv(self, testenv):
1284 """Generate a script that can be used by tests to restore the original
1284 """Generate a script that can be used by tests to restore the original
1285 environment."""
1285 environment."""
1286 # Put the restoreenv script inside self._threadtmp
1286 # Put the restoreenv script inside self._threadtmp
1287 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1287 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1288 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1288 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1289
1289
1290 # Only restore environment variable names that the shell allows
1290 # Only restore environment variable names that the shell allows
1291 # us to export.
1291 # us to export.
1292 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1292 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1293
1293
1294 # Do not restore these variables; otherwise tests would fail.
1294 # Do not restore these variables; otherwise tests would fail.
1295 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1295 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1296
1296
1297 with open(scriptpath, 'w') as envf:
1297 with open(scriptpath, 'w') as envf:
1298 for name, value in origenviron.items():
1298 for name, value in origenviron.items():
1299 if not name_regex.match(name):
1299 if not name_regex.match(name):
1300 # Skip environment variables with unusual names not
1300 # Skip environment variables with unusual names not
1301 # allowed by most shells.
1301 # allowed by most shells.
1302 continue
1302 continue
1303 if name in reqnames:
1303 if name in reqnames:
1304 continue
1304 continue
1305 envf.write('%s=%s\n' % (name, shellquote(value)))
1305 envf.write('%s=%s\n' % (name, shellquote(value)))
1306
1306
1307 for name in testenv:
1307 for name in testenv:
1308 if name in origenviron or name in reqnames:
1308 if name in origenviron or name in reqnames:
1309 continue
1309 continue
1310 envf.write('unset %s\n' % (name,))
1310 envf.write('unset %s\n' % (name,))
1311
1311
1312 def _getenv(self):
1312 def _getenv(self):
1313 """Obtain environment variables to use during test execution."""
1313 """Obtain environment variables to use during test execution."""
1314
1314
1315 def defineport(i):
1315 def defineport(i):
1316 offset = '' if i == 0 else '%s' % i
1316 offset = '' if i == 0 else '%s' % i
1317 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1317 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1318
1318
1319 env = os.environ.copy()
1319 env = os.environ.copy()
1320 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1320 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1321 env['HGEMITWARNINGS'] = '1'
1321 env['HGEMITWARNINGS'] = '1'
1322 env['TESTTMP'] = _bytes2sys(self._testtmp)
1322 env['TESTTMP'] = _bytes2sys(self._testtmp)
1323 env['TESTNAME'] = self.name
1323 env['TESTNAME'] = self.name
1324 env['HOME'] = _bytes2sys(self._testtmp)
1324 env['HOME'] = _bytes2sys(self._testtmp)
1325 # This number should match portneeded in _getport
1325 # This number should match portneeded in _getport
1326 for port in xrange(3):
1326 for port in xrange(3):
1327 # This list should be parallel to _portmap in _getreplacements
1327 # This list should be parallel to _portmap in _getreplacements
1328 defineport(port)
1328 defineport(port)
1329 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1329 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1330 env["DAEMON_PIDS"] = _bytes2sys(
1330 env["DAEMON_PIDS"] = _bytes2sys(
1331 os.path.join(self._threadtmp, b'daemon.pids')
1331 os.path.join(self._threadtmp, b'daemon.pids')
1332 )
1332 )
1333 env["HGEDITOR"] = (
1333 env["HGEDITOR"] = (
1334 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1334 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1335 )
1335 )
1336 env["HGUSER"] = "test"
1336 env["HGUSER"] = "test"
1337 env["HGENCODING"] = "ascii"
1337 env["HGENCODING"] = "ascii"
1338 env["HGENCODINGMODE"] = "strict"
1338 env["HGENCODINGMODE"] = "strict"
1339 env["HGHOSTNAME"] = "test-hostname"
1339 env["HGHOSTNAME"] = "test-hostname"
1340 env['HGIPV6'] = str(int(self._useipv6))
1340 env['HGIPV6'] = str(int(self._useipv6))
1341 # See contrib/catapipe.py for how to use this functionality.
1341 # See contrib/catapipe.py for how to use this functionality.
1342 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1342 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1343 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1343 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1344 # non-test one in as a default, otherwise set to devnull
1344 # non-test one in as a default, otherwise set to devnull
1345 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1345 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1346 'HGCATAPULTSERVERPIPE', os.devnull
1346 'HGCATAPULTSERVERPIPE', os.devnull
1347 )
1347 )
1348
1348
1349 extraextensions = []
1349 extraextensions = []
1350 for opt in self._extraconfigopts:
1350 for opt in self._extraconfigopts:
1351 section, key = _sys2bytes(opt).split(b'.', 1)
1351 section, key = _sys2bytes(opt).split(b'.', 1)
1352 if section != 'extensions':
1352 if section != 'extensions':
1353 continue
1353 continue
1354 name = key.split(b'=', 1)[0]
1354 name = key.split(b'=', 1)[0]
1355 extraextensions.append(name)
1355 extraextensions.append(name)
1356
1356
1357 if extraextensions:
1357 if extraextensions:
1358 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1358 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1359
1359
1360 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1360 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1361 # IP addresses.
1361 # IP addresses.
1362 env['LOCALIP'] = _bytes2sys(self._localip())
1362 env['LOCALIP'] = _bytes2sys(self._localip())
1363
1363
1364 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1364 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1365 # but this is needed for testing python instances like dummyssh,
1365 # but this is needed for testing python instances like dummyssh,
1366 # dummysmtpd.py, and dumbhttp.py.
1366 # dummysmtpd.py, and dumbhttp.py.
1367 if PYTHON3 and os.name == 'nt':
1367 if PYTHON3 and os.name == 'nt':
1368 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1368 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1369
1369
1370 # Modified HOME in test environment can confuse Rust tools. So set
1370 # Modified HOME in test environment can confuse Rust tools. So set
1371 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1371 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1372 # present and these variables aren't already defined.
1372 # present and these variables aren't already defined.
1373 cargo_home_path = os.path.expanduser('~/.cargo')
1373 cargo_home_path = os.path.expanduser('~/.cargo')
1374 rustup_home_path = os.path.expanduser('~/.rustup')
1374 rustup_home_path = os.path.expanduser('~/.rustup')
1375
1375
1376 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1376 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1377 env['CARGO_HOME'] = cargo_home_path
1377 env['CARGO_HOME'] = cargo_home_path
1378 if (
1378 if (
1379 os.path.exists(rustup_home_path)
1379 os.path.exists(rustup_home_path)
1380 and b'RUSTUP_HOME' not in osenvironb
1380 and b'RUSTUP_HOME' not in osenvironb
1381 ):
1381 ):
1382 env['RUSTUP_HOME'] = rustup_home_path
1382 env['RUSTUP_HOME'] = rustup_home_path
1383
1383
1384 # Reset some environment variables to well-known values so that
1384 # Reset some environment variables to well-known values so that
1385 # the tests produce repeatable output.
1385 # the tests produce repeatable output.
1386 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1386 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1387 env['TZ'] = 'GMT'
1387 env['TZ'] = 'GMT'
1388 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1388 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1389 env['COLUMNS'] = '80'
1389 env['COLUMNS'] = '80'
1390 env['TERM'] = 'xterm'
1390 env['TERM'] = 'xterm'
1391
1391
1392 dropped = [
1392 dropped = [
1393 'CDPATH',
1393 'CDPATH',
1394 'CHGDEBUG',
1394 'CHGDEBUG',
1395 'EDITOR',
1395 'EDITOR',
1396 'GREP_OPTIONS',
1396 'GREP_OPTIONS',
1397 'HG',
1397 'HG',
1398 'HGMERGE',
1398 'HGMERGE',
1399 'HGPLAIN',
1399 'HGPLAIN',
1400 'HGPLAINEXCEPT',
1400 'HGPLAINEXCEPT',
1401 'HGPROF',
1401 'HGPROF',
1402 'http_proxy',
1402 'http_proxy',
1403 'no_proxy',
1403 'no_proxy',
1404 'NO_PROXY',
1404 'NO_PROXY',
1405 'PAGER',
1405 'PAGER',
1406 'VISUAL',
1406 'VISUAL',
1407 ]
1407 ]
1408
1408
1409 for k in dropped:
1409 for k in dropped:
1410 if k in env:
1410 if k in env:
1411 del env[k]
1411 del env[k]
1412
1412
1413 # unset env related to hooks
1413 # unset env related to hooks
1414 for k in list(env):
1414 for k in list(env):
1415 if k.startswith('HG_'):
1415 if k.startswith('HG_'):
1416 del env[k]
1416 del env[k]
1417
1417
1418 if self._usechg:
1418 if self._usechg:
1419 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1419 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1420
1420
1421 return env
1421 return env
1422
1422
1423 def _createhgrc(self, path):
1423 def _createhgrc(self, path):
1424 """Create an hgrc file for this test."""
1424 """Create an hgrc file for this test."""
1425 with open(path, 'wb') as hgrc:
1425 with open(path, 'wb') as hgrc:
1426 hgrc.write(b'[ui]\n')
1426 hgrc.write(b'[ui]\n')
1427 hgrc.write(b'slash = True\n')
1427 hgrc.write(b'slash = True\n')
1428 hgrc.write(b'interactive = False\n')
1428 hgrc.write(b'interactive = False\n')
1429 hgrc.write(b'merge = internal:merge\n')
1429 hgrc.write(b'merge = internal:merge\n')
1430 hgrc.write(b'mergemarkers = detailed\n')
1430 hgrc.write(b'mergemarkers = detailed\n')
1431 hgrc.write(b'promptecho = True\n')
1431 hgrc.write(b'promptecho = True\n')
1432 hgrc.write(b'[defaults]\n')
1432 hgrc.write(b'[defaults]\n')
1433 hgrc.write(b'[devel]\n')
1433 hgrc.write(b'[devel]\n')
1434 hgrc.write(b'all-warnings = true\n')
1434 hgrc.write(b'all-warnings = true\n')
1435 hgrc.write(b'default-date = 0 0\n')
1435 hgrc.write(b'default-date = 0 0\n')
1436 hgrc.write(b'[largefiles]\n')
1436 hgrc.write(b'[largefiles]\n')
1437 hgrc.write(
1437 hgrc.write(
1438 b'usercache = %s\n'
1438 b'usercache = %s\n'
1439 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1439 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1440 )
1440 )
1441 hgrc.write(b'[lfs]\n')
1441 hgrc.write(b'[lfs]\n')
1442 hgrc.write(
1442 hgrc.write(
1443 b'usercache = %s\n'
1443 b'usercache = %s\n'
1444 % (os.path.join(self._testtmp, b'.cache/lfs'))
1444 % (os.path.join(self._testtmp, b'.cache/lfs'))
1445 )
1445 )
1446 hgrc.write(b'[web]\n')
1446 hgrc.write(b'[web]\n')
1447 hgrc.write(b'address = localhost\n')
1447 hgrc.write(b'address = localhost\n')
1448 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1448 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1449 hgrc.write(b'server-header = testing stub value\n')
1449 hgrc.write(b'server-header = testing stub value\n')
1450
1450
1451 for opt in self._extraconfigopts:
1451 for opt in self._extraconfigopts:
1452 section, key = _sys2bytes(opt).split(b'.', 1)
1452 section, key = _sys2bytes(opt).split(b'.', 1)
1453 assert b'=' in key, (
1453 assert b'=' in key, (
1454 'extra config opt %s must ' 'have an = for assignment' % opt
1454 'extra config opt %s must ' 'have an = for assignment' % opt
1455 )
1455 )
1456 hgrc.write(b'[%s]\n%s\n' % (section, key))
1456 hgrc.write(b'[%s]\n%s\n' % (section, key))
1457
1457
1458 def fail(self, msg):
1458 def fail(self, msg):
1459 # unittest differentiates between errored and failed.
1459 # unittest differentiates between errored and failed.
1460 # Failed is denoted by AssertionError (by default at least).
1460 # Failed is denoted by AssertionError (by default at least).
1461 raise AssertionError(msg)
1461 raise AssertionError(msg)
1462
1462
1463 def _runcommand(self, cmd, env, normalizenewlines=False):
1463 def _runcommand(self, cmd, env, normalizenewlines=False):
1464 """Run command in a sub-process, capturing the output (stdout and
1464 """Run command in a sub-process, capturing the output (stdout and
1465 stderr).
1465 stderr).
1466
1466
1467 Return a tuple (exitcode, output). output is None in debug mode.
1467 Return a tuple (exitcode, output). output is None in debug mode.
1468 """
1468 """
1469 if self._debug:
1469 if self._debug:
1470 proc = subprocess.Popen(
1470 proc = subprocess.Popen(
1471 _bytes2sys(cmd),
1471 _bytes2sys(cmd),
1472 shell=True,
1472 shell=True,
1473 cwd=_bytes2sys(self._testtmp),
1473 cwd=_bytes2sys(self._testtmp),
1474 env=env,
1474 env=env,
1475 )
1475 )
1476 ret = proc.wait()
1476 ret = proc.wait()
1477 return (ret, None)
1477 return (ret, None)
1478
1478
1479 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1479 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1480
1480
1481 def cleanup():
1481 def cleanup():
1482 terminate(proc)
1482 terminate(proc)
1483 ret = proc.wait()
1483 ret = proc.wait()
1484 if ret == 0:
1484 if ret == 0:
1485 ret = signal.SIGTERM << 8
1485 ret = signal.SIGTERM << 8
1486 killdaemons(env['DAEMON_PIDS'])
1486 killdaemons(env['DAEMON_PIDS'])
1487 return ret
1487 return ret
1488
1488
1489 proc.tochild.close()
1489 proc.tochild.close()
1490
1490
1491 try:
1491 try:
1492 output = proc.fromchild.read()
1492 output = proc.fromchild.read()
1493 except KeyboardInterrupt:
1493 except KeyboardInterrupt:
1494 vlog('# Handling keyboard interrupt')
1494 vlog('# Handling keyboard interrupt')
1495 cleanup()
1495 cleanup()
1496 raise
1496 raise
1497
1497
1498 ret = proc.wait()
1498 ret = proc.wait()
1499 if wifexited(ret):
1499 if wifexited(ret):
1500 ret = os.WEXITSTATUS(ret)
1500 ret = os.WEXITSTATUS(ret)
1501
1501
1502 if proc.timeout:
1502 if proc.timeout:
1503 ret = 'timeout'
1503 ret = 'timeout'
1504
1504
1505 if ret:
1505 if ret:
1506 killdaemons(env['DAEMON_PIDS'])
1506 killdaemons(env['DAEMON_PIDS'])
1507
1507
1508 for s, r in self._getreplacements():
1508 for s, r in self._getreplacements():
1509 output = re.sub(s, r, output)
1509 output = re.sub(s, r, output)
1510
1510
1511 if normalizenewlines:
1511 if normalizenewlines:
1512 output = output.replace(b'\r\n', b'\n')
1512 output = output.replace(b'\r\n', b'\n')
1513
1513
1514 return ret, output.splitlines(True)
1514 return ret, output.splitlines(True)
1515
1515
1516
1516
1517 class PythonTest(Test):
1517 class PythonTest(Test):
1518 """A Python-based test."""
1518 """A Python-based test."""
1519
1519
1520 @property
1520 @property
1521 def refpath(self):
1521 def refpath(self):
1522 return os.path.join(self._testdir, b'%s.out' % self.bname)
1522 return os.path.join(self._testdir, b'%s.out' % self.bname)
1523
1523
1524 def _run(self, env):
1524 def _run(self, env):
1525 # Quote the python(3) executable for Windows
1525 # Quote the python(3) executable for Windows
1526 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1526 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1527 vlog("# Running", cmd.decode("utf-8"))
1527 vlog("# Running", cmd.decode("utf-8"))
1528 normalizenewlines = os.name == 'nt'
1528 normalizenewlines = os.name == 'nt'
1529 result = self._runcommand(cmd, env, normalizenewlines=normalizenewlines)
1529 result = self._runcommand(cmd, env, normalizenewlines=normalizenewlines)
1530 if self._aborted:
1530 if self._aborted:
1531 raise KeyboardInterrupt()
1531 raise KeyboardInterrupt()
1532
1532
1533 return result
1533 return result
1534
1534
1535
1535
1536 # Some glob patterns apply only in some circumstances, so the script
1536 # Some glob patterns apply only in some circumstances, so the script
1537 # might want to remove (glob) annotations that otherwise should be
1537 # might want to remove (glob) annotations that otherwise should be
1538 # retained.
1538 # retained.
1539 checkcodeglobpats = [
1539 checkcodeglobpats = [
1540 # On Windows it looks like \ doesn't require a (glob), but we know
1540 # On Windows it looks like \ doesn't require a (glob), but we know
1541 # better.
1541 # better.
1542 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1542 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1543 re.compile(br'^moving \S+/.*[^)]$'),
1543 re.compile(br'^moving \S+/.*[^)]$'),
1544 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1544 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1545 # Not all platforms have 127.0.0.1 as loopback (though most do),
1545 # Not all platforms have 127.0.0.1 as loopback (though most do),
1546 # so we always glob that too.
1546 # so we always glob that too.
1547 re.compile(br'.*\$LOCALIP.*$'),
1547 re.compile(br'.*\$LOCALIP.*$'),
1548 ]
1548 ]
1549
1549
1550 bchr = chr
1550 bchr = chr
1551 if PYTHON3:
1551 if PYTHON3:
1552 bchr = lambda x: bytes([x])
1552 bchr = lambda x: bytes([x])
1553
1553
1554 WARN_UNDEFINED = 1
1554 WARN_UNDEFINED = 1
1555 WARN_YES = 2
1555 WARN_YES = 2
1556 WARN_NO = 3
1556 WARN_NO = 3
1557
1557
1558 MARK_OPTIONAL = b" (?)\n"
1558 MARK_OPTIONAL = b" (?)\n"
1559
1559
1560
1560
1561 def isoptional(line):
1561 def isoptional(line):
1562 return line.endswith(MARK_OPTIONAL)
1562 return line.endswith(MARK_OPTIONAL)
1563
1563
1564
1564
1565 class TTest(Test):
1565 class TTest(Test):
1566 """A "t test" is a test backed by a .t file."""
1566 """A "t test" is a test backed by a .t file."""
1567
1567
1568 SKIPPED_PREFIX = b'skipped: '
1568 SKIPPED_PREFIX = b'skipped: '
1569 FAILED_PREFIX = b'hghave check failed: '
1569 FAILED_PREFIX = b'hghave check failed: '
1570 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1570 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1571
1571
1572 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1572 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1573 ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)}
1573 ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)}
1574 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1574 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1575
1575
1576 def __init__(self, path, *args, **kwds):
1576 def __init__(self, path, *args, **kwds):
1577 # accept an extra "case" parameter
1577 # accept an extra "case" parameter
1578 case = kwds.pop('case', [])
1578 case = kwds.pop('case', [])
1579 self._case = case
1579 self._case = case
1580 self._allcases = {x for y in parsettestcases(path) for x in y}
1580 self._allcases = {x for y in parsettestcases(path) for x in y}
1581 super(TTest, self).__init__(path, *args, **kwds)
1581 super(TTest, self).__init__(path, *args, **kwds)
1582 if case:
1582 if case:
1583 casepath = b'#'.join(case)
1583 casepath = b'#'.join(case)
1584 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1584 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1585 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1585 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1586 self._tmpname += b'-%s' % casepath
1586 self._tmpname += b'-%s' % casepath
1587 self._have = {}
1587 self._have = {}
1588
1588
1589 @property
1589 @property
1590 def refpath(self):
1590 def refpath(self):
1591 return os.path.join(self._testdir, self.bname)
1591 return os.path.join(self._testdir, self.bname)
1592
1592
1593 def _run(self, env):
1593 def _run(self, env):
1594 with open(self.path, 'rb') as f:
1594 with open(self.path, 'rb') as f:
1595 lines = f.readlines()
1595 lines = f.readlines()
1596
1596
1597 # .t file is both reference output and the test input, keep reference
1597 # .t file is both reference output and the test input, keep reference
1598 # output updated with the the test input. This avoids some race
1598 # output updated with the the test input. This avoids some race
1599 # conditions where the reference output does not match the actual test.
1599 # conditions where the reference output does not match the actual test.
1600 if self._refout is not None:
1600 if self._refout is not None:
1601 self._refout = lines
1601 self._refout = lines
1602
1602
1603 salt, script, after, expected = self._parsetest(lines)
1603 salt, script, after, expected = self._parsetest(lines)
1604
1604
1605 # Write out the generated script.
1605 # Write out the generated script.
1606 fname = b'%s.sh' % self._testtmp
1606 fname = b'%s.sh' % self._testtmp
1607 with open(fname, 'wb') as f:
1607 with open(fname, 'wb') as f:
1608 for l in script:
1608 for l in script:
1609 f.write(l)
1609 f.write(l)
1610
1610
1611 cmd = b'%s "%s"' % (self._shell, fname)
1611 cmd = b'%s "%s"' % (self._shell, fname)
1612 vlog("# Running", cmd.decode("utf-8"))
1612 vlog("# Running", cmd.decode("utf-8"))
1613
1613
1614 exitcode, output = self._runcommand(cmd, env)
1614 exitcode, output = self._runcommand(cmd, env)
1615
1615
1616 if self._aborted:
1616 if self._aborted:
1617 raise KeyboardInterrupt()
1617 raise KeyboardInterrupt()
1618
1618
1619 # Do not merge output if skipped. Return hghave message instead.
1619 # Do not merge output if skipped. Return hghave message instead.
1620 # Similarly, with --debug, output is None.
1620 # Similarly, with --debug, output is None.
1621 if exitcode == self.SKIPPED_STATUS or output is None:
1621 if exitcode == self.SKIPPED_STATUS or output is None:
1622 return exitcode, output
1622 return exitcode, output
1623
1623
1624 return self._processoutput(exitcode, output, salt, after, expected)
1624 return self._processoutput(exitcode, output, salt, after, expected)
1625
1625
1626 def _hghave(self, reqs):
1626 def _hghave(self, reqs):
1627 allreqs = b' '.join(reqs)
1627 allreqs = b' '.join(reqs)
1628
1628
1629 self._detectslow(reqs)
1629 self._detectslow(reqs)
1630
1630
1631 if allreqs in self._have:
1631 if allreqs in self._have:
1632 return self._have.get(allreqs)
1632 return self._have.get(allreqs)
1633
1633
1634 # TODO do something smarter when all other uses of hghave are gone.
1634 # TODO do something smarter when all other uses of hghave are gone.
1635 runtestdir = os.path.abspath(os.path.dirname(_sys2bytes(__file__)))
1635 runtestdir = osenvironb[b'RUNTESTDIR']
1636 tdir = runtestdir.replace(b'\\', b'/')
1636 tdir = runtestdir.replace(b'\\', b'/')
1637 proc = Popen4(
1637 proc = Popen4(
1638 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1638 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1639 self._testtmp,
1639 self._testtmp,
1640 0,
1640 0,
1641 self._getenv(),
1641 self._getenv(),
1642 )
1642 )
1643 stdout, stderr = proc.communicate()
1643 stdout, stderr = proc.communicate()
1644 ret = proc.wait()
1644 ret = proc.wait()
1645 if wifexited(ret):
1645 if wifexited(ret):
1646 ret = os.WEXITSTATUS(ret)
1646 ret = os.WEXITSTATUS(ret)
1647 if ret == 2:
1647 if ret == 2:
1648 print(stdout.decode('utf-8'))
1648 print(stdout.decode('utf-8'))
1649 sys.exit(1)
1649 sys.exit(1)
1650
1650
1651 if ret != 0:
1651 if ret != 0:
1652 self._have[allreqs] = (False, stdout)
1652 self._have[allreqs] = (False, stdout)
1653 return False, stdout
1653 return False, stdout
1654
1654
1655 self._have[allreqs] = (True, None)
1655 self._have[allreqs] = (True, None)
1656 return True, None
1656 return True, None
1657
1657
1658 def _detectslow(self, reqs):
1658 def _detectslow(self, reqs):
1659 """update the timeout of slow test when appropriate"""
1659 """update the timeout of slow test when appropriate"""
1660 if b'slow' in reqs:
1660 if b'slow' in reqs:
1661 self._timeout = self._slowtimeout
1661 self._timeout = self._slowtimeout
1662
1662
1663 def _iftest(self, args):
1663 def _iftest(self, args):
1664 # implements "#if"
1664 # implements "#if"
1665 reqs = []
1665 reqs = []
1666 for arg in args:
1666 for arg in args:
1667 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1667 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1668 if arg[3:] in self._case:
1668 if arg[3:] in self._case:
1669 return False
1669 return False
1670 elif arg in self._allcases:
1670 elif arg in self._allcases:
1671 if arg not in self._case:
1671 if arg not in self._case:
1672 return False
1672 return False
1673 else:
1673 else:
1674 reqs.append(arg)
1674 reqs.append(arg)
1675 self._detectslow(reqs)
1675 self._detectslow(reqs)
1676 return self._hghave(reqs)[0]
1676 return self._hghave(reqs)[0]
1677
1677
1678 def _parsetest(self, lines):
1678 def _parsetest(self, lines):
1679 # We generate a shell script which outputs unique markers to line
1679 # We generate a shell script which outputs unique markers to line
1680 # up script results with our source. These markers include input
1680 # up script results with our source. These markers include input
1681 # line number and the last return code.
1681 # line number and the last return code.
1682 salt = b"SALT%d" % time.time()
1682 salt = b"SALT%d" % time.time()
1683
1683
1684 def addsalt(line, inpython):
1684 def addsalt(line, inpython):
1685 if inpython:
1685 if inpython:
1686 script.append(b'%s %d 0\n' % (salt, line))
1686 script.append(b'%s %d 0\n' % (salt, line))
1687 else:
1687 else:
1688 script.append(b'echo %s %d $?\n' % (salt, line))
1688 script.append(b'echo %s %d $?\n' % (salt, line))
1689
1689
1690 activetrace = []
1690 activetrace = []
1691 session = str(uuid.uuid4())
1691 session = str(uuid.uuid4())
1692 if PYTHON3:
1692 if PYTHON3:
1693 session = session.encode('ascii')
1693 session = session.encode('ascii')
1694 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1694 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1695 'HGCATAPULTSERVERPIPE'
1695 'HGCATAPULTSERVERPIPE'
1696 )
1696 )
1697
1697
1698 def toggletrace(cmd=None):
1698 def toggletrace(cmd=None):
1699 if not hgcatapult or hgcatapult == os.devnull:
1699 if not hgcatapult or hgcatapult == os.devnull:
1700 return
1700 return
1701
1701
1702 if activetrace:
1702 if activetrace:
1703 script.append(
1703 script.append(
1704 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1704 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1705 % (session, activetrace[0])
1705 % (session, activetrace[0])
1706 )
1706 )
1707 if cmd is None:
1707 if cmd is None:
1708 return
1708 return
1709
1709
1710 if isinstance(cmd, str):
1710 if isinstance(cmd, str):
1711 quoted = shellquote(cmd.strip())
1711 quoted = shellquote(cmd.strip())
1712 else:
1712 else:
1713 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1713 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1714 quoted = quoted.replace(b'\\', b'\\\\')
1714 quoted = quoted.replace(b'\\', b'\\\\')
1715 script.append(
1715 script.append(
1716 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1716 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1717 % (session, quoted)
1717 % (session, quoted)
1718 )
1718 )
1719 activetrace[0:] = [quoted]
1719 activetrace[0:] = [quoted]
1720
1720
1721 script = []
1721 script = []
1722
1722
1723 # After we run the shell script, we re-unify the script output
1723 # After we run the shell script, we re-unify the script output
1724 # with non-active parts of the source, with synchronization by our
1724 # with non-active parts of the source, with synchronization by our
1725 # SALT line number markers. The after table contains the non-active
1725 # SALT line number markers. The after table contains the non-active
1726 # components, ordered by line number.
1726 # components, ordered by line number.
1727 after = {}
1727 after = {}
1728
1728
1729 # Expected shell script output.
1729 # Expected shell script output.
1730 expected = {}
1730 expected = {}
1731
1731
1732 pos = prepos = -1
1732 pos = prepos = -1
1733
1733
1734 # True or False when in a true or false conditional section
1734 # True or False when in a true or false conditional section
1735 skipping = None
1735 skipping = None
1736
1736
1737 # We keep track of whether or not we're in a Python block so we
1737 # We keep track of whether or not we're in a Python block so we
1738 # can generate the surrounding doctest magic.
1738 # can generate the surrounding doctest magic.
1739 inpython = False
1739 inpython = False
1740
1740
1741 if self._debug:
1741 if self._debug:
1742 script.append(b'set -x\n')
1742 script.append(b'set -x\n')
1743 if self._hgcommand != b'hg':
1743 if self._hgcommand != b'hg':
1744 script.append(b'alias hg="%s"\n' % self._hgcommand)
1744 script.append(b'alias hg="%s"\n' % self._hgcommand)
1745 if os.getenv('MSYSTEM'):
1745 if os.getenv('MSYSTEM'):
1746 script.append(b'alias pwd="pwd -W"\n')
1746 script.append(b'alias pwd="pwd -W"\n')
1747
1747
1748 if hgcatapult and hgcatapult != os.devnull:
1748 if hgcatapult and hgcatapult != os.devnull:
1749 if PYTHON3:
1749 if PYTHON3:
1750 hgcatapult = hgcatapult.encode('utf8')
1750 hgcatapult = hgcatapult.encode('utf8')
1751 cataname = self.name.encode('utf8')
1751 cataname = self.name.encode('utf8')
1752 else:
1752 else:
1753 cataname = self.name
1753 cataname = self.name
1754
1754
1755 # Kludge: use a while loop to keep the pipe from getting
1755 # Kludge: use a while loop to keep the pipe from getting
1756 # closed by our echo commands. The still-running file gets
1756 # closed by our echo commands. The still-running file gets
1757 # reaped at the end of the script, which causes the while
1757 # reaped at the end of the script, which causes the while
1758 # loop to exit and closes the pipe. Sigh.
1758 # loop to exit and closes the pipe. Sigh.
1759 script.append(
1759 script.append(
1760 b'rtendtracing() {\n'
1760 b'rtendtracing() {\n'
1761 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1761 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1762 b' rm -f "$TESTTMP/.still-running"\n'
1762 b' rm -f "$TESTTMP/.still-running"\n'
1763 b'}\n'
1763 b'}\n'
1764 b'trap "rtendtracing" 0\n'
1764 b'trap "rtendtracing" 0\n'
1765 b'touch "$TESTTMP/.still-running"\n'
1765 b'touch "$TESTTMP/.still-running"\n'
1766 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1766 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1767 b'> %(catapult)s &\n'
1767 b'> %(catapult)s &\n'
1768 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1768 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1769 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1769 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1770 % {
1770 % {
1771 b'name': cataname,
1771 b'name': cataname,
1772 b'session': session,
1772 b'session': session,
1773 b'catapult': hgcatapult,
1773 b'catapult': hgcatapult,
1774 }
1774 }
1775 )
1775 )
1776
1776
1777 if self._case:
1777 if self._case:
1778 casestr = b'#'.join(self._case)
1778 casestr = b'#'.join(self._case)
1779 if isinstance(casestr, str):
1779 if isinstance(casestr, str):
1780 quoted = shellquote(casestr)
1780 quoted = shellquote(casestr)
1781 else:
1781 else:
1782 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1782 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1783 script.append(b'TESTCASE=%s\n' % quoted)
1783 script.append(b'TESTCASE=%s\n' % quoted)
1784 script.append(b'export TESTCASE\n')
1784 script.append(b'export TESTCASE\n')
1785
1785
1786 n = 0
1786 n = 0
1787 for n, l in enumerate(lines):
1787 for n, l in enumerate(lines):
1788 if not l.endswith(b'\n'):
1788 if not l.endswith(b'\n'):
1789 l += b'\n'
1789 l += b'\n'
1790 if l.startswith(b'#require'):
1790 if l.startswith(b'#require'):
1791 lsplit = l.split()
1791 lsplit = l.split()
1792 if len(lsplit) < 2 or lsplit[0] != b'#require':
1792 if len(lsplit) < 2 or lsplit[0] != b'#require':
1793 after.setdefault(pos, []).append(
1793 after.setdefault(pos, []).append(
1794 b' !!! invalid #require\n'
1794 b' !!! invalid #require\n'
1795 )
1795 )
1796 if not skipping:
1796 if not skipping:
1797 haveresult, message = self._hghave(lsplit[1:])
1797 haveresult, message = self._hghave(lsplit[1:])
1798 if not haveresult:
1798 if not haveresult:
1799 script = [b'echo "%s"\nexit 80\n' % message]
1799 script = [b'echo "%s"\nexit 80\n' % message]
1800 break
1800 break
1801 after.setdefault(pos, []).append(l)
1801 after.setdefault(pos, []).append(l)
1802 elif l.startswith(b'#if'):
1802 elif l.startswith(b'#if'):
1803 lsplit = l.split()
1803 lsplit = l.split()
1804 if len(lsplit) < 2 or lsplit[0] != b'#if':
1804 if len(lsplit) < 2 or lsplit[0] != b'#if':
1805 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1805 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1806 if skipping is not None:
1806 if skipping is not None:
1807 after.setdefault(pos, []).append(b' !!! nested #if\n')
1807 after.setdefault(pos, []).append(b' !!! nested #if\n')
1808 skipping = not self._iftest(lsplit[1:])
1808 skipping = not self._iftest(lsplit[1:])
1809 after.setdefault(pos, []).append(l)
1809 after.setdefault(pos, []).append(l)
1810 elif l.startswith(b'#else'):
1810 elif l.startswith(b'#else'):
1811 if skipping is None:
1811 if skipping is None:
1812 after.setdefault(pos, []).append(b' !!! missing #if\n')
1812 after.setdefault(pos, []).append(b' !!! missing #if\n')
1813 skipping = not skipping
1813 skipping = not skipping
1814 after.setdefault(pos, []).append(l)
1814 after.setdefault(pos, []).append(l)
1815 elif l.startswith(b'#endif'):
1815 elif l.startswith(b'#endif'):
1816 if skipping is None:
1816 if skipping is None:
1817 after.setdefault(pos, []).append(b' !!! missing #if\n')
1817 after.setdefault(pos, []).append(b' !!! missing #if\n')
1818 skipping = None
1818 skipping = None
1819 after.setdefault(pos, []).append(l)
1819 after.setdefault(pos, []).append(l)
1820 elif skipping:
1820 elif skipping:
1821 after.setdefault(pos, []).append(l)
1821 after.setdefault(pos, []).append(l)
1822 elif l.startswith(b' >>> '): # python inlines
1822 elif l.startswith(b' >>> '): # python inlines
1823 after.setdefault(pos, []).append(l)
1823 after.setdefault(pos, []).append(l)
1824 prepos = pos
1824 prepos = pos
1825 pos = n
1825 pos = n
1826 if not inpython:
1826 if not inpython:
1827 # We've just entered a Python block. Add the header.
1827 # We've just entered a Python block. Add the header.
1828 inpython = True
1828 inpython = True
1829 addsalt(prepos, False) # Make sure we report the exit code.
1829 addsalt(prepos, False) # Make sure we report the exit code.
1830 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1830 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1831 addsalt(n, True)
1831 addsalt(n, True)
1832 script.append(l[2:])
1832 script.append(l[2:])
1833 elif l.startswith(b' ... '): # python inlines
1833 elif l.startswith(b' ... '): # python inlines
1834 after.setdefault(prepos, []).append(l)
1834 after.setdefault(prepos, []).append(l)
1835 script.append(l[2:])
1835 script.append(l[2:])
1836 elif l.startswith(b' $ '): # commands
1836 elif l.startswith(b' $ '): # commands
1837 if inpython:
1837 if inpython:
1838 script.append(b'EOF\n')
1838 script.append(b'EOF\n')
1839 inpython = False
1839 inpython = False
1840 after.setdefault(pos, []).append(l)
1840 after.setdefault(pos, []).append(l)
1841 prepos = pos
1841 prepos = pos
1842 pos = n
1842 pos = n
1843 addsalt(n, False)
1843 addsalt(n, False)
1844 rawcmd = l[4:]
1844 rawcmd = l[4:]
1845 cmd = rawcmd.split()
1845 cmd = rawcmd.split()
1846 toggletrace(rawcmd)
1846 toggletrace(rawcmd)
1847 if len(cmd) == 2 and cmd[0] == b'cd':
1847 if len(cmd) == 2 and cmd[0] == b'cd':
1848 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1848 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1849 script.append(rawcmd)
1849 script.append(rawcmd)
1850 elif l.startswith(b' > '): # continuations
1850 elif l.startswith(b' > '): # continuations
1851 after.setdefault(prepos, []).append(l)
1851 after.setdefault(prepos, []).append(l)
1852 script.append(l[4:])
1852 script.append(l[4:])
1853 elif l.startswith(b' '): # results
1853 elif l.startswith(b' '): # results
1854 # Queue up a list of expected results.
1854 # Queue up a list of expected results.
1855 expected.setdefault(pos, []).append(l[2:])
1855 expected.setdefault(pos, []).append(l[2:])
1856 else:
1856 else:
1857 if inpython:
1857 if inpython:
1858 script.append(b'EOF\n')
1858 script.append(b'EOF\n')
1859 inpython = False
1859 inpython = False
1860 # Non-command/result. Queue up for merged output.
1860 # Non-command/result. Queue up for merged output.
1861 after.setdefault(pos, []).append(l)
1861 after.setdefault(pos, []).append(l)
1862
1862
1863 if inpython:
1863 if inpython:
1864 script.append(b'EOF\n')
1864 script.append(b'EOF\n')
1865 if skipping is not None:
1865 if skipping is not None:
1866 after.setdefault(pos, []).append(b' !!! missing #endif\n')
1866 after.setdefault(pos, []).append(b' !!! missing #endif\n')
1867 addsalt(n + 1, False)
1867 addsalt(n + 1, False)
1868 # Need to end any current per-command trace
1868 # Need to end any current per-command trace
1869 if activetrace:
1869 if activetrace:
1870 toggletrace()
1870 toggletrace()
1871 return salt, script, after, expected
1871 return salt, script, after, expected
1872
1872
1873 def _processoutput(self, exitcode, output, salt, after, expected):
1873 def _processoutput(self, exitcode, output, salt, after, expected):
1874 # Merge the script output back into a unified test.
1874 # Merge the script output back into a unified test.
1875 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
1875 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
1876 if exitcode != 0:
1876 if exitcode != 0:
1877 warnonly = WARN_NO
1877 warnonly = WARN_NO
1878
1878
1879 pos = -1
1879 pos = -1
1880 postout = []
1880 postout = []
1881 for out_rawline in output:
1881 for out_rawline in output:
1882 out_line, cmd_line = out_rawline, None
1882 out_line, cmd_line = out_rawline, None
1883 if salt in out_rawline:
1883 if salt in out_rawline:
1884 out_line, cmd_line = out_rawline.split(salt, 1)
1884 out_line, cmd_line = out_rawline.split(salt, 1)
1885
1885
1886 pos, postout, warnonly = self._process_out_line(
1886 pos, postout, warnonly = self._process_out_line(
1887 out_line, pos, postout, expected, warnonly
1887 out_line, pos, postout, expected, warnonly
1888 )
1888 )
1889 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
1889 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
1890
1890
1891 if pos in after:
1891 if pos in after:
1892 postout += after.pop(pos)
1892 postout += after.pop(pos)
1893
1893
1894 if warnonly == WARN_YES:
1894 if warnonly == WARN_YES:
1895 exitcode = False # Set exitcode to warned.
1895 exitcode = False # Set exitcode to warned.
1896
1896
1897 return exitcode, postout
1897 return exitcode, postout
1898
1898
1899 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1899 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1900 while out_line:
1900 while out_line:
1901 if not out_line.endswith(b'\n'):
1901 if not out_line.endswith(b'\n'):
1902 out_line += b' (no-eol)\n'
1902 out_line += b' (no-eol)\n'
1903
1903
1904 # Find the expected output at the current position.
1904 # Find the expected output at the current position.
1905 els = [None]
1905 els = [None]
1906 if expected.get(pos, None):
1906 if expected.get(pos, None):
1907 els = expected[pos]
1907 els = expected[pos]
1908
1908
1909 optional = []
1909 optional = []
1910 for i, el in enumerate(els):
1910 for i, el in enumerate(els):
1911 r = False
1911 r = False
1912 if el:
1912 if el:
1913 r, exact = self.linematch(el, out_line)
1913 r, exact = self.linematch(el, out_line)
1914 if isinstance(r, str):
1914 if isinstance(r, str):
1915 if r == '-glob':
1915 if r == '-glob':
1916 out_line = ''.join(el.rsplit(' (glob)', 1))
1916 out_line = ''.join(el.rsplit(' (glob)', 1))
1917 r = '' # Warn only this line.
1917 r = '' # Warn only this line.
1918 elif r == "retry":
1918 elif r == "retry":
1919 postout.append(b' ' + el)
1919 postout.append(b' ' + el)
1920 else:
1920 else:
1921 log('\ninfo, unknown linematch result: %r\n' % r)
1921 log('\ninfo, unknown linematch result: %r\n' % r)
1922 r = False
1922 r = False
1923 if r:
1923 if r:
1924 els.pop(i)
1924 els.pop(i)
1925 break
1925 break
1926 if el:
1926 if el:
1927 if isoptional(el):
1927 if isoptional(el):
1928 optional.append(i)
1928 optional.append(i)
1929 else:
1929 else:
1930 m = optline.match(el)
1930 m = optline.match(el)
1931 if m:
1931 if m:
1932 conditions = [c for c in m.group(2).split(b' ')]
1932 conditions = [c for c in m.group(2).split(b' ')]
1933
1933
1934 if not self._iftest(conditions):
1934 if not self._iftest(conditions):
1935 optional.append(i)
1935 optional.append(i)
1936 if exact:
1936 if exact:
1937 # Don't allow line to be matches against a later
1937 # Don't allow line to be matches against a later
1938 # line in the output
1938 # line in the output
1939 els.pop(i)
1939 els.pop(i)
1940 break
1940 break
1941
1941
1942 if r:
1942 if r:
1943 if r == "retry":
1943 if r == "retry":
1944 continue
1944 continue
1945 # clean up any optional leftovers
1945 # clean up any optional leftovers
1946 for i in optional:
1946 for i in optional:
1947 postout.append(b' ' + els[i])
1947 postout.append(b' ' + els[i])
1948 for i in reversed(optional):
1948 for i in reversed(optional):
1949 del els[i]
1949 del els[i]
1950 postout.append(b' ' + el)
1950 postout.append(b' ' + el)
1951 else:
1951 else:
1952 if self.NEEDESCAPE(out_line):
1952 if self.NEEDESCAPE(out_line):
1953 out_line = TTest._stringescape(
1953 out_line = TTest._stringescape(
1954 b'%s (esc)\n' % out_line.rstrip(b'\n')
1954 b'%s (esc)\n' % out_line.rstrip(b'\n')
1955 )
1955 )
1956 postout.append(b' ' + out_line) # Let diff deal with it.
1956 postout.append(b' ' + out_line) # Let diff deal with it.
1957 if r != '': # If line failed.
1957 if r != '': # If line failed.
1958 warnonly = WARN_NO
1958 warnonly = WARN_NO
1959 elif warnonly == WARN_UNDEFINED:
1959 elif warnonly == WARN_UNDEFINED:
1960 warnonly = WARN_YES
1960 warnonly = WARN_YES
1961 break
1961 break
1962 else:
1962 else:
1963 # clean up any optional leftovers
1963 # clean up any optional leftovers
1964 while expected.get(pos, None):
1964 while expected.get(pos, None):
1965 el = expected[pos].pop(0)
1965 el = expected[pos].pop(0)
1966 if el:
1966 if el:
1967 if not isoptional(el):
1967 if not isoptional(el):
1968 m = optline.match(el)
1968 m = optline.match(el)
1969 if m:
1969 if m:
1970 conditions = [c for c in m.group(2).split(b' ')]
1970 conditions = [c for c in m.group(2).split(b' ')]
1971
1971
1972 if self._iftest(conditions):
1972 if self._iftest(conditions):
1973 # Don't append as optional line
1973 # Don't append as optional line
1974 continue
1974 continue
1975 else:
1975 else:
1976 continue
1976 continue
1977 postout.append(b' ' + el)
1977 postout.append(b' ' + el)
1978 return pos, postout, warnonly
1978 return pos, postout, warnonly
1979
1979
1980 def _process_cmd_line(self, cmd_line, pos, postout, after):
1980 def _process_cmd_line(self, cmd_line, pos, postout, after):
1981 """process a "command" part of a line from unified test output"""
1981 """process a "command" part of a line from unified test output"""
1982 if cmd_line:
1982 if cmd_line:
1983 # Add on last return code.
1983 # Add on last return code.
1984 ret = int(cmd_line.split()[1])
1984 ret = int(cmd_line.split()[1])
1985 if ret != 0:
1985 if ret != 0:
1986 postout.append(b' [%d]\n' % ret)
1986 postout.append(b' [%d]\n' % ret)
1987 if pos in after:
1987 if pos in after:
1988 # Merge in non-active test bits.
1988 # Merge in non-active test bits.
1989 postout += after.pop(pos)
1989 postout += after.pop(pos)
1990 pos = int(cmd_line.split()[0])
1990 pos = int(cmd_line.split()[0])
1991 return pos, postout
1991 return pos, postout
1992
1992
1993 @staticmethod
1993 @staticmethod
1994 def rematch(el, l):
1994 def rematch(el, l):
1995 try:
1995 try:
1996 # parse any flags at the beginning of the regex. Only 'i' is
1996 # parse any flags at the beginning of the regex. Only 'i' is
1997 # supported right now, but this should be easy to extend.
1997 # supported right now, but this should be easy to extend.
1998 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
1998 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
1999 flags = flags or b''
1999 flags = flags or b''
2000 el = flags + b'(?:' + el + b')'
2000 el = flags + b'(?:' + el + b')'
2001 # use \Z to ensure that the regex matches to the end of the string
2001 # use \Z to ensure that the regex matches to the end of the string
2002 if os.name == 'nt':
2002 if os.name == 'nt':
2003 return re.match(el + br'\r?\n\Z', l)
2003 return re.match(el + br'\r?\n\Z', l)
2004 return re.match(el + br'\n\Z', l)
2004 return re.match(el + br'\n\Z', l)
2005 except re.error:
2005 except re.error:
2006 # el is an invalid regex
2006 # el is an invalid regex
2007 return False
2007 return False
2008
2008
2009 @staticmethod
2009 @staticmethod
2010 def globmatch(el, l):
2010 def globmatch(el, l):
2011 # The only supported special characters are * and ? plus / which also
2011 # The only supported special characters are * and ? plus / which also
2012 # matches \ on windows. Escaping of these characters is supported.
2012 # matches \ on windows. Escaping of these characters is supported.
2013 if el + b'\n' == l:
2013 if el + b'\n' == l:
2014 if os.altsep:
2014 if os.altsep:
2015 # matching on "/" is not needed for this line
2015 # matching on "/" is not needed for this line
2016 for pat in checkcodeglobpats:
2016 for pat in checkcodeglobpats:
2017 if pat.match(el):
2017 if pat.match(el):
2018 return True
2018 return True
2019 return b'-glob'
2019 return b'-glob'
2020 return True
2020 return True
2021 el = el.replace(b'$LOCALIP', b'*')
2021 el = el.replace(b'$LOCALIP', b'*')
2022 i, n = 0, len(el)
2022 i, n = 0, len(el)
2023 res = b''
2023 res = b''
2024 while i < n:
2024 while i < n:
2025 c = el[i : i + 1]
2025 c = el[i : i + 1]
2026 i += 1
2026 i += 1
2027 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2027 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2028 res += el[i - 1 : i + 1]
2028 res += el[i - 1 : i + 1]
2029 i += 1
2029 i += 1
2030 elif c == b'*':
2030 elif c == b'*':
2031 res += b'.*'
2031 res += b'.*'
2032 elif c == b'?':
2032 elif c == b'?':
2033 res += b'.'
2033 res += b'.'
2034 elif c == b'/' and os.altsep:
2034 elif c == b'/' and os.altsep:
2035 res += b'[/\\\\]'
2035 res += b'[/\\\\]'
2036 else:
2036 else:
2037 res += re.escape(c)
2037 res += re.escape(c)
2038 return TTest.rematch(res, l)
2038 return TTest.rematch(res, l)
2039
2039
2040 def linematch(self, el, l):
2040 def linematch(self, el, l):
2041 if el == l: # perfect match (fast)
2041 if el == l: # perfect match (fast)
2042 return True, True
2042 return True, True
2043 retry = False
2043 retry = False
2044 if isoptional(el):
2044 if isoptional(el):
2045 retry = "retry"
2045 retry = "retry"
2046 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2046 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2047 else:
2047 else:
2048 m = optline.match(el)
2048 m = optline.match(el)
2049 if m:
2049 if m:
2050 conditions = [c for c in m.group(2).split(b' ')]
2050 conditions = [c for c in m.group(2).split(b' ')]
2051
2051
2052 el = m.group(1) + b"\n"
2052 el = m.group(1) + b"\n"
2053 if not self._iftest(conditions):
2053 if not self._iftest(conditions):
2054 # listed feature missing, should not match
2054 # listed feature missing, should not match
2055 return "retry", False
2055 return "retry", False
2056
2056
2057 if el.endswith(b" (esc)\n"):
2057 if el.endswith(b" (esc)\n"):
2058 if PYTHON3:
2058 if PYTHON3:
2059 el = el[:-7].decode('unicode_escape') + '\n'
2059 el = el[:-7].decode('unicode_escape') + '\n'
2060 el = el.encode('utf-8')
2060 el = el.encode('utf-8')
2061 else:
2061 else:
2062 el = el[:-7].decode('string-escape') + '\n'
2062 el = el[:-7].decode('string-escape') + '\n'
2063 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
2063 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
2064 return True, True
2064 return True, True
2065 if el.endswith(b" (re)\n"):
2065 if el.endswith(b" (re)\n"):
2066 return (TTest.rematch(el[:-6], l) or retry), False
2066 return (TTest.rematch(el[:-6], l) or retry), False
2067 if el.endswith(b" (glob)\n"):
2067 if el.endswith(b" (glob)\n"):
2068 # ignore '(glob)' added to l by 'replacements'
2068 # ignore '(glob)' added to l by 'replacements'
2069 if l.endswith(b" (glob)\n"):
2069 if l.endswith(b" (glob)\n"):
2070 l = l[:-8] + b"\n"
2070 l = l[:-8] + b"\n"
2071 return (TTest.globmatch(el[:-8], l) or retry), False
2071 return (TTest.globmatch(el[:-8], l) or retry), False
2072 if os.altsep:
2072 if os.altsep:
2073 _l = l.replace(b'\\', b'/')
2073 _l = l.replace(b'\\', b'/')
2074 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
2074 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
2075 return True, True
2075 return True, True
2076 return retry, True
2076 return retry, True
2077
2077
2078 @staticmethod
2078 @staticmethod
2079 def parsehghaveoutput(lines):
2079 def parsehghaveoutput(lines):
2080 '''Parse hghave log lines.
2080 '''Parse hghave log lines.
2081
2081
2082 Return tuple of lists (missing, failed):
2082 Return tuple of lists (missing, failed):
2083 * the missing/unknown features
2083 * the missing/unknown features
2084 * the features for which existence check failed'''
2084 * the features for which existence check failed'''
2085 missing = []
2085 missing = []
2086 failed = []
2086 failed = []
2087 for line in lines:
2087 for line in lines:
2088 if line.startswith(TTest.SKIPPED_PREFIX):
2088 if line.startswith(TTest.SKIPPED_PREFIX):
2089 line = line.splitlines()[0]
2089 line = line.splitlines()[0]
2090 missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :]))
2090 missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :]))
2091 elif line.startswith(TTest.FAILED_PREFIX):
2091 elif line.startswith(TTest.FAILED_PREFIX):
2092 line = line.splitlines()[0]
2092 line = line.splitlines()[0]
2093 failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :]))
2093 failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :]))
2094
2094
2095 return missing, failed
2095 return missing, failed
2096
2096
2097 @staticmethod
2097 @staticmethod
2098 def _escapef(m):
2098 def _escapef(m):
2099 return TTest.ESCAPEMAP[m.group(0)]
2099 return TTest.ESCAPEMAP[m.group(0)]
2100
2100
2101 @staticmethod
2101 @staticmethod
2102 def _stringescape(s):
2102 def _stringescape(s):
2103 return TTest.ESCAPESUB(TTest._escapef, s)
2103 return TTest.ESCAPESUB(TTest._escapef, s)
2104
2104
2105
2105
2106 iolock = threading.RLock()
2106 iolock = threading.RLock()
2107 firstlock = threading.RLock()
2107 firstlock = threading.RLock()
2108 firsterror = False
2108 firsterror = False
2109
2109
2110
2110
2111 class TestResult(unittest._TextTestResult):
2111 class TestResult(unittest._TextTestResult):
2112 """Holds results when executing via unittest."""
2112 """Holds results when executing via unittest."""
2113
2113
2114 # Don't worry too much about accessing the non-public _TextTestResult.
2114 # Don't worry too much about accessing the non-public _TextTestResult.
2115 # It is relatively common in Python testing tools.
2115 # It is relatively common in Python testing tools.
2116 def __init__(self, options, *args, **kwargs):
2116 def __init__(self, options, *args, **kwargs):
2117 super(TestResult, self).__init__(*args, **kwargs)
2117 super(TestResult, self).__init__(*args, **kwargs)
2118
2118
2119 self._options = options
2119 self._options = options
2120
2120
2121 # unittest.TestResult didn't have skipped until 2.7. We need to
2121 # unittest.TestResult didn't have skipped until 2.7. We need to
2122 # polyfill it.
2122 # polyfill it.
2123 self.skipped = []
2123 self.skipped = []
2124
2124
2125 # We have a custom "ignored" result that isn't present in any Python
2125 # We have a custom "ignored" result that isn't present in any Python
2126 # unittest implementation. It is very similar to skipped. It may make
2126 # unittest implementation. It is very similar to skipped. It may make
2127 # sense to map it into skip some day.
2127 # sense to map it into skip some day.
2128 self.ignored = []
2128 self.ignored = []
2129
2129
2130 self.times = []
2130 self.times = []
2131 self._firststarttime = None
2131 self._firststarttime = None
2132 # Data stored for the benefit of generating xunit reports.
2132 # Data stored for the benefit of generating xunit reports.
2133 self.successes = []
2133 self.successes = []
2134 self.faildata = {}
2134 self.faildata = {}
2135
2135
2136 if options.color == 'auto':
2136 if options.color == 'auto':
2137 self.color = pygmentspresent and self.stream.isatty()
2137 self.color = pygmentspresent and self.stream.isatty()
2138 elif options.color == 'never':
2138 elif options.color == 'never':
2139 self.color = False
2139 self.color = False
2140 else: # 'always', for testing purposes
2140 else: # 'always', for testing purposes
2141 self.color = pygmentspresent
2141 self.color = pygmentspresent
2142
2142
2143 def onStart(self, test):
2143 def onStart(self, test):
2144 """ Can be overriden by custom TestResult
2144 """ Can be overriden by custom TestResult
2145 """
2145 """
2146
2146
2147 def onEnd(self):
2147 def onEnd(self):
2148 """ Can be overriden by custom TestResult
2148 """ Can be overriden by custom TestResult
2149 """
2149 """
2150
2150
2151 def addFailure(self, test, reason):
2151 def addFailure(self, test, reason):
2152 self.failures.append((test, reason))
2152 self.failures.append((test, reason))
2153
2153
2154 if self._options.first:
2154 if self._options.first:
2155 self.stop()
2155 self.stop()
2156 else:
2156 else:
2157 with iolock:
2157 with iolock:
2158 if reason == "timed out":
2158 if reason == "timed out":
2159 self.stream.write('t')
2159 self.stream.write('t')
2160 else:
2160 else:
2161 if not self._options.nodiff:
2161 if not self._options.nodiff:
2162 self.stream.write('\n')
2162 self.stream.write('\n')
2163 # Exclude the '\n' from highlighting to lex correctly
2163 # Exclude the '\n' from highlighting to lex correctly
2164 formatted = 'ERROR: %s output changed\n' % test
2164 formatted = 'ERROR: %s output changed\n' % test
2165 self.stream.write(highlightmsg(formatted, self.color))
2165 self.stream.write(highlightmsg(formatted, self.color))
2166 self.stream.write('!')
2166 self.stream.write('!')
2167
2167
2168 self.stream.flush()
2168 self.stream.flush()
2169
2169
2170 def addSuccess(self, test):
2170 def addSuccess(self, test):
2171 with iolock:
2171 with iolock:
2172 super(TestResult, self).addSuccess(test)
2172 super(TestResult, self).addSuccess(test)
2173 self.successes.append(test)
2173 self.successes.append(test)
2174
2174
2175 def addError(self, test, err):
2175 def addError(self, test, err):
2176 super(TestResult, self).addError(test, err)
2176 super(TestResult, self).addError(test, err)
2177 if self._options.first:
2177 if self._options.first:
2178 self.stop()
2178 self.stop()
2179
2179
2180 # Polyfill.
2180 # Polyfill.
2181 def addSkip(self, test, reason):
2181 def addSkip(self, test, reason):
2182 self.skipped.append((test, reason))
2182 self.skipped.append((test, reason))
2183 with iolock:
2183 with iolock:
2184 if self.showAll:
2184 if self.showAll:
2185 self.stream.writeln('skipped %s' % reason)
2185 self.stream.writeln('skipped %s' % reason)
2186 else:
2186 else:
2187 self.stream.write('s')
2187 self.stream.write('s')
2188 self.stream.flush()
2188 self.stream.flush()
2189
2189
2190 def addIgnore(self, test, reason):
2190 def addIgnore(self, test, reason):
2191 self.ignored.append((test, reason))
2191 self.ignored.append((test, reason))
2192 with iolock:
2192 with iolock:
2193 if self.showAll:
2193 if self.showAll:
2194 self.stream.writeln('ignored %s' % reason)
2194 self.stream.writeln('ignored %s' % reason)
2195 else:
2195 else:
2196 if reason not in ('not retesting', "doesn't match keyword"):
2196 if reason not in ('not retesting', "doesn't match keyword"):
2197 self.stream.write('i')
2197 self.stream.write('i')
2198 else:
2198 else:
2199 self.testsRun += 1
2199 self.testsRun += 1
2200 self.stream.flush()
2200 self.stream.flush()
2201
2201
2202 def addOutputMismatch(self, test, ret, got, expected):
2202 def addOutputMismatch(self, test, ret, got, expected):
2203 """Record a mismatch in test output for a particular test."""
2203 """Record a mismatch in test output for a particular test."""
2204 if self.shouldStop or firsterror:
2204 if self.shouldStop or firsterror:
2205 # don't print, some other test case already failed and
2205 # don't print, some other test case already failed and
2206 # printed, we're just stale and probably failed due to our
2206 # printed, we're just stale and probably failed due to our
2207 # temp dir getting cleaned up.
2207 # temp dir getting cleaned up.
2208 return
2208 return
2209
2209
2210 accepted = False
2210 accepted = False
2211 lines = []
2211 lines = []
2212
2212
2213 with iolock:
2213 with iolock:
2214 if self._options.nodiff:
2214 if self._options.nodiff:
2215 pass
2215 pass
2216 elif self._options.view:
2216 elif self._options.view:
2217 v = self._options.view
2217 v = self._options.view
2218 subprocess.call(
2218 subprocess.call(
2219 r'"%s" "%s" "%s"'
2219 r'"%s" "%s" "%s"'
2220 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2220 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2221 shell=True,
2221 shell=True,
2222 )
2222 )
2223 else:
2223 else:
2224 servefail, lines = getdiff(
2224 servefail, lines = getdiff(
2225 expected, got, test.refpath, test.errpath
2225 expected, got, test.refpath, test.errpath
2226 )
2226 )
2227 self.stream.write('\n')
2227 self.stream.write('\n')
2228 for line in lines:
2228 for line in lines:
2229 line = highlightdiff(line, self.color)
2229 line = highlightdiff(line, self.color)
2230 if PYTHON3:
2230 if PYTHON3:
2231 self.stream.flush()
2231 self.stream.flush()
2232 self.stream.buffer.write(line)
2232 self.stream.buffer.write(line)
2233 self.stream.buffer.flush()
2233 self.stream.buffer.flush()
2234 else:
2234 else:
2235 self.stream.write(line)
2235 self.stream.write(line)
2236 self.stream.flush()
2236 self.stream.flush()
2237
2237
2238 if servefail:
2238 if servefail:
2239 raise test.failureException(
2239 raise test.failureException(
2240 'server failed to start (HGPORT=%s)' % test._startport
2240 'server failed to start (HGPORT=%s)' % test._startport
2241 )
2241 )
2242
2242
2243 # handle interactive prompt without releasing iolock
2243 # handle interactive prompt without releasing iolock
2244 if self._options.interactive:
2244 if self._options.interactive:
2245 if test.readrefout() != expected:
2245 if test.readrefout() != expected:
2246 self.stream.write(
2246 self.stream.write(
2247 'Reference output has changed (run again to prompt '
2247 'Reference output has changed (run again to prompt '
2248 'changes)'
2248 'changes)'
2249 )
2249 )
2250 else:
2250 else:
2251 self.stream.write('Accept this change? [n] ')
2251 self.stream.write('Accept this change? [n] ')
2252 self.stream.flush()
2252 self.stream.flush()
2253 answer = sys.stdin.readline().strip()
2253 answer = sys.stdin.readline().strip()
2254 if answer.lower() in ('y', 'yes'):
2254 if answer.lower() in ('y', 'yes'):
2255 if test.path.endswith(b'.t'):
2255 if test.path.endswith(b'.t'):
2256 rename(test.errpath, test.path)
2256 rename(test.errpath, test.path)
2257 else:
2257 else:
2258 rename(test.errpath, '%s.out' % test.path)
2258 rename(test.errpath, '%s.out' % test.path)
2259 accepted = True
2259 accepted = True
2260 if not accepted:
2260 if not accepted:
2261 self.faildata[test.name] = b''.join(lines)
2261 self.faildata[test.name] = b''.join(lines)
2262
2262
2263 return accepted
2263 return accepted
2264
2264
2265 def startTest(self, test):
2265 def startTest(self, test):
2266 super(TestResult, self).startTest(test)
2266 super(TestResult, self).startTest(test)
2267
2267
2268 # os.times module computes the user time and system time spent by
2268 # os.times module computes the user time and system time spent by
2269 # child's processes along with real elapsed time taken by a process.
2269 # child's processes along with real elapsed time taken by a process.
2270 # This module has one limitation. It can only work for Linux user
2270 # This module has one limitation. It can only work for Linux user
2271 # and not for Windows. Hence why we fall back to another function
2271 # and not for Windows. Hence why we fall back to another function
2272 # for wall time calculations.
2272 # for wall time calculations.
2273 test.started_times = os.times()
2273 test.started_times = os.times()
2274 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2274 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2275 test.started_time = time.time()
2275 test.started_time = time.time()
2276 if self._firststarttime is None: # thread racy but irrelevant
2276 if self._firststarttime is None: # thread racy but irrelevant
2277 self._firststarttime = test.started_time
2277 self._firststarttime = test.started_time
2278
2278
2279 def stopTest(self, test, interrupted=False):
2279 def stopTest(self, test, interrupted=False):
2280 super(TestResult, self).stopTest(test)
2280 super(TestResult, self).stopTest(test)
2281
2281
2282 test.stopped_times = os.times()
2282 test.stopped_times = os.times()
2283 stopped_time = time.time()
2283 stopped_time = time.time()
2284
2284
2285 starttime = test.started_times
2285 starttime = test.started_times
2286 endtime = test.stopped_times
2286 endtime = test.stopped_times
2287 origin = self._firststarttime
2287 origin = self._firststarttime
2288 self.times.append(
2288 self.times.append(
2289 (
2289 (
2290 test.name,
2290 test.name,
2291 endtime[2] - starttime[2], # user space CPU time
2291 endtime[2] - starttime[2], # user space CPU time
2292 endtime[3] - starttime[3], # sys space CPU time
2292 endtime[3] - starttime[3], # sys space CPU time
2293 stopped_time - test.started_time, # real time
2293 stopped_time - test.started_time, # real time
2294 test.started_time - origin, # start date in run context
2294 test.started_time - origin, # start date in run context
2295 stopped_time - origin, # end date in run context
2295 stopped_time - origin, # end date in run context
2296 )
2296 )
2297 )
2297 )
2298
2298
2299 if interrupted:
2299 if interrupted:
2300 with iolock:
2300 with iolock:
2301 self.stream.writeln(
2301 self.stream.writeln(
2302 'INTERRUPTED: %s (after %d seconds)'
2302 'INTERRUPTED: %s (after %d seconds)'
2303 % (test.name, self.times[-1][3])
2303 % (test.name, self.times[-1][3])
2304 )
2304 )
2305
2305
2306
2306
2307 def getTestResult():
2307 def getTestResult():
2308 """
2308 """
2309 Returns the relevant test result
2309 Returns the relevant test result
2310 """
2310 """
2311 if "CUSTOM_TEST_RESULT" in os.environ:
2311 if "CUSTOM_TEST_RESULT" in os.environ:
2312 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2312 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2313 return testresultmodule.TestResult
2313 return testresultmodule.TestResult
2314 else:
2314 else:
2315 return TestResult
2315 return TestResult
2316
2316
2317
2317
2318 class TestSuite(unittest.TestSuite):
2318 class TestSuite(unittest.TestSuite):
2319 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2319 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2320
2320
2321 def __init__(
2321 def __init__(
2322 self,
2322 self,
2323 testdir,
2323 testdir,
2324 jobs=1,
2324 jobs=1,
2325 whitelist=None,
2325 whitelist=None,
2326 blacklist=None,
2326 blacklist=None,
2327 retest=False,
2327 retest=False,
2328 keywords=None,
2328 keywords=None,
2329 loop=False,
2329 loop=False,
2330 runs_per_test=1,
2330 runs_per_test=1,
2331 loadtest=None,
2331 loadtest=None,
2332 showchannels=False,
2332 showchannels=False,
2333 *args,
2333 *args,
2334 **kwargs
2334 **kwargs
2335 ):
2335 ):
2336 """Create a new instance that can run tests with a configuration.
2336 """Create a new instance that can run tests with a configuration.
2337
2337
2338 testdir specifies the directory where tests are executed from. This
2338 testdir specifies the directory where tests are executed from. This
2339 is typically the ``tests`` directory from Mercurial's source
2339 is typically the ``tests`` directory from Mercurial's source
2340 repository.
2340 repository.
2341
2341
2342 jobs specifies the number of jobs to run concurrently. Each test
2342 jobs specifies the number of jobs to run concurrently. Each test
2343 executes on its own thread. Tests actually spawn new processes, so
2343 executes on its own thread. Tests actually spawn new processes, so
2344 state mutation should not be an issue.
2344 state mutation should not be an issue.
2345
2345
2346 If there is only one job, it will use the main thread.
2346 If there is only one job, it will use the main thread.
2347
2347
2348 whitelist and blacklist denote tests that have been whitelisted and
2348 whitelist and blacklist denote tests that have been whitelisted and
2349 blacklisted, respectively. These arguments don't belong in TestSuite.
2349 blacklisted, respectively. These arguments don't belong in TestSuite.
2350 Instead, whitelist and blacklist should be handled by the thing that
2350 Instead, whitelist and blacklist should be handled by the thing that
2351 populates the TestSuite with tests. They are present to preserve
2351 populates the TestSuite with tests. They are present to preserve
2352 backwards compatible behavior which reports skipped tests as part
2352 backwards compatible behavior which reports skipped tests as part
2353 of the results.
2353 of the results.
2354
2354
2355 retest denotes whether to retest failed tests. This arguably belongs
2355 retest denotes whether to retest failed tests. This arguably belongs
2356 outside of TestSuite.
2356 outside of TestSuite.
2357
2357
2358 keywords denotes key words that will be used to filter which tests
2358 keywords denotes key words that will be used to filter which tests
2359 to execute. This arguably belongs outside of TestSuite.
2359 to execute. This arguably belongs outside of TestSuite.
2360
2360
2361 loop denotes whether to loop over tests forever.
2361 loop denotes whether to loop over tests forever.
2362 """
2362 """
2363 super(TestSuite, self).__init__(*args, **kwargs)
2363 super(TestSuite, self).__init__(*args, **kwargs)
2364
2364
2365 self._jobs = jobs
2365 self._jobs = jobs
2366 self._whitelist = whitelist
2366 self._whitelist = whitelist
2367 self._blacklist = blacklist
2367 self._blacklist = blacklist
2368 self._retest = retest
2368 self._retest = retest
2369 self._keywords = keywords
2369 self._keywords = keywords
2370 self._loop = loop
2370 self._loop = loop
2371 self._runs_per_test = runs_per_test
2371 self._runs_per_test = runs_per_test
2372 self._loadtest = loadtest
2372 self._loadtest = loadtest
2373 self._showchannels = showchannels
2373 self._showchannels = showchannels
2374
2374
2375 def run(self, result):
2375 def run(self, result):
2376 # We have a number of filters that need to be applied. We do this
2376 # We have a number of filters that need to be applied. We do this
2377 # here instead of inside Test because it makes the running logic for
2377 # here instead of inside Test because it makes the running logic for
2378 # Test simpler.
2378 # Test simpler.
2379 tests = []
2379 tests = []
2380 num_tests = [0]
2380 num_tests = [0]
2381 for test in self._tests:
2381 for test in self._tests:
2382
2382
2383 def get():
2383 def get():
2384 num_tests[0] += 1
2384 num_tests[0] += 1
2385 if getattr(test, 'should_reload', False):
2385 if getattr(test, 'should_reload', False):
2386 return self._loadtest(test, num_tests[0])
2386 return self._loadtest(test, num_tests[0])
2387 return test
2387 return test
2388
2388
2389 if not os.path.exists(test.path):
2389 if not os.path.exists(test.path):
2390 result.addSkip(test, "Doesn't exist")
2390 result.addSkip(test, "Doesn't exist")
2391 continue
2391 continue
2392
2392
2393 if not (self._whitelist and test.bname in self._whitelist):
2393 if not (self._whitelist and test.bname in self._whitelist):
2394 if self._blacklist and test.bname in self._blacklist:
2394 if self._blacklist and test.bname in self._blacklist:
2395 result.addSkip(test, 'blacklisted')
2395 result.addSkip(test, 'blacklisted')
2396 continue
2396 continue
2397
2397
2398 if self._retest and not os.path.exists(test.errpath):
2398 if self._retest and not os.path.exists(test.errpath):
2399 result.addIgnore(test, 'not retesting')
2399 result.addIgnore(test, 'not retesting')
2400 continue
2400 continue
2401
2401
2402 if self._keywords:
2402 if self._keywords:
2403 with open(test.path, 'rb') as f:
2403 with open(test.path, 'rb') as f:
2404 t = f.read().lower() + test.bname.lower()
2404 t = f.read().lower() + test.bname.lower()
2405 ignored = False
2405 ignored = False
2406 for k in self._keywords.lower().split():
2406 for k in self._keywords.lower().split():
2407 if k not in t:
2407 if k not in t:
2408 result.addIgnore(test, "doesn't match keyword")
2408 result.addIgnore(test, "doesn't match keyword")
2409 ignored = True
2409 ignored = True
2410 break
2410 break
2411
2411
2412 if ignored:
2412 if ignored:
2413 continue
2413 continue
2414 for _ in xrange(self._runs_per_test):
2414 for _ in xrange(self._runs_per_test):
2415 tests.append(get())
2415 tests.append(get())
2416
2416
2417 runtests = list(tests)
2417 runtests = list(tests)
2418 done = queue.Queue()
2418 done = queue.Queue()
2419 running = 0
2419 running = 0
2420
2420
2421 channels = [""] * self._jobs
2421 channels = [""] * self._jobs
2422
2422
2423 def job(test, result):
2423 def job(test, result):
2424 for n, v in enumerate(channels):
2424 for n, v in enumerate(channels):
2425 if not v:
2425 if not v:
2426 channel = n
2426 channel = n
2427 break
2427 break
2428 else:
2428 else:
2429 raise ValueError('Could not find output channel')
2429 raise ValueError('Could not find output channel')
2430 channels[channel] = "=" + test.name[5:].split(".")[0]
2430 channels[channel] = "=" + test.name[5:].split(".")[0]
2431 try:
2431 try:
2432 test(result)
2432 test(result)
2433 done.put(None)
2433 done.put(None)
2434 except KeyboardInterrupt:
2434 except KeyboardInterrupt:
2435 pass
2435 pass
2436 except: # re-raises
2436 except: # re-raises
2437 done.put(('!', test, 'run-test raised an error, see traceback'))
2437 done.put(('!', test, 'run-test raised an error, see traceback'))
2438 raise
2438 raise
2439 finally:
2439 finally:
2440 try:
2440 try:
2441 channels[channel] = ''
2441 channels[channel] = ''
2442 except IndexError:
2442 except IndexError:
2443 pass
2443 pass
2444
2444
2445 def stat():
2445 def stat():
2446 count = 0
2446 count = 0
2447 while channels:
2447 while channels:
2448 d = '\n%03s ' % count
2448 d = '\n%03s ' % count
2449 for n, v in enumerate(channels):
2449 for n, v in enumerate(channels):
2450 if v:
2450 if v:
2451 d += v[0]
2451 d += v[0]
2452 channels[n] = v[1:] or '.'
2452 channels[n] = v[1:] or '.'
2453 else:
2453 else:
2454 d += ' '
2454 d += ' '
2455 d += ' '
2455 d += ' '
2456 with iolock:
2456 with iolock:
2457 sys.stdout.write(d + ' ')
2457 sys.stdout.write(d + ' ')
2458 sys.stdout.flush()
2458 sys.stdout.flush()
2459 for x in xrange(10):
2459 for x in xrange(10):
2460 if channels:
2460 if channels:
2461 time.sleep(0.1)
2461 time.sleep(0.1)
2462 count += 1
2462 count += 1
2463
2463
2464 stoppedearly = False
2464 stoppedearly = False
2465
2465
2466 if self._showchannels:
2466 if self._showchannels:
2467 statthread = threading.Thread(target=stat, name="stat")
2467 statthread = threading.Thread(target=stat, name="stat")
2468 statthread.start()
2468 statthread.start()
2469
2469
2470 try:
2470 try:
2471 while tests or running:
2471 while tests or running:
2472 if not done.empty() or running == self._jobs or not tests:
2472 if not done.empty() or running == self._jobs or not tests:
2473 try:
2473 try:
2474 done.get(True, 1)
2474 done.get(True, 1)
2475 running -= 1
2475 running -= 1
2476 if result and result.shouldStop:
2476 if result and result.shouldStop:
2477 stoppedearly = True
2477 stoppedearly = True
2478 break
2478 break
2479 except queue.Empty:
2479 except queue.Empty:
2480 continue
2480 continue
2481 if tests and not running == self._jobs:
2481 if tests and not running == self._jobs:
2482 test = tests.pop(0)
2482 test = tests.pop(0)
2483 if self._loop:
2483 if self._loop:
2484 if getattr(test, 'should_reload', False):
2484 if getattr(test, 'should_reload', False):
2485 num_tests[0] += 1
2485 num_tests[0] += 1
2486 tests.append(self._loadtest(test, num_tests[0]))
2486 tests.append(self._loadtest(test, num_tests[0]))
2487 else:
2487 else:
2488 tests.append(test)
2488 tests.append(test)
2489 if self._jobs == 1:
2489 if self._jobs == 1:
2490 job(test, result)
2490 job(test, result)
2491 else:
2491 else:
2492 t = threading.Thread(
2492 t = threading.Thread(
2493 target=job, name=test.name, args=(test, result)
2493 target=job, name=test.name, args=(test, result)
2494 )
2494 )
2495 t.start()
2495 t.start()
2496 running += 1
2496 running += 1
2497
2497
2498 # If we stop early we still need to wait on started tests to
2498 # If we stop early we still need to wait on started tests to
2499 # finish. Otherwise, there is a race between the test completing
2499 # finish. Otherwise, there is a race between the test completing
2500 # and the test's cleanup code running. This could result in the
2500 # and the test's cleanup code running. This could result in the
2501 # test reporting incorrect.
2501 # test reporting incorrect.
2502 if stoppedearly:
2502 if stoppedearly:
2503 while running:
2503 while running:
2504 try:
2504 try:
2505 done.get(True, 1)
2505 done.get(True, 1)
2506 running -= 1
2506 running -= 1
2507 except queue.Empty:
2507 except queue.Empty:
2508 continue
2508 continue
2509 except KeyboardInterrupt:
2509 except KeyboardInterrupt:
2510 for test in runtests:
2510 for test in runtests:
2511 test.abort()
2511 test.abort()
2512
2512
2513 channels = []
2513 channels = []
2514
2514
2515 return result
2515 return result
2516
2516
2517
2517
2518 # Save the most recent 5 wall-clock runtimes of each test to a
2518 # Save the most recent 5 wall-clock runtimes of each test to a
2519 # human-readable text file named .testtimes. Tests are sorted
2519 # human-readable text file named .testtimes. Tests are sorted
2520 # alphabetically, while times for each test are listed from oldest to
2520 # alphabetically, while times for each test are listed from oldest to
2521 # newest.
2521 # newest.
2522
2522
2523
2523
2524 def loadtimes(outputdir):
2524 def loadtimes(outputdir):
2525 times = []
2525 times = []
2526 try:
2526 try:
2527 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2527 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2528 for line in fp:
2528 for line in fp:
2529 m = re.match('(.*?) ([0-9. ]+)', line)
2529 m = re.match('(.*?) ([0-9. ]+)', line)
2530 times.append(
2530 times.append(
2531 (m.group(1), [float(t) for t in m.group(2).split()])
2531 (m.group(1), [float(t) for t in m.group(2).split()])
2532 )
2532 )
2533 except IOError as err:
2533 except IOError as err:
2534 if err.errno != errno.ENOENT:
2534 if err.errno != errno.ENOENT:
2535 raise
2535 raise
2536 return times
2536 return times
2537
2537
2538
2538
2539 def savetimes(outputdir, result):
2539 def savetimes(outputdir, result):
2540 saved = dict(loadtimes(outputdir))
2540 saved = dict(loadtimes(outputdir))
2541 maxruns = 5
2541 maxruns = 5
2542 skipped = {str(t[0]) for t in result.skipped}
2542 skipped = {str(t[0]) for t in result.skipped}
2543 for tdata in result.times:
2543 for tdata in result.times:
2544 test, real = tdata[0], tdata[3]
2544 test, real = tdata[0], tdata[3]
2545 if test not in skipped:
2545 if test not in skipped:
2546 ts = saved.setdefault(test, [])
2546 ts = saved.setdefault(test, [])
2547 ts.append(real)
2547 ts.append(real)
2548 ts[:] = ts[-maxruns:]
2548 ts[:] = ts[-maxruns:]
2549
2549
2550 fd, tmpname = tempfile.mkstemp(
2550 fd, tmpname = tempfile.mkstemp(
2551 prefix=b'.testtimes', dir=outputdir, text=True
2551 prefix=b'.testtimes', dir=outputdir, text=True
2552 )
2552 )
2553 with os.fdopen(fd, 'w') as fp:
2553 with os.fdopen(fd, 'w') as fp:
2554 for name, ts in sorted(saved.items()):
2554 for name, ts in sorted(saved.items()):
2555 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2555 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2556 timepath = os.path.join(outputdir, b'.testtimes')
2556 timepath = os.path.join(outputdir, b'.testtimes')
2557 try:
2557 try:
2558 os.unlink(timepath)
2558 os.unlink(timepath)
2559 except OSError:
2559 except OSError:
2560 pass
2560 pass
2561 try:
2561 try:
2562 os.rename(tmpname, timepath)
2562 os.rename(tmpname, timepath)
2563 except OSError:
2563 except OSError:
2564 pass
2564 pass
2565
2565
2566
2566
2567 class TextTestRunner(unittest.TextTestRunner):
2567 class TextTestRunner(unittest.TextTestRunner):
2568 """Custom unittest test runner that uses appropriate settings."""
2568 """Custom unittest test runner that uses appropriate settings."""
2569
2569
2570 def __init__(self, runner, *args, **kwargs):
2570 def __init__(self, runner, *args, **kwargs):
2571 super(TextTestRunner, self).__init__(*args, **kwargs)
2571 super(TextTestRunner, self).__init__(*args, **kwargs)
2572
2572
2573 self._runner = runner
2573 self._runner = runner
2574
2574
2575 self._result = getTestResult()(
2575 self._result = getTestResult()(
2576 self._runner.options, self.stream, self.descriptions, self.verbosity
2576 self._runner.options, self.stream, self.descriptions, self.verbosity
2577 )
2577 )
2578
2578
2579 def listtests(self, test):
2579 def listtests(self, test):
2580 test = sorted(test, key=lambda t: t.name)
2580 test = sorted(test, key=lambda t: t.name)
2581
2581
2582 self._result.onStart(test)
2582 self._result.onStart(test)
2583
2583
2584 for t in test:
2584 for t in test:
2585 print(t.name)
2585 print(t.name)
2586 self._result.addSuccess(t)
2586 self._result.addSuccess(t)
2587
2587
2588 if self._runner.options.xunit:
2588 if self._runner.options.xunit:
2589 with open(self._runner.options.xunit, "wb") as xuf:
2589 with open(self._runner.options.xunit, "wb") as xuf:
2590 self._writexunit(self._result, xuf)
2590 self._writexunit(self._result, xuf)
2591
2591
2592 if self._runner.options.json:
2592 if self._runner.options.json:
2593 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2593 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2594 with open(jsonpath, 'w') as fp:
2594 with open(jsonpath, 'w') as fp:
2595 self._writejson(self._result, fp)
2595 self._writejson(self._result, fp)
2596
2596
2597 return self._result
2597 return self._result
2598
2598
2599 def run(self, test):
2599 def run(self, test):
2600 self._result.onStart(test)
2600 self._result.onStart(test)
2601 test(self._result)
2601 test(self._result)
2602
2602
2603 failed = len(self._result.failures)
2603 failed = len(self._result.failures)
2604 skipped = len(self._result.skipped)
2604 skipped = len(self._result.skipped)
2605 ignored = len(self._result.ignored)
2605 ignored = len(self._result.ignored)
2606
2606
2607 with iolock:
2607 with iolock:
2608 self.stream.writeln('')
2608 self.stream.writeln('')
2609
2609
2610 if not self._runner.options.noskips:
2610 if not self._runner.options.noskips:
2611 for test, msg in sorted(
2611 for test, msg in sorted(
2612 self._result.skipped, key=lambda s: s[0].name
2612 self._result.skipped, key=lambda s: s[0].name
2613 ):
2613 ):
2614 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2614 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2615 msg = highlightmsg(formatted, self._result.color)
2615 msg = highlightmsg(formatted, self._result.color)
2616 self.stream.write(msg)
2616 self.stream.write(msg)
2617 for test, msg in sorted(
2617 for test, msg in sorted(
2618 self._result.failures, key=lambda f: f[0].name
2618 self._result.failures, key=lambda f: f[0].name
2619 ):
2619 ):
2620 formatted = 'Failed %s: %s\n' % (test.name, msg)
2620 formatted = 'Failed %s: %s\n' % (test.name, msg)
2621 self.stream.write(highlightmsg(formatted, self._result.color))
2621 self.stream.write(highlightmsg(formatted, self._result.color))
2622 for test, msg in sorted(
2622 for test, msg in sorted(
2623 self._result.errors, key=lambda e: e[0].name
2623 self._result.errors, key=lambda e: e[0].name
2624 ):
2624 ):
2625 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2625 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2626
2626
2627 if self._runner.options.xunit:
2627 if self._runner.options.xunit:
2628 with open(self._runner.options.xunit, "wb") as xuf:
2628 with open(self._runner.options.xunit, "wb") as xuf:
2629 self._writexunit(self._result, xuf)
2629 self._writexunit(self._result, xuf)
2630
2630
2631 if self._runner.options.json:
2631 if self._runner.options.json:
2632 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2632 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2633 with open(jsonpath, 'w') as fp:
2633 with open(jsonpath, 'w') as fp:
2634 self._writejson(self._result, fp)
2634 self._writejson(self._result, fp)
2635
2635
2636 self._runner._checkhglib('Tested')
2636 self._runner._checkhglib('Tested')
2637
2637
2638 savetimes(self._runner._outputdir, self._result)
2638 savetimes(self._runner._outputdir, self._result)
2639
2639
2640 if failed and self._runner.options.known_good_rev:
2640 if failed and self._runner.options.known_good_rev:
2641 self._bisecttests(t for t, m in self._result.failures)
2641 self._bisecttests(t for t, m in self._result.failures)
2642 self.stream.writeln(
2642 self.stream.writeln(
2643 '# Ran %d tests, %d skipped, %d failed.'
2643 '# Ran %d tests, %d skipped, %d failed.'
2644 % (self._result.testsRun, skipped + ignored, failed)
2644 % (self._result.testsRun, skipped + ignored, failed)
2645 )
2645 )
2646 if failed:
2646 if failed:
2647 self.stream.writeln(
2647 self.stream.writeln(
2648 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2648 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2649 )
2649 )
2650 if self._runner.options.time:
2650 if self._runner.options.time:
2651 self.printtimes(self._result.times)
2651 self.printtimes(self._result.times)
2652
2652
2653 if self._runner.options.exceptions:
2653 if self._runner.options.exceptions:
2654 exceptions = aggregateexceptions(
2654 exceptions = aggregateexceptions(
2655 os.path.join(self._runner._outputdir, b'exceptions')
2655 os.path.join(self._runner._outputdir, b'exceptions')
2656 )
2656 )
2657
2657
2658 self.stream.writeln('Exceptions Report:')
2658 self.stream.writeln('Exceptions Report:')
2659 self.stream.writeln(
2659 self.stream.writeln(
2660 '%d total from %d frames'
2660 '%d total from %d frames'
2661 % (exceptions['total'], len(exceptions['exceptioncounts']))
2661 % (exceptions['total'], len(exceptions['exceptioncounts']))
2662 )
2662 )
2663 combined = exceptions['combined']
2663 combined = exceptions['combined']
2664 for key in sorted(combined, key=combined.get, reverse=True):
2664 for key in sorted(combined, key=combined.get, reverse=True):
2665 frame, line, exc = key
2665 frame, line, exc = key
2666 totalcount, testcount, leastcount, leasttest = combined[key]
2666 totalcount, testcount, leastcount, leasttest = combined[key]
2667
2667
2668 self.stream.writeln(
2668 self.stream.writeln(
2669 '%d (%d tests)\t%s: %s (%s - %d total)'
2669 '%d (%d tests)\t%s: %s (%s - %d total)'
2670 % (
2670 % (
2671 totalcount,
2671 totalcount,
2672 testcount,
2672 testcount,
2673 frame,
2673 frame,
2674 exc,
2674 exc,
2675 leasttest,
2675 leasttest,
2676 leastcount,
2676 leastcount,
2677 )
2677 )
2678 )
2678 )
2679
2679
2680 self.stream.flush()
2680 self.stream.flush()
2681
2681
2682 return self._result
2682 return self._result
2683
2683
2684 def _bisecttests(self, tests):
2684 def _bisecttests(self, tests):
2685 bisectcmd = ['hg', 'bisect']
2685 bisectcmd = ['hg', 'bisect']
2686 bisectrepo = self._runner.options.bisect_repo
2686 bisectrepo = self._runner.options.bisect_repo
2687 if bisectrepo:
2687 if bisectrepo:
2688 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2688 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2689
2689
2690 def pread(args):
2690 def pread(args):
2691 env = os.environ.copy()
2691 env = os.environ.copy()
2692 env['HGPLAIN'] = '1'
2692 env['HGPLAIN'] = '1'
2693 p = subprocess.Popen(
2693 p = subprocess.Popen(
2694 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2694 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2695 )
2695 )
2696 data = p.stdout.read()
2696 data = p.stdout.read()
2697 p.wait()
2697 p.wait()
2698 return data
2698 return data
2699
2699
2700 for test in tests:
2700 for test in tests:
2701 pread(bisectcmd + ['--reset']),
2701 pread(bisectcmd + ['--reset']),
2702 pread(bisectcmd + ['--bad', '.'])
2702 pread(bisectcmd + ['--bad', '.'])
2703 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2703 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2704 # TODO: we probably need to forward more options
2704 # TODO: we probably need to forward more options
2705 # that alter hg's behavior inside the tests.
2705 # that alter hg's behavior inside the tests.
2706 opts = ''
2706 opts = ''
2707 withhg = self._runner.options.with_hg
2707 withhg = self._runner.options.with_hg
2708 if withhg:
2708 if withhg:
2709 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2709 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2710 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2710 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2711 data = pread(bisectcmd + ['--command', rtc])
2711 data = pread(bisectcmd + ['--command', rtc])
2712 m = re.search(
2712 m = re.search(
2713 (
2713 (
2714 br'\nThe first (?P<goodbad>bad|good) revision '
2714 br'\nThe first (?P<goodbad>bad|good) revision '
2715 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2715 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2716 br'summary: +(?P<summary>[^\n]+)\n'
2716 br'summary: +(?P<summary>[^\n]+)\n'
2717 ),
2717 ),
2718 data,
2718 data,
2719 (re.MULTILINE | re.DOTALL),
2719 (re.MULTILINE | re.DOTALL),
2720 )
2720 )
2721 if m is None:
2721 if m is None:
2722 self.stream.writeln(
2722 self.stream.writeln(
2723 'Failed to identify failure point for %s' % test
2723 'Failed to identify failure point for %s' % test
2724 )
2724 )
2725 continue
2725 continue
2726 dat = m.groupdict()
2726 dat = m.groupdict()
2727 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2727 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2728 self.stream.writeln(
2728 self.stream.writeln(
2729 '%s %s by %s (%s)'
2729 '%s %s by %s (%s)'
2730 % (
2730 % (
2731 test,
2731 test,
2732 verb,
2732 verb,
2733 dat['node'].decode('ascii'),
2733 dat['node'].decode('ascii'),
2734 dat['summary'].decode('utf8', 'ignore'),
2734 dat['summary'].decode('utf8', 'ignore'),
2735 )
2735 )
2736 )
2736 )
2737
2737
2738 def printtimes(self, times):
2738 def printtimes(self, times):
2739 # iolock held by run
2739 # iolock held by run
2740 self.stream.writeln('# Producing time report')
2740 self.stream.writeln('# Producing time report')
2741 times.sort(key=lambda t: (t[3]))
2741 times.sort(key=lambda t: (t[3]))
2742 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2742 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2743 self.stream.writeln(
2743 self.stream.writeln(
2744 '%-7s %-7s %-7s %-7s %-7s %s'
2744 '%-7s %-7s %-7s %-7s %-7s %s'
2745 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2745 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2746 )
2746 )
2747 for tdata in times:
2747 for tdata in times:
2748 test = tdata[0]
2748 test = tdata[0]
2749 cuser, csys, real, start, end = tdata[1:6]
2749 cuser, csys, real, start, end = tdata[1:6]
2750 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2750 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2751
2751
2752 @staticmethod
2752 @staticmethod
2753 def _writexunit(result, outf):
2753 def _writexunit(result, outf):
2754 # See http://llg.cubic.org/docs/junit/ for a reference.
2754 # See http://llg.cubic.org/docs/junit/ for a reference.
2755 timesd = {t[0]: t[3] for t in result.times}
2755 timesd = {t[0]: t[3] for t in result.times}
2756 doc = minidom.Document()
2756 doc = minidom.Document()
2757 s = doc.createElement('testsuite')
2757 s = doc.createElement('testsuite')
2758 s.setAttribute('errors', "0") # TODO
2758 s.setAttribute('errors', "0") # TODO
2759 s.setAttribute('failures', str(len(result.failures)))
2759 s.setAttribute('failures', str(len(result.failures)))
2760 s.setAttribute('name', 'run-tests')
2760 s.setAttribute('name', 'run-tests')
2761 s.setAttribute(
2761 s.setAttribute(
2762 'skipped', str(len(result.skipped) + len(result.ignored))
2762 'skipped', str(len(result.skipped) + len(result.ignored))
2763 )
2763 )
2764 s.setAttribute('tests', str(result.testsRun))
2764 s.setAttribute('tests', str(result.testsRun))
2765 doc.appendChild(s)
2765 doc.appendChild(s)
2766 for tc in result.successes:
2766 for tc in result.successes:
2767 t = doc.createElement('testcase')
2767 t = doc.createElement('testcase')
2768 t.setAttribute('name', tc.name)
2768 t.setAttribute('name', tc.name)
2769 tctime = timesd.get(tc.name)
2769 tctime = timesd.get(tc.name)
2770 if tctime is not None:
2770 if tctime is not None:
2771 t.setAttribute('time', '%.3f' % tctime)
2771 t.setAttribute('time', '%.3f' % tctime)
2772 s.appendChild(t)
2772 s.appendChild(t)
2773 for tc, err in sorted(result.faildata.items()):
2773 for tc, err in sorted(result.faildata.items()):
2774 t = doc.createElement('testcase')
2774 t = doc.createElement('testcase')
2775 t.setAttribute('name', tc)
2775 t.setAttribute('name', tc)
2776 tctime = timesd.get(tc)
2776 tctime = timesd.get(tc)
2777 if tctime is not None:
2777 if tctime is not None:
2778 t.setAttribute('time', '%.3f' % tctime)
2778 t.setAttribute('time', '%.3f' % tctime)
2779 # createCDATASection expects a unicode or it will
2779 # createCDATASection expects a unicode or it will
2780 # convert using default conversion rules, which will
2780 # convert using default conversion rules, which will
2781 # fail if string isn't ASCII.
2781 # fail if string isn't ASCII.
2782 err = cdatasafe(err).decode('utf-8', 'replace')
2782 err = cdatasafe(err).decode('utf-8', 'replace')
2783 cd = doc.createCDATASection(err)
2783 cd = doc.createCDATASection(err)
2784 # Use 'failure' here instead of 'error' to match errors = 0,
2784 # Use 'failure' here instead of 'error' to match errors = 0,
2785 # failures = len(result.failures) in the testsuite element.
2785 # failures = len(result.failures) in the testsuite element.
2786 failelem = doc.createElement('failure')
2786 failelem = doc.createElement('failure')
2787 failelem.setAttribute('message', 'output changed')
2787 failelem.setAttribute('message', 'output changed')
2788 failelem.setAttribute('type', 'output-mismatch')
2788 failelem.setAttribute('type', 'output-mismatch')
2789 failelem.appendChild(cd)
2789 failelem.appendChild(cd)
2790 t.appendChild(failelem)
2790 t.appendChild(failelem)
2791 s.appendChild(t)
2791 s.appendChild(t)
2792 for tc, message in result.skipped:
2792 for tc, message in result.skipped:
2793 # According to the schema, 'skipped' has no attributes. So store
2793 # According to the schema, 'skipped' has no attributes. So store
2794 # the skip message as a text node instead.
2794 # the skip message as a text node instead.
2795 t = doc.createElement('testcase')
2795 t = doc.createElement('testcase')
2796 t.setAttribute('name', tc.name)
2796 t.setAttribute('name', tc.name)
2797 binmessage = message.encode('utf-8')
2797 binmessage = message.encode('utf-8')
2798 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2798 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2799 cd = doc.createCDATASection(message)
2799 cd = doc.createCDATASection(message)
2800 skipelem = doc.createElement('skipped')
2800 skipelem = doc.createElement('skipped')
2801 skipelem.appendChild(cd)
2801 skipelem.appendChild(cd)
2802 t.appendChild(skipelem)
2802 t.appendChild(skipelem)
2803 s.appendChild(t)
2803 s.appendChild(t)
2804 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2804 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2805
2805
2806 @staticmethod
2806 @staticmethod
2807 def _writejson(result, outf):
2807 def _writejson(result, outf):
2808 timesd = {}
2808 timesd = {}
2809 for tdata in result.times:
2809 for tdata in result.times:
2810 test = tdata[0]
2810 test = tdata[0]
2811 timesd[test] = tdata[1:]
2811 timesd[test] = tdata[1:]
2812
2812
2813 outcome = {}
2813 outcome = {}
2814 groups = [
2814 groups = [
2815 ('success', ((tc, None) for tc in result.successes)),
2815 ('success', ((tc, None) for tc in result.successes)),
2816 ('failure', result.failures),
2816 ('failure', result.failures),
2817 ('skip', result.skipped),
2817 ('skip', result.skipped),
2818 ]
2818 ]
2819 for res, testcases in groups:
2819 for res, testcases in groups:
2820 for tc, __ in testcases:
2820 for tc, __ in testcases:
2821 if tc.name in timesd:
2821 if tc.name in timesd:
2822 diff = result.faildata.get(tc.name, b'')
2822 diff = result.faildata.get(tc.name, b'')
2823 try:
2823 try:
2824 diff = diff.decode('unicode_escape')
2824 diff = diff.decode('unicode_escape')
2825 except UnicodeDecodeError as e:
2825 except UnicodeDecodeError as e:
2826 diff = '%r decoding diff, sorry' % e
2826 diff = '%r decoding diff, sorry' % e
2827 tres = {
2827 tres = {
2828 'result': res,
2828 'result': res,
2829 'time': ('%0.3f' % timesd[tc.name][2]),
2829 'time': ('%0.3f' % timesd[tc.name][2]),
2830 'cuser': ('%0.3f' % timesd[tc.name][0]),
2830 'cuser': ('%0.3f' % timesd[tc.name][0]),
2831 'csys': ('%0.3f' % timesd[tc.name][1]),
2831 'csys': ('%0.3f' % timesd[tc.name][1]),
2832 'start': ('%0.3f' % timesd[tc.name][3]),
2832 'start': ('%0.3f' % timesd[tc.name][3]),
2833 'end': ('%0.3f' % timesd[tc.name][4]),
2833 'end': ('%0.3f' % timesd[tc.name][4]),
2834 'diff': diff,
2834 'diff': diff,
2835 }
2835 }
2836 else:
2836 else:
2837 # blacklisted test
2837 # blacklisted test
2838 tres = {'result': res}
2838 tres = {'result': res}
2839
2839
2840 outcome[tc.name] = tres
2840 outcome[tc.name] = tres
2841 jsonout = json.dumps(
2841 jsonout = json.dumps(
2842 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2842 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2843 )
2843 )
2844 outf.writelines(("testreport =", jsonout))
2844 outf.writelines(("testreport =", jsonout))
2845
2845
2846
2846
2847 def sorttests(testdescs, previoustimes, shuffle=False):
2847 def sorttests(testdescs, previoustimes, shuffle=False):
2848 """Do an in-place sort of tests."""
2848 """Do an in-place sort of tests."""
2849 if shuffle:
2849 if shuffle:
2850 random.shuffle(testdescs)
2850 random.shuffle(testdescs)
2851 return
2851 return
2852
2852
2853 if previoustimes:
2853 if previoustimes:
2854
2854
2855 def sortkey(f):
2855 def sortkey(f):
2856 f = f['path']
2856 f = f['path']
2857 if f in previoustimes:
2857 if f in previoustimes:
2858 # Use most recent time as estimate
2858 # Use most recent time as estimate
2859 return -(previoustimes[f][-1])
2859 return -(previoustimes[f][-1])
2860 else:
2860 else:
2861 # Default to a rather arbitrary value of 1 second for new tests
2861 # Default to a rather arbitrary value of 1 second for new tests
2862 return -1.0
2862 return -1.0
2863
2863
2864 else:
2864 else:
2865 # keywords for slow tests
2865 # keywords for slow tests
2866 slow = {
2866 slow = {
2867 b'svn': 10,
2867 b'svn': 10,
2868 b'cvs': 10,
2868 b'cvs': 10,
2869 b'hghave': 10,
2869 b'hghave': 10,
2870 b'largefiles-update': 10,
2870 b'largefiles-update': 10,
2871 b'run-tests': 10,
2871 b'run-tests': 10,
2872 b'corruption': 10,
2872 b'corruption': 10,
2873 b'race': 10,
2873 b'race': 10,
2874 b'i18n': 10,
2874 b'i18n': 10,
2875 b'check': 100,
2875 b'check': 100,
2876 b'gendoc': 100,
2876 b'gendoc': 100,
2877 b'contrib-perf': 200,
2877 b'contrib-perf': 200,
2878 b'merge-combination': 100,
2878 b'merge-combination': 100,
2879 }
2879 }
2880 perf = {}
2880 perf = {}
2881
2881
2882 def sortkey(f):
2882 def sortkey(f):
2883 # run largest tests first, as they tend to take the longest
2883 # run largest tests first, as they tend to take the longest
2884 f = f['path']
2884 f = f['path']
2885 try:
2885 try:
2886 return perf[f]
2886 return perf[f]
2887 except KeyError:
2887 except KeyError:
2888 try:
2888 try:
2889 val = -os.stat(f).st_size
2889 val = -os.stat(f).st_size
2890 except OSError as e:
2890 except OSError as e:
2891 if e.errno != errno.ENOENT:
2891 if e.errno != errno.ENOENT:
2892 raise
2892 raise
2893 perf[f] = -1e9 # file does not exist, tell early
2893 perf[f] = -1e9 # file does not exist, tell early
2894 return -1e9
2894 return -1e9
2895 for kw, mul in slow.items():
2895 for kw, mul in slow.items():
2896 if kw in f:
2896 if kw in f:
2897 val *= mul
2897 val *= mul
2898 if f.endswith(b'.py'):
2898 if f.endswith(b'.py'):
2899 val /= 10.0
2899 val /= 10.0
2900 perf[f] = val / 1000.0
2900 perf[f] = val / 1000.0
2901 return perf[f]
2901 return perf[f]
2902
2902
2903 testdescs.sort(key=sortkey)
2903 testdescs.sort(key=sortkey)
2904
2904
2905
2905
2906 class TestRunner(object):
2906 class TestRunner(object):
2907 """Holds context for executing tests.
2907 """Holds context for executing tests.
2908
2908
2909 Tests rely on a lot of state. This object holds it for them.
2909 Tests rely on a lot of state. This object holds it for them.
2910 """
2910 """
2911
2911
2912 # Programs required to run tests.
2912 # Programs required to run tests.
2913 REQUIREDTOOLS = [
2913 REQUIREDTOOLS = [
2914 b'diff',
2914 b'diff',
2915 b'grep',
2915 b'grep',
2916 b'unzip',
2916 b'unzip',
2917 b'gunzip',
2917 b'gunzip',
2918 b'bunzip2',
2918 b'bunzip2',
2919 b'sed',
2919 b'sed',
2920 ]
2920 ]
2921
2921
2922 # Maps file extensions to test class.
2922 # Maps file extensions to test class.
2923 TESTTYPES = [
2923 TESTTYPES = [
2924 (b'.py', PythonTest),
2924 (b'.py', PythonTest),
2925 (b'.t', TTest),
2925 (b'.t', TTest),
2926 ]
2926 ]
2927
2927
2928 def __init__(self):
2928 def __init__(self):
2929 self.options = None
2929 self.options = None
2930 self._hgroot = None
2930 self._hgroot = None
2931 self._testdir = None
2931 self._testdir = None
2932 self._outputdir = None
2932 self._outputdir = None
2933 self._hgtmp = None
2933 self._hgtmp = None
2934 self._installdir = None
2934 self._installdir = None
2935 self._bindir = None
2935 self._bindir = None
2936 self._tmpbinddir = None
2936 self._tmpbinddir = None
2937 self._pythondir = None
2937 self._pythondir = None
2938 self._coveragefile = None
2938 self._coveragefile = None
2939 self._createdfiles = []
2939 self._createdfiles = []
2940 self._hgcommand = None
2940 self._hgcommand = None
2941 self._hgpath = None
2941 self._hgpath = None
2942 self._portoffset = 0
2942 self._portoffset = 0
2943 self._ports = {}
2943 self._ports = {}
2944
2944
2945 def run(self, args, parser=None):
2945 def run(self, args, parser=None):
2946 """Run the test suite."""
2946 """Run the test suite."""
2947 oldmask = os.umask(0o22)
2947 oldmask = os.umask(0o22)
2948 try:
2948 try:
2949 parser = parser or getparser()
2949 parser = parser or getparser()
2950 options = parseargs(args, parser)
2950 options = parseargs(args, parser)
2951 tests = [_sys2bytes(a) for a in options.tests]
2951 tests = [_sys2bytes(a) for a in options.tests]
2952 if options.test_list is not None:
2952 if options.test_list is not None:
2953 for listfile in options.test_list:
2953 for listfile in options.test_list:
2954 with open(listfile, 'rb') as f:
2954 with open(listfile, 'rb') as f:
2955 tests.extend(t for t in f.read().splitlines() if t)
2955 tests.extend(t for t in f.read().splitlines() if t)
2956 self.options = options
2956 self.options = options
2957
2957
2958 self._checktools()
2958 self._checktools()
2959 testdescs = self.findtests(tests)
2959 testdescs = self.findtests(tests)
2960 if options.profile_runner:
2960 if options.profile_runner:
2961 import statprof
2961 import statprof
2962
2962
2963 statprof.start()
2963 statprof.start()
2964 result = self._run(testdescs)
2964 result = self._run(testdescs)
2965 if options.profile_runner:
2965 if options.profile_runner:
2966 statprof.stop()
2966 statprof.stop()
2967 statprof.display()
2967 statprof.display()
2968 return result
2968 return result
2969
2969
2970 finally:
2970 finally:
2971 os.umask(oldmask)
2971 os.umask(oldmask)
2972
2972
2973 def _run(self, testdescs):
2973 def _run(self, testdescs):
2974 testdir = getcwdb()
2974 testdir = getcwdb()
2975 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2975 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2976 # assume all tests in same folder for now
2976 # assume all tests in same folder for now
2977 if testdescs:
2977 if testdescs:
2978 pathname = os.path.dirname(testdescs[0]['path'])
2978 pathname = os.path.dirname(testdescs[0]['path'])
2979 if pathname:
2979 if pathname:
2980 testdir = os.path.join(testdir, pathname)
2980 testdir = os.path.join(testdir, pathname)
2981 self._testdir = osenvironb[b'TESTDIR'] = testdir
2981 self._testdir = osenvironb[b'TESTDIR'] = testdir
2982 if self.options.outputdir:
2982 if self.options.outputdir:
2983 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
2983 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
2984 else:
2984 else:
2985 self._outputdir = getcwdb()
2985 self._outputdir = getcwdb()
2986 if testdescs and pathname:
2986 if testdescs and pathname:
2987 self._outputdir = os.path.join(self._outputdir, pathname)
2987 self._outputdir = os.path.join(self._outputdir, pathname)
2988 previoustimes = {}
2988 previoustimes = {}
2989 if self.options.order_by_runtime:
2989 if self.options.order_by_runtime:
2990 previoustimes = dict(loadtimes(self._outputdir))
2990 previoustimes = dict(loadtimes(self._outputdir))
2991 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2991 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2992
2992
2993 if 'PYTHONHASHSEED' not in os.environ:
2993 if 'PYTHONHASHSEED' not in os.environ:
2994 # use a random python hash seed all the time
2994 # use a random python hash seed all the time
2995 # we do the randomness ourself to know what seed is used
2995 # we do the randomness ourself to know what seed is used
2996 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2996 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2997
2997
2998 # Rayon (Rust crate for multi-threading) will use all logical CPU cores
2998 # Rayon (Rust crate for multi-threading) will use all logical CPU cores
2999 # by default, causing thrashing on high-cpu-count systems.
2999 # by default, causing thrashing on high-cpu-count systems.
3000 # Setting its limit to 3 during tests should still let us uncover
3000 # Setting its limit to 3 during tests should still let us uncover
3001 # multi-threading bugs while keeping the thrashing reasonable.
3001 # multi-threading bugs while keeping the thrashing reasonable.
3002 os.environ.setdefault("RAYON_NUM_THREADS", "3")
3002 os.environ.setdefault("RAYON_NUM_THREADS", "3")
3003
3003
3004 if self.options.tmpdir:
3004 if self.options.tmpdir:
3005 self.options.keep_tmpdir = True
3005 self.options.keep_tmpdir = True
3006 tmpdir = _sys2bytes(self.options.tmpdir)
3006 tmpdir = _sys2bytes(self.options.tmpdir)
3007 if os.path.exists(tmpdir):
3007 if os.path.exists(tmpdir):
3008 # Meaning of tmpdir has changed since 1.3: we used to create
3008 # Meaning of tmpdir has changed since 1.3: we used to create
3009 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
3009 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
3010 # tmpdir already exists.
3010 # tmpdir already exists.
3011 print("error: temp dir %r already exists" % tmpdir)
3011 print("error: temp dir %r already exists" % tmpdir)
3012 return 1
3012 return 1
3013
3013
3014 os.makedirs(tmpdir)
3014 os.makedirs(tmpdir)
3015 else:
3015 else:
3016 d = None
3016 d = None
3017 if os.name == 'nt':
3017 if os.name == 'nt':
3018 # without this, we get the default temp dir location, but
3018 # without this, we get the default temp dir location, but
3019 # in all lowercase, which causes troubles with paths (issue3490)
3019 # in all lowercase, which causes troubles with paths (issue3490)
3020 d = osenvironb.get(b'TMP', None)
3020 d = osenvironb.get(b'TMP', None)
3021 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
3021 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
3022
3022
3023 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3023 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3024
3024
3025 if self.options.with_hg:
3025 if self.options.with_hg:
3026 self._installdir = None
3026 self._installdir = None
3027 whg = self.options.with_hg
3027 whg = self.options.with_hg
3028 self._bindir = os.path.dirname(os.path.realpath(whg))
3028 self._bindir = os.path.dirname(os.path.realpath(whg))
3029 assert isinstance(self._bindir, bytes)
3029 assert isinstance(self._bindir, bytes)
3030 self._hgcommand = os.path.basename(whg)
3030 self._hgcommand = os.path.basename(whg)
3031 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
3031 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
3032 os.makedirs(self._tmpbindir)
3032 os.makedirs(self._tmpbindir)
3033
3033
3034 normbin = os.path.normpath(os.path.abspath(whg))
3034 normbin = os.path.normpath(os.path.abspath(whg))
3035 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3035 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3036
3036
3037 # Other Python scripts in the test harness need to
3037 # Other Python scripts in the test harness need to
3038 # `import mercurial`. If `hg` is a Python script, we assume
3038 # `import mercurial`. If `hg` is a Python script, we assume
3039 # the Mercurial modules are relative to its path and tell the tests
3039 # the Mercurial modules are relative to its path and tell the tests
3040 # to load Python modules from its directory.
3040 # to load Python modules from its directory.
3041 with open(whg, 'rb') as fh:
3041 with open(whg, 'rb') as fh:
3042 initial = fh.read(1024)
3042 initial = fh.read(1024)
3043
3043
3044 if re.match(b'#!.*python', initial):
3044 if re.match(b'#!.*python', initial):
3045 self._pythondir = self._bindir
3045 self._pythondir = self._bindir
3046 # If it looks like our in-repo Rust binary, use the source root.
3046 # If it looks like our in-repo Rust binary, use the source root.
3047 # This is a bit hacky. But rhg is still not supported outside the
3047 # This is a bit hacky. But rhg is still not supported outside the
3048 # source directory. So until it is, do the simple thing.
3048 # source directory. So until it is, do the simple thing.
3049 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3049 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3050 self._pythondir = os.path.dirname(self._testdir)
3050 self._pythondir = os.path.dirname(self._testdir)
3051 # Fall back to the legacy behavior.
3051 # Fall back to the legacy behavior.
3052 else:
3052 else:
3053 self._pythondir = self._bindir
3053 self._pythondir = self._bindir
3054
3054
3055 else:
3055 else:
3056 self._installdir = os.path.join(self._hgtmp, b"install")
3056 self._installdir = os.path.join(self._hgtmp, b"install")
3057 self._bindir = os.path.join(self._installdir, b"bin")
3057 self._bindir = os.path.join(self._installdir, b"bin")
3058 self._hgcommand = b'hg'
3058 self._hgcommand = b'hg'
3059 self._tmpbindir = self._bindir
3059 self._tmpbindir = self._bindir
3060 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3060 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3061
3061
3062 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3062 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3063 # a python script and feed it to python.exe. Legacy stdio is force
3063 # a python script and feed it to python.exe. Legacy stdio is force
3064 # enabled by hg.exe, and this is a more realistic way to launch hg
3064 # enabled by hg.exe, and this is a more realistic way to launch hg
3065 # anyway.
3065 # anyway.
3066 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
3066 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
3067 self._hgcommand += b'.exe'
3067 self._hgcommand += b'.exe'
3068
3068
3069 # set CHGHG, then replace "hg" command by "chg"
3069 # set CHGHG, then replace "hg" command by "chg"
3070 chgbindir = self._bindir
3070 chgbindir = self._bindir
3071 if self.options.chg or self.options.with_chg:
3071 if self.options.chg or self.options.with_chg:
3072 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
3072 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
3073 else:
3073 else:
3074 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
3074 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
3075 if self.options.chg:
3075 if self.options.chg:
3076 self._hgcommand = b'chg'
3076 self._hgcommand = b'chg'
3077 elif self.options.with_chg:
3077 elif self.options.with_chg:
3078 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3078 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3079 self._hgcommand = os.path.basename(self.options.with_chg)
3079 self._hgcommand = os.path.basename(self.options.with_chg)
3080
3080
3081 osenvironb[b"BINDIR"] = self._bindir
3081 osenvironb[b"BINDIR"] = self._bindir
3082 osenvironb[b"PYTHON"] = PYTHON
3082 osenvironb[b"PYTHON"] = PYTHON
3083
3083
3084 fileb = _sys2bytes(__file__)
3084 fileb = _sys2bytes(__file__)
3085 runtestdir = os.path.abspath(os.path.dirname(fileb))
3085 runtestdir = os.path.abspath(os.path.dirname(fileb))
3086 osenvironb[b'RUNTESTDIR'] = runtestdir
3086 osenvironb[b'RUNTESTDIR'] = runtestdir
3087 if PYTHON3:
3087 if PYTHON3:
3088 sepb = _sys2bytes(os.pathsep)
3088 sepb = _sys2bytes(os.pathsep)
3089 else:
3089 else:
3090 sepb = os.pathsep
3090 sepb = os.pathsep
3091 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3091 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3092 if os.path.islink(__file__):
3092 if os.path.islink(__file__):
3093 # test helper will likely be at the end of the symlink
3093 # test helper will likely be at the end of the symlink
3094 realfile = os.path.realpath(fileb)
3094 realfile = os.path.realpath(fileb)
3095 realdir = os.path.abspath(os.path.dirname(realfile))
3095 realdir = os.path.abspath(os.path.dirname(realfile))
3096 path.insert(2, realdir)
3096 path.insert(2, realdir)
3097 if chgbindir != self._bindir:
3097 if chgbindir != self._bindir:
3098 path.insert(1, chgbindir)
3098 path.insert(1, chgbindir)
3099 if self._testdir != runtestdir:
3099 if self._testdir != runtestdir:
3100 path = [self._testdir] + path
3100 path = [self._testdir] + path
3101 if self._tmpbindir != self._bindir:
3101 if self._tmpbindir != self._bindir:
3102 path = [self._tmpbindir] + path
3102 path = [self._tmpbindir] + path
3103 osenvironb[b"PATH"] = sepb.join(path)
3103 osenvironb[b"PATH"] = sepb.join(path)
3104
3104
3105 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3105 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3106 # can run .../tests/run-tests.py test-foo where test-foo
3106 # can run .../tests/run-tests.py test-foo where test-foo
3107 # adds an extension to HGRC. Also include run-test.py directory to
3107 # adds an extension to HGRC. Also include run-test.py directory to
3108 # import modules like heredoctest.
3108 # import modules like heredoctest.
3109 pypath = [self._pythondir, self._testdir, runtestdir]
3109 pypath = [self._pythondir, self._testdir, runtestdir]
3110 # We have to augment PYTHONPATH, rather than simply replacing
3110 # We have to augment PYTHONPATH, rather than simply replacing
3111 # it, in case external libraries are only available via current
3111 # it, in case external libraries are only available via current
3112 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3112 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3113 # are in /opt/subversion.)
3113 # are in /opt/subversion.)
3114 oldpypath = osenvironb.get(IMPL_PATH)
3114 oldpypath = osenvironb.get(IMPL_PATH)
3115 if oldpypath:
3115 if oldpypath:
3116 pypath.append(oldpypath)
3116 pypath.append(oldpypath)
3117 osenvironb[IMPL_PATH] = sepb.join(pypath)
3117 osenvironb[IMPL_PATH] = sepb.join(pypath)
3118
3118
3119 if self.options.pure:
3119 if self.options.pure:
3120 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3120 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3121 os.environ["HGMODULEPOLICY"] = "py"
3121 os.environ["HGMODULEPOLICY"] = "py"
3122 if self.options.rust:
3122 if self.options.rust:
3123 os.environ["HGMODULEPOLICY"] = "rust+c"
3123 os.environ["HGMODULEPOLICY"] = "rust+c"
3124 if self.options.no_rust:
3124 if self.options.no_rust:
3125 current_policy = os.environ.get("HGMODULEPOLICY", "")
3125 current_policy = os.environ.get("HGMODULEPOLICY", "")
3126 if current_policy.startswith("rust+"):
3126 if current_policy.startswith("rust+"):
3127 os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :]
3127 os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :]
3128 os.environ.pop("HGWITHRUSTEXT", None)
3128 os.environ.pop("HGWITHRUSTEXT", None)
3129
3129
3130 if self.options.allow_slow_tests:
3130 if self.options.allow_slow_tests:
3131 os.environ["HGTEST_SLOW"] = "slow"
3131 os.environ["HGTEST_SLOW"] = "slow"
3132 elif 'HGTEST_SLOW' in os.environ:
3132 elif 'HGTEST_SLOW' in os.environ:
3133 del os.environ['HGTEST_SLOW']
3133 del os.environ['HGTEST_SLOW']
3134
3134
3135 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3135 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3136
3136
3137 if self.options.exceptions:
3137 if self.options.exceptions:
3138 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3138 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3139 try:
3139 try:
3140 os.makedirs(exceptionsdir)
3140 os.makedirs(exceptionsdir)
3141 except OSError as e:
3141 except OSError as e:
3142 if e.errno != errno.EEXIST:
3142 if e.errno != errno.EEXIST:
3143 raise
3143 raise
3144
3144
3145 # Remove all existing exception reports.
3145 # Remove all existing exception reports.
3146 for f in os.listdir(exceptionsdir):
3146 for f in os.listdir(exceptionsdir):
3147 os.unlink(os.path.join(exceptionsdir, f))
3147 os.unlink(os.path.join(exceptionsdir, f))
3148
3148
3149 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3149 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3150 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3150 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3151 self.options.extra_config_opt.append(
3151 self.options.extra_config_opt.append(
3152 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3152 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3153 )
3153 )
3154
3154
3155 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3155 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3156 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3156 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3157 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3157 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3158 vlog("# Using PATH", os.environ["PATH"])
3158 vlog("# Using PATH", os.environ["PATH"])
3159 vlog(
3159 vlog(
3160 "# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]),
3160 "# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]),
3161 )
3161 )
3162 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3162 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3163
3163
3164 try:
3164 try:
3165 return self._runtests(testdescs) or 0
3165 return self._runtests(testdescs) or 0
3166 finally:
3166 finally:
3167 time.sleep(0.1)
3167 time.sleep(0.1)
3168 self._cleanup()
3168 self._cleanup()
3169
3169
3170 def findtests(self, args):
3170 def findtests(self, args):
3171 """Finds possible test files from arguments.
3171 """Finds possible test files from arguments.
3172
3172
3173 If you wish to inject custom tests into the test harness, this would
3173 If you wish to inject custom tests into the test harness, this would
3174 be a good function to monkeypatch or override in a derived class.
3174 be a good function to monkeypatch or override in a derived class.
3175 """
3175 """
3176 if not args:
3176 if not args:
3177 if self.options.changed:
3177 if self.options.changed:
3178 proc = Popen4(
3178 proc = Popen4(
3179 b'hg st --rev "%s" -man0 .'
3179 b'hg st --rev "%s" -man0 .'
3180 % _sys2bytes(self.options.changed),
3180 % _sys2bytes(self.options.changed),
3181 None,
3181 None,
3182 0,
3182 0,
3183 )
3183 )
3184 stdout, stderr = proc.communicate()
3184 stdout, stderr = proc.communicate()
3185 args = stdout.strip(b'\0').split(b'\0')
3185 args = stdout.strip(b'\0').split(b'\0')
3186 else:
3186 else:
3187 args = os.listdir(b'.')
3187 args = os.listdir(b'.')
3188
3188
3189 expanded_args = []
3189 expanded_args = []
3190 for arg in args:
3190 for arg in args:
3191 if os.path.isdir(arg):
3191 if os.path.isdir(arg):
3192 if not arg.endswith(b'/'):
3192 if not arg.endswith(b'/'):
3193 arg += b'/'
3193 arg += b'/'
3194 expanded_args.extend([arg + a for a in os.listdir(arg)])
3194 expanded_args.extend([arg + a for a in os.listdir(arg)])
3195 else:
3195 else:
3196 expanded_args.append(arg)
3196 expanded_args.append(arg)
3197 args = expanded_args
3197 args = expanded_args
3198
3198
3199 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3199 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3200 tests = []
3200 tests = []
3201 for t in args:
3201 for t in args:
3202 case = []
3202 case = []
3203
3203
3204 if not (
3204 if not (
3205 os.path.basename(t).startswith(b'test-')
3205 os.path.basename(t).startswith(b'test-')
3206 and (t.endswith(b'.py') or t.endswith(b'.t'))
3206 and (t.endswith(b'.py') or t.endswith(b'.t'))
3207 ):
3207 ):
3208
3208
3209 m = testcasepattern.match(os.path.basename(t))
3209 m = testcasepattern.match(os.path.basename(t))
3210 if m is not None:
3210 if m is not None:
3211 t_basename, casestr = m.groups()
3211 t_basename, casestr = m.groups()
3212 t = os.path.join(os.path.dirname(t), t_basename)
3212 t = os.path.join(os.path.dirname(t), t_basename)
3213 if casestr:
3213 if casestr:
3214 case = casestr.split(b'#')
3214 case = casestr.split(b'#')
3215 else:
3215 else:
3216 continue
3216 continue
3217
3217
3218 if t.endswith(b'.t'):
3218 if t.endswith(b'.t'):
3219 # .t file may contain multiple test cases
3219 # .t file may contain multiple test cases
3220 casedimensions = parsettestcases(t)
3220 casedimensions = parsettestcases(t)
3221 if casedimensions:
3221 if casedimensions:
3222 cases = []
3222 cases = []
3223
3223
3224 def addcases(case, casedimensions):
3224 def addcases(case, casedimensions):
3225 if not casedimensions:
3225 if not casedimensions:
3226 cases.append(case)
3226 cases.append(case)
3227 else:
3227 else:
3228 for c in casedimensions[0]:
3228 for c in casedimensions[0]:
3229 addcases(case + [c], casedimensions[1:])
3229 addcases(case + [c], casedimensions[1:])
3230
3230
3231 addcases([], casedimensions)
3231 addcases([], casedimensions)
3232 if case and case in cases:
3232 if case and case in cases:
3233 cases = [case]
3233 cases = [case]
3234 elif case:
3234 elif case:
3235 # Ignore invalid cases
3235 # Ignore invalid cases
3236 cases = []
3236 cases = []
3237 else:
3237 else:
3238 pass
3238 pass
3239 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3239 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3240 else:
3240 else:
3241 tests.append({'path': t})
3241 tests.append({'path': t})
3242 else:
3242 else:
3243 tests.append({'path': t})
3243 tests.append({'path': t})
3244 return tests
3244 return tests
3245
3245
3246 def _runtests(self, testdescs):
3246 def _runtests(self, testdescs):
3247 def _reloadtest(test, i):
3247 def _reloadtest(test, i):
3248 # convert a test back to its description dict
3248 # convert a test back to its description dict
3249 desc = {'path': test.path}
3249 desc = {'path': test.path}
3250 case = getattr(test, '_case', [])
3250 case = getattr(test, '_case', [])
3251 if case:
3251 if case:
3252 desc['case'] = case
3252 desc['case'] = case
3253 return self._gettest(desc, i)
3253 return self._gettest(desc, i)
3254
3254
3255 try:
3255 try:
3256 if self.options.restart:
3256 if self.options.restart:
3257 orig = list(testdescs)
3257 orig = list(testdescs)
3258 while testdescs:
3258 while testdescs:
3259 desc = testdescs[0]
3259 desc = testdescs[0]
3260 # desc['path'] is a relative path
3260 # desc['path'] is a relative path
3261 if 'case' in desc:
3261 if 'case' in desc:
3262 casestr = b'#'.join(desc['case'])
3262 casestr = b'#'.join(desc['case'])
3263 errpath = b'%s#%s.err' % (desc['path'], casestr)
3263 errpath = b'%s#%s.err' % (desc['path'], casestr)
3264 else:
3264 else:
3265 errpath = b'%s.err' % desc['path']
3265 errpath = b'%s.err' % desc['path']
3266 errpath = os.path.join(self._outputdir, errpath)
3266 errpath = os.path.join(self._outputdir, errpath)
3267 if os.path.exists(errpath):
3267 if os.path.exists(errpath):
3268 break
3268 break
3269 testdescs.pop(0)
3269 testdescs.pop(0)
3270 if not testdescs:
3270 if not testdescs:
3271 print("running all tests")
3271 print("running all tests")
3272 testdescs = orig
3272 testdescs = orig
3273
3273
3274 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3274 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3275 num_tests = len(tests) * self.options.runs_per_test
3275 num_tests = len(tests) * self.options.runs_per_test
3276
3276
3277 jobs = min(num_tests, self.options.jobs)
3277 jobs = min(num_tests, self.options.jobs)
3278
3278
3279 failed = False
3279 failed = False
3280 kws = self.options.keywords
3280 kws = self.options.keywords
3281 if kws is not None and PYTHON3:
3281 if kws is not None and PYTHON3:
3282 kws = kws.encode('utf-8')
3282 kws = kws.encode('utf-8')
3283
3283
3284 suite = TestSuite(
3284 suite = TestSuite(
3285 self._testdir,
3285 self._testdir,
3286 jobs=jobs,
3286 jobs=jobs,
3287 whitelist=self.options.whitelisted,
3287 whitelist=self.options.whitelisted,
3288 blacklist=self.options.blacklist,
3288 blacklist=self.options.blacklist,
3289 retest=self.options.retest,
3289 retest=self.options.retest,
3290 keywords=kws,
3290 keywords=kws,
3291 loop=self.options.loop,
3291 loop=self.options.loop,
3292 runs_per_test=self.options.runs_per_test,
3292 runs_per_test=self.options.runs_per_test,
3293 showchannels=self.options.showchannels,
3293 showchannels=self.options.showchannels,
3294 tests=tests,
3294 tests=tests,
3295 loadtest=_reloadtest,
3295 loadtest=_reloadtest,
3296 )
3296 )
3297 verbosity = 1
3297 verbosity = 1
3298 if self.options.list_tests:
3298 if self.options.list_tests:
3299 verbosity = 0
3299 verbosity = 0
3300 elif self.options.verbose:
3300 elif self.options.verbose:
3301 verbosity = 2
3301 verbosity = 2
3302 runner = TextTestRunner(self, verbosity=verbosity)
3302 runner = TextTestRunner(self, verbosity=verbosity)
3303
3303
3304 if self.options.list_tests:
3304 if self.options.list_tests:
3305 result = runner.listtests(suite)
3305 result = runner.listtests(suite)
3306 else:
3306 else:
3307 if self._installdir:
3307 if self._installdir:
3308 self._installhg()
3308 self._installhg()
3309 self._checkhglib("Testing")
3309 self._checkhglib("Testing")
3310 else:
3310 else:
3311 self._usecorrectpython()
3311 self._usecorrectpython()
3312 if self.options.chg:
3312 if self.options.chg:
3313 assert self._installdir
3313 assert self._installdir
3314 self._installchg()
3314 self._installchg()
3315
3315
3316 log(
3316 log(
3317 'running %d tests using %d parallel processes'
3317 'running %d tests using %d parallel processes'
3318 % (num_tests, jobs)
3318 % (num_tests, jobs)
3319 )
3319 )
3320
3320
3321 result = runner.run(suite)
3321 result = runner.run(suite)
3322
3322
3323 if result.failures or result.errors:
3323 if result.failures or result.errors:
3324 failed = True
3324 failed = True
3325
3325
3326 result.onEnd()
3326 result.onEnd()
3327
3327
3328 if self.options.anycoverage:
3328 if self.options.anycoverage:
3329 self._outputcoverage()
3329 self._outputcoverage()
3330 except KeyboardInterrupt:
3330 except KeyboardInterrupt:
3331 failed = True
3331 failed = True
3332 print("\ninterrupted!")
3332 print("\ninterrupted!")
3333
3333
3334 if failed:
3334 if failed:
3335 return 1
3335 return 1
3336
3336
3337 def _getport(self, count):
3337 def _getport(self, count):
3338 port = self._ports.get(count) # do we have a cached entry?
3338 port = self._ports.get(count) # do we have a cached entry?
3339 if port is None:
3339 if port is None:
3340 portneeded = 3
3340 portneeded = 3
3341 # above 100 tries we just give up and let test reports failure
3341 # above 100 tries we just give up and let test reports failure
3342 for tries in xrange(100):
3342 for tries in xrange(100):
3343 allfree = True
3343 allfree = True
3344 port = self.options.port + self._portoffset
3344 port = self.options.port + self._portoffset
3345 for idx in xrange(portneeded):
3345 for idx in xrange(portneeded):
3346 if not checkportisavailable(port + idx):
3346 if not checkportisavailable(port + idx):
3347 allfree = False
3347 allfree = False
3348 break
3348 break
3349 self._portoffset += portneeded
3349 self._portoffset += portneeded
3350 if allfree:
3350 if allfree:
3351 break
3351 break
3352 self._ports[count] = port
3352 self._ports[count] = port
3353 return port
3353 return port
3354
3354
3355 def _gettest(self, testdesc, count):
3355 def _gettest(self, testdesc, count):
3356 """Obtain a Test by looking at its filename.
3356 """Obtain a Test by looking at its filename.
3357
3357
3358 Returns a Test instance. The Test may not be runnable if it doesn't
3358 Returns a Test instance. The Test may not be runnable if it doesn't
3359 map to a known type.
3359 map to a known type.
3360 """
3360 """
3361 path = testdesc['path']
3361 path = testdesc['path']
3362 lctest = path.lower()
3362 lctest = path.lower()
3363 testcls = Test
3363 testcls = Test
3364
3364
3365 for ext, cls in self.TESTTYPES:
3365 for ext, cls in self.TESTTYPES:
3366 if lctest.endswith(ext):
3366 if lctest.endswith(ext):
3367 testcls = cls
3367 testcls = cls
3368 break
3368 break
3369
3369
3370 refpath = os.path.join(getcwdb(), path)
3370 refpath = os.path.join(getcwdb(), path)
3371 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3371 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3372
3372
3373 # extra keyword parameters. 'case' is used by .t tests
3373 # extra keyword parameters. 'case' is used by .t tests
3374 kwds = {k: testdesc[k] for k in ['case'] if k in testdesc}
3374 kwds = {k: testdesc[k] for k in ['case'] if k in testdesc}
3375
3375
3376 t = testcls(
3376 t = testcls(
3377 refpath,
3377 refpath,
3378 self._outputdir,
3378 self._outputdir,
3379 tmpdir,
3379 tmpdir,
3380 keeptmpdir=self.options.keep_tmpdir,
3380 keeptmpdir=self.options.keep_tmpdir,
3381 debug=self.options.debug,
3381 debug=self.options.debug,
3382 first=self.options.first,
3382 first=self.options.first,
3383 timeout=self.options.timeout,
3383 timeout=self.options.timeout,
3384 startport=self._getport(count),
3384 startport=self._getport(count),
3385 extraconfigopts=self.options.extra_config_opt,
3385 extraconfigopts=self.options.extra_config_opt,
3386 shell=self.options.shell,
3386 shell=self.options.shell,
3387 hgcommand=self._hgcommand,
3387 hgcommand=self._hgcommand,
3388 usechg=bool(self.options.with_chg or self.options.chg),
3388 usechg=bool(self.options.with_chg or self.options.chg),
3389 useipv6=useipv6,
3389 useipv6=useipv6,
3390 **kwds
3390 **kwds
3391 )
3391 )
3392 t.should_reload = True
3392 t.should_reload = True
3393 return t
3393 return t
3394
3394
3395 def _cleanup(self):
3395 def _cleanup(self):
3396 """Clean up state from this test invocation."""
3396 """Clean up state from this test invocation."""
3397 if self.options.keep_tmpdir:
3397 if self.options.keep_tmpdir:
3398 return
3398 return
3399
3399
3400 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3400 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3401 shutil.rmtree(self._hgtmp, True)
3401 shutil.rmtree(self._hgtmp, True)
3402 for f in self._createdfiles:
3402 for f in self._createdfiles:
3403 try:
3403 try:
3404 os.remove(f)
3404 os.remove(f)
3405 except OSError:
3405 except OSError:
3406 pass
3406 pass
3407
3407
3408 def _usecorrectpython(self):
3408 def _usecorrectpython(self):
3409 """Configure the environment to use the appropriate Python in tests."""
3409 """Configure the environment to use the appropriate Python in tests."""
3410 # Tests must use the same interpreter as us or bad things will happen.
3410 # Tests must use the same interpreter as us or bad things will happen.
3411 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3411 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3412
3412
3413 # os.symlink() is a thing with py3 on Windows, but it requires
3413 # os.symlink() is a thing with py3 on Windows, but it requires
3414 # Administrator rights.
3414 # Administrator rights.
3415 if getattr(os, 'symlink', None) and os.name != 'nt':
3415 if getattr(os, 'symlink', None) and os.name != 'nt':
3416 vlog(
3416 vlog(
3417 "# Making python executable in test path a symlink to '%s'"
3417 "# Making python executable in test path a symlink to '%s'"
3418 % sysexecutable
3418 % sysexecutable
3419 )
3419 )
3420 mypython = os.path.join(self._tmpbindir, pyexename)
3420 mypython = os.path.join(self._tmpbindir, pyexename)
3421 try:
3421 try:
3422 if os.readlink(mypython) == sysexecutable:
3422 if os.readlink(mypython) == sysexecutable:
3423 return
3423 return
3424 os.unlink(mypython)
3424 os.unlink(mypython)
3425 except OSError as err:
3425 except OSError as err:
3426 if err.errno != errno.ENOENT:
3426 if err.errno != errno.ENOENT:
3427 raise
3427 raise
3428 if self._findprogram(pyexename) != sysexecutable:
3428 if self._findprogram(pyexename) != sysexecutable:
3429 try:
3429 try:
3430 os.symlink(sysexecutable, mypython)
3430 os.symlink(sysexecutable, mypython)
3431 self._createdfiles.append(mypython)
3431 self._createdfiles.append(mypython)
3432 except OSError as err:
3432 except OSError as err:
3433 # child processes may race, which is harmless
3433 # child processes may race, which is harmless
3434 if err.errno != errno.EEXIST:
3434 if err.errno != errno.EEXIST:
3435 raise
3435 raise
3436 else:
3436 else:
3437 exedir, exename = os.path.split(sysexecutable)
3437 exedir, exename = os.path.split(sysexecutable)
3438 vlog(
3438 vlog(
3439 "# Modifying search path to find %s as %s in '%s'"
3439 "# Modifying search path to find %s as %s in '%s'"
3440 % (exename, pyexename, exedir)
3440 % (exename, pyexename, exedir)
3441 )
3441 )
3442 path = os.environ['PATH'].split(os.pathsep)
3442 path = os.environ['PATH'].split(os.pathsep)
3443 while exedir in path:
3443 while exedir in path:
3444 path.remove(exedir)
3444 path.remove(exedir)
3445 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3445 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3446 if not self._findprogram(pyexename):
3446 if not self._findprogram(pyexename):
3447 print("WARNING: Cannot find %s in search path" % pyexename)
3447 print("WARNING: Cannot find %s in search path" % pyexename)
3448
3448
3449 def _installhg(self):
3449 def _installhg(self):
3450 """Install hg into the test environment.
3450 """Install hg into the test environment.
3451
3451
3452 This will also configure hg with the appropriate testing settings.
3452 This will also configure hg with the appropriate testing settings.
3453 """
3453 """
3454 vlog("# Performing temporary installation of HG")
3454 vlog("# Performing temporary installation of HG")
3455 installerrs = os.path.join(self._hgtmp, b"install.err")
3455 installerrs = os.path.join(self._hgtmp, b"install.err")
3456 compiler = ''
3456 compiler = ''
3457 if self.options.compiler:
3457 if self.options.compiler:
3458 compiler = '--compiler ' + self.options.compiler
3458 compiler = '--compiler ' + self.options.compiler
3459 setup_opts = b""
3459 setup_opts = b""
3460 if self.options.pure:
3460 if self.options.pure:
3461 setup_opts = b"--pure"
3461 setup_opts = b"--pure"
3462 elif self.options.rust:
3462 elif self.options.rust:
3463 setup_opts = b"--rust"
3463 setup_opts = b"--rust"
3464 elif self.options.no_rust:
3464 elif self.options.no_rust:
3465 setup_opts = b"--no-rust"
3465 setup_opts = b"--no-rust"
3466
3466
3467 # Run installer in hg root
3467 # Run installer in hg root
3468 script = os.path.realpath(sys.argv[0])
3468 script = os.path.realpath(sys.argv[0])
3469 exe = sysexecutable
3469 exe = sysexecutable
3470 if PYTHON3:
3470 if PYTHON3:
3471 compiler = _sys2bytes(compiler)
3471 compiler = _sys2bytes(compiler)
3472 script = _sys2bytes(script)
3472 script = _sys2bytes(script)
3473 exe = _sys2bytes(exe)
3473 exe = _sys2bytes(exe)
3474 hgroot = os.path.dirname(os.path.dirname(script))
3474 hgroot = os.path.dirname(os.path.dirname(script))
3475 self._hgroot = hgroot
3475 self._hgroot = hgroot
3476 os.chdir(hgroot)
3476 os.chdir(hgroot)
3477 nohome = b'--home=""'
3477 nohome = b'--home=""'
3478 if os.name == 'nt':
3478 if os.name == 'nt':
3479 # The --home="" trick works only on OS where os.sep == '/'
3479 # The --home="" trick works only on OS where os.sep == '/'
3480 # because of a distutils convert_path() fast-path. Avoid it at
3480 # because of a distutils convert_path() fast-path. Avoid it at
3481 # least on Windows for now, deal with .pydistutils.cfg bugs
3481 # least on Windows for now, deal with .pydistutils.cfg bugs
3482 # when they happen.
3482 # when they happen.
3483 nohome = b''
3483 nohome = b''
3484 cmd = (
3484 cmd = (
3485 b'"%(exe)s" setup.py %(setup_opts)s clean --all'
3485 b'"%(exe)s" setup.py %(setup_opts)s clean --all'
3486 b' build %(compiler)s --build-base="%(base)s"'
3486 b' build %(compiler)s --build-base="%(base)s"'
3487 b' install --force --prefix="%(prefix)s"'
3487 b' install --force --prefix="%(prefix)s"'
3488 b' --install-lib="%(libdir)s"'
3488 b' --install-lib="%(libdir)s"'
3489 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3489 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3490 % {
3490 % {
3491 b'exe': exe,
3491 b'exe': exe,
3492 b'setup_opts': setup_opts,
3492 b'setup_opts': setup_opts,
3493 b'compiler': compiler,
3493 b'compiler': compiler,
3494 b'base': os.path.join(self._hgtmp, b"build"),
3494 b'base': os.path.join(self._hgtmp, b"build"),
3495 b'prefix': self._installdir,
3495 b'prefix': self._installdir,
3496 b'libdir': self._pythondir,
3496 b'libdir': self._pythondir,
3497 b'bindir': self._bindir,
3497 b'bindir': self._bindir,
3498 b'nohome': nohome,
3498 b'nohome': nohome,
3499 b'logfile': installerrs,
3499 b'logfile': installerrs,
3500 }
3500 }
3501 )
3501 )
3502
3502
3503 # setuptools requires install directories to exist.
3503 # setuptools requires install directories to exist.
3504 def makedirs(p):
3504 def makedirs(p):
3505 try:
3505 try:
3506 os.makedirs(p)
3506 os.makedirs(p)
3507 except OSError as e:
3507 except OSError as e:
3508 if e.errno != errno.EEXIST:
3508 if e.errno != errno.EEXIST:
3509 raise
3509 raise
3510
3510
3511 makedirs(self._pythondir)
3511 makedirs(self._pythondir)
3512 makedirs(self._bindir)
3512 makedirs(self._bindir)
3513
3513
3514 vlog("# Running", cmd.decode("utf-8"))
3514 vlog("# Running", cmd.decode("utf-8"))
3515 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3515 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3516 if not self.options.verbose:
3516 if not self.options.verbose:
3517 try:
3517 try:
3518 os.remove(installerrs)
3518 os.remove(installerrs)
3519 except OSError as e:
3519 except OSError as e:
3520 if e.errno != errno.ENOENT:
3520 if e.errno != errno.ENOENT:
3521 raise
3521 raise
3522 else:
3522 else:
3523 with open(installerrs, 'rb') as f:
3523 with open(installerrs, 'rb') as f:
3524 for line in f:
3524 for line in f:
3525 if PYTHON3:
3525 if PYTHON3:
3526 sys.stdout.buffer.write(line)
3526 sys.stdout.buffer.write(line)
3527 else:
3527 else:
3528 sys.stdout.write(line)
3528 sys.stdout.write(line)
3529 sys.exit(1)
3529 sys.exit(1)
3530 os.chdir(self._testdir)
3530 os.chdir(self._testdir)
3531
3531
3532 self._usecorrectpython()
3532 self._usecorrectpython()
3533
3533
3534 hgbat = os.path.join(self._bindir, b'hg.bat')
3534 hgbat = os.path.join(self._bindir, b'hg.bat')
3535 if os.path.isfile(hgbat):
3535 if os.path.isfile(hgbat):
3536 # hg.bat expects to be put in bin/scripts while run-tests.py
3536 # hg.bat expects to be put in bin/scripts while run-tests.py
3537 # installation layout put it in bin/ directly. Fix it
3537 # installation layout put it in bin/ directly. Fix it
3538 with open(hgbat, 'rb') as f:
3538 with open(hgbat, 'rb') as f:
3539 data = f.read()
3539 data = f.read()
3540 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3540 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3541 data = data.replace(
3541 data = data.replace(
3542 br'"%~dp0..\python" "%~dp0hg" %*',
3542 br'"%~dp0..\python" "%~dp0hg" %*',
3543 b'"%~dp0python" "%~dp0hg" %*',
3543 b'"%~dp0python" "%~dp0hg" %*',
3544 )
3544 )
3545 with open(hgbat, 'wb') as f:
3545 with open(hgbat, 'wb') as f:
3546 f.write(data)
3546 f.write(data)
3547 else:
3547 else:
3548 print('WARNING: cannot fix hg.bat reference to python.exe')
3548 print('WARNING: cannot fix hg.bat reference to python.exe')
3549
3549
3550 if self.options.anycoverage:
3550 if self.options.anycoverage:
3551 custom = os.path.join(
3551 custom = os.path.join(
3552 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3552 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3553 )
3553 )
3554 target = os.path.join(self._pythondir, b'sitecustomize.py')
3554 target = os.path.join(self._pythondir, b'sitecustomize.py')
3555 vlog('# Installing coverage trigger to %s' % target)
3555 vlog('# Installing coverage trigger to %s' % target)
3556 shutil.copyfile(custom, target)
3556 shutil.copyfile(custom, target)
3557 rc = os.path.join(self._testdir, b'.coveragerc')
3557 rc = os.path.join(self._testdir, b'.coveragerc')
3558 vlog('# Installing coverage rc to %s' % rc)
3558 vlog('# Installing coverage rc to %s' % rc)
3559 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3559 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3560 covdir = os.path.join(self._installdir, b'..', b'coverage')
3560 covdir = os.path.join(self._installdir, b'..', b'coverage')
3561 try:
3561 try:
3562 os.mkdir(covdir)
3562 os.mkdir(covdir)
3563 except OSError as e:
3563 except OSError as e:
3564 if e.errno != errno.EEXIST:
3564 if e.errno != errno.EEXIST:
3565 raise
3565 raise
3566
3566
3567 osenvironb[b'COVERAGE_DIR'] = covdir
3567 osenvironb[b'COVERAGE_DIR'] = covdir
3568
3568
3569 def _checkhglib(self, verb):
3569 def _checkhglib(self, verb):
3570 """Ensure that the 'mercurial' package imported by python is
3570 """Ensure that the 'mercurial' package imported by python is
3571 the one we expect it to be. If not, print a warning to stderr."""
3571 the one we expect it to be. If not, print a warning to stderr."""
3572 if (self._bindir == self._pythondir) and (
3572 if (self._bindir == self._pythondir) and (
3573 self._bindir != self._tmpbindir
3573 self._bindir != self._tmpbindir
3574 ):
3574 ):
3575 # The pythondir has been inferred from --with-hg flag.
3575 # The pythondir has been inferred from --with-hg flag.
3576 # We cannot expect anything sensible here.
3576 # We cannot expect anything sensible here.
3577 return
3577 return
3578 expecthg = os.path.join(self._pythondir, b'mercurial')
3578 expecthg = os.path.join(self._pythondir, b'mercurial')
3579 actualhg = self._gethgpath()
3579 actualhg = self._gethgpath()
3580 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3580 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3581 sys.stderr.write(
3581 sys.stderr.write(
3582 'warning: %s with unexpected mercurial lib: %s\n'
3582 'warning: %s with unexpected mercurial lib: %s\n'
3583 ' (expected %s)\n' % (verb, actualhg, expecthg)
3583 ' (expected %s)\n' % (verb, actualhg, expecthg)
3584 )
3584 )
3585
3585
3586 def _gethgpath(self):
3586 def _gethgpath(self):
3587 """Return the path to the mercurial package that is actually found by
3587 """Return the path to the mercurial package that is actually found by
3588 the current Python interpreter."""
3588 the current Python interpreter."""
3589 if self._hgpath is not None:
3589 if self._hgpath is not None:
3590 return self._hgpath
3590 return self._hgpath
3591
3591
3592 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3592 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3593 cmd = cmd % PYTHON
3593 cmd = cmd % PYTHON
3594 if PYTHON3:
3594 if PYTHON3:
3595 cmd = _bytes2sys(cmd)
3595 cmd = _bytes2sys(cmd)
3596
3596
3597 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3597 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3598 out, err = p.communicate()
3598 out, err = p.communicate()
3599
3599
3600 self._hgpath = out.strip()
3600 self._hgpath = out.strip()
3601
3601
3602 return self._hgpath
3602 return self._hgpath
3603
3603
3604 def _installchg(self):
3604 def _installchg(self):
3605 """Install chg into the test environment"""
3605 """Install chg into the test environment"""
3606 vlog('# Performing temporary installation of CHG')
3606 vlog('# Performing temporary installation of CHG')
3607 assert os.path.dirname(self._bindir) == self._installdir
3607 assert os.path.dirname(self._bindir) == self._installdir
3608 assert self._hgroot, 'must be called after _installhg()'
3608 assert self._hgroot, 'must be called after _installhg()'
3609 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3609 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3610 b'make': b'make', # TODO: switch by option or environment?
3610 b'make': b'make', # TODO: switch by option or environment?
3611 b'prefix': self._installdir,
3611 b'prefix': self._installdir,
3612 }
3612 }
3613 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3613 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3614 vlog("# Running", cmd)
3614 vlog("# Running", cmd)
3615 proc = subprocess.Popen(
3615 proc = subprocess.Popen(
3616 cmd,
3616 cmd,
3617 shell=True,
3617 shell=True,
3618 cwd=cwd,
3618 cwd=cwd,
3619 stdin=subprocess.PIPE,
3619 stdin=subprocess.PIPE,
3620 stdout=subprocess.PIPE,
3620 stdout=subprocess.PIPE,
3621 stderr=subprocess.STDOUT,
3621 stderr=subprocess.STDOUT,
3622 )
3622 )
3623 out, _err = proc.communicate()
3623 out, _err = proc.communicate()
3624 if proc.returncode != 0:
3624 if proc.returncode != 0:
3625 if PYTHON3:
3625 if PYTHON3:
3626 sys.stdout.buffer.write(out)
3626 sys.stdout.buffer.write(out)
3627 else:
3627 else:
3628 sys.stdout.write(out)
3628 sys.stdout.write(out)
3629 sys.exit(1)
3629 sys.exit(1)
3630
3630
3631 def _outputcoverage(self):
3631 def _outputcoverage(self):
3632 """Produce code coverage output."""
3632 """Produce code coverage output."""
3633 import coverage
3633 import coverage
3634
3634
3635 coverage = coverage.coverage
3635 coverage = coverage.coverage
3636
3636
3637 vlog('# Producing coverage report')
3637 vlog('# Producing coverage report')
3638 # chdir is the easiest way to get short, relative paths in the
3638 # chdir is the easiest way to get short, relative paths in the
3639 # output.
3639 # output.
3640 os.chdir(self._hgroot)
3640 os.chdir(self._hgroot)
3641 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3641 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3642 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3642 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3643
3643
3644 # Map install directory paths back to source directory.
3644 # Map install directory paths back to source directory.
3645 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3645 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3646
3646
3647 cov.combine()
3647 cov.combine()
3648
3648
3649 omit = [
3649 omit = [
3650 _bytes2sys(os.path.join(x, b'*'))
3650 _bytes2sys(os.path.join(x, b'*'))
3651 for x in [self._bindir, self._testdir]
3651 for x in [self._bindir, self._testdir]
3652 ]
3652 ]
3653 cov.report(ignore_errors=True, omit=omit)
3653 cov.report(ignore_errors=True, omit=omit)
3654
3654
3655 if self.options.htmlcov:
3655 if self.options.htmlcov:
3656 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3656 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3657 cov.html_report(directory=htmldir, omit=omit)
3657 cov.html_report(directory=htmldir, omit=omit)
3658 if self.options.annotate:
3658 if self.options.annotate:
3659 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3659 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3660 if not os.path.isdir(adir):
3660 if not os.path.isdir(adir):
3661 os.mkdir(adir)
3661 os.mkdir(adir)
3662 cov.annotate(directory=adir, omit=omit)
3662 cov.annotate(directory=adir, omit=omit)
3663
3663
3664 def _findprogram(self, program):
3664 def _findprogram(self, program):
3665 """Search PATH for a executable program"""
3665 """Search PATH for a executable program"""
3666 dpb = _sys2bytes(os.defpath)
3666 dpb = _sys2bytes(os.defpath)
3667 sepb = _sys2bytes(os.pathsep)
3667 sepb = _sys2bytes(os.pathsep)
3668 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3668 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3669 name = os.path.join(p, program)
3669 name = os.path.join(p, program)
3670 if os.name == 'nt' or os.access(name, os.X_OK):
3670 if os.name == 'nt' or os.access(name, os.X_OK):
3671 return name
3671 return name
3672 return None
3672 return None
3673
3673
3674 def _checktools(self):
3674 def _checktools(self):
3675 """Ensure tools required to run tests are present."""
3675 """Ensure tools required to run tests are present."""
3676 for p in self.REQUIREDTOOLS:
3676 for p in self.REQUIREDTOOLS:
3677 if os.name == 'nt' and not p.endswith(b'.exe'):
3677 if os.name == 'nt' and not p.endswith(b'.exe'):
3678 p += b'.exe'
3678 p += b'.exe'
3679 found = self._findprogram(p)
3679 found = self._findprogram(p)
3680 p = p.decode("utf-8")
3680 p = p.decode("utf-8")
3681 if found:
3681 if found:
3682 vlog("# Found prerequisite", p, "at", _bytes2sys(found))
3682 vlog("# Found prerequisite", p, "at", _bytes2sys(found))
3683 else:
3683 else:
3684 print("WARNING: Did not find prerequisite tool: %s " % p)
3684 print("WARNING: Did not find prerequisite tool: %s " % p)
3685
3685
3686
3686
3687 def aggregateexceptions(path):
3687 def aggregateexceptions(path):
3688 exceptioncounts = collections.Counter()
3688 exceptioncounts = collections.Counter()
3689 testsbyfailure = collections.defaultdict(set)
3689 testsbyfailure = collections.defaultdict(set)
3690 failuresbytest = collections.defaultdict(set)
3690 failuresbytest = collections.defaultdict(set)
3691
3691
3692 for f in os.listdir(path):
3692 for f in os.listdir(path):
3693 with open(os.path.join(path, f), 'rb') as fh:
3693 with open(os.path.join(path, f), 'rb') as fh:
3694 data = fh.read().split(b'\0')
3694 data = fh.read().split(b'\0')
3695 if len(data) != 5:
3695 if len(data) != 5:
3696 continue
3696 continue
3697
3697
3698 exc, mainframe, hgframe, hgline, testname = data
3698 exc, mainframe, hgframe, hgline, testname = data
3699 exc = exc.decode('utf-8')
3699 exc = exc.decode('utf-8')
3700 mainframe = mainframe.decode('utf-8')
3700 mainframe = mainframe.decode('utf-8')
3701 hgframe = hgframe.decode('utf-8')
3701 hgframe = hgframe.decode('utf-8')
3702 hgline = hgline.decode('utf-8')
3702 hgline = hgline.decode('utf-8')
3703 testname = testname.decode('utf-8')
3703 testname = testname.decode('utf-8')
3704
3704
3705 key = (hgframe, hgline, exc)
3705 key = (hgframe, hgline, exc)
3706 exceptioncounts[key] += 1
3706 exceptioncounts[key] += 1
3707 testsbyfailure[key].add(testname)
3707 testsbyfailure[key].add(testname)
3708 failuresbytest[testname].add(key)
3708 failuresbytest[testname].add(key)
3709
3709
3710 # Find test having fewest failures for each failure.
3710 # Find test having fewest failures for each failure.
3711 leastfailing = {}
3711 leastfailing = {}
3712 for key, tests in testsbyfailure.items():
3712 for key, tests in testsbyfailure.items():
3713 fewesttest = None
3713 fewesttest = None
3714 fewestcount = 99999999
3714 fewestcount = 99999999
3715 for test in sorted(tests):
3715 for test in sorted(tests):
3716 if len(failuresbytest[test]) < fewestcount:
3716 if len(failuresbytest[test]) < fewestcount:
3717 fewesttest = test
3717 fewesttest = test
3718 fewestcount = len(failuresbytest[test])
3718 fewestcount = len(failuresbytest[test])
3719
3719
3720 leastfailing[key] = (fewestcount, fewesttest)
3720 leastfailing[key] = (fewestcount, fewesttest)
3721
3721
3722 # Create a combined counter so we can sort by total occurrences and
3722 # Create a combined counter so we can sort by total occurrences and
3723 # impacted tests.
3723 # impacted tests.
3724 combined = {}
3724 combined = {}
3725 for key in exceptioncounts:
3725 for key in exceptioncounts:
3726 combined[key] = (
3726 combined[key] = (
3727 exceptioncounts[key],
3727 exceptioncounts[key],
3728 len(testsbyfailure[key]),
3728 len(testsbyfailure[key]),
3729 leastfailing[key][0],
3729 leastfailing[key][0],
3730 leastfailing[key][1],
3730 leastfailing[key][1],
3731 )
3731 )
3732
3732
3733 return {
3733 return {
3734 'exceptioncounts': exceptioncounts,
3734 'exceptioncounts': exceptioncounts,
3735 'total': sum(exceptioncounts.values()),
3735 'total': sum(exceptioncounts.values()),
3736 'combined': combined,
3736 'combined': combined,
3737 'leastfailing': leastfailing,
3737 'leastfailing': leastfailing,
3738 'byfailure': testsbyfailure,
3738 'byfailure': testsbyfailure,
3739 'bytest': failuresbytest,
3739 'bytest': failuresbytest,
3740 }
3740 }
3741
3741
3742
3742
3743 if __name__ == '__main__':
3743 if __name__ == '__main__':
3744 runner = TestRunner()
3744 runner = TestRunner()
3745
3745
3746 try:
3746 try:
3747 import msvcrt
3747 import msvcrt
3748
3748
3749 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3749 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3750 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3750 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3751 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3751 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3752 except ImportError:
3752 except ImportError:
3753 pass
3753 pass
3754
3754
3755 sys.exit(runner.run(sys.argv[1:]))
3755 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now