File: test-revlog-raw.py

package info (click to toggle)
mercurial 5.6.1-4
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 38,792 kB
  • sloc: python: 196,559; ansic: 45,504; tcl: 3,715; lisp: 1,483; sh: 1,475; cpp: 864; javascript: 649; makefile: 581; xml: 36; sql: 30
file content (502 lines) | stat: -rw-r--r-- 16,249 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# test revlog interaction about raw data (flagprocessor)

from __future__ import absolute_import, print_function

import collections
import hashlib
import sys

from mercurial import (
    encoding,
    node,
    revlog,
    transaction,
    vfs,
)

from mercurial.revlogutils import (
    deltas,
    flagutil,
)

# TESTTMP is optional. This makes it convenient to run without run-tests.py
tvfs = vfs.vfs(encoding.environ.get(b'TESTTMP', b'/tmp'))

# Enable generaldelta otherwise revlog won't use delta as expected by the test
tvfs.options = {
    b'generaldelta': True,
    b'revlogv1': True,
    b'sparse-revlog': True,
}

# The test wants to control whether to use delta explicitly, based on
# "storedeltachains".
revlog.revlog._isgooddeltainfo = lambda self, d, textlen: self._storedeltachains


def abort(msg):
    print('abort: %s' % msg)
    # Return 0 so run-tests.py could compare the output.
    sys.exit()


# Register a revlog processor for flag EXTSTORED.
#
# It simply prepends a fixed header, and replaces '1' to 'i'. So it has
# insertion and replacement, and may be interesting to test revlog's line-based
# deltas.
_extheader = b'E\n'


def readprocessor(self, rawtext):
    # True: the returned text could be used to verify hash
    text = rawtext[len(_extheader) :].replace(b'i', b'1')
    return text, True, {}


def writeprocessor(self, text, sidedata):
    # False: the returned rawtext shouldn't be used to verify hash
    rawtext = _extheader + text.replace(b'1', b'i')
    return rawtext, False


def rawprocessor(self, rawtext):
    # False: do not verify hash. Only the content returned by "readprocessor"
    # can be used to verify hash.
    return False


flagutil.addflagprocessor(
    revlog.REVIDX_EXTSTORED, (readprocessor, writeprocessor, rawprocessor)
)

# Utilities about reading and appending revlog


def newtransaction():
    # A transaction is required to write revlogs
    report = lambda msg: None
    return transaction.transaction(report, tvfs, {'plain': tvfs}, b'journal')


def newrevlog(name=b'_testrevlog.i', recreate=False):
    if recreate:
        tvfs.tryunlink(name)
    rlog = revlog.revlog(tvfs, name)
    return rlog


def appendrev(rlog, text, tr, isext=False, isdelta=True):
    '''Append a revision. If isext is True, set the EXTSTORED flag so flag
    processor will be used (and rawtext is different from text). If isdelta is
    True, force the revision to be a delta, otherwise it's full text.
    '''
    nextrev = len(rlog)
    p1 = rlog.node(nextrev - 1)
    p2 = node.nullid
    if isext:
        flags = revlog.REVIDX_EXTSTORED
    else:
        flags = revlog.REVIDX_DEFAULT_FLAGS
    # Change storedeltachains temporarily, to override revlog's delta decision
    rlog._storedeltachains = isdelta
    try:
        rlog.addrevision(text, tr, nextrev, p1, p2, flags=flags)
        return nextrev
    except Exception as ex:
        abort('rev %d: failed to append: %s' % (nextrev, ex))
    finally:
        # Restore storedeltachains. It is always True, see revlog.__init__
        rlog._storedeltachains = True


def addgroupcopy(rlog, tr, destname=b'_destrevlog.i', optimaldelta=True):
    '''Copy revlog to destname using revlog.addgroup. Return the copied revlog.

    This emulates push or pull. They use changegroup. Changegroup requires
    repo to work. We don't have a repo, so a dummy changegroup is used.

    If optimaldelta is True, use optimized delta parent, so the destination
    revlog could probably reuse it. Otherwise it builds sub-optimal delta, and
    the destination revlog needs more work to use it.

    This exercises some revlog.addgroup (and revlog._addrevision(text=None))
    code path, which is not covered by "appendrev" alone.
    '''

    class dummychangegroup(object):
        @staticmethod
        def deltachunk(pnode):
            pnode = pnode or node.nullid
            parentrev = rlog.rev(pnode)
            r = parentrev + 1
            if r >= len(rlog):
                return {}
            if optimaldelta:
                deltaparent = parentrev
            else:
                # suboptimal deltaparent
                deltaparent = min(0, parentrev)
            if not rlog.candelta(deltaparent, r):
                deltaparent = -1
            return {
                b'node': rlog.node(r),
                b'p1': pnode,
                b'p2': node.nullid,
                b'cs': rlog.node(rlog.linkrev(r)),
                b'flags': rlog.flags(r),
                b'deltabase': rlog.node(deltaparent),
                b'delta': rlog.revdiff(deltaparent, r),
            }

        def deltaiter(self):
            chain = None
            for chunkdata in iter(lambda: self.deltachunk(chain), {}):
                node = chunkdata[b'node']
                p1 = chunkdata[b'p1']
                p2 = chunkdata[b'p2']
                cs = chunkdata[b'cs']
                deltabase = chunkdata[b'deltabase']
                delta = chunkdata[b'delta']
                flags = chunkdata[b'flags']

                chain = node

                yield (node, p1, p2, cs, deltabase, delta, flags)

    def linkmap(lnode):
        return rlog.rev(lnode)

    dlog = newrevlog(destname, recreate=True)
    dummydeltas = dummychangegroup().deltaiter()
    dlog.addgroup(dummydeltas, linkmap, tr)
    return dlog


def lowlevelcopy(rlog, tr, destname=b'_destrevlog.i'):
    '''Like addgroupcopy, but use the low level revlog._addrevision directly.

    It exercises some code paths that are hard to reach easily otherwise.
    '''
    dlog = newrevlog(destname, recreate=True)
    for r in rlog:
        p1 = rlog.node(r - 1)
        p2 = node.nullid
        if r == 0 or (rlog.flags(r) & revlog.REVIDX_EXTSTORED):
            text = rlog.rawdata(r)
            cachedelta = None
        else:
            # deltaparent cannot have EXTSTORED flag.
            deltaparent = max(
                [-1]
                + [
                    p
                    for p in range(r)
                    if rlog.flags(p) & revlog.REVIDX_EXTSTORED == 0
                ]
            )
            text = None
            cachedelta = (deltaparent, rlog.revdiff(deltaparent, r))
        flags = rlog.flags(r)
        ifh = dfh = None
        try:
            ifh = dlog.opener(dlog.indexfile, b'a+')
            if not dlog._inline:
                dfh = dlog.opener(dlog.datafile, b'a+')
            dlog._addrevision(
                rlog.node(r), text, tr, r, p1, p2, flags, cachedelta, ifh, dfh
            )
        finally:
            if dfh is not None:
                dfh.close()
            if ifh is not None:
                ifh.close()
    return dlog


# Utilities to generate revisions for testing


def genbits(n):
    '''Given a number n, generate (2 ** (n * 2) + 1) numbers in range(2 ** n).
    i.e. the generated numbers have a width of n bits.

    The combination of two adjacent numbers will cover all possible cases.
    That is to say, given any x, y where both x, and y are in range(2 ** n),
    there is an x followed immediately by y in the generated sequence.
    '''
    m = 2 ** n

    # Gray Code. See https://en.wikipedia.org/wiki/Gray_code
    gray = lambda x: x ^ (x >> 1)
    reversegray = {gray(i): i for i in range(m)}

    # Generate (n * 2) bit gray code, yield lower n bits as X, and look for
    # the next unused gray code where higher n bits equal to X.

    # For gray codes whose higher bits are X, a[X] of them have been used.
    a = [0] * m

    # Iterate from 0.
    x = 0
    yield x
    for i in range(m * m):
        x = reversegray[x]
        y = gray(a[x] + x * m) & (m - 1)
        assert a[x] < m
        a[x] += 1
        x = y
        yield x


def gentext(rev):
    '''Given a revision number, generate dummy text'''
    return b''.join(b'%d\n' % j for j in range(-1, rev % 5))


def writecases(rlog, tr):
    '''Write some revisions interested to the test.

    The test is interested in 3 properties of a revision:

        - Is it a delta or a full text? (isdelta)
          This is to catch some delta application issues.
        - Does it have a flag of EXTSTORED? (isext)
          This is to catch some flag processor issues. Especially when
          interacted with revlog deltas.
        - Is its text empty? (isempty)
          This is less important. It is intended to try to catch some careless
          checks like "if text" instead of "if text is None". Note: if flag
          processor is involved, raw text may be not empty.

    Write 65 revisions. So that all combinations of the above flags for
    adjacent revisions are covered. That is to say,

        len(set(
            (r.delta, r.ext, r.empty, (r+1).delta, (r+1).ext, (r+1).empty)
            for r in range(len(rlog) - 1)
           )) is 64.

    Where "r.delta", "r.ext", and "r.empty" are booleans matching properties
    mentioned above.

    Return expected [(text, rawtext)].
    '''
    result = []
    for i, x in enumerate(genbits(3)):
        isdelta, isext, isempty = bool(x & 1), bool(x & 2), bool(x & 4)
        if isempty:
            text = b''
        else:
            text = gentext(i)
        rev = appendrev(rlog, text, tr, isext=isext, isdelta=isdelta)

        # Verify text, rawtext, and rawsize
        if isext:
            rawtext = writeprocessor(None, text, {})[0]
        else:
            rawtext = text
        if rlog.rawsize(rev) != len(rawtext):
            abort('rev %d: wrong rawsize' % rev)
        if rlog.revision(rev, raw=False) != text:
            abort('rev %d: wrong text' % rev)
        if rlog.rawdata(rev) != rawtext:
            abort('rev %d: wrong rawtext' % rev)
        result.append((text, rawtext))

        # Verify flags like isdelta, isext work as expected
        # isdelta can be overridden to False if this or p1 has isext set
        if bool(rlog.deltaparent(rev) > -1) and not isdelta:
            abort('rev %d: isdelta is unexpected' % rev)
        if bool(rlog.flags(rev)) != isext:
            abort('rev %d: isext is ineffective' % rev)
    return result


# Main test and checking


def checkrevlog(rlog, expected):
    '''Check if revlog has expected contents. expected is [(text, rawtext)]'''
    # Test using different access orders. This could expose some issues
    # depending on revlog caching (see revlog._cache).
    for r0 in range(len(rlog) - 1):
        r1 = r0 + 1
        for revorder in [[r0, r1], [r1, r0]]:
            for raworder in [[True], [False], [True, False], [False, True]]:
                nlog = newrevlog()
                for rev in revorder:
                    for raw in raworder:
                        if raw:
                            t = nlog.rawdata(rev)
                        else:
                            t = nlog.revision(rev)
                        if t != expected[rev][int(raw)]:
                            abort(
                                'rev %d: corrupted %stext'
                                % (rev, raw and 'raw' or '')
                            )


slicingdata = [
    ([0, 1, 2, 3, 55, 56, 58, 59, 60], [[0, 1], [2], [58], [59, 60]], 10),
    ([0, 1, 2, 3, 55, 56, 58, 59, 60], [[0, 1], [2], [58], [59, 60]], 10),
    (
        [-1, 0, 1, 2, 3, 55, 56, 58, 59, 60],
        [[-1, 0, 1], [2], [58], [59, 60]],
        10,
    ),
]


def slicingtest(rlog):
    oldmin = rlog._srmingapsize
    try:
        # the test revlog is small, we remove the floor under which we
        # slicing is diregarded.
        rlog._srmingapsize = 0
        for item in slicingdata:
            chain, expected, target = item
            result = deltas.slicechunk(rlog, chain, targetsize=target)
            result = list(result)
            if result != expected:
                print('slicing differ:')
                print('  chain: %s' % chain)
                print('  target: %s' % target)
                print('  expected: %s' % expected)
                print('  result:   %s' % result)
    finally:
        rlog._srmingapsize = oldmin


def md5sum(s):
    return hashlib.md5(s).digest()


def _maketext(*coord):
    """create piece of text according to range of integers

    The test returned use a md5sum of the integer to make it less
    compressible"""
    pieces = []
    for start, size in coord:
        num = range(start, start + size)
        p = [md5sum(b'%d' % r) for r in num]
        pieces.append(b'\n'.join(p))
    return b'\n'.join(pieces) + b'\n'


data = [
    _maketext((0, 120), (456, 60)),
    _maketext((0, 120), (345, 60)),
    _maketext((0, 120), (734, 60)),
    _maketext((0, 120), (734, 60), (923, 45)),
    _maketext((0, 120), (734, 60), (234, 45)),
    _maketext((0, 120), (734, 60), (564, 45)),
    _maketext((0, 120), (734, 60), (361, 45)),
    _maketext((0, 120), (734, 60), (489, 45)),
    _maketext((0, 120), (123, 60)),
    _maketext((0, 120), (145, 60)),
    _maketext((0, 120), (104, 60)),
    _maketext((0, 120), (430, 60)),
    _maketext((0, 120), (430, 60), (923, 45)),
    _maketext((0, 120), (430, 60), (234, 45)),
    _maketext((0, 120), (430, 60), (564, 45)),
    _maketext((0, 120), (430, 60), (361, 45)),
    _maketext((0, 120), (430, 60), (489, 45)),
    _maketext((0, 120), (249, 60)),
    _maketext((0, 120), (832, 60)),
    _maketext((0, 120), (891, 60)),
    _maketext((0, 120), (543, 60)),
    _maketext((0, 120), (120, 60)),
    _maketext((0, 120), (60, 60), (768, 30)),
    _maketext((0, 120), (60, 60), (260, 30)),
    _maketext((0, 120), (60, 60), (450, 30)),
    _maketext((0, 120), (60, 60), (361, 30)),
    _maketext((0, 120), (60, 60), (886, 30)),
    _maketext((0, 120), (60, 60), (116, 30)),
    _maketext((0, 120), (60, 60), (567, 30), (629, 40)),
    _maketext((0, 120), (60, 60), (569, 30), (745, 40)),
    _maketext((0, 120), (60, 60), (777, 30), (700, 40)),
    _maketext((0, 120), (60, 60), (618, 30), (398, 40), (158, 10)),
]


def makesnapshot(tr):
    rl = newrevlog(name=b'_snaprevlog3.i', recreate=True)
    for i in data:
        appendrev(rl, i, tr)
    return rl


snapshots = [-1, 0, 6, 8, 11, 17, 19, 21, 25, 30]


def issnapshottest(rlog):
    result = []
    if rlog.issnapshot(-1):
        result.append(-1)
    for rev in rlog:
        if rlog.issnapshot(rev):
            result.append(rev)
    if snapshots != result:
        print('snapshot differ:')
        print('  expected: %s' % snapshots)
        print('  got:      %s' % result)


snapshotmapall = {0: [6, 8, 11, 17, 19, 25], 8: [21], -1: [0, 30]}
snapshotmap15 = {0: [17, 19, 25], 8: [21], -1: [30]}


def findsnapshottest(rlog):
    resultall = collections.defaultdict(list)
    deltas._findsnapshots(rlog, resultall, 0)
    resultall = dict(resultall.items())
    if resultall != snapshotmapall:
        print('snapshot map  differ:')
        print('  expected: %s' % snapshotmapall)
        print('  got:      %s' % resultall)
    result15 = collections.defaultdict(list)
    deltas._findsnapshots(rlog, result15, 15)
    result15 = dict(result15.items())
    if result15 != snapshotmap15:
        print('snapshot map  differ:')
        print('  expected: %s' % snapshotmap15)
        print('  got:      %s' % result15)


def maintest():
    with newtransaction() as tr:
        rl = newrevlog(recreate=True)
        expected = writecases(rl, tr)
        checkrevlog(rl, expected)
        print('local test passed')
        # Copy via revlog.addgroup
        rl1 = addgroupcopy(rl, tr)
        checkrevlog(rl1, expected)
        rl2 = addgroupcopy(rl, tr, optimaldelta=False)
        checkrevlog(rl2, expected)
        print('addgroupcopy test passed')
        # Copy via revlog.clone
        rl3 = newrevlog(name=b'_destrevlog3.i', recreate=True)
        rl.clone(tr, rl3)
        checkrevlog(rl3, expected)
        print('clone test passed')
        # Copy via low-level revlog._addrevision
        rl4 = lowlevelcopy(rl, tr)
        checkrevlog(rl4, expected)
        print('lowlevelcopy test passed')
        slicingtest(rl)
        print('slicing test passed')
        rl5 = makesnapshot(tr)
        issnapshottest(rl5)
        print('issnapshot test passed')
        findsnapshottest(rl5)
        print('findsnapshot test passed')


try:
    maintest()
except Exception as ex:
    abort('crashed: %s' % ex)