/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-06-10 16:40:42 UTC
  • mfrom: (6653.6.7 rename-controldir)
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170610164042-zrxqgy2htyduvke2
MergeĀ rename-controldirĀ branch.

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