/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/test_knit.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2018-11-16 18:26:22 UTC
  • mfrom: (7167.1.4 run-flake8)
  • Revision ID: breezy.the.bot@gmail.com-20181116182622-qw3gan3hz78a2imw
Add a flake8 test.

Merged from https://code.launchpad.net/~jelmer/brz/run-flake8/+merge/358902

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Tests for Knit data structure"""
 
18
 
 
19
import gzip
 
20
from io import BytesIO
 
21
import sys
 
22
 
 
23
from .. import (
 
24
    errors,
 
25
    multiparent,
 
26
    osutils,
 
27
    tests,
 
28
    transport,
 
29
    )
 
30
from ..bzr import (
 
31
    knit,
 
32
    pack,
 
33
    )
 
34
from ..bzr.index import *
 
35
from ..bzr.knit import (
 
36
    AnnotatedKnitContent,
 
37
    KnitContent,
 
38
    KnitCorrupt,
 
39
    KnitDataStreamIncompatible,
 
40
    KnitDataStreamUnknown,
 
41
    KnitHeaderError,
 
42
    KnitIndexUnknownMethod,
 
43
    KnitVersionedFiles,
 
44
    PlainKnitContent,
 
45
    _VFContentMapGenerator,
 
46
    _KndxIndex,
 
47
    _KnitGraphIndex,
 
48
    _KnitKeyAccess,
 
49
    make_file_factory,
 
50
    )
 
51
from ..patiencediff import PatienceSequenceMatcher
 
52
from ..bzr import (
 
53
    knitpack_repo,
 
54
    pack_repo,
 
55
    )
 
56
from . import (
 
57
    TestCase,
 
58
    TestCaseWithMemoryTransport,
 
59
    TestCaseWithTransport,
 
60
    TestNotApplicable,
 
61
    )
 
62
from ..bzr.versionedfile import (
 
63
    AbsentContentFactory,
 
64
    ConstantMapper,
 
65
    network_bytes_to_kind_and_offset,
 
66
    RecordingVersionedFilesDecorator,
 
67
    )
 
68
from . import (
 
69
    features,
 
70
    )
 
71
 
 
72
 
 
73
compiled_knit_feature = features.ModuleAvailableFeature(
 
74
    'breezy.bzr._knit_load_data_pyx')
 
75
 
 
76
 
 
77
class ErrorTests(TestCase):
 
78
 
 
79
    def test_knit_data_stream_incompatible(self):
 
80
        error = KnitDataStreamIncompatible(
 
81
            'stream format', 'target format')
 
82
        self.assertEqual('Cannot insert knit data stream of format '
 
83
                         '"stream format" into knit of format '
 
84
                         '"target format".', str(error))
 
85
 
 
86
    def test_knit_data_stream_unknown(self):
 
87
        error = KnitDataStreamUnknown(
 
88
            'stream format')
 
89
        self.assertEqual('Cannot parse knit data stream of format '
 
90
                         '"stream format".', str(error))
 
91
 
 
92
    def test_knit_header_error(self):
 
93
        error = KnitHeaderError('line foo\n', 'path/to/file')
 
94
        self.assertEqual("Knit header error: 'line foo\\n' unexpected"
 
95
                         " for file \"path/to/file\".", str(error))
 
96
 
 
97
    def test_knit_index_unknown_method(self):
 
98
        error = KnitIndexUnknownMethod('http://host/foo.kndx',
 
99
                                       ['bad', 'no-eol'])
 
100
        self.assertEqual("Knit index http://host/foo.kndx does not have a"
 
101
                         " known method in options: ['bad', 'no-eol']",
 
102
                         str(error))
 
103
 
 
104
 
 
105
class KnitContentTestsMixin(object):
 
106
 
 
107
    def test_constructor(self):
 
108
        content = self._make_content([])
 
109
 
 
110
    def test_text(self):
 
111
        content = self._make_content([])
 
112
        self.assertEqual(content.text(), [])
 
113
 
 
114
        content = self._make_content([(b"origin1", b"text1"), (b"origin2", b"text2")])
 
115
        self.assertEqual(content.text(), [b"text1", b"text2"])
 
116
 
 
117
    def test_copy(self):
 
118
        content = self._make_content([(b"origin1", b"text1"), (b"origin2", b"text2")])
 
119
        copy = content.copy()
 
120
        self.assertIsInstance(copy, content.__class__)
 
121
        self.assertEqual(copy.annotate(), content.annotate())
 
122
 
 
123
    def assertDerivedBlocksEqual(self, source, target, noeol=False):
 
124
        """Assert that the derived matching blocks match real output"""
 
125
        source_lines = source.splitlines(True)
 
126
        target_lines = target.splitlines(True)
 
127
        def nl(line):
 
128
            if noeol and not line.endswith('\n'):
 
129
                return line + '\n'
 
130
            else:
 
131
                return line
 
132
        source_content = self._make_content([(None, nl(l)) for l in source_lines])
 
133
        target_content = self._make_content([(None, nl(l)) for l in target_lines])
 
134
        line_delta = source_content.line_delta(target_content)
 
135
        delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
 
136
            source_lines, target_lines))
 
137
        matcher = PatienceSequenceMatcher(None, source_lines, target_lines)
 
138
        matcher_blocks = list(matcher.get_matching_blocks())
 
139
        self.assertEqual(matcher_blocks, delta_blocks)
 
140
 
 
141
    def test_get_line_delta_blocks(self):
 
142
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
 
143
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
 
144
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
 
145
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
 
146
        self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
 
147
        self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
 
148
        self.assertDerivedBlocksEqual(TEXT_1A, '')
 
149
        self.assertDerivedBlocksEqual('', TEXT_1A)
 
150
        self.assertDerivedBlocksEqual('', '')
 
151
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
 
152
 
 
153
    def test_get_line_delta_blocks_noeol(self):
 
154
        """Handle historical knit deltas safely
 
155
 
 
156
        Some existing knit deltas don't consider the last line to differ
 
157
        when the only difference whether it has a final newline.
 
158
 
 
159
        New knit deltas appear to always consider the last line to differ
 
160
        in this case.
 
161
        """
 
162
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
 
163
        self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
 
164
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
 
165
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
 
166
 
 
167
 
 
168
TEXT_1 = """\
 
169
Banana cup cakes:
 
170
 
 
171
- bananas
 
172
- eggs
 
173
- broken tea cups
 
174
"""
 
175
 
 
176
TEXT_1A = """\
 
177
Banana cup cake recipe
 
178
(serves 6)
 
179
 
 
180
- bananas
 
181
- eggs
 
182
- broken tea cups
 
183
- self-raising flour
 
184
"""
 
185
 
 
186
TEXT_1B = """\
 
187
Banana cup cake recipe
 
188
 
 
189
- bananas (do not use plantains!!!)
 
190
- broken tea cups
 
191
- flour
 
192
"""
 
193
 
 
194
delta_1_1a = """\
 
195
0,1,2
 
196
Banana cup cake recipe
 
197
(serves 6)
 
198
5,5,1
 
199
- self-raising flour
 
200
"""
 
201
 
 
202
TEXT_2 = """\
 
203
Boeuf bourguignon
 
204
 
 
205
- beef
 
206
- red wine
 
207
- small onions
 
208
- carrot
 
209
- mushrooms
 
210
"""
 
211
 
 
212
 
 
213
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
 
214
 
 
215
    def _make_content(self, lines):
 
216
        annotated_content = AnnotatedKnitContent(lines)
 
217
        return PlainKnitContent(annotated_content.text(), 'bogus')
 
218
 
 
219
    def test_annotate(self):
 
220
        content = self._make_content([])
 
221
        self.assertEqual(content.annotate(), [])
 
222
 
 
223
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
224
        self.assertEqual(content.annotate(),
 
225
            [("bogus", "text1"), ("bogus", "text2")])
 
226
 
 
227
    def test_line_delta(self):
 
228
        content1 = self._make_content([("", "a"), ("", "b")])
 
229
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
230
        self.assertEqual(content1.line_delta(content2),
 
231
            [(1, 2, 2, ["a", "c"])])
 
232
 
 
233
    def test_line_delta_iter(self):
 
234
        content1 = self._make_content([("", "a"), ("", "b")])
 
235
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
236
        it = content1.line_delta_iter(content2)
 
237
        self.assertEqual(next(it), (1, 2, 2, ["a", "c"]))
 
238
        self.assertRaises(StopIteration, next, it)
 
239
 
 
240
 
 
241
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
 
242
 
 
243
    def _make_content(self, lines):
 
244
        return AnnotatedKnitContent(lines)
 
245
 
 
246
    def test_annotate(self):
 
247
        content = self._make_content([])
 
248
        self.assertEqual(content.annotate(), [])
 
249
 
 
250
        content = self._make_content([(b"origin1", b"text1"), (b"origin2", b"text2")])
 
251
        self.assertEqual(content.annotate(),
 
252
            [(b"origin1", b"text1"), (b"origin2", b"text2")])
 
253
 
 
254
    def test_line_delta(self):
 
255
        content1 = self._make_content([("", "a"), ("", "b")])
 
256
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
257
        self.assertEqual(content1.line_delta(content2),
 
258
            [(1, 2, 2, [("", "a"), ("", "c")])])
 
259
 
 
260
    def test_line_delta_iter(self):
 
261
        content1 = self._make_content([("", "a"), ("", "b")])
 
262
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
263
        it = content1.line_delta_iter(content2)
 
264
        self.assertEqual(next(it), (1, 2, 2, [("", "a"), ("", "c")]))
 
265
        self.assertRaises(StopIteration, next, it)
 
266
 
 
267
 
 
268
class MockTransport(object):
 
269
 
 
270
    def __init__(self, file_lines=None):
 
271
        self.file_lines = file_lines
 
272
        self.calls = []
 
273
        # We have no base directory for the MockTransport
 
274
        self.base = ''
 
275
 
 
276
    def get(self, filename):
 
277
        if self.file_lines is None:
 
278
            raise errors.NoSuchFile(filename)
 
279
        else:
 
280
            return BytesIO(b"\n".join(self.file_lines))
 
281
 
 
282
    def readv(self, relpath, offsets):
 
283
        fp = self.get(relpath)
 
284
        for offset, size in offsets:
 
285
            fp.seek(offset)
 
286
            yield offset, fp.read(size)
 
287
 
 
288
    def __getattr__(self, name):
 
289
        def queue_call(*args, **kwargs):
 
290
            self.calls.append((name, args, kwargs))
 
291
        return queue_call
 
292
 
 
293
 
 
294
class MockReadvFailingTransport(MockTransport):
 
295
    """Fail in the middle of a readv() result.
 
296
 
 
297
    This Transport will successfully yield the first two requested hunks, but
 
298
    raise NoSuchFile for the rest.
 
299
    """
 
300
 
 
301
    def readv(self, relpath, offsets):
 
302
        count = 0
 
303
        for result in MockTransport.readv(self, relpath, offsets):
 
304
            count += 1
 
305
            # we use 2 because the first offset is the pack header, the second
 
306
            # is the first actual content requset
 
307
            if count > 2:
 
308
                raise errors.NoSuchFile(relpath)
 
309
            yield result
 
310
 
 
311
 
 
312
class KnitRecordAccessTestsMixin(object):
 
313
    """Tests for getting and putting knit records."""
 
314
 
 
315
    def test_add_raw_records(self):
 
316
        """Add_raw_records adds records retrievable later."""
 
317
        access = self.get_access()
 
318
        memos = access.add_raw_records([(b'key', 10)], b'1234567890')
 
319
        self.assertEqual([b'1234567890'], list(access.get_raw_records(memos)))
 
320
 
 
321
    def test_add_several_raw_records(self):
 
322
        """add_raw_records with many records and read some back."""
 
323
        access = self.get_access()
 
324
        memos = access.add_raw_records([(b'key', 10), (b'key2', 2), (b'key3', 5)],
 
325
            b'12345678901234567')
 
326
        self.assertEqual([b'1234567890', b'12', b'34567'],
 
327
            list(access.get_raw_records(memos)))
 
328
        self.assertEqual([b'1234567890'],
 
329
            list(access.get_raw_records(memos[0:1])))
 
330
        self.assertEqual([b'12'],
 
331
            list(access.get_raw_records(memos[1:2])))
 
332
        self.assertEqual([b'34567'],
 
333
            list(access.get_raw_records(memos[2:3])))
 
334
        self.assertEqual([b'1234567890', b'34567'],
 
335
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
336
 
 
337
 
 
338
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
339
    """Tests for the .kndx implementation."""
 
340
 
 
341
    def get_access(self):
 
342
        """Get a .knit style access instance."""
 
343
        mapper = ConstantMapper("foo")
 
344
        access = _KnitKeyAccess(self.get_transport(), mapper)
 
345
        return access
 
346
 
 
347
 
 
348
class _TestException(Exception):
 
349
    """Just an exception for local tests to use."""
 
350
 
 
351
 
 
352
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
353
    """Tests for the pack based access."""
 
354
 
 
355
    def get_access(self):
 
356
        return self._get_access()[0]
 
357
 
 
358
    def _get_access(self, packname='packfile', index='FOO'):
 
359
        transport = self.get_transport()
 
360
        def write_data(bytes):
 
361
            transport.append_bytes(packname, bytes)
 
362
        writer = pack.ContainerWriter(write_data)
 
363
        writer.begin()
 
364
        access = pack_repo._DirectPackAccess({})
 
365
        access.set_writer(writer, index, (transport, packname))
 
366
        return access, writer
 
367
 
 
368
    def make_pack_file(self):
 
369
        """Create a pack file with 2 records."""
 
370
        access, writer = self._get_access(packname='packname', index='foo')
 
371
        memos = []
 
372
        memos.extend(access.add_raw_records([(b'key1', 10)], b'1234567890'))
 
373
        memos.extend(access.add_raw_records([(b'key2', 5)], b'12345'))
 
374
        writer.end()
 
375
        return memos
 
376
 
 
377
    def test_pack_collection_pack_retries(self):
 
378
        """An explicit pack of a pack collection succeeds even when a
 
379
        concurrent pack happens.
 
380
        """
 
381
        builder = self.make_branch_builder('.')
 
382
        builder.start_series()
 
383
        builder.build_snapshot(None, [
 
384
            ('add', ('', b'root-id', 'directory', None)),
 
385
            ('add', ('file', b'file-id', 'file', b'content\nrev 1\n')),
 
386
            ], revision_id=b'rev-1')
 
387
        builder.build_snapshot([b'rev-1'], [
 
388
            ('modify', ('file', b'content\nrev 2\n')),
 
389
            ], revision_id=b'rev-2')
 
390
        builder.build_snapshot([b'rev-2'], [
 
391
            ('modify', ('file', b'content\nrev 3\n')),
 
392
            ], revision_id=b'rev-3')
 
393
        self.addCleanup(builder.finish_series)
 
394
        b = builder.get_branch()
 
395
        self.addCleanup(b.lock_write().unlock)
 
396
        repo = b.repository
 
397
        collection = repo._pack_collection
 
398
        # Concurrently repack the repo.
 
399
        reopened_repo = repo.controldir.open_repository()
 
400
        reopened_repo.pack()
 
401
        # Pack the new pack.
 
402
        collection.pack()
 
403
 
 
404
    def make_vf_for_retrying(self):
 
405
        """Create 3 packs and a reload function.
 
406
 
 
407
        Originally, 2 pack files will have the data, but one will be missing.
 
408
        And then the third will be used in place of the first two if reload()
 
409
        is called.
 
410
 
 
411
        :return: (versioned_file, reload_counter)
 
412
            versioned_file  a KnitVersionedFiles using the packs for access
 
413
        """
 
414
        builder = self.make_branch_builder('.', format="1.9")
 
415
        builder.start_series()
 
416
        builder.build_snapshot(None, [
 
417
            ('add', ('', b'root-id', 'directory', None)),
 
418
            ('add', ('file', b'file-id', 'file', b'content\nrev 1\n')),
 
419
            ], revision_id=b'rev-1')
 
420
        builder.build_snapshot([b'rev-1'], [
 
421
            ('modify', ('file', b'content\nrev 2\n')),
 
422
            ], revision_id=b'rev-2')
 
423
        builder.build_snapshot([b'rev-2'], [
 
424
            ('modify', ('file', b'content\nrev 3\n')),
 
425
            ], revision_id=b'rev-3')
 
426
        builder.finish_series()
 
427
        b = builder.get_branch()
 
428
        b.lock_write()
 
429
        self.addCleanup(b.unlock)
 
430
        # Pack these three revisions into another pack file, but don't remove
 
431
        # the originals
 
432
        repo = b.repository
 
433
        collection = repo._pack_collection
 
434
        collection.ensure_loaded()
 
435
        orig_packs = collection.packs
 
436
        packer = knitpack_repo.KnitPacker(collection, orig_packs, '.testpack')
 
437
        new_pack = packer.pack()
 
438
        # forget about the new pack
 
439
        collection.reset()
 
440
        repo.refresh_data()
 
441
        vf = repo.revisions
 
442
        # Set up a reload() function that switches to using the new pack file
 
443
        new_index = new_pack.revision_index
 
444
        access_tuple = new_pack.access_tuple()
 
445
        reload_counter = [0, 0, 0]
 
446
        def reload():
 
447
            reload_counter[0] += 1
 
448
            if reload_counter[1] > 0:
 
449
                # We already reloaded, nothing more to do
 
450
                reload_counter[2] += 1
 
451
                return False
 
452
            reload_counter[1] += 1
 
453
            vf._index._graph_index._indices[:] = [new_index]
 
454
            vf._access._indices.clear()
 
455
            vf._access._indices[new_index] = access_tuple
 
456
            return True
 
457
        # Delete one of the pack files so the data will need to be reloaded. We
 
458
        # will delete the file with 'rev-2' in it
 
459
        trans, name = orig_packs[1].access_tuple()
 
460
        trans.delete(name)
 
461
        # We don't have the index trigger reloading because we want to test
 
462
        # that we reload when the .pack disappears
 
463
        vf._access._reload_func = reload
 
464
        return vf, reload_counter
 
465
 
 
466
    def make_reload_func(self, return_val=True):
 
467
        reload_called = [0]
 
468
        def reload():
 
469
            reload_called[0] += 1
 
470
            return return_val
 
471
        return reload_called, reload
 
472
 
 
473
    def make_retry_exception(self):
 
474
        # We raise a real exception so that sys.exc_info() is properly
 
475
        # populated
 
476
        try:
 
477
            raise _TestException('foobar')
 
478
        except _TestException as e:
 
479
            retry_exc = errors.RetryWithNewPacks(None, reload_occurred=False,
 
480
                                                 exc_info=sys.exc_info())
 
481
        # GZ 2010-08-10: Cycle with exc_info affects 3 tests
 
482
        return retry_exc
 
483
 
 
484
    def test_read_from_several_packs(self):
 
485
        access, writer = self._get_access()
 
486
        memos = []
 
487
        memos.extend(access.add_raw_records([(b'key', 10)], b'1234567890'))
 
488
        writer.end()
 
489
        access, writer = self._get_access('pack2', 'FOOBAR')
 
490
        memos.extend(access.add_raw_records([(b'key', 5)], b'12345'))
 
491
        writer.end()
 
492
        access, writer = self._get_access('pack3', 'BAZ')
 
493
        memos.extend(access.add_raw_records([(b'key', 5)], b'alpha'))
 
494
        writer.end()
 
495
        transport = self.get_transport()
 
496
        access = pack_repo._DirectPackAccess({"FOO":(transport, 'packfile'),
 
497
            "FOOBAR":(transport, 'pack2'),
 
498
            "BAZ":(transport, 'pack3')})
 
499
        self.assertEqual([b'1234567890', b'12345', b'alpha'],
 
500
            list(access.get_raw_records(memos)))
 
501
        self.assertEqual([b'1234567890'],
 
502
            list(access.get_raw_records(memos[0:1])))
 
503
        self.assertEqual([b'12345'],
 
504
            list(access.get_raw_records(memos[1:2])))
 
505
        self.assertEqual([b'alpha'],
 
506
            list(access.get_raw_records(memos[2:3])))
 
507
        self.assertEqual([b'1234567890', b'alpha'],
 
508
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
509
 
 
510
    def test_set_writer(self):
 
511
        """The writer should be settable post construction."""
 
512
        access = pack_repo._DirectPackAccess({})
 
513
        transport = self.get_transport()
 
514
        packname = 'packfile'
 
515
        index = 'foo'
 
516
        def write_data(bytes):
 
517
            transport.append_bytes(packname, bytes)
 
518
        writer = pack.ContainerWriter(write_data)
 
519
        writer.begin()
 
520
        access.set_writer(writer, index, (transport, packname))
 
521
        memos = access.add_raw_records([(b'key', 10)], b'1234567890')
 
522
        writer.end()
 
523
        self.assertEqual([b'1234567890'], list(access.get_raw_records(memos)))
 
524
 
 
525
    def test_missing_index_raises_retry(self):
 
526
        memos = self.make_pack_file()
 
527
        transport = self.get_transport()
 
528
        reload_called, reload_func = self.make_reload_func()
 
529
        # Note that the index key has changed from 'foo' to 'bar'
 
530
        access = pack_repo._DirectPackAccess({'bar':(transport, 'packname')},
 
531
                                   reload_func=reload_func)
 
532
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
533
                                  access.get_raw_records, memos)
 
534
        # Because a key was passed in which does not match our index list, we
 
535
        # assume that the listing was already reloaded
 
536
        self.assertTrue(e.reload_occurred)
 
537
        self.assertIsInstance(e.exc_info, tuple)
 
538
        self.assertIs(e.exc_info[0], KeyError)
 
539
        self.assertIsInstance(e.exc_info[1], KeyError)
 
540
 
 
541
    def test_missing_index_raises_key_error_with_no_reload(self):
 
542
        memos = self.make_pack_file()
 
543
        transport = self.get_transport()
 
544
        # Note that the index key has changed from 'foo' to 'bar'
 
545
        access = pack_repo._DirectPackAccess({'bar':(transport, 'packname')})
 
546
        e = self.assertListRaises(KeyError, access.get_raw_records, memos)
 
547
 
 
548
    def test_missing_file_raises_retry(self):
 
549
        memos = self.make_pack_file()
 
550
        transport = self.get_transport()
 
551
        reload_called, reload_func = self.make_reload_func()
 
552
        # Note that the 'filename' has been changed to 'different-packname'
 
553
        access = pack_repo._DirectPackAccess(
 
554
            {'foo':(transport, 'different-packname')},
 
555
            reload_func=reload_func)
 
556
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
557
                                  access.get_raw_records, memos)
 
558
        # The file has gone missing, so we assume we need to reload
 
559
        self.assertFalse(e.reload_occurred)
 
560
        self.assertIsInstance(e.exc_info, tuple)
 
561
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
562
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
563
        self.assertEqual('different-packname', e.exc_info[1].path)
 
564
 
 
565
    def test_missing_file_raises_no_such_file_with_no_reload(self):
 
566
        memos = self.make_pack_file()
 
567
        transport = self.get_transport()
 
568
        # Note that the 'filename' has been changed to 'different-packname'
 
569
        access = pack_repo._DirectPackAccess(
 
570
            {'foo': (transport, 'different-packname')})
 
571
        e = self.assertListRaises(errors.NoSuchFile,
 
572
                                  access.get_raw_records, memos)
 
573
 
 
574
    def test_failing_readv_raises_retry(self):
 
575
        memos = self.make_pack_file()
 
576
        transport = self.get_transport()
 
577
        failing_transport = MockReadvFailingTransport(
 
578
                                [transport.get_bytes('packname')])
 
579
        reload_called, reload_func = self.make_reload_func()
 
580
        access = pack_repo._DirectPackAccess(
 
581
            {'foo': (failing_transport, 'packname')},
 
582
            reload_func=reload_func)
 
583
        # Asking for a single record will not trigger the Mock failure
 
584
        self.assertEqual([b'1234567890'],
 
585
            list(access.get_raw_records(memos[:1])))
 
586
        self.assertEqual([b'12345'],
 
587
            list(access.get_raw_records(memos[1:2])))
 
588
        # A multiple offset readv() will fail mid-way through
 
589
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
590
                                  access.get_raw_records, memos)
 
591
        # The file has gone missing, so we assume we need to reload
 
592
        self.assertFalse(e.reload_occurred)
 
593
        self.assertIsInstance(e.exc_info, tuple)
 
594
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
595
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
596
        self.assertEqual('packname', e.exc_info[1].path)
 
597
 
 
598
    def test_failing_readv_raises_no_such_file_with_no_reload(self):
 
599
        memos = self.make_pack_file()
 
600
        transport = self.get_transport()
 
601
        failing_transport = MockReadvFailingTransport(
 
602
                                [transport.get_bytes('packname')])
 
603
        reload_called, reload_func = self.make_reload_func()
 
604
        access = pack_repo._DirectPackAccess(
 
605
            {'foo':(failing_transport, 'packname')})
 
606
        # Asking for a single record will not trigger the Mock failure
 
607
        self.assertEqual([b'1234567890'],
 
608
            list(access.get_raw_records(memos[:1])))
 
609
        self.assertEqual([b'12345'],
 
610
            list(access.get_raw_records(memos[1:2])))
 
611
        # A multiple offset readv() will fail mid-way through
 
612
        e = self.assertListRaises(errors.NoSuchFile,
 
613
                                  access.get_raw_records, memos)
 
614
 
 
615
    def test_reload_or_raise_no_reload(self):
 
616
        access = pack_repo._DirectPackAccess({}, reload_func=None)
 
617
        retry_exc = self.make_retry_exception()
 
618
        # Without a reload_func, we will just re-raise the original exception
 
619
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
620
 
 
621
    def test_reload_or_raise_reload_changed(self):
 
622
        reload_called, reload_func = self.make_reload_func(return_val=True)
 
623
        access = pack_repo._DirectPackAccess({}, reload_func=reload_func)
 
624
        retry_exc = self.make_retry_exception()
 
625
        access.reload_or_raise(retry_exc)
 
626
        self.assertEqual([1], reload_called)
 
627
        retry_exc.reload_occurred=True
 
628
        access.reload_or_raise(retry_exc)
 
629
        self.assertEqual([2], reload_called)
 
630
 
 
631
    def test_reload_or_raise_reload_no_change(self):
 
632
        reload_called, reload_func = self.make_reload_func(return_val=False)
 
633
        access = pack_repo._DirectPackAccess({}, reload_func=reload_func)
 
634
        retry_exc = self.make_retry_exception()
 
635
        # If reload_occurred is False, then we consider it an error to have
 
636
        # reload_func() return False (no changes).
 
637
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
638
        self.assertEqual([1], reload_called)
 
639
        retry_exc.reload_occurred=True
 
640
        # If reload_occurred is True, then we assume nothing changed because
 
641
        # it had changed earlier, but didn't change again
 
642
        access.reload_or_raise(retry_exc)
 
643
        self.assertEqual([2], reload_called)
 
644
 
 
645
    def test_annotate_retries(self):
 
646
        vf, reload_counter = self.make_vf_for_retrying()
 
647
        # It is a little bit bogus to annotate the Revision VF, but it works,
 
648
        # as we have ancestry stored there
 
649
        key = (b'rev-3',)
 
650
        reload_lines = vf.annotate(key)
 
651
        self.assertEqual([1, 1, 0], reload_counter)
 
652
        plain_lines = vf.annotate(key)
 
653
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
654
        if reload_lines != plain_lines:
 
655
            self.fail('Annotation was not identical with reloading.')
 
656
        # Now delete the packs-in-use, which should trigger another reload, but
 
657
        # this time we just raise an exception because we can't recover
 
658
        for trans, name in vf._access._indices.values():
 
659
            trans.delete(name)
 
660
        self.assertRaises(errors.NoSuchFile, vf.annotate, key)
 
661
        self.assertEqual([2, 1, 1], reload_counter)
 
662
 
 
663
    def test__get_record_map_retries(self):
 
664
        vf, reload_counter = self.make_vf_for_retrying()
 
665
        keys = [(b'rev-1',), (b'rev-2',), (b'rev-3',)]
 
666
        records = vf._get_record_map(keys)
 
667
        self.assertEqual(keys, sorted(records.keys()))
 
668
        self.assertEqual([1, 1, 0], reload_counter)
 
669
        # Now delete the packs-in-use, which should trigger another reload, but
 
670
        # this time we just raise an exception because we can't recover
 
671
        for trans, name in vf._access._indices.values():
 
672
            trans.delete(name)
 
673
        self.assertRaises(errors.NoSuchFile, vf._get_record_map, keys)
 
674
        self.assertEqual([2, 1, 1], reload_counter)
 
675
 
 
676
    def test_get_record_stream_retries(self):
 
677
        vf, reload_counter = self.make_vf_for_retrying()
 
678
        keys = [(b'rev-1',), (b'rev-2',), (b'rev-3',)]
 
679
        record_stream = vf.get_record_stream(keys, 'topological', False)
 
680
        record = next(record_stream)
 
681
        self.assertEqual((b'rev-1',), record.key)
 
682
        self.assertEqual([0, 0, 0], reload_counter)
 
683
        record = next(record_stream)
 
684
        self.assertEqual((b'rev-2',), record.key)
 
685
        self.assertEqual([1, 1, 0], reload_counter)
 
686
        record = next(record_stream)
 
687
        self.assertEqual((b'rev-3',), record.key)
 
688
        self.assertEqual([1, 1, 0], reload_counter)
 
689
        # Now delete all pack files, and see that we raise the right error
 
690
        for trans, name in vf._access._indices.values():
 
691
            trans.delete(name)
 
692
        self.assertListRaises(errors.NoSuchFile,
 
693
            vf.get_record_stream, keys, 'topological', False)
 
694
 
 
695
    def test_iter_lines_added_or_present_in_keys_retries(self):
 
696
        vf, reload_counter = self.make_vf_for_retrying()
 
697
        keys = [(b'rev-1',), (b'rev-2',), (b'rev-3',)]
 
698
        # Unfortunately, iter_lines_added_or_present_in_keys iterates the
 
699
        # result in random order (determined by the iteration order from a
 
700
        # set()), so we don't have any solid way to trigger whether data is
 
701
        # read before or after. However we tried to delete the middle node to
 
702
        # exercise the code well.
 
703
        # What we care about is that all lines are always yielded, but not
 
704
        # duplicated
 
705
        count = 0
 
706
        reload_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
707
        self.assertEqual([1, 1, 0], reload_counter)
 
708
        # Now do it again, to make sure the result is equivalent
 
709
        plain_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
710
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
711
        self.assertEqual(plain_lines, reload_lines)
 
712
        self.assertEqual(21, len(plain_lines))
 
713
        # Now delete all pack files, and see that we raise the right error
 
714
        for trans, name in vf._access._indices.values():
 
715
            trans.delete(name)
 
716
        self.assertListRaises(errors.NoSuchFile,
 
717
            vf.iter_lines_added_or_present_in_keys, keys)
 
718
        self.assertEqual([2, 1, 1], reload_counter)
 
719
 
 
720
    def test_get_record_stream_yields_disk_sorted_order(self):
 
721
        # if we get 'unordered' pick a semi-optimal order for reading. The
 
722
        # order should be grouped by pack file, and then by position in file
 
723
        repo = self.make_repository('test', format='pack-0.92')
 
724
        repo.lock_write()
 
725
        self.addCleanup(repo.unlock)
 
726
        repo.start_write_group()
 
727
        vf = repo.texts
 
728
        vf.add_lines((b'f-id', b'rev-5'), [(b'f-id', b'rev-4')], [b'lines\n'])
 
729
        vf.add_lines((b'f-id', b'rev-1'), [], [b'lines\n'])
 
730
        vf.add_lines((b'f-id', b'rev-2'), [(b'f-id', b'rev-1')], [b'lines\n'])
 
731
        repo.commit_write_group()
 
732
        # We inserted them as rev-5, rev-1, rev-2, we should get them back in
 
733
        # the same order
 
734
        stream = vf.get_record_stream([(b'f-id', b'rev-1'), (b'f-id', b'rev-5'),
 
735
                                       (b'f-id', b'rev-2')], 'unordered', False)
 
736
        keys = [r.key for r in stream]
 
737
        self.assertEqual([(b'f-id', b'rev-5'), (b'f-id', b'rev-1'),
 
738
                          (b'f-id', b'rev-2')], keys)
 
739
        repo.start_write_group()
 
740
        vf.add_lines((b'f-id', b'rev-4'), [(b'f-id', b'rev-3')], [b'lines\n'])
 
741
        vf.add_lines((b'f-id', b'rev-3'), [(b'f-id', b'rev-2')], [b'lines\n'])
 
742
        vf.add_lines((b'f-id', b'rev-6'), [(b'f-id', b'rev-5')], [b'lines\n'])
 
743
        repo.commit_write_group()
 
744
        # Request in random order, to make sure the output order isn't based on
 
745
        # the request
 
746
        request_keys = set((b'f-id', b'rev-%d' % i) for i in range(1, 7))
 
747
        stream = vf.get_record_stream(request_keys, 'unordered', False)
 
748
        keys = [r.key for r in stream]
 
749
        # We want to get the keys back in disk order, but it doesn't matter
 
750
        # which pack we read from first. So this can come back in 2 orders
 
751
        alt1 = [(b'f-id', b'rev-%d' % i) for i in [4, 3, 6, 5, 1, 2]]
 
752
        alt2 = [(b'f-id', b'rev-%d' % i) for i in [5, 1, 2, 4, 3, 6]]
 
753
        if keys != alt1 and keys != alt2:
 
754
            self.fail('Returned key order did not match either expected order.'
 
755
                      ' expected %s or %s, not %s'
 
756
                      % (alt1, alt2, keys))
 
757
 
 
758
 
 
759
class LowLevelKnitDataTests(TestCase):
 
760
 
 
761
    def create_gz_content(self, text):
 
762
        sio = BytesIO()
 
763
        with gzip.GzipFile(mode='wb', fileobj=sio) as gz_file:
 
764
            gz_file.write(text)
 
765
        return sio.getvalue()
 
766
 
 
767
    def make_multiple_records(self):
 
768
        """Create the content for multiple records."""
 
769
        sha1sum = osutils.sha_string(b'foo\nbar\n')
 
770
        total_txt = []
 
771
        gz_txt = self.create_gz_content(b'version rev-id-1 2 %s\n'
 
772
                                        b'foo\n'
 
773
                                        b'bar\n'
 
774
                                        b'end rev-id-1\n'
 
775
                                        % (sha1sum,))
 
776
        record_1 = (0, len(gz_txt), sha1sum)
 
777
        total_txt.append(gz_txt)
 
778
        sha1sum = osutils.sha_string(b'baz\n')
 
779
        gz_txt = self.create_gz_content(b'version rev-id-2 1 %s\n'
 
780
                                        b'baz\n'
 
781
                                        b'end rev-id-2\n'
 
782
                                        % (sha1sum,))
 
783
        record_2 = (record_1[1], len(gz_txt), sha1sum)
 
784
        total_txt.append(gz_txt)
 
785
        return total_txt, record_1, record_2
 
786
 
 
787
    def test_valid_knit_data(self):
 
788
        sha1sum = osutils.sha_string(b'foo\nbar\n')
 
789
        gz_txt = self.create_gz_content(b'version rev-id-1 2 %s\n'
 
790
                                        b'foo\n'
 
791
                                        b'bar\n'
 
792
                                        b'end rev-id-1\n'
 
793
                                        % (sha1sum,))
 
794
        transport = MockTransport([gz_txt])
 
795
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
796
        knit = KnitVersionedFiles(None, access)
 
797
        records = [((b'rev-id-1',), ((b'rev-id-1',), 0, len(gz_txt)))]
 
798
 
 
799
        contents = list(knit._read_records_iter(records))
 
800
        self.assertEqual([((b'rev-id-1',), [b'foo\n', b'bar\n'],
 
801
            b'4e48e2c9a3d2ca8a708cb0cc545700544efb5021')], contents)
 
802
 
 
803
        raw_contents = list(knit._read_records_iter_raw(records))
 
804
        self.assertEqual([((b'rev-id-1',), gz_txt, sha1sum)], raw_contents)
 
805
 
 
806
    def test_multiple_records_valid(self):
 
807
        total_txt, record_1, record_2 = self.make_multiple_records()
 
808
        transport = MockTransport([b''.join(total_txt)])
 
809
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
810
        knit = KnitVersionedFiles(None, access)
 
811
        records = [((b'rev-id-1',), ((b'rev-id-1',), record_1[0], record_1[1])),
 
812
                   ((b'rev-id-2',), ((b'rev-id-2',), record_2[0], record_2[1]))]
 
813
 
 
814
        contents = list(knit._read_records_iter(records))
 
815
        self.assertEqual([((b'rev-id-1',), [b'foo\n', b'bar\n'], record_1[2]),
 
816
                          ((b'rev-id-2',), [b'baz\n'], record_2[2])],
 
817
                         contents)
 
818
 
 
819
        raw_contents = list(knit._read_records_iter_raw(records))
 
820
        self.assertEqual([((b'rev-id-1',), total_txt[0], record_1[2]),
 
821
                          ((b'rev-id-2',), total_txt[1], record_2[2])],
 
822
                         raw_contents)
 
823
 
 
824
    def test_not_enough_lines(self):
 
825
        sha1sum = osutils.sha_string(b'foo\n')
 
826
        # record says 2 lines data says 1
 
827
        gz_txt = self.create_gz_content(b'version rev-id-1 2 %s\n'
 
828
                                        b'foo\n'
 
829
                                        b'end rev-id-1\n'
 
830
                                        % (sha1sum,))
 
831
        transport = MockTransport([gz_txt])
 
832
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
833
        knit = KnitVersionedFiles(None, access)
 
834
        records = [((b'rev-id-1',), ((b'rev-id-1',), 0, len(gz_txt)))]
 
835
        self.assertRaises(KnitCorrupt, list,
 
836
            knit._read_records_iter(records))
 
837
 
 
838
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
839
        raw_contents = list(knit._read_records_iter_raw(records))
 
840
        self.assertEqual([((b'rev-id-1',),  gz_txt, sha1sum)], raw_contents)
 
841
 
 
842
    def test_too_many_lines(self):
 
843
        sha1sum = osutils.sha_string(b'foo\nbar\n')
 
844
        # record says 1 lines data says 2
 
845
        gz_txt = self.create_gz_content(b'version rev-id-1 1 %s\n'
 
846
                                        b'foo\n'
 
847
                                        b'bar\n'
 
848
                                        b'end rev-id-1\n'
 
849
                                        % (sha1sum,))
 
850
        transport = MockTransport([gz_txt])
 
851
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
852
        knit = KnitVersionedFiles(None, access)
 
853
        records = [((b'rev-id-1',), ((b'rev-id-1',), 0, len(gz_txt)))]
 
854
        self.assertRaises(KnitCorrupt, list,
 
855
            knit._read_records_iter(records))
 
856
 
 
857
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
858
        raw_contents = list(knit._read_records_iter_raw(records))
 
859
        self.assertEqual([((b'rev-id-1',), gz_txt, sha1sum)], raw_contents)
 
860
 
 
861
    def test_mismatched_version_id(self):
 
862
        sha1sum = osutils.sha_string(b'foo\nbar\n')
 
863
        gz_txt = self.create_gz_content(b'version rev-id-1 2 %s\n'
 
864
                                        b'foo\n'
 
865
                                        b'bar\n'
 
866
                                        b'end rev-id-1\n'
 
867
                                        % (sha1sum,))
 
868
        transport = MockTransport([gz_txt])
 
869
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
870
        knit = KnitVersionedFiles(None, access)
 
871
        # We are asking for rev-id-2, but the data is rev-id-1
 
872
        records = [((b'rev-id-2',), ((b'rev-id-2',), 0, len(gz_txt)))]
 
873
        self.assertRaises(KnitCorrupt, list,
 
874
            knit._read_records_iter(records))
 
875
 
 
876
        # read_records_iter_raw detects mismatches in the header
 
877
        self.assertRaises(KnitCorrupt, list,
 
878
            knit._read_records_iter_raw(records))
 
879
 
 
880
    def test_uncompressed_data(self):
 
881
        sha1sum = osutils.sha_string(b'foo\nbar\n')
 
882
        txt = (b'version rev-id-1 2 %s\n'
 
883
               b'foo\n'
 
884
               b'bar\n'
 
885
               b'end rev-id-1\n'
 
886
               % (sha1sum,))
 
887
        transport = MockTransport([txt])
 
888
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
889
        knit = KnitVersionedFiles(None, access)
 
890
        records = [((b'rev-id-1',), ((b'rev-id-1',), 0, len(txt)))]
 
891
 
 
892
        # We don't have valid gzip data ==> corrupt
 
893
        self.assertRaises(KnitCorrupt, list,
 
894
            knit._read_records_iter(records))
 
895
 
 
896
        # read_records_iter_raw will notice the bad data
 
897
        self.assertRaises(KnitCorrupt, list,
 
898
            knit._read_records_iter_raw(records))
 
899
 
 
900
    def test_corrupted_data(self):
 
901
        sha1sum = osutils.sha_string(b'foo\nbar\n')
 
902
        gz_txt = self.create_gz_content(b'version rev-id-1 2 %s\n'
 
903
                                        b'foo\n'
 
904
                                        b'bar\n'
 
905
                                        b'end rev-id-1\n'
 
906
                                        % (sha1sum,))
 
907
        # Change 2 bytes in the middle to \xff
 
908
        gz_txt = gz_txt[:10] + b'\xff\xff' + gz_txt[12:]
 
909
        transport = MockTransport([gz_txt])
 
910
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
911
        knit = KnitVersionedFiles(None, access)
 
912
        records = [((b'rev-id-1',), ((b'rev-id-1',), 0, len(gz_txt)))]
 
913
        self.assertRaises(KnitCorrupt, list,
 
914
            knit._read_records_iter(records))
 
915
        # read_records_iter_raw will barf on bad gz data
 
916
        self.assertRaises(KnitCorrupt, list,
 
917
            knit._read_records_iter_raw(records))
 
918
 
 
919
 
 
920
class LowLevelKnitIndexTests(TestCase):
 
921
 
 
922
    @property
 
923
    def _load_data(self):
 
924
        from ..bzr._knit_load_data_py import _load_data_py
 
925
        return _load_data_py
 
926
 
 
927
    def get_knit_index(self, transport, name, mode):
 
928
        mapper = ConstantMapper(name)
 
929
        self.overrideAttr(knit, '_load_data', self._load_data)
 
930
        allow_writes = lambda: 'w' in mode
 
931
        return _KndxIndex(transport, mapper, lambda: None, allow_writes, lambda: True)
 
932
 
 
933
    def test_create_file(self):
 
934
        transport = MockTransport()
 
935
        index = self.get_knit_index(transport, "filename", "w")
 
936
        index.keys()
 
937
        call = transport.calls.pop(0)
 
938
        # call[1][1] is a BytesIO - we can't test it by simple equality.
 
939
        self.assertEqual('put_file_non_atomic', call[0])
 
940
        self.assertEqual('filename.kndx', call[1][0])
 
941
        # With no history, _KndxIndex writes a new index:
 
942
        self.assertEqual(_KndxIndex.HEADER,
 
943
            call[1][1].getvalue())
 
944
        self.assertEqual({'create_parent_dir': True}, call[2])
 
945
 
 
946
    def test_read_utf8_version_id(self):
 
947
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
948
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
949
        transport = MockTransport([
 
950
            _KndxIndex.HEADER,
 
951
            b'%s option 0 1 :' % (utf8_revision_id,)
 
952
            ])
 
953
        index = self.get_knit_index(transport, "filename", "r")
 
954
        # _KndxIndex is a private class, and deals in utf8 revision_ids, not
 
955
        # Unicode revision_ids.
 
956
        self.assertEqual({(utf8_revision_id,):()},
 
957
            index.get_parent_map(index.keys()))
 
958
        self.assertFalse((unicode_revision_id,) in index.keys())
 
959
 
 
960
    def test_read_utf8_parents(self):
 
961
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
962
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
963
        transport = MockTransport([
 
964
            _KndxIndex.HEADER,
 
965
            b"version option 0 1 .%s :" % (utf8_revision_id,)
 
966
            ])
 
967
        index = self.get_knit_index(transport, "filename", "r")
 
968
        self.assertEqual({(b"version",): ((utf8_revision_id,),)},
 
969
            index.get_parent_map(index.keys()))
 
970
 
 
971
    def test_read_ignore_corrupted_lines(self):
 
972
        transport = MockTransport([
 
973
            _KndxIndex.HEADER,
 
974
            b"corrupted",
 
975
            b"corrupted options 0 1 .b .c ",
 
976
            b"version options 0 1 :"
 
977
            ])
 
978
        index = self.get_knit_index(transport, "filename", "r")
 
979
        self.assertEqual(1, len(index.keys()))
 
980
        self.assertEqual({(b"version",)}, index.keys())
 
981
 
 
982
    def test_read_corrupted_header(self):
 
983
        transport = MockTransport([b'not a bzr knit index header\n'])
 
984
        index = self.get_knit_index(transport, "filename", "r")
 
985
        self.assertRaises(KnitHeaderError, index.keys)
 
986
 
 
987
    def test_read_duplicate_entries(self):
 
988
        transport = MockTransport([
 
989
            _KndxIndex.HEADER,
 
990
            b"parent options 0 1 :",
 
991
            b"version options1 0 1 0 :",
 
992
            b"version options2 1 2 .other :",
 
993
            b"version options3 3 4 0 .other :"
 
994
            ])
 
995
        index = self.get_knit_index(transport, "filename", "r")
 
996
        self.assertEqual(2, len(index.keys()))
 
997
        # check that the index used is the first one written. (Specific
 
998
        # to KnitIndex style indices.
 
999
        self.assertEqual(b"1", index._dictionary_compress([(b"version",)]))
 
1000
        self.assertEqual(((b"version",), 3, 4), index.get_position((b"version",)))
 
1001
        self.assertEqual([b"options3"], index.get_options((b"version",)))
 
1002
        self.assertEqual({(b"version",): ((b"parent",), (b"other",))},
 
1003
            index.get_parent_map([(b"version",)]))
 
1004
 
 
1005
    def test_read_compressed_parents(self):
 
1006
        transport = MockTransport([
 
1007
            _KndxIndex.HEADER,
 
1008
            b"a option 0 1 :",
 
1009
            b"b option 0 1 0 :",
 
1010
            b"c option 0 1 1 0 :",
 
1011
            ])
 
1012
        index = self.get_knit_index(transport, "filename", "r")
 
1013
        self.assertEqual({(b"b",):((b"a",),), (b"c",):((b"b",), (b"a",))},
 
1014
            index.get_parent_map([(b"b",), (b"c",)]))
 
1015
 
 
1016
    def test_write_utf8_version_id(self):
 
1017
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
1018
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
1019
        transport = MockTransport([
 
1020
            _KndxIndex.HEADER
 
1021
            ])
 
1022
        index = self.get_knit_index(transport, "filename", "r")
 
1023
        index.add_records([
 
1024
            ((utf8_revision_id,), [b"option"], ((utf8_revision_id,), 0, 1), [])])
 
1025
        call = transport.calls.pop(0)
 
1026
        # call[1][1] is a BytesIO - we can't test it by simple equality.
 
1027
        self.assertEqual('put_file_non_atomic', call[0])
 
1028
        self.assertEqual('filename.kndx', call[1][0])
 
1029
        # With no history, _KndxIndex writes a new index:
 
1030
        self.assertEqual(_KndxIndex.HEADER +
 
1031
            b"\n%s option 0 1  :" % (utf8_revision_id,),
 
1032
            call[1][1].getvalue())
 
1033
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1034
 
 
1035
    def test_write_utf8_parents(self):
 
1036
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
1037
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
1038
        transport = MockTransport([
 
1039
            _KndxIndex.HEADER
 
1040
            ])
 
1041
        index = self.get_knit_index(transport, "filename", "r")
 
1042
        index.add_records([
 
1043
            ((b"version",), [b"option"], ((b"version",), 0, 1), [(utf8_revision_id,)])])
 
1044
        call = transport.calls.pop(0)
 
1045
        # call[1][1] is a BytesIO - we can't test it by simple equality.
 
1046
        self.assertEqual('put_file_non_atomic', call[0])
 
1047
        self.assertEqual('filename.kndx', call[1][0])
 
1048
        # With no history, _KndxIndex writes a new index:
 
1049
        self.assertEqual(_KndxIndex.HEADER +
 
1050
            b"\nversion option 0 1 .%s :" % (utf8_revision_id,),
 
1051
            call[1][1].getvalue())
 
1052
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1053
 
 
1054
    def test_keys(self):
 
1055
        transport = MockTransport([
 
1056
            _KndxIndex.HEADER
 
1057
            ])
 
1058
        index = self.get_knit_index(transport, "filename", "r")
 
1059
 
 
1060
        self.assertEqual(set(), index.keys())
 
1061
 
 
1062
        index.add_records([((b"a",), [b"option"], ((b"a",), 0, 1), [])])
 
1063
        self.assertEqual({(b"a",)}, index.keys())
 
1064
 
 
1065
        index.add_records([((b"a",), [b"option"], ((b"a",), 0, 1), [])])
 
1066
        self.assertEqual({(b"a",)}, index.keys())
 
1067
 
 
1068
        index.add_records([((b"b",), [b"option"], ((b"b",), 0, 1), [])])
 
1069
        self.assertEqual({(b"a",), (b"b",)}, index.keys())
 
1070
 
 
1071
    def add_a_b(self, index, random_id=None):
 
1072
        kwargs = {}
 
1073
        if random_id is not None:
 
1074
            kwargs["random_id"] = random_id
 
1075
        index.add_records([
 
1076
            ((b"a",), [b"option"], ((b"a",), 0, 1), [(b"b",)]),
 
1077
            ((b"a",), [b"opt"], ((b"a",), 1, 2), [(b"c",)]),
 
1078
            ((b"b",), [b"option"], ((b"b",), 2, 3), [(b"a",)])
 
1079
            ], **kwargs)
 
1080
 
 
1081
    def assertIndexIsAB(self, index):
 
1082
        self.assertEqual({
 
1083
            (b'a',): ((b'c',),),
 
1084
            (b'b',): ((b'a',),),
 
1085
            },
 
1086
            index.get_parent_map(index.keys()))
 
1087
        self.assertEqual(((b"a",), 1, 2), index.get_position((b"a",)))
 
1088
        self.assertEqual(((b"b",), 2, 3), index.get_position((b"b",)))
 
1089
        self.assertEqual([b"opt"], index.get_options((b"a",)))
 
1090
 
 
1091
    def test_add_versions(self):
 
1092
        transport = MockTransport([
 
1093
            _KndxIndex.HEADER
 
1094
            ])
 
1095
        index = self.get_knit_index(transport, "filename", "r")
 
1096
 
 
1097
        self.add_a_b(index)
 
1098
        call = transport.calls.pop(0)
 
1099
        # call[1][1] is a BytesIO - we can't test it by simple equality.
 
1100
        self.assertEqual('put_file_non_atomic', call[0])
 
1101
        self.assertEqual('filename.kndx', call[1][0])
 
1102
        # With no history, _KndxIndex writes a new index:
 
1103
        self.assertEqual(
 
1104
            _KndxIndex.HEADER +
 
1105
            b"\na option 0 1 .b :"
 
1106
            b"\na opt 1 2 .c :"
 
1107
            b"\nb option 2 3 0 :",
 
1108
            call[1][1].getvalue())
 
1109
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1110
        self.assertIndexIsAB(index)
 
1111
 
 
1112
    def test_add_versions_random_id_is_accepted(self):
 
1113
        transport = MockTransport([
 
1114
            _KndxIndex.HEADER
 
1115
            ])
 
1116
        index = self.get_knit_index(transport, "filename", "r")
 
1117
        self.add_a_b(index, random_id=True)
 
1118
 
 
1119
    def test_delay_create_and_add_versions(self):
 
1120
        transport = MockTransport()
 
1121
 
 
1122
        index = self.get_knit_index(transport, "filename", "w")
 
1123
        # dir_mode=0777)
 
1124
        self.assertEqual([], transport.calls)
 
1125
        self.add_a_b(index)
 
1126
        #self.assertEqual(
 
1127
        #[    {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
 
1128
        #    kwargs)
 
1129
        # Two calls: one during which we load the existing index (and when its
 
1130
        # missing create it), then a second where we write the contents out.
 
1131
        self.assertEqual(2, len(transport.calls))
 
1132
        call = transport.calls.pop(0)
 
1133
        self.assertEqual('put_file_non_atomic', call[0])
 
1134
        self.assertEqual('filename.kndx', call[1][0])
 
1135
        # With no history, _KndxIndex writes a new index:
 
1136
        self.assertEqual(_KndxIndex.HEADER, call[1][1].getvalue())
 
1137
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1138
        call = transport.calls.pop(0)
 
1139
        # call[1][1] is a BytesIO - we can't test it by simple equality.
 
1140
        self.assertEqual('put_file_non_atomic', call[0])
 
1141
        self.assertEqual('filename.kndx', call[1][0])
 
1142
        # With no history, _KndxIndex writes a new index:
 
1143
        self.assertEqual(
 
1144
            _KndxIndex.HEADER +
 
1145
            b"\na option 0 1 .b :"
 
1146
            b"\na opt 1 2 .c :"
 
1147
            b"\nb option 2 3 0 :",
 
1148
            call[1][1].getvalue())
 
1149
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1150
 
 
1151
    def assertTotalBuildSize(self, size, keys, positions):
 
1152
        self.assertEqual(size,
 
1153
                         knit._get_total_build_size(None, keys, positions))
 
1154
 
 
1155
    def test__get_total_build_size(self):
 
1156
        positions = {
 
1157
            (b'a',): (('fulltext', False), ((b'a',), 0, 100), None),
 
1158
            (b'b',): (('line-delta', False), ((b'b',), 100, 21), (b'a',)),
 
1159
            (b'c',): (('line-delta', False), ((b'c',), 121, 35), (b'b',)),
 
1160
            (b'd',): (('line-delta', False), ((b'd',), 156, 12), (b'b',)),
 
1161
            }
 
1162
        self.assertTotalBuildSize(100, [(b'a',)], positions)
 
1163
        self.assertTotalBuildSize(121, [(b'b',)], positions)
 
1164
        # c needs both a & b
 
1165
        self.assertTotalBuildSize(156, [(b'c',)], positions)
 
1166
        # we shouldn't count 'b' twice
 
1167
        self.assertTotalBuildSize(156, [(b'b',), (b'c',)], positions)
 
1168
        self.assertTotalBuildSize(133, [(b'd',)], positions)
 
1169
        self.assertTotalBuildSize(168, [(b'c',), (b'd',)], positions)
 
1170
 
 
1171
    def test_get_position(self):
 
1172
        transport = MockTransport([
 
1173
            _KndxIndex.HEADER,
 
1174
            b"a option 0 1 :",
 
1175
            b"b option 1 2 :"
 
1176
            ])
 
1177
        index = self.get_knit_index(transport, "filename", "r")
 
1178
 
 
1179
        self.assertEqual(((b"a",), 0, 1), index.get_position((b"a",)))
 
1180
        self.assertEqual(((b"b",), 1, 2), index.get_position((b"b",)))
 
1181
 
 
1182
    def test_get_method(self):
 
1183
        transport = MockTransport([
 
1184
            _KndxIndex.HEADER,
 
1185
            b"a fulltext,unknown 0 1 :",
 
1186
            b"b unknown,line-delta 1 2 :",
 
1187
            b"c bad 3 4 :"
 
1188
            ])
 
1189
        index = self.get_knit_index(transport, "filename", "r")
 
1190
 
 
1191
        self.assertEqual("fulltext", index.get_method(b"a"))
 
1192
        self.assertEqual("line-delta", index.get_method(b"b"))
 
1193
        self.assertRaises(knit.KnitIndexUnknownMethod, index.get_method, b"c")
 
1194
 
 
1195
    def test_get_options(self):
 
1196
        transport = MockTransport([
 
1197
            _KndxIndex.HEADER,
 
1198
            b"a opt1 0 1 :",
 
1199
            b"b opt2,opt3 1 2 :"
 
1200
            ])
 
1201
        index = self.get_knit_index(transport, "filename", "r")
 
1202
 
 
1203
        self.assertEqual([b"opt1"], index.get_options(b"a"))
 
1204
        self.assertEqual([b"opt2", b"opt3"], index.get_options(b"b"))
 
1205
 
 
1206
    def test_get_parent_map(self):
 
1207
        transport = MockTransport([
 
1208
            _KndxIndex.HEADER,
 
1209
            b"a option 0 1 :",
 
1210
            b"b option 1 2 0 .c :",
 
1211
            b"c option 1 2 1 0 .e :"
 
1212
            ])
 
1213
        index = self.get_knit_index(transport, "filename", "r")
 
1214
 
 
1215
        self.assertEqual({
 
1216
            (b"a",): (),
 
1217
            (b"b",): ((b"a",), (b"c",)),
 
1218
            (b"c",): ((b"b",), (b"a",), (b"e",)),
 
1219
            }, index.get_parent_map(index.keys()))
 
1220
 
 
1221
    def test_impossible_parent(self):
 
1222
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
 
1223
        transport = MockTransport([
 
1224
            _KndxIndex.HEADER,
 
1225
            b"a option 0 1 :",
 
1226
            b"b option 0 1 4 :"  # We don't have a 4th record
 
1227
            ])
 
1228
        index = self.get_knit_index(transport, 'filename', 'r')
 
1229
        self.assertRaises(KnitCorrupt, index.keys)
 
1230
 
 
1231
    def test_corrupted_parent(self):
 
1232
        transport = MockTransport([
 
1233
            _KndxIndex.HEADER,
 
1234
            b"a option 0 1 :",
 
1235
            b"b option 0 1 :",
 
1236
            b"c option 0 1 1v :", # Can't have a parent of '1v'
 
1237
            ])
 
1238
        index = self.get_knit_index(transport, 'filename', 'r')
 
1239
        self.assertRaises(KnitCorrupt, index.keys)
 
1240
 
 
1241
    def test_corrupted_parent_in_list(self):
 
1242
        transport = MockTransport([
 
1243
            _KndxIndex.HEADER,
 
1244
            b"a option 0 1 :",
 
1245
            b"b option 0 1 :",
 
1246
            b"c option 0 1 1 v :", # Can't have a parent of 'v'
 
1247
            ])
 
1248
        index = self.get_knit_index(transport, 'filename', 'r')
 
1249
        self.assertRaises(KnitCorrupt, index.keys)
 
1250
 
 
1251
    def test_invalid_position(self):
 
1252
        transport = MockTransport([
 
1253
            _KndxIndex.HEADER,
 
1254
            b"a option 1v 1 :",
 
1255
            ])
 
1256
        index = self.get_knit_index(transport, 'filename', 'r')
 
1257
        self.assertRaises(KnitCorrupt, index.keys)
 
1258
 
 
1259
    def test_invalid_size(self):
 
1260
        transport = MockTransport([
 
1261
            _KndxIndex.HEADER,
 
1262
            b"a option 1 1v :",
 
1263
            ])
 
1264
        index = self.get_knit_index(transport, 'filename', 'r')
 
1265
        self.assertRaises(KnitCorrupt, index.keys)
 
1266
 
 
1267
    def test_scan_unvalidated_index_not_implemented(self):
 
1268
        transport = MockTransport()
 
1269
        index = self.get_knit_index(transport, 'filename', 'r')
 
1270
        self.assertRaises(
 
1271
            NotImplementedError, index.scan_unvalidated_index,
 
1272
            'dummy graph_index')
 
1273
        self.assertRaises(
 
1274
            NotImplementedError, index.get_missing_compression_parents)
 
1275
 
 
1276
    def test_short_line(self):
 
1277
        transport = MockTransport([
 
1278
            _KndxIndex.HEADER,
 
1279
            b"a option 0 10  :",
 
1280
            b"b option 10 10 0", # This line isn't terminated, ignored
 
1281
            ])
 
1282
        index = self.get_knit_index(transport, "filename", "r")
 
1283
        self.assertEqual({(b'a',)}, index.keys())
 
1284
 
 
1285
    def test_skip_incomplete_record(self):
 
1286
        # A line with bogus data should just be skipped
 
1287
        transport = MockTransport([
 
1288
            _KndxIndex.HEADER,
 
1289
            b"a option 0 10  :",
 
1290
            b"b option 10 10 0", # This line isn't terminated, ignored
 
1291
            b"c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1292
            ])
 
1293
        index = self.get_knit_index(transport, "filename", "r")
 
1294
        self.assertEqual({(b'a',), (b'c',)}, index.keys())
 
1295
 
 
1296
    def test_trailing_characters(self):
 
1297
        # A line with bogus data should just be skipped
 
1298
        transport = MockTransport([
 
1299
            _KndxIndex.HEADER,
 
1300
            b"a option 0 10  :",
 
1301
            b"b option 10 10 0 :a", # This line has extra trailing characters
 
1302
            b"c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1303
            ])
 
1304
        index = self.get_knit_index(transport, "filename", "r")
 
1305
        self.assertEqual({(b'a',), (b'c',)}, index.keys())
 
1306
 
 
1307
 
 
1308
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
 
1309
 
 
1310
    _test_needs_features = [compiled_knit_feature]
 
1311
 
 
1312
    @property
 
1313
    def _load_data(self):
 
1314
        from ..bzr._knit_load_data_pyx import _load_data_c
 
1315
        return _load_data_c
 
1316
 
 
1317
 
 
1318
class Test_KnitAnnotator(TestCaseWithMemoryTransport):
 
1319
 
 
1320
    def make_annotator(self):
 
1321
        factory = knit.make_pack_factory(True, True, 1)
 
1322
        vf = factory(self.get_transport())
 
1323
        return knit._KnitAnnotator(vf)
 
1324
 
 
1325
    def test__expand_fulltext(self):
 
1326
        ann = self.make_annotator()
 
1327
        rev_key = (b'rev-id',)
 
1328
        ann._num_compression_children[rev_key] = 1
 
1329
        res = ann._expand_record(rev_key, ((b'parent-id',),), None,
 
1330
                           [b'line1\n', b'line2\n'], ('fulltext', True))
 
1331
        # The content object and text lines should be cached appropriately
 
1332
        self.assertEqual([b'line1\n', b'line2'], res)
 
1333
        content_obj = ann._content_objects[rev_key]
 
1334
        self.assertEqual([b'line1\n', b'line2\n'], content_obj._lines)
 
1335
        self.assertEqual(res, content_obj.text())
 
1336
        self.assertEqual(res, ann._text_cache[rev_key])
 
1337
 
 
1338
    def test__expand_delta_comp_parent_not_available(self):
 
1339
        # Parent isn't available yet, so we return nothing, but queue up this
 
1340
        # node for later processing
 
1341
        ann = self.make_annotator()
 
1342
        rev_key = (b'rev-id',)
 
1343
        parent_key = (b'parent-id',)
 
1344
        record = [b'0,1,1\n', b'new-line\n']
 
1345
        details = ('line-delta', False)
 
1346
        res = ann._expand_record(rev_key, (parent_key,), parent_key,
 
1347
                                 record, details)
 
1348
        self.assertEqual(None, res)
 
1349
        self.assertTrue(parent_key in ann._pending_deltas)
 
1350
        pending = ann._pending_deltas[parent_key]
 
1351
        self.assertEqual(1, len(pending))
 
1352
        self.assertEqual((rev_key, (parent_key,), record, details), pending[0])
 
1353
 
 
1354
    def test__expand_record_tracks_num_children(self):
 
1355
        ann = self.make_annotator()
 
1356
        rev_key = (b'rev-id',)
 
1357
        rev2_key = (b'rev2-id',)
 
1358
        parent_key = (b'parent-id',)
 
1359
        record = [b'0,1,1\n', b'new-line\n']
 
1360
        details = ('line-delta', False)
 
1361
        ann._num_compression_children[parent_key] = 2
 
1362
        ann._expand_record(parent_key, (), None, [b'line1\n', b'line2\n'],
 
1363
                           ('fulltext', False))
 
1364
        res = ann._expand_record(rev_key, (parent_key,), parent_key,
 
1365
                                 record, details)
 
1366
        self.assertEqual({parent_key: 1}, ann._num_compression_children)
 
1367
        # Expanding the second child should remove the content object, and the
 
1368
        # num_compression_children entry
 
1369
        res = ann._expand_record(rev2_key, (parent_key,), parent_key,
 
1370
                                 record, details)
 
1371
        self.assertFalse(parent_key in ann._content_objects)
 
1372
        self.assertEqual({}, ann._num_compression_children)
 
1373
        # We should not cache the content_objects for rev2 and rev, because
 
1374
        # they do not have compression children of their own.
 
1375
        self.assertEqual({}, ann._content_objects)
 
1376
 
 
1377
    def test__expand_delta_records_blocks(self):
 
1378
        ann = self.make_annotator()
 
1379
        rev_key = (b'rev-id',)
 
1380
        parent_key = (b'parent-id',)
 
1381
        record = [b'0,1,1\n', b'new-line\n']
 
1382
        details = ('line-delta', True)
 
1383
        ann._num_compression_children[parent_key] = 2
 
1384
        ann._expand_record(parent_key, (), None,
 
1385
                           [b'line1\n', b'line2\n', b'line3\n'],
 
1386
                           ('fulltext', False))
 
1387
        ann._expand_record(rev_key, (parent_key,), parent_key, record, details)
 
1388
        self.assertEqual({(rev_key, parent_key): [(1, 1, 1), (3, 3, 0)]},
 
1389
                         ann._matching_blocks)
 
1390
        rev2_key = (b'rev2-id',)
 
1391
        record = [b'0,1,1\n', b'new-line\n']
 
1392
        details = ('line-delta', False)
 
1393
        ann._expand_record(rev2_key, (parent_key,), parent_key, record, details)
 
1394
        self.assertEqual([(1, 1, 2), (3, 3, 0)],
 
1395
                         ann._matching_blocks[(rev2_key, parent_key)])
 
1396
 
 
1397
    def test__get_parent_ann_uses_matching_blocks(self):
 
1398
        ann = self.make_annotator()
 
1399
        rev_key = (b'rev-id',)
 
1400
        parent_key = (b'parent-id',)
 
1401
        parent_ann = [(parent_key,)]*3
 
1402
        block_key = (rev_key, parent_key)
 
1403
        ann._annotations_cache[parent_key] = parent_ann
 
1404
        ann._matching_blocks[block_key] = [(0, 1, 1), (3, 3, 0)]
 
1405
        # We should not try to access any parent_lines content, because we know
 
1406
        # we already have the matching blocks
 
1407
        par_ann, blocks = ann._get_parent_annotations_and_matches(rev_key,
 
1408
                                        [b'1\n', b'2\n', b'3\n'], parent_key)
 
1409
        self.assertEqual(parent_ann, par_ann)
 
1410
        self.assertEqual([(0, 1, 1), (3, 3, 0)], blocks)
 
1411
        self.assertEqual({}, ann._matching_blocks)
 
1412
 
 
1413
    def test__process_pending(self):
 
1414
        ann = self.make_annotator()
 
1415
        rev_key = (b'rev-id',)
 
1416
        p1_key = (b'p1-id',)
 
1417
        p2_key = (b'p2-id',)
 
1418
        record = [b'0,1,1\n', b'new-line\n']
 
1419
        details = ('line-delta', False)
 
1420
        p1_record = [b'line1\n', b'line2\n']
 
1421
        ann._num_compression_children[p1_key] = 1
 
1422
        res = ann._expand_record(rev_key, (p1_key, p2_key), p1_key,
 
1423
                                 record, details)
 
1424
        self.assertEqual(None, res)
 
1425
        # self.assertTrue(p1_key in ann._pending_deltas)
 
1426
        self.assertEqual({}, ann._pending_annotation)
 
1427
        # Now insert p1, and we should be able to expand the delta
 
1428
        res = ann._expand_record(p1_key, (), None, p1_record,
 
1429
                                 ('fulltext', False))
 
1430
        self.assertEqual(p1_record, res)
 
1431
        ann._annotations_cache[p1_key] = [(p1_key,)]*2
 
1432
        res = ann._process_pending(p1_key)
 
1433
        self.assertEqual([], res)
 
1434
        self.assertFalse(p1_key in ann._pending_deltas)
 
1435
        self.assertTrue(p2_key in ann._pending_annotation)
 
1436
        self.assertEqual({p2_key: [(rev_key, (p1_key, p2_key))]},
 
1437
                         ann._pending_annotation)
 
1438
        # Now fill in parent 2, and pending annotation should be satisfied
 
1439
        res = ann._expand_record(p2_key, (), None, [], ('fulltext', False))
 
1440
        ann._annotations_cache[p2_key] = []
 
1441
        res = ann._process_pending(p2_key)
 
1442
        self.assertEqual([rev_key], res)
 
1443
        self.assertEqual({}, ann._pending_annotation)
 
1444
        self.assertEqual({}, ann._pending_deltas)
 
1445
 
 
1446
    def test_record_delta_removes_basis(self):
 
1447
        ann = self.make_annotator()
 
1448
        ann._expand_record((b'parent-id',), (), None,
 
1449
                           [b'line1\n', b'line2\n'], ('fulltext', False))
 
1450
        ann._num_compression_children[b'parent-id'] = 2
 
1451
 
 
1452
    def test_annotate_special_text(self):
 
1453
        ann = self.make_annotator()
 
1454
        vf = ann._vf
 
1455
        rev1_key = (b'rev-1',)
 
1456
        rev2_key = (b'rev-2',)
 
1457
        rev3_key = (b'rev-3',)
 
1458
        spec_key = (b'special:',)
 
1459
        vf.add_lines(rev1_key, [], [b'initial content\n'])
 
1460
        vf.add_lines(rev2_key, [rev1_key], [b'initial content\n',
 
1461
                                            b'common content\n',
 
1462
                                            b'content in 2\n'])
 
1463
        vf.add_lines(rev3_key, [rev1_key], [b'initial content\n',
 
1464
                                            b'common content\n',
 
1465
                                            b'content in 3\n'])
 
1466
        spec_text = (b'initial content\n'
 
1467
                     b'common content\n'
 
1468
                     b'content in 2\n'
 
1469
                     b'content in 3\n')
 
1470
        ann.add_special_text(spec_key, [rev2_key, rev3_key], spec_text)
 
1471
        anns, lines = ann.annotate(spec_key)
 
1472
        self.assertEqual([(rev1_key,),
 
1473
                          (rev2_key, rev3_key),
 
1474
                          (rev2_key,),
 
1475
                          (rev3_key,),
 
1476
                         ], anns)
 
1477
        self.assertEqualDiff(spec_text, b''.join(lines))
 
1478
 
 
1479
 
 
1480
class KnitTests(TestCaseWithTransport):
 
1481
    """Class containing knit test helper routines."""
 
1482
 
 
1483
    def make_test_knit(self, annotate=False, name='test'):
 
1484
        mapper = ConstantMapper(name)
 
1485
        return make_file_factory(annotate, mapper)(self.get_transport())
 
1486
 
 
1487
 
 
1488
class TestBadShaError(KnitTests):
 
1489
    """Tests for handling of sha errors."""
 
1490
 
 
1491
    def test_sha_exception_has_text(self):
 
1492
        # having the failed text included in the error allows for recovery.
 
1493
        source = self.make_test_knit()
 
1494
        target = self.make_test_knit(name="target")
 
1495
        if not source._max_delta_chain:
 
1496
            raise TestNotApplicable(
 
1497
                "cannot get delta-caused sha failures without deltas.")
 
1498
        # create a basis
 
1499
        basis = (b'basis',)
 
1500
        broken = (b'broken',)
 
1501
        source.add_lines(basis, (), [b'foo\n'])
 
1502
        source.add_lines(broken, (basis,), [b'foo\n', b'bar\n'])
 
1503
        # Seed target with a bad basis text
 
1504
        target.add_lines(basis, (), [b'gam\n'])
 
1505
        target.insert_record_stream(
 
1506
            source.get_record_stream([broken], 'unordered', False))
 
1507
        err = self.assertRaises(KnitCorrupt,
 
1508
            next(target.get_record_stream([broken], 'unordered', True
 
1509
            )).get_bytes_as, 'chunked')
 
1510
        self.assertEqual([b'gam\n', b'bar\n'], err.content)
 
1511
        # Test for formatting with live data
 
1512
        self.assertStartsWith(str(err), "Knit ")
 
1513
 
 
1514
 
 
1515
class TestKnitIndex(KnitTests):
 
1516
 
 
1517
    def test_add_versions_dictionary_compresses(self):
 
1518
        """Adding versions to the index should update the lookup dict"""
 
1519
        knit = self.make_test_knit()
 
1520
        idx = knit._index
 
1521
        idx.add_records([((b'a-1',), [b'fulltext'], ((b'a-1',), 0, 0), [])])
 
1522
        self.check_file_contents('test.kndx',
 
1523
            b'# bzr knit index 8\n'
 
1524
            b'\n'
 
1525
            b'a-1 fulltext 0 0  :'
 
1526
            )
 
1527
        idx.add_records([
 
1528
            ((b'a-2',), [b'fulltext'], ((b'a-2',), 0, 0), [(b'a-1',)]),
 
1529
            ((b'a-3',), [b'fulltext'], ((b'a-3',), 0, 0), [(b'a-2',)]),
 
1530
            ])
 
1531
        self.check_file_contents('test.kndx',
 
1532
            b'# bzr knit index 8\n'
 
1533
            b'\n'
 
1534
            b'a-1 fulltext 0 0  :\n'
 
1535
            b'a-2 fulltext 0 0 0 :\n'
 
1536
            b'a-3 fulltext 0 0 1 :'
 
1537
            )
 
1538
        self.assertEqual({(b'a-3',), (b'a-1',), (b'a-2',)}, idx.keys())
 
1539
        self.assertEqual({
 
1540
            (b'a-1',): (((b'a-1',), 0, 0), None, (), ('fulltext', False)),
 
1541
            (b'a-2',): (((b'a-2',), 0, 0), None, ((b'a-1',),), ('fulltext', False)),
 
1542
            (b'a-3',): (((b'a-3',), 0, 0), None, ((b'a-2',),), ('fulltext', False)),
 
1543
            }, idx.get_build_details(idx.keys()))
 
1544
        self.assertEqual({(b'a-1',): (),
 
1545
            (b'a-2',): ((b'a-1',),),
 
1546
            (b'a-3',): ((b'a-2',),),},
 
1547
            idx.get_parent_map(idx.keys()))
 
1548
 
 
1549
    def test_add_versions_fails_clean(self):
 
1550
        """If add_versions fails in the middle, it restores a pristine state.
 
1551
 
 
1552
        Any modifications that are made to the index are reset if all versions
 
1553
        cannot be added.
 
1554
        """
 
1555
        # This cheats a little bit by passing in a generator which will
 
1556
        # raise an exception before the processing finishes
 
1557
        # Other possibilities would be to have an version with the wrong number
 
1558
        # of entries, or to make the backing transport unable to write any
 
1559
        # files.
 
1560
 
 
1561
        knit = self.make_test_knit()
 
1562
        idx = knit._index
 
1563
        idx.add_records([((b'a-1',), [b'fulltext'], ((b'a-1',), 0, 0), [])])
 
1564
 
 
1565
        class StopEarly(Exception):
 
1566
            pass
 
1567
 
 
1568
        def generate_failure():
 
1569
            """Add some entries and then raise an exception"""
 
1570
            yield ((b'a-2',), [b'fulltext'], (None, 0, 0), (b'a-1',))
 
1571
            yield ((b'a-3',), [b'fulltext'], (None, 0, 0), (b'a-2',))
 
1572
            raise StopEarly()
 
1573
 
 
1574
        # Assert the pre-condition
 
1575
        def assertA1Only():
 
1576
            self.assertEqual({(b'a-1',)}, set(idx.keys()))
 
1577
            self.assertEqual(
 
1578
                {(b'a-1',): (((b'a-1',), 0, 0), None, (), ('fulltext', False))},
 
1579
                idx.get_build_details([(b'a-1',)]))
 
1580
            self.assertEqual({(b'a-1',):()}, idx.get_parent_map(idx.keys()))
 
1581
 
 
1582
        assertA1Only()
 
1583
        self.assertRaises(StopEarly, idx.add_records, generate_failure())
 
1584
        # And it shouldn't be modified
 
1585
        assertA1Only()
 
1586
 
 
1587
    def test_knit_index_ignores_empty_files(self):
 
1588
        # There was a race condition in older bzr, where a ^C at the right time
 
1589
        # could leave an empty .kndx file, which bzr would later claim was a
 
1590
        # corrupted file since the header was not present. In reality, the file
 
1591
        # just wasn't created, so it should be ignored.
 
1592
        t = transport.get_transport_from_path('.')
 
1593
        t.put_bytes('test.kndx', b'')
 
1594
 
 
1595
        knit = self.make_test_knit()
 
1596
 
 
1597
    def test_knit_index_checks_header(self):
 
1598
        t = transport.get_transport_from_path('.')
 
1599
        t.put_bytes('test.kndx', b'# not really a knit header\n\n')
 
1600
        k = self.make_test_knit()
 
1601
        self.assertRaises(KnitHeaderError, k.keys)
 
1602
 
 
1603
 
 
1604
class TestGraphIndexKnit(KnitTests):
 
1605
    """Tests for knits using a GraphIndex rather than a KnitIndex."""
 
1606
 
 
1607
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1608
        builder = GraphIndexBuilder(ref_lists)
 
1609
        for node, references, value in nodes:
 
1610
            builder.add_node(node, references, value)
 
1611
        stream = builder.finish()
 
1612
        trans = self.get_transport()
 
1613
        size = trans.put_file(name, stream)
 
1614
        return GraphIndex(trans, name, size)
 
1615
 
 
1616
    def two_graph_index(self, deltas=False, catch_adds=False):
 
1617
        """Build a two-graph index.
 
1618
 
 
1619
        :param deltas: If true, use underlying indices with two node-ref
 
1620
            lists and 'parent' set to a delta-compressed against tail.
 
1621
        """
 
1622
        # build a complex graph across several indices.
 
1623
        if deltas:
 
1624
            # delta compression inn the index
 
1625
            index1 = self.make_g_index('1', 2, [
 
1626
                ((b'tip', ), b'N0 100', ([(b'parent', )], [], )),
 
1627
                ((b'tail', ), b'', ([], []))])
 
1628
            index2 = self.make_g_index('2', 2, [
 
1629
                ((b'parent', ), b' 100 78', ([(b'tail', ), (b'ghost', )], [(b'tail', )])),
 
1630
                ((b'separate', ), b'', ([], []))])
 
1631
        else:
 
1632
            # just blob location and graph in the index.
 
1633
            index1 = self.make_g_index('1', 1, [
 
1634
                ((b'tip', ), b'N0 100', ([(b'parent', )], )),
 
1635
                ((b'tail', ), b'', ([], ))])
 
1636
            index2 = self.make_g_index('2', 1, [
 
1637
                ((b'parent', ), b' 100 78', ([(b'tail', ), (b'ghost', )], )),
 
1638
                ((b'separate', ), b'', ([], ))])
 
1639
        combined_index = CombinedGraphIndex([index1, index2])
 
1640
        if catch_adds:
 
1641
            self.combined_index = combined_index
 
1642
            self.caught_entries = []
 
1643
            add_callback = self.catch_add
 
1644
        else:
 
1645
            add_callback = None
 
1646
        return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
 
1647
            add_callback=add_callback)
 
1648
 
 
1649
    def test_keys(self):
 
1650
        index = self.two_graph_index()
 
1651
        self.assertEqual({(b'tail',), (b'tip',), (b'parent',), (b'separate',)},
 
1652
            set(index.keys()))
 
1653
 
 
1654
    def test_get_position(self):
 
1655
        index = self.two_graph_index()
 
1656
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position((b'tip',)))
 
1657
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position((b'parent',)))
 
1658
 
 
1659
    def test_get_method_deltas(self):
 
1660
        index = self.two_graph_index(deltas=True)
 
1661
        self.assertEqual('fulltext', index.get_method((b'tip',)))
 
1662
        self.assertEqual('line-delta', index.get_method((b'parent',)))
 
1663
 
 
1664
    def test_get_method_no_deltas(self):
 
1665
        # check that the parent-history lookup is ignored with deltas=False.
 
1666
        index = self.two_graph_index(deltas=False)
 
1667
        self.assertEqual('fulltext', index.get_method((b'tip',)))
 
1668
        self.assertEqual('fulltext', index.get_method((b'parent',)))
 
1669
 
 
1670
    def test_get_options_deltas(self):
 
1671
        index = self.two_graph_index(deltas=True)
 
1672
        self.assertEqual([b'fulltext', b'no-eol'], index.get_options((b'tip',)))
 
1673
        self.assertEqual([b'line-delta'], index.get_options((b'parent',)))
 
1674
 
 
1675
    def test_get_options_no_deltas(self):
 
1676
        # check that the parent-history lookup is ignored with deltas=False.
 
1677
        index = self.two_graph_index(deltas=False)
 
1678
        self.assertEqual([b'fulltext', b'no-eol'], index.get_options((b'tip',)))
 
1679
        self.assertEqual([b'fulltext'], index.get_options((b'parent',)))
 
1680
 
 
1681
    def test_get_parent_map(self):
 
1682
        index = self.two_graph_index()
 
1683
        self.assertEqual({(b'parent',):((b'tail',), (b'ghost',))},
 
1684
            index.get_parent_map([(b'parent',), (b'ghost',)]))
 
1685
 
 
1686
    def catch_add(self, entries):
 
1687
        self.caught_entries.append(entries)
 
1688
 
 
1689
    def test_add_no_callback_errors(self):
 
1690
        index = self.two_graph_index()
 
1691
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1692
            [((b'new',), b'fulltext,no-eol', (None, 50, 60), [b'separate'])])
 
1693
 
 
1694
    def test_add_version_smoke(self):
 
1695
        index = self.two_graph_index(catch_adds=True)
 
1696
        index.add_records([((b'new',), b'fulltext,no-eol', (None, 50, 60),
 
1697
            [(b'separate',)])])
 
1698
        self.assertEqual([[((b'new', ), b'N50 60', (((b'separate',),),))]],
 
1699
            self.caught_entries)
 
1700
 
 
1701
    def test_add_version_delta_not_delta_index(self):
 
1702
        index = self.two_graph_index(catch_adds=True)
 
1703
        self.assertRaises(KnitCorrupt, index.add_records,
 
1704
            [((b'new',), b'no-eol,line-delta', (None, 0, 100), [(b'parent',)])])
 
1705
        self.assertEqual([], self.caught_entries)
 
1706
 
 
1707
    def test_add_version_same_dup(self):
 
1708
        index = self.two_graph_index(catch_adds=True)
 
1709
        # options can be spelt two different ways
 
1710
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 100), [(b'parent',)])])
 
1711
        index.add_records([((b'tip',), b'no-eol,fulltext', (None, 0, 100), [(b'parent',)])])
 
1712
        # position/length are ignored (because each pack could have fulltext or
 
1713
        # delta, and be at a different position.
 
1714
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 50, 100),
 
1715
            [(b'parent',)])])
 
1716
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 1000),
 
1717
            [(b'parent',)])])
 
1718
        # but neither should have added data:
 
1719
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1720
 
 
1721
    def test_add_version_different_dup(self):
 
1722
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1723
        # change options
 
1724
        self.assertRaises(KnitCorrupt, index.add_records,
 
1725
            [((b'tip',), b'line-delta', (None, 0, 100), [(b'parent',)])])
 
1726
        self.assertRaises(KnitCorrupt, index.add_records,
 
1727
            [((b'tip',), b'fulltext', (None, 0, 100), [(b'parent',)])])
 
1728
        # parents
 
1729
        self.assertRaises(KnitCorrupt, index.add_records,
 
1730
            [((b'tip',), b'fulltext,no-eol', (None, 0, 100), [])])
 
1731
        self.assertEqual([], self.caught_entries)
 
1732
 
 
1733
    def test_add_versions_nodeltas(self):
 
1734
        index = self.two_graph_index(catch_adds=True)
 
1735
        index.add_records([
 
1736
                ((b'new',), b'fulltext,no-eol', (None, 50, 60), [(b'separate',)]),
 
1737
                ((b'new2',), b'fulltext', (None, 0, 6), [(b'new',)]),
 
1738
                ])
 
1739
        self.assertEqual([((b'new', ), b'N50 60', (((b'separate',),),)),
 
1740
            ((b'new2', ), b' 0 6', (((b'new',),),))],
 
1741
            sorted(self.caught_entries[0]))
 
1742
        self.assertEqual(1, len(self.caught_entries))
 
1743
 
 
1744
    def test_add_versions_deltas(self):
 
1745
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1746
        index.add_records([
 
1747
                ((b'new',), b'fulltext,no-eol', (None, 50, 60), [(b'separate',)]),
 
1748
                ((b'new2',), b'line-delta', (None, 0, 6), [(b'new',)]),
 
1749
                ])
 
1750
        self.assertEqual([((b'new', ), b'N50 60', (((b'separate',),), ())),
 
1751
            ((b'new2', ), b' 0 6', (((b'new',),), ((b'new',),), ))],
 
1752
            sorted(self.caught_entries[0]))
 
1753
        self.assertEqual(1, len(self.caught_entries))
 
1754
 
 
1755
    def test_add_versions_delta_not_delta_index(self):
 
1756
        index = self.two_graph_index(catch_adds=True)
 
1757
        self.assertRaises(KnitCorrupt, index.add_records,
 
1758
            [((b'new',), b'no-eol,line-delta', (None, 0, 100), [(b'parent',)])])
 
1759
        self.assertEqual([], self.caught_entries)
 
1760
 
 
1761
    def test_add_versions_random_id_accepted(self):
 
1762
        index = self.two_graph_index(catch_adds=True)
 
1763
        index.add_records([], random_id=True)
 
1764
 
 
1765
    def test_add_versions_same_dup(self):
 
1766
        index = self.two_graph_index(catch_adds=True)
 
1767
        # options can be spelt two different ways
 
1768
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 100),
 
1769
            [(b'parent',)])])
 
1770
        index.add_records([((b'tip',), b'no-eol,fulltext', (None, 0, 100),
 
1771
            [(b'parent',)])])
 
1772
        # position/length are ignored (because each pack could have fulltext or
 
1773
        # delta, and be at a different position.
 
1774
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 50, 100),
 
1775
            [(b'parent',)])])
 
1776
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 1000),
 
1777
            [(b'parent',)])])
 
1778
        # but neither should have added data.
 
1779
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1780
 
 
1781
    def test_add_versions_different_dup(self):
 
1782
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1783
        # change options
 
1784
        self.assertRaises(KnitCorrupt, index.add_records,
 
1785
            [((b'tip',), b'line-delta', (None, 0, 100), [(b'parent',)])])
 
1786
        self.assertRaises(KnitCorrupt, index.add_records,
 
1787
            [((b'tip',), b'fulltext', (None, 0, 100), [(b'parent',)])])
 
1788
        # parents
 
1789
        self.assertRaises(KnitCorrupt, index.add_records,
 
1790
            [((b'tip',), b'fulltext,no-eol', (None, 0, 100), [])])
 
1791
        # change options in the second record
 
1792
        self.assertRaises(KnitCorrupt, index.add_records,
 
1793
            [((b'tip',), b'fulltext,no-eol', (None, 0, 100), [(b'parent',)]),
 
1794
             ((b'tip',), b'line-delta', (None, 0, 100), [(b'parent',)])])
 
1795
        self.assertEqual([], self.caught_entries)
 
1796
 
 
1797
    def make_g_index_missing_compression_parent(self):
 
1798
        graph_index = self.make_g_index('missing_comp', 2,
 
1799
            [((b'tip', ), b' 100 78',
 
1800
              ([(b'missing-parent', ), (b'ghost', )], [(b'missing-parent', )]))])
 
1801
        return graph_index
 
1802
 
 
1803
    def make_g_index_missing_parent(self):
 
1804
        graph_index = self.make_g_index('missing_parent', 2,
 
1805
            [((b'parent', ), b' 100 78', ([], [])),
 
1806
             ((b'tip', ), b' 100 78',
 
1807
              ([(b'parent', ), (b'missing-parent', )], [(b'parent', )])),
 
1808
              ])
 
1809
        return graph_index
 
1810
 
 
1811
    def make_g_index_no_external_refs(self):
 
1812
        graph_index = self.make_g_index('no_external_refs', 2,
 
1813
            [((b'rev', ), b' 100 78',
 
1814
              ([(b'parent', ), (b'ghost', )], []))])
 
1815
        return graph_index
 
1816
 
 
1817
    def test_add_good_unvalidated_index(self):
 
1818
        unvalidated = self.make_g_index_no_external_refs()
 
1819
        combined = CombinedGraphIndex([unvalidated])
 
1820
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1821
        index.scan_unvalidated_index(unvalidated)
 
1822
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1823
 
 
1824
    def test_add_missing_compression_parent_unvalidated_index(self):
 
1825
        unvalidated = self.make_g_index_missing_compression_parent()
 
1826
        combined = CombinedGraphIndex([unvalidated])
 
1827
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1828
        index.scan_unvalidated_index(unvalidated)
 
1829
        # This also checks that its only the compression parent that is
 
1830
        # examined, otherwise 'ghost' would also be reported as a missing
 
1831
        # parent.
 
1832
        self.assertEqual(
 
1833
            frozenset([(b'missing-parent',)]),
 
1834
            index.get_missing_compression_parents())
 
1835
 
 
1836
    def test_add_missing_noncompression_parent_unvalidated_index(self):
 
1837
        unvalidated = self.make_g_index_missing_parent()
 
1838
        combined = CombinedGraphIndex([unvalidated])
 
1839
        index = _KnitGraphIndex(combined, lambda: True, deltas=True,
 
1840
            track_external_parent_refs=True)
 
1841
        index.scan_unvalidated_index(unvalidated)
 
1842
        self.assertEqual(
 
1843
            frozenset([(b'missing-parent',)]), index.get_missing_parents())
 
1844
 
 
1845
    def test_track_external_parent_refs(self):
 
1846
        g_index = self.make_g_index('empty', 2, [])
 
1847
        combined = CombinedGraphIndex([g_index])
 
1848
        index = _KnitGraphIndex(combined, lambda: True, deltas=True,
 
1849
            add_callback=self.catch_add, track_external_parent_refs=True)
 
1850
        self.caught_entries = []
 
1851
        index.add_records([
 
1852
            ((b'new-key',), b'fulltext,no-eol', (None, 50, 60),
 
1853
             [(b'parent-1',), (b'parent-2',)])])
 
1854
        self.assertEqual(
 
1855
            frozenset([(b'parent-1',), (b'parent-2',)]),
 
1856
            index.get_missing_parents())
 
1857
 
 
1858
    def test_add_unvalidated_index_with_present_external_references(self):
 
1859
        index = self.two_graph_index(deltas=True)
 
1860
        # Ugly hack to get at one of the underlying GraphIndex objects that
 
1861
        # two_graph_index built.
 
1862
        unvalidated = index._graph_index._indices[1]
 
1863
        # 'parent' is an external ref of _indices[1] (unvalidated), but is
 
1864
        # present in _indices[0].
 
1865
        index.scan_unvalidated_index(unvalidated)
 
1866
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1867
 
 
1868
    def make_new_missing_parent_g_index(self, name):
 
1869
        missing_parent = name.encode('ascii') + b'-missing-parent'
 
1870
        graph_index = self.make_g_index(name, 2,
 
1871
            [((name.encode('ascii') + b'tip', ), b' 100 78',
 
1872
              ([(missing_parent, ), (b'ghost', )], [(missing_parent, )]))])
 
1873
        return graph_index
 
1874
 
 
1875
    def test_add_mulitiple_unvalidated_indices_with_missing_parents(self):
 
1876
        g_index_1 = self.make_new_missing_parent_g_index('one')
 
1877
        g_index_2 = self.make_new_missing_parent_g_index('two')
 
1878
        combined = CombinedGraphIndex([g_index_1, g_index_2])
 
1879
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1880
        index.scan_unvalidated_index(g_index_1)
 
1881
        index.scan_unvalidated_index(g_index_2)
 
1882
        self.assertEqual(
 
1883
            frozenset([(b'one-missing-parent',), (b'two-missing-parent',)]),
 
1884
            index.get_missing_compression_parents())
 
1885
 
 
1886
    def test_add_mulitiple_unvalidated_indices_with_mutual_dependencies(self):
 
1887
        graph_index_a = self.make_g_index('one', 2,
 
1888
            [((b'parent-one', ), b' 100 78', ([(b'non-compression-parent',)], [])),
 
1889
             ((b'child-of-two', ), b' 100 78',
 
1890
              ([(b'parent-two',)], [(b'parent-two',)]))])
 
1891
        graph_index_b = self.make_g_index('two', 2,
 
1892
            [((b'parent-two', ), b' 100 78', ([(b'non-compression-parent',)], [])),
 
1893
             ((b'child-of-one', ), b' 100 78',
 
1894
              ([(b'parent-one',)], [(b'parent-one',)]))])
 
1895
        combined = CombinedGraphIndex([graph_index_a, graph_index_b])
 
1896
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1897
        index.scan_unvalidated_index(graph_index_a)
 
1898
        index.scan_unvalidated_index(graph_index_b)
 
1899
        self.assertEqual(
 
1900
            frozenset([]), index.get_missing_compression_parents())
 
1901
 
 
1902
 
 
1903
class TestNoParentsGraphIndexKnit(KnitTests):
 
1904
    """Tests for knits using _KnitGraphIndex with no parents."""
 
1905
 
 
1906
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1907
        builder = GraphIndexBuilder(ref_lists)
 
1908
        for node, references in nodes:
 
1909
            builder.add_node(node, references)
 
1910
        stream = builder.finish()
 
1911
        trans = self.get_transport()
 
1912
        size = trans.put_file(name, stream)
 
1913
        return GraphIndex(trans, name, size)
 
1914
 
 
1915
    def test_add_good_unvalidated_index(self):
 
1916
        unvalidated = self.make_g_index('unvalidated')
 
1917
        combined = CombinedGraphIndex([unvalidated])
 
1918
        index = _KnitGraphIndex(combined, lambda: True, parents=False)
 
1919
        index.scan_unvalidated_index(unvalidated)
 
1920
        self.assertEqual(frozenset(),
 
1921
            index.get_missing_compression_parents())
 
1922
 
 
1923
    def test_parents_deltas_incompatible(self):
 
1924
        index = CombinedGraphIndex([])
 
1925
        self.assertRaises(knit.KnitError, _KnitGraphIndex, lambda:True,
 
1926
            index, deltas=True, parents=False)
 
1927
 
 
1928
    def two_graph_index(self, catch_adds=False):
 
1929
        """Build a two-graph index.
 
1930
 
 
1931
        :param deltas: If true, use underlying indices with two node-ref
 
1932
            lists and 'parent' set to a delta-compressed against tail.
 
1933
        """
 
1934
        # put several versions in the index.
 
1935
        index1 = self.make_g_index('1', 0, [
 
1936
            ((b'tip', ), b'N0 100'),
 
1937
            ((b'tail', ), b'')])
 
1938
        index2 = self.make_g_index('2', 0, [
 
1939
            ((b'parent', ), b' 100 78'),
 
1940
            ((b'separate', ), b'')])
 
1941
        combined_index = CombinedGraphIndex([index1, index2])
 
1942
        if catch_adds:
 
1943
            self.combined_index = combined_index
 
1944
            self.caught_entries = []
 
1945
            add_callback = self.catch_add
 
1946
        else:
 
1947
            add_callback = None
 
1948
        return _KnitGraphIndex(combined_index, lambda:True, parents=False,
 
1949
            add_callback=add_callback)
 
1950
 
 
1951
    def test_keys(self):
 
1952
        index = self.two_graph_index()
 
1953
        self.assertEqual({(b'tail',), (b'tip',), (b'parent',), (b'separate',)},
 
1954
            set(index.keys()))
 
1955
 
 
1956
    def test_get_position(self):
 
1957
        index = self.two_graph_index()
 
1958
        self.assertEqual((index._graph_index._indices[0], 0, 100),
 
1959
            index.get_position((b'tip',)))
 
1960
        self.assertEqual((index._graph_index._indices[1], 100, 78),
 
1961
            index.get_position((b'parent',)))
 
1962
 
 
1963
    def test_get_method(self):
 
1964
        index = self.two_graph_index()
 
1965
        self.assertEqual('fulltext', index.get_method((b'tip',)))
 
1966
        self.assertEqual([b'fulltext'], index.get_options((b'parent',)))
 
1967
 
 
1968
    def test_get_options(self):
 
1969
        index = self.two_graph_index()
 
1970
        self.assertEqual([b'fulltext', b'no-eol'], index.get_options((b'tip',)))
 
1971
        self.assertEqual([b'fulltext'], index.get_options((b'parent',)))
 
1972
 
 
1973
    def test_get_parent_map(self):
 
1974
        index = self.two_graph_index()
 
1975
        self.assertEqual({(b'parent',):None},
 
1976
            index.get_parent_map([(b'parent',), (b'ghost',)]))
 
1977
 
 
1978
    def catch_add(self, entries):
 
1979
        self.caught_entries.append(entries)
 
1980
 
 
1981
    def test_add_no_callback_errors(self):
 
1982
        index = self.two_graph_index()
 
1983
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1984
            [((b'new',), b'fulltext,no-eol', (None, 50, 60), [(b'separate',)])])
 
1985
 
 
1986
    def test_add_version_smoke(self):
 
1987
        index = self.two_graph_index(catch_adds=True)
 
1988
        index.add_records([((b'new',), b'fulltext,no-eol', (None, 50, 60), [])])
 
1989
        self.assertEqual([[((b'new', ), b'N50 60')]],
 
1990
            self.caught_entries)
 
1991
 
 
1992
    def test_add_version_delta_not_delta_index(self):
 
1993
        index = self.two_graph_index(catch_adds=True)
 
1994
        self.assertRaises(KnitCorrupt, index.add_records,
 
1995
            [((b'new',), b'no-eol,line-delta', (None, 0, 100), [])])
 
1996
        self.assertEqual([], self.caught_entries)
 
1997
 
 
1998
    def test_add_version_same_dup(self):
 
1999
        index = self.two_graph_index(catch_adds=True)
 
2000
        # options can be spelt two different ways
 
2001
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 100), [])])
 
2002
        index.add_records([((b'tip',), b'no-eol,fulltext', (None, 0, 100), [])])
 
2003
        # position/length are ignored (because each pack could have fulltext or
 
2004
        # delta, and be at a different position.
 
2005
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 50, 100), [])])
 
2006
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 1000), [])])
 
2007
        # but neither should have added data.
 
2008
        self.assertEqual([[], [], [], []], self.caught_entries)
 
2009
 
 
2010
    def test_add_version_different_dup(self):
 
2011
        index = self.two_graph_index(catch_adds=True)
 
2012
        # change options
 
2013
        self.assertRaises(KnitCorrupt, index.add_records,
 
2014
            [((b'tip',), b'no-eol,line-delta', (None, 0, 100), [])])
 
2015
        self.assertRaises(KnitCorrupt, index.add_records,
 
2016
            [((b'tip',), b'line-delta,no-eol', (None, 0, 100), [])])
 
2017
        self.assertRaises(KnitCorrupt, index.add_records,
 
2018
            [((b'tip',), b'fulltext', (None, 0, 100), [])])
 
2019
        # parents
 
2020
        self.assertRaises(KnitCorrupt, index.add_records,
 
2021
            [((b'tip',), b'fulltext,no-eol', (None, 0, 100), [(b'parent',)])])
 
2022
        self.assertEqual([], self.caught_entries)
 
2023
 
 
2024
    def test_add_versions(self):
 
2025
        index = self.two_graph_index(catch_adds=True)
 
2026
        index.add_records([
 
2027
                ((b'new',), b'fulltext,no-eol', (None, 50, 60), []),
 
2028
                ((b'new2',), b'fulltext', (None, 0, 6), []),
 
2029
                ])
 
2030
        self.assertEqual([((b'new', ), b'N50 60'), ((b'new2', ), b' 0 6')],
 
2031
            sorted(self.caught_entries[0]))
 
2032
        self.assertEqual(1, len(self.caught_entries))
 
2033
 
 
2034
    def test_add_versions_delta_not_delta_index(self):
 
2035
        index = self.two_graph_index(catch_adds=True)
 
2036
        self.assertRaises(KnitCorrupt, index.add_records,
 
2037
            [((b'new',), b'no-eol,line-delta', (None, 0, 100), [(b'parent',)])])
 
2038
        self.assertEqual([], self.caught_entries)
 
2039
 
 
2040
    def test_add_versions_parents_not_parents_index(self):
 
2041
        index = self.two_graph_index(catch_adds=True)
 
2042
        self.assertRaises(KnitCorrupt, index.add_records,
 
2043
            [((b'new',), b'no-eol,fulltext', (None, 0, 100), [(b'parent',)])])
 
2044
        self.assertEqual([], self.caught_entries)
 
2045
 
 
2046
    def test_add_versions_random_id_accepted(self):
 
2047
        index = self.two_graph_index(catch_adds=True)
 
2048
        index.add_records([], random_id=True)
 
2049
 
 
2050
    def test_add_versions_same_dup(self):
 
2051
        index = self.two_graph_index(catch_adds=True)
 
2052
        # options can be spelt two different ways
 
2053
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 100), [])])
 
2054
        index.add_records([((b'tip',), b'no-eol,fulltext', (None, 0, 100), [])])
 
2055
        # position/length are ignored (because each pack could have fulltext or
 
2056
        # delta, and be at a different position.
 
2057
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 50, 100), [])])
 
2058
        index.add_records([((b'tip',), b'fulltext,no-eol', (None, 0, 1000), [])])
 
2059
        # but neither should have added data.
 
2060
        self.assertEqual([[], [], [], []], self.caught_entries)
 
2061
 
 
2062
    def test_add_versions_different_dup(self):
 
2063
        index = self.two_graph_index(catch_adds=True)
 
2064
        # change options
 
2065
        self.assertRaises(KnitCorrupt, index.add_records,
 
2066
            [((b'tip',), b'no-eol,line-delta', (None, 0, 100), [])])
 
2067
        self.assertRaises(KnitCorrupt, index.add_records,
 
2068
            [((b'tip',), b'line-delta,no-eol', (None, 0, 100), [])])
 
2069
        self.assertRaises(KnitCorrupt, index.add_records,
 
2070
            [((b'tip',), b'fulltext', (None, 0, 100), [])])
 
2071
        # parents
 
2072
        self.assertRaises(KnitCorrupt, index.add_records,
 
2073
            [((b'tip',), b'fulltext,no-eol', (None, 0, 100), [(b'parent',)])])
 
2074
        # change options in the second record
 
2075
        self.assertRaises(KnitCorrupt, index.add_records,
 
2076
            [((b'tip',), b'fulltext,no-eol', (None, 0, 100), []),
 
2077
             ((b'tip',), b'no-eol,line-delta', (None, 0, 100), [])])
 
2078
        self.assertEqual([], self.caught_entries)
 
2079
 
 
2080
 
 
2081
class TestKnitVersionedFiles(KnitTests):
 
2082
 
 
2083
    def assertGroupKeysForIo(self, exp_groups, keys, non_local_keys,
 
2084
                             positions, _min_buffer_size=None):
 
2085
        kvf = self.make_test_knit()
 
2086
        if _min_buffer_size is None:
 
2087
            _min_buffer_size = knit._STREAM_MIN_BUFFER_SIZE
 
2088
        self.assertEqual(exp_groups, kvf._group_keys_for_io(keys,
 
2089
                                        non_local_keys, positions,
 
2090
                                        _min_buffer_size=_min_buffer_size))
 
2091
 
 
2092
    def assertSplitByPrefix(self, expected_map, expected_prefix_order,
 
2093
                            keys):
 
2094
        split, prefix_order = KnitVersionedFiles._split_by_prefix(keys)
 
2095
        self.assertEqual(expected_map, split)
 
2096
        self.assertEqual(expected_prefix_order, prefix_order)
 
2097
 
 
2098
    def test__group_keys_for_io(self):
 
2099
        ft_detail = ('fulltext', False)
 
2100
        ld_detail = ('line-delta', False)
 
2101
        f_a = (b'f', b'a')
 
2102
        f_b = (b'f', b'b')
 
2103
        f_c = (b'f', b'c')
 
2104
        g_a = (b'g', b'a')
 
2105
        g_b = (b'g', b'b')
 
2106
        g_c = (b'g', b'c')
 
2107
        positions = {
 
2108
            f_a: (ft_detail, (f_a, 0, 100), None),
 
2109
            f_b: (ld_detail, (f_b, 100, 21), f_a),
 
2110
            f_c: (ld_detail, (f_c, 180, 15), f_b),
 
2111
            g_a: (ft_detail, (g_a, 121, 35), None),
 
2112
            g_b: (ld_detail, (g_b, 156, 12), g_a),
 
2113
            g_c: (ld_detail, (g_c, 195, 13), g_a),
 
2114
            }
 
2115
        self.assertGroupKeysForIo([([f_a], set())],
 
2116
                                  [f_a], [], positions)
 
2117
        self.assertGroupKeysForIo([([f_a], {f_a})],
 
2118
                                  [f_a], [f_a], positions)
 
2119
        self.assertGroupKeysForIo([([f_a, f_b], set([]))],
 
2120
                                  [f_a, f_b], [], positions)
 
2121
        self.assertGroupKeysForIo([([f_a, f_b], {f_b})],
 
2122
                                  [f_a, f_b], [f_b], positions)
 
2123
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
2124
                                  [f_a, g_a, f_b, g_b], [], positions)
 
2125
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
2126
                                  [f_a, g_a, f_b, g_b], [], positions,
 
2127
                                  _min_buffer_size=150)
 
2128
        self.assertGroupKeysForIo([([f_a, f_b], set()), ([g_a, g_b], set())],
 
2129
                                  [f_a, g_a, f_b, g_b], [], positions,
 
2130
                                  _min_buffer_size=100)
 
2131
        self.assertGroupKeysForIo([([f_c], set()), ([g_b], set())],
 
2132
                                  [f_c, g_b], [], positions,
 
2133
                                  _min_buffer_size=125)
 
2134
        self.assertGroupKeysForIo([([g_b, f_c], set())],
 
2135
                                  [g_b, f_c], [], positions,
 
2136
                                  _min_buffer_size=125)
 
2137
 
 
2138
    def test__split_by_prefix(self):
 
2139
        self.assertSplitByPrefix({b'f': [(b'f', b'a'), (b'f', b'b')],
 
2140
                                  b'g': [(b'g', b'b'), (b'g', b'a')],
 
2141
                                 }, [b'f', b'g'],
 
2142
                                 [(b'f', b'a'), (b'g', b'b'),
 
2143
                                  (b'g', b'a'), (b'f', b'b')])
 
2144
 
 
2145
        self.assertSplitByPrefix({b'f': [(b'f', b'a'), (b'f', b'b')],
 
2146
                                  b'g': [(b'g', b'b'), (b'g', b'a')],
 
2147
                                 }, [b'f', b'g'],
 
2148
                                 [(b'f', b'a'), (b'f', b'b'),
 
2149
                                  (b'g', b'b'), (b'g', b'a')])
 
2150
 
 
2151
        self.assertSplitByPrefix({b'f': [(b'f', b'a'), (b'f', b'b')],
 
2152
                                  b'g': [(b'g', b'b'), (b'g', b'a')],
 
2153
                                 }, [b'f', b'g'],
 
2154
                                 [(b'f', b'a'), (b'f', b'b'),
 
2155
                                  (b'g', b'b'), (b'g', b'a')])
 
2156
 
 
2157
        self.assertSplitByPrefix({b'f': [(b'f', b'a'), (b'f', b'b')],
 
2158
                                  b'g': [(b'g', b'b'), (b'g', b'a')],
 
2159
                                  b'': [(b'a',), (b'b',)]
 
2160
                                 }, [b'f', b'g', b''],
 
2161
                                 [(b'f', b'a'), (b'g', b'b'),
 
2162
                                  (b'a',), (b'b',),
 
2163
                                  (b'g', b'a'), (b'f', b'b')])
 
2164
 
 
2165
 
 
2166
class TestStacking(KnitTests):
 
2167
 
 
2168
    def get_basis_and_test_knit(self):
 
2169
        basis = self.make_test_knit(name='basis')
 
2170
        basis = RecordingVersionedFilesDecorator(basis)
 
2171
        test = self.make_test_knit(name='test')
 
2172
        test.add_fallback_versioned_files(basis)
 
2173
        return basis, test
 
2174
 
 
2175
    def test_add_fallback_versioned_files(self):
 
2176
        basis = self.make_test_knit(name='basis')
 
2177
        test = self.make_test_knit(name='test')
 
2178
        # It must not error; other tests test that the fallback is referred to
 
2179
        # when accessing data.
 
2180
        test.add_fallback_versioned_files(basis)
 
2181
 
 
2182
    def test_add_lines(self):
 
2183
        # lines added to the test are not added to the basis
 
2184
        basis, test = self.get_basis_and_test_knit()
 
2185
        key = (b'foo',)
 
2186
        key_basis = (b'bar',)
 
2187
        key_cross_border = (b'quux',)
 
2188
        key_delta = (b'zaphod',)
 
2189
        test.add_lines(key, (), [b'foo\n'])
 
2190
        self.assertEqual({}, basis.get_parent_map([key]))
 
2191
        # lines added to the test that reference across the stack do a
 
2192
        # fulltext.
 
2193
        basis.add_lines(key_basis, (), [b'foo\n'])
 
2194
        basis.calls = []
 
2195
        test.add_lines(key_cross_border, (key_basis,), [b'foo\n'])
 
2196
        self.assertEqual('fulltext', test._index.get_method(key_cross_border))
 
2197
        # we don't even need to look at the basis to see that this should be
 
2198
        # stored as a fulltext
 
2199
        self.assertEqual([], basis.calls)
 
2200
        # Subsequent adds do delta.
 
2201
        basis.calls = []
 
2202
        test.add_lines(key_delta, (key_cross_border,), [b'foo\n'])
 
2203
        self.assertEqual('line-delta', test._index.get_method(key_delta))
 
2204
        self.assertEqual([], basis.calls)
 
2205
 
 
2206
    def test_annotate(self):
 
2207
        # annotations from the test knit are answered without asking the basis
 
2208
        basis, test = self.get_basis_and_test_knit()
 
2209
        key = (b'foo',)
 
2210
        key_basis = (b'bar',)
 
2211
        key_missing = (b'missing',)
 
2212
        test.add_lines(key, (), [b'foo\n'])
 
2213
        details = test.annotate(key)
 
2214
        self.assertEqual([(key, b'foo\n')], details)
 
2215
        self.assertEqual([], basis.calls)
 
2216
        # But texts that are not in the test knit are looked for in the basis
 
2217
        # directly.
 
2218
        basis.add_lines(key_basis, (), [b'foo\n', b'bar\n'])
 
2219
        basis.calls = []
 
2220
        details = test.annotate(key_basis)
 
2221
        self.assertEqual([(key_basis, b'foo\n'), (key_basis, b'bar\n')], details)
 
2222
        # Not optimised to date:
 
2223
        # self.assertEqual([("annotate", key_basis)], basis.calls)
 
2224
        self.assertEqual([('get_parent_map', {key_basis}),
 
2225
            ('get_parent_map', {key_basis}),
 
2226
            ('get_record_stream', [key_basis], 'topological', True)],
 
2227
            basis.calls)
 
2228
 
 
2229
    def test_check(self):
 
2230
        # At the moment checking a stacked knit does implicitly check the
 
2231
        # fallback files.
 
2232
        basis, test = self.get_basis_and_test_knit()
 
2233
        test.check()
 
2234
 
 
2235
    def test_get_parent_map(self):
 
2236
        # parents in the test knit are answered without asking the basis
 
2237
        basis, test = self.get_basis_and_test_knit()
 
2238
        key = (b'foo',)
 
2239
        key_basis = (b'bar',)
 
2240
        key_missing = (b'missing',)
 
2241
        test.add_lines(key, (), [])
 
2242
        parent_map = test.get_parent_map([key])
 
2243
        self.assertEqual({key: ()}, parent_map)
 
2244
        self.assertEqual([], basis.calls)
 
2245
        # But parents that are not in the test knit are looked for in the basis
 
2246
        basis.add_lines(key_basis, (), [])
 
2247
        basis.calls = []
 
2248
        parent_map = test.get_parent_map([key, key_basis, key_missing])
 
2249
        self.assertEqual({key: (),
 
2250
            key_basis: ()}, parent_map)
 
2251
        self.assertEqual([("get_parent_map", {key_basis, key_missing})],
 
2252
            basis.calls)
 
2253
 
 
2254
    def test_get_record_stream_unordered_fulltexts(self):
 
2255
        # records from the test knit are answered without asking the basis:
 
2256
        basis, test = self.get_basis_and_test_knit()
 
2257
        key = (b'foo',)
 
2258
        key_basis = (b'bar',)
 
2259
        key_missing = (b'missing',)
 
2260
        test.add_lines(key, (), [b'foo\n'])
 
2261
        records = list(test.get_record_stream([key], 'unordered', True))
 
2262
        self.assertEqual(1, len(records))
 
2263
        self.assertEqual([], basis.calls)
 
2264
        # Missing (from test knit) objects are retrieved from the basis:
 
2265
        basis.add_lines(key_basis, (), [b'foo\n', b'bar\n'])
 
2266
        basis.calls = []
 
2267
        records = list(test.get_record_stream([key_basis, key_missing],
 
2268
            'unordered', True))
 
2269
        self.assertEqual(2, len(records))
 
2270
        calls = list(basis.calls)
 
2271
        for record in records:
 
2272
            self.assertSubset([record.key], (key_basis, key_missing))
 
2273
            if record.key == key_missing:
 
2274
                self.assertIsInstance(record, AbsentContentFactory)
 
2275
            else:
 
2276
                reference = list(basis.get_record_stream([key_basis],
 
2277
                    'unordered', True))[0]
 
2278
                self.assertEqual(reference.key, record.key)
 
2279
                self.assertEqual(reference.sha1, record.sha1)
 
2280
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2281
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2282
                    record.get_bytes_as(record.storage_kind))
 
2283
                self.assertEqual(reference.get_bytes_as('fulltext'),
 
2284
                    record.get_bytes_as('fulltext'))
 
2285
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2286
        # ask which fallbacks have which parents.
 
2287
        self.assertEqual([
 
2288
            ("get_parent_map", {key_basis, key_missing}),
 
2289
            ("get_record_stream", [key_basis], 'unordered', True)],
 
2290
            calls)
 
2291
 
 
2292
    def test_get_record_stream_ordered_fulltexts(self):
 
2293
        # ordering is preserved down into the fallback store.
 
2294
        basis, test = self.get_basis_and_test_knit()
 
2295
        key = (b'foo',)
 
2296
        key_basis = (b'bar',)
 
2297
        key_basis_2 = (b'quux',)
 
2298
        key_missing = (b'missing',)
 
2299
        test.add_lines(key, (key_basis,), [b'foo\n'])
 
2300
        # Missing (from test knit) objects are retrieved from the basis:
 
2301
        basis.add_lines(key_basis, (key_basis_2,), [b'foo\n', b'bar\n'])
 
2302
        basis.add_lines(key_basis_2, (), [b'quux\n'])
 
2303
        basis.calls = []
 
2304
        # ask for in non-topological order
 
2305
        records = list(test.get_record_stream(
 
2306
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
 
2307
        self.assertEqual(4, len(records))
 
2308
        results = []
 
2309
        for record in records:
 
2310
            self.assertSubset([record.key],
 
2311
                (key_basis, key_missing, key_basis_2, key))
 
2312
            if record.key == key_missing:
 
2313
                self.assertIsInstance(record, AbsentContentFactory)
 
2314
            else:
 
2315
                results.append((record.key, record.sha1, record.storage_kind,
 
2316
                    record.get_bytes_as('fulltext')))
 
2317
        calls = list(basis.calls)
 
2318
        order = [record[0] for record in results]
 
2319
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2320
        for result in results:
 
2321
            if result[0] == key:
 
2322
                source = test
 
2323
            else:
 
2324
                source = basis
 
2325
            record = next(source.get_record_stream([result[0]], 'unordered',
 
2326
                True))
 
2327
            self.assertEqual(record.key, result[0])
 
2328
            self.assertEqual(record.sha1, result[1])
 
2329
            # We used to check that the storage kind matched, but actually it
 
2330
            # depends on whether it was sourced from the basis, or in a single
 
2331
            # group, because asking for full texts returns proxy objects to a
 
2332
            # _ContentMapGenerator object; so checking the kind is unneeded.
 
2333
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
 
2334
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2335
        # ask which fallbacks have which parents.
 
2336
        self.assertEqual(2, len(calls))
 
2337
        self.assertEqual(
 
2338
                ("get_parent_map", {key_basis, key_basis_2, key_missing}),
 
2339
                calls[0])
 
2340
        # topological is requested from the fallback, because that is what
 
2341
        # was requested at the top level.
 
2342
        self.assertIn(
 
2343
                calls[1], [
 
2344
                ("get_record_stream", [key_basis_2, key_basis], 'topological', True),
 
2345
                ("get_record_stream", [key_basis, key_basis_2], 'topological', True)])
 
2346
 
 
2347
    def test_get_record_stream_unordered_deltas(self):
 
2348
        # records from the test knit are answered without asking the basis:
 
2349
        basis, test = self.get_basis_and_test_knit()
 
2350
        key = (b'foo',)
 
2351
        key_basis = (b'bar',)
 
2352
        key_missing = (b'missing',)
 
2353
        test.add_lines(key, (), [b'foo\n'])
 
2354
        records = list(test.get_record_stream([key], 'unordered', False))
 
2355
        self.assertEqual(1, len(records))
 
2356
        self.assertEqual([], basis.calls)
 
2357
        # Missing (from test knit) objects are retrieved from the basis:
 
2358
        basis.add_lines(key_basis, (), [b'foo\n', b'bar\n'])
 
2359
        basis.calls = []
 
2360
        records = list(test.get_record_stream([key_basis, key_missing],
 
2361
            'unordered', False))
 
2362
        self.assertEqual(2, len(records))
 
2363
        calls = list(basis.calls)
 
2364
        for record in records:
 
2365
            self.assertSubset([record.key], (key_basis, key_missing))
 
2366
            if record.key == key_missing:
 
2367
                self.assertIsInstance(record, AbsentContentFactory)
 
2368
            else:
 
2369
                reference = list(basis.get_record_stream([key_basis],
 
2370
                    'unordered', False))[0]
 
2371
                self.assertEqual(reference.key, record.key)
 
2372
                self.assertEqual(reference.sha1, record.sha1)
 
2373
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2374
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2375
                    record.get_bytes_as(record.storage_kind))
 
2376
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2377
        # ask which fallbacks have which parents.
 
2378
        self.assertEqual([
 
2379
            ("get_parent_map", {key_basis, key_missing}),
 
2380
            ("get_record_stream", [key_basis], 'unordered', False)],
 
2381
            calls)
 
2382
 
 
2383
    def test_get_record_stream_ordered_deltas(self):
 
2384
        # ordering is preserved down into the fallback store.
 
2385
        basis, test = self.get_basis_and_test_knit()
 
2386
        key = (b'foo',)
 
2387
        key_basis = (b'bar',)
 
2388
        key_basis_2 = (b'quux',)
 
2389
        key_missing = (b'missing',)
 
2390
        test.add_lines(key, (key_basis,), [b'foo\n'])
 
2391
        # Missing (from test knit) objects are retrieved from the basis:
 
2392
        basis.add_lines(key_basis, (key_basis_2,), [b'foo\n', b'bar\n'])
 
2393
        basis.add_lines(key_basis_2, (), [b'quux\n'])
 
2394
        basis.calls = []
 
2395
        # ask for in non-topological order
 
2396
        records = list(test.get_record_stream(
 
2397
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
 
2398
        self.assertEqual(4, len(records))
 
2399
        results = []
 
2400
        for record in records:
 
2401
            self.assertSubset([record.key],
 
2402
                (key_basis, key_missing, key_basis_2, key))
 
2403
            if record.key == key_missing:
 
2404
                self.assertIsInstance(record, AbsentContentFactory)
 
2405
            else:
 
2406
                results.append((record.key, record.sha1, record.storage_kind,
 
2407
                    record.get_bytes_as(record.storage_kind)))
 
2408
        calls = list(basis.calls)
 
2409
        order = [record[0] for record in results]
 
2410
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2411
        for result in results:
 
2412
            if result[0] == key:
 
2413
                source = test
 
2414
            else:
 
2415
                source = basis
 
2416
            record = next(source.get_record_stream([result[0]], 'unordered',
 
2417
                False))
 
2418
            self.assertEqual(record.key, result[0])
 
2419
            self.assertEqual(record.sha1, result[1])
 
2420
            self.assertEqual(record.storage_kind, result[2])
 
2421
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
 
2422
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2423
        # ask which fallbacks have which parents.
 
2424
        self.assertEqual([
 
2425
            ("get_parent_map", {key_basis, key_basis_2, key_missing}),
 
2426
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
 
2427
            calls)
 
2428
 
 
2429
    def test_get_sha1s(self):
 
2430
        # sha1's in the test knit are answered without asking the basis
 
2431
        basis, test = self.get_basis_and_test_knit()
 
2432
        key = (b'foo',)
 
2433
        key_basis = (b'bar',)
 
2434
        key_missing = (b'missing',)
 
2435
        test.add_lines(key, (), [b'foo\n'])
 
2436
        key_sha1sum = osutils.sha_string(b'foo\n')
 
2437
        sha1s = test.get_sha1s([key])
 
2438
        self.assertEqual({key: key_sha1sum}, sha1s)
 
2439
        self.assertEqual([], basis.calls)
 
2440
        # But texts that are not in the test knit are looked for in the basis
 
2441
        # directly (rather than via text reconstruction) so that remote servers
 
2442
        # etc don't have to answer with full content.
 
2443
        basis.add_lines(key_basis, (), [b'foo\n', b'bar\n'])
 
2444
        basis_sha1sum = osutils.sha_string(b'foo\nbar\n')
 
2445
        basis.calls = []
 
2446
        sha1s = test.get_sha1s([key, key_missing, key_basis])
 
2447
        self.assertEqual({key: key_sha1sum,
 
2448
            key_basis: basis_sha1sum}, sha1s)
 
2449
        self.assertEqual([("get_sha1s", {key_basis, key_missing})],
 
2450
            basis.calls)
 
2451
 
 
2452
    def test_insert_record_stream(self):
 
2453
        # records are inserted as normal; insert_record_stream builds on
 
2454
        # add_lines, so a smoke test should be all that's needed:
 
2455
        key = (b'foo',)
 
2456
        key_basis = (b'bar',)
 
2457
        key_delta = (b'zaphod',)
 
2458
        basis, test = self.get_basis_and_test_knit()
 
2459
        source = self.make_test_knit(name='source')
 
2460
        basis.add_lines(key_basis, (), [b'foo\n'])
 
2461
        basis.calls = []
 
2462
        source.add_lines(key_basis, (), [b'foo\n'])
 
2463
        source.add_lines(key_delta, (key_basis,), [b'bar\n'])
 
2464
        stream = source.get_record_stream([key_delta], 'unordered', False)
 
2465
        test.insert_record_stream(stream)
 
2466
        # XXX: this does somewhat too many calls in making sure of whether it
 
2467
        # has to recreate the full text.
 
2468
        self.assertEqual([("get_parent_map", {key_basis}),
 
2469
             ('get_parent_map', {key_basis}),
 
2470
             ('get_record_stream', [key_basis], 'unordered', True)],
 
2471
            basis.calls)
 
2472
        self.assertEqual({key_delta:(key_basis,)},
 
2473
            test.get_parent_map([key_delta]))
 
2474
        self.assertEqual(b'bar\n', next(test.get_record_stream([key_delta],
 
2475
            'unordered', True)).get_bytes_as('fulltext'))
 
2476
 
 
2477
    def test_iter_lines_added_or_present_in_keys(self):
 
2478
        # Lines from the basis are returned, and lines for a given key are only
 
2479
        # returned once.
 
2480
        key1 = (b'foo1',)
 
2481
        key2 = (b'foo2',)
 
2482
        # all sources are asked for keys:
 
2483
        basis, test = self.get_basis_and_test_knit()
 
2484
        basis.add_lines(key1, (), [b"foo"])
 
2485
        basis.calls = []
 
2486
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
 
2487
        self.assertEqual([(b"foo\n", key1)], lines)
 
2488
        self.assertEqual([("iter_lines_added_or_present_in_keys", {key1})],
 
2489
            basis.calls)
 
2490
        # keys in both are not duplicated:
 
2491
        test.add_lines(key2, (), [b"bar\n"])
 
2492
        basis.add_lines(key2, (), [b"bar\n"])
 
2493
        basis.calls = []
 
2494
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
 
2495
        self.assertEqual([(b"bar\n", key2)], lines)
 
2496
        self.assertEqual([], basis.calls)
 
2497
 
 
2498
    def test_keys(self):
 
2499
        key1 = (b'foo1',)
 
2500
        key2 = (b'foo2',)
 
2501
        # all sources are asked for keys:
 
2502
        basis, test = self.get_basis_and_test_knit()
 
2503
        keys = test.keys()
 
2504
        self.assertEqual(set(), set(keys))
 
2505
        self.assertEqual([("keys",)], basis.calls)
 
2506
        # keys from a basis are returned:
 
2507
        basis.add_lines(key1, (), [])
 
2508
        basis.calls = []
 
2509
        keys = test.keys()
 
2510
        self.assertEqual({key1}, set(keys))
 
2511
        self.assertEqual([("keys",)], basis.calls)
 
2512
        # keys in both are not duplicated:
 
2513
        test.add_lines(key2, (), [])
 
2514
        basis.add_lines(key2, (), [])
 
2515
        basis.calls = []
 
2516
        keys = test.keys()
 
2517
        self.assertEqual(2, len(keys))
 
2518
        self.assertEqual({key1, key2}, set(keys))
 
2519
        self.assertEqual([("keys",)], basis.calls)
 
2520
 
 
2521
    def test_add_mpdiffs(self):
 
2522
        # records are inserted as normal; add_mpdiff builds on
 
2523
        # add_lines, so a smoke test should be all that's needed:
 
2524
        key = (b'foo',)
 
2525
        key_basis = (b'bar',)
 
2526
        key_delta = (b'zaphod',)
 
2527
        basis, test = self.get_basis_and_test_knit()
 
2528
        source = self.make_test_knit(name='source')
 
2529
        basis.add_lines(key_basis, (), [b'foo\n'])
 
2530
        basis.calls = []
 
2531
        source.add_lines(key_basis, (), [b'foo\n'])
 
2532
        source.add_lines(key_delta, (key_basis,), [b'bar\n'])
 
2533
        diffs = source.make_mpdiffs([key_delta])
 
2534
        test.add_mpdiffs([(key_delta, (key_basis,),
 
2535
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
 
2536
        self.assertEqual([("get_parent_map", {key_basis}),
 
2537
            ('get_record_stream', [key_basis], 'unordered', True),],
 
2538
            basis.calls)
 
2539
        self.assertEqual({key_delta:(key_basis,)},
 
2540
            test.get_parent_map([key_delta]))
 
2541
        self.assertEqual(b'bar\n', next(test.get_record_stream([key_delta],
 
2542
            'unordered', True)).get_bytes_as('fulltext'))
 
2543
 
 
2544
    def test_make_mpdiffs(self):
 
2545
        # Generating an mpdiff across a stacking boundary should detect parent
 
2546
        # texts regions.
 
2547
        key = (b'foo',)
 
2548
        key_left = (b'bar',)
 
2549
        key_right = (b'zaphod',)
 
2550
        basis, test = self.get_basis_and_test_knit()
 
2551
        basis.add_lines(key_left, (), [b'bar\n'])
 
2552
        basis.add_lines(key_right, (), [b'zaphod\n'])
 
2553
        basis.calls = []
 
2554
        test.add_lines(key, (key_left, key_right),
 
2555
            [b'bar\n', b'foo\n', b'zaphod\n'])
 
2556
        diffs = test.make_mpdiffs([key])
 
2557
        self.assertEqual([
 
2558
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
 
2559
                multiparent.NewText([b'foo\n']),
 
2560
                multiparent.ParentText(1, 0, 2, 1)])],
 
2561
            diffs)
 
2562
        self.assertEqual(3, len(basis.calls))
 
2563
        self.assertEqual([
 
2564
            ("get_parent_map", {key_left, key_right}),
 
2565
            ("get_parent_map", {key_left, key_right}),
 
2566
            ],
 
2567
            basis.calls[:-1])
 
2568
        last_call = basis.calls[-1]
 
2569
        self.assertEqual('get_record_stream', last_call[0])
 
2570
        self.assertEqual({key_left, key_right}, set(last_call[1]))
 
2571
        self.assertEqual('topological', last_call[2])
 
2572
        self.assertEqual(True, last_call[3])
 
2573
 
 
2574
 
 
2575
class TestNetworkBehaviour(KnitTests):
 
2576
    """Tests for getting data out of/into knits over the network."""
 
2577
 
 
2578
    def test_include_delta_closure_generates_a_knit_delta_closure(self):
 
2579
        vf = self.make_test_knit(name='test')
 
2580
        # put in three texts, giving ft, delta, delta
 
2581
        vf.add_lines((b'base',), (), [b'base\n', b'content\n'])
 
2582
        vf.add_lines((b'd1',), ((b'base',),), [b'd1\n'])
 
2583
        vf.add_lines((b'd2',), ((b'd1',),), [b'd2\n'])
 
2584
        # But heuristics could interfere, so check what happened:
 
2585
        self.assertEqual(['knit-ft-gz', 'knit-delta-gz', 'knit-delta-gz'],
 
2586
            [record.storage_kind for record in
 
2587
             vf.get_record_stream([(b'base',), (b'd1',), (b'd2',)],
 
2588
                'topological', False)])
 
2589
        # generate a stream of just the deltas include_delta_closure=True,
 
2590
        # serialise to the network, and check that we get a delta closure on the wire.
 
2591
        stream = vf.get_record_stream([(b'd1',), (b'd2',)], 'topological', True)
 
2592
        netb = [record.get_bytes_as(record.storage_kind) for record in stream]
 
2593
        # The first bytes should be a memo from _ContentMapGenerator, and the
 
2594
        # second bytes should be empty (because its a API proxy not something
 
2595
        # for wire serialisation.
 
2596
        self.assertEqual(b'', netb[1])
 
2597
        bytes = netb[0]
 
2598
        kind, line_end = network_bytes_to_kind_and_offset(bytes)
 
2599
        self.assertEqual('knit-delta-closure', kind)
 
2600
 
 
2601
 
 
2602
class TestContentMapGenerator(KnitTests):
 
2603
    """Tests for ContentMapGenerator"""
 
2604
 
 
2605
    def test_get_record_stream_gives_records(self):
 
2606
        vf = self.make_test_knit(name='test')
 
2607
        # put in three texts, giving ft, delta, delta
 
2608
        vf.add_lines((b'base',), (), [b'base\n', b'content\n'])
 
2609
        vf.add_lines((b'd1',), ((b'base',),), [b'd1\n'])
 
2610
        vf.add_lines((b'd2',), ((b'd1',),), [b'd2\n'])
 
2611
        keys = [(b'd1',), (b'd2',)]
 
2612
        generator = _VFContentMapGenerator(vf, keys,
 
2613
            global_map=vf.get_parent_map(keys))
 
2614
        for record in generator.get_record_stream():
 
2615
            if record.key == (b'd1',):
 
2616
                self.assertEqual(b'd1\n', record.get_bytes_as('fulltext'))
 
2617
            else:
 
2618
                self.assertEqual(b'd2\n', record.get_bytes_as('fulltext'))
 
2619
 
 
2620
    def test_get_record_stream_kinds_are_raw(self):
 
2621
        vf = self.make_test_knit(name='test')
 
2622
        # put in three texts, giving ft, delta, delta
 
2623
        vf.add_lines((b'base',), (), [b'base\n', b'content\n'])
 
2624
        vf.add_lines((b'd1',), ((b'base',),), [b'd1\n'])
 
2625
        vf.add_lines((b'd2',), ((b'd1',),), [b'd2\n'])
 
2626
        keys = [(b'base',), (b'd1',), (b'd2',)]
 
2627
        generator = _VFContentMapGenerator(vf, keys,
 
2628
            global_map=vf.get_parent_map(keys))
 
2629
        kinds = {(b'base',): 'knit-delta-closure',
 
2630
            (b'd1',): 'knit-delta-closure-ref',
 
2631
            (b'd2',): 'knit-delta-closure-ref',
 
2632
            }
 
2633
        for record in generator.get_record_stream():
 
2634
            self.assertEqual(kinds[record.key], record.storage_kind)