~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-23 23:54:45 UTC
  • Revision ID: mbp@sourcefrog.net-20050323235445-a185b1b1f2a86dfe
mention "info" in top-level help

Show diffs side-by-side

added added

removed removed

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