/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: Jelmer Vernooij
  • Date: 2017-08-07 11:49:46 UTC
  • mto: (6747.3.4 avoid-set-revid-3)
  • mto: This revision was merged to the branch mainline in revision 6750.
  • Revision ID: jelmer@jelmer.uk-20170807114946-luclmxuawyzhpiot
Avoid setting revision_ids.

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