##// 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 1 #!/usr/bin/env python3
2 2 #
3 3 # byteify-strings.py - transform string literals to be Python 3 safe
4 4 #
5 5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import, print_function
11 11
12 12 import argparse
13 13 import contextlib
14 14 import errno
15 15 import os
16 16 import sys
17 17 import tempfile
18 18 import token
19 19 import tokenize
20 20
21 21
22 22 def adjusttokenpos(t, ofs):
23 23 """Adjust start/end column of the given token"""
24 24 return t._replace(
25 25 start=(t.start[0], t.start[1] + ofs), end=(t.end[0], t.end[1] + ofs)
26 26 )
27 27
28 28
29 29 def replacetokens(tokens, opts):
30 30 """Transform a stream of tokens from raw to Python 3.
31 31
32 32 Returns a generator of possibly rewritten tokens.
33 33
34 34 The input token list may be mutated as part of processing. However,
35 35 its changes do not necessarily match the output token stream.
36 36 """
37 37 sysstrtokens = set()
38 38
39 39 # The following utility functions access the tokens list and i index of
40 40 # the for i, t enumerate(tokens) loop below
41 41 def _isop(j, *o):
42 42 """Assert that tokens[j] is an OP with one of the given values"""
43 43 try:
44 44 return tokens[j].type == token.OP and tokens[j].string in o
45 45 except IndexError:
46 46 return False
47 47
48 48 def _findargnofcall(n):
49 49 """Find arg n of a call expression (start at 0)
50 50
51 51 Returns index of the first token of that argument, or None if
52 52 there is not that many arguments.
53 53
54 54 Assumes that token[i + 1] is '('.
55 55
56 56 """
57 57 nested = 0
58 58 for j in range(i + 2, len(tokens)):
59 59 if _isop(j, ')', ']', '}'):
60 60 # end of call, tuple, subscription or dict / set
61 61 nested -= 1
62 62 if nested < 0:
63 63 return None
64 64 elif n == 0:
65 65 # this is the starting position of arg
66 66 return j
67 67 elif _isop(j, '(', '[', '{'):
68 68 nested += 1
69 69 elif _isop(j, ',') and nested == 0:
70 70 n -= 1
71 71
72 72 return None
73 73
74 74 def _ensuresysstr(j):
75 75 """Make sure the token at j is a system string
76 76
77 77 Remember the given token so the string transformer won't add
78 78 the byte prefix.
79 79
80 80 Ignores tokens that are not strings. Assumes bounds checking has
81 81 already been done.
82 82
83 83 """
84 84 k = j
85 85 currtoken = tokens[k]
86 86 while currtoken.type in (token.STRING, token.NEWLINE, tokenize.NL):
87 87 k += 1
88 88 if currtoken.type == token.STRING and currtoken.string.startswith(
89 89 ("'", '"')
90 90 ):
91 91 sysstrtokens.add(currtoken)
92 92 try:
93 93 currtoken = tokens[k]
94 94 except IndexError:
95 95 break
96 96
97 97 def _isitemaccess(j):
98 98 """Assert the next tokens form an item access on `tokens[j]` and that
99 99 `tokens[j]` is a name.
100 100 """
101 101 try:
102 102 return (
103 103 tokens[j].type == token.NAME
104 104 and _isop(j + 1, '[')
105 105 and tokens[j + 2].type == token.STRING
106 106 and _isop(j + 3, ']')
107 107 )
108 108 except IndexError:
109 109 return False
110 110
111 111 def _ismethodcall(j, *methodnames):
112 112 """Assert the next tokens form a call to `methodname` with a string
113 113 as first argument on `tokens[j]` and that `tokens[j]` is a name.
114 114 """
115 115 try:
116 116 return (
117 117 tokens[j].type == token.NAME
118 118 and _isop(j + 1, '.')
119 119 and tokens[j + 2].type == token.NAME
120 120 and tokens[j + 2].string in methodnames
121 121 and _isop(j + 3, '(')
122 122 and tokens[j + 4].type == token.STRING
123 123 )
124 124 except IndexError:
125 125 return False
126 126
127 127 coldelta = 0 # column increment for new opening parens
128 128 coloffset = -1 # column offset for the current line (-1: TBD)
129 129 parens = [(0, 0, 0, -1)] # stack of (line, end-column, column-offset, type)
130 130 ignorenextline = False # don't transform the next line
131 131 insideignoreblock = False # don't transform until turned off
132 132 for i, t in enumerate(tokens):
133 133 # Compute the column offset for the current line, such that
134 134 # the current line will be aligned to the last opening paren
135 135 # as before.
136 136 if coloffset < 0:
137 137 lastparen = parens[-1]
138 138 if t.start[1] == lastparen[1]:
139 139 coloffset = lastparen[2]
140 140 elif t.start[1] + 1 == lastparen[1] and lastparen[3] not in (
141 141 token.NEWLINE,
142 142 tokenize.NL,
143 143 ):
144 144 # fix misaligned indent of s/util.Abort/error.Abort/
145 145 coloffset = lastparen[2] + (lastparen[1] - t.start[1])
146 146 else:
147 147 coloffset = 0
148 148
149 149 # Reset per-line attributes at EOL.
150 150 if t.type in (token.NEWLINE, tokenize.NL):
151 151 yield adjusttokenpos(t, coloffset)
152 152 coldelta = 0
153 153 coloffset = -1
154 154 if not insideignoreblock:
155 155 ignorenextline = (
156 156 tokens[i - 1].type == token.COMMENT
157 157 and tokens[i - 1].string == "# no-py3-transform"
158 158 )
159 159 continue
160 160
161 161 if t.type == token.COMMENT:
162 162 if t.string == "# py3-transform: off":
163 163 insideignoreblock = True
164 164 if t.string == "# py3-transform: on":
165 165 insideignoreblock = False
166 166
167 167 if ignorenextline or insideignoreblock:
168 168 yield adjusttokenpos(t, coloffset)
169 169 continue
170 170
171 171 # Remember the last paren position.
172 172 if _isop(i, '(', '[', '{'):
173 173 parens.append(t.end + (coloffset + coldelta, tokens[i + 1].type))
174 174 elif _isop(i, ')', ']', '}'):
175 175 parens.pop()
176 176
177 177 # Convert most string literals to byte literals. String literals
178 178 # in Python 2 are bytes. String literals in Python 3 are unicode.
179 179 # Most strings in Mercurial are bytes and unicode strings are rare.
180 180 # Rather than rewrite all string literals to use ``b''`` to indicate
181 181 # byte strings, we apply this token transformer to insert the ``b``
182 182 # prefix nearly everywhere.
183 183 if t.type == token.STRING and t not in sysstrtokens:
184 184 s = t.string
185 185
186 186 # Preserve docstrings as string literals. This is inconsistent
187 187 # with regular unprefixed strings. However, the
188 188 # "from __future__" parsing (which allows a module docstring to
189 189 # exist before it) doesn't properly handle the docstring if it
190 190 # is b''' prefixed, leading to a SyntaxError. We leave all
191 191 # docstrings as unprefixed to avoid this. This means Mercurial
192 192 # components touching docstrings need to handle unicode,
193 193 # unfortunately.
194 194 if s[0:3] in ("'''", '"""'):
195 195 # If it's assigned to something, it's not a docstring
196 196 if not _isop(i - 1, '='):
197 197 yield adjusttokenpos(t, coloffset)
198 198 continue
199 199
200 200 # If the first character isn't a quote, it is likely a string
201 201 # prefixing character (such as 'b', 'u', or 'r'. Ignore.
202 202 if s[0] not in ("'", '"'):
203 203 yield adjusttokenpos(t, coloffset)
204 204 continue
205 205
206 206 # String literal. Prefix to make a b'' string.
207 207 yield adjusttokenpos(t._replace(string='b%s' % t.string), coloffset)
208 208 coldelta += 1
209 209 continue
210 210
211 211 # This looks like a function call.
212 212 if t.type == token.NAME and _isop(i + 1, '('):
213 213 fn = t.string
214 214
215 215 # *attr() builtins don't accept byte strings to 2nd argument.
216 216 if fn in (
217 217 'getattr',
218 218 'setattr',
219 219 'hasattr',
220 220 'safehasattr',
221 221 'wrapfunction',
222 222 'wrapclass',
223 223 'addattr',
224 224 ) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')):
225 225 arg1idx = _findargnofcall(1)
226 226 if arg1idx is not None:
227 227 _ensuresysstr(arg1idx)
228 228
229 229 # .encode() and .decode() on str/bytes/unicode don't accept
230 230 # byte strings on Python 3.
231 231 elif fn in ('encode', 'decode') and _isop(i - 1, '.'):
232 232 for argn in range(2):
233 233 argidx = _findargnofcall(argn)
234 234 if argidx is not None:
235 235 _ensuresysstr(argidx)
236 236
237 237 # It changes iteritems/values to items/values as they are not
238 238 # present in Python 3 world.
239 239 elif opts['dictiter'] and fn in ('iteritems', 'itervalues'):
240 240 yield adjusttokenpos(t._replace(string=fn[4:]), coloffset)
241 241 continue
242 242
243 243 if t.type == token.NAME and t.string in opts['treat-as-kwargs']:
244 244 if _isitemaccess(i):
245 245 _ensuresysstr(i + 2)
246 246 if _ismethodcall(i, 'get', 'pop', 'setdefault', 'popitem'):
247 247 _ensuresysstr(i + 4)
248 248
249 249 # Looks like "if __name__ == '__main__'".
250 250 if (
251 251 t.type == token.NAME
252 252 and t.string == '__name__'
253 253 and _isop(i + 1, '==')
254 254 ):
255 255 _ensuresysstr(i + 2)
256 256
257 257 # Emit unmodified token.
258 258 yield adjusttokenpos(t, coloffset)
259 259
260 260
261 261 def process(fin, fout, opts):
262 262 tokens = tokenize.tokenize(fin.readline)
263 263 tokens = replacetokens(list(tokens), opts)
264 264 fout.write(tokenize.untokenize(tokens))
265 265
266 266
267 267 def tryunlink(fname):
268 268 try:
269 269 os.unlink(fname)
270 270 except OSError as err:
271 271 if err.errno != errno.ENOENT:
272 272 raise
273 273
274 274
275 275 @contextlib.contextmanager
276 276 def editinplace(fname):
277 277 n = os.path.basename(fname)
278 278 d = os.path.dirname(fname)
279 279 fp = tempfile.NamedTemporaryFile(
280 280 prefix='.%s-' % n, suffix='~', dir=d, delete=False
281 281 )
282 282 try:
283 283 yield fp
284 284 fp.close()
285 285 if os.name == 'nt':
286 286 tryunlink(fname)
287 287 os.rename(fp.name, fname)
288 288 finally:
289 289 fp.close()
290 290 tryunlink(fp.name)
291 291
292 292
293 293 def main():
294 294 ap = argparse.ArgumentParser()
295 295 ap.add_argument(
296 296 '--version', action='version', version='Byteify strings 1.0'
297 297 )
298 298 ap.add_argument(
299 299 '-i',
300 300 '--inplace',
301 301 action='store_true',
302 302 default=False,
303 303 help='edit files in place',
304 304 )
305 305 ap.add_argument(
306 306 '--dictiter',
307 307 action='store_true',
308 308 default=False,
309 309 help='rewrite iteritems() and itervalues()',
310 310 ),
311 311 ap.add_argument(
312 312 '--allow-attr-methods',
313 313 action='store_true',
314 314 default=False,
315 315 help='also handle attr*() when they are methods',
316 316 ),
317 317 ap.add_argument(
318 318 '--treat-as-kwargs',
319 319 nargs="+",
320 320 default=[],
321 321 help="ignore kwargs-like objects",
322 322 ),
323 323 ap.add_argument('files', metavar='FILE', nargs='+', help='source file')
324 324 args = ap.parse_args()
325 325 opts = {
326 326 'dictiter': args.dictiter,
327 327 'treat-as-kwargs': set(args.treat_as_kwargs),
328 328 'allow-attr-methods': args.allow_attr_methods,
329 329 }
330 330 for fname in args.files:
331 fname = os.path.realpath(fname)
331 332 if args.inplace:
332 333 with editinplace(fname) as fout:
333 334 with open(fname, 'rb') as fin:
334 335 process(fin, fout, opts)
335 336 else:
336 337 with open(fname, 'rb') as fin:
337 338 fout = sys.stdout.buffer
338 339 process(fin, fout, opts)
339 340
340 341
341 342 if __name__ == '__main__':
342 343 if sys.version_info[0:2] < (3, 7):
343 344 print('This script must be run under Python 3.7+')
344 345 sys.exit(3)
345 346 main()
@@ -1,82 +1,81 b''
1 1 image: octobus/ci-mercurial-core
2 2
3 3 # The runner made a clone as root.
4 4 # We make a new clone owned by user used to run the step.
5 5 before_script:
6 6 - hg clone . /tmp/mercurial-ci/ --noupdate
7 7 - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
8 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 11 variables:
12 12 PYTHON: python
13 13 TEST_HGMODULEPOLICY: "allow"
14 14
15 15 .runtests_template: &runtests
16 16 script:
17 - cd tests/
18 17 - echo "python used, $PYTHON"
19 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 21 checks-py2:
23 22 <<: *runtests
24 23 variables:
25 24 RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
26 25
27 26 checks-py3:
28 27 <<: *runtests
29 28 variables:
30 29 RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
31 30 PYTHON: python3
32 31
33 32 rust-cargo-test-py2: &rust_cargo_test
34 33 script:
35 34 - echo "python used, $PYTHON"
36 35 - make rust-tests
37 36
38 37 rust-cargo-test-py3:
39 38 <<: *rust_cargo_test
40 39 variables:
41 40 PYTHON: python3
42 41
43 42 test-py2:
44 43 <<: *runtests
45 44 variables:
46 45 RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
47 46 TEST_HGMODULEPOLICY: "c"
48 47
49 48 test-py3:
50 49 <<: *runtests
51 50 variables:
52 51 RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
53 52 PYTHON: python3
54 53 TEST_HGMODULEPOLICY: "c"
55 54
56 55 test-py2-pure:
57 56 <<: *runtests
58 57 variables:
59 58 RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
60 59 TEST_HGMODULEPOLICY: "py"
61 60
62 61 test-py3-pure:
63 62 <<: *runtests
64 63 variables:
65 64 RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
66 65 PYTHON: python3
67 66 TEST_HGMODULEPOLICY: "py"
68 67
69 68 test-py2-rust:
70 69 <<: *runtests
71 70 variables:
72 71 HGWITHRUSTEXT: cpython
73 72 RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
74 73 TEST_HGMODULEPOLICY: "rust+c"
75 74
76 75 test-py3-rust:
77 76 <<: *runtests
78 77 variables:
79 78 HGWITHRUSTEXT: cpython
80 79 RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
81 80 PYTHON: python3
82 81 TEST_HGMODULEPOLICY: "rust+c"
@@ -1,386 +1,390 b''
1 1 # archival.py - revision archival for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import gzip
11 11 import os
12 12 import struct
13 13 import tarfile
14 14 import time
15 15 import zipfile
16 16 import zlib
17 17
18 18 from .i18n import _
19 19 from .node import nullrev
20 20 from .pycompat import open
21 21
22 22 from . import (
23 23 error,
24 24 formatter,
25 25 match as matchmod,
26 26 pycompat,
27 27 scmutil,
28 28 util,
29 29 vfs as vfsmod,
30 30 )
31 31
32 32 stringio = util.stringio
33 33
34 34 # from unzip source code:
35 35 _UNX_IFREG = 0x8000
36 36 _UNX_IFLNK = 0xA000
37 37
38 38
39 39 def tidyprefix(dest, kind, prefix):
40 40 '''choose prefix to use for names in archive. make sure prefix is
41 41 safe for consumers.'''
42 42
43 43 if prefix:
44 44 prefix = util.normpath(prefix)
45 45 else:
46 46 if not isinstance(dest, bytes):
47 47 raise ValueError(b'dest must be string if no prefix')
48 48 prefix = os.path.basename(dest)
49 49 lower = prefix.lower()
50 50 for sfx in exts.get(kind, []):
51 51 if lower.endswith(sfx):
52 52 prefix = prefix[: -len(sfx)]
53 53 break
54 54 lpfx = os.path.normpath(util.localpath(prefix))
55 55 prefix = util.pconvert(lpfx)
56 56 if not prefix.endswith(b'/'):
57 57 prefix += b'/'
58 58 # Drop the leading '.' path component if present, so Windows can read the
59 59 # zip files (issue4634)
60 60 if prefix.startswith(b'./'):
61 61 prefix = prefix[2:]
62 62 if prefix.startswith(b'../') or os.path.isabs(lpfx) or b'/../' in prefix:
63 63 raise error.Abort(_(b'archive prefix contains illegal components'))
64 64 return prefix
65 65
66 66
67 67 exts = {
68 68 b'tar': [b'.tar'],
69 69 b'tbz2': [b'.tbz2', b'.tar.bz2'],
70 70 b'tgz': [b'.tgz', b'.tar.gz'],
71 71 b'zip': [b'.zip'],
72 72 b'txz': [b'.txz', b'.tar.xz'],
73 73 }
74 74
75 75
76 76 def guesskind(dest):
77 77 for kind, extensions in pycompat.iteritems(exts):
78 78 if any(dest.endswith(ext) for ext in extensions):
79 79 return kind
80 80 return None
81 81
82 82
83 83 def _rootctx(repo):
84 84 # repo[0] may be hidden
85 85 for rev in repo:
86 86 return repo[rev]
87 87 return repo[nullrev]
88 88
89 89
90 90 # {tags} on ctx includes local tags and 'tip', with no current way to limit
91 91 # that to global tags. Therefore, use {latesttag} as a substitute when
92 92 # the distance is 0, since that will be the list of global tags on ctx.
93 93 _defaultmetatemplate = br'''
94 94 repo: {root}
95 95 node: {ifcontains(rev, revset("wdir()"), "{p1node}{dirty}", "{node}")}
96 96 branch: {branch|utf8}
97 97 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
98 98 separate("\n",
99 99 join(latesttag % "latesttag: {tag}", "\n"),
100 100 "latesttagdistance: {latesttagdistance}",
101 101 "changessincelatesttag: {changessincelatesttag}"))}
102 102 '''[
103 103 1:
104 104 ] # drop leading '\n'
105 105
106 106
107 107 def buildmetadata(ctx):
108 108 '''build content of .hg_archival.txt'''
109 109 repo = ctx.repo()
110 110
111 111 opts = {
112 112 b'template': repo.ui.config(
113 113 b'experimental', b'archivemetatemplate', _defaultmetatemplate
114 114 )
115 115 }
116 116
117 117 out = util.stringio()
118 118
119 119 fm = formatter.formatter(repo.ui, out, b'archive', opts)
120 120 fm.startitem()
121 121 fm.context(ctx=ctx)
122 122 fm.data(root=_rootctx(repo).hex())
123 123
124 124 if ctx.rev() is None:
125 125 dirty = b''
126 126 if ctx.dirty(missing=True):
127 127 dirty = b'+'
128 128 fm.data(dirty=dirty)
129 129 fm.end()
130 130
131 131 return out.getvalue()
132 132
133 133
134 134 class tarit(object):
135 135 '''write archive to tar file or stream. can write uncompressed,
136 136 or compress with gzip or bzip2.'''
137 137
138 class GzipFileWithTime(gzip.GzipFile):
139 def __init__(self, *args, **kw):
140 timestamp = None
141 if 'timestamp' in kw:
142 timestamp = kw.pop('timestamp')
143 if timestamp is None:
144 self.timestamp = time.time()
145 else:
146 self.timestamp = timestamp
147 gzip.GzipFile.__init__(self, *args, **kw)
138 if pycompat.ispy3:
139 GzipFileWithTime = gzip.GzipFile # camelcase-required
140 else:
141
142 class GzipFileWithTime(gzip.GzipFile):
143 def __init__(self, *args, **kw):
144 timestamp = None
145 if 'mtime' in kw:
146 timestamp = kw.pop('mtime')
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):
150 self.fileobj.write(b'\037\213') # magic header
151 self.fileobj.write(b'\010') # compression method
152 fname = self.name
153 if fname and fname.endswith(b'.gz'):
154 fname = fname[:-3]
155 flags = 0
156 if fname:
157 flags = gzip.FNAME # pytype: disable=module-attr
158 self.fileobj.write(pycompat.bytechr(flags))
159 gzip.write32u( # pytype: disable=module-attr
160 self.fileobj, int(self.timestamp)
161 )
162 self.fileobj.write(b'\002')
163 self.fileobj.write(b'\377')
164 if fname:
165 self.fileobj.write(fname + b'\000')
153 def _write_gzip_header(self):
154 self.fileobj.write(b'\037\213') # magic header
155 self.fileobj.write(b'\010') # compression method
156 fname = self.name
157 if fname and fname.endswith(b'.gz'):
158 fname = fname[:-3]
159 flags = 0
160 if fname:
161 flags = gzip.FNAME # pytype: disable=module-attr
162 self.fileobj.write(pycompat.bytechr(flags))
163 gzip.write32u( # pytype: disable=module-attr
164 self.fileobj, int(self.timestamp)
165 )
166 self.fileobj.write(b'\002')
167 self.fileobj.write(b'\377')
168 if fname:
169 self.fileobj.write(fname + b'\000')
166 170
167 171 def __init__(self, dest, mtime, kind=b''):
168 172 self.mtime = mtime
169 173 self.fileobj = None
170 174
171 175 def taropen(mode, name=b'', fileobj=None):
172 176 if kind == b'gz':
173 177 mode = mode[0:1]
174 178 if not fileobj:
175 179 fileobj = open(name, mode + b'b')
176 180 gzfileobj = self.GzipFileWithTime(
177 181 name,
178 182 pycompat.sysstr(mode + b'b'),
179 183 zlib.Z_BEST_COMPRESSION,
180 184 fileobj,
181 timestamp=mtime,
185 mtime=mtime,
182 186 )
183 187 self.fileobj = gzfileobj
184 188 return tarfile.TarFile.taropen( # pytype: disable=attribute-error
185 189 name, pycompat.sysstr(mode), gzfileobj
186 190 )
187 191 else:
188 192 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj)
189 193
190 194 if isinstance(dest, bytes):
191 195 self.z = taropen(b'w:', name=dest)
192 196 else:
193 197 self.z = taropen(b'w|', fileobj=dest)
194 198
195 199 def addfile(self, name, mode, islink, data):
196 200 name = pycompat.fsdecode(name)
197 201 i = tarfile.TarInfo(name)
198 202 i.mtime = self.mtime
199 203 i.size = len(data)
200 204 if islink:
201 205 i.type = tarfile.SYMTYPE
202 206 i.mode = 0o777
203 207 i.linkname = pycompat.fsdecode(data)
204 208 data = None
205 209 i.size = 0
206 210 else:
207 211 i.mode = mode
208 212 data = stringio(data)
209 213 self.z.addfile(i, data)
210 214
211 215 def done(self):
212 216 self.z.close()
213 217 if self.fileobj:
214 218 self.fileobj.close()
215 219
216 220
217 221 class zipit(object):
218 222 '''write archive to zip file or stream. can write uncompressed,
219 223 or compressed with deflate.'''
220 224
221 225 def __init__(self, dest, mtime, compress=True):
222 226 if isinstance(dest, bytes):
223 227 dest = pycompat.fsdecode(dest)
224 228 self.z = zipfile.ZipFile(
225 229 dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED
226 230 )
227 231
228 232 # Python's zipfile module emits deprecation warnings if we try
229 233 # to store files with a date before 1980.
230 234 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
231 235 if mtime < epoch:
232 236 mtime = epoch
233 237
234 238 self.mtime = mtime
235 239 self.date_time = time.gmtime(mtime)[:6]
236 240
237 241 def addfile(self, name, mode, islink, data):
238 242 i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time)
239 243 i.compress_type = self.z.compression # pytype: disable=attribute-error
240 244 # unzip will not honor unix file modes unless file creator is
241 245 # set to unix (id 3).
242 246 i.create_system = 3
243 247 ftype = _UNX_IFREG
244 248 if islink:
245 249 mode = 0o777
246 250 ftype = _UNX_IFLNK
247 251 i.external_attr = (mode | ftype) << 16
248 252 # add "extended-timestamp" extra block, because zip archives
249 253 # without this will be extracted with unexpected timestamp,
250 254 # if TZ is not configured as GMT
251 255 i.extra += struct.pack(
252 256 b'<hhBl',
253 257 0x5455, # block type: "extended-timestamp"
254 258 1 + 4, # size of this block
255 259 1, # "modification time is present"
256 260 int(self.mtime),
257 261 ) # last modification (UTC)
258 262 self.z.writestr(i, data)
259 263
260 264 def done(self):
261 265 self.z.close()
262 266
263 267
264 268 class fileit(object):
265 269 '''write archive as files in directory.'''
266 270
267 271 def __init__(self, name, mtime):
268 272 self.basedir = name
269 273 self.opener = vfsmod.vfs(self.basedir)
270 274 self.mtime = mtime
271 275
272 276 def addfile(self, name, mode, islink, data):
273 277 if islink:
274 278 self.opener.symlink(data, name)
275 279 return
276 280 f = self.opener(name, b"w", atomictemp=False)
277 281 f.write(data)
278 282 f.close()
279 283 destfile = os.path.join(self.basedir, name)
280 284 os.chmod(destfile, mode)
281 285 if self.mtime is not None:
282 286 os.utime(destfile, (self.mtime, self.mtime))
283 287
284 288 def done(self):
285 289 pass
286 290
287 291
288 292 archivers = {
289 293 b'files': fileit,
290 294 b'tar': tarit,
291 295 b'tbz2': lambda name, mtime: tarit(name, mtime, b'bz2'),
292 296 b'tgz': lambda name, mtime: tarit(name, mtime, b'gz'),
293 297 b'txz': lambda name, mtime: tarit(name, mtime, b'xz'),
294 298 b'uzip': lambda name, mtime: zipit(name, mtime, False),
295 299 b'zip': zipit,
296 300 }
297 301
298 302
299 303 def archive(
300 304 repo,
301 305 dest,
302 306 node,
303 307 kind,
304 308 decode=True,
305 309 match=None,
306 310 prefix=b'',
307 311 mtime=None,
308 312 subrepos=False,
309 313 ):
310 314 '''create archive of repo as it was at node.
311 315
312 316 dest can be name of directory, name of archive file, or file
313 317 object to write archive to.
314 318
315 319 kind is type of archive to create.
316 320
317 321 decode tells whether to put files through decode filters from
318 322 hgrc.
319 323
320 324 match is a matcher to filter names of files to write to archive.
321 325
322 326 prefix is name of path to put before every archive member.
323 327
324 328 mtime is the modified time, in seconds, or None to use the changeset time.
325 329
326 330 subrepos tells whether to include subrepos.
327 331 '''
328 332
329 333 if kind == b'txz' and not pycompat.ispy3:
330 334 raise error.Abort(_(b'xz compression is only available in Python 3'))
331 335
332 336 if kind == b'files':
333 337 if prefix:
334 338 raise error.Abort(_(b'cannot give prefix when archiving to files'))
335 339 else:
336 340 prefix = tidyprefix(dest, kind, prefix)
337 341
338 342 def write(name, mode, islink, getdata):
339 343 data = getdata()
340 344 if decode:
341 345 data = repo.wwritedata(name, data)
342 346 archiver.addfile(prefix + name, mode, islink, data)
343 347
344 348 if kind not in archivers:
345 349 raise error.Abort(_(b"unknown archive type '%s'") % kind)
346 350
347 351 ctx = repo[node]
348 352 archiver = archivers[kind](dest, mtime or ctx.date()[0])
349 353
350 354 if not match:
351 355 match = scmutil.matchall(repo)
352 356
353 357 if repo.ui.configbool(b"ui", b"archivemeta"):
354 358 name = b'.hg_archival.txt'
355 359 if match(name):
356 360 write(name, 0o644, False, lambda: buildmetadata(ctx))
357 361
358 362 files = list(ctx.manifest().walk(match))
359 363 total = len(files)
360 364 if total:
361 365 files.sort()
362 366 scmutil.prefetchfiles(
363 367 repo, [ctx.rev()], scmutil.matchfiles(repo, files)
364 368 )
365 369 progress = repo.ui.makeprogress(
366 370 _(b'archiving'), unit=_(b'files'), total=total
367 371 )
368 372 progress.update(0)
369 373 for f in files:
370 374 ff = ctx.flags(f)
371 375 write(f, b'x' in ff and 0o755 or 0o644, b'l' in ff, ctx[f].data)
372 376 progress.increment(item=f)
373 377 progress.complete()
374 378
375 379 if subrepos:
376 380 for subpath in sorted(ctx.substate):
377 381 sub = ctx.workingsub(subpath)
378 382 submatch = matchmod.subdirmatcher(subpath, match)
379 383 subprefix = prefix + subpath + b'/'
380 384 total += sub.archive(archiver, subprefix, submatch, decode)
381 385
382 386 if total == 0:
383 387 raise error.Abort(_(b'no files match the archive pattern'))
384 388
385 389 archiver.done()
386 390 return total
@@ -1,2914 +1,2917 b''
1 1 /*
2 2 parsers.c - efficient content parsing
3 3
4 4 Copyright 2008 Matt Mackall <mpm@selenic.com> and others
5 5
6 6 This software may be used and distributed according to the terms of
7 7 the GNU General Public License, incorporated herein by reference.
8 8 */
9 9
10 10 #define PY_SSIZE_T_CLEAN
11 11 #include <Python.h>
12 12 #include <assert.h>
13 13 #include <ctype.h>
14 14 #include <limits.h>
15 15 #include <stddef.h>
16 16 #include <stdlib.h>
17 17 #include <string.h>
18 18
19 19 #include "bitmanipulation.h"
20 20 #include "charencode.h"
21 21 #include "revlog.h"
22 22 #include "util.h"
23 23
24 24 #ifdef IS_PY3K
25 25 /* The mapping of Python types is meant to be temporary to get Python
26 26 * 3 to compile. We should remove this once Python 3 support is fully
27 27 * supported and proper types are used in the extensions themselves. */
28 28 #define PyInt_Check PyLong_Check
29 29 #define PyInt_FromLong PyLong_FromLong
30 30 #define PyInt_FromSsize_t PyLong_FromSsize_t
31 31 #define PyInt_AsLong PyLong_AsLong
32 32 #endif
33 33
34 34 typedef struct indexObjectStruct indexObject;
35 35
36 36 typedef struct {
37 37 int children[16];
38 38 } nodetreenode;
39 39
40 40 typedef struct {
41 41 int abi_version;
42 42 Py_ssize_t (*index_length)(const indexObject *);
43 43 const char *(*index_node)(indexObject *, Py_ssize_t);
44 44 int (*index_parents)(PyObject *, int, int *);
45 45 } Revlog_CAPI;
46 46
47 47 /*
48 48 * A base-16 trie for fast node->rev mapping.
49 49 *
50 50 * Positive value is index of the next node in the trie
51 51 * Negative value is a leaf: -(rev + 2)
52 52 * Zero is empty
53 53 */
54 54 typedef struct {
55 55 indexObject *index;
56 56 nodetreenode *nodes;
57 57 unsigned length; /* # nodes in use */
58 58 unsigned capacity; /* # nodes allocated */
59 59 int depth; /* maximum depth of tree */
60 60 int splits; /* # splits performed */
61 61 } nodetree;
62 62
63 63 typedef struct {
64 64 PyObject_HEAD /* ; */
65 65 nodetree nt;
66 66 } nodetreeObject;
67 67
68 68 /*
69 69 * This class has two behaviors.
70 70 *
71 71 * When used in a list-like way (with integer keys), we decode an
72 72 * entry in a RevlogNG index file on demand. We have limited support for
73 73 * integer-keyed insert and delete, only at elements right before the
74 74 * end.
75 75 *
76 76 * With string keys, we lazily perform a reverse mapping from node to
77 77 * rev, using a base-16 trie.
78 78 */
79 79 struct indexObjectStruct {
80 80 PyObject_HEAD
81 81 /* Type-specific fields go here. */
82 82 PyObject *data; /* raw bytes of index */
83 83 Py_buffer buf; /* buffer of data */
84 84 PyObject **cache; /* cached tuples */
85 85 const char **offsets; /* populated on demand */
86 86 Py_ssize_t raw_length; /* original number of elements */
87 87 Py_ssize_t length; /* current number of elements */
88 88 PyObject *added; /* populated on demand */
89 89 PyObject *headrevs; /* cache, invalidated on changes */
90 90 PyObject *filteredrevs; /* filtered revs set */
91 91 nodetree nt; /* base-16 trie */
92 92 int ntinitialized; /* 0 or 1 */
93 93 int ntrev; /* last rev scanned */
94 94 int ntlookups; /* # lookups */
95 95 int ntmisses; /* # lookups that miss the cache */
96 96 int inlined;
97 97 };
98 98
99 99 static Py_ssize_t index_length(const indexObject *self)
100 100 {
101 101 if (self->added == NULL)
102 102 return self->length;
103 103 return self->length + PyList_GET_SIZE(self->added);
104 104 }
105 105
106 106 static PyObject *nullentry = NULL;
107 107 static const char nullid[20] = {0};
108 108 static const Py_ssize_t nullrev = -1;
109 109
110 110 static Py_ssize_t inline_scan(indexObject *self, const char **offsets);
111 111
112 112 #if LONG_MAX == 0x7fffffffL
113 113 static const char *const tuple_format = PY23("Kiiiiiis#", "Kiiiiiiy#");
114 114 #else
115 115 static const char *const tuple_format = PY23("kiiiiiis#", "kiiiiiiy#");
116 116 #endif
117 117
118 118 /* A RevlogNG v1 index entry is 64 bytes long. */
119 119 static const long v1_hdrsize = 64;
120 120
121 121 static void raise_revlog_error(void)
122 122 {
123 123 PyObject *mod = NULL, *dict = NULL, *errclass = NULL;
124 124
125 125 mod = PyImport_ImportModule("mercurial.error");
126 126 if (mod == NULL) {
127 127 goto cleanup;
128 128 }
129 129
130 130 dict = PyModule_GetDict(mod);
131 131 if (dict == NULL) {
132 132 goto cleanup;
133 133 }
134 134 Py_INCREF(dict);
135 135
136 136 errclass = PyDict_GetItemString(dict, "RevlogError");
137 137 if (errclass == NULL) {
138 138 PyErr_SetString(PyExc_SystemError,
139 139 "could not find RevlogError");
140 140 goto cleanup;
141 141 }
142 142
143 143 /* value of exception is ignored by callers */
144 144 PyErr_SetString(errclass, "RevlogError");
145 145
146 146 cleanup:
147 147 Py_XDECREF(dict);
148 148 Py_XDECREF(mod);
149 149 }
150 150
151 151 /*
152 152 * Return a pointer to the beginning of a RevlogNG record.
153 153 */
154 154 static const char *index_deref(indexObject *self, Py_ssize_t pos)
155 155 {
156 156 if (self->inlined && pos > 0) {
157 157 if (self->offsets == NULL) {
158 158 self->offsets = PyMem_Malloc(self->raw_length *
159 159 sizeof(*self->offsets));
160 160 if (self->offsets == NULL)
161 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 167 return self->offsets[pos];
165 168 }
166 169
167 170 return (const char *)(self->buf.buf) + pos * v1_hdrsize;
168 171 }
169 172
170 173 /*
171 174 * Get parents of the given rev.
172 175 *
173 176 * The specified rev must be valid and must not be nullrev. A returned
174 177 * parent revision may be nullrev, but is guaranteed to be in valid range.
175 178 */
176 179 static inline int index_get_parents(indexObject *self, Py_ssize_t rev, int *ps,
177 180 int maxrev)
178 181 {
179 182 if (rev >= self->length) {
180 183 long tmp;
181 184 PyObject *tuple =
182 185 PyList_GET_ITEM(self->added, rev - self->length);
183 186 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 5), &tmp)) {
184 187 return -1;
185 188 }
186 189 ps[0] = (int)tmp;
187 190 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 6), &tmp)) {
188 191 return -1;
189 192 }
190 193 ps[1] = (int)tmp;
191 194 } else {
192 195 const char *data = index_deref(self, rev);
193 196 ps[0] = getbe32(data + 24);
194 197 ps[1] = getbe32(data + 28);
195 198 }
196 199 /* If index file is corrupted, ps[] may point to invalid revisions. So
197 200 * there is a risk of buffer overflow to trust them unconditionally. */
198 201 if (ps[0] < -1 || ps[0] > maxrev || ps[1] < -1 || ps[1] > maxrev) {
199 202 PyErr_SetString(PyExc_ValueError, "parent out of range");
200 203 return -1;
201 204 }
202 205 return 0;
203 206 }
204 207
205 208 /*
206 209 * Get parents of the given rev.
207 210 *
208 211 * If the specified rev is out of range, IndexError will be raised. If the
209 212 * revlog entry is corrupted, ValueError may be raised.
210 213 *
211 214 * Returns 0 on success or -1 on failure.
212 215 */
213 216 static int HgRevlogIndex_GetParents(PyObject *op, int rev, int *ps)
214 217 {
215 218 int tiprev;
216 219 if (!op || !HgRevlogIndex_Check(op) || !ps) {
217 220 PyErr_BadInternalCall();
218 221 return -1;
219 222 }
220 223 tiprev = (int)index_length((indexObject *)op) - 1;
221 224 if (rev < -1 || rev > tiprev) {
222 225 PyErr_Format(PyExc_IndexError, "rev out of range: %d", rev);
223 226 return -1;
224 227 } else if (rev == -1) {
225 228 ps[0] = ps[1] = -1;
226 229 return 0;
227 230 } else {
228 231 return index_get_parents((indexObject *)op, rev, ps, tiprev);
229 232 }
230 233 }
231 234
232 235 static inline int64_t index_get_start(indexObject *self, Py_ssize_t rev)
233 236 {
234 237 uint64_t offset;
235 238 if (rev == nullrev) {
236 239 return 0;
237 240 }
238 241 if (rev >= self->length) {
239 242 PyObject *tuple;
240 243 PyObject *pylong;
241 244 PY_LONG_LONG tmp;
242 245 tuple = PyList_GET_ITEM(self->added, rev - self->length);
243 246 pylong = PyTuple_GET_ITEM(tuple, 0);
244 247 tmp = PyLong_AsLongLong(pylong);
245 248 if (tmp == -1 && PyErr_Occurred()) {
246 249 return -1;
247 250 }
248 251 if (tmp < 0) {
249 252 PyErr_Format(PyExc_OverflowError,
250 253 "revlog entry size out of bound (%lld)",
251 254 (long long)tmp);
252 255 return -1;
253 256 }
254 257 offset = (uint64_t)tmp;
255 258 } else {
256 259 const char *data = index_deref(self, rev);
257 260 offset = getbe32(data + 4);
258 261 if (rev == 0) {
259 262 /* mask out version number for the first entry */
260 263 offset &= 0xFFFF;
261 264 } else {
262 265 uint32_t offset_high = getbe32(data);
263 266 offset |= ((uint64_t)offset_high) << 32;
264 267 }
265 268 }
266 269 return (int64_t)(offset >> 16);
267 270 }
268 271
269 272 static inline int index_get_length(indexObject *self, Py_ssize_t rev)
270 273 {
271 274 if (rev == nullrev) {
272 275 return 0;
273 276 }
274 277 if (rev >= self->length) {
275 278 PyObject *tuple;
276 279 PyObject *pylong;
277 280 long ret;
278 281 tuple = PyList_GET_ITEM(self->added, rev - self->length);
279 282 pylong = PyTuple_GET_ITEM(tuple, 1);
280 283 ret = PyInt_AsLong(pylong);
281 284 if (ret == -1 && PyErr_Occurred()) {
282 285 return -1;
283 286 }
284 287 if (ret < 0 || ret > (long)INT_MAX) {
285 288 PyErr_Format(PyExc_OverflowError,
286 289 "revlog entry size out of bound (%ld)",
287 290 ret);
288 291 return -1;
289 292 }
290 293 return (int)ret;
291 294 } else {
292 295 const char *data = index_deref(self, rev);
293 296 int tmp = (int)getbe32(data + 8);
294 297 if (tmp < 0) {
295 298 PyErr_Format(PyExc_OverflowError,
296 299 "revlog entry size out of bound (%d)",
297 300 tmp);
298 301 return -1;
299 302 }
300 303 return tmp;
301 304 }
302 305 }
303 306
304 307 /*
305 308 * RevlogNG format (all in big endian, data may be inlined):
306 309 * 6 bytes: offset
307 310 * 2 bytes: flags
308 311 * 4 bytes: compressed length
309 312 * 4 bytes: uncompressed length
310 313 * 4 bytes: base revision
311 314 * 4 bytes: link revision
312 315 * 4 bytes: parent 1 revision
313 316 * 4 bytes: parent 2 revision
314 317 * 32 bytes: nodeid (only 20 bytes used)
315 318 */
316 319 static PyObject *index_get(indexObject *self, Py_ssize_t pos)
317 320 {
318 321 uint64_t offset_flags;
319 322 int comp_len, uncomp_len, base_rev, link_rev, parent_1, parent_2;
320 323 const char *c_node_id;
321 324 const char *data;
322 325 Py_ssize_t length = index_length(self);
323 326 PyObject *entry;
324 327
325 328 if (pos == nullrev) {
326 329 Py_INCREF(nullentry);
327 330 return nullentry;
328 331 }
329 332
330 333 if (pos < 0 || pos >= length) {
331 334 PyErr_SetString(PyExc_IndexError, "revlog index out of range");
332 335 return NULL;
333 336 }
334 337
335 338 if (pos >= self->length) {
336 339 PyObject *obj;
337 340 obj = PyList_GET_ITEM(self->added, pos - self->length);
338 341 Py_INCREF(obj);
339 342 return obj;
340 343 }
341 344
342 345 if (self->cache) {
343 346 if (self->cache[pos]) {
344 347 Py_INCREF(self->cache[pos]);
345 348 return self->cache[pos];
346 349 }
347 350 } else {
348 351 self->cache = calloc(self->raw_length, sizeof(PyObject *));
349 352 if (self->cache == NULL)
350 353 return PyErr_NoMemory();
351 354 }
352 355
353 356 data = index_deref(self, pos);
354 357 if (data == NULL)
355 358 return NULL;
356 359
357 360 offset_flags = getbe32(data + 4);
358 361 if (pos == 0) /* mask out version number for the first entry */
359 362 offset_flags &= 0xFFFF;
360 363 else {
361 364 uint32_t offset_high = getbe32(data);
362 365 offset_flags |= ((uint64_t)offset_high) << 32;
363 366 }
364 367
365 368 comp_len = getbe32(data + 8);
366 369 uncomp_len = getbe32(data + 12);
367 370 base_rev = getbe32(data + 16);
368 371 link_rev = getbe32(data + 20);
369 372 parent_1 = getbe32(data + 24);
370 373 parent_2 = getbe32(data + 28);
371 374 c_node_id = data + 32;
372 375
373 376 entry = Py_BuildValue(tuple_format, offset_flags, comp_len, uncomp_len,
374 377 base_rev, link_rev, parent_1, parent_2, c_node_id,
375 378 (Py_ssize_t)20);
376 379
377 380 if (entry) {
378 381 PyObject_GC_UnTrack(entry);
379 382 Py_INCREF(entry);
380 383 }
381 384
382 385 self->cache[pos] = entry;
383 386
384 387 return entry;
385 388 }
386 389
387 390 /*
388 391 * Return the 20-byte SHA of the node corresponding to the given rev.
389 392 */
390 393 static const char *index_node(indexObject *self, Py_ssize_t pos)
391 394 {
392 395 Py_ssize_t length = index_length(self);
393 396 const char *data;
394 397
395 398 if (pos == nullrev)
396 399 return nullid;
397 400
398 401 if (pos >= length)
399 402 return NULL;
400 403
401 404 if (pos >= self->length) {
402 405 PyObject *tuple, *str;
403 406 tuple = PyList_GET_ITEM(self->added, pos - self->length);
404 407 str = PyTuple_GetItem(tuple, 7);
405 408 return str ? PyBytes_AS_STRING(str) : NULL;
406 409 }
407 410
408 411 data = index_deref(self, pos);
409 412 return data ? data + 32 : NULL;
410 413 }
411 414
412 415 /*
413 416 * Return the 20-byte SHA of the node corresponding to the given rev. The
414 417 * rev is assumed to be existing. If not, an exception is set.
415 418 */
416 419 static const char *index_node_existing(indexObject *self, Py_ssize_t pos)
417 420 {
418 421 const char *node = index_node(self, pos);
419 422 if (node == NULL) {
420 423 PyErr_Format(PyExc_IndexError, "could not access rev %d",
421 424 (int)pos);
422 425 }
423 426 return node;
424 427 }
425 428
426 429 static int nt_insert(nodetree *self, const char *node, int rev);
427 430
428 431 static int node_check(PyObject *obj, char **node)
429 432 {
430 433 Py_ssize_t nodelen;
431 434 if (PyBytes_AsStringAndSize(obj, node, &nodelen) == -1)
432 435 return -1;
433 436 if (nodelen == 20)
434 437 return 0;
435 438 PyErr_SetString(PyExc_ValueError, "20-byte hash required");
436 439 return -1;
437 440 }
438 441
439 442 static PyObject *index_append(indexObject *self, PyObject *obj)
440 443 {
441 444 char *node;
442 445 Py_ssize_t len;
443 446
444 447 if (!PyTuple_Check(obj) || PyTuple_GET_SIZE(obj) != 8) {
445 448 PyErr_SetString(PyExc_TypeError, "8-tuple required");
446 449 return NULL;
447 450 }
448 451
449 452 if (node_check(PyTuple_GET_ITEM(obj, 7), &node) == -1)
450 453 return NULL;
451 454
452 455 len = index_length(self);
453 456
454 457 if (self->added == NULL) {
455 458 self->added = PyList_New(0);
456 459 if (self->added == NULL)
457 460 return NULL;
458 461 }
459 462
460 463 if (PyList_Append(self->added, obj) == -1)
461 464 return NULL;
462 465
463 466 if (self->ntinitialized)
464 467 nt_insert(&self->nt, node, (int)len);
465 468
466 469 Py_CLEAR(self->headrevs);
467 470 Py_RETURN_NONE;
468 471 }
469 472
470 473 static PyObject *index_stats(indexObject *self)
471 474 {
472 475 PyObject *obj = PyDict_New();
473 476 PyObject *s = NULL;
474 477 PyObject *t = NULL;
475 478
476 479 if (obj == NULL)
477 480 return NULL;
478 481
479 482 #define istat(__n, __d) \
480 483 do { \
481 484 s = PyBytes_FromString(__d); \
482 485 t = PyInt_FromSsize_t(self->__n); \
483 486 if (!s || !t) \
484 487 goto bail; \
485 488 if (PyDict_SetItem(obj, s, t) == -1) \
486 489 goto bail; \
487 490 Py_CLEAR(s); \
488 491 Py_CLEAR(t); \
489 492 } while (0)
490 493
491 494 if (self->added) {
492 495 Py_ssize_t len = PyList_GET_SIZE(self->added);
493 496 s = PyBytes_FromString("index entries added");
494 497 t = PyInt_FromSsize_t(len);
495 498 if (!s || !t)
496 499 goto bail;
497 500 if (PyDict_SetItem(obj, s, t) == -1)
498 501 goto bail;
499 502 Py_CLEAR(s);
500 503 Py_CLEAR(t);
501 504 }
502 505
503 506 if (self->raw_length != self->length)
504 507 istat(raw_length, "revs on disk");
505 508 istat(length, "revs in memory");
506 509 istat(ntlookups, "node trie lookups");
507 510 istat(ntmisses, "node trie misses");
508 511 istat(ntrev, "node trie last rev scanned");
509 512 if (self->ntinitialized) {
510 513 istat(nt.capacity, "node trie capacity");
511 514 istat(nt.depth, "node trie depth");
512 515 istat(nt.length, "node trie count");
513 516 istat(nt.splits, "node trie splits");
514 517 }
515 518
516 519 #undef istat
517 520
518 521 return obj;
519 522
520 523 bail:
521 524 Py_XDECREF(obj);
522 525 Py_XDECREF(s);
523 526 Py_XDECREF(t);
524 527 return NULL;
525 528 }
526 529
527 530 /*
528 531 * When we cache a list, we want to be sure the caller can't mutate
529 532 * the cached copy.
530 533 */
531 534 static PyObject *list_copy(PyObject *list)
532 535 {
533 536 Py_ssize_t len = PyList_GET_SIZE(list);
534 537 PyObject *newlist = PyList_New(len);
535 538 Py_ssize_t i;
536 539
537 540 if (newlist == NULL)
538 541 return NULL;
539 542
540 543 for (i = 0; i < len; i++) {
541 544 PyObject *obj = PyList_GET_ITEM(list, i);
542 545 Py_INCREF(obj);
543 546 PyList_SET_ITEM(newlist, i, obj);
544 547 }
545 548
546 549 return newlist;
547 550 }
548 551
549 552 static int check_filter(PyObject *filter, Py_ssize_t arg)
550 553 {
551 554 if (filter) {
552 555 PyObject *arglist, *result;
553 556 int isfiltered;
554 557
555 558 arglist = Py_BuildValue("(n)", arg);
556 559 if (!arglist) {
557 560 return -1;
558 561 }
559 562
560 563 result = PyEval_CallObject(filter, arglist);
561 564 Py_DECREF(arglist);
562 565 if (!result) {
563 566 return -1;
564 567 }
565 568
566 569 /* PyObject_IsTrue returns 1 if true, 0 if false, -1 if error,
567 570 * same as this function, so we can just return it directly.*/
568 571 isfiltered = PyObject_IsTrue(result);
569 572 Py_DECREF(result);
570 573 return isfiltered;
571 574 } else {
572 575 return 0;
573 576 }
574 577 }
575 578
576 579 static Py_ssize_t add_roots_get_min(indexObject *self, PyObject *list,
577 580 Py_ssize_t marker, char *phases)
578 581 {
579 582 PyObject *iter = NULL;
580 583 PyObject *iter_item = NULL;
581 584 Py_ssize_t min_idx = index_length(self) + 2;
582 585 long iter_item_long;
583 586
584 587 if (PyList_GET_SIZE(list) != 0) {
585 588 iter = PyObject_GetIter(list);
586 589 if (iter == NULL)
587 590 return -2;
588 591 while ((iter_item = PyIter_Next(iter))) {
589 592 if (!pylong_to_long(iter_item, &iter_item_long)) {
590 593 Py_DECREF(iter_item);
591 594 return -2;
592 595 }
593 596 Py_DECREF(iter_item);
594 597 if (iter_item_long < min_idx)
595 598 min_idx = iter_item_long;
596 599 phases[iter_item_long] = (char)marker;
597 600 }
598 601 Py_DECREF(iter);
599 602 }
600 603
601 604 return min_idx;
602 605 }
603 606
604 607 static inline void set_phase_from_parents(char *phases, int parent_1,
605 608 int parent_2, Py_ssize_t i)
606 609 {
607 610 if (parent_1 >= 0 && phases[parent_1] > phases[i])
608 611 phases[i] = phases[parent_1];
609 612 if (parent_2 >= 0 && phases[parent_2] > phases[i])
610 613 phases[i] = phases[parent_2];
611 614 }
612 615
613 616 static PyObject *reachableroots2(indexObject *self, PyObject *args)
614 617 {
615 618
616 619 /* Input */
617 620 long minroot;
618 621 PyObject *includepatharg = NULL;
619 622 int includepath = 0;
620 623 /* heads and roots are lists */
621 624 PyObject *heads = NULL;
622 625 PyObject *roots = NULL;
623 626 PyObject *reachable = NULL;
624 627
625 628 PyObject *val;
626 629 Py_ssize_t len = index_length(self);
627 630 long revnum;
628 631 Py_ssize_t k;
629 632 Py_ssize_t i;
630 633 Py_ssize_t l;
631 634 int r;
632 635 int parents[2];
633 636
634 637 /* Internal data structure:
635 638 * tovisit: array of length len+1 (all revs + nullrev), filled upto
636 639 * lentovisit
637 640 *
638 641 * revstates: array of length len+1 (all revs + nullrev) */
639 642 int *tovisit = NULL;
640 643 long lentovisit = 0;
641 644 enum { RS_SEEN = 1, RS_ROOT = 2, RS_REACHABLE = 4 };
642 645 char *revstates = NULL;
643 646
644 647 /* Get arguments */
645 648 if (!PyArg_ParseTuple(args, "lO!O!O!", &minroot, &PyList_Type, &heads,
646 649 &PyList_Type, &roots, &PyBool_Type,
647 650 &includepatharg))
648 651 goto bail;
649 652
650 653 if (includepatharg == Py_True)
651 654 includepath = 1;
652 655
653 656 /* Initialize return set */
654 657 reachable = PyList_New(0);
655 658 if (reachable == NULL)
656 659 goto bail;
657 660
658 661 /* Initialize internal datastructures */
659 662 tovisit = (int *)malloc((len + 1) * sizeof(int));
660 663 if (tovisit == NULL) {
661 664 PyErr_NoMemory();
662 665 goto bail;
663 666 }
664 667
665 668 revstates = (char *)calloc(len + 1, 1);
666 669 if (revstates == NULL) {
667 670 PyErr_NoMemory();
668 671 goto bail;
669 672 }
670 673
671 674 l = PyList_GET_SIZE(roots);
672 675 for (i = 0; i < l; i++) {
673 676 revnum = PyInt_AsLong(PyList_GET_ITEM(roots, i));
674 677 if (revnum == -1 && PyErr_Occurred())
675 678 goto bail;
676 679 /* If root is out of range, e.g. wdir(), it must be unreachable
677 680 * from heads. So we can just ignore it. */
678 681 if (revnum + 1 < 0 || revnum + 1 >= len + 1)
679 682 continue;
680 683 revstates[revnum + 1] |= RS_ROOT;
681 684 }
682 685
683 686 /* Populate tovisit with all the heads */
684 687 l = PyList_GET_SIZE(heads);
685 688 for (i = 0; i < l; i++) {
686 689 revnum = PyInt_AsLong(PyList_GET_ITEM(heads, i));
687 690 if (revnum == -1 && PyErr_Occurred())
688 691 goto bail;
689 692 if (revnum + 1 < 0 || revnum + 1 >= len + 1) {
690 693 PyErr_SetString(PyExc_IndexError, "head out of range");
691 694 goto bail;
692 695 }
693 696 if (!(revstates[revnum + 1] & RS_SEEN)) {
694 697 tovisit[lentovisit++] = (int)revnum;
695 698 revstates[revnum + 1] |= RS_SEEN;
696 699 }
697 700 }
698 701
699 702 /* Visit the tovisit list and find the reachable roots */
700 703 k = 0;
701 704 while (k < lentovisit) {
702 705 /* Add the node to reachable if it is a root*/
703 706 revnum = tovisit[k++];
704 707 if (revstates[revnum + 1] & RS_ROOT) {
705 708 revstates[revnum + 1] |= RS_REACHABLE;
706 709 val = PyInt_FromLong(revnum);
707 710 if (val == NULL)
708 711 goto bail;
709 712 r = PyList_Append(reachable, val);
710 713 Py_DECREF(val);
711 714 if (r < 0)
712 715 goto bail;
713 716 if (includepath == 0)
714 717 continue;
715 718 }
716 719
717 720 /* Add its parents to the list of nodes to visit */
718 721 if (revnum == nullrev)
719 722 continue;
720 723 r = index_get_parents(self, revnum, parents, (int)len - 1);
721 724 if (r < 0)
722 725 goto bail;
723 726 for (i = 0; i < 2; i++) {
724 727 if (!(revstates[parents[i] + 1] & RS_SEEN) &&
725 728 parents[i] >= minroot) {
726 729 tovisit[lentovisit++] = parents[i];
727 730 revstates[parents[i] + 1] |= RS_SEEN;
728 731 }
729 732 }
730 733 }
731 734
732 735 /* Find all the nodes in between the roots we found and the heads
733 736 * and add them to the reachable set */
734 737 if (includepath == 1) {
735 738 long minidx = minroot;
736 739 if (minidx < 0)
737 740 minidx = 0;
738 741 for (i = minidx; i < len; i++) {
739 742 if (!(revstates[i + 1] & RS_SEEN))
740 743 continue;
741 744 r = index_get_parents(self, i, parents, (int)len - 1);
742 745 /* Corrupted index file, error is set from
743 746 * index_get_parents */
744 747 if (r < 0)
745 748 goto bail;
746 749 if (((revstates[parents[0] + 1] |
747 750 revstates[parents[1] + 1]) &
748 751 RS_REACHABLE) &&
749 752 !(revstates[i + 1] & RS_REACHABLE)) {
750 753 revstates[i + 1] |= RS_REACHABLE;
751 754 val = PyInt_FromSsize_t(i);
752 755 if (val == NULL)
753 756 goto bail;
754 757 r = PyList_Append(reachable, val);
755 758 Py_DECREF(val);
756 759 if (r < 0)
757 760 goto bail;
758 761 }
759 762 }
760 763 }
761 764
762 765 free(revstates);
763 766 free(tovisit);
764 767 return reachable;
765 768 bail:
766 769 Py_XDECREF(reachable);
767 770 free(revstates);
768 771 free(tovisit);
769 772 return NULL;
770 773 }
771 774
772 775 static PyObject *compute_phases_map_sets(indexObject *self, PyObject *args)
773 776 {
774 777 PyObject *roots = Py_None;
775 778 PyObject *ret = NULL;
776 779 PyObject *phasessize = NULL;
777 780 PyObject *phaseroots = NULL;
778 781 PyObject *phaseset = NULL;
779 782 PyObject *phasessetlist = NULL;
780 783 PyObject *rev = NULL;
781 784 Py_ssize_t len = index_length(self);
782 785 Py_ssize_t numphase = 0;
783 786 Py_ssize_t minrevallphases = 0;
784 787 Py_ssize_t minrevphase = 0;
785 788 Py_ssize_t i = 0;
786 789 char *phases = NULL;
787 790 long phase;
788 791
789 792 if (!PyArg_ParseTuple(args, "O", &roots))
790 793 goto done;
791 794 if (roots == NULL || !PyList_Check(roots)) {
792 795 PyErr_SetString(PyExc_TypeError, "roots must be a list");
793 796 goto done;
794 797 }
795 798
796 799 phases = calloc(
797 800 len, 1); /* phase per rev: {0: public, 1: draft, 2: secret} */
798 801 if (phases == NULL) {
799 802 PyErr_NoMemory();
800 803 goto done;
801 804 }
802 805 /* Put the phase information of all the roots in phases */
803 806 numphase = PyList_GET_SIZE(roots) + 1;
804 807 minrevallphases = len + 1;
805 808 phasessetlist = PyList_New(numphase);
806 809 if (phasessetlist == NULL)
807 810 goto done;
808 811
809 812 PyList_SET_ITEM(phasessetlist, 0, Py_None);
810 813 Py_INCREF(Py_None);
811 814
812 815 for (i = 0; i < numphase - 1; i++) {
813 816 phaseroots = PyList_GET_ITEM(roots, i);
814 817 phaseset = PySet_New(NULL);
815 818 if (phaseset == NULL)
816 819 goto release;
817 820 PyList_SET_ITEM(phasessetlist, i + 1, phaseset);
818 821 if (!PyList_Check(phaseroots)) {
819 822 PyErr_SetString(PyExc_TypeError,
820 823 "roots item must be a list");
821 824 goto release;
822 825 }
823 826 minrevphase =
824 827 add_roots_get_min(self, phaseroots, i + 1, phases);
825 828 if (minrevphase == -2) /* Error from add_roots_get_min */
826 829 goto release;
827 830 minrevallphases = MIN(minrevallphases, minrevphase);
828 831 }
829 832 /* Propagate the phase information from the roots to the revs */
830 833 if (minrevallphases != -1) {
831 834 int parents[2];
832 835 for (i = minrevallphases; i < len; i++) {
833 836 if (index_get_parents(self, i, parents, (int)len - 1) <
834 837 0)
835 838 goto release;
836 839 set_phase_from_parents(phases, parents[0], parents[1],
837 840 i);
838 841 }
839 842 }
840 843 /* Transform phase list to a python list */
841 844 phasessize = PyInt_FromSsize_t(len);
842 845 if (phasessize == NULL)
843 846 goto release;
844 847 for (i = 0; i < len; i++) {
845 848 phase = phases[i];
846 849 /* We only store the sets of phase for non public phase, the
847 850 * public phase is computed as a difference */
848 851 if (phase != 0) {
849 852 phaseset = PyList_GET_ITEM(phasessetlist, phase);
850 853 rev = PyInt_FromSsize_t(i);
851 854 if (rev == NULL)
852 855 goto release;
853 856 PySet_Add(phaseset, rev);
854 857 Py_XDECREF(rev);
855 858 }
856 859 }
857 860 ret = PyTuple_Pack(2, phasessize, phasessetlist);
858 861
859 862 release:
860 863 Py_XDECREF(phasessize);
861 864 Py_XDECREF(phasessetlist);
862 865 done:
863 866 free(phases);
864 867 return ret;
865 868 }
866 869
867 870 static PyObject *index_headrevs(indexObject *self, PyObject *args)
868 871 {
869 872 Py_ssize_t i, j, len;
870 873 char *nothead = NULL;
871 874 PyObject *heads = NULL;
872 875 PyObject *filter = NULL;
873 876 PyObject *filteredrevs = Py_None;
874 877
875 878 if (!PyArg_ParseTuple(args, "|O", &filteredrevs)) {
876 879 return NULL;
877 880 }
878 881
879 882 if (self->headrevs && filteredrevs == self->filteredrevs)
880 883 return list_copy(self->headrevs);
881 884
882 885 Py_DECREF(self->filteredrevs);
883 886 self->filteredrevs = filteredrevs;
884 887 Py_INCREF(filteredrevs);
885 888
886 889 if (filteredrevs != Py_None) {
887 890 filter = PyObject_GetAttrString(filteredrevs, "__contains__");
888 891 if (!filter) {
889 892 PyErr_SetString(
890 893 PyExc_TypeError,
891 894 "filteredrevs has no attribute __contains__");
892 895 goto bail;
893 896 }
894 897 }
895 898
896 899 len = index_length(self);
897 900 heads = PyList_New(0);
898 901 if (heads == NULL)
899 902 goto bail;
900 903 if (len == 0) {
901 904 PyObject *nullid = PyInt_FromLong(-1);
902 905 if (nullid == NULL || PyList_Append(heads, nullid) == -1) {
903 906 Py_XDECREF(nullid);
904 907 goto bail;
905 908 }
906 909 goto done;
907 910 }
908 911
909 912 nothead = calloc(len, 1);
910 913 if (nothead == NULL) {
911 914 PyErr_NoMemory();
912 915 goto bail;
913 916 }
914 917
915 918 for (i = len - 1; i >= 0; i--) {
916 919 int isfiltered;
917 920 int parents[2];
918 921
919 922 /* If nothead[i] == 1, it means we've seen an unfiltered child
920 923 * of this node already, and therefore this node is not
921 924 * filtered. So we can skip the expensive check_filter step.
922 925 */
923 926 if (nothead[i] != 1) {
924 927 isfiltered = check_filter(filter, i);
925 928 if (isfiltered == -1) {
926 929 PyErr_SetString(PyExc_TypeError,
927 930 "unable to check filter");
928 931 goto bail;
929 932 }
930 933
931 934 if (isfiltered) {
932 935 nothead[i] = 1;
933 936 continue;
934 937 }
935 938 }
936 939
937 940 if (index_get_parents(self, i, parents, (int)len - 1) < 0)
938 941 goto bail;
939 942 for (j = 0; j < 2; j++) {
940 943 if (parents[j] >= 0)
941 944 nothead[parents[j]] = 1;
942 945 }
943 946 }
944 947
945 948 for (i = 0; i < len; i++) {
946 949 PyObject *head;
947 950
948 951 if (nothead[i])
949 952 continue;
950 953 head = PyInt_FromSsize_t(i);
951 954 if (head == NULL || PyList_Append(heads, head) == -1) {
952 955 Py_XDECREF(head);
953 956 goto bail;
954 957 }
955 958 }
956 959
957 960 done:
958 961 self->headrevs = heads;
959 962 Py_XDECREF(filter);
960 963 free(nothead);
961 964 return list_copy(self->headrevs);
962 965 bail:
963 966 Py_XDECREF(filter);
964 967 Py_XDECREF(heads);
965 968 free(nothead);
966 969 return NULL;
967 970 }
968 971
969 972 /**
970 973 * Obtain the base revision index entry.
971 974 *
972 975 * Callers must ensure that rev >= 0 or illegal memory access may occur.
973 976 */
974 977 static inline int index_baserev(indexObject *self, int rev)
975 978 {
976 979 const char *data;
977 980 int result;
978 981
979 982 if (rev >= self->length) {
980 983 PyObject *tuple =
981 984 PyList_GET_ITEM(self->added, rev - self->length);
982 985 long ret;
983 986 if (!pylong_to_long(PyTuple_GET_ITEM(tuple, 3), &ret)) {
984 987 return -2;
985 988 }
986 989 result = (int)ret;
987 990 } else {
988 991 data = index_deref(self, rev);
989 992 if (data == NULL) {
990 993 return -2;
991 994 }
992 995
993 996 result = getbe32(data + 16);
994 997 }
995 998 if (result > rev) {
996 999 PyErr_Format(
997 1000 PyExc_ValueError,
998 1001 "corrupted revlog, revision base above revision: %d, %d",
999 1002 rev, result);
1000 1003 return -2;
1001 1004 }
1002 1005 if (result < -1) {
1003 1006 PyErr_Format(
1004 1007 PyExc_ValueError,
1005 1008 "corrupted revlog, revision base out of range: %d, %d", rev,
1006 1009 result);
1007 1010 return -2;
1008 1011 }
1009 1012 return result;
1010 1013 }
1011 1014
1012 1015 /**
1013 1016 * Find if a revision is a snapshot or not
1014 1017 *
1015 1018 * Only relevant for sparse-revlog case.
1016 1019 * Callers must ensure that rev is in a valid range.
1017 1020 */
1018 1021 static int index_issnapshotrev(indexObject *self, Py_ssize_t rev)
1019 1022 {
1020 1023 int ps[2];
1021 1024 Py_ssize_t base;
1022 1025 while (rev >= 0) {
1023 1026 base = (Py_ssize_t)index_baserev(self, rev);
1024 1027 if (base == rev) {
1025 1028 base = -1;
1026 1029 }
1027 1030 if (base == -2) {
1028 1031 assert(PyErr_Occurred());
1029 1032 return -1;
1030 1033 }
1031 1034 if (base == -1) {
1032 1035 return 1;
1033 1036 }
1034 1037 if (index_get_parents(self, rev, ps, (int)rev) < 0) {
1035 1038 assert(PyErr_Occurred());
1036 1039 return -1;
1037 1040 };
1038 1041 if (base == ps[0] || base == ps[1]) {
1039 1042 return 0;
1040 1043 }
1041 1044 rev = base;
1042 1045 }
1043 1046 return rev == -1;
1044 1047 }
1045 1048
1046 1049 static PyObject *index_issnapshot(indexObject *self, PyObject *value)
1047 1050 {
1048 1051 long rev;
1049 1052 int issnap;
1050 1053 Py_ssize_t length = index_length(self);
1051 1054
1052 1055 if (!pylong_to_long(value, &rev)) {
1053 1056 return NULL;
1054 1057 }
1055 1058 if (rev < -1 || rev >= length) {
1056 1059 PyErr_Format(PyExc_ValueError, "revlog index out of range: %ld",
1057 1060 rev);
1058 1061 return NULL;
1059 1062 };
1060 1063 issnap = index_issnapshotrev(self, (Py_ssize_t)rev);
1061 1064 if (issnap < 0) {
1062 1065 return NULL;
1063 1066 };
1064 1067 return PyBool_FromLong((long)issnap);
1065 1068 }
1066 1069
1067 1070 static PyObject *index_findsnapshots(indexObject *self, PyObject *args)
1068 1071 {
1069 1072 Py_ssize_t start_rev;
1070 1073 PyObject *cache;
1071 1074 Py_ssize_t base;
1072 1075 Py_ssize_t rev;
1073 1076 PyObject *key = NULL;
1074 1077 PyObject *value = NULL;
1075 1078 const Py_ssize_t length = index_length(self);
1076 1079 if (!PyArg_ParseTuple(args, "O!n", &PyDict_Type, &cache, &start_rev)) {
1077 1080 return NULL;
1078 1081 }
1079 1082 for (rev = start_rev; rev < length; rev++) {
1080 1083 int issnap;
1081 1084 PyObject *allvalues = NULL;
1082 1085 issnap = index_issnapshotrev(self, rev);
1083 1086 if (issnap < 0) {
1084 1087 goto bail;
1085 1088 }
1086 1089 if (issnap == 0) {
1087 1090 continue;
1088 1091 }
1089 1092 base = (Py_ssize_t)index_baserev(self, rev);
1090 1093 if (base == rev) {
1091 1094 base = -1;
1092 1095 }
1093 1096 if (base == -2) {
1094 1097 assert(PyErr_Occurred());
1095 1098 goto bail;
1096 1099 }
1097 1100 key = PyInt_FromSsize_t(base);
1098 1101 allvalues = PyDict_GetItem(cache, key);
1099 1102 if (allvalues == NULL && PyErr_Occurred()) {
1100 1103 goto bail;
1101 1104 }
1102 1105 if (allvalues == NULL) {
1103 1106 int r;
1104 1107 allvalues = PyList_New(0);
1105 1108 if (!allvalues) {
1106 1109 goto bail;
1107 1110 }
1108 1111 r = PyDict_SetItem(cache, key, allvalues);
1109 1112 Py_DECREF(allvalues);
1110 1113 if (r < 0) {
1111 1114 goto bail;
1112 1115 }
1113 1116 }
1114 1117 value = PyInt_FromSsize_t(rev);
1115 1118 if (PyList_Append(allvalues, value)) {
1116 1119 goto bail;
1117 1120 }
1118 1121 Py_CLEAR(key);
1119 1122 Py_CLEAR(value);
1120 1123 }
1121 1124 Py_RETURN_NONE;
1122 1125 bail:
1123 1126 Py_XDECREF(key);
1124 1127 Py_XDECREF(value);
1125 1128 return NULL;
1126 1129 }
1127 1130
1128 1131 static PyObject *index_deltachain(indexObject *self, PyObject *args)
1129 1132 {
1130 1133 int rev, generaldelta;
1131 1134 PyObject *stoparg;
1132 1135 int stoprev, iterrev, baserev = -1;
1133 1136 int stopped;
1134 1137 PyObject *chain = NULL, *result = NULL;
1135 1138 const Py_ssize_t length = index_length(self);
1136 1139
1137 1140 if (!PyArg_ParseTuple(args, "iOi", &rev, &stoparg, &generaldelta)) {
1138 1141 return NULL;
1139 1142 }
1140 1143
1141 1144 if (PyInt_Check(stoparg)) {
1142 1145 stoprev = (int)PyInt_AsLong(stoparg);
1143 1146 if (stoprev == -1 && PyErr_Occurred()) {
1144 1147 return NULL;
1145 1148 }
1146 1149 } else if (stoparg == Py_None) {
1147 1150 stoprev = -2;
1148 1151 } else {
1149 1152 PyErr_SetString(PyExc_ValueError,
1150 1153 "stoprev must be integer or None");
1151 1154 return NULL;
1152 1155 }
1153 1156
1154 1157 if (rev < 0 || rev >= length) {
1155 1158 PyErr_SetString(PyExc_ValueError, "revlog index out of range");
1156 1159 return NULL;
1157 1160 }
1158 1161
1159 1162 chain = PyList_New(0);
1160 1163 if (chain == NULL) {
1161 1164 return NULL;
1162 1165 }
1163 1166
1164 1167 baserev = index_baserev(self, rev);
1165 1168
1166 1169 /* This should never happen. */
1167 1170 if (baserev <= -2) {
1168 1171 /* Error should be set by index_deref() */
1169 1172 assert(PyErr_Occurred());
1170 1173 goto bail;
1171 1174 }
1172 1175
1173 1176 iterrev = rev;
1174 1177
1175 1178 while (iterrev != baserev && iterrev != stoprev) {
1176 1179 PyObject *value = PyInt_FromLong(iterrev);
1177 1180 if (value == NULL) {
1178 1181 goto bail;
1179 1182 }
1180 1183 if (PyList_Append(chain, value)) {
1181 1184 Py_DECREF(value);
1182 1185 goto bail;
1183 1186 }
1184 1187 Py_DECREF(value);
1185 1188
1186 1189 if (generaldelta) {
1187 1190 iterrev = baserev;
1188 1191 } else {
1189 1192 iterrev--;
1190 1193 }
1191 1194
1192 1195 if (iterrev < 0) {
1193 1196 break;
1194 1197 }
1195 1198
1196 1199 if (iterrev >= length) {
1197 1200 PyErr_SetString(PyExc_IndexError,
1198 1201 "revision outside index");
1199 1202 return NULL;
1200 1203 }
1201 1204
1202 1205 baserev = index_baserev(self, iterrev);
1203 1206
1204 1207 /* This should never happen. */
1205 1208 if (baserev <= -2) {
1206 1209 /* Error should be set by index_deref() */
1207 1210 assert(PyErr_Occurred());
1208 1211 goto bail;
1209 1212 }
1210 1213 }
1211 1214
1212 1215 if (iterrev == stoprev) {
1213 1216 stopped = 1;
1214 1217 } else {
1215 1218 PyObject *value = PyInt_FromLong(iterrev);
1216 1219 if (value == NULL) {
1217 1220 goto bail;
1218 1221 }
1219 1222 if (PyList_Append(chain, value)) {
1220 1223 Py_DECREF(value);
1221 1224 goto bail;
1222 1225 }
1223 1226 Py_DECREF(value);
1224 1227
1225 1228 stopped = 0;
1226 1229 }
1227 1230
1228 1231 if (PyList_Reverse(chain)) {
1229 1232 goto bail;
1230 1233 }
1231 1234
1232 1235 result = Py_BuildValue("OO", chain, stopped ? Py_True : Py_False);
1233 1236 Py_DECREF(chain);
1234 1237 return result;
1235 1238
1236 1239 bail:
1237 1240 Py_DECREF(chain);
1238 1241 return NULL;
1239 1242 }
1240 1243
1241 1244 static inline int64_t
1242 1245 index_segment_span(indexObject *self, Py_ssize_t start_rev, Py_ssize_t end_rev)
1243 1246 {
1244 1247 int64_t start_offset;
1245 1248 int64_t end_offset;
1246 1249 int end_size;
1247 1250 start_offset = index_get_start(self, start_rev);
1248 1251 if (start_offset < 0) {
1249 1252 return -1;
1250 1253 }
1251 1254 end_offset = index_get_start(self, end_rev);
1252 1255 if (end_offset < 0) {
1253 1256 return -1;
1254 1257 }
1255 1258 end_size = index_get_length(self, end_rev);
1256 1259 if (end_size < 0) {
1257 1260 return -1;
1258 1261 }
1259 1262 if (end_offset < start_offset) {
1260 1263 PyErr_Format(PyExc_ValueError,
1261 1264 "corrupted revlog index: inconsistent offset "
1262 1265 "between revisions (%zd) and (%zd)",
1263 1266 start_rev, end_rev);
1264 1267 return -1;
1265 1268 }
1266 1269 return (end_offset - start_offset) + (int64_t)end_size;
1267 1270 }
1268 1271
1269 1272 /* returns endidx so that revs[startidx:endidx] has no empty trailing revs */
1270 1273 static Py_ssize_t trim_endidx(indexObject *self, const Py_ssize_t *revs,
1271 1274 Py_ssize_t startidx, Py_ssize_t endidx)
1272 1275 {
1273 1276 int length;
1274 1277 while (endidx > 1 && endidx > startidx) {
1275 1278 length = index_get_length(self, revs[endidx - 1]);
1276 1279 if (length < 0) {
1277 1280 return -1;
1278 1281 }
1279 1282 if (length != 0) {
1280 1283 break;
1281 1284 }
1282 1285 endidx -= 1;
1283 1286 }
1284 1287 return endidx;
1285 1288 }
1286 1289
1287 1290 struct Gap {
1288 1291 int64_t size;
1289 1292 Py_ssize_t idx;
1290 1293 };
1291 1294
1292 1295 static int gap_compare(const void *left, const void *right)
1293 1296 {
1294 1297 const struct Gap *l_left = ((const struct Gap *)left);
1295 1298 const struct Gap *l_right = ((const struct Gap *)right);
1296 1299 if (l_left->size < l_right->size) {
1297 1300 return -1;
1298 1301 } else if (l_left->size > l_right->size) {
1299 1302 return 1;
1300 1303 }
1301 1304 return 0;
1302 1305 }
1303 1306 static int Py_ssize_t_compare(const void *left, const void *right)
1304 1307 {
1305 1308 const Py_ssize_t l_left = *(const Py_ssize_t *)left;
1306 1309 const Py_ssize_t l_right = *(const Py_ssize_t *)right;
1307 1310 if (l_left < l_right) {
1308 1311 return -1;
1309 1312 } else if (l_left > l_right) {
1310 1313 return 1;
1311 1314 }
1312 1315 return 0;
1313 1316 }
1314 1317
1315 1318 static PyObject *index_slicechunktodensity(indexObject *self, PyObject *args)
1316 1319 {
1317 1320 /* method arguments */
1318 1321 PyObject *list_revs = NULL; /* revisions in the chain */
1319 1322 double targetdensity = 0; /* min density to achieve */
1320 1323 Py_ssize_t mingapsize = 0; /* threshold to ignore gaps */
1321 1324
1322 1325 /* other core variables */
1323 1326 Py_ssize_t idxlen = index_length(self);
1324 1327 Py_ssize_t i; /* used for various iteration */
1325 1328 PyObject *result = NULL; /* the final return of the function */
1326 1329
1327 1330 /* generic information about the delta chain being slice */
1328 1331 Py_ssize_t num_revs = 0; /* size of the full delta chain */
1329 1332 Py_ssize_t *revs = NULL; /* native array of revision in the chain */
1330 1333 int64_t chainpayload = 0; /* sum of all delta in the chain */
1331 1334 int64_t deltachainspan = 0; /* distance from first byte to last byte */
1332 1335
1333 1336 /* variable used for slicing the delta chain */
1334 1337 int64_t readdata = 0; /* amount of data currently planned to be read */
1335 1338 double density = 0; /* ration of payload data compared to read ones */
1336 1339 int64_t previous_end;
1337 1340 struct Gap *gaps = NULL; /* array of notable gap in the chain */
1338 1341 Py_ssize_t num_gaps =
1339 1342 0; /* total number of notable gap recorded so far */
1340 1343 Py_ssize_t *selected_indices = NULL; /* indices of gap skipped over */
1341 1344 Py_ssize_t num_selected = 0; /* number of gaps skipped */
1342 1345 PyObject *chunk = NULL; /* individual slice */
1343 1346 PyObject *allchunks = NULL; /* all slices */
1344 1347 Py_ssize_t previdx;
1345 1348
1346 1349 /* parsing argument */
1347 1350 if (!PyArg_ParseTuple(args, "O!dn", &PyList_Type, &list_revs,
1348 1351 &targetdensity, &mingapsize)) {
1349 1352 goto bail;
1350 1353 }
1351 1354
1352 1355 /* If the delta chain contains a single element, we do not need slicing
1353 1356 */
1354 1357 num_revs = PyList_GET_SIZE(list_revs);
1355 1358 if (num_revs <= 1) {
1356 1359 result = PyTuple_Pack(1, list_revs);
1357 1360 goto done;
1358 1361 }
1359 1362
1360 1363 /* Turn the python list into a native integer array (for efficiency) */
1361 1364 revs = (Py_ssize_t *)calloc(num_revs, sizeof(Py_ssize_t));
1362 1365 if (revs == NULL) {
1363 1366 PyErr_NoMemory();
1364 1367 goto bail;
1365 1368 }
1366 1369 for (i = 0; i < num_revs; i++) {
1367 1370 Py_ssize_t revnum = PyInt_AsLong(PyList_GET_ITEM(list_revs, i));
1368 1371 if (revnum == -1 && PyErr_Occurred()) {
1369 1372 goto bail;
1370 1373 }
1371 1374 if (revnum < nullrev || revnum >= idxlen) {
1372 1375 PyErr_Format(PyExc_IndexError,
1373 1376 "index out of range: %zd", revnum);
1374 1377 goto bail;
1375 1378 }
1376 1379 revs[i] = revnum;
1377 1380 }
1378 1381
1379 1382 /* Compute and check various property of the unsliced delta chain */
1380 1383 deltachainspan = index_segment_span(self, revs[0], revs[num_revs - 1]);
1381 1384 if (deltachainspan < 0) {
1382 1385 goto bail;
1383 1386 }
1384 1387
1385 1388 if (deltachainspan <= mingapsize) {
1386 1389 result = PyTuple_Pack(1, list_revs);
1387 1390 goto done;
1388 1391 }
1389 1392 chainpayload = 0;
1390 1393 for (i = 0; i < num_revs; i++) {
1391 1394 int tmp = index_get_length(self, revs[i]);
1392 1395 if (tmp < 0) {
1393 1396 goto bail;
1394 1397 }
1395 1398 chainpayload += tmp;
1396 1399 }
1397 1400
1398 1401 readdata = deltachainspan;
1399 1402 density = 1.0;
1400 1403
1401 1404 if (0 < deltachainspan) {
1402 1405 density = (double)chainpayload / (double)deltachainspan;
1403 1406 }
1404 1407
1405 1408 if (density >= targetdensity) {
1406 1409 result = PyTuple_Pack(1, list_revs);
1407 1410 goto done;
1408 1411 }
1409 1412
1410 1413 /* if chain is too sparse, look for relevant gaps */
1411 1414 gaps = (struct Gap *)calloc(num_revs, sizeof(struct Gap));
1412 1415 if (gaps == NULL) {
1413 1416 PyErr_NoMemory();
1414 1417 goto bail;
1415 1418 }
1416 1419
1417 1420 previous_end = -1;
1418 1421 for (i = 0; i < num_revs; i++) {
1419 1422 int64_t revstart;
1420 1423 int revsize;
1421 1424 revstart = index_get_start(self, revs[i]);
1422 1425 if (revstart < 0) {
1423 1426 goto bail;
1424 1427 };
1425 1428 revsize = index_get_length(self, revs[i]);
1426 1429 if (revsize < 0) {
1427 1430 goto bail;
1428 1431 };
1429 1432 if (revsize == 0) {
1430 1433 continue;
1431 1434 }
1432 1435 if (previous_end >= 0) {
1433 1436 int64_t gapsize = revstart - previous_end;
1434 1437 if (gapsize > mingapsize) {
1435 1438 gaps[num_gaps].size = gapsize;
1436 1439 gaps[num_gaps].idx = i;
1437 1440 num_gaps += 1;
1438 1441 }
1439 1442 }
1440 1443 previous_end = revstart + revsize;
1441 1444 }
1442 1445 if (num_gaps == 0) {
1443 1446 result = PyTuple_Pack(1, list_revs);
1444 1447 goto done;
1445 1448 }
1446 1449 qsort(gaps, num_gaps, sizeof(struct Gap), &gap_compare);
1447 1450
1448 1451 /* Slice the largest gap first, they improve the density the most */
1449 1452 selected_indices =
1450 1453 (Py_ssize_t *)malloc((num_gaps + 1) * sizeof(Py_ssize_t));
1451 1454 if (selected_indices == NULL) {
1452 1455 PyErr_NoMemory();
1453 1456 goto bail;
1454 1457 }
1455 1458
1456 1459 for (i = num_gaps - 1; i >= 0; i--) {
1457 1460 selected_indices[num_selected] = gaps[i].idx;
1458 1461 readdata -= gaps[i].size;
1459 1462 num_selected += 1;
1460 1463 if (readdata <= 0) {
1461 1464 density = 1.0;
1462 1465 } else {
1463 1466 density = (double)chainpayload / (double)readdata;
1464 1467 }
1465 1468 if (density >= targetdensity) {
1466 1469 break;
1467 1470 }
1468 1471 }
1469 1472 qsort(selected_indices, num_selected, sizeof(Py_ssize_t),
1470 1473 &Py_ssize_t_compare);
1471 1474
1472 1475 /* create the resulting slice */
1473 1476 allchunks = PyList_New(0);
1474 1477 if (allchunks == NULL) {
1475 1478 goto bail;
1476 1479 }
1477 1480 previdx = 0;
1478 1481 selected_indices[num_selected] = num_revs;
1479 1482 for (i = 0; i <= num_selected; i++) {
1480 1483 Py_ssize_t idx = selected_indices[i];
1481 1484 Py_ssize_t endidx = trim_endidx(self, revs, previdx, idx);
1482 1485 if (endidx < 0) {
1483 1486 goto bail;
1484 1487 }
1485 1488 if (previdx < endidx) {
1486 1489 chunk = PyList_GetSlice(list_revs, previdx, endidx);
1487 1490 if (chunk == NULL) {
1488 1491 goto bail;
1489 1492 }
1490 1493 if (PyList_Append(allchunks, chunk) == -1) {
1491 1494 goto bail;
1492 1495 }
1493 1496 Py_DECREF(chunk);
1494 1497 chunk = NULL;
1495 1498 }
1496 1499 previdx = idx;
1497 1500 }
1498 1501 result = allchunks;
1499 1502 goto done;
1500 1503
1501 1504 bail:
1502 1505 Py_XDECREF(allchunks);
1503 1506 Py_XDECREF(chunk);
1504 1507 done:
1505 1508 free(revs);
1506 1509 free(gaps);
1507 1510 free(selected_indices);
1508 1511 return result;
1509 1512 }
1510 1513
1511 1514 static inline int nt_level(const char *node, Py_ssize_t level)
1512 1515 {
1513 1516 int v = node[level >> 1];
1514 1517 if (!(level & 1))
1515 1518 v >>= 4;
1516 1519 return v & 0xf;
1517 1520 }
1518 1521
1519 1522 /*
1520 1523 * Return values:
1521 1524 *
1522 1525 * -4: match is ambiguous (multiple candidates)
1523 1526 * -2: not found
1524 1527 * rest: valid rev
1525 1528 */
1526 1529 static int nt_find(nodetree *self, const char *node, Py_ssize_t nodelen,
1527 1530 int hex)
1528 1531 {
1529 1532 int (*getnybble)(const char *, Py_ssize_t) = hex ? hexdigit : nt_level;
1530 1533 int level, maxlevel, off;
1531 1534
1532 1535 if (nodelen == 20 && node[0] == '\0' && memcmp(node, nullid, 20) == 0)
1533 1536 return -1;
1534 1537
1535 1538 if (hex)
1536 1539 maxlevel = nodelen > 40 ? 40 : (int)nodelen;
1537 1540 else
1538 1541 maxlevel = nodelen > 20 ? 40 : ((int)nodelen * 2);
1539 1542
1540 1543 for (level = off = 0; level < maxlevel; level++) {
1541 1544 int k = getnybble(node, level);
1542 1545 nodetreenode *n = &self->nodes[off];
1543 1546 int v = n->children[k];
1544 1547
1545 1548 if (v < 0) {
1546 1549 const char *n;
1547 1550 Py_ssize_t i;
1548 1551
1549 1552 v = -(v + 2);
1550 1553 n = index_node(self->index, v);
1551 1554 if (n == NULL)
1552 1555 return -2;
1553 1556 for (i = level; i < maxlevel; i++)
1554 1557 if (getnybble(node, i) != nt_level(n, i))
1555 1558 return -2;
1556 1559 return v;
1557 1560 }
1558 1561 if (v == 0)
1559 1562 return -2;
1560 1563 off = v;
1561 1564 }
1562 1565 /* multiple matches against an ambiguous prefix */
1563 1566 return -4;
1564 1567 }
1565 1568
1566 1569 static int nt_new(nodetree *self)
1567 1570 {
1568 1571 if (self->length == self->capacity) {
1569 1572 unsigned newcapacity;
1570 1573 nodetreenode *newnodes;
1571 1574 newcapacity = self->capacity * 2;
1572 1575 if (newcapacity >= INT_MAX / sizeof(nodetreenode)) {
1573 1576 PyErr_SetString(PyExc_MemoryError,
1574 1577 "overflow in nt_new");
1575 1578 return -1;
1576 1579 }
1577 1580 newnodes =
1578 1581 realloc(self->nodes, newcapacity * sizeof(nodetreenode));
1579 1582 if (newnodes == NULL) {
1580 1583 PyErr_SetString(PyExc_MemoryError, "out of memory");
1581 1584 return -1;
1582 1585 }
1583 1586 self->capacity = newcapacity;
1584 1587 self->nodes = newnodes;
1585 1588 memset(&self->nodes[self->length], 0,
1586 1589 sizeof(nodetreenode) * (self->capacity - self->length));
1587 1590 }
1588 1591 return self->length++;
1589 1592 }
1590 1593
1591 1594 static int nt_insert(nodetree *self, const char *node, int rev)
1592 1595 {
1593 1596 int level = 0;
1594 1597 int off = 0;
1595 1598
1596 1599 while (level < 40) {
1597 1600 int k = nt_level(node, level);
1598 1601 nodetreenode *n;
1599 1602 int v;
1600 1603
1601 1604 n = &self->nodes[off];
1602 1605 v = n->children[k];
1603 1606
1604 1607 if (v == 0) {
1605 1608 n->children[k] = -rev - 2;
1606 1609 return 0;
1607 1610 }
1608 1611 if (v < 0) {
1609 1612 const char *oldnode =
1610 1613 index_node_existing(self->index, -(v + 2));
1611 1614 int noff;
1612 1615
1613 1616 if (oldnode == NULL)
1614 1617 return -1;
1615 1618 if (!memcmp(oldnode, node, 20)) {
1616 1619 n->children[k] = -rev - 2;
1617 1620 return 0;
1618 1621 }
1619 1622 noff = nt_new(self);
1620 1623 if (noff == -1)
1621 1624 return -1;
1622 1625 /* self->nodes may have been changed by realloc */
1623 1626 self->nodes[off].children[k] = noff;
1624 1627 off = noff;
1625 1628 n = &self->nodes[off];
1626 1629 n->children[nt_level(oldnode, ++level)] = v;
1627 1630 if (level > self->depth)
1628 1631 self->depth = level;
1629 1632 self->splits += 1;
1630 1633 } else {
1631 1634 level += 1;
1632 1635 off = v;
1633 1636 }
1634 1637 }
1635 1638
1636 1639 return -1;
1637 1640 }
1638 1641
1639 1642 static PyObject *ntobj_insert(nodetreeObject *self, PyObject *args)
1640 1643 {
1641 1644 Py_ssize_t rev;
1642 1645 const char *node;
1643 1646 Py_ssize_t length;
1644 1647 if (!PyArg_ParseTuple(args, "n", &rev))
1645 1648 return NULL;
1646 1649 length = index_length(self->nt.index);
1647 1650 if (rev < 0 || rev >= length) {
1648 1651 PyErr_SetString(PyExc_ValueError, "revlog index out of range");
1649 1652 return NULL;
1650 1653 }
1651 1654 node = index_node_existing(self->nt.index, rev);
1652 1655 if (nt_insert(&self->nt, node, (int)rev) == -1)
1653 1656 return NULL;
1654 1657 Py_RETURN_NONE;
1655 1658 }
1656 1659
1657 1660 static int nt_delete_node(nodetree *self, const char *node)
1658 1661 {
1659 1662 /* rev==-2 happens to get encoded as 0, which is interpreted as not set
1660 1663 */
1661 1664 return nt_insert(self, node, -2);
1662 1665 }
1663 1666
1664 1667 static int nt_init(nodetree *self, indexObject *index, unsigned capacity)
1665 1668 {
1666 1669 /* Initialize before overflow-checking to avoid nt_dealloc() crash. */
1667 1670 self->nodes = NULL;
1668 1671
1669 1672 self->index = index;
1670 1673 /* The input capacity is in terms of revisions, while the field is in
1671 1674 * terms of nodetree nodes. */
1672 1675 self->capacity = (capacity < 4 ? 4 : capacity / 2);
1673 1676 self->depth = 0;
1674 1677 self->splits = 0;
1675 1678 if ((size_t)self->capacity > INT_MAX / sizeof(nodetreenode)) {
1676 1679 PyErr_SetString(PyExc_ValueError, "overflow in init_nt");
1677 1680 return -1;
1678 1681 }
1679 1682 self->nodes = calloc(self->capacity, sizeof(nodetreenode));
1680 1683 if (self->nodes == NULL) {
1681 1684 PyErr_NoMemory();
1682 1685 return -1;
1683 1686 }
1684 1687 self->length = 1;
1685 1688 return 0;
1686 1689 }
1687 1690
1688 1691 static int ntobj_init(nodetreeObject *self, PyObject *args)
1689 1692 {
1690 1693 PyObject *index;
1691 1694 unsigned capacity;
1692 1695 if (!PyArg_ParseTuple(args, "O!I", &HgRevlogIndex_Type, &index,
1693 1696 &capacity))
1694 1697 return -1;
1695 1698 Py_INCREF(index);
1696 1699 return nt_init(&self->nt, (indexObject *)index, capacity);
1697 1700 }
1698 1701
1699 1702 static int nt_partialmatch(nodetree *self, const char *node, Py_ssize_t nodelen)
1700 1703 {
1701 1704 return nt_find(self, node, nodelen, 1);
1702 1705 }
1703 1706
1704 1707 /*
1705 1708 * Find the length of the shortest unique prefix of node.
1706 1709 *
1707 1710 * Return values:
1708 1711 *
1709 1712 * -3: error (exception set)
1710 1713 * -2: not found (no exception set)
1711 1714 * rest: length of shortest prefix
1712 1715 */
1713 1716 static int nt_shortest(nodetree *self, const char *node)
1714 1717 {
1715 1718 int level, off;
1716 1719
1717 1720 for (level = off = 0; level < 40; level++) {
1718 1721 int k, v;
1719 1722 nodetreenode *n = &self->nodes[off];
1720 1723 k = nt_level(node, level);
1721 1724 v = n->children[k];
1722 1725 if (v < 0) {
1723 1726 const char *n;
1724 1727 v = -(v + 2);
1725 1728 n = index_node_existing(self->index, v);
1726 1729 if (n == NULL)
1727 1730 return -3;
1728 1731 if (memcmp(node, n, 20) != 0)
1729 1732 /*
1730 1733 * Found a unique prefix, but it wasn't for the
1731 1734 * requested node (i.e the requested node does
1732 1735 * not exist).
1733 1736 */
1734 1737 return -2;
1735 1738 return level + 1;
1736 1739 }
1737 1740 if (v == 0)
1738 1741 return -2;
1739 1742 off = v;
1740 1743 }
1741 1744 /*
1742 1745 * The node was still not unique after 40 hex digits, so this won't
1743 1746 * happen. Also, if we get here, then there's a programming error in
1744 1747 * this file that made us insert a node longer than 40 hex digits.
1745 1748 */
1746 1749 PyErr_SetString(PyExc_Exception, "broken node tree");
1747 1750 return -3;
1748 1751 }
1749 1752
1750 1753 static PyObject *ntobj_shortest(nodetreeObject *self, PyObject *args)
1751 1754 {
1752 1755 PyObject *val;
1753 1756 char *node;
1754 1757 int length;
1755 1758
1756 1759 if (!PyArg_ParseTuple(args, "O", &val))
1757 1760 return NULL;
1758 1761 if (node_check(val, &node) == -1)
1759 1762 return NULL;
1760 1763
1761 1764 length = nt_shortest(&self->nt, node);
1762 1765 if (length == -3)
1763 1766 return NULL;
1764 1767 if (length == -2) {
1765 1768 raise_revlog_error();
1766 1769 return NULL;
1767 1770 }
1768 1771 return PyInt_FromLong(length);
1769 1772 }
1770 1773
1771 1774 static void nt_dealloc(nodetree *self)
1772 1775 {
1773 1776 free(self->nodes);
1774 1777 self->nodes = NULL;
1775 1778 }
1776 1779
1777 1780 static void ntobj_dealloc(nodetreeObject *self)
1778 1781 {
1779 1782 Py_XDECREF(self->nt.index);
1780 1783 nt_dealloc(&self->nt);
1781 1784 PyObject_Del(self);
1782 1785 }
1783 1786
1784 1787 static PyMethodDef ntobj_methods[] = {
1785 1788 {"insert", (PyCFunction)ntobj_insert, METH_VARARGS,
1786 1789 "insert an index entry"},
1787 1790 {"shortest", (PyCFunction)ntobj_shortest, METH_VARARGS,
1788 1791 "find length of shortest hex nodeid of a binary ID"},
1789 1792 {NULL} /* Sentinel */
1790 1793 };
1791 1794
1792 1795 static PyTypeObject nodetreeType = {
1793 1796 PyVarObject_HEAD_INIT(NULL, 0) /* header */
1794 1797 "parsers.nodetree", /* tp_name */
1795 1798 sizeof(nodetreeObject), /* tp_basicsize */
1796 1799 0, /* tp_itemsize */
1797 1800 (destructor)ntobj_dealloc, /* tp_dealloc */
1798 1801 0, /* tp_print */
1799 1802 0, /* tp_getattr */
1800 1803 0, /* tp_setattr */
1801 1804 0, /* tp_compare */
1802 1805 0, /* tp_repr */
1803 1806 0, /* tp_as_number */
1804 1807 0, /* tp_as_sequence */
1805 1808 0, /* tp_as_mapping */
1806 1809 0, /* tp_hash */
1807 1810 0, /* tp_call */
1808 1811 0, /* tp_str */
1809 1812 0, /* tp_getattro */
1810 1813 0, /* tp_setattro */
1811 1814 0, /* tp_as_buffer */
1812 1815 Py_TPFLAGS_DEFAULT, /* tp_flags */
1813 1816 "nodetree", /* tp_doc */
1814 1817 0, /* tp_traverse */
1815 1818 0, /* tp_clear */
1816 1819 0, /* tp_richcompare */
1817 1820 0, /* tp_weaklistoffset */
1818 1821 0, /* tp_iter */
1819 1822 0, /* tp_iternext */
1820 1823 ntobj_methods, /* tp_methods */
1821 1824 0, /* tp_members */
1822 1825 0, /* tp_getset */
1823 1826 0, /* tp_base */
1824 1827 0, /* tp_dict */
1825 1828 0, /* tp_descr_get */
1826 1829 0, /* tp_descr_set */
1827 1830 0, /* tp_dictoffset */
1828 1831 (initproc)ntobj_init, /* tp_init */
1829 1832 0, /* tp_alloc */
1830 1833 };
1831 1834
1832 1835 static int index_init_nt(indexObject *self)
1833 1836 {
1834 1837 if (!self->ntinitialized) {
1835 1838 if (nt_init(&self->nt, self, (int)self->raw_length) == -1) {
1836 1839 nt_dealloc(&self->nt);
1837 1840 return -1;
1838 1841 }
1839 1842 if (nt_insert(&self->nt, nullid, -1) == -1) {
1840 1843 nt_dealloc(&self->nt);
1841 1844 return -1;
1842 1845 }
1843 1846 self->ntinitialized = 1;
1844 1847 self->ntrev = (int)index_length(self);
1845 1848 self->ntlookups = 1;
1846 1849 self->ntmisses = 0;
1847 1850 }
1848 1851 return 0;
1849 1852 }
1850 1853
1851 1854 /*
1852 1855 * Return values:
1853 1856 *
1854 1857 * -3: error (exception set)
1855 1858 * -2: not found (no exception set)
1856 1859 * rest: valid rev
1857 1860 */
1858 1861 static int index_find_node(indexObject *self, const char *node,
1859 1862 Py_ssize_t nodelen)
1860 1863 {
1861 1864 int rev;
1862 1865
1863 1866 if (index_init_nt(self) == -1)
1864 1867 return -3;
1865 1868
1866 1869 self->ntlookups++;
1867 1870 rev = nt_find(&self->nt, node, nodelen, 0);
1868 1871 if (rev >= -1)
1869 1872 return rev;
1870 1873
1871 1874 /*
1872 1875 * For the first handful of lookups, we scan the entire index,
1873 1876 * and cache only the matching nodes. This optimizes for cases
1874 1877 * like "hg tip", where only a few nodes are accessed.
1875 1878 *
1876 1879 * After that, we cache every node we visit, using a single
1877 1880 * scan amortized over multiple lookups. This gives the best
1878 1881 * bulk performance, e.g. for "hg log".
1879 1882 */
1880 1883 if (self->ntmisses++ < 4) {
1881 1884 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1882 1885 const char *n = index_node_existing(self, rev);
1883 1886 if (n == NULL)
1884 1887 return -3;
1885 1888 if (memcmp(node, n, nodelen > 20 ? 20 : nodelen) == 0) {
1886 1889 if (nt_insert(&self->nt, n, rev) == -1)
1887 1890 return -3;
1888 1891 break;
1889 1892 }
1890 1893 }
1891 1894 } else {
1892 1895 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1893 1896 const char *n = index_node_existing(self, rev);
1894 1897 if (n == NULL)
1895 1898 return -3;
1896 1899 if (nt_insert(&self->nt, n, rev) == -1) {
1897 1900 self->ntrev = rev + 1;
1898 1901 return -3;
1899 1902 }
1900 1903 if (memcmp(node, n, nodelen > 20 ? 20 : nodelen) == 0) {
1901 1904 break;
1902 1905 }
1903 1906 }
1904 1907 self->ntrev = rev;
1905 1908 }
1906 1909
1907 1910 if (rev >= 0)
1908 1911 return rev;
1909 1912 return -2;
1910 1913 }
1911 1914
1912 1915 static PyObject *index_getitem(indexObject *self, PyObject *value)
1913 1916 {
1914 1917 char *node;
1915 1918 int rev;
1916 1919
1917 1920 if (PyInt_Check(value)) {
1918 1921 long idx;
1919 1922 if (!pylong_to_long(value, &idx)) {
1920 1923 return NULL;
1921 1924 }
1922 1925 return index_get(self, idx);
1923 1926 }
1924 1927
1925 1928 if (node_check(value, &node) == -1)
1926 1929 return NULL;
1927 1930 rev = index_find_node(self, node, 20);
1928 1931 if (rev >= -1)
1929 1932 return PyInt_FromLong(rev);
1930 1933 if (rev == -2)
1931 1934 raise_revlog_error();
1932 1935 return NULL;
1933 1936 }
1934 1937
1935 1938 /*
1936 1939 * Fully populate the radix tree.
1937 1940 */
1938 1941 static int index_populate_nt(indexObject *self)
1939 1942 {
1940 1943 int rev;
1941 1944 if (self->ntrev > 0) {
1942 1945 for (rev = self->ntrev - 1; rev >= 0; rev--) {
1943 1946 const char *n = index_node_existing(self, rev);
1944 1947 if (n == NULL)
1945 1948 return -1;
1946 1949 if (nt_insert(&self->nt, n, rev) == -1)
1947 1950 return -1;
1948 1951 }
1949 1952 self->ntrev = -1;
1950 1953 }
1951 1954 return 0;
1952 1955 }
1953 1956
1954 1957 static PyObject *index_partialmatch(indexObject *self, PyObject *args)
1955 1958 {
1956 1959 const char *fullnode;
1957 1960 Py_ssize_t nodelen;
1958 1961 char *node;
1959 1962 int rev, i;
1960 1963
1961 1964 if (!PyArg_ParseTuple(args, PY23("s#", "y#"), &node, &nodelen))
1962 1965 return NULL;
1963 1966
1964 1967 if (nodelen < 1) {
1965 1968 PyErr_SetString(PyExc_ValueError, "key too short");
1966 1969 return NULL;
1967 1970 }
1968 1971
1969 1972 if (nodelen > 40) {
1970 1973 PyErr_SetString(PyExc_ValueError, "key too long");
1971 1974 return NULL;
1972 1975 }
1973 1976
1974 1977 for (i = 0; i < nodelen; i++)
1975 1978 hexdigit(node, i);
1976 1979 if (PyErr_Occurred()) {
1977 1980 /* input contains non-hex characters */
1978 1981 PyErr_Clear();
1979 1982 Py_RETURN_NONE;
1980 1983 }
1981 1984
1982 1985 if (index_init_nt(self) == -1)
1983 1986 return NULL;
1984 1987 if (index_populate_nt(self) == -1)
1985 1988 return NULL;
1986 1989 rev = nt_partialmatch(&self->nt, node, nodelen);
1987 1990
1988 1991 switch (rev) {
1989 1992 case -4:
1990 1993 raise_revlog_error();
1991 1994 return NULL;
1992 1995 case -2:
1993 1996 Py_RETURN_NONE;
1994 1997 case -1:
1995 1998 return PyBytes_FromStringAndSize(nullid, 20);
1996 1999 }
1997 2000
1998 2001 fullnode = index_node_existing(self, rev);
1999 2002 if (fullnode == NULL) {
2000 2003 return NULL;
2001 2004 }
2002 2005 return PyBytes_FromStringAndSize(fullnode, 20);
2003 2006 }
2004 2007
2005 2008 static PyObject *index_shortest(indexObject *self, PyObject *args)
2006 2009 {
2007 2010 PyObject *val;
2008 2011 char *node;
2009 2012 int length;
2010 2013
2011 2014 if (!PyArg_ParseTuple(args, "O", &val))
2012 2015 return NULL;
2013 2016 if (node_check(val, &node) == -1)
2014 2017 return NULL;
2015 2018
2016 2019 self->ntlookups++;
2017 2020 if (index_init_nt(self) == -1)
2018 2021 return NULL;
2019 2022 if (index_populate_nt(self) == -1)
2020 2023 return NULL;
2021 2024 length = nt_shortest(&self->nt, node);
2022 2025 if (length == -3)
2023 2026 return NULL;
2024 2027 if (length == -2) {
2025 2028 raise_revlog_error();
2026 2029 return NULL;
2027 2030 }
2028 2031 return PyInt_FromLong(length);
2029 2032 }
2030 2033
2031 2034 static PyObject *index_m_get(indexObject *self, PyObject *args)
2032 2035 {
2033 2036 PyObject *val;
2034 2037 char *node;
2035 2038 int rev;
2036 2039
2037 2040 if (!PyArg_ParseTuple(args, "O", &val))
2038 2041 return NULL;
2039 2042 if (node_check(val, &node) == -1)
2040 2043 return NULL;
2041 2044 rev = index_find_node(self, node, 20);
2042 2045 if (rev == -3)
2043 2046 return NULL;
2044 2047 if (rev == -2)
2045 2048 Py_RETURN_NONE;
2046 2049 return PyInt_FromLong(rev);
2047 2050 }
2048 2051
2049 2052 static int index_contains(indexObject *self, PyObject *value)
2050 2053 {
2051 2054 char *node;
2052 2055
2053 2056 if (PyInt_Check(value)) {
2054 2057 long rev;
2055 2058 if (!pylong_to_long(value, &rev)) {
2056 2059 return -1;
2057 2060 }
2058 2061 return rev >= -1 && rev < index_length(self);
2059 2062 }
2060 2063
2061 2064 if (node_check(value, &node) == -1)
2062 2065 return -1;
2063 2066
2064 2067 switch (index_find_node(self, node, 20)) {
2065 2068 case -3:
2066 2069 return -1;
2067 2070 case -2:
2068 2071 return 0;
2069 2072 default:
2070 2073 return 1;
2071 2074 }
2072 2075 }
2073 2076
2074 2077 static PyObject *index_m_has_node(indexObject *self, PyObject *args)
2075 2078 {
2076 2079 int ret = index_contains(self, args);
2077 2080 if (ret < 0)
2078 2081 return NULL;
2079 2082 return PyBool_FromLong((long)ret);
2080 2083 }
2081 2084
2082 2085 static PyObject *index_m_rev(indexObject *self, PyObject *val)
2083 2086 {
2084 2087 char *node;
2085 2088 int rev;
2086 2089
2087 2090 if (node_check(val, &node) == -1)
2088 2091 return NULL;
2089 2092 rev = index_find_node(self, node, 20);
2090 2093 if (rev >= -1)
2091 2094 return PyInt_FromLong(rev);
2092 2095 if (rev == -2)
2093 2096 raise_revlog_error();
2094 2097 return NULL;
2095 2098 }
2096 2099
2097 2100 typedef uint64_t bitmask;
2098 2101
2099 2102 /*
2100 2103 * Given a disjoint set of revs, return all candidates for the
2101 2104 * greatest common ancestor. In revset notation, this is the set
2102 2105 * "heads(::a and ::b and ...)"
2103 2106 */
2104 2107 static PyObject *find_gca_candidates(indexObject *self, const int *revs,
2105 2108 int revcount)
2106 2109 {
2107 2110 const bitmask allseen = (1ull << revcount) - 1;
2108 2111 const bitmask poison = 1ull << revcount;
2109 2112 PyObject *gca = PyList_New(0);
2110 2113 int i, v, interesting;
2111 2114 int maxrev = -1;
2112 2115 bitmask sp;
2113 2116 bitmask *seen;
2114 2117
2115 2118 if (gca == NULL)
2116 2119 return PyErr_NoMemory();
2117 2120
2118 2121 for (i = 0; i < revcount; i++) {
2119 2122 if (revs[i] > maxrev)
2120 2123 maxrev = revs[i];
2121 2124 }
2122 2125
2123 2126 seen = calloc(sizeof(*seen), maxrev + 1);
2124 2127 if (seen == NULL) {
2125 2128 Py_DECREF(gca);
2126 2129 return PyErr_NoMemory();
2127 2130 }
2128 2131
2129 2132 for (i = 0; i < revcount; i++)
2130 2133 seen[revs[i]] = 1ull << i;
2131 2134
2132 2135 interesting = revcount;
2133 2136
2134 2137 for (v = maxrev; v >= 0 && interesting; v--) {
2135 2138 bitmask sv = seen[v];
2136 2139 int parents[2];
2137 2140
2138 2141 if (!sv)
2139 2142 continue;
2140 2143
2141 2144 if (sv < poison) {
2142 2145 interesting -= 1;
2143 2146 if (sv == allseen) {
2144 2147 PyObject *obj = PyInt_FromLong(v);
2145 2148 if (obj == NULL)
2146 2149 goto bail;
2147 2150 if (PyList_Append(gca, obj) == -1) {
2148 2151 Py_DECREF(obj);
2149 2152 goto bail;
2150 2153 }
2151 2154 sv |= poison;
2152 2155 for (i = 0; i < revcount; i++) {
2153 2156 if (revs[i] == v)
2154 2157 goto done;
2155 2158 }
2156 2159 }
2157 2160 }
2158 2161 if (index_get_parents(self, v, parents, maxrev) < 0)
2159 2162 goto bail;
2160 2163
2161 2164 for (i = 0; i < 2; i++) {
2162 2165 int p = parents[i];
2163 2166 if (p == -1)
2164 2167 continue;
2165 2168 sp = seen[p];
2166 2169 if (sv < poison) {
2167 2170 if (sp == 0) {
2168 2171 seen[p] = sv;
2169 2172 interesting++;
2170 2173 } else if (sp != sv)
2171 2174 seen[p] |= sv;
2172 2175 } else {
2173 2176 if (sp && sp < poison)
2174 2177 interesting--;
2175 2178 seen[p] = sv;
2176 2179 }
2177 2180 }
2178 2181 }
2179 2182
2180 2183 done:
2181 2184 free(seen);
2182 2185 return gca;
2183 2186 bail:
2184 2187 free(seen);
2185 2188 Py_XDECREF(gca);
2186 2189 return NULL;
2187 2190 }
2188 2191
2189 2192 /*
2190 2193 * Given a disjoint set of revs, return the subset with the longest
2191 2194 * path to the root.
2192 2195 */
2193 2196 static PyObject *find_deepest(indexObject *self, PyObject *revs)
2194 2197 {
2195 2198 const Py_ssize_t revcount = PyList_GET_SIZE(revs);
2196 2199 static const Py_ssize_t capacity = 24;
2197 2200 int *depth, *interesting = NULL;
2198 2201 int i, j, v, ninteresting;
2199 2202 PyObject *dict = NULL, *keys = NULL;
2200 2203 long *seen = NULL;
2201 2204 int maxrev = -1;
2202 2205 long final;
2203 2206
2204 2207 if (revcount > capacity) {
2205 2208 PyErr_Format(PyExc_OverflowError,
2206 2209 "bitset size (%ld) > capacity (%ld)",
2207 2210 (long)revcount, (long)capacity);
2208 2211 return NULL;
2209 2212 }
2210 2213
2211 2214 for (i = 0; i < revcount; i++) {
2212 2215 int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
2213 2216 if (n > maxrev)
2214 2217 maxrev = n;
2215 2218 }
2216 2219
2217 2220 depth = calloc(sizeof(*depth), maxrev + 1);
2218 2221 if (depth == NULL)
2219 2222 return PyErr_NoMemory();
2220 2223
2221 2224 seen = calloc(sizeof(*seen), maxrev + 1);
2222 2225 if (seen == NULL) {
2223 2226 PyErr_NoMemory();
2224 2227 goto bail;
2225 2228 }
2226 2229
2227 2230 interesting = calloc(sizeof(*interesting), ((size_t)1) << revcount);
2228 2231 if (interesting == NULL) {
2229 2232 PyErr_NoMemory();
2230 2233 goto bail;
2231 2234 }
2232 2235
2233 2236 if (PyList_Sort(revs) == -1)
2234 2237 goto bail;
2235 2238
2236 2239 for (i = 0; i < revcount; i++) {
2237 2240 int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
2238 2241 long b = 1l << i;
2239 2242 depth[n] = 1;
2240 2243 seen[n] = b;
2241 2244 interesting[b] = 1;
2242 2245 }
2243 2246
2244 2247 /* invariant: ninteresting is the number of non-zero entries in
2245 2248 * interesting. */
2246 2249 ninteresting = (int)revcount;
2247 2250
2248 2251 for (v = maxrev; v >= 0 && ninteresting > 1; v--) {
2249 2252 int dv = depth[v];
2250 2253 int parents[2];
2251 2254 long sv;
2252 2255
2253 2256 if (dv == 0)
2254 2257 continue;
2255 2258
2256 2259 sv = seen[v];
2257 2260 if (index_get_parents(self, v, parents, maxrev) < 0)
2258 2261 goto bail;
2259 2262
2260 2263 for (i = 0; i < 2; i++) {
2261 2264 int p = parents[i];
2262 2265 long sp;
2263 2266 int dp;
2264 2267
2265 2268 if (p == -1)
2266 2269 continue;
2267 2270
2268 2271 dp = depth[p];
2269 2272 sp = seen[p];
2270 2273 if (dp <= dv) {
2271 2274 depth[p] = dv + 1;
2272 2275 if (sp != sv) {
2273 2276 interesting[sv] += 1;
2274 2277 seen[p] = sv;
2275 2278 if (sp) {
2276 2279 interesting[sp] -= 1;
2277 2280 if (interesting[sp] == 0)
2278 2281 ninteresting -= 1;
2279 2282 }
2280 2283 }
2281 2284 } else if (dv == dp - 1) {
2282 2285 long nsp = sp | sv;
2283 2286 if (nsp == sp)
2284 2287 continue;
2285 2288 seen[p] = nsp;
2286 2289 interesting[sp] -= 1;
2287 2290 if (interesting[sp] == 0)
2288 2291 ninteresting -= 1;
2289 2292 if (interesting[nsp] == 0)
2290 2293 ninteresting += 1;
2291 2294 interesting[nsp] += 1;
2292 2295 }
2293 2296 }
2294 2297 interesting[sv] -= 1;
2295 2298 if (interesting[sv] == 0)
2296 2299 ninteresting -= 1;
2297 2300 }
2298 2301
2299 2302 final = 0;
2300 2303 j = ninteresting;
2301 2304 for (i = 0; i < (int)(2 << revcount) && j > 0; i++) {
2302 2305 if (interesting[i] == 0)
2303 2306 continue;
2304 2307 final |= i;
2305 2308 j -= 1;
2306 2309 }
2307 2310 if (final == 0) {
2308 2311 keys = PyList_New(0);
2309 2312 goto bail;
2310 2313 }
2311 2314
2312 2315 dict = PyDict_New();
2313 2316 if (dict == NULL)
2314 2317 goto bail;
2315 2318
2316 2319 for (i = 0; i < revcount; i++) {
2317 2320 PyObject *key;
2318 2321
2319 2322 if ((final & (1 << i)) == 0)
2320 2323 continue;
2321 2324
2322 2325 key = PyList_GET_ITEM(revs, i);
2323 2326 Py_INCREF(key);
2324 2327 Py_INCREF(Py_None);
2325 2328 if (PyDict_SetItem(dict, key, Py_None) == -1) {
2326 2329 Py_DECREF(key);
2327 2330 Py_DECREF(Py_None);
2328 2331 goto bail;
2329 2332 }
2330 2333 }
2331 2334
2332 2335 keys = PyDict_Keys(dict);
2333 2336
2334 2337 bail:
2335 2338 free(depth);
2336 2339 free(seen);
2337 2340 free(interesting);
2338 2341 Py_XDECREF(dict);
2339 2342
2340 2343 return keys;
2341 2344 }
2342 2345
2343 2346 /*
2344 2347 * Given a (possibly overlapping) set of revs, return all the
2345 2348 * common ancestors heads: heads(::args[0] and ::a[1] and ...)
2346 2349 */
2347 2350 static PyObject *index_commonancestorsheads(indexObject *self, PyObject *args)
2348 2351 {
2349 2352 PyObject *ret = NULL;
2350 2353 Py_ssize_t argcount, i, len;
2351 2354 bitmask repeat = 0;
2352 2355 int revcount = 0;
2353 2356 int *revs;
2354 2357
2355 2358 argcount = PySequence_Length(args);
2356 2359 revs = PyMem_Malloc(argcount * sizeof(*revs));
2357 2360 if (argcount > 0 && revs == NULL)
2358 2361 return PyErr_NoMemory();
2359 2362 len = index_length(self);
2360 2363
2361 2364 for (i = 0; i < argcount; i++) {
2362 2365 static const int capacity = 24;
2363 2366 PyObject *obj = PySequence_GetItem(args, i);
2364 2367 bitmask x;
2365 2368 long val;
2366 2369
2367 2370 if (!PyInt_Check(obj)) {
2368 2371 PyErr_SetString(PyExc_TypeError,
2369 2372 "arguments must all be ints");
2370 2373 Py_DECREF(obj);
2371 2374 goto bail;
2372 2375 }
2373 2376 val = PyInt_AsLong(obj);
2374 2377 Py_DECREF(obj);
2375 2378 if (val == -1) {
2376 2379 ret = PyList_New(0);
2377 2380 goto done;
2378 2381 }
2379 2382 if (val < 0 || val >= len) {
2380 2383 PyErr_SetString(PyExc_IndexError, "index out of range");
2381 2384 goto bail;
2382 2385 }
2383 2386 /* this cheesy bloom filter lets us avoid some more
2384 2387 * expensive duplicate checks in the common set-is-disjoint
2385 2388 * case */
2386 2389 x = 1ull << (val & 0x3f);
2387 2390 if (repeat & x) {
2388 2391 int k;
2389 2392 for (k = 0; k < revcount; k++) {
2390 2393 if (val == revs[k])
2391 2394 goto duplicate;
2392 2395 }
2393 2396 } else
2394 2397 repeat |= x;
2395 2398 if (revcount >= capacity) {
2396 2399 PyErr_Format(PyExc_OverflowError,
2397 2400 "bitset size (%d) > capacity (%d)",
2398 2401 revcount, capacity);
2399 2402 goto bail;
2400 2403 }
2401 2404 revs[revcount++] = (int)val;
2402 2405 duplicate:;
2403 2406 }
2404 2407
2405 2408 if (revcount == 0) {
2406 2409 ret = PyList_New(0);
2407 2410 goto done;
2408 2411 }
2409 2412 if (revcount == 1) {
2410 2413 PyObject *obj;
2411 2414 ret = PyList_New(1);
2412 2415 if (ret == NULL)
2413 2416 goto bail;
2414 2417 obj = PyInt_FromLong(revs[0]);
2415 2418 if (obj == NULL)
2416 2419 goto bail;
2417 2420 PyList_SET_ITEM(ret, 0, obj);
2418 2421 goto done;
2419 2422 }
2420 2423
2421 2424 ret = find_gca_candidates(self, revs, revcount);
2422 2425 if (ret == NULL)
2423 2426 goto bail;
2424 2427
2425 2428 done:
2426 2429 PyMem_Free(revs);
2427 2430 return ret;
2428 2431
2429 2432 bail:
2430 2433 PyMem_Free(revs);
2431 2434 Py_XDECREF(ret);
2432 2435 return NULL;
2433 2436 }
2434 2437
2435 2438 /*
2436 2439 * Given a (possibly overlapping) set of revs, return the greatest
2437 2440 * common ancestors: those with the longest path to the root.
2438 2441 */
2439 2442 static PyObject *index_ancestors(indexObject *self, PyObject *args)
2440 2443 {
2441 2444 PyObject *ret;
2442 2445 PyObject *gca = index_commonancestorsheads(self, args);
2443 2446 if (gca == NULL)
2444 2447 return NULL;
2445 2448
2446 2449 if (PyList_GET_SIZE(gca) <= 1) {
2447 2450 return gca;
2448 2451 }
2449 2452
2450 2453 ret = find_deepest(self, gca);
2451 2454 Py_DECREF(gca);
2452 2455 return ret;
2453 2456 }
2454 2457
2455 2458 /*
2456 2459 * Invalidate any trie entries introduced by added revs.
2457 2460 */
2458 2461 static void index_invalidate_added(indexObject *self, Py_ssize_t start)
2459 2462 {
2460 2463 Py_ssize_t i, len = PyList_GET_SIZE(self->added);
2461 2464
2462 2465 for (i = start; i < len; i++) {
2463 2466 PyObject *tuple = PyList_GET_ITEM(self->added, i);
2464 2467 PyObject *node = PyTuple_GET_ITEM(tuple, 7);
2465 2468
2466 2469 nt_delete_node(&self->nt, PyBytes_AS_STRING(node));
2467 2470 }
2468 2471
2469 2472 if (start == 0)
2470 2473 Py_CLEAR(self->added);
2471 2474 }
2472 2475
2473 2476 /*
2474 2477 * Delete a numeric range of revs, which must be at the end of the
2475 2478 * range.
2476 2479 */
2477 2480 static int index_slice_del(indexObject *self, PyObject *item)
2478 2481 {
2479 2482 Py_ssize_t start, stop, step, slicelength;
2480 2483 Py_ssize_t length = index_length(self) + 1;
2481 2484 int ret = 0;
2482 2485
2483 2486 /* Argument changed from PySliceObject* to PyObject* in Python 3. */
2484 2487 #ifdef IS_PY3K
2485 2488 if (PySlice_GetIndicesEx(item, length, &start, &stop, &step,
2486 2489 &slicelength) < 0)
2487 2490 #else
2488 2491 if (PySlice_GetIndicesEx((PySliceObject *)item, length, &start, &stop,
2489 2492 &step, &slicelength) < 0)
2490 2493 #endif
2491 2494 return -1;
2492 2495
2493 2496 if (slicelength <= 0)
2494 2497 return 0;
2495 2498
2496 2499 if ((step < 0 && start < stop) || (step > 0 && start > stop))
2497 2500 stop = start;
2498 2501
2499 2502 if (step < 0) {
2500 2503 stop = start + 1;
2501 2504 start = stop + step * (slicelength - 1) - 1;
2502 2505 step = -step;
2503 2506 }
2504 2507
2505 2508 if (step != 1) {
2506 2509 PyErr_SetString(PyExc_ValueError,
2507 2510 "revlog index delete requires step size of 1");
2508 2511 return -1;
2509 2512 }
2510 2513
2511 2514 if (stop != length - 1) {
2512 2515 PyErr_SetString(PyExc_IndexError,
2513 2516 "revlog index deletion indices are invalid");
2514 2517 return -1;
2515 2518 }
2516 2519
2517 2520 if (start < self->length) {
2518 2521 if (self->ntinitialized) {
2519 2522 Py_ssize_t i;
2520 2523
2521 2524 for (i = start; i < self->length; i++) {
2522 2525 const char *node = index_node_existing(self, i);
2523 2526 if (node == NULL)
2524 2527 return -1;
2525 2528
2526 2529 nt_delete_node(&self->nt, node);
2527 2530 }
2528 2531 if (self->added)
2529 2532 index_invalidate_added(self, 0);
2530 2533 if (self->ntrev > start)
2531 2534 self->ntrev = (int)start;
2532 2535 } else if (self->added) {
2533 2536 Py_CLEAR(self->added);
2534 2537 }
2535 2538
2536 2539 self->length = start;
2537 2540 if (start < self->raw_length) {
2538 2541 if (self->cache) {
2539 2542 Py_ssize_t i;
2540 2543 for (i = start; i < self->raw_length; i++)
2541 2544 Py_CLEAR(self->cache[i]);
2542 2545 }
2543 2546 self->raw_length = start;
2544 2547 }
2545 2548 goto done;
2546 2549 }
2547 2550
2548 2551 if (self->ntinitialized) {
2549 2552 index_invalidate_added(self, start - self->length);
2550 2553 if (self->ntrev > start)
2551 2554 self->ntrev = (int)start;
2552 2555 }
2553 2556 if (self->added)
2554 2557 ret = PyList_SetSlice(self->added, start - self->length,
2555 2558 PyList_GET_SIZE(self->added), NULL);
2556 2559 done:
2557 2560 Py_CLEAR(self->headrevs);
2558 2561 return ret;
2559 2562 }
2560 2563
2561 2564 /*
2562 2565 * Supported ops:
2563 2566 *
2564 2567 * slice deletion
2565 2568 * string assignment (extend node->rev mapping)
2566 2569 * string deletion (shrink node->rev mapping)
2567 2570 */
2568 2571 static int index_assign_subscript(indexObject *self, PyObject *item,
2569 2572 PyObject *value)
2570 2573 {
2571 2574 char *node;
2572 2575 long rev;
2573 2576
2574 2577 if (PySlice_Check(item) && value == NULL)
2575 2578 return index_slice_del(self, item);
2576 2579
2577 2580 if (node_check(item, &node) == -1)
2578 2581 return -1;
2579 2582
2580 2583 if (value == NULL)
2581 2584 return self->ntinitialized ? nt_delete_node(&self->nt, node)
2582 2585 : 0;
2583 2586 rev = PyInt_AsLong(value);
2584 2587 if (rev > INT_MAX || rev < 0) {
2585 2588 if (!PyErr_Occurred())
2586 2589 PyErr_SetString(PyExc_ValueError, "rev out of range");
2587 2590 return -1;
2588 2591 }
2589 2592
2590 2593 if (index_init_nt(self) == -1)
2591 2594 return -1;
2592 2595 return nt_insert(&self->nt, node, (int)rev);
2593 2596 }
2594 2597
2595 2598 /*
2596 2599 * Find all RevlogNG entries in an index that has inline data. Update
2597 2600 * the optional "offsets" table with those entries.
2598 2601 */
2599 2602 static Py_ssize_t inline_scan(indexObject *self, const char **offsets)
2600 2603 {
2601 2604 const char *data = (const char *)self->buf.buf;
2602 2605 Py_ssize_t pos = 0;
2603 2606 Py_ssize_t end = self->buf.len;
2604 2607 long incr = v1_hdrsize;
2605 2608 Py_ssize_t len = 0;
2606 2609
2607 2610 while (pos + v1_hdrsize <= end && pos >= 0) {
2608 2611 uint32_t comp_len;
2609 2612 /* 3rd element of header is length of compressed inline data */
2610 2613 comp_len = getbe32(data + pos + 8);
2611 2614 incr = v1_hdrsize + comp_len;
2612 2615 if (offsets)
2613 2616 offsets[len] = data + pos;
2614 2617 len++;
2615 2618 pos += incr;
2616 2619 }
2617 2620
2618 2621 if (pos != end) {
2619 2622 if (!PyErr_Occurred())
2620 2623 PyErr_SetString(PyExc_ValueError, "corrupt index file");
2621 2624 return -1;
2622 2625 }
2623 2626
2624 2627 return len;
2625 2628 }
2626 2629
2627 2630 static int index_init(indexObject *self, PyObject *args)
2628 2631 {
2629 2632 PyObject *data_obj, *inlined_obj;
2630 2633 Py_ssize_t size;
2631 2634
2632 2635 /* Initialize before argument-checking to avoid index_dealloc() crash.
2633 2636 */
2634 2637 self->raw_length = 0;
2635 2638 self->added = NULL;
2636 2639 self->cache = NULL;
2637 2640 self->data = NULL;
2638 2641 memset(&self->buf, 0, sizeof(self->buf));
2639 2642 self->headrevs = NULL;
2640 2643 self->filteredrevs = Py_None;
2641 2644 Py_INCREF(Py_None);
2642 2645 self->ntinitialized = 0;
2643 2646 self->offsets = NULL;
2644 2647
2645 2648 if (!PyArg_ParseTuple(args, "OO", &data_obj, &inlined_obj))
2646 2649 return -1;
2647 2650 if (!PyObject_CheckBuffer(data_obj)) {
2648 2651 PyErr_SetString(PyExc_TypeError,
2649 2652 "data does not support buffer interface");
2650 2653 return -1;
2651 2654 }
2652 2655
2653 2656 if (PyObject_GetBuffer(data_obj, &self->buf, PyBUF_SIMPLE) == -1)
2654 2657 return -1;
2655 2658 size = self->buf.len;
2656 2659
2657 2660 self->inlined = inlined_obj && PyObject_IsTrue(inlined_obj);
2658 2661 self->data = data_obj;
2659 2662
2660 2663 self->ntlookups = self->ntmisses = 0;
2661 2664 self->ntrev = -1;
2662 2665 Py_INCREF(self->data);
2663 2666
2664 2667 if (self->inlined) {
2665 2668 Py_ssize_t len = inline_scan(self, NULL);
2666 2669 if (len == -1)
2667 2670 goto bail;
2668 2671 self->raw_length = len;
2669 2672 self->length = len;
2670 2673 } else {
2671 2674 if (size % v1_hdrsize) {
2672 2675 PyErr_SetString(PyExc_ValueError, "corrupt index file");
2673 2676 goto bail;
2674 2677 }
2675 2678 self->raw_length = size / v1_hdrsize;
2676 2679 self->length = self->raw_length;
2677 2680 }
2678 2681
2679 2682 return 0;
2680 2683 bail:
2681 2684 return -1;
2682 2685 }
2683 2686
2684 2687 static PyObject *index_nodemap(indexObject *self)
2685 2688 {
2686 2689 Py_INCREF(self);
2687 2690 return (PyObject *)self;
2688 2691 }
2689 2692
2690 2693 static void _index_clearcaches(indexObject *self)
2691 2694 {
2692 2695 if (self->cache) {
2693 2696 Py_ssize_t i;
2694 2697
2695 2698 for (i = 0; i < self->raw_length; i++)
2696 2699 Py_CLEAR(self->cache[i]);
2697 2700 free(self->cache);
2698 2701 self->cache = NULL;
2699 2702 }
2700 2703 if (self->offsets) {
2701 2704 PyMem_Free((void *)self->offsets);
2702 2705 self->offsets = NULL;
2703 2706 }
2704 2707 if (self->ntinitialized) {
2705 2708 nt_dealloc(&self->nt);
2706 2709 }
2707 2710 self->ntinitialized = 0;
2708 2711 Py_CLEAR(self->headrevs);
2709 2712 }
2710 2713
2711 2714 static PyObject *index_clearcaches(indexObject *self)
2712 2715 {
2713 2716 _index_clearcaches(self);
2714 2717 self->ntrev = -1;
2715 2718 self->ntlookups = self->ntmisses = 0;
2716 2719 Py_RETURN_NONE;
2717 2720 }
2718 2721
2719 2722 static void index_dealloc(indexObject *self)
2720 2723 {
2721 2724 _index_clearcaches(self);
2722 2725 Py_XDECREF(self->filteredrevs);
2723 2726 if (self->buf.buf) {
2724 2727 PyBuffer_Release(&self->buf);
2725 2728 memset(&self->buf, 0, sizeof(self->buf));
2726 2729 }
2727 2730 Py_XDECREF(self->data);
2728 2731 Py_XDECREF(self->added);
2729 2732 PyObject_Del(self);
2730 2733 }
2731 2734
2732 2735 static PySequenceMethods index_sequence_methods = {
2733 2736 (lenfunc)index_length, /* sq_length */
2734 2737 0, /* sq_concat */
2735 2738 0, /* sq_repeat */
2736 2739 (ssizeargfunc)index_get, /* sq_item */
2737 2740 0, /* sq_slice */
2738 2741 0, /* sq_ass_item */
2739 2742 0, /* sq_ass_slice */
2740 2743 (objobjproc)index_contains, /* sq_contains */
2741 2744 };
2742 2745
2743 2746 static PyMappingMethods index_mapping_methods = {
2744 2747 (lenfunc)index_length, /* mp_length */
2745 2748 (binaryfunc)index_getitem, /* mp_subscript */
2746 2749 (objobjargproc)index_assign_subscript, /* mp_ass_subscript */
2747 2750 };
2748 2751
2749 2752 static PyMethodDef index_methods[] = {
2750 2753 {"ancestors", (PyCFunction)index_ancestors, METH_VARARGS,
2751 2754 "return the gca set of the given revs"},
2752 2755 {"commonancestorsheads", (PyCFunction)index_commonancestorsheads,
2753 2756 METH_VARARGS,
2754 2757 "return the heads of the common ancestors of the given revs"},
2755 2758 {"clearcaches", (PyCFunction)index_clearcaches, METH_NOARGS,
2756 2759 "clear the index caches"},
2757 2760 {"get", (PyCFunction)index_m_get, METH_VARARGS, "get an index entry"},
2758 2761 {"get_rev", (PyCFunction)index_m_get, METH_VARARGS,
2759 2762 "return `rev` associated with a node or None"},
2760 2763 {"has_node", (PyCFunction)index_m_has_node, METH_O,
2761 2764 "return True if the node exist in the index"},
2762 2765 {"rev", (PyCFunction)index_m_rev, METH_O,
2763 2766 "return `rev` associated with a node or raise RevlogError"},
2764 2767 {"computephasesmapsets", (PyCFunction)compute_phases_map_sets, METH_VARARGS,
2765 2768 "compute phases"},
2766 2769 {"reachableroots2", (PyCFunction)reachableroots2, METH_VARARGS,
2767 2770 "reachableroots"},
2768 2771 {"headrevs", (PyCFunction)index_headrevs, METH_VARARGS,
2769 2772 "get head revisions"}, /* Can do filtering since 3.2 */
2770 2773 {"headrevsfiltered", (PyCFunction)index_headrevs, METH_VARARGS,
2771 2774 "get filtered head revisions"}, /* Can always do filtering */
2772 2775 {"issnapshot", (PyCFunction)index_issnapshot, METH_O,
2773 2776 "True if the object is a snapshot"},
2774 2777 {"findsnapshots", (PyCFunction)index_findsnapshots, METH_VARARGS,
2775 2778 "Gather snapshot data in a cache dict"},
2776 2779 {"deltachain", (PyCFunction)index_deltachain, METH_VARARGS,
2777 2780 "determine revisions with deltas to reconstruct fulltext"},
2778 2781 {"slicechunktodensity", (PyCFunction)index_slicechunktodensity,
2779 2782 METH_VARARGS, "determine revisions with deltas to reconstruct fulltext"},
2780 2783 {"append", (PyCFunction)index_append, METH_O, "append an index entry"},
2781 2784 {"partialmatch", (PyCFunction)index_partialmatch, METH_VARARGS,
2782 2785 "match a potentially ambiguous node ID"},
2783 2786 {"shortest", (PyCFunction)index_shortest, METH_VARARGS,
2784 2787 "find length of shortest hex nodeid of a binary ID"},
2785 2788 {"stats", (PyCFunction)index_stats, METH_NOARGS, "stats for the index"},
2786 2789 {NULL} /* Sentinel */
2787 2790 };
2788 2791
2789 2792 static PyGetSetDef index_getset[] = {
2790 2793 {"nodemap", (getter)index_nodemap, NULL, "nodemap", NULL},
2791 2794 {NULL} /* Sentinel */
2792 2795 };
2793 2796
2794 2797 PyTypeObject HgRevlogIndex_Type = {
2795 2798 PyVarObject_HEAD_INIT(NULL, 0) /* header */
2796 2799 "parsers.index", /* tp_name */
2797 2800 sizeof(indexObject), /* tp_basicsize */
2798 2801 0, /* tp_itemsize */
2799 2802 (destructor)index_dealloc, /* tp_dealloc */
2800 2803 0, /* tp_print */
2801 2804 0, /* tp_getattr */
2802 2805 0, /* tp_setattr */
2803 2806 0, /* tp_compare */
2804 2807 0, /* tp_repr */
2805 2808 0, /* tp_as_number */
2806 2809 &index_sequence_methods, /* tp_as_sequence */
2807 2810 &index_mapping_methods, /* tp_as_mapping */
2808 2811 0, /* tp_hash */
2809 2812 0, /* tp_call */
2810 2813 0, /* tp_str */
2811 2814 0, /* tp_getattro */
2812 2815 0, /* tp_setattro */
2813 2816 0, /* tp_as_buffer */
2814 2817 Py_TPFLAGS_DEFAULT, /* tp_flags */
2815 2818 "revlog index", /* tp_doc */
2816 2819 0, /* tp_traverse */
2817 2820 0, /* tp_clear */
2818 2821 0, /* tp_richcompare */
2819 2822 0, /* tp_weaklistoffset */
2820 2823 0, /* tp_iter */
2821 2824 0, /* tp_iternext */
2822 2825 index_methods, /* tp_methods */
2823 2826 0, /* tp_members */
2824 2827 index_getset, /* tp_getset */
2825 2828 0, /* tp_base */
2826 2829 0, /* tp_dict */
2827 2830 0, /* tp_descr_get */
2828 2831 0, /* tp_descr_set */
2829 2832 0, /* tp_dictoffset */
2830 2833 (initproc)index_init, /* tp_init */
2831 2834 0, /* tp_alloc */
2832 2835 };
2833 2836
2834 2837 /*
2835 2838 * returns a tuple of the form (index, index, cache) with elements as
2836 2839 * follows:
2837 2840 *
2838 2841 * index: an index object that lazily parses RevlogNG records
2839 2842 * cache: if data is inlined, a tuple (0, index_file_content), else None
2840 2843 * index_file_content could be a string, or a buffer
2841 2844 *
2842 2845 * added complications are for backwards compatibility
2843 2846 */
2844 2847 PyObject *parse_index2(PyObject *self, PyObject *args)
2845 2848 {
2846 2849 PyObject *tuple = NULL, *cache = NULL;
2847 2850 indexObject *idx;
2848 2851 int ret;
2849 2852
2850 2853 idx = PyObject_New(indexObject, &HgRevlogIndex_Type);
2851 2854 if (idx == NULL)
2852 2855 goto bail;
2853 2856
2854 2857 ret = index_init(idx, args);
2855 2858 if (ret == -1)
2856 2859 goto bail;
2857 2860
2858 2861 if (idx->inlined) {
2859 2862 cache = Py_BuildValue("iO", 0, idx->data);
2860 2863 if (cache == NULL)
2861 2864 goto bail;
2862 2865 } else {
2863 2866 cache = Py_None;
2864 2867 Py_INCREF(cache);
2865 2868 }
2866 2869
2867 2870 tuple = Py_BuildValue("NN", idx, cache);
2868 2871 if (!tuple)
2869 2872 goto bail;
2870 2873 return tuple;
2871 2874
2872 2875 bail:
2873 2876 Py_XDECREF(idx);
2874 2877 Py_XDECREF(cache);
2875 2878 Py_XDECREF(tuple);
2876 2879 return NULL;
2877 2880 }
2878 2881
2879 2882 static Revlog_CAPI CAPI = {
2880 2883 /* increment the abi_version field upon each change in the Revlog_CAPI
2881 2884 struct or in the ABI of the listed functions */
2882 2885 2,
2883 2886 index_length,
2884 2887 index_node,
2885 2888 HgRevlogIndex_GetParents,
2886 2889 };
2887 2890
2888 2891 void revlog_module_init(PyObject *mod)
2889 2892 {
2890 2893 PyObject *caps = NULL;
2891 2894 HgRevlogIndex_Type.tp_new = PyType_GenericNew;
2892 2895 if (PyType_Ready(&HgRevlogIndex_Type) < 0)
2893 2896 return;
2894 2897 Py_INCREF(&HgRevlogIndex_Type);
2895 2898 PyModule_AddObject(mod, "index", (PyObject *)&HgRevlogIndex_Type);
2896 2899
2897 2900 nodetreeType.tp_new = PyType_GenericNew;
2898 2901 if (PyType_Ready(&nodetreeType) < 0)
2899 2902 return;
2900 2903 Py_INCREF(&nodetreeType);
2901 2904 PyModule_AddObject(mod, "nodetree", (PyObject *)&nodetreeType);
2902 2905
2903 2906 if (!nullentry) {
2904 2907 nullentry =
2905 2908 Py_BuildValue(PY23("iiiiiiis#", "iiiiiiiy#"), 0, 0, 0, -1,
2906 2909 -1, -1, -1, nullid, (Py_ssize_t)20);
2907 2910 }
2908 2911 if (nullentry)
2909 2912 PyObject_GC_UnTrack(nullentry);
2910 2913
2911 2914 caps = PyCapsule_New(&CAPI, "mercurial.cext.parsers.revlog_CAPI", NULL);
2912 2915 if (caps != NULL)
2913 2916 PyModule_AddObject(mod, "revlog_CAPI", caps);
2914 2917 }
@@ -1,191 +1,199 b''
1 1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
2 2 // and Mercurial contributors
3 3 //
4 4 // This software may be used and distributed according to the terms of the
5 5 // GNU General Public License version 2 or any later version.
6 6 mod ancestors;
7 7 pub mod dagops;
8 8 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
9 9 mod dirstate;
10 10 pub mod discovery;
11 11 pub mod testing; // unconditionally built, for use from integration tests
12 12 pub use dirstate::{
13 13 dirs_multiset::{DirsMultiset, DirsMultisetIter},
14 14 dirstate_map::DirstateMap,
15 15 parsers::{pack_dirstate, parse_dirstate, PARENT_SIZE},
16 16 status::{
17 17 status, BadMatch, BadType, DirstateStatus, StatusError, StatusOptions,
18 18 },
19 19 CopyMap, CopyMapIter, DirstateEntry, DirstateParents, EntryState,
20 20 StateMap, StateMapIter,
21 21 };
22 22 mod filepatterns;
23 23 pub mod matchers;
24 24 pub mod revlog;
25 25 pub use revlog::*;
26 26 #[cfg(feature = "with-re2")]
27 27 pub mod re2;
28 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 38 use crate::utils::hg_path::{HgPathBuf, HgPathError};
31 39 pub use filepatterns::{
32 40 parse_pattern_syntax, read_pattern_file, IgnorePattern,
33 41 PatternFileWarning, PatternSyntax,
34 42 };
35 43 use std::collections::HashMap;
36 44 use twox_hash::RandomXxHashBuilder64;
37 45
38 46 /// This is a contract between the `micro-timer` crate and us, to expose
39 47 /// the `log` crate as `crate::log`.
40 48 use log;
41 49
42 50 pub type LineNumber = usize;
43 51
44 52 /// Rust's default hasher is too slow because it tries to prevent collision
45 53 /// attacks. We are not concerned about those: if an ill-minded person has
46 54 /// write access to your repository, you have other issues.
47 55 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
48 56
49 57 #[derive(Clone, Debug, PartialEq)]
50 58 pub enum DirstateParseError {
51 59 TooLittleData,
52 60 Overflow,
53 61 CorruptedEntry(String),
54 62 Damaged,
55 63 }
56 64
57 65 impl From<std::io::Error> for DirstateParseError {
58 66 fn from(e: std::io::Error) -> Self {
59 67 DirstateParseError::CorruptedEntry(e.to_string())
60 68 }
61 69 }
62 70
63 71 impl ToString for DirstateParseError {
64 72 fn to_string(&self) -> String {
65 73 use crate::DirstateParseError::*;
66 74 match self {
67 75 TooLittleData => "Too little data for dirstate.".to_string(),
68 76 Overflow => "Overflow in dirstate.".to_string(),
69 77 CorruptedEntry(e) => format!("Corrupted entry: {:?}.", e),
70 78 Damaged => "Dirstate appears to be damaged.".to_string(),
71 79 }
72 80 }
73 81 }
74 82
75 83 #[derive(Debug, PartialEq)]
76 84 pub enum DirstatePackError {
77 85 CorruptedEntry(String),
78 86 CorruptedParent,
79 87 BadSize(usize, usize),
80 88 }
81 89
82 90 impl From<std::io::Error> for DirstatePackError {
83 91 fn from(e: std::io::Error) -> Self {
84 92 DirstatePackError::CorruptedEntry(e.to_string())
85 93 }
86 94 }
87 95 #[derive(Debug, PartialEq)]
88 96 pub enum DirstateMapError {
89 97 PathNotFound(HgPathBuf),
90 98 EmptyPath,
91 99 InvalidPath(HgPathError),
92 100 }
93 101
94 102 impl ToString for DirstateMapError {
95 103 fn to_string(&self) -> String {
96 104 match self {
97 105 DirstateMapError::PathNotFound(_) => {
98 106 "expected a value, found none".to_string()
99 107 }
100 108 DirstateMapError::EmptyPath => "Overflow in dirstate.".to_string(),
101 109 DirstateMapError::InvalidPath(e) => e.to_string(),
102 110 }
103 111 }
104 112 }
105 113
106 114 #[derive(Debug)]
107 115 pub enum DirstateError {
108 116 Parse(DirstateParseError),
109 117 Pack(DirstatePackError),
110 118 Map(DirstateMapError),
111 119 IO(std::io::Error),
112 120 }
113 121
114 122 impl From<DirstateParseError> for DirstateError {
115 123 fn from(e: DirstateParseError) -> Self {
116 124 DirstateError::Parse(e)
117 125 }
118 126 }
119 127
120 128 impl From<DirstatePackError> for DirstateError {
121 129 fn from(e: DirstatePackError) -> Self {
122 130 DirstateError::Pack(e)
123 131 }
124 132 }
125 133
126 134 #[derive(Debug)]
127 135 pub enum PatternError {
128 136 Path(HgPathError),
129 137 UnsupportedSyntax(String),
130 138 UnsupportedSyntaxInFile(String, String, usize),
131 139 TooLong(usize),
132 140 IO(std::io::Error),
133 141 /// Needed a pattern that can be turned into a regex but got one that
134 142 /// can't. This should only happen through programmer error.
135 143 NonRegexPattern(IgnorePattern),
136 144 /// This is temporary, see `re2/mod.rs`.
137 145 /// This will cause a fallback to Python.
138 146 Re2NotInstalled,
139 147 }
140 148
141 149 impl ToString for PatternError {
142 150 fn to_string(&self) -> String {
143 151 match self {
144 152 PatternError::UnsupportedSyntax(syntax) => {
145 153 format!("Unsupported syntax {}", syntax)
146 154 }
147 155 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
148 156 format!(
149 157 "{}:{}: unsupported syntax {}",
150 158 file_path, line, syntax
151 159 )
152 160 }
153 161 PatternError::TooLong(size) => {
154 162 format!("matcher pattern is too long ({} bytes)", size)
155 163 }
156 164 PatternError::IO(e) => e.to_string(),
157 165 PatternError::Path(e) => e.to_string(),
158 166 PatternError::NonRegexPattern(pattern) => {
159 167 format!("'{:?}' cannot be turned into a regex", pattern)
160 168 }
161 169 PatternError::Re2NotInstalled => {
162 170 "Re2 is not installed, cannot use regex functionality."
163 171 .to_string()
164 172 }
165 173 }
166 174 }
167 175 }
168 176
169 177 impl From<DirstateMapError> for DirstateError {
170 178 fn from(e: DirstateMapError) -> Self {
171 179 DirstateError::Map(e)
172 180 }
173 181 }
174 182
175 183 impl From<std::io::Error> for DirstateError {
176 184 fn from(e: std::io::Error) -> Self {
177 185 DirstateError::IO(e)
178 186 }
179 187 }
180 188
181 189 impl From<std::io::Error> for PatternError {
182 190 fn from(e: std::io::Error) -> Self {
183 191 PatternError::IO(e)
184 192 }
185 193 }
186 194
187 195 impl From<HgPathError> for PatternError {
188 196 fn from(e: HgPathError) -> Self {
189 197 PatternError::Path(e)
190 198 }
191 199 }
@@ -1,3755 +1,3755 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 # Modifying this script is tricky because it has many modes:
11 11 # - serial (default) vs parallel (-jN, N > 1)
12 12 # - no coverage (default) vs coverage (-c, -C, -s)
13 13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 14 # - tests are a mix of shell scripts and Python scripts
15 15 #
16 16 # If you change this script, it is recommended that you ensure you
17 17 # haven't broken it by running it in various modes with a representative
18 18 # sample of test scripts. For example:
19 19 #
20 20 # 1) serial, no coverage, temp install:
21 21 # ./run-tests.py test-s*
22 22 # 2) serial, no coverage, local hg:
23 23 # ./run-tests.py --local test-s*
24 24 # 3) serial, coverage, temp install:
25 25 # ./run-tests.py -c test-s*
26 26 # 4) serial, coverage, local hg:
27 27 # ./run-tests.py -c --local test-s* # unsupported
28 28 # 5) parallel, no coverage, temp install:
29 29 # ./run-tests.py -j2 test-s*
30 30 # 6) parallel, no coverage, local hg:
31 31 # ./run-tests.py -j2 --local test-s*
32 32 # 7) parallel, coverage, temp install:
33 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 34 # 8) parallel, coverage, local install:
35 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 36 # 9) parallel, custom tmp dir:
37 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 38 # 10) parallel, pure, tests that call run-tests:
39 39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 40 #
41 41 # (You could use any subset of the tests: test-s* happens to match
42 42 # enough that it's worth doing parallel runs, few enough that it
43 43 # completes fairly quickly, includes both shell and Python scripts, and
44 44 # includes some scripts that run daemon processes.)
45 45
46 46 from __future__ import absolute_import, print_function
47 47
48 48 import argparse
49 49 import collections
50 50 import difflib
51 51 import distutils.version as version
52 52 import errno
53 53 import json
54 54 import multiprocessing
55 55 import os
56 56 import platform
57 57 import random
58 58 import re
59 59 import shutil
60 60 import signal
61 61 import socket
62 62 import subprocess
63 63 import sys
64 64 import sysconfig
65 65 import tempfile
66 66 import threading
67 67 import time
68 68 import unittest
69 69 import uuid
70 70 import xml.dom.minidom as minidom
71 71
72 72 try:
73 73 import Queue as queue
74 74 except ImportError:
75 75 import queue
76 76
77 77 try:
78 78 import shlex
79 79
80 80 shellquote = shlex.quote
81 81 except (ImportError, AttributeError):
82 82 import pipes
83 83
84 84 shellquote = pipes.quote
85 85
86 86 processlock = threading.Lock()
87 87
88 88 pygmentspresent = False
89 89 # ANSI color is unsupported prior to Windows 10
90 90 if os.name != 'nt':
91 91 try: # is pygments installed
92 92 import pygments
93 93 import pygments.lexers as lexers
94 94 import pygments.lexer as lexer
95 95 import pygments.formatters as formatters
96 96 import pygments.token as token
97 97 import pygments.style as style
98 98
99 99 pygmentspresent = True
100 100 difflexer = lexers.DiffLexer()
101 101 terminal256formatter = formatters.Terminal256Formatter()
102 102 except ImportError:
103 103 pass
104 104
105 105 if pygmentspresent:
106 106
107 107 class TestRunnerStyle(style.Style):
108 108 default_style = ""
109 109 skipped = token.string_to_tokentype("Token.Generic.Skipped")
110 110 failed = token.string_to_tokentype("Token.Generic.Failed")
111 111 skippedname = token.string_to_tokentype("Token.Generic.SName")
112 112 failedname = token.string_to_tokentype("Token.Generic.FName")
113 113 styles = {
114 114 skipped: '#e5e5e5',
115 115 skippedname: '#00ffff',
116 116 failed: '#7f0000',
117 117 failedname: '#ff0000',
118 118 }
119 119
120 120 class TestRunnerLexer(lexer.RegexLexer):
121 121 testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?'
122 122 tokens = {
123 123 'root': [
124 124 (r'^Skipped', token.Generic.Skipped, 'skipped'),
125 125 (r'^Failed ', token.Generic.Failed, 'failed'),
126 126 (r'^ERROR: ', token.Generic.Failed, 'failed'),
127 127 ],
128 128 'skipped': [
129 129 (testpattern, token.Generic.SName),
130 130 (r':.*', token.Generic.Skipped),
131 131 ],
132 132 'failed': [
133 133 (testpattern, token.Generic.FName),
134 134 (r'(:| ).*', token.Generic.Failed),
135 135 ],
136 136 }
137 137
138 138 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
139 139 runnerlexer = TestRunnerLexer()
140 140
141 141 origenviron = os.environ.copy()
142 142
143 143 if sys.version_info > (3, 5, 0):
144 144 PYTHON3 = True
145 145 xrange = range # we use xrange in one place, and we'd rather not use range
146 146
147 147 def _sys2bytes(p):
148 148 if p is None:
149 149 return p
150 150 return p.encode('utf-8')
151 151
152 152 def _bytes2sys(p):
153 153 if p is None:
154 154 return p
155 155 return p.decode('utf-8')
156 156
157 157 osenvironb = getattr(os, 'environb', None)
158 158 if osenvironb is None:
159 159 # Windows lacks os.environb, for instance. A proxy over the real thing
160 160 # instead of a copy allows the environment to be updated via bytes on
161 161 # all platforms.
162 162 class environbytes(object):
163 163 def __init__(self, strenv):
164 164 self.__len__ = strenv.__len__
165 165 self.clear = strenv.clear
166 166 self._strenv = strenv
167 167
168 168 def __getitem__(self, k):
169 169 v = self._strenv.__getitem__(_bytes2sys(k))
170 170 return _sys2bytes(v)
171 171
172 172 def __setitem__(self, k, v):
173 173 self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
174 174
175 175 def __delitem__(self, k):
176 176 self._strenv.__delitem__(_bytes2sys(k))
177 177
178 178 def __contains__(self, k):
179 179 return self._strenv.__contains__(_bytes2sys(k))
180 180
181 181 def __iter__(self):
182 182 return iter([_sys2bytes(k) for k in iter(self._strenv)])
183 183
184 184 def get(self, k, default=None):
185 185 v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
186 186 return _sys2bytes(v)
187 187
188 188 def pop(self, k, default=None):
189 189 v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
190 190 return _sys2bytes(v)
191 191
192 192 osenvironb = environbytes(os.environ)
193 193
194 194 getcwdb = getattr(os, 'getcwdb')
195 195 if not getcwdb or os.name == 'nt':
196 196 getcwdb = lambda: _sys2bytes(os.getcwd())
197 197
198 198 elif sys.version_info >= (3, 0, 0):
199 199 print(
200 200 '%s is only supported on Python 3.5+ and 2.7, not %s'
201 201 % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
202 202 )
203 203 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
204 204 else:
205 205 PYTHON3 = False
206 206
207 207 # In python 2.x, path operations are generally done using
208 208 # bytestrings by default, so we don't have to do any extra
209 209 # fiddling there. We define the wrapper functions anyway just to
210 210 # help keep code consistent between platforms.
211 211 def _sys2bytes(p):
212 212 return p
213 213
214 214 _bytes2sys = _sys2bytes
215 215 osenvironb = os.environ
216 216 getcwdb = os.getcwd
217 217
218 218 # For Windows support
219 219 wifexited = getattr(os, "WIFEXITED", lambda x: False)
220 220
221 221 # Whether to use IPv6
222 222 def checksocketfamily(name, port=20058):
223 223 """return true if we can listen on localhost using family=name
224 224
225 225 name should be either 'AF_INET', or 'AF_INET6'.
226 226 port being used is okay - EADDRINUSE is considered as successful.
227 227 """
228 228 family = getattr(socket, name, None)
229 229 if family is None:
230 230 return False
231 231 try:
232 232 s = socket.socket(family, socket.SOCK_STREAM)
233 233 s.bind(('localhost', port))
234 234 s.close()
235 235 return True
236 236 except socket.error as exc:
237 237 if exc.errno == errno.EADDRINUSE:
238 238 return True
239 239 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
240 240 return False
241 241 else:
242 242 raise
243 243 else:
244 244 return False
245 245
246 246
247 247 # useipv6 will be set by parseargs
248 248 useipv6 = None
249 249
250 250
251 251 def checkportisavailable(port):
252 252 """return true if a port seems free to bind on localhost"""
253 253 if useipv6:
254 254 family = socket.AF_INET6
255 255 else:
256 256 family = socket.AF_INET
257 257 try:
258 258 s = socket.socket(family, socket.SOCK_STREAM)
259 259 s.bind(('localhost', port))
260 260 s.close()
261 261 return True
262 262 except socket.error as exc:
263 263 if exc.errno not in (
264 264 errno.EADDRINUSE,
265 265 errno.EADDRNOTAVAIL,
266 266 errno.EPROTONOSUPPORT,
267 267 ):
268 268 raise
269 269 return False
270 270
271 271
272 272 closefds = os.name == 'posix'
273 273
274 274
275 275 def Popen4(cmd, wd, timeout, env=None):
276 276 processlock.acquire()
277 277 p = subprocess.Popen(
278 278 _bytes2sys(cmd),
279 279 shell=True,
280 280 bufsize=-1,
281 281 cwd=_bytes2sys(wd),
282 282 env=env,
283 283 close_fds=closefds,
284 284 stdin=subprocess.PIPE,
285 285 stdout=subprocess.PIPE,
286 286 stderr=subprocess.STDOUT,
287 287 )
288 288 processlock.release()
289 289
290 290 p.fromchild = p.stdout
291 291 p.tochild = p.stdin
292 292 p.childerr = p.stderr
293 293
294 294 p.timeout = False
295 295 if timeout:
296 296
297 297 def t():
298 298 start = time.time()
299 299 while time.time() - start < timeout and p.returncode is None:
300 300 time.sleep(0.1)
301 301 p.timeout = True
302 302 if p.returncode is None:
303 303 terminate(p)
304 304
305 305 threading.Thread(target=t).start()
306 306
307 307 return p
308 308
309 309
310 310 if sys.executable:
311 311 sysexecutable = sys.executable
312 312 elif os.environ.get('PYTHONEXECUTABLE'):
313 313 sysexecutable = os.environ['PYTHONEXECUTABLE']
314 314 elif os.environ.get('PYTHON'):
315 315 sysexecutable = os.environ['PYTHON']
316 316 else:
317 317 raise AssertionError('Could not find Python interpreter')
318 318
319 319 PYTHON = _sys2bytes(sysexecutable.replace('\\', '/'))
320 320 IMPL_PATH = b'PYTHONPATH'
321 321 if 'java' in sys.platform:
322 322 IMPL_PATH = b'JYTHONPATH'
323 323
324 324 defaults = {
325 325 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()),
326 326 'timeout': ('HGTEST_TIMEOUT', 180),
327 327 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500),
328 328 'port': ('HGTEST_PORT', 20059),
329 329 'shell': ('HGTEST_SHELL', 'sh'),
330 330 }
331 331
332 332
333 333 def canonpath(path):
334 334 return os.path.realpath(os.path.expanduser(path))
335 335
336 336
337 337 def parselistfiles(files, listtype, warn=True):
338 338 entries = dict()
339 339 for filename in files:
340 340 try:
341 341 path = os.path.expanduser(os.path.expandvars(filename))
342 342 f = open(path, "rb")
343 343 except IOError as err:
344 344 if err.errno != errno.ENOENT:
345 345 raise
346 346 if warn:
347 347 print("warning: no such %s file: %s" % (listtype, filename))
348 348 continue
349 349
350 350 for line in f.readlines():
351 351 line = line.split(b'#', 1)[0].strip()
352 352 if line:
353 353 entries[line] = filename
354 354
355 355 f.close()
356 356 return entries
357 357
358 358
359 359 def parsettestcases(path):
360 360 """read a .t test file, return a set of test case names
361 361
362 362 If path does not exist, return an empty set.
363 363 """
364 364 cases = []
365 365 try:
366 366 with open(path, 'rb') as f:
367 367 for l in f:
368 368 if l.startswith(b'#testcases '):
369 369 cases.append(sorted(l[11:].split()))
370 370 except IOError as ex:
371 371 if ex.errno != errno.ENOENT:
372 372 raise
373 373 return cases
374 374
375 375
376 376 def getparser():
377 377 """Obtain the OptionParser used by the CLI."""
378 378 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
379 379
380 380 selection = parser.add_argument_group('Test Selection')
381 381 selection.add_argument(
382 382 '--allow-slow-tests',
383 383 action='store_true',
384 384 help='allow extremely slow tests',
385 385 )
386 386 selection.add_argument(
387 387 "--blacklist",
388 388 action="append",
389 389 help="skip tests listed in the specified blacklist file",
390 390 )
391 391 selection.add_argument(
392 392 "--changed",
393 393 help="run tests that are changed in parent rev or working directory",
394 394 )
395 395 selection.add_argument(
396 396 "-k", "--keywords", help="run tests matching keywords"
397 397 )
398 398 selection.add_argument(
399 399 "-r", "--retest", action="store_true", help="retest failed tests"
400 400 )
401 401 selection.add_argument(
402 402 "--test-list",
403 403 action="append",
404 404 help="read tests to run from the specified file",
405 405 )
406 406 selection.add_argument(
407 407 "--whitelist",
408 408 action="append",
409 409 help="always run tests listed in the specified whitelist file",
410 410 )
411 411 selection.add_argument(
412 412 'tests', metavar='TESTS', nargs='*', help='Tests to run'
413 413 )
414 414
415 415 harness = parser.add_argument_group('Test Harness Behavior')
416 416 harness.add_argument(
417 417 '--bisect-repo',
418 418 metavar='bisect_repo',
419 419 help=(
420 420 "Path of a repo to bisect. Use together with " "--known-good-rev"
421 421 ),
422 422 )
423 423 harness.add_argument(
424 424 "-d",
425 425 "--debug",
426 426 action="store_true",
427 427 help="debug mode: write output of test scripts to console"
428 428 " rather than capturing and diffing it (disables timeout)",
429 429 )
430 430 harness.add_argument(
431 431 "-f",
432 432 "--first",
433 433 action="store_true",
434 434 help="exit on the first test failure",
435 435 )
436 436 harness.add_argument(
437 437 "-i",
438 438 "--interactive",
439 439 action="store_true",
440 440 help="prompt to accept changed output",
441 441 )
442 442 harness.add_argument(
443 443 "-j",
444 444 "--jobs",
445 445 type=int,
446 446 help="number of jobs to run in parallel"
447 447 " (default: $%s or %d)" % defaults['jobs'],
448 448 )
449 449 harness.add_argument(
450 450 "--keep-tmpdir",
451 451 action="store_true",
452 452 help="keep temporary directory after running tests",
453 453 )
454 454 harness.add_argument(
455 455 '--known-good-rev',
456 456 metavar="known_good_rev",
457 457 help=(
458 458 "Automatically bisect any failures using this "
459 459 "revision as a known-good revision."
460 460 ),
461 461 )
462 462 harness.add_argument(
463 463 "--list-tests",
464 464 action="store_true",
465 465 help="list tests instead of running them",
466 466 )
467 467 harness.add_argument(
468 468 "--loop", action="store_true", help="loop tests repeatedly"
469 469 )
470 470 harness.add_argument(
471 471 '--random', action="store_true", help='run tests in random order'
472 472 )
473 473 harness.add_argument(
474 474 '--order-by-runtime',
475 475 action="store_true",
476 476 help='run slowest tests first, according to .testtimes',
477 477 )
478 478 harness.add_argument(
479 479 "-p",
480 480 "--port",
481 481 type=int,
482 482 help="port on which servers should listen"
483 483 " (default: $%s or %d)" % defaults['port'],
484 484 )
485 485 harness.add_argument(
486 486 '--profile-runner',
487 487 action='store_true',
488 488 help='run statprof on run-tests',
489 489 )
490 490 harness.add_argument(
491 491 "-R", "--restart", action="store_true", help="restart at last error"
492 492 )
493 493 harness.add_argument(
494 494 "--runs-per-test",
495 495 type=int,
496 496 dest="runs_per_test",
497 497 help="run each test N times (default=1)",
498 498 default=1,
499 499 )
500 500 harness.add_argument(
501 501 "--shell", help="shell to use (default: $%s or %s)" % defaults['shell']
502 502 )
503 503 harness.add_argument(
504 504 '--showchannels', action='store_true', help='show scheduling channels'
505 505 )
506 506 harness.add_argument(
507 507 "--slowtimeout",
508 508 type=int,
509 509 help="kill errant slow tests after SLOWTIMEOUT seconds"
510 510 " (default: $%s or %d)" % defaults['slowtimeout'],
511 511 )
512 512 harness.add_argument(
513 513 "-t",
514 514 "--timeout",
515 515 type=int,
516 516 help="kill errant tests after TIMEOUT seconds"
517 517 " (default: $%s or %d)" % defaults['timeout'],
518 518 )
519 519 harness.add_argument(
520 520 "--tmpdir",
521 521 help="run tests in the given temporary directory"
522 522 " (implies --keep-tmpdir)",
523 523 )
524 524 harness.add_argument(
525 525 "-v", "--verbose", action="store_true", help="output verbose messages"
526 526 )
527 527
528 528 hgconf = parser.add_argument_group('Mercurial Configuration')
529 529 hgconf.add_argument(
530 530 "--chg",
531 531 action="store_true",
532 532 help="install and use chg wrapper in place of hg",
533 533 )
534 534 hgconf.add_argument("--compiler", help="compiler to build with")
535 535 hgconf.add_argument(
536 536 '--extra-config-opt',
537 537 action="append",
538 538 default=[],
539 539 help='set the given config opt in the test hgrc',
540 540 )
541 541 hgconf.add_argument(
542 542 "-l",
543 543 "--local",
544 544 action="store_true",
545 545 help="shortcut for --with-hg=<testdir>/../hg, "
546 546 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set",
547 547 )
548 548 hgconf.add_argument(
549 549 "--ipv6",
550 550 action="store_true",
551 551 help="prefer IPv6 to IPv4 for network related tests",
552 552 )
553 553 hgconf.add_argument(
554 554 "--pure",
555 555 action="store_true",
556 556 help="use pure Python code instead of C extensions",
557 557 )
558 558 hgconf.add_argument(
559 559 "--rust",
560 560 action="store_true",
561 561 help="use Rust code alongside C extensions",
562 562 )
563 563 hgconf.add_argument(
564 564 "--no-rust",
565 565 action="store_true",
566 566 help="do not use Rust code even if compiled",
567 567 )
568 568 hgconf.add_argument(
569 569 "--with-chg",
570 570 metavar="CHG",
571 571 help="use specified chg wrapper in place of hg",
572 572 )
573 573 hgconf.add_argument(
574 574 "--with-hg",
575 575 metavar="HG",
576 576 help="test using specified hg script rather than a "
577 577 "temporary installation",
578 578 )
579 579
580 580 reporting = parser.add_argument_group('Results Reporting')
581 581 reporting.add_argument(
582 582 "-C",
583 583 "--annotate",
584 584 action="store_true",
585 585 help="output files annotated with coverage",
586 586 )
587 587 reporting.add_argument(
588 588 "--color",
589 589 choices=["always", "auto", "never"],
590 590 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
591 591 help="colorisation: always|auto|never (default: auto)",
592 592 )
593 593 reporting.add_argument(
594 594 "-c",
595 595 "--cover",
596 596 action="store_true",
597 597 help="print a test coverage report",
598 598 )
599 599 reporting.add_argument(
600 600 '--exceptions',
601 601 action='store_true',
602 602 help='log all exceptions and generate an exception report',
603 603 )
604 604 reporting.add_argument(
605 605 "-H",
606 606 "--htmlcov",
607 607 action="store_true",
608 608 help="create an HTML report of the coverage of the files",
609 609 )
610 610 reporting.add_argument(
611 611 "--json",
612 612 action="store_true",
613 613 help="store test result data in 'report.json' file",
614 614 )
615 615 reporting.add_argument(
616 616 "--outputdir",
617 617 help="directory to write error logs to (default=test directory)",
618 618 )
619 619 reporting.add_argument(
620 620 "-n", "--nodiff", action="store_true", help="skip showing test changes"
621 621 )
622 622 reporting.add_argument(
623 623 "-S",
624 624 "--noskips",
625 625 action="store_true",
626 626 help="don't report skip tests verbosely",
627 627 )
628 628 reporting.add_argument(
629 629 "--time", action="store_true", help="time how long each test takes"
630 630 )
631 631 reporting.add_argument("--view", help="external diff viewer")
632 632 reporting.add_argument(
633 633 "--xunit", help="record xunit results at specified path"
634 634 )
635 635
636 636 for option, (envvar, default) in defaults.items():
637 637 defaults[option] = type(default)(os.environ.get(envvar, default))
638 638 parser.set_defaults(**defaults)
639 639
640 640 return parser
641 641
642 642
643 643 def parseargs(args, parser):
644 644 """Parse arguments with our OptionParser and validate results."""
645 645 options = parser.parse_args(args)
646 646
647 647 # jython is always pure
648 648 if 'java' in sys.platform or '__pypy__' in sys.modules:
649 649 options.pure = True
650 650
651 651 if platform.python_implementation() != 'CPython' and options.rust:
652 652 parser.error('Rust extensions are only available with CPython')
653 653
654 654 if options.pure and options.rust:
655 655 parser.error('--rust cannot be used with --pure')
656 656
657 657 if options.rust and options.no_rust:
658 658 parser.error('--rust cannot be used with --no-rust')
659 659
660 660 if options.local:
661 661 if options.with_hg or options.with_chg:
662 662 parser.error('--local cannot be used with --with-hg or --with-chg')
663 663 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
664 664 reporootdir = os.path.dirname(testdir)
665 665 pathandattrs = [(b'hg', 'with_hg')]
666 666 if options.chg:
667 667 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
668 668 for relpath, attr in pathandattrs:
669 669 binpath = os.path.join(reporootdir, relpath)
670 670 if os.name != 'nt' and not os.access(binpath, os.X_OK):
671 671 parser.error(
672 672 '--local specified, but %r not found or '
673 673 'not executable' % binpath
674 674 )
675 675 setattr(options, attr, _bytes2sys(binpath))
676 676
677 677 if options.with_hg:
678 678 options.with_hg = canonpath(_sys2bytes(options.with_hg))
679 679 if not (
680 680 os.path.isfile(options.with_hg)
681 681 and os.access(options.with_hg, os.X_OK)
682 682 ):
683 683 parser.error('--with-hg must specify an executable hg script')
684 684 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
685 685 sys.stderr.write('warning: --with-hg should specify an hg script\n')
686 686 sys.stderr.flush()
687 687
688 688 if (options.chg or options.with_chg) and os.name == 'nt':
689 689 parser.error('chg does not work on %s' % os.name)
690 690 if options.with_chg:
691 691 options.chg = False # no installation to temporary location
692 692 options.with_chg = canonpath(_sys2bytes(options.with_chg))
693 693 if not (
694 694 os.path.isfile(options.with_chg)
695 695 and os.access(options.with_chg, os.X_OK)
696 696 ):
697 697 parser.error('--with-chg must specify a chg executable')
698 698 if options.chg and options.with_hg:
699 699 # chg shares installation location with hg
700 700 parser.error(
701 701 '--chg does not work when --with-hg is specified '
702 702 '(use --with-chg instead)'
703 703 )
704 704
705 705 if options.color == 'always' and not pygmentspresent:
706 706 sys.stderr.write(
707 707 'warning: --color=always ignored because '
708 708 'pygments is not installed\n'
709 709 )
710 710
711 711 if options.bisect_repo and not options.known_good_rev:
712 712 parser.error("--bisect-repo cannot be used without --known-good-rev")
713 713
714 714 global useipv6
715 715 if options.ipv6:
716 716 useipv6 = checksocketfamily('AF_INET6')
717 717 else:
718 718 # only use IPv6 if IPv4 is unavailable and IPv6 is available
719 719 useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily(
720 720 'AF_INET6'
721 721 )
722 722
723 723 options.anycoverage = options.cover or options.annotate or options.htmlcov
724 724 if options.anycoverage:
725 725 try:
726 726 import coverage
727 727
728 728 covver = version.StrictVersion(coverage.__version__).version
729 729 if covver < (3, 3):
730 730 parser.error('coverage options require coverage 3.3 or later')
731 731 except ImportError:
732 732 parser.error('coverage options now require the coverage package')
733 733
734 734 if options.anycoverage and options.local:
735 735 # this needs some path mangling somewhere, I guess
736 736 parser.error(
737 737 "sorry, coverage options do not work when --local " "is specified"
738 738 )
739 739
740 740 if options.anycoverage and options.with_hg:
741 741 parser.error(
742 742 "sorry, coverage options do not work when --with-hg " "is specified"
743 743 )
744 744
745 745 global verbose
746 746 if options.verbose:
747 747 verbose = ''
748 748
749 749 if options.tmpdir:
750 750 options.tmpdir = canonpath(options.tmpdir)
751 751
752 752 if options.jobs < 1:
753 753 parser.error('--jobs must be positive')
754 754 if options.interactive and options.debug:
755 755 parser.error("-i/--interactive and -d/--debug are incompatible")
756 756 if options.debug:
757 757 if options.timeout != defaults['timeout']:
758 758 sys.stderr.write('warning: --timeout option ignored with --debug\n')
759 759 if options.slowtimeout != defaults['slowtimeout']:
760 760 sys.stderr.write(
761 761 'warning: --slowtimeout option ignored with --debug\n'
762 762 )
763 763 options.timeout = 0
764 764 options.slowtimeout = 0
765 765
766 766 if options.blacklist:
767 767 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
768 768 if options.whitelist:
769 769 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
770 770 else:
771 771 options.whitelisted = {}
772 772
773 773 if options.showchannels:
774 774 options.nodiff = True
775 775
776 776 return options
777 777
778 778
779 779 def rename(src, dst):
780 780 """Like os.rename(), trade atomicity and opened files friendliness
781 781 for existing destination support.
782 782 """
783 783 shutil.copy(src, dst)
784 784 os.remove(src)
785 785
786 786
787 787 def makecleanable(path):
788 788 """Try to fix directory permission recursively so that the entire tree
789 789 can be deleted"""
790 790 for dirpath, dirnames, _filenames in os.walk(path, topdown=True):
791 791 for d in dirnames:
792 792 p = os.path.join(dirpath, d)
793 793 try:
794 794 os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx
795 795 except OSError:
796 796 pass
797 797
798 798
799 799 _unified_diff = difflib.unified_diff
800 800 if PYTHON3:
801 801 import functools
802 802
803 803 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
804 804
805 805
806 806 def getdiff(expected, output, ref, err):
807 807 servefail = False
808 808 lines = []
809 809 for line in _unified_diff(expected, output, ref, err):
810 810 if line.startswith(b'+++') or line.startswith(b'---'):
811 811 line = line.replace(b'\\', b'/')
812 812 if line.endswith(b' \n'):
813 813 line = line[:-2] + b'\n'
814 814 lines.append(line)
815 815 if not servefail and line.startswith(
816 816 b'+ abort: child process failed to start'
817 817 ):
818 818 servefail = True
819 819
820 820 return servefail, lines
821 821
822 822
823 823 verbose = False
824 824
825 825
826 826 def vlog(*msg):
827 827 """Log only when in verbose mode."""
828 828 if verbose is False:
829 829 return
830 830
831 831 return log(*msg)
832 832
833 833
834 834 # Bytes that break XML even in a CDATA block: control characters 0-31
835 835 # sans \t, \n and \r
836 836 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
837 837
838 838 # Match feature conditionalized output lines in the form, capturing the feature
839 839 # list in group 2, and the preceeding line output in group 1:
840 840 #
841 841 # output..output (feature !)\n
842 842 optline = re.compile(br'(.*) \((.+?) !\)\n$')
843 843
844 844
845 845 def cdatasafe(data):
846 846 """Make a string safe to include in a CDATA block.
847 847
848 848 Certain control characters are illegal in a CDATA block, and
849 849 there's no way to include a ]]> in a CDATA either. This function
850 850 replaces illegal bytes with ? and adds a space between the ]] so
851 851 that it won't break the CDATA block.
852 852 """
853 853 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
854 854
855 855
856 856 def log(*msg):
857 857 """Log something to stdout.
858 858
859 859 Arguments are strings to print.
860 860 """
861 861 with iolock:
862 862 if verbose:
863 863 print(verbose, end=' ')
864 864 for m in msg:
865 865 print(m, end=' ')
866 866 print()
867 867 sys.stdout.flush()
868 868
869 869
870 870 def highlightdiff(line, color):
871 871 if not color:
872 872 return line
873 873 assert pygmentspresent
874 874 return pygments.highlight(
875 875 line.decode('latin1'), difflexer, terminal256formatter
876 876 ).encode('latin1')
877 877
878 878
879 879 def highlightmsg(msg, color):
880 880 if not color:
881 881 return msg
882 882 assert pygmentspresent
883 883 return pygments.highlight(msg, runnerlexer, runnerformatter)
884 884
885 885
886 886 def terminate(proc):
887 887 """Terminate subprocess"""
888 888 vlog('# Terminating process %d' % proc.pid)
889 889 try:
890 890 proc.terminate()
891 891 except OSError:
892 892 pass
893 893
894 894
895 895 def killdaemons(pidfile):
896 896 import killdaemons as killmod
897 897
898 898 return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog)
899 899
900 900
901 901 class Test(unittest.TestCase):
902 902 """Encapsulates a single, runnable test.
903 903
904 904 While this class conforms to the unittest.TestCase API, it differs in that
905 905 instances need to be instantiated manually. (Typically, unittest.TestCase
906 906 classes are instantiated automatically by scanning modules.)
907 907 """
908 908
909 909 # Status code reserved for skipped tests (used by hghave).
910 910 SKIPPED_STATUS = 80
911 911
912 912 def __init__(
913 913 self,
914 914 path,
915 915 outputdir,
916 916 tmpdir,
917 917 keeptmpdir=False,
918 918 debug=False,
919 919 first=False,
920 920 timeout=None,
921 921 startport=None,
922 922 extraconfigopts=None,
923 923 shell=None,
924 924 hgcommand=None,
925 925 slowtimeout=None,
926 926 usechg=False,
927 927 useipv6=False,
928 928 ):
929 929 """Create a test from parameters.
930 930
931 931 path is the full path to the file defining the test.
932 932
933 933 tmpdir is the main temporary directory to use for this test.
934 934
935 935 keeptmpdir determines whether to keep the test's temporary directory
936 936 after execution. It defaults to removal (False).
937 937
938 938 debug mode will make the test execute verbosely, with unfiltered
939 939 output.
940 940
941 941 timeout controls the maximum run time of the test. It is ignored when
942 942 debug is True. See slowtimeout for tests with #require slow.
943 943
944 944 slowtimeout overrides timeout if the test has #require slow.
945 945
946 946 startport controls the starting port number to use for this test. Each
947 947 test will reserve 3 port numbers for execution. It is the caller's
948 948 responsibility to allocate a non-overlapping port range to Test
949 949 instances.
950 950
951 951 extraconfigopts is an iterable of extra hgrc config options. Values
952 952 must have the form "key=value" (something understood by hgrc). Values
953 953 of the form "foo.key=value" will result in "[foo] key=value".
954 954
955 955 shell is the shell to execute tests in.
956 956 """
957 957 if timeout is None:
958 958 timeout = defaults['timeout']
959 959 if startport is None:
960 960 startport = defaults['port']
961 961 if slowtimeout is None:
962 962 slowtimeout = defaults['slowtimeout']
963 963 self.path = path
964 964 self.bname = os.path.basename(path)
965 965 self.name = _bytes2sys(self.bname)
966 966 self._testdir = os.path.dirname(path)
967 967 self._outputdir = outputdir
968 968 self._tmpname = os.path.basename(path)
969 969 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
970 970
971 971 self._threadtmp = tmpdir
972 972 self._keeptmpdir = keeptmpdir
973 973 self._debug = debug
974 974 self._first = first
975 975 self._timeout = timeout
976 976 self._slowtimeout = slowtimeout
977 977 self._startport = startport
978 978 self._extraconfigopts = extraconfigopts or []
979 979 self._shell = _sys2bytes(shell)
980 980 self._hgcommand = hgcommand or b'hg'
981 981 self._usechg = usechg
982 982 self._useipv6 = useipv6
983 983
984 984 self._aborted = False
985 985 self._daemonpids = []
986 986 self._finished = None
987 987 self._ret = None
988 988 self._out = None
989 989 self._skipped = None
990 990 self._testtmp = None
991 991 self._chgsockdir = None
992 992
993 993 self._refout = self.readrefout()
994 994
995 995 def readrefout(self):
996 996 """read reference output"""
997 997 # If we're not in --debug mode and reference output file exists,
998 998 # check test output against it.
999 999 if self._debug:
1000 1000 return None # to match "out is None"
1001 1001 elif os.path.exists(self.refpath):
1002 1002 with open(self.refpath, 'rb') as f:
1003 1003 return f.read().splitlines(True)
1004 1004 else:
1005 1005 return []
1006 1006
1007 1007 # needed to get base class __repr__ running
1008 1008 @property
1009 1009 def _testMethodName(self):
1010 1010 return self.name
1011 1011
1012 1012 def __str__(self):
1013 1013 return self.name
1014 1014
1015 1015 def shortDescription(self):
1016 1016 return self.name
1017 1017
1018 1018 def setUp(self):
1019 1019 """Tasks to perform before run()."""
1020 1020 self._finished = False
1021 1021 self._ret = None
1022 1022 self._out = None
1023 1023 self._skipped = None
1024 1024
1025 1025 try:
1026 1026 os.mkdir(self._threadtmp)
1027 1027 except OSError as e:
1028 1028 if e.errno != errno.EEXIST:
1029 1029 raise
1030 1030
1031 1031 name = self._tmpname
1032 1032 self._testtmp = os.path.join(self._threadtmp, name)
1033 1033 os.mkdir(self._testtmp)
1034 1034
1035 1035 # Remove any previous output files.
1036 1036 if os.path.exists(self.errpath):
1037 1037 try:
1038 1038 os.remove(self.errpath)
1039 1039 except OSError as e:
1040 1040 # We might have raced another test to clean up a .err
1041 1041 # file, so ignore ENOENT when removing a previous .err
1042 1042 # file.
1043 1043 if e.errno != errno.ENOENT:
1044 1044 raise
1045 1045
1046 1046 if self._usechg:
1047 1047 self._chgsockdir = os.path.join(
1048 1048 self._threadtmp, b'%s.chgsock' % name
1049 1049 )
1050 1050 os.mkdir(self._chgsockdir)
1051 1051
1052 1052 def run(self, result):
1053 1053 """Run this test and report results against a TestResult instance."""
1054 1054 # This function is extremely similar to unittest.TestCase.run(). Once
1055 1055 # we require Python 2.7 (or at least its version of unittest), this
1056 1056 # function can largely go away.
1057 1057 self._result = result
1058 1058 result.startTest(self)
1059 1059 try:
1060 1060 try:
1061 1061 self.setUp()
1062 1062 except (KeyboardInterrupt, SystemExit):
1063 1063 self._aborted = True
1064 1064 raise
1065 1065 except Exception:
1066 1066 result.addError(self, sys.exc_info())
1067 1067 return
1068 1068
1069 1069 success = False
1070 1070 try:
1071 1071 self.runTest()
1072 1072 except KeyboardInterrupt:
1073 1073 self._aborted = True
1074 1074 raise
1075 1075 except unittest.SkipTest as e:
1076 1076 result.addSkip(self, str(e))
1077 1077 # The base class will have already counted this as a
1078 1078 # test we "ran", but we want to exclude skipped tests
1079 1079 # from those we count towards those run.
1080 1080 result.testsRun -= 1
1081 1081 except self.failureException as e:
1082 1082 # This differs from unittest in that we don't capture
1083 1083 # the stack trace. This is for historical reasons and
1084 1084 # this decision could be revisited in the future,
1085 1085 # especially for PythonTest instances.
1086 1086 if result.addFailure(self, str(e)):
1087 1087 success = True
1088 1088 except Exception:
1089 1089 result.addError(self, sys.exc_info())
1090 1090 else:
1091 1091 success = True
1092 1092
1093 1093 try:
1094 1094 self.tearDown()
1095 1095 except (KeyboardInterrupt, SystemExit):
1096 1096 self._aborted = True
1097 1097 raise
1098 1098 except Exception:
1099 1099 result.addError(self, sys.exc_info())
1100 1100 success = False
1101 1101
1102 1102 if success:
1103 1103 result.addSuccess(self)
1104 1104 finally:
1105 1105 result.stopTest(self, interrupted=self._aborted)
1106 1106
1107 1107 def runTest(self):
1108 1108 """Run this test instance.
1109 1109
1110 1110 This will return a tuple describing the result of the test.
1111 1111 """
1112 1112 env = self._getenv()
1113 1113 self._genrestoreenv(env)
1114 1114 self._daemonpids.append(env['DAEMON_PIDS'])
1115 1115 self._createhgrc(env['HGRCPATH'])
1116 1116
1117 1117 vlog('# Test', self.name)
1118 1118
1119 1119 ret, out = self._run(env)
1120 1120 self._finished = True
1121 1121 self._ret = ret
1122 1122 self._out = out
1123 1123
1124 1124 def describe(ret):
1125 1125 if ret < 0:
1126 1126 return 'killed by signal: %d' % -ret
1127 1127 return 'returned error code %d' % ret
1128 1128
1129 1129 self._skipped = False
1130 1130
1131 1131 if ret == self.SKIPPED_STATUS:
1132 1132 if out is None: # Debug mode, nothing to parse.
1133 1133 missing = ['unknown']
1134 1134 failed = None
1135 1135 else:
1136 1136 missing, failed = TTest.parsehghaveoutput(out)
1137 1137
1138 1138 if not missing:
1139 1139 missing = ['skipped']
1140 1140
1141 1141 if failed:
1142 1142 self.fail('hg have failed checking for %s' % failed[-1])
1143 1143 else:
1144 1144 self._skipped = True
1145 1145 raise unittest.SkipTest(missing[-1])
1146 1146 elif ret == 'timeout':
1147 1147 self.fail('timed out')
1148 1148 elif ret is False:
1149 1149 self.fail('no result code from test')
1150 1150 elif out != self._refout:
1151 1151 # Diff generation may rely on written .err file.
1152 1152 if (
1153 1153 (ret != 0 or out != self._refout)
1154 1154 and not self._skipped
1155 1155 and not self._debug
1156 1156 ):
1157 1157 with open(self.errpath, 'wb') as f:
1158 1158 for line in out:
1159 1159 f.write(line)
1160 1160
1161 1161 # The result object handles diff calculation for us.
1162 1162 with firstlock:
1163 1163 if self._result.addOutputMismatch(self, ret, out, self._refout):
1164 1164 # change was accepted, skip failing
1165 1165 return
1166 1166 if self._first:
1167 1167 global firsterror
1168 1168 firsterror = True
1169 1169
1170 1170 if ret:
1171 1171 msg = 'output changed and ' + describe(ret)
1172 1172 else:
1173 1173 msg = 'output changed'
1174 1174
1175 1175 self.fail(msg)
1176 1176 elif ret:
1177 1177 self.fail(describe(ret))
1178 1178
1179 1179 def tearDown(self):
1180 1180 """Tasks to perform after run()."""
1181 1181 for entry in self._daemonpids:
1182 1182 killdaemons(entry)
1183 1183 self._daemonpids = []
1184 1184
1185 1185 if self._keeptmpdir:
1186 1186 log(
1187 1187 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1188 1188 % (_bytes2sys(self._testtmp), _bytes2sys(self._threadtmp),)
1189 1189 )
1190 1190 else:
1191 1191 try:
1192 1192 shutil.rmtree(self._testtmp)
1193 1193 except OSError:
1194 1194 # unreadable directory may be left in $TESTTMP; fix permission
1195 1195 # and try again
1196 1196 makecleanable(self._testtmp)
1197 1197 shutil.rmtree(self._testtmp, True)
1198 1198 shutil.rmtree(self._threadtmp, True)
1199 1199
1200 1200 if self._usechg:
1201 1201 # chgservers will stop automatically after they find the socket
1202 1202 # files are deleted
1203 1203 shutil.rmtree(self._chgsockdir, True)
1204 1204
1205 1205 if (
1206 1206 (self._ret != 0 or self._out != self._refout)
1207 1207 and not self._skipped
1208 1208 and not self._debug
1209 1209 and self._out
1210 1210 ):
1211 1211 with open(self.errpath, 'wb') as f:
1212 1212 for line in self._out:
1213 1213 f.write(line)
1214 1214
1215 1215 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1216 1216
1217 1217 def _run(self, env):
1218 1218 # This should be implemented in child classes to run tests.
1219 1219 raise unittest.SkipTest('unknown test type')
1220 1220
1221 1221 def abort(self):
1222 1222 """Terminate execution of this test."""
1223 1223 self._aborted = True
1224 1224
1225 1225 def _portmap(self, i):
1226 1226 offset = b'' if i == 0 else b'%d' % i
1227 1227 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1228 1228
1229 1229 def _getreplacements(self):
1230 1230 """Obtain a mapping of text replacements to apply to test output.
1231 1231
1232 1232 Test output needs to be normalized so it can be compared to expected
1233 1233 output. This function defines how some of that normalization will
1234 1234 occur.
1235 1235 """
1236 1236 r = [
1237 1237 # This list should be parallel to defineport in _getenv
1238 1238 self._portmap(0),
1239 1239 self._portmap(1),
1240 1240 self._portmap(2),
1241 1241 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1242 1242 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1243 1243 ]
1244 1244 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1245 1245
1246 1246 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1247 1247
1248 1248 if os.path.exists(replacementfile):
1249 1249 data = {}
1250 1250 with open(replacementfile, mode='rb') as source:
1251 1251 # the intermediate 'compile' step help with debugging
1252 1252 code = compile(source.read(), replacementfile, 'exec')
1253 1253 exec(code, data)
1254 1254 for value in data.get('substitutions', ()):
1255 1255 if len(value) != 2:
1256 1256 msg = 'malformatted substitution in %s: %r'
1257 1257 msg %= (replacementfile, value)
1258 1258 raise ValueError(msg)
1259 1259 r.append(value)
1260 1260 return r
1261 1261
1262 1262 def _escapepath(self, p):
1263 1263 if os.name == 'nt':
1264 1264 return b''.join(
1265 1265 c.isalpha()
1266 1266 and b'[%s%s]' % (c.lower(), c.upper())
1267 1267 or c in b'/\\'
1268 1268 and br'[/\\]'
1269 1269 or c.isdigit()
1270 1270 and c
1271 1271 or b'\\' + c
1272 1272 for c in [p[i : i + 1] for i in range(len(p))]
1273 1273 )
1274 1274 else:
1275 1275 return re.escape(p)
1276 1276
1277 1277 def _localip(self):
1278 1278 if self._useipv6:
1279 1279 return b'::1'
1280 1280 else:
1281 1281 return b'127.0.0.1'
1282 1282
1283 1283 def _genrestoreenv(self, testenv):
1284 1284 """Generate a script that can be used by tests to restore the original
1285 1285 environment."""
1286 1286 # Put the restoreenv script inside self._threadtmp
1287 1287 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1288 1288 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1289 1289
1290 1290 # Only restore environment variable names that the shell allows
1291 1291 # us to export.
1292 1292 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1293 1293
1294 1294 # Do not restore these variables; otherwise tests would fail.
1295 1295 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1296 1296
1297 1297 with open(scriptpath, 'w') as envf:
1298 1298 for name, value in origenviron.items():
1299 1299 if not name_regex.match(name):
1300 1300 # Skip environment variables with unusual names not
1301 1301 # allowed by most shells.
1302 1302 continue
1303 1303 if name in reqnames:
1304 1304 continue
1305 1305 envf.write('%s=%s\n' % (name, shellquote(value)))
1306 1306
1307 1307 for name in testenv:
1308 1308 if name in origenviron or name in reqnames:
1309 1309 continue
1310 1310 envf.write('unset %s\n' % (name,))
1311 1311
1312 1312 def _getenv(self):
1313 1313 """Obtain environment variables to use during test execution."""
1314 1314
1315 1315 def defineport(i):
1316 1316 offset = '' if i == 0 else '%s' % i
1317 1317 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1318 1318
1319 1319 env = os.environ.copy()
1320 1320 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1321 1321 env['HGEMITWARNINGS'] = '1'
1322 1322 env['TESTTMP'] = _bytes2sys(self._testtmp)
1323 1323 env['TESTNAME'] = self.name
1324 1324 env['HOME'] = _bytes2sys(self._testtmp)
1325 1325 # This number should match portneeded in _getport
1326 1326 for port in xrange(3):
1327 1327 # This list should be parallel to _portmap in _getreplacements
1328 1328 defineport(port)
1329 1329 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1330 1330 env["DAEMON_PIDS"] = _bytes2sys(
1331 1331 os.path.join(self._threadtmp, b'daemon.pids')
1332 1332 )
1333 1333 env["HGEDITOR"] = (
1334 1334 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1335 1335 )
1336 1336 env["HGUSER"] = "test"
1337 1337 env["HGENCODING"] = "ascii"
1338 1338 env["HGENCODINGMODE"] = "strict"
1339 1339 env["HGHOSTNAME"] = "test-hostname"
1340 1340 env['HGIPV6'] = str(int(self._useipv6))
1341 1341 # See contrib/catapipe.py for how to use this functionality.
1342 1342 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1343 1343 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1344 1344 # non-test one in as a default, otherwise set to devnull
1345 1345 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1346 1346 'HGCATAPULTSERVERPIPE', os.devnull
1347 1347 )
1348 1348
1349 1349 extraextensions = []
1350 1350 for opt in self._extraconfigopts:
1351 1351 section, key = _sys2bytes(opt).split(b'.', 1)
1352 1352 if section != 'extensions':
1353 1353 continue
1354 1354 name = key.split(b'=', 1)[0]
1355 1355 extraextensions.append(name)
1356 1356
1357 1357 if extraextensions:
1358 1358 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1359 1359
1360 1360 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1361 1361 # IP addresses.
1362 1362 env['LOCALIP'] = _bytes2sys(self._localip())
1363 1363
1364 1364 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1365 1365 # but this is needed for testing python instances like dummyssh,
1366 1366 # dummysmtpd.py, and dumbhttp.py.
1367 1367 if PYTHON3 and os.name == 'nt':
1368 1368 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1369 1369
1370 1370 # Modified HOME in test environment can confuse Rust tools. So set
1371 1371 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1372 1372 # present and these variables aren't already defined.
1373 1373 cargo_home_path = os.path.expanduser('~/.cargo')
1374 1374 rustup_home_path = os.path.expanduser('~/.rustup')
1375 1375
1376 1376 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1377 1377 env['CARGO_HOME'] = cargo_home_path
1378 1378 if (
1379 1379 os.path.exists(rustup_home_path)
1380 1380 and b'RUSTUP_HOME' not in osenvironb
1381 1381 ):
1382 1382 env['RUSTUP_HOME'] = rustup_home_path
1383 1383
1384 1384 # Reset some environment variables to well-known values so that
1385 1385 # the tests produce repeatable output.
1386 1386 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1387 1387 env['TZ'] = 'GMT'
1388 1388 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1389 1389 env['COLUMNS'] = '80'
1390 1390 env['TERM'] = 'xterm'
1391 1391
1392 1392 dropped = [
1393 1393 'CDPATH',
1394 1394 'CHGDEBUG',
1395 1395 'EDITOR',
1396 1396 'GREP_OPTIONS',
1397 1397 'HG',
1398 1398 'HGMERGE',
1399 1399 'HGPLAIN',
1400 1400 'HGPLAINEXCEPT',
1401 1401 'HGPROF',
1402 1402 'http_proxy',
1403 1403 'no_proxy',
1404 1404 'NO_PROXY',
1405 1405 'PAGER',
1406 1406 'VISUAL',
1407 1407 ]
1408 1408
1409 1409 for k in dropped:
1410 1410 if k in env:
1411 1411 del env[k]
1412 1412
1413 1413 # unset env related to hooks
1414 1414 for k in list(env):
1415 1415 if k.startswith('HG_'):
1416 1416 del env[k]
1417 1417
1418 1418 if self._usechg:
1419 1419 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1420 1420
1421 1421 return env
1422 1422
1423 1423 def _createhgrc(self, path):
1424 1424 """Create an hgrc file for this test."""
1425 1425 with open(path, 'wb') as hgrc:
1426 1426 hgrc.write(b'[ui]\n')
1427 1427 hgrc.write(b'slash = True\n')
1428 1428 hgrc.write(b'interactive = False\n')
1429 1429 hgrc.write(b'merge = internal:merge\n')
1430 1430 hgrc.write(b'mergemarkers = detailed\n')
1431 1431 hgrc.write(b'promptecho = True\n')
1432 1432 hgrc.write(b'[defaults]\n')
1433 1433 hgrc.write(b'[devel]\n')
1434 1434 hgrc.write(b'all-warnings = true\n')
1435 1435 hgrc.write(b'default-date = 0 0\n')
1436 1436 hgrc.write(b'[largefiles]\n')
1437 1437 hgrc.write(
1438 1438 b'usercache = %s\n'
1439 1439 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1440 1440 )
1441 1441 hgrc.write(b'[lfs]\n')
1442 1442 hgrc.write(
1443 1443 b'usercache = %s\n'
1444 1444 % (os.path.join(self._testtmp, b'.cache/lfs'))
1445 1445 )
1446 1446 hgrc.write(b'[web]\n')
1447 1447 hgrc.write(b'address = localhost\n')
1448 1448 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1449 1449 hgrc.write(b'server-header = testing stub value\n')
1450 1450
1451 1451 for opt in self._extraconfigopts:
1452 1452 section, key = _sys2bytes(opt).split(b'.', 1)
1453 1453 assert b'=' in key, (
1454 1454 'extra config opt %s must ' 'have an = for assignment' % opt
1455 1455 )
1456 1456 hgrc.write(b'[%s]\n%s\n' % (section, key))
1457 1457
1458 1458 def fail(self, msg):
1459 1459 # unittest differentiates between errored and failed.
1460 1460 # Failed is denoted by AssertionError (by default at least).
1461 1461 raise AssertionError(msg)
1462 1462
1463 1463 def _runcommand(self, cmd, env, normalizenewlines=False):
1464 1464 """Run command in a sub-process, capturing the output (stdout and
1465 1465 stderr).
1466 1466
1467 1467 Return a tuple (exitcode, output). output is None in debug mode.
1468 1468 """
1469 1469 if self._debug:
1470 1470 proc = subprocess.Popen(
1471 1471 _bytes2sys(cmd),
1472 1472 shell=True,
1473 1473 cwd=_bytes2sys(self._testtmp),
1474 1474 env=env,
1475 1475 )
1476 1476 ret = proc.wait()
1477 1477 return (ret, None)
1478 1478
1479 1479 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1480 1480
1481 1481 def cleanup():
1482 1482 terminate(proc)
1483 1483 ret = proc.wait()
1484 1484 if ret == 0:
1485 1485 ret = signal.SIGTERM << 8
1486 1486 killdaemons(env['DAEMON_PIDS'])
1487 1487 return ret
1488 1488
1489 1489 proc.tochild.close()
1490 1490
1491 1491 try:
1492 1492 output = proc.fromchild.read()
1493 1493 except KeyboardInterrupt:
1494 1494 vlog('# Handling keyboard interrupt')
1495 1495 cleanup()
1496 1496 raise
1497 1497
1498 1498 ret = proc.wait()
1499 1499 if wifexited(ret):
1500 1500 ret = os.WEXITSTATUS(ret)
1501 1501
1502 1502 if proc.timeout:
1503 1503 ret = 'timeout'
1504 1504
1505 1505 if ret:
1506 1506 killdaemons(env['DAEMON_PIDS'])
1507 1507
1508 1508 for s, r in self._getreplacements():
1509 1509 output = re.sub(s, r, output)
1510 1510
1511 1511 if normalizenewlines:
1512 1512 output = output.replace(b'\r\n', b'\n')
1513 1513
1514 1514 return ret, output.splitlines(True)
1515 1515
1516 1516
1517 1517 class PythonTest(Test):
1518 1518 """A Python-based test."""
1519 1519
1520 1520 @property
1521 1521 def refpath(self):
1522 1522 return os.path.join(self._testdir, b'%s.out' % self.bname)
1523 1523
1524 1524 def _run(self, env):
1525 1525 # Quote the python(3) executable for Windows
1526 1526 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1527 1527 vlog("# Running", cmd.decode("utf-8"))
1528 1528 normalizenewlines = os.name == 'nt'
1529 1529 result = self._runcommand(cmd, env, normalizenewlines=normalizenewlines)
1530 1530 if self._aborted:
1531 1531 raise KeyboardInterrupt()
1532 1532
1533 1533 return result
1534 1534
1535 1535
1536 1536 # Some glob patterns apply only in some circumstances, so the script
1537 1537 # might want to remove (glob) annotations that otherwise should be
1538 1538 # retained.
1539 1539 checkcodeglobpats = [
1540 1540 # On Windows it looks like \ doesn't require a (glob), but we know
1541 1541 # better.
1542 1542 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1543 1543 re.compile(br'^moving \S+/.*[^)]$'),
1544 1544 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1545 1545 # Not all platforms have 127.0.0.1 as loopback (though most do),
1546 1546 # so we always glob that too.
1547 1547 re.compile(br'.*\$LOCALIP.*$'),
1548 1548 ]
1549 1549
1550 1550 bchr = chr
1551 1551 if PYTHON3:
1552 1552 bchr = lambda x: bytes([x])
1553 1553
1554 1554 WARN_UNDEFINED = 1
1555 1555 WARN_YES = 2
1556 1556 WARN_NO = 3
1557 1557
1558 1558 MARK_OPTIONAL = b" (?)\n"
1559 1559
1560 1560
1561 1561 def isoptional(line):
1562 1562 return line.endswith(MARK_OPTIONAL)
1563 1563
1564 1564
1565 1565 class TTest(Test):
1566 1566 """A "t test" is a test backed by a .t file."""
1567 1567
1568 1568 SKIPPED_PREFIX = b'skipped: '
1569 1569 FAILED_PREFIX = b'hghave check failed: '
1570 1570 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1571 1571
1572 1572 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1573 1573 ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)}
1574 1574 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1575 1575
1576 1576 def __init__(self, path, *args, **kwds):
1577 1577 # accept an extra "case" parameter
1578 1578 case = kwds.pop('case', [])
1579 1579 self._case = case
1580 1580 self._allcases = {x for y in parsettestcases(path) for x in y}
1581 1581 super(TTest, self).__init__(path, *args, **kwds)
1582 1582 if case:
1583 1583 casepath = b'#'.join(case)
1584 1584 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1585 1585 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1586 1586 self._tmpname += b'-%s' % casepath
1587 1587 self._have = {}
1588 1588
1589 1589 @property
1590 1590 def refpath(self):
1591 1591 return os.path.join(self._testdir, self.bname)
1592 1592
1593 1593 def _run(self, env):
1594 1594 with open(self.path, 'rb') as f:
1595 1595 lines = f.readlines()
1596 1596
1597 1597 # .t file is both reference output and the test input, keep reference
1598 1598 # output updated with the the test input. This avoids some race
1599 1599 # conditions where the reference output does not match the actual test.
1600 1600 if self._refout is not None:
1601 1601 self._refout = lines
1602 1602
1603 1603 salt, script, after, expected = self._parsetest(lines)
1604 1604
1605 1605 # Write out the generated script.
1606 1606 fname = b'%s.sh' % self._testtmp
1607 1607 with open(fname, 'wb') as f:
1608 1608 for l in script:
1609 1609 f.write(l)
1610 1610
1611 1611 cmd = b'%s "%s"' % (self._shell, fname)
1612 1612 vlog("# Running", cmd.decode("utf-8"))
1613 1613
1614 1614 exitcode, output = self._runcommand(cmd, env)
1615 1615
1616 1616 if self._aborted:
1617 1617 raise KeyboardInterrupt()
1618 1618
1619 1619 # Do not merge output if skipped. Return hghave message instead.
1620 1620 # Similarly, with --debug, output is None.
1621 1621 if exitcode == self.SKIPPED_STATUS or output is None:
1622 1622 return exitcode, output
1623 1623
1624 1624 return self._processoutput(exitcode, output, salt, after, expected)
1625 1625
1626 1626 def _hghave(self, reqs):
1627 1627 allreqs = b' '.join(reqs)
1628 1628
1629 1629 self._detectslow(reqs)
1630 1630
1631 1631 if allreqs in self._have:
1632 1632 return self._have.get(allreqs)
1633 1633
1634 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 1636 tdir = runtestdir.replace(b'\\', b'/')
1637 1637 proc = Popen4(
1638 1638 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1639 1639 self._testtmp,
1640 1640 0,
1641 1641 self._getenv(),
1642 1642 )
1643 1643 stdout, stderr = proc.communicate()
1644 1644 ret = proc.wait()
1645 1645 if wifexited(ret):
1646 1646 ret = os.WEXITSTATUS(ret)
1647 1647 if ret == 2:
1648 1648 print(stdout.decode('utf-8'))
1649 1649 sys.exit(1)
1650 1650
1651 1651 if ret != 0:
1652 1652 self._have[allreqs] = (False, stdout)
1653 1653 return False, stdout
1654 1654
1655 1655 self._have[allreqs] = (True, None)
1656 1656 return True, None
1657 1657
1658 1658 def _detectslow(self, reqs):
1659 1659 """update the timeout of slow test when appropriate"""
1660 1660 if b'slow' in reqs:
1661 1661 self._timeout = self._slowtimeout
1662 1662
1663 1663 def _iftest(self, args):
1664 1664 # implements "#if"
1665 1665 reqs = []
1666 1666 for arg in args:
1667 1667 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1668 1668 if arg[3:] in self._case:
1669 1669 return False
1670 1670 elif arg in self._allcases:
1671 1671 if arg not in self._case:
1672 1672 return False
1673 1673 else:
1674 1674 reqs.append(arg)
1675 1675 self._detectslow(reqs)
1676 1676 return self._hghave(reqs)[0]
1677 1677
1678 1678 def _parsetest(self, lines):
1679 1679 # We generate a shell script which outputs unique markers to line
1680 1680 # up script results with our source. These markers include input
1681 1681 # line number and the last return code.
1682 1682 salt = b"SALT%d" % time.time()
1683 1683
1684 1684 def addsalt(line, inpython):
1685 1685 if inpython:
1686 1686 script.append(b'%s %d 0\n' % (salt, line))
1687 1687 else:
1688 1688 script.append(b'echo %s %d $?\n' % (salt, line))
1689 1689
1690 1690 activetrace = []
1691 1691 session = str(uuid.uuid4())
1692 1692 if PYTHON3:
1693 1693 session = session.encode('ascii')
1694 1694 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1695 1695 'HGCATAPULTSERVERPIPE'
1696 1696 )
1697 1697
1698 1698 def toggletrace(cmd=None):
1699 1699 if not hgcatapult or hgcatapult == os.devnull:
1700 1700 return
1701 1701
1702 1702 if activetrace:
1703 1703 script.append(
1704 1704 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1705 1705 % (session, activetrace[0])
1706 1706 )
1707 1707 if cmd is None:
1708 1708 return
1709 1709
1710 1710 if isinstance(cmd, str):
1711 1711 quoted = shellquote(cmd.strip())
1712 1712 else:
1713 1713 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1714 1714 quoted = quoted.replace(b'\\', b'\\\\')
1715 1715 script.append(
1716 1716 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1717 1717 % (session, quoted)
1718 1718 )
1719 1719 activetrace[0:] = [quoted]
1720 1720
1721 1721 script = []
1722 1722
1723 1723 # After we run the shell script, we re-unify the script output
1724 1724 # with non-active parts of the source, with synchronization by our
1725 1725 # SALT line number markers. The after table contains the non-active
1726 1726 # components, ordered by line number.
1727 1727 after = {}
1728 1728
1729 1729 # Expected shell script output.
1730 1730 expected = {}
1731 1731
1732 1732 pos = prepos = -1
1733 1733
1734 1734 # True or False when in a true or false conditional section
1735 1735 skipping = None
1736 1736
1737 1737 # We keep track of whether or not we're in a Python block so we
1738 1738 # can generate the surrounding doctest magic.
1739 1739 inpython = False
1740 1740
1741 1741 if self._debug:
1742 1742 script.append(b'set -x\n')
1743 1743 if self._hgcommand != b'hg':
1744 1744 script.append(b'alias hg="%s"\n' % self._hgcommand)
1745 1745 if os.getenv('MSYSTEM'):
1746 1746 script.append(b'alias pwd="pwd -W"\n')
1747 1747
1748 1748 if hgcatapult and hgcatapult != os.devnull:
1749 1749 if PYTHON3:
1750 1750 hgcatapult = hgcatapult.encode('utf8')
1751 1751 cataname = self.name.encode('utf8')
1752 1752 else:
1753 1753 cataname = self.name
1754 1754
1755 1755 # Kludge: use a while loop to keep the pipe from getting
1756 1756 # closed by our echo commands. The still-running file gets
1757 1757 # reaped at the end of the script, which causes the while
1758 1758 # loop to exit and closes the pipe. Sigh.
1759 1759 script.append(
1760 1760 b'rtendtracing() {\n'
1761 1761 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1762 1762 b' rm -f "$TESTTMP/.still-running"\n'
1763 1763 b'}\n'
1764 1764 b'trap "rtendtracing" 0\n'
1765 1765 b'touch "$TESTTMP/.still-running"\n'
1766 1766 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1767 1767 b'> %(catapult)s &\n'
1768 1768 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1769 1769 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1770 1770 % {
1771 1771 b'name': cataname,
1772 1772 b'session': session,
1773 1773 b'catapult': hgcatapult,
1774 1774 }
1775 1775 )
1776 1776
1777 1777 if self._case:
1778 1778 casestr = b'#'.join(self._case)
1779 1779 if isinstance(casestr, str):
1780 1780 quoted = shellquote(casestr)
1781 1781 else:
1782 1782 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1783 1783 script.append(b'TESTCASE=%s\n' % quoted)
1784 1784 script.append(b'export TESTCASE\n')
1785 1785
1786 1786 n = 0
1787 1787 for n, l in enumerate(lines):
1788 1788 if not l.endswith(b'\n'):
1789 1789 l += b'\n'
1790 1790 if l.startswith(b'#require'):
1791 1791 lsplit = l.split()
1792 1792 if len(lsplit) < 2 or lsplit[0] != b'#require':
1793 1793 after.setdefault(pos, []).append(
1794 1794 b' !!! invalid #require\n'
1795 1795 )
1796 1796 if not skipping:
1797 1797 haveresult, message = self._hghave(lsplit[1:])
1798 1798 if not haveresult:
1799 1799 script = [b'echo "%s"\nexit 80\n' % message]
1800 1800 break
1801 1801 after.setdefault(pos, []).append(l)
1802 1802 elif l.startswith(b'#if'):
1803 1803 lsplit = l.split()
1804 1804 if len(lsplit) < 2 or lsplit[0] != b'#if':
1805 1805 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1806 1806 if skipping is not None:
1807 1807 after.setdefault(pos, []).append(b' !!! nested #if\n')
1808 1808 skipping = not self._iftest(lsplit[1:])
1809 1809 after.setdefault(pos, []).append(l)
1810 1810 elif l.startswith(b'#else'):
1811 1811 if skipping is None:
1812 1812 after.setdefault(pos, []).append(b' !!! missing #if\n')
1813 1813 skipping = not skipping
1814 1814 after.setdefault(pos, []).append(l)
1815 1815 elif l.startswith(b'#endif'):
1816 1816 if skipping is None:
1817 1817 after.setdefault(pos, []).append(b' !!! missing #if\n')
1818 1818 skipping = None
1819 1819 after.setdefault(pos, []).append(l)
1820 1820 elif skipping:
1821 1821 after.setdefault(pos, []).append(l)
1822 1822 elif l.startswith(b' >>> '): # python inlines
1823 1823 after.setdefault(pos, []).append(l)
1824 1824 prepos = pos
1825 1825 pos = n
1826 1826 if not inpython:
1827 1827 # We've just entered a Python block. Add the header.
1828 1828 inpython = True
1829 1829 addsalt(prepos, False) # Make sure we report the exit code.
1830 1830 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1831 1831 addsalt(n, True)
1832 1832 script.append(l[2:])
1833 1833 elif l.startswith(b' ... '): # python inlines
1834 1834 after.setdefault(prepos, []).append(l)
1835 1835 script.append(l[2:])
1836 1836 elif l.startswith(b' $ '): # commands
1837 1837 if inpython:
1838 1838 script.append(b'EOF\n')
1839 1839 inpython = False
1840 1840 after.setdefault(pos, []).append(l)
1841 1841 prepos = pos
1842 1842 pos = n
1843 1843 addsalt(n, False)
1844 1844 rawcmd = l[4:]
1845 1845 cmd = rawcmd.split()
1846 1846 toggletrace(rawcmd)
1847 1847 if len(cmd) == 2 and cmd[0] == b'cd':
1848 1848 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1849 1849 script.append(rawcmd)
1850 1850 elif l.startswith(b' > '): # continuations
1851 1851 after.setdefault(prepos, []).append(l)
1852 1852 script.append(l[4:])
1853 1853 elif l.startswith(b' '): # results
1854 1854 # Queue up a list of expected results.
1855 1855 expected.setdefault(pos, []).append(l[2:])
1856 1856 else:
1857 1857 if inpython:
1858 1858 script.append(b'EOF\n')
1859 1859 inpython = False
1860 1860 # Non-command/result. Queue up for merged output.
1861 1861 after.setdefault(pos, []).append(l)
1862 1862
1863 1863 if inpython:
1864 1864 script.append(b'EOF\n')
1865 1865 if skipping is not None:
1866 1866 after.setdefault(pos, []).append(b' !!! missing #endif\n')
1867 1867 addsalt(n + 1, False)
1868 1868 # Need to end any current per-command trace
1869 1869 if activetrace:
1870 1870 toggletrace()
1871 1871 return salt, script, after, expected
1872 1872
1873 1873 def _processoutput(self, exitcode, output, salt, after, expected):
1874 1874 # Merge the script output back into a unified test.
1875 1875 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
1876 1876 if exitcode != 0:
1877 1877 warnonly = WARN_NO
1878 1878
1879 1879 pos = -1
1880 1880 postout = []
1881 1881 for out_rawline in output:
1882 1882 out_line, cmd_line = out_rawline, None
1883 1883 if salt in out_rawline:
1884 1884 out_line, cmd_line = out_rawline.split(salt, 1)
1885 1885
1886 1886 pos, postout, warnonly = self._process_out_line(
1887 1887 out_line, pos, postout, expected, warnonly
1888 1888 )
1889 1889 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
1890 1890
1891 1891 if pos in after:
1892 1892 postout += after.pop(pos)
1893 1893
1894 1894 if warnonly == WARN_YES:
1895 1895 exitcode = False # Set exitcode to warned.
1896 1896
1897 1897 return exitcode, postout
1898 1898
1899 1899 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1900 1900 while out_line:
1901 1901 if not out_line.endswith(b'\n'):
1902 1902 out_line += b' (no-eol)\n'
1903 1903
1904 1904 # Find the expected output at the current position.
1905 1905 els = [None]
1906 1906 if expected.get(pos, None):
1907 1907 els = expected[pos]
1908 1908
1909 1909 optional = []
1910 1910 for i, el in enumerate(els):
1911 1911 r = False
1912 1912 if el:
1913 1913 r, exact = self.linematch(el, out_line)
1914 1914 if isinstance(r, str):
1915 1915 if r == '-glob':
1916 1916 out_line = ''.join(el.rsplit(' (glob)', 1))
1917 1917 r = '' # Warn only this line.
1918 1918 elif r == "retry":
1919 1919 postout.append(b' ' + el)
1920 1920 else:
1921 1921 log('\ninfo, unknown linematch result: %r\n' % r)
1922 1922 r = False
1923 1923 if r:
1924 1924 els.pop(i)
1925 1925 break
1926 1926 if el:
1927 1927 if isoptional(el):
1928 1928 optional.append(i)
1929 1929 else:
1930 1930 m = optline.match(el)
1931 1931 if m:
1932 1932 conditions = [c for c in m.group(2).split(b' ')]
1933 1933
1934 1934 if not self._iftest(conditions):
1935 1935 optional.append(i)
1936 1936 if exact:
1937 1937 # Don't allow line to be matches against a later
1938 1938 # line in the output
1939 1939 els.pop(i)
1940 1940 break
1941 1941
1942 1942 if r:
1943 1943 if r == "retry":
1944 1944 continue
1945 1945 # clean up any optional leftovers
1946 1946 for i in optional:
1947 1947 postout.append(b' ' + els[i])
1948 1948 for i in reversed(optional):
1949 1949 del els[i]
1950 1950 postout.append(b' ' + el)
1951 1951 else:
1952 1952 if self.NEEDESCAPE(out_line):
1953 1953 out_line = TTest._stringescape(
1954 1954 b'%s (esc)\n' % out_line.rstrip(b'\n')
1955 1955 )
1956 1956 postout.append(b' ' + out_line) # Let diff deal with it.
1957 1957 if r != '': # If line failed.
1958 1958 warnonly = WARN_NO
1959 1959 elif warnonly == WARN_UNDEFINED:
1960 1960 warnonly = WARN_YES
1961 1961 break
1962 1962 else:
1963 1963 # clean up any optional leftovers
1964 1964 while expected.get(pos, None):
1965 1965 el = expected[pos].pop(0)
1966 1966 if el:
1967 1967 if not isoptional(el):
1968 1968 m = optline.match(el)
1969 1969 if m:
1970 1970 conditions = [c for c in m.group(2).split(b' ')]
1971 1971
1972 1972 if self._iftest(conditions):
1973 1973 # Don't append as optional line
1974 1974 continue
1975 1975 else:
1976 1976 continue
1977 1977 postout.append(b' ' + el)
1978 1978 return pos, postout, warnonly
1979 1979
1980 1980 def _process_cmd_line(self, cmd_line, pos, postout, after):
1981 1981 """process a "command" part of a line from unified test output"""
1982 1982 if cmd_line:
1983 1983 # Add on last return code.
1984 1984 ret = int(cmd_line.split()[1])
1985 1985 if ret != 0:
1986 1986 postout.append(b' [%d]\n' % ret)
1987 1987 if pos in after:
1988 1988 # Merge in non-active test bits.
1989 1989 postout += after.pop(pos)
1990 1990 pos = int(cmd_line.split()[0])
1991 1991 return pos, postout
1992 1992
1993 1993 @staticmethod
1994 1994 def rematch(el, l):
1995 1995 try:
1996 1996 # parse any flags at the beginning of the regex. Only 'i' is
1997 1997 # supported right now, but this should be easy to extend.
1998 1998 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
1999 1999 flags = flags or b''
2000 2000 el = flags + b'(?:' + el + b')'
2001 2001 # use \Z to ensure that the regex matches to the end of the string
2002 2002 if os.name == 'nt':
2003 2003 return re.match(el + br'\r?\n\Z', l)
2004 2004 return re.match(el + br'\n\Z', l)
2005 2005 except re.error:
2006 2006 # el is an invalid regex
2007 2007 return False
2008 2008
2009 2009 @staticmethod
2010 2010 def globmatch(el, l):
2011 2011 # The only supported special characters are * and ? plus / which also
2012 2012 # matches \ on windows. Escaping of these characters is supported.
2013 2013 if el + b'\n' == l:
2014 2014 if os.altsep:
2015 2015 # matching on "/" is not needed for this line
2016 2016 for pat in checkcodeglobpats:
2017 2017 if pat.match(el):
2018 2018 return True
2019 2019 return b'-glob'
2020 2020 return True
2021 2021 el = el.replace(b'$LOCALIP', b'*')
2022 2022 i, n = 0, len(el)
2023 2023 res = b''
2024 2024 while i < n:
2025 2025 c = el[i : i + 1]
2026 2026 i += 1
2027 2027 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2028 2028 res += el[i - 1 : i + 1]
2029 2029 i += 1
2030 2030 elif c == b'*':
2031 2031 res += b'.*'
2032 2032 elif c == b'?':
2033 2033 res += b'.'
2034 2034 elif c == b'/' and os.altsep:
2035 2035 res += b'[/\\\\]'
2036 2036 else:
2037 2037 res += re.escape(c)
2038 2038 return TTest.rematch(res, l)
2039 2039
2040 2040 def linematch(self, el, l):
2041 2041 if el == l: # perfect match (fast)
2042 2042 return True, True
2043 2043 retry = False
2044 2044 if isoptional(el):
2045 2045 retry = "retry"
2046 2046 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2047 2047 else:
2048 2048 m = optline.match(el)
2049 2049 if m:
2050 2050 conditions = [c for c in m.group(2).split(b' ')]
2051 2051
2052 2052 el = m.group(1) + b"\n"
2053 2053 if not self._iftest(conditions):
2054 2054 # listed feature missing, should not match
2055 2055 return "retry", False
2056 2056
2057 2057 if el.endswith(b" (esc)\n"):
2058 2058 if PYTHON3:
2059 2059 el = el[:-7].decode('unicode_escape') + '\n'
2060 2060 el = el.encode('utf-8')
2061 2061 else:
2062 2062 el = el[:-7].decode('string-escape') + '\n'
2063 2063 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
2064 2064 return True, True
2065 2065 if el.endswith(b" (re)\n"):
2066 2066 return (TTest.rematch(el[:-6], l) or retry), False
2067 2067 if el.endswith(b" (glob)\n"):
2068 2068 # ignore '(glob)' added to l by 'replacements'
2069 2069 if l.endswith(b" (glob)\n"):
2070 2070 l = l[:-8] + b"\n"
2071 2071 return (TTest.globmatch(el[:-8], l) or retry), False
2072 2072 if os.altsep:
2073 2073 _l = l.replace(b'\\', b'/')
2074 2074 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
2075 2075 return True, True
2076 2076 return retry, True
2077 2077
2078 2078 @staticmethod
2079 2079 def parsehghaveoutput(lines):
2080 2080 '''Parse hghave log lines.
2081 2081
2082 2082 Return tuple of lists (missing, failed):
2083 2083 * the missing/unknown features
2084 2084 * the features for which existence check failed'''
2085 2085 missing = []
2086 2086 failed = []
2087 2087 for line in lines:
2088 2088 if line.startswith(TTest.SKIPPED_PREFIX):
2089 2089 line = line.splitlines()[0]
2090 2090 missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :]))
2091 2091 elif line.startswith(TTest.FAILED_PREFIX):
2092 2092 line = line.splitlines()[0]
2093 2093 failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :]))
2094 2094
2095 2095 return missing, failed
2096 2096
2097 2097 @staticmethod
2098 2098 def _escapef(m):
2099 2099 return TTest.ESCAPEMAP[m.group(0)]
2100 2100
2101 2101 @staticmethod
2102 2102 def _stringescape(s):
2103 2103 return TTest.ESCAPESUB(TTest._escapef, s)
2104 2104
2105 2105
2106 2106 iolock = threading.RLock()
2107 2107 firstlock = threading.RLock()
2108 2108 firsterror = False
2109 2109
2110 2110
2111 2111 class TestResult(unittest._TextTestResult):
2112 2112 """Holds results when executing via unittest."""
2113 2113
2114 2114 # Don't worry too much about accessing the non-public _TextTestResult.
2115 2115 # It is relatively common in Python testing tools.
2116 2116 def __init__(self, options, *args, **kwargs):
2117 2117 super(TestResult, self).__init__(*args, **kwargs)
2118 2118
2119 2119 self._options = options
2120 2120
2121 2121 # unittest.TestResult didn't have skipped until 2.7. We need to
2122 2122 # polyfill it.
2123 2123 self.skipped = []
2124 2124
2125 2125 # We have a custom "ignored" result that isn't present in any Python
2126 2126 # unittest implementation. It is very similar to skipped. It may make
2127 2127 # sense to map it into skip some day.
2128 2128 self.ignored = []
2129 2129
2130 2130 self.times = []
2131 2131 self._firststarttime = None
2132 2132 # Data stored for the benefit of generating xunit reports.
2133 2133 self.successes = []
2134 2134 self.faildata = {}
2135 2135
2136 2136 if options.color == 'auto':
2137 2137 self.color = pygmentspresent and self.stream.isatty()
2138 2138 elif options.color == 'never':
2139 2139 self.color = False
2140 2140 else: # 'always', for testing purposes
2141 2141 self.color = pygmentspresent
2142 2142
2143 2143 def onStart(self, test):
2144 2144 """ Can be overriden by custom TestResult
2145 2145 """
2146 2146
2147 2147 def onEnd(self):
2148 2148 """ Can be overriden by custom TestResult
2149 2149 """
2150 2150
2151 2151 def addFailure(self, test, reason):
2152 2152 self.failures.append((test, reason))
2153 2153
2154 2154 if self._options.first:
2155 2155 self.stop()
2156 2156 else:
2157 2157 with iolock:
2158 2158 if reason == "timed out":
2159 2159 self.stream.write('t')
2160 2160 else:
2161 2161 if not self._options.nodiff:
2162 2162 self.stream.write('\n')
2163 2163 # Exclude the '\n' from highlighting to lex correctly
2164 2164 formatted = 'ERROR: %s output changed\n' % test
2165 2165 self.stream.write(highlightmsg(formatted, self.color))
2166 2166 self.stream.write('!')
2167 2167
2168 2168 self.stream.flush()
2169 2169
2170 2170 def addSuccess(self, test):
2171 2171 with iolock:
2172 2172 super(TestResult, self).addSuccess(test)
2173 2173 self.successes.append(test)
2174 2174
2175 2175 def addError(self, test, err):
2176 2176 super(TestResult, self).addError(test, err)
2177 2177 if self._options.first:
2178 2178 self.stop()
2179 2179
2180 2180 # Polyfill.
2181 2181 def addSkip(self, test, reason):
2182 2182 self.skipped.append((test, reason))
2183 2183 with iolock:
2184 2184 if self.showAll:
2185 2185 self.stream.writeln('skipped %s' % reason)
2186 2186 else:
2187 2187 self.stream.write('s')
2188 2188 self.stream.flush()
2189 2189
2190 2190 def addIgnore(self, test, reason):
2191 2191 self.ignored.append((test, reason))
2192 2192 with iolock:
2193 2193 if self.showAll:
2194 2194 self.stream.writeln('ignored %s' % reason)
2195 2195 else:
2196 2196 if reason not in ('not retesting', "doesn't match keyword"):
2197 2197 self.stream.write('i')
2198 2198 else:
2199 2199 self.testsRun += 1
2200 2200 self.stream.flush()
2201 2201
2202 2202 def addOutputMismatch(self, test, ret, got, expected):
2203 2203 """Record a mismatch in test output for a particular test."""
2204 2204 if self.shouldStop or firsterror:
2205 2205 # don't print, some other test case already failed and
2206 2206 # printed, we're just stale and probably failed due to our
2207 2207 # temp dir getting cleaned up.
2208 2208 return
2209 2209
2210 2210 accepted = False
2211 2211 lines = []
2212 2212
2213 2213 with iolock:
2214 2214 if self._options.nodiff:
2215 2215 pass
2216 2216 elif self._options.view:
2217 2217 v = self._options.view
2218 2218 subprocess.call(
2219 2219 r'"%s" "%s" "%s"'
2220 2220 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2221 2221 shell=True,
2222 2222 )
2223 2223 else:
2224 2224 servefail, lines = getdiff(
2225 2225 expected, got, test.refpath, test.errpath
2226 2226 )
2227 2227 self.stream.write('\n')
2228 2228 for line in lines:
2229 2229 line = highlightdiff(line, self.color)
2230 2230 if PYTHON3:
2231 2231 self.stream.flush()
2232 2232 self.stream.buffer.write(line)
2233 2233 self.stream.buffer.flush()
2234 2234 else:
2235 2235 self.stream.write(line)
2236 2236 self.stream.flush()
2237 2237
2238 2238 if servefail:
2239 2239 raise test.failureException(
2240 2240 'server failed to start (HGPORT=%s)' % test._startport
2241 2241 )
2242 2242
2243 2243 # handle interactive prompt without releasing iolock
2244 2244 if self._options.interactive:
2245 2245 if test.readrefout() != expected:
2246 2246 self.stream.write(
2247 2247 'Reference output has changed (run again to prompt '
2248 2248 'changes)'
2249 2249 )
2250 2250 else:
2251 2251 self.stream.write('Accept this change? [n] ')
2252 2252 self.stream.flush()
2253 2253 answer = sys.stdin.readline().strip()
2254 2254 if answer.lower() in ('y', 'yes'):
2255 2255 if test.path.endswith(b'.t'):
2256 2256 rename(test.errpath, test.path)
2257 2257 else:
2258 2258 rename(test.errpath, '%s.out' % test.path)
2259 2259 accepted = True
2260 2260 if not accepted:
2261 2261 self.faildata[test.name] = b''.join(lines)
2262 2262
2263 2263 return accepted
2264 2264
2265 2265 def startTest(self, test):
2266 2266 super(TestResult, self).startTest(test)
2267 2267
2268 2268 # os.times module computes the user time and system time spent by
2269 2269 # child's processes along with real elapsed time taken by a process.
2270 2270 # This module has one limitation. It can only work for Linux user
2271 2271 # and not for Windows. Hence why we fall back to another function
2272 2272 # for wall time calculations.
2273 2273 test.started_times = os.times()
2274 2274 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2275 2275 test.started_time = time.time()
2276 2276 if self._firststarttime is None: # thread racy but irrelevant
2277 2277 self._firststarttime = test.started_time
2278 2278
2279 2279 def stopTest(self, test, interrupted=False):
2280 2280 super(TestResult, self).stopTest(test)
2281 2281
2282 2282 test.stopped_times = os.times()
2283 2283 stopped_time = time.time()
2284 2284
2285 2285 starttime = test.started_times
2286 2286 endtime = test.stopped_times
2287 2287 origin = self._firststarttime
2288 2288 self.times.append(
2289 2289 (
2290 2290 test.name,
2291 2291 endtime[2] - starttime[2], # user space CPU time
2292 2292 endtime[3] - starttime[3], # sys space CPU time
2293 2293 stopped_time - test.started_time, # real time
2294 2294 test.started_time - origin, # start date in run context
2295 2295 stopped_time - origin, # end date in run context
2296 2296 )
2297 2297 )
2298 2298
2299 2299 if interrupted:
2300 2300 with iolock:
2301 2301 self.stream.writeln(
2302 2302 'INTERRUPTED: %s (after %d seconds)'
2303 2303 % (test.name, self.times[-1][3])
2304 2304 )
2305 2305
2306 2306
2307 2307 def getTestResult():
2308 2308 """
2309 2309 Returns the relevant test result
2310 2310 """
2311 2311 if "CUSTOM_TEST_RESULT" in os.environ:
2312 2312 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2313 2313 return testresultmodule.TestResult
2314 2314 else:
2315 2315 return TestResult
2316 2316
2317 2317
2318 2318 class TestSuite(unittest.TestSuite):
2319 2319 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2320 2320
2321 2321 def __init__(
2322 2322 self,
2323 2323 testdir,
2324 2324 jobs=1,
2325 2325 whitelist=None,
2326 2326 blacklist=None,
2327 2327 retest=False,
2328 2328 keywords=None,
2329 2329 loop=False,
2330 2330 runs_per_test=1,
2331 2331 loadtest=None,
2332 2332 showchannels=False,
2333 2333 *args,
2334 2334 **kwargs
2335 2335 ):
2336 2336 """Create a new instance that can run tests with a configuration.
2337 2337
2338 2338 testdir specifies the directory where tests are executed from. This
2339 2339 is typically the ``tests`` directory from Mercurial's source
2340 2340 repository.
2341 2341
2342 2342 jobs specifies the number of jobs to run concurrently. Each test
2343 2343 executes on its own thread. Tests actually spawn new processes, so
2344 2344 state mutation should not be an issue.
2345 2345
2346 2346 If there is only one job, it will use the main thread.
2347 2347
2348 2348 whitelist and blacklist denote tests that have been whitelisted and
2349 2349 blacklisted, respectively. These arguments don't belong in TestSuite.
2350 2350 Instead, whitelist and blacklist should be handled by the thing that
2351 2351 populates the TestSuite with tests. They are present to preserve
2352 2352 backwards compatible behavior which reports skipped tests as part
2353 2353 of the results.
2354 2354
2355 2355 retest denotes whether to retest failed tests. This arguably belongs
2356 2356 outside of TestSuite.
2357 2357
2358 2358 keywords denotes key words that will be used to filter which tests
2359 2359 to execute. This arguably belongs outside of TestSuite.
2360 2360
2361 2361 loop denotes whether to loop over tests forever.
2362 2362 """
2363 2363 super(TestSuite, self).__init__(*args, **kwargs)
2364 2364
2365 2365 self._jobs = jobs
2366 2366 self._whitelist = whitelist
2367 2367 self._blacklist = blacklist
2368 2368 self._retest = retest
2369 2369 self._keywords = keywords
2370 2370 self._loop = loop
2371 2371 self._runs_per_test = runs_per_test
2372 2372 self._loadtest = loadtest
2373 2373 self._showchannels = showchannels
2374 2374
2375 2375 def run(self, result):
2376 2376 # We have a number of filters that need to be applied. We do this
2377 2377 # here instead of inside Test because it makes the running logic for
2378 2378 # Test simpler.
2379 2379 tests = []
2380 2380 num_tests = [0]
2381 2381 for test in self._tests:
2382 2382
2383 2383 def get():
2384 2384 num_tests[0] += 1
2385 2385 if getattr(test, 'should_reload', False):
2386 2386 return self._loadtest(test, num_tests[0])
2387 2387 return test
2388 2388
2389 2389 if not os.path.exists(test.path):
2390 2390 result.addSkip(test, "Doesn't exist")
2391 2391 continue
2392 2392
2393 2393 if not (self._whitelist and test.bname in self._whitelist):
2394 2394 if self._blacklist and test.bname in self._blacklist:
2395 2395 result.addSkip(test, 'blacklisted')
2396 2396 continue
2397 2397
2398 2398 if self._retest and not os.path.exists(test.errpath):
2399 2399 result.addIgnore(test, 'not retesting')
2400 2400 continue
2401 2401
2402 2402 if self._keywords:
2403 2403 with open(test.path, 'rb') as f:
2404 2404 t = f.read().lower() + test.bname.lower()
2405 2405 ignored = False
2406 2406 for k in self._keywords.lower().split():
2407 2407 if k not in t:
2408 2408 result.addIgnore(test, "doesn't match keyword")
2409 2409 ignored = True
2410 2410 break
2411 2411
2412 2412 if ignored:
2413 2413 continue
2414 2414 for _ in xrange(self._runs_per_test):
2415 2415 tests.append(get())
2416 2416
2417 2417 runtests = list(tests)
2418 2418 done = queue.Queue()
2419 2419 running = 0
2420 2420
2421 2421 channels = [""] * self._jobs
2422 2422
2423 2423 def job(test, result):
2424 2424 for n, v in enumerate(channels):
2425 2425 if not v:
2426 2426 channel = n
2427 2427 break
2428 2428 else:
2429 2429 raise ValueError('Could not find output channel')
2430 2430 channels[channel] = "=" + test.name[5:].split(".")[0]
2431 2431 try:
2432 2432 test(result)
2433 2433 done.put(None)
2434 2434 except KeyboardInterrupt:
2435 2435 pass
2436 2436 except: # re-raises
2437 2437 done.put(('!', test, 'run-test raised an error, see traceback'))
2438 2438 raise
2439 2439 finally:
2440 2440 try:
2441 2441 channels[channel] = ''
2442 2442 except IndexError:
2443 2443 pass
2444 2444
2445 2445 def stat():
2446 2446 count = 0
2447 2447 while channels:
2448 2448 d = '\n%03s ' % count
2449 2449 for n, v in enumerate(channels):
2450 2450 if v:
2451 2451 d += v[0]
2452 2452 channels[n] = v[1:] or '.'
2453 2453 else:
2454 2454 d += ' '
2455 2455 d += ' '
2456 2456 with iolock:
2457 2457 sys.stdout.write(d + ' ')
2458 2458 sys.stdout.flush()
2459 2459 for x in xrange(10):
2460 2460 if channels:
2461 2461 time.sleep(0.1)
2462 2462 count += 1
2463 2463
2464 2464 stoppedearly = False
2465 2465
2466 2466 if self._showchannels:
2467 2467 statthread = threading.Thread(target=stat, name="stat")
2468 2468 statthread.start()
2469 2469
2470 2470 try:
2471 2471 while tests or running:
2472 2472 if not done.empty() or running == self._jobs or not tests:
2473 2473 try:
2474 2474 done.get(True, 1)
2475 2475 running -= 1
2476 2476 if result and result.shouldStop:
2477 2477 stoppedearly = True
2478 2478 break
2479 2479 except queue.Empty:
2480 2480 continue
2481 2481 if tests and not running == self._jobs:
2482 2482 test = tests.pop(0)
2483 2483 if self._loop:
2484 2484 if getattr(test, 'should_reload', False):
2485 2485 num_tests[0] += 1
2486 2486 tests.append(self._loadtest(test, num_tests[0]))
2487 2487 else:
2488 2488 tests.append(test)
2489 2489 if self._jobs == 1:
2490 2490 job(test, result)
2491 2491 else:
2492 2492 t = threading.Thread(
2493 2493 target=job, name=test.name, args=(test, result)
2494 2494 )
2495 2495 t.start()
2496 2496 running += 1
2497 2497
2498 2498 # If we stop early we still need to wait on started tests to
2499 2499 # finish. Otherwise, there is a race between the test completing
2500 2500 # and the test's cleanup code running. This could result in the
2501 2501 # test reporting incorrect.
2502 2502 if stoppedearly:
2503 2503 while running:
2504 2504 try:
2505 2505 done.get(True, 1)
2506 2506 running -= 1
2507 2507 except queue.Empty:
2508 2508 continue
2509 2509 except KeyboardInterrupt:
2510 2510 for test in runtests:
2511 2511 test.abort()
2512 2512
2513 2513 channels = []
2514 2514
2515 2515 return result
2516 2516
2517 2517
2518 2518 # Save the most recent 5 wall-clock runtimes of each test to a
2519 2519 # human-readable text file named .testtimes. Tests are sorted
2520 2520 # alphabetically, while times for each test are listed from oldest to
2521 2521 # newest.
2522 2522
2523 2523
2524 2524 def loadtimes(outputdir):
2525 2525 times = []
2526 2526 try:
2527 2527 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2528 2528 for line in fp:
2529 2529 m = re.match('(.*?) ([0-9. ]+)', line)
2530 2530 times.append(
2531 2531 (m.group(1), [float(t) for t in m.group(2).split()])
2532 2532 )
2533 2533 except IOError as err:
2534 2534 if err.errno != errno.ENOENT:
2535 2535 raise
2536 2536 return times
2537 2537
2538 2538
2539 2539 def savetimes(outputdir, result):
2540 2540 saved = dict(loadtimes(outputdir))
2541 2541 maxruns = 5
2542 2542 skipped = {str(t[0]) for t in result.skipped}
2543 2543 for tdata in result.times:
2544 2544 test, real = tdata[0], tdata[3]
2545 2545 if test not in skipped:
2546 2546 ts = saved.setdefault(test, [])
2547 2547 ts.append(real)
2548 2548 ts[:] = ts[-maxruns:]
2549 2549
2550 2550 fd, tmpname = tempfile.mkstemp(
2551 2551 prefix=b'.testtimes', dir=outputdir, text=True
2552 2552 )
2553 2553 with os.fdopen(fd, 'w') as fp:
2554 2554 for name, ts in sorted(saved.items()):
2555 2555 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2556 2556 timepath = os.path.join(outputdir, b'.testtimes')
2557 2557 try:
2558 2558 os.unlink(timepath)
2559 2559 except OSError:
2560 2560 pass
2561 2561 try:
2562 2562 os.rename(tmpname, timepath)
2563 2563 except OSError:
2564 2564 pass
2565 2565
2566 2566
2567 2567 class TextTestRunner(unittest.TextTestRunner):
2568 2568 """Custom unittest test runner that uses appropriate settings."""
2569 2569
2570 2570 def __init__(self, runner, *args, **kwargs):
2571 2571 super(TextTestRunner, self).__init__(*args, **kwargs)
2572 2572
2573 2573 self._runner = runner
2574 2574
2575 2575 self._result = getTestResult()(
2576 2576 self._runner.options, self.stream, self.descriptions, self.verbosity
2577 2577 )
2578 2578
2579 2579 def listtests(self, test):
2580 2580 test = sorted(test, key=lambda t: t.name)
2581 2581
2582 2582 self._result.onStart(test)
2583 2583
2584 2584 for t in test:
2585 2585 print(t.name)
2586 2586 self._result.addSuccess(t)
2587 2587
2588 2588 if self._runner.options.xunit:
2589 2589 with open(self._runner.options.xunit, "wb") as xuf:
2590 2590 self._writexunit(self._result, xuf)
2591 2591
2592 2592 if self._runner.options.json:
2593 2593 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2594 2594 with open(jsonpath, 'w') as fp:
2595 2595 self._writejson(self._result, fp)
2596 2596
2597 2597 return self._result
2598 2598
2599 2599 def run(self, test):
2600 2600 self._result.onStart(test)
2601 2601 test(self._result)
2602 2602
2603 2603 failed = len(self._result.failures)
2604 2604 skipped = len(self._result.skipped)
2605 2605 ignored = len(self._result.ignored)
2606 2606
2607 2607 with iolock:
2608 2608 self.stream.writeln('')
2609 2609
2610 2610 if not self._runner.options.noskips:
2611 2611 for test, msg in sorted(
2612 2612 self._result.skipped, key=lambda s: s[0].name
2613 2613 ):
2614 2614 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2615 2615 msg = highlightmsg(formatted, self._result.color)
2616 2616 self.stream.write(msg)
2617 2617 for test, msg in sorted(
2618 2618 self._result.failures, key=lambda f: f[0].name
2619 2619 ):
2620 2620 formatted = 'Failed %s: %s\n' % (test.name, msg)
2621 2621 self.stream.write(highlightmsg(formatted, self._result.color))
2622 2622 for test, msg in sorted(
2623 2623 self._result.errors, key=lambda e: e[0].name
2624 2624 ):
2625 2625 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2626 2626
2627 2627 if self._runner.options.xunit:
2628 2628 with open(self._runner.options.xunit, "wb") as xuf:
2629 2629 self._writexunit(self._result, xuf)
2630 2630
2631 2631 if self._runner.options.json:
2632 2632 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2633 2633 with open(jsonpath, 'w') as fp:
2634 2634 self._writejson(self._result, fp)
2635 2635
2636 2636 self._runner._checkhglib('Tested')
2637 2637
2638 2638 savetimes(self._runner._outputdir, self._result)
2639 2639
2640 2640 if failed and self._runner.options.known_good_rev:
2641 2641 self._bisecttests(t for t, m in self._result.failures)
2642 2642 self.stream.writeln(
2643 2643 '# Ran %d tests, %d skipped, %d failed.'
2644 2644 % (self._result.testsRun, skipped + ignored, failed)
2645 2645 )
2646 2646 if failed:
2647 2647 self.stream.writeln(
2648 2648 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2649 2649 )
2650 2650 if self._runner.options.time:
2651 2651 self.printtimes(self._result.times)
2652 2652
2653 2653 if self._runner.options.exceptions:
2654 2654 exceptions = aggregateexceptions(
2655 2655 os.path.join(self._runner._outputdir, b'exceptions')
2656 2656 )
2657 2657
2658 2658 self.stream.writeln('Exceptions Report:')
2659 2659 self.stream.writeln(
2660 2660 '%d total from %d frames'
2661 2661 % (exceptions['total'], len(exceptions['exceptioncounts']))
2662 2662 )
2663 2663 combined = exceptions['combined']
2664 2664 for key in sorted(combined, key=combined.get, reverse=True):
2665 2665 frame, line, exc = key
2666 2666 totalcount, testcount, leastcount, leasttest = combined[key]
2667 2667
2668 2668 self.stream.writeln(
2669 2669 '%d (%d tests)\t%s: %s (%s - %d total)'
2670 2670 % (
2671 2671 totalcount,
2672 2672 testcount,
2673 2673 frame,
2674 2674 exc,
2675 2675 leasttest,
2676 2676 leastcount,
2677 2677 )
2678 2678 )
2679 2679
2680 2680 self.stream.flush()
2681 2681
2682 2682 return self._result
2683 2683
2684 2684 def _bisecttests(self, tests):
2685 2685 bisectcmd = ['hg', 'bisect']
2686 2686 bisectrepo = self._runner.options.bisect_repo
2687 2687 if bisectrepo:
2688 2688 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2689 2689
2690 2690 def pread(args):
2691 2691 env = os.environ.copy()
2692 2692 env['HGPLAIN'] = '1'
2693 2693 p = subprocess.Popen(
2694 2694 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2695 2695 )
2696 2696 data = p.stdout.read()
2697 2697 p.wait()
2698 2698 return data
2699 2699
2700 2700 for test in tests:
2701 2701 pread(bisectcmd + ['--reset']),
2702 2702 pread(bisectcmd + ['--bad', '.'])
2703 2703 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2704 2704 # TODO: we probably need to forward more options
2705 2705 # that alter hg's behavior inside the tests.
2706 2706 opts = ''
2707 2707 withhg = self._runner.options.with_hg
2708 2708 if withhg:
2709 2709 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2710 2710 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2711 2711 data = pread(bisectcmd + ['--command', rtc])
2712 2712 m = re.search(
2713 2713 (
2714 2714 br'\nThe first (?P<goodbad>bad|good) revision '
2715 2715 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2716 2716 br'summary: +(?P<summary>[^\n]+)\n'
2717 2717 ),
2718 2718 data,
2719 2719 (re.MULTILINE | re.DOTALL),
2720 2720 )
2721 2721 if m is None:
2722 2722 self.stream.writeln(
2723 2723 'Failed to identify failure point for %s' % test
2724 2724 )
2725 2725 continue
2726 2726 dat = m.groupdict()
2727 2727 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2728 2728 self.stream.writeln(
2729 2729 '%s %s by %s (%s)'
2730 2730 % (
2731 2731 test,
2732 2732 verb,
2733 2733 dat['node'].decode('ascii'),
2734 2734 dat['summary'].decode('utf8', 'ignore'),
2735 2735 )
2736 2736 )
2737 2737
2738 2738 def printtimes(self, times):
2739 2739 # iolock held by run
2740 2740 self.stream.writeln('# Producing time report')
2741 2741 times.sort(key=lambda t: (t[3]))
2742 2742 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2743 2743 self.stream.writeln(
2744 2744 '%-7s %-7s %-7s %-7s %-7s %s'
2745 2745 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2746 2746 )
2747 2747 for tdata in times:
2748 2748 test = tdata[0]
2749 2749 cuser, csys, real, start, end = tdata[1:6]
2750 2750 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2751 2751
2752 2752 @staticmethod
2753 2753 def _writexunit(result, outf):
2754 2754 # See http://llg.cubic.org/docs/junit/ for a reference.
2755 2755 timesd = {t[0]: t[3] for t in result.times}
2756 2756 doc = minidom.Document()
2757 2757 s = doc.createElement('testsuite')
2758 2758 s.setAttribute('errors', "0") # TODO
2759 2759 s.setAttribute('failures', str(len(result.failures)))
2760 2760 s.setAttribute('name', 'run-tests')
2761 2761 s.setAttribute(
2762 2762 'skipped', str(len(result.skipped) + len(result.ignored))
2763 2763 )
2764 2764 s.setAttribute('tests', str(result.testsRun))
2765 2765 doc.appendChild(s)
2766 2766 for tc in result.successes:
2767 2767 t = doc.createElement('testcase')
2768 2768 t.setAttribute('name', tc.name)
2769 2769 tctime = timesd.get(tc.name)
2770 2770 if tctime is not None:
2771 2771 t.setAttribute('time', '%.3f' % tctime)
2772 2772 s.appendChild(t)
2773 2773 for tc, err in sorted(result.faildata.items()):
2774 2774 t = doc.createElement('testcase')
2775 2775 t.setAttribute('name', tc)
2776 2776 tctime = timesd.get(tc)
2777 2777 if tctime is not None:
2778 2778 t.setAttribute('time', '%.3f' % tctime)
2779 2779 # createCDATASection expects a unicode or it will
2780 2780 # convert using default conversion rules, which will
2781 2781 # fail if string isn't ASCII.
2782 2782 err = cdatasafe(err).decode('utf-8', 'replace')
2783 2783 cd = doc.createCDATASection(err)
2784 2784 # Use 'failure' here instead of 'error' to match errors = 0,
2785 2785 # failures = len(result.failures) in the testsuite element.
2786 2786 failelem = doc.createElement('failure')
2787 2787 failelem.setAttribute('message', 'output changed')
2788 2788 failelem.setAttribute('type', 'output-mismatch')
2789 2789 failelem.appendChild(cd)
2790 2790 t.appendChild(failelem)
2791 2791 s.appendChild(t)
2792 2792 for tc, message in result.skipped:
2793 2793 # According to the schema, 'skipped' has no attributes. So store
2794 2794 # the skip message as a text node instead.
2795 2795 t = doc.createElement('testcase')
2796 2796 t.setAttribute('name', tc.name)
2797 2797 binmessage = message.encode('utf-8')
2798 2798 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2799 2799 cd = doc.createCDATASection(message)
2800 2800 skipelem = doc.createElement('skipped')
2801 2801 skipelem.appendChild(cd)
2802 2802 t.appendChild(skipelem)
2803 2803 s.appendChild(t)
2804 2804 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2805 2805
2806 2806 @staticmethod
2807 2807 def _writejson(result, outf):
2808 2808 timesd = {}
2809 2809 for tdata in result.times:
2810 2810 test = tdata[0]
2811 2811 timesd[test] = tdata[1:]
2812 2812
2813 2813 outcome = {}
2814 2814 groups = [
2815 2815 ('success', ((tc, None) for tc in result.successes)),
2816 2816 ('failure', result.failures),
2817 2817 ('skip', result.skipped),
2818 2818 ]
2819 2819 for res, testcases in groups:
2820 2820 for tc, __ in testcases:
2821 2821 if tc.name in timesd:
2822 2822 diff = result.faildata.get(tc.name, b'')
2823 2823 try:
2824 2824 diff = diff.decode('unicode_escape')
2825 2825 except UnicodeDecodeError as e:
2826 2826 diff = '%r decoding diff, sorry' % e
2827 2827 tres = {
2828 2828 'result': res,
2829 2829 'time': ('%0.3f' % timesd[tc.name][2]),
2830 2830 'cuser': ('%0.3f' % timesd[tc.name][0]),
2831 2831 'csys': ('%0.3f' % timesd[tc.name][1]),
2832 2832 'start': ('%0.3f' % timesd[tc.name][3]),
2833 2833 'end': ('%0.3f' % timesd[tc.name][4]),
2834 2834 'diff': diff,
2835 2835 }
2836 2836 else:
2837 2837 # blacklisted test
2838 2838 tres = {'result': res}
2839 2839
2840 2840 outcome[tc.name] = tres
2841 2841 jsonout = json.dumps(
2842 2842 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2843 2843 )
2844 2844 outf.writelines(("testreport =", jsonout))
2845 2845
2846 2846
2847 2847 def sorttests(testdescs, previoustimes, shuffle=False):
2848 2848 """Do an in-place sort of tests."""
2849 2849 if shuffle:
2850 2850 random.shuffle(testdescs)
2851 2851 return
2852 2852
2853 2853 if previoustimes:
2854 2854
2855 2855 def sortkey(f):
2856 2856 f = f['path']
2857 2857 if f in previoustimes:
2858 2858 # Use most recent time as estimate
2859 2859 return -(previoustimes[f][-1])
2860 2860 else:
2861 2861 # Default to a rather arbitrary value of 1 second for new tests
2862 2862 return -1.0
2863 2863
2864 2864 else:
2865 2865 # keywords for slow tests
2866 2866 slow = {
2867 2867 b'svn': 10,
2868 2868 b'cvs': 10,
2869 2869 b'hghave': 10,
2870 2870 b'largefiles-update': 10,
2871 2871 b'run-tests': 10,
2872 2872 b'corruption': 10,
2873 2873 b'race': 10,
2874 2874 b'i18n': 10,
2875 2875 b'check': 100,
2876 2876 b'gendoc': 100,
2877 2877 b'contrib-perf': 200,
2878 2878 b'merge-combination': 100,
2879 2879 }
2880 2880 perf = {}
2881 2881
2882 2882 def sortkey(f):
2883 2883 # run largest tests first, as they tend to take the longest
2884 2884 f = f['path']
2885 2885 try:
2886 2886 return perf[f]
2887 2887 except KeyError:
2888 2888 try:
2889 2889 val = -os.stat(f).st_size
2890 2890 except OSError as e:
2891 2891 if e.errno != errno.ENOENT:
2892 2892 raise
2893 2893 perf[f] = -1e9 # file does not exist, tell early
2894 2894 return -1e9
2895 2895 for kw, mul in slow.items():
2896 2896 if kw in f:
2897 2897 val *= mul
2898 2898 if f.endswith(b'.py'):
2899 2899 val /= 10.0
2900 2900 perf[f] = val / 1000.0
2901 2901 return perf[f]
2902 2902
2903 2903 testdescs.sort(key=sortkey)
2904 2904
2905 2905
2906 2906 class TestRunner(object):
2907 2907 """Holds context for executing tests.
2908 2908
2909 2909 Tests rely on a lot of state. This object holds it for them.
2910 2910 """
2911 2911
2912 2912 # Programs required to run tests.
2913 2913 REQUIREDTOOLS = [
2914 2914 b'diff',
2915 2915 b'grep',
2916 2916 b'unzip',
2917 2917 b'gunzip',
2918 2918 b'bunzip2',
2919 2919 b'sed',
2920 2920 ]
2921 2921
2922 2922 # Maps file extensions to test class.
2923 2923 TESTTYPES = [
2924 2924 (b'.py', PythonTest),
2925 2925 (b'.t', TTest),
2926 2926 ]
2927 2927
2928 2928 def __init__(self):
2929 2929 self.options = None
2930 2930 self._hgroot = None
2931 2931 self._testdir = None
2932 2932 self._outputdir = None
2933 2933 self._hgtmp = None
2934 2934 self._installdir = None
2935 2935 self._bindir = None
2936 2936 self._tmpbinddir = None
2937 2937 self._pythondir = None
2938 2938 self._coveragefile = None
2939 2939 self._createdfiles = []
2940 2940 self._hgcommand = None
2941 2941 self._hgpath = None
2942 2942 self._portoffset = 0
2943 2943 self._ports = {}
2944 2944
2945 2945 def run(self, args, parser=None):
2946 2946 """Run the test suite."""
2947 2947 oldmask = os.umask(0o22)
2948 2948 try:
2949 2949 parser = parser or getparser()
2950 2950 options = parseargs(args, parser)
2951 2951 tests = [_sys2bytes(a) for a in options.tests]
2952 2952 if options.test_list is not None:
2953 2953 for listfile in options.test_list:
2954 2954 with open(listfile, 'rb') as f:
2955 2955 tests.extend(t for t in f.read().splitlines() if t)
2956 2956 self.options = options
2957 2957
2958 2958 self._checktools()
2959 2959 testdescs = self.findtests(tests)
2960 2960 if options.profile_runner:
2961 2961 import statprof
2962 2962
2963 2963 statprof.start()
2964 2964 result = self._run(testdescs)
2965 2965 if options.profile_runner:
2966 2966 statprof.stop()
2967 2967 statprof.display()
2968 2968 return result
2969 2969
2970 2970 finally:
2971 2971 os.umask(oldmask)
2972 2972
2973 2973 def _run(self, testdescs):
2974 2974 testdir = getcwdb()
2975 2975 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2976 2976 # assume all tests in same folder for now
2977 2977 if testdescs:
2978 2978 pathname = os.path.dirname(testdescs[0]['path'])
2979 2979 if pathname:
2980 2980 testdir = os.path.join(testdir, pathname)
2981 2981 self._testdir = osenvironb[b'TESTDIR'] = testdir
2982 2982 if self.options.outputdir:
2983 2983 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
2984 2984 else:
2985 2985 self._outputdir = getcwdb()
2986 2986 if testdescs and pathname:
2987 2987 self._outputdir = os.path.join(self._outputdir, pathname)
2988 2988 previoustimes = {}
2989 2989 if self.options.order_by_runtime:
2990 2990 previoustimes = dict(loadtimes(self._outputdir))
2991 2991 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2992 2992
2993 2993 if 'PYTHONHASHSEED' not in os.environ:
2994 2994 # use a random python hash seed all the time
2995 2995 # we do the randomness ourself to know what seed is used
2996 2996 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2997 2997
2998 2998 # Rayon (Rust crate for multi-threading) will use all logical CPU cores
2999 2999 # by default, causing thrashing on high-cpu-count systems.
3000 3000 # Setting its limit to 3 during tests should still let us uncover
3001 3001 # multi-threading bugs while keeping the thrashing reasonable.
3002 3002 os.environ.setdefault("RAYON_NUM_THREADS", "3")
3003 3003
3004 3004 if self.options.tmpdir:
3005 3005 self.options.keep_tmpdir = True
3006 3006 tmpdir = _sys2bytes(self.options.tmpdir)
3007 3007 if os.path.exists(tmpdir):
3008 3008 # Meaning of tmpdir has changed since 1.3: we used to create
3009 3009 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
3010 3010 # tmpdir already exists.
3011 3011 print("error: temp dir %r already exists" % tmpdir)
3012 3012 return 1
3013 3013
3014 3014 os.makedirs(tmpdir)
3015 3015 else:
3016 3016 d = None
3017 3017 if os.name == 'nt':
3018 3018 # without this, we get the default temp dir location, but
3019 3019 # in all lowercase, which causes troubles with paths (issue3490)
3020 3020 d = osenvironb.get(b'TMP', None)
3021 3021 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
3022 3022
3023 3023 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3024 3024
3025 3025 if self.options.with_hg:
3026 3026 self._installdir = None
3027 3027 whg = self.options.with_hg
3028 3028 self._bindir = os.path.dirname(os.path.realpath(whg))
3029 3029 assert isinstance(self._bindir, bytes)
3030 3030 self._hgcommand = os.path.basename(whg)
3031 3031 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
3032 3032 os.makedirs(self._tmpbindir)
3033 3033
3034 3034 normbin = os.path.normpath(os.path.abspath(whg))
3035 3035 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3036 3036
3037 3037 # Other Python scripts in the test harness need to
3038 3038 # `import mercurial`. If `hg` is a Python script, we assume
3039 3039 # the Mercurial modules are relative to its path and tell the tests
3040 3040 # to load Python modules from its directory.
3041 3041 with open(whg, 'rb') as fh:
3042 3042 initial = fh.read(1024)
3043 3043
3044 3044 if re.match(b'#!.*python', initial):
3045 3045 self._pythondir = self._bindir
3046 3046 # If it looks like our in-repo Rust binary, use the source root.
3047 3047 # This is a bit hacky. But rhg is still not supported outside the
3048 3048 # source directory. So until it is, do the simple thing.
3049 3049 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3050 3050 self._pythondir = os.path.dirname(self._testdir)
3051 3051 # Fall back to the legacy behavior.
3052 3052 else:
3053 3053 self._pythondir = self._bindir
3054 3054
3055 3055 else:
3056 3056 self._installdir = os.path.join(self._hgtmp, b"install")
3057 3057 self._bindir = os.path.join(self._installdir, b"bin")
3058 3058 self._hgcommand = b'hg'
3059 3059 self._tmpbindir = self._bindir
3060 3060 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3061 3061
3062 3062 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3063 3063 # a python script and feed it to python.exe. Legacy stdio is force
3064 3064 # enabled by hg.exe, and this is a more realistic way to launch hg
3065 3065 # anyway.
3066 3066 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
3067 3067 self._hgcommand += b'.exe'
3068 3068
3069 3069 # set CHGHG, then replace "hg" command by "chg"
3070 3070 chgbindir = self._bindir
3071 3071 if self.options.chg or self.options.with_chg:
3072 3072 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
3073 3073 else:
3074 3074 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
3075 3075 if self.options.chg:
3076 3076 self._hgcommand = b'chg'
3077 3077 elif self.options.with_chg:
3078 3078 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3079 3079 self._hgcommand = os.path.basename(self.options.with_chg)
3080 3080
3081 3081 osenvironb[b"BINDIR"] = self._bindir
3082 3082 osenvironb[b"PYTHON"] = PYTHON
3083 3083
3084 3084 fileb = _sys2bytes(__file__)
3085 3085 runtestdir = os.path.abspath(os.path.dirname(fileb))
3086 3086 osenvironb[b'RUNTESTDIR'] = runtestdir
3087 3087 if PYTHON3:
3088 3088 sepb = _sys2bytes(os.pathsep)
3089 3089 else:
3090 3090 sepb = os.pathsep
3091 3091 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3092 3092 if os.path.islink(__file__):
3093 3093 # test helper will likely be at the end of the symlink
3094 3094 realfile = os.path.realpath(fileb)
3095 3095 realdir = os.path.abspath(os.path.dirname(realfile))
3096 3096 path.insert(2, realdir)
3097 3097 if chgbindir != self._bindir:
3098 3098 path.insert(1, chgbindir)
3099 3099 if self._testdir != runtestdir:
3100 3100 path = [self._testdir] + path
3101 3101 if self._tmpbindir != self._bindir:
3102 3102 path = [self._tmpbindir] + path
3103 3103 osenvironb[b"PATH"] = sepb.join(path)
3104 3104
3105 3105 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3106 3106 # can run .../tests/run-tests.py test-foo where test-foo
3107 3107 # adds an extension to HGRC. Also include run-test.py directory to
3108 3108 # import modules like heredoctest.
3109 3109 pypath = [self._pythondir, self._testdir, runtestdir]
3110 3110 # We have to augment PYTHONPATH, rather than simply replacing
3111 3111 # it, in case external libraries are only available via current
3112 3112 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3113 3113 # are in /opt/subversion.)
3114 3114 oldpypath = osenvironb.get(IMPL_PATH)
3115 3115 if oldpypath:
3116 3116 pypath.append(oldpypath)
3117 3117 osenvironb[IMPL_PATH] = sepb.join(pypath)
3118 3118
3119 3119 if self.options.pure:
3120 3120 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3121 3121 os.environ["HGMODULEPOLICY"] = "py"
3122 3122 if self.options.rust:
3123 3123 os.environ["HGMODULEPOLICY"] = "rust+c"
3124 3124 if self.options.no_rust:
3125 3125 current_policy = os.environ.get("HGMODULEPOLICY", "")
3126 3126 if current_policy.startswith("rust+"):
3127 3127 os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :]
3128 3128 os.environ.pop("HGWITHRUSTEXT", None)
3129 3129
3130 3130 if self.options.allow_slow_tests:
3131 3131 os.environ["HGTEST_SLOW"] = "slow"
3132 3132 elif 'HGTEST_SLOW' in os.environ:
3133 3133 del os.environ['HGTEST_SLOW']
3134 3134
3135 3135 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3136 3136
3137 3137 if self.options.exceptions:
3138 3138 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3139 3139 try:
3140 3140 os.makedirs(exceptionsdir)
3141 3141 except OSError as e:
3142 3142 if e.errno != errno.EEXIST:
3143 3143 raise
3144 3144
3145 3145 # Remove all existing exception reports.
3146 3146 for f in os.listdir(exceptionsdir):
3147 3147 os.unlink(os.path.join(exceptionsdir, f))
3148 3148
3149 3149 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3150 3150 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3151 3151 self.options.extra_config_opt.append(
3152 3152 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3153 3153 )
3154 3154
3155 3155 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3156 3156 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3157 3157 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3158 3158 vlog("# Using PATH", os.environ["PATH"])
3159 3159 vlog(
3160 3160 "# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]),
3161 3161 )
3162 3162 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3163 3163
3164 3164 try:
3165 3165 return self._runtests(testdescs) or 0
3166 3166 finally:
3167 3167 time.sleep(0.1)
3168 3168 self._cleanup()
3169 3169
3170 3170 def findtests(self, args):
3171 3171 """Finds possible test files from arguments.
3172 3172
3173 3173 If you wish to inject custom tests into the test harness, this would
3174 3174 be a good function to monkeypatch or override in a derived class.
3175 3175 """
3176 3176 if not args:
3177 3177 if self.options.changed:
3178 3178 proc = Popen4(
3179 3179 b'hg st --rev "%s" -man0 .'
3180 3180 % _sys2bytes(self.options.changed),
3181 3181 None,
3182 3182 0,
3183 3183 )
3184 3184 stdout, stderr = proc.communicate()
3185 3185 args = stdout.strip(b'\0').split(b'\0')
3186 3186 else:
3187 3187 args = os.listdir(b'.')
3188 3188
3189 3189 expanded_args = []
3190 3190 for arg in args:
3191 3191 if os.path.isdir(arg):
3192 3192 if not arg.endswith(b'/'):
3193 3193 arg += b'/'
3194 3194 expanded_args.extend([arg + a for a in os.listdir(arg)])
3195 3195 else:
3196 3196 expanded_args.append(arg)
3197 3197 args = expanded_args
3198 3198
3199 3199 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3200 3200 tests = []
3201 3201 for t in args:
3202 3202 case = []
3203 3203
3204 3204 if not (
3205 3205 os.path.basename(t).startswith(b'test-')
3206 3206 and (t.endswith(b'.py') or t.endswith(b'.t'))
3207 3207 ):
3208 3208
3209 3209 m = testcasepattern.match(os.path.basename(t))
3210 3210 if m is not None:
3211 3211 t_basename, casestr = m.groups()
3212 3212 t = os.path.join(os.path.dirname(t), t_basename)
3213 3213 if casestr:
3214 3214 case = casestr.split(b'#')
3215 3215 else:
3216 3216 continue
3217 3217
3218 3218 if t.endswith(b'.t'):
3219 3219 # .t file may contain multiple test cases
3220 3220 casedimensions = parsettestcases(t)
3221 3221 if casedimensions:
3222 3222 cases = []
3223 3223
3224 3224 def addcases(case, casedimensions):
3225 3225 if not casedimensions:
3226 3226 cases.append(case)
3227 3227 else:
3228 3228 for c in casedimensions[0]:
3229 3229 addcases(case + [c], casedimensions[1:])
3230 3230
3231 3231 addcases([], casedimensions)
3232 3232 if case and case in cases:
3233 3233 cases = [case]
3234 3234 elif case:
3235 3235 # Ignore invalid cases
3236 3236 cases = []
3237 3237 else:
3238 3238 pass
3239 3239 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3240 3240 else:
3241 3241 tests.append({'path': t})
3242 3242 else:
3243 3243 tests.append({'path': t})
3244 3244 return tests
3245 3245
3246 3246 def _runtests(self, testdescs):
3247 3247 def _reloadtest(test, i):
3248 3248 # convert a test back to its description dict
3249 3249 desc = {'path': test.path}
3250 3250 case = getattr(test, '_case', [])
3251 3251 if case:
3252 3252 desc['case'] = case
3253 3253 return self._gettest(desc, i)
3254 3254
3255 3255 try:
3256 3256 if self.options.restart:
3257 3257 orig = list(testdescs)
3258 3258 while testdescs:
3259 3259 desc = testdescs[0]
3260 3260 # desc['path'] is a relative path
3261 3261 if 'case' in desc:
3262 3262 casestr = b'#'.join(desc['case'])
3263 3263 errpath = b'%s#%s.err' % (desc['path'], casestr)
3264 3264 else:
3265 3265 errpath = b'%s.err' % desc['path']
3266 3266 errpath = os.path.join(self._outputdir, errpath)
3267 3267 if os.path.exists(errpath):
3268 3268 break
3269 3269 testdescs.pop(0)
3270 3270 if not testdescs:
3271 3271 print("running all tests")
3272 3272 testdescs = orig
3273 3273
3274 3274 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3275 3275 num_tests = len(tests) * self.options.runs_per_test
3276 3276
3277 3277 jobs = min(num_tests, self.options.jobs)
3278 3278
3279 3279 failed = False
3280 3280 kws = self.options.keywords
3281 3281 if kws is not None and PYTHON3:
3282 3282 kws = kws.encode('utf-8')
3283 3283
3284 3284 suite = TestSuite(
3285 3285 self._testdir,
3286 3286 jobs=jobs,
3287 3287 whitelist=self.options.whitelisted,
3288 3288 blacklist=self.options.blacklist,
3289 3289 retest=self.options.retest,
3290 3290 keywords=kws,
3291 3291 loop=self.options.loop,
3292 3292 runs_per_test=self.options.runs_per_test,
3293 3293 showchannels=self.options.showchannels,
3294 3294 tests=tests,
3295 3295 loadtest=_reloadtest,
3296 3296 )
3297 3297 verbosity = 1
3298 3298 if self.options.list_tests:
3299 3299 verbosity = 0
3300 3300 elif self.options.verbose:
3301 3301 verbosity = 2
3302 3302 runner = TextTestRunner(self, verbosity=verbosity)
3303 3303
3304 3304 if self.options.list_tests:
3305 3305 result = runner.listtests(suite)
3306 3306 else:
3307 3307 if self._installdir:
3308 3308 self._installhg()
3309 3309 self._checkhglib("Testing")
3310 3310 else:
3311 3311 self._usecorrectpython()
3312 3312 if self.options.chg:
3313 3313 assert self._installdir
3314 3314 self._installchg()
3315 3315
3316 3316 log(
3317 3317 'running %d tests using %d parallel processes'
3318 3318 % (num_tests, jobs)
3319 3319 )
3320 3320
3321 3321 result = runner.run(suite)
3322 3322
3323 3323 if result.failures or result.errors:
3324 3324 failed = True
3325 3325
3326 3326 result.onEnd()
3327 3327
3328 3328 if self.options.anycoverage:
3329 3329 self._outputcoverage()
3330 3330 except KeyboardInterrupt:
3331 3331 failed = True
3332 3332 print("\ninterrupted!")
3333 3333
3334 3334 if failed:
3335 3335 return 1
3336 3336
3337 3337 def _getport(self, count):
3338 3338 port = self._ports.get(count) # do we have a cached entry?
3339 3339 if port is None:
3340 3340 portneeded = 3
3341 3341 # above 100 tries we just give up and let test reports failure
3342 3342 for tries in xrange(100):
3343 3343 allfree = True
3344 3344 port = self.options.port + self._portoffset
3345 3345 for idx in xrange(portneeded):
3346 3346 if not checkportisavailable(port + idx):
3347 3347 allfree = False
3348 3348 break
3349 3349 self._portoffset += portneeded
3350 3350 if allfree:
3351 3351 break
3352 3352 self._ports[count] = port
3353 3353 return port
3354 3354
3355 3355 def _gettest(self, testdesc, count):
3356 3356 """Obtain a Test by looking at its filename.
3357 3357
3358 3358 Returns a Test instance. The Test may not be runnable if it doesn't
3359 3359 map to a known type.
3360 3360 """
3361 3361 path = testdesc['path']
3362 3362 lctest = path.lower()
3363 3363 testcls = Test
3364 3364
3365 3365 for ext, cls in self.TESTTYPES:
3366 3366 if lctest.endswith(ext):
3367 3367 testcls = cls
3368 3368 break
3369 3369
3370 3370 refpath = os.path.join(getcwdb(), path)
3371 3371 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3372 3372
3373 3373 # extra keyword parameters. 'case' is used by .t tests
3374 3374 kwds = {k: testdesc[k] for k in ['case'] if k in testdesc}
3375 3375
3376 3376 t = testcls(
3377 3377 refpath,
3378 3378 self._outputdir,
3379 3379 tmpdir,
3380 3380 keeptmpdir=self.options.keep_tmpdir,
3381 3381 debug=self.options.debug,
3382 3382 first=self.options.first,
3383 3383 timeout=self.options.timeout,
3384 3384 startport=self._getport(count),
3385 3385 extraconfigopts=self.options.extra_config_opt,
3386 3386 shell=self.options.shell,
3387 3387 hgcommand=self._hgcommand,
3388 3388 usechg=bool(self.options.with_chg or self.options.chg),
3389 3389 useipv6=useipv6,
3390 3390 **kwds
3391 3391 )
3392 3392 t.should_reload = True
3393 3393 return t
3394 3394
3395 3395 def _cleanup(self):
3396 3396 """Clean up state from this test invocation."""
3397 3397 if self.options.keep_tmpdir:
3398 3398 return
3399 3399
3400 3400 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3401 3401 shutil.rmtree(self._hgtmp, True)
3402 3402 for f in self._createdfiles:
3403 3403 try:
3404 3404 os.remove(f)
3405 3405 except OSError:
3406 3406 pass
3407 3407
3408 3408 def _usecorrectpython(self):
3409 3409 """Configure the environment to use the appropriate Python in tests."""
3410 3410 # Tests must use the same interpreter as us or bad things will happen.
3411 3411 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3412 3412
3413 3413 # os.symlink() is a thing with py3 on Windows, but it requires
3414 3414 # Administrator rights.
3415 3415 if getattr(os, 'symlink', None) and os.name != 'nt':
3416 3416 vlog(
3417 3417 "# Making python executable in test path a symlink to '%s'"
3418 3418 % sysexecutable
3419 3419 )
3420 3420 mypython = os.path.join(self._tmpbindir, pyexename)
3421 3421 try:
3422 3422 if os.readlink(mypython) == sysexecutable:
3423 3423 return
3424 3424 os.unlink(mypython)
3425 3425 except OSError as err:
3426 3426 if err.errno != errno.ENOENT:
3427 3427 raise
3428 3428 if self._findprogram(pyexename) != sysexecutable:
3429 3429 try:
3430 3430 os.symlink(sysexecutable, mypython)
3431 3431 self._createdfiles.append(mypython)
3432 3432 except OSError as err:
3433 3433 # child processes may race, which is harmless
3434 3434 if err.errno != errno.EEXIST:
3435 3435 raise
3436 3436 else:
3437 3437 exedir, exename = os.path.split(sysexecutable)
3438 3438 vlog(
3439 3439 "# Modifying search path to find %s as %s in '%s'"
3440 3440 % (exename, pyexename, exedir)
3441 3441 )
3442 3442 path = os.environ['PATH'].split(os.pathsep)
3443 3443 while exedir in path:
3444 3444 path.remove(exedir)
3445 3445 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3446 3446 if not self._findprogram(pyexename):
3447 3447 print("WARNING: Cannot find %s in search path" % pyexename)
3448 3448
3449 3449 def _installhg(self):
3450 3450 """Install hg into the test environment.
3451 3451
3452 3452 This will also configure hg with the appropriate testing settings.
3453 3453 """
3454 3454 vlog("# Performing temporary installation of HG")
3455 3455 installerrs = os.path.join(self._hgtmp, b"install.err")
3456 3456 compiler = ''
3457 3457 if self.options.compiler:
3458 3458 compiler = '--compiler ' + self.options.compiler
3459 3459 setup_opts = b""
3460 3460 if self.options.pure:
3461 3461 setup_opts = b"--pure"
3462 3462 elif self.options.rust:
3463 3463 setup_opts = b"--rust"
3464 3464 elif self.options.no_rust:
3465 3465 setup_opts = b"--no-rust"
3466 3466
3467 3467 # Run installer in hg root
3468 3468 script = os.path.realpath(sys.argv[0])
3469 3469 exe = sysexecutable
3470 3470 if PYTHON3:
3471 3471 compiler = _sys2bytes(compiler)
3472 3472 script = _sys2bytes(script)
3473 3473 exe = _sys2bytes(exe)
3474 3474 hgroot = os.path.dirname(os.path.dirname(script))
3475 3475 self._hgroot = hgroot
3476 3476 os.chdir(hgroot)
3477 3477 nohome = b'--home=""'
3478 3478 if os.name == 'nt':
3479 3479 # The --home="" trick works only on OS where os.sep == '/'
3480 3480 # because of a distutils convert_path() fast-path. Avoid it at
3481 3481 # least on Windows for now, deal with .pydistutils.cfg bugs
3482 3482 # when they happen.
3483 3483 nohome = b''
3484 3484 cmd = (
3485 3485 b'"%(exe)s" setup.py %(setup_opts)s clean --all'
3486 3486 b' build %(compiler)s --build-base="%(base)s"'
3487 3487 b' install --force --prefix="%(prefix)s"'
3488 3488 b' --install-lib="%(libdir)s"'
3489 3489 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3490 3490 % {
3491 3491 b'exe': exe,
3492 3492 b'setup_opts': setup_opts,
3493 3493 b'compiler': compiler,
3494 3494 b'base': os.path.join(self._hgtmp, b"build"),
3495 3495 b'prefix': self._installdir,
3496 3496 b'libdir': self._pythondir,
3497 3497 b'bindir': self._bindir,
3498 3498 b'nohome': nohome,
3499 3499 b'logfile': installerrs,
3500 3500 }
3501 3501 )
3502 3502
3503 3503 # setuptools requires install directories to exist.
3504 3504 def makedirs(p):
3505 3505 try:
3506 3506 os.makedirs(p)
3507 3507 except OSError as e:
3508 3508 if e.errno != errno.EEXIST:
3509 3509 raise
3510 3510
3511 3511 makedirs(self._pythondir)
3512 3512 makedirs(self._bindir)
3513 3513
3514 3514 vlog("# Running", cmd.decode("utf-8"))
3515 3515 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3516 3516 if not self.options.verbose:
3517 3517 try:
3518 3518 os.remove(installerrs)
3519 3519 except OSError as e:
3520 3520 if e.errno != errno.ENOENT:
3521 3521 raise
3522 3522 else:
3523 3523 with open(installerrs, 'rb') as f:
3524 3524 for line in f:
3525 3525 if PYTHON3:
3526 3526 sys.stdout.buffer.write(line)
3527 3527 else:
3528 3528 sys.stdout.write(line)
3529 3529 sys.exit(1)
3530 3530 os.chdir(self._testdir)
3531 3531
3532 3532 self._usecorrectpython()
3533 3533
3534 3534 hgbat = os.path.join(self._bindir, b'hg.bat')
3535 3535 if os.path.isfile(hgbat):
3536 3536 # hg.bat expects to be put in bin/scripts while run-tests.py
3537 3537 # installation layout put it in bin/ directly. Fix it
3538 3538 with open(hgbat, 'rb') as f:
3539 3539 data = f.read()
3540 3540 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3541 3541 data = data.replace(
3542 3542 br'"%~dp0..\python" "%~dp0hg" %*',
3543 3543 b'"%~dp0python" "%~dp0hg" %*',
3544 3544 )
3545 3545 with open(hgbat, 'wb') as f:
3546 3546 f.write(data)
3547 3547 else:
3548 3548 print('WARNING: cannot fix hg.bat reference to python.exe')
3549 3549
3550 3550 if self.options.anycoverage:
3551 3551 custom = os.path.join(
3552 3552 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3553 3553 )
3554 3554 target = os.path.join(self._pythondir, b'sitecustomize.py')
3555 3555 vlog('# Installing coverage trigger to %s' % target)
3556 3556 shutil.copyfile(custom, target)
3557 3557 rc = os.path.join(self._testdir, b'.coveragerc')
3558 3558 vlog('# Installing coverage rc to %s' % rc)
3559 3559 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3560 3560 covdir = os.path.join(self._installdir, b'..', b'coverage')
3561 3561 try:
3562 3562 os.mkdir(covdir)
3563 3563 except OSError as e:
3564 3564 if e.errno != errno.EEXIST:
3565 3565 raise
3566 3566
3567 3567 osenvironb[b'COVERAGE_DIR'] = covdir
3568 3568
3569 3569 def _checkhglib(self, verb):
3570 3570 """Ensure that the 'mercurial' package imported by python is
3571 3571 the one we expect it to be. If not, print a warning to stderr."""
3572 3572 if (self._bindir == self._pythondir) and (
3573 3573 self._bindir != self._tmpbindir
3574 3574 ):
3575 3575 # The pythondir has been inferred from --with-hg flag.
3576 3576 # We cannot expect anything sensible here.
3577 3577 return
3578 3578 expecthg = os.path.join(self._pythondir, b'mercurial')
3579 3579 actualhg = self._gethgpath()
3580 3580 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3581 3581 sys.stderr.write(
3582 3582 'warning: %s with unexpected mercurial lib: %s\n'
3583 3583 ' (expected %s)\n' % (verb, actualhg, expecthg)
3584 3584 )
3585 3585
3586 3586 def _gethgpath(self):
3587 3587 """Return the path to the mercurial package that is actually found by
3588 3588 the current Python interpreter."""
3589 3589 if self._hgpath is not None:
3590 3590 return self._hgpath
3591 3591
3592 3592 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3593 3593 cmd = cmd % PYTHON
3594 3594 if PYTHON3:
3595 3595 cmd = _bytes2sys(cmd)
3596 3596
3597 3597 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3598 3598 out, err = p.communicate()
3599 3599
3600 3600 self._hgpath = out.strip()
3601 3601
3602 3602 return self._hgpath
3603 3603
3604 3604 def _installchg(self):
3605 3605 """Install chg into the test environment"""
3606 3606 vlog('# Performing temporary installation of CHG')
3607 3607 assert os.path.dirname(self._bindir) == self._installdir
3608 3608 assert self._hgroot, 'must be called after _installhg()'
3609 3609 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3610 3610 b'make': b'make', # TODO: switch by option or environment?
3611 3611 b'prefix': self._installdir,
3612 3612 }
3613 3613 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3614 3614 vlog("# Running", cmd)
3615 3615 proc = subprocess.Popen(
3616 3616 cmd,
3617 3617 shell=True,
3618 3618 cwd=cwd,
3619 3619 stdin=subprocess.PIPE,
3620 3620 stdout=subprocess.PIPE,
3621 3621 stderr=subprocess.STDOUT,
3622 3622 )
3623 3623 out, _err = proc.communicate()
3624 3624 if proc.returncode != 0:
3625 3625 if PYTHON3:
3626 3626 sys.stdout.buffer.write(out)
3627 3627 else:
3628 3628 sys.stdout.write(out)
3629 3629 sys.exit(1)
3630 3630
3631 3631 def _outputcoverage(self):
3632 3632 """Produce code coverage output."""
3633 3633 import coverage
3634 3634
3635 3635 coverage = coverage.coverage
3636 3636
3637 3637 vlog('# Producing coverage report')
3638 3638 # chdir is the easiest way to get short, relative paths in the
3639 3639 # output.
3640 3640 os.chdir(self._hgroot)
3641 3641 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3642 3642 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3643 3643
3644 3644 # Map install directory paths back to source directory.
3645 3645 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3646 3646
3647 3647 cov.combine()
3648 3648
3649 3649 omit = [
3650 3650 _bytes2sys(os.path.join(x, b'*'))
3651 3651 for x in [self._bindir, self._testdir]
3652 3652 ]
3653 3653 cov.report(ignore_errors=True, omit=omit)
3654 3654
3655 3655 if self.options.htmlcov:
3656 3656 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3657 3657 cov.html_report(directory=htmldir, omit=omit)
3658 3658 if self.options.annotate:
3659 3659 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3660 3660 if not os.path.isdir(adir):
3661 3661 os.mkdir(adir)
3662 3662 cov.annotate(directory=adir, omit=omit)
3663 3663
3664 3664 def _findprogram(self, program):
3665 3665 """Search PATH for a executable program"""
3666 3666 dpb = _sys2bytes(os.defpath)
3667 3667 sepb = _sys2bytes(os.pathsep)
3668 3668 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3669 3669 name = os.path.join(p, program)
3670 3670 if os.name == 'nt' or os.access(name, os.X_OK):
3671 3671 return name
3672 3672 return None
3673 3673
3674 3674 def _checktools(self):
3675 3675 """Ensure tools required to run tests are present."""
3676 3676 for p in self.REQUIREDTOOLS:
3677 3677 if os.name == 'nt' and not p.endswith(b'.exe'):
3678 3678 p += b'.exe'
3679 3679 found = self._findprogram(p)
3680 3680 p = p.decode("utf-8")
3681 3681 if found:
3682 3682 vlog("# Found prerequisite", p, "at", _bytes2sys(found))
3683 3683 else:
3684 3684 print("WARNING: Did not find prerequisite tool: %s " % p)
3685 3685
3686 3686
3687 3687 def aggregateexceptions(path):
3688 3688 exceptioncounts = collections.Counter()
3689 3689 testsbyfailure = collections.defaultdict(set)
3690 3690 failuresbytest = collections.defaultdict(set)
3691 3691
3692 3692 for f in os.listdir(path):
3693 3693 with open(os.path.join(path, f), 'rb') as fh:
3694 3694 data = fh.read().split(b'\0')
3695 3695 if len(data) != 5:
3696 3696 continue
3697 3697
3698 3698 exc, mainframe, hgframe, hgline, testname = data
3699 3699 exc = exc.decode('utf-8')
3700 3700 mainframe = mainframe.decode('utf-8')
3701 3701 hgframe = hgframe.decode('utf-8')
3702 3702 hgline = hgline.decode('utf-8')
3703 3703 testname = testname.decode('utf-8')
3704 3704
3705 3705 key = (hgframe, hgline, exc)
3706 3706 exceptioncounts[key] += 1
3707 3707 testsbyfailure[key].add(testname)
3708 3708 failuresbytest[testname].add(key)
3709 3709
3710 3710 # Find test having fewest failures for each failure.
3711 3711 leastfailing = {}
3712 3712 for key, tests in testsbyfailure.items():
3713 3713 fewesttest = None
3714 3714 fewestcount = 99999999
3715 3715 for test in sorted(tests):
3716 3716 if len(failuresbytest[test]) < fewestcount:
3717 3717 fewesttest = test
3718 3718 fewestcount = len(failuresbytest[test])
3719 3719
3720 3720 leastfailing[key] = (fewestcount, fewesttest)
3721 3721
3722 3722 # Create a combined counter so we can sort by total occurrences and
3723 3723 # impacted tests.
3724 3724 combined = {}
3725 3725 for key in exceptioncounts:
3726 3726 combined[key] = (
3727 3727 exceptioncounts[key],
3728 3728 len(testsbyfailure[key]),
3729 3729 leastfailing[key][0],
3730 3730 leastfailing[key][1],
3731 3731 )
3732 3732
3733 3733 return {
3734 3734 'exceptioncounts': exceptioncounts,
3735 3735 'total': sum(exceptioncounts.values()),
3736 3736 'combined': combined,
3737 3737 'leastfailing': leastfailing,
3738 3738 'byfailure': testsbyfailure,
3739 3739 'bytest': failuresbytest,
3740 3740 }
3741 3741
3742 3742
3743 3743 if __name__ == '__main__':
3744 3744 runner = TestRunner()
3745 3745
3746 3746 try:
3747 3747 import msvcrt
3748 3748
3749 3749 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3750 3750 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3751 3751 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3752 3752 except ImportError:
3753 3753 pass
3754 3754
3755 3755 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now