~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Sidnei da Silva
  • Date: 2009-07-03 15:06:42 UTC
  • mto: (4531.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 4532.
  • Revision ID: sidnei.da.silva@canonical.com-20090703150642-hjfra5waj5879cae
- Add top-level make target to build all installers using buildout and another to cleanup

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""Tests for Knit data structure"""
18
18
 
19
19
from cStringIO import StringIO
20
20
import difflib
21
21
import gzip
22
 
import sha
 
22
import sys
23
23
 
24
24
from bzrlib import (
25
25
    errors,
 
26
    generate_ids,
 
27
    knit,
 
28
    multiparent,
 
29
    osutils,
 
30
    pack,
26
31
    )
27
32
from bzrlib.errors import (
28
33
    RevisionAlreadyPresent,
30
35
    RevisionNotPresent,
31
36
    NoSuchFile,
32
37
    )
 
38
from bzrlib.index import *
33
39
from bzrlib.knit import (
 
40
    AnnotatedKnitContent,
34
41
    KnitContent,
35
 
    KnitVersionedFile,
36
 
    KnitPlainFactory,
37
 
    KnitAnnotateFactory,
38
 
    _KnitData,
39
 
    _KnitIndex,
40
 
    WeaveToKnit,
41
 
    )
42
 
from bzrlib.osutils import split_lines
43
 
from bzrlib.tests import TestCase, TestCaseWithTransport
44
 
from bzrlib.transport import TransportLogger, get_transport
 
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
45
62
from bzrlib.transport.memory import MemoryTransport
46
 
from bzrlib.weave import Weave
47
 
 
48
 
 
49
 
class KnitContentTests(TestCase):
 
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):
50
88
 
51
89
    def test_constructor(self):
52
 
        content = KnitContent([])
 
90
        content = self._make_content([])
53
91
 
54
92
    def test_text(self):
55
 
        content = KnitContent([])
 
93
        content = self._make_content([])
56
94
        self.assertEqual(content.text(), [])
57
95
 
58
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
 
96
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
59
97
        self.assertEqual(content.text(), ["text1", "text2"])
60
98
 
61
 
    def test_annotate(self):
62
 
        content = KnitContent([])
63
 
        self.assertEqual(content.annotate(), [])
64
 
 
65
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
66
 
        self.assertEqual(content.annotate(),
67
 
            [("origin1", "text1"), ("origin2", "text2")])
68
 
 
69
 
    def test_annotate_iter(self):
70
 
        content = KnitContent([])
71
 
        it = content.annotate_iter()
72
 
        self.assertRaises(StopIteration, it.next)
73
 
 
74
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
75
 
        it = content.annotate_iter()
76
 
        self.assertEqual(it.next(), ("origin1", "text1"))
77
 
        self.assertEqual(it.next(), ("origin2", "text2"))
78
 
        self.assertRaises(StopIteration, it.next)
79
 
 
80
99
    def test_copy(self):
81
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
 
100
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
82
101
        copy = content.copy()
83
 
        self.assertIsInstance(copy, KnitContent)
84
 
        self.assertEqual(copy.annotate(),
 
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(),
85
234
            [("origin1", "text1"), ("origin2", "text2")])
86
235
 
87
236
    def test_line_delta(self):
88
 
        content1 = KnitContent([("", "a"), ("", "b")])
89
 
        content2 = KnitContent([("", "a"), ("", "a"), ("", "c")])
 
237
        content1 = self._make_content([("", "a"), ("", "b")])
 
238
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
90
239
        self.assertEqual(content1.line_delta(content2),
91
240
            [(1, 2, 2, [("", "a"), ("", "c")])])
92
241
 
93
242
    def test_line_delta_iter(self):
94
 
        content1 = KnitContent([("", "a"), ("", "b")])
95
 
        content2 = KnitContent([("", "a"), ("", "a"), ("", "c")])
 
243
        content1 = self._make_content([("", "a"), ("", "b")])
 
244
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
96
245
        it = content1.line_delta_iter(content2)
97
246
        self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
98
247
        self.assertRaises(StopIteration, it.next)
124
273
        return queue_call
125
274
 
126
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
 
127
700
class LowLevelKnitDataTests(TestCase):
128
701
 
129
702
    def create_gz_content(self, text):
133
706
        gz_file.close()
134
707
        return sio.getvalue()
135
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
 
136
729
    def test_valid_knit_data(self):
137
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
730
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
138
731
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
139
732
                                        'foo\n'
140
733
                                        'bar\n'
141
734
                                        'end rev-id-1\n'
142
735
                                        % (sha1sum,))
143
736
        transport = MockTransport([gz_txt])
144
 
        data = _KnitData(transport, 'filename', mode='r')
145
 
        records = [('rev-id-1', 0, len(gz_txt))]
146
 
 
147
 
        contents = data.read_records(records)
148
 
        self.assertEqual({'rev-id-1':(['foo\n', 'bar\n'], sha1sum)}, contents)
149
 
 
150
 
        raw_contents = list(data.read_records_iter_raw(records))
151
 
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
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)
152
765
 
153
766
    def test_not_enough_lines(self):
154
 
        sha1sum = sha.new('foo\n').hexdigest()
 
767
        sha1sum = osutils.sha('foo\n').hexdigest()
155
768
        # record says 2 lines data says 1
156
769
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
157
770
                                        'foo\n'
158
771
                                        'end rev-id-1\n'
159
772
                                        % (sha1sum,))
160
773
        transport = MockTransport([gz_txt])
161
 
        data = _KnitData(transport, 'filename', mode='r')
162
 
        records = [('rev-id-1', 0, len(gz_txt))]
163
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
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))
164
779
 
165
780
        # read_records_iter_raw won't detect that sort of mismatch/corruption
166
 
        raw_contents = list(data.read_records_iter_raw(records))
167
 
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
781
        raw_contents = list(knit._read_records_iter_raw(records))
 
782
        self.assertEqual([(('rev-id-1',),  gz_txt, sha1sum)], raw_contents)
168
783
 
169
784
    def test_too_many_lines(self):
170
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
785
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
171
786
        # record says 1 lines data says 2
172
787
        gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
173
788
                                        'foo\n'
175
790
                                        'end rev-id-1\n'
176
791
                                        % (sha1sum,))
177
792
        transport = MockTransport([gz_txt])
178
 
        data = _KnitData(transport, 'filename', mode='r')
179
 
        records = [('rev-id-1', 0, len(gz_txt))]
180
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
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))
181
798
 
182
799
        # read_records_iter_raw won't detect that sort of mismatch/corruption
183
 
        raw_contents = list(data.read_records_iter_raw(records))
184
 
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
800
        raw_contents = list(knit._read_records_iter_raw(records))
 
801
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
185
802
 
186
803
    def test_mismatched_version_id(self):
187
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
804
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
188
805
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
189
806
                                        'foo\n'
190
807
                                        'bar\n'
191
808
                                        'end rev-id-1\n'
192
809
                                        % (sha1sum,))
193
810
        transport = MockTransport([gz_txt])
194
 
        data = _KnitData(transport, 'filename', mode='r')
 
811
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
812
        knit = KnitVersionedFiles(None, access)
195
813
        # We are asking for rev-id-2, but the data is rev-id-1
196
 
        records = [('rev-id-2', 0, len(gz_txt))]
197
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
814
        records = [(('rev-id-2',), (('rev-id-2',), 0, len(gz_txt)))]
 
815
        self.assertRaises(errors.KnitCorrupt, list,
 
816
            knit._read_records_iter(records))
198
817
 
199
 
        # read_records_iter_raw will notice if we request the wrong version.
 
818
        # read_records_iter_raw detects mismatches in the header
200
819
        self.assertRaises(errors.KnitCorrupt, list,
201
 
                          data.read_records_iter_raw(records))
 
820
            knit._read_records_iter_raw(records))
202
821
 
203
822
    def test_uncompressed_data(self):
204
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
823
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
205
824
        txt = ('version rev-id-1 2 %s\n'
206
825
               'foo\n'
207
826
               'bar\n'
208
827
               'end rev-id-1\n'
209
828
               % (sha1sum,))
210
829
        transport = MockTransport([txt])
211
 
        data = _KnitData(transport, 'filename', mode='r')
212
 
        records = [('rev-id-1', 0, len(txt))]
 
830
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
831
        knit = KnitVersionedFiles(None, access)
 
832
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(txt)))]
213
833
 
214
834
        # We don't have valid gzip data ==> corrupt
215
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
835
        self.assertRaises(errors.KnitCorrupt, list,
 
836
            knit._read_records_iter(records))
216
837
 
217
838
        # read_records_iter_raw will notice the bad data
218
839
        self.assertRaises(errors.KnitCorrupt, list,
219
 
                          data.read_records_iter_raw(records))
 
840
            knit._read_records_iter_raw(records))
220
841
 
221
842
    def test_corrupted_data(self):
222
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
843
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
223
844
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
224
845
                                        'foo\n'
225
846
                                        'bar\n'
228
849
        # Change 2 bytes in the middle to \xff
229
850
        gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
230
851
        transport = MockTransport([gz_txt])
231
 
        data = _KnitData(transport, 'filename', mode='r')
232
 
        records = [('rev-id-1', 0, len(gz_txt))]
233
 
 
234
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
235
 
 
236
 
        # read_records_iter_raw will notice if we request the wrong version.
237
 
        self.assertRaises(errors.KnitCorrupt, list,
238
 
                          data.read_records_iter_raw(records))
 
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))
239
860
 
240
861
 
241
862
class LowLevelKnitIndexTests(TestCase):
242
863
 
243
 
    def test_no_such_file(self):
244
 
        transport = MockTransport()
245
 
 
246
 
        self.assertRaises(NoSuchFile, _KnitIndex, transport, "filename", "r")
247
 
        self.assertRaises(NoSuchFile, _KnitIndex, transport,
248
 
            "filename", "w", create=False)
 
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)
249
874
 
250
875
    def test_create_file(self):
251
876
        transport = MockTransport()
252
 
 
253
 
        index = _KnitIndex(transport, "filename", "w",
254
 
            file_mode="wb", create=True)
255
 
        self.assertEqual(
256
 
                ("put_bytes_non_atomic",
257
 
                    ("filename", index.HEADER), {"mode": "wb"}),
258
 
                transport.calls.pop(0))
259
 
 
260
 
    def test_delay_create_file(self):
261
 
        transport = MockTransport()
262
 
 
263
 
        index = _KnitIndex(transport, "filename", "w",
264
 
            create=True, file_mode="wb", create_parent_dir=True,
265
 
            delay_create=True, dir_mode=0777)
266
 
        self.assertEqual([], transport.calls)
267
 
 
268
 
        index.add_versions([])
269
 
        name, (filename, f), kwargs = transport.calls.pop(0)
270
 
        self.assertEqual("put_file_non_atomic", name)
271
 
        self.assertEqual(
272
 
            {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
273
 
            kwargs)
274
 
        self.assertEqual("filename", filename)
275
 
        self.assertEqual(index.HEADER, f.read())
276
 
 
277
 
        index.add_versions([])
278
 
        self.assertEqual(("append_bytes", ("filename", ""), {}),
279
 
            transport.calls.pop(0))
 
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])
280
887
 
281
888
    def test_read_utf8_version_id(self):
282
889
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
283
890
        utf8_revision_id = unicode_revision_id.encode('utf-8')
284
891
        transport = MockTransport([
285
 
            _KnitIndex.HEADER,
 
892
            _KndxIndex.HEADER,
286
893
            '%s option 0 1 :' % (utf8_revision_id,)
287
894
            ])
288
 
        index = _KnitIndex(transport, "filename", "r")
289
 
        # _KnitIndex is a private class, and deals in utf8 revision_ids, not
 
895
        index = self.get_knit_index(transport, "filename", "r")
 
896
        # _KndxIndex is a private class, and deals in utf8 revision_ids, not
290
897
        # Unicode revision_ids.
291
 
        self.assertTrue(index.has_version(utf8_revision_id))
292
 
        self.assertFalse(index.has_version(unicode_revision_id))
 
898
        self.assertEqual({(utf8_revision_id,):()},
 
899
            index.get_parent_map(index.keys()))
 
900
        self.assertFalse((unicode_revision_id,) in index.keys())
293
901
 
294
902
    def test_read_utf8_parents(self):
295
903
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
296
904
        utf8_revision_id = unicode_revision_id.encode('utf-8')
297
905
        transport = MockTransport([
298
 
            _KnitIndex.HEADER,
 
906
            _KndxIndex.HEADER,
299
907
            "version option 0 1 .%s :" % (utf8_revision_id,)
300
908
            ])
301
 
        index = _KnitIndex(transport, "filename", "r")
302
 
        self.assertEqual([utf8_revision_id],
303
 
            index.get_parents_with_ghosts("version"))
 
909
        index = self.get_knit_index(transport, "filename", "r")
 
910
        self.assertEqual({("version",):((utf8_revision_id,),)},
 
911
            index.get_parent_map(index.keys()))
304
912
 
305
913
    def test_read_ignore_corrupted_lines(self):
306
914
        transport = MockTransport([
307
 
            _KnitIndex.HEADER,
 
915
            _KndxIndex.HEADER,
308
916
            "corrupted",
309
917
            "corrupted options 0 1 .b .c ",
310
918
            "version options 0 1 :"
311
919
            ])
312
 
        index = _KnitIndex(transport, "filename", "r")
313
 
        self.assertEqual(1, index.num_versions())
314
 
        self.assertTrue(index.has_version("version"))
 
920
        index = self.get_knit_index(transport, "filename", "r")
 
921
        self.assertEqual(1, len(index.keys()))
 
922
        self.assertEqual(set([("version",)]), index.keys())
315
923
 
316
924
    def test_read_corrupted_header(self):
317
925
        transport = MockTransport(['not a bzr knit index header\n'])
318
 
        self.assertRaises(KnitHeaderError,
319
 
            _KnitIndex, transport, "filename", "r")
 
926
        index = self.get_knit_index(transport, "filename", "r")
 
927
        self.assertRaises(KnitHeaderError, index.keys)
320
928
 
321
929
    def test_read_duplicate_entries(self):
322
930
        transport = MockTransport([
323
 
            _KnitIndex.HEADER,
 
931
            _KndxIndex.HEADER,
324
932
            "parent options 0 1 :",
325
933
            "version options1 0 1 0 :",
326
934
            "version options2 1 2 .other :",
327
935
            "version options3 3 4 0 .other :"
328
936
            ])
329
 
        index = _KnitIndex(transport, "filename", "r")
330
 
        self.assertEqual(2, index.num_versions())
331
 
        self.assertEqual(1, index.lookup("version"))
332
 
        self.assertEqual((3, 4), index.get_position("version"))
333
 
        self.assertEqual(["options3"], index.get_options("version"))
334
 
        self.assertEqual(["parent", "other"],
335
 
            index.get_parents_with_ghosts("version"))
 
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",)]))
336
946
 
337
947
    def test_read_compressed_parents(self):
338
948
        transport = MockTransport([
339
 
            _KnitIndex.HEADER,
 
949
            _KndxIndex.HEADER,
340
950
            "a option 0 1 :",
341
951
            "b option 0 1 0 :",
342
952
            "c option 0 1 1 0 :",
343
953
            ])
344
 
        index = _KnitIndex(transport, "filename", "r")
345
 
        self.assertEqual(["a"], index.get_parents("b"))
346
 
        self.assertEqual(["b", "a"], index.get_parents("c"))
 
954
        index = self.get_knit_index(transport, "filename", "r")
 
955
        self.assertEqual({("b",):(("a",),), ("c",):(("b",), ("a",))},
 
956
            index.get_parent_map([("b",), ("c",)]))
347
957
 
348
958
    def test_write_utf8_version_id(self):
349
959
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
350
960
        utf8_revision_id = unicode_revision_id.encode('utf-8')
351
961
        transport = MockTransport([
352
 
            _KnitIndex.HEADER
 
962
            _KndxIndex.HEADER
353
963
            ])
354
 
        index = _KnitIndex(transport, "filename", "r")
355
 
        index.add_version(utf8_revision_id, ["option"], 0, 1, [])
356
 
        self.assertEqual(("append_bytes", ("filename",
357
 
            "\n%s option 0 1  :" % (utf8_revision_id,)),
358
 
            {}),
359
 
            transport.calls.pop(0))
 
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])
360
976
 
361
977
    def test_write_utf8_parents(self):
362
978
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
363
979
        utf8_revision_id = unicode_revision_id.encode('utf-8')
364
980
        transport = MockTransport([
365
 
            _KnitIndex.HEADER
366
 
            ])
367
 
        index = _KnitIndex(transport, "filename", "r")
368
 
        index.add_version("version", ["option"], 0, 1, [utf8_revision_id])
369
 
        self.assertEqual(("append_bytes", ("filename",
370
 
            "\nversion option 0 1 .%s :" % (utf8_revision_id,)),
371
 
            {}),
372
 
            transport.calls.pop(0))
373
 
 
374
 
    def test_get_graph(self):
375
 
        transport = MockTransport()
376
 
        index = _KnitIndex(transport, "filename", "w", create=True)
377
 
        self.assertEqual([], index.get_graph())
378
 
 
379
 
        index.add_version("a", ["option"], 0, 1, ["b"])
380
 
        self.assertEqual([("a", ["b"])], index.get_graph())
381
 
 
382
 
        index.add_version("c", ["option"], 0, 1, ["d"])
383
 
        self.assertEqual([("a", ["b"]), ("c", ["d"])],
384
 
            sorted(index.get_graph()))
385
 
 
386
 
    def test_get_ancestry(self):
387
 
        transport = MockTransport([
388
 
            _KnitIndex.HEADER,
389
 
            "a option 0 1 :",
390
 
            "b option 0 1 0 .e :",
391
 
            "c option 0 1 1 0 :",
392
 
            "d option 0 1 2 .f :"
393
 
            ])
394
 
        index = _KnitIndex(transport, "filename", "r")
395
 
 
396
 
        self.assertEqual([], index.get_ancestry([]))
397
 
        self.assertEqual(["a"], index.get_ancestry(["a"]))
398
 
        self.assertEqual(["a", "b"], index.get_ancestry(["b"]))
399
 
        self.assertEqual(["a", "b", "c"], index.get_ancestry(["c"]))
400
 
        self.assertEqual(["a", "b", "c", "d"], index.get_ancestry(["d"]))
401
 
        self.assertEqual(["a", "b"], index.get_ancestry(["a", "b"]))
402
 
        self.assertEqual(["a", "b", "c"], index.get_ancestry(["a", "c"]))
403
 
 
404
 
        self.assertRaises(RevisionNotPresent, index.get_ancestry, ["e"])
405
 
 
406
 
    def test_get_ancestry_with_ghosts(self):
407
 
        transport = MockTransport([
408
 
            _KnitIndex.HEADER,
409
 
            "a option 0 1 :",
410
 
            "b option 0 1 0 .e :",
411
 
            "c option 0 1 0 .f .g :",
412
 
            "d option 0 1 2 .h .j .k :"
413
 
            ])
414
 
        index = _KnitIndex(transport, "filename", "r")
415
 
 
416
 
        self.assertEqual([], index.get_ancestry_with_ghosts([]))
417
 
        self.assertEqual(["a"], index.get_ancestry_with_ghosts(["a"]))
418
 
        self.assertEqual(["a", "e", "b"],
419
 
            index.get_ancestry_with_ghosts(["b"]))
420
 
        self.assertEqual(["a", "g", "f", "c"],
421
 
            index.get_ancestry_with_ghosts(["c"]))
422
 
        self.assertEqual(["a", "g", "f", "c", "k", "j", "h", "d"],
423
 
            index.get_ancestry_with_ghosts(["d"]))
424
 
        self.assertEqual(["a", "e", "b"],
425
 
            index.get_ancestry_with_ghosts(["a", "b"]))
426
 
        self.assertEqual(["a", "g", "f", "c"],
427
 
            index.get_ancestry_with_ghosts(["a", "c"]))
 
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:
428
1045
        self.assertEqual(
429
 
            ["a", "g", "f", "c", "e", "b", "k", "j", "h", "d"],
430
 
            index.get_ancestry_with_ghosts(["b", "d"]))
431
 
 
432
 
        self.assertRaises(RevisionNotPresent,
433
 
            index.get_ancestry_with_ghosts, ["e"])
434
 
 
435
 
    def test_num_versions(self):
436
 
        transport = MockTransport([
437
 
            _KnitIndex.HEADER
438
 
            ])
439
 
        index = _KnitIndex(transport, "filename", "r")
440
 
 
441
 
        self.assertEqual(0, index.num_versions())
442
 
        self.assertEqual(0, len(index))
443
 
 
444
 
        index.add_version("a", ["option"], 0, 1, [])
445
 
        self.assertEqual(1, index.num_versions())
446
 
        self.assertEqual(1, len(index))
447
 
 
448
 
        index.add_version("a", ["option2"], 1, 2, [])
449
 
        self.assertEqual(1, index.num_versions())
450
 
        self.assertEqual(1, len(index))
451
 
 
452
 
        index.add_version("b", ["option"], 0, 1, [])
453
 
        self.assertEqual(2, index.num_versions())
454
 
        self.assertEqual(2, len(index))
455
 
 
456
 
    def test_get_versions(self):
457
 
        transport = MockTransport([
458
 
            _KnitIndex.HEADER
459
 
            ])
460
 
        index = _KnitIndex(transport, "filename", "r")
461
 
 
462
 
        self.assertEqual([], index.get_versions())
463
 
 
464
 
        index.add_version("a", ["option"], 0, 1, [])
465
 
        self.assertEqual(["a"], index.get_versions())
466
 
 
467
 
        index.add_version("a", ["option"], 0, 1, [])
468
 
        self.assertEqual(["a"], index.get_versions())
469
 
 
470
 
        index.add_version("b", ["option"], 0, 1, [])
471
 
        self.assertEqual(["a", "b"], index.get_versions())
472
 
 
473
 
    def test_idx_to_name(self):
474
 
        transport = MockTransport([
475
 
            _KnitIndex.HEADER,
476
 
            "a option 0 1 :",
477
 
            "b option 0 1 :"
478
 
            ])
479
 
        index = _KnitIndex(transport, "filename", "r")
480
 
 
481
 
        self.assertEqual("a", index.idx_to_name(0))
482
 
        self.assertEqual("b", index.idx_to_name(1))
483
 
        self.assertEqual("b", index.idx_to_name(-1))
484
 
        self.assertEqual("a", index.idx_to_name(-2))
485
 
 
486
 
    def test_lookup(self):
487
 
        transport = MockTransport([
488
 
            _KnitIndex.HEADER,
489
 
            "a option 0 1 :",
490
 
            "b option 0 1 :"
491
 
            ])
492
 
        index = _KnitIndex(transport, "filename", "r")
493
 
 
494
 
        self.assertEqual(0, index.lookup("a"))
495
 
        self.assertEqual(1, index.lookup("b"))
496
 
 
497
 
    def test_add_version(self):
498
 
        transport = MockTransport([
499
 
            _KnitIndex.HEADER
500
 
            ])
501
 
        index = _KnitIndex(transport, "filename", "r")
502
 
 
503
 
        index.add_version("a", ["option"], 0, 1, ["b"])
504
 
        self.assertEqual(("append_bytes",
505
 
            ("filename", "\na option 0 1 .b :"),
506
 
            {}), transport.calls.pop(0))
507
 
        self.assertTrue(index.has_version("a"))
508
 
        self.assertEqual(1, index.num_versions())
509
 
        self.assertEqual((0, 1), index.get_position("a"))
510
 
        self.assertEqual(["option"], index.get_options("a"))
511
 
        self.assertEqual(["b"], index.get_parents_with_ghosts("a"))
512
 
 
513
 
        index.add_version("a", ["opt"], 1, 2, ["c"])
514
 
        self.assertEqual(("append_bytes",
515
 
            ("filename", "\na opt 1 2 .c :"),
516
 
            {}), transport.calls.pop(0))
517
 
        self.assertTrue(index.has_version("a"))
518
 
        self.assertEqual(1, index.num_versions())
519
 
        self.assertEqual((1, 2), index.get_position("a"))
520
 
        self.assertEqual(["opt"], index.get_options("a"))
521
 
        self.assertEqual(["c"], index.get_parents_with_ghosts("a"))
522
 
 
523
 
        index.add_version("b", ["option"], 2, 3, ["a"])
524
 
        self.assertEqual(("append_bytes",
525
 
            ("filename", "\nb option 2 3 0 :"),
526
 
            {}), transport.calls.pop(0))
527
 
        self.assertTrue(index.has_version("b"))
528
 
        self.assertEqual(2, index.num_versions())
529
 
        self.assertEqual((2, 3), index.get_position("b"))
530
 
        self.assertEqual(["option"], index.get_options("b"))
531
 
        self.assertEqual(["a"], index.get_parents_with_ghosts("b"))
532
 
 
533
 
    def test_add_versions(self):
534
 
        transport = MockTransport([
535
 
            _KnitIndex.HEADER
536
 
            ])
537
 
        index = _KnitIndex(transport, "filename", "r")
538
 
 
539
 
        index.add_versions([
540
 
            ("a", ["option"], 0, 1, ["b"]),
541
 
            ("a", ["opt"], 1, 2, ["c"]),
542
 
            ("b", ["option"], 2, 3, ["a"])
543
 
            ])
544
 
        self.assertEqual(("append_bytes", ("filename",
 
1046
            _KndxIndex.HEADER +
545
1047
            "\na option 0 1 .b :"
546
1048
            "\na opt 1 2 .c :"
547
 
            "\nb option 2 3 0 :"
548
 
            ), {}), transport.calls.pop(0))
549
 
        self.assertTrue(index.has_version("a"))
550
 
        self.assertTrue(index.has_version("b"))
551
 
        self.assertEqual(2, index.num_versions())
552
 
        self.assertEqual((1, 2), index.get_position("a"))
553
 
        self.assertEqual((2, 3), index.get_position("b"))
554
 
        self.assertEqual(["opt"], index.get_options("a"))
555
 
        self.assertEqual(["option"], index.get_options("b"))
556
 
        self.assertEqual(["c"], index.get_parents_with_ghosts("a"))
557
 
        self.assertEqual(["a"], index.get_parents_with_ghosts("b"))
 
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)
558
1060
 
559
1061
    def test_delay_create_and_add_versions(self):
560
1062
        transport = MockTransport()
561
1063
 
562
 
        index = _KnitIndex(transport, "filename", "w",
563
 
            create=True, file_mode="wb", create_parent_dir=True,
564
 
            delay_create=True, dir_mode=0777)
 
1064
        index = self.get_knit_index(transport, "filename", "w")
 
1065
        # dir_mode=0777)
565
1066
        self.assertEqual([], transport.calls)
566
 
 
567
 
        index.add_versions([
568
 
            ("a", ["option"], 0, 1, ["b"]),
569
 
            ("a", ["opt"], 1, 2, ["c"]),
570
 
            ("b", ["option"], 2, 3, ["a"])
571
 
            ])
572
 
        name, (filename, f), kwargs = transport.calls.pop(0)
573
 
        self.assertEqual("put_file_non_atomic", name)
574
 
        self.assertEqual(
575
 
            {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
576
 
            kwargs)
577
 
        self.assertEqual("filename", filename)
578
 
        self.assertEqual(
579
 
            index.HEADER +
 
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 +
580
1087
            "\na option 0 1 .b :"
581
1088
            "\na opt 1 2 .c :"
582
1089
            "\nb option 2 3 0 :",
583
 
            f.read())
584
 
 
585
 
    def test_has_version(self):
586
 
        transport = MockTransport([
587
 
            _KnitIndex.HEADER,
588
 
            "a option 0 1 :"
589
 
            ])
590
 
        index = _KnitIndex(transport, "filename", "r")
591
 
 
592
 
        self.assertTrue(index.has_version("a"))
593
 
        self.assertFalse(index.has_version("b"))
 
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)
594
1112
 
595
1113
    def test_get_position(self):
596
1114
        transport = MockTransport([
597
 
            _KnitIndex.HEADER,
 
1115
            _KndxIndex.HEADER,
598
1116
            "a option 0 1 :",
599
1117
            "b option 1 2 :"
600
1118
            ])
601
 
        index = _KnitIndex(transport, "filename", "r")
 
1119
        index = self.get_knit_index(transport, "filename", "r")
602
1120
 
603
 
        self.assertEqual((0, 1), index.get_position("a"))
604
 
        self.assertEqual((1, 2), index.get_position("b"))
 
1121
        self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
 
1122
        self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
605
1123
 
606
1124
    def test_get_method(self):
607
1125
        transport = MockTransport([
608
 
            _KnitIndex.HEADER,
 
1126
            _KndxIndex.HEADER,
609
1127
            "a fulltext,unknown 0 1 :",
610
1128
            "b unknown,line-delta 1 2 :",
611
1129
            "c bad 3 4 :"
612
1130
            ])
613
 
        index = _KnitIndex(transport, "filename", "r")
 
1131
        index = self.get_knit_index(transport, "filename", "r")
614
1132
 
615
1133
        self.assertEqual("fulltext", index.get_method("a"))
616
1134
        self.assertEqual("line-delta", index.get_method("b"))
618
1136
 
619
1137
    def test_get_options(self):
620
1138
        transport = MockTransport([
621
 
            _KnitIndex.HEADER,
 
1139
            _KndxIndex.HEADER,
622
1140
            "a opt1 0 1 :",
623
1141
            "b opt2,opt3 1 2 :"
624
1142
            ])
625
 
        index = _KnitIndex(transport, "filename", "r")
 
1143
        index = self.get_knit_index(transport, "filename", "r")
626
1144
 
627
1145
        self.assertEqual(["opt1"], index.get_options("a"))
628
1146
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
629
1147
 
630
 
    def test_get_parents(self):
631
 
        transport = MockTransport([
632
 
            _KnitIndex.HEADER,
633
 
            "a option 0 1 :",
634
 
            "b option 1 2 0 .c :",
635
 
            "c option 1 2 1 0 .e :"
636
 
            ])
637
 
        index = _KnitIndex(transport, "filename", "r")
638
 
 
639
 
        self.assertEqual([], index.get_parents("a"))
640
 
        self.assertEqual(["a", "c"], index.get_parents("b"))
641
 
        self.assertEqual(["b", "a"], index.get_parents("c"))
642
 
 
643
 
    def test_get_parents_with_ghosts(self):
644
 
        transport = MockTransport([
645
 
            _KnitIndex.HEADER,
646
 
            "a option 0 1 :",
647
 
            "b option 1 2 0 .c :",
648
 
            "c option 1 2 1 0 .e :"
649
 
            ])
650
 
        index = _KnitIndex(transport, "filename", "r")
651
 
 
652
 
        self.assertEqual([], index.get_parents_with_ghosts("a"))
653
 
        self.assertEqual(["a", "c"], index.get_parents_with_ghosts("b"))
654
 
        self.assertEqual(["b", "a", "e"],
655
 
            index.get_parents_with_ghosts("c"))
656
 
 
657
 
    def test_check_versions_present(self):
658
 
        transport = MockTransport([
659
 
            _KnitIndex.HEADER,
660
 
            "a option 0 1 :",
661
 
            "b option 0 1 :"
662
 
            ])
663
 
        index = _KnitIndex(transport, "filename", "r")
664
 
 
665
 
        check = index.check_versions_present
666
 
 
667
 
        check([])
668
 
        check(["a"])
669
 
        check(["b"])
670
 
        check(["a", "b"])
671
 
        self.assertRaises(RevisionNotPresent, check, ["c"])
672
 
        self.assertRaises(RevisionNotPresent, check, ["a", "b", "c"])
 
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)
673
1314
 
674
1315
 
675
1316
class KnitTests(TestCaseWithTransport):
676
1317
    """Class containing knit test helper routines."""
677
1318
 
678
 
    def make_test_knit(self, annotate=False, delay_create=False):
679
 
        if not annotate:
680
 
            factory = KnitPlainFactory()
681
 
        else:
682
 
            factory = None
683
 
        return KnitVersionedFile('test', get_transport('.'), access_mode='w',
684
 
                                 factory=factory, create=True,
685
 
                                 delay_create=delay_create)
686
 
 
687
 
 
688
 
class BasicKnitTests(KnitTests):
689
 
 
690
 
    def add_stock_one_and_one_a(self, k):
691
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
692
 
        k.add_lines('text-1a', ['text-1'], split_lines(TEXT_1A))
693
 
 
694
 
    def test_knit_constructor(self):
695
 
        """Construct empty k"""
696
 
        self.make_test_knit()
697
 
 
698
 
    def test_knit_add(self):
699
 
        """Store one text in knit and retrieve"""
700
 
        k = self.make_test_knit()
701
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
702
 
        self.assertTrue(k.has_version('text-1'))
703
 
        self.assertEqualDiff(''.join(k.get_lines('text-1')), TEXT_1)
704
 
 
705
 
    def test_knit_reload(self):
706
 
        # test that the content in a reloaded knit is correct
707
 
        k = self.make_test_knit()
708
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
709
 
        del k
710
 
        k2 = KnitVersionedFile('test', get_transport('.'), access_mode='r', factory=KnitPlainFactory(), create=True)
711
 
        self.assertTrue(k2.has_version('text-1'))
712
 
        self.assertEqualDiff(''.join(k2.get_lines('text-1')), TEXT_1)
713
 
 
714
 
    def test_knit_several(self):
715
 
        """Store several texts in a knit"""
716
 
        k = self.make_test_knit()
717
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
718
 
        k.add_lines('text-2', [], split_lines(TEXT_2))
719
 
        self.assertEqualDiff(''.join(k.get_lines('text-1')), TEXT_1)
720
 
        self.assertEqualDiff(''.join(k.get_lines('text-2')), TEXT_2)
721
 
        
722
 
    def test_repeated_add(self):
723
 
        """Knit traps attempt to replace existing version"""
724
 
        k = self.make_test_knit()
725
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
726
 
        self.assertRaises(RevisionAlreadyPresent, 
727
 
                k.add_lines,
728
 
                'text-1', [], split_lines(TEXT_1))
729
 
 
730
 
    def test_empty(self):
731
 
        k = self.make_test_knit(True)
732
 
        k.add_lines('text-1', [], [])
733
 
        self.assertEquals(k.get_lines('text-1'), [])
734
 
 
735
 
    def test_incomplete(self):
736
 
        """Test if texts without a ending line-end can be inserted and
737
 
        extracted."""
738
 
        k = KnitVersionedFile('test', get_transport('.'), delta=False, create=True)
739
 
        k.add_lines('text-1', [], ['a\n',    'b'  ])
740
 
        k.add_lines('text-2', ['text-1'], ['a\rb\n', 'b\n'])
741
 
        # reopening ensures maximum room for confusion
742
 
        k = KnitVersionedFile('test', get_transport('.'), delta=False, create=True)
743
 
        self.assertEquals(k.get_lines('text-1'), ['a\n',    'b'  ])
744
 
        self.assertEquals(k.get_lines('text-2'), ['a\rb\n', 'b\n'])
745
 
 
746
 
    def test_delta(self):
747
 
        """Expression of knit delta as lines"""
748
 
        k = self.make_test_knit()
749
 
        td = list(line_delta(TEXT_1.splitlines(True),
750
 
                             TEXT_1A.splitlines(True)))
751
 
        self.assertEqualDiff(''.join(td), delta_1_1a)
752
 
        out = apply_line_delta(TEXT_1.splitlines(True), td)
753
 
        self.assertEqualDiff(''.join(out), TEXT_1A)
754
 
 
755
 
    def test_add_with_parents(self):
756
 
        """Store in knit with parents"""
757
 
        k = self.make_test_knit()
758
 
        self.add_stock_one_and_one_a(k)
759
 
        self.assertEquals(k.get_parents('text-1'), [])
760
 
        self.assertEquals(k.get_parents('text-1a'), ['text-1'])
761
 
 
762
 
    def test_ancestry(self):
763
 
        """Store in knit with parents"""
764
 
        k = self.make_test_knit()
765
 
        self.add_stock_one_and_one_a(k)
766
 
        self.assertEquals(set(k.get_ancestry(['text-1a'])), set(['text-1a', 'text-1']))
767
 
 
768
 
    def test_add_delta(self):
769
 
        """Store in knit with parents"""
770
 
        k = KnitVersionedFile('test', get_transport('.'), factory=KnitPlainFactory(),
771
 
            delta=True, create=True)
772
 
        self.add_stock_one_and_one_a(k)
773
 
        k.clear_cache()
774
 
        self.assertEqualDiff(''.join(k.get_lines('text-1a')), TEXT_1A)
775
 
 
776
 
    def test_annotate(self):
777
 
        """Annotations"""
778
 
        k = KnitVersionedFile('knit', get_transport('.'), factory=KnitAnnotateFactory(),
779
 
            delta=True, create=True)
780
 
        self.insert_and_test_small_annotate(k)
781
 
 
782
 
    def insert_and_test_small_annotate(self, k):
783
 
        """test annotation with k works correctly."""
784
 
        k.add_lines('text-1', [], ['a\n', 'b\n'])
785
 
        k.add_lines('text-2', ['text-1'], ['a\n', 'c\n'])
786
 
 
787
 
        origins = k.annotate('text-2')
788
 
        self.assertEquals(origins[0], ('text-1', 'a\n'))
789
 
        self.assertEquals(origins[1], ('text-2', 'c\n'))
790
 
 
791
 
    def test_annotate_fulltext(self):
792
 
        """Annotations"""
793
 
        k = KnitVersionedFile('knit', get_transport('.'), factory=KnitAnnotateFactory(),
794
 
            delta=False, create=True)
795
 
        self.insert_and_test_small_annotate(k)
796
 
 
797
 
    def test_annotate_merge_1(self):
798
 
        k = self.make_test_knit(True)
799
 
        k.add_lines('text-a1', [], ['a\n', 'b\n'])
800
 
        k.add_lines('text-a2', [], ['d\n', 'c\n'])
801
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['d\n', 'b\n'])
802
 
        origins = k.annotate('text-am')
803
 
        self.assertEquals(origins[0], ('text-a2', 'd\n'))
804
 
        self.assertEquals(origins[1], ('text-a1', 'b\n'))
805
 
 
806
 
    def test_annotate_merge_2(self):
807
 
        k = self.make_test_knit(True)
808
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
809
 
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
810
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['a\n', 'y\n', 'c\n'])
811
 
        origins = k.annotate('text-am')
812
 
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
813
 
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
814
 
        self.assertEquals(origins[2], ('text-a1', 'c\n'))
815
 
 
816
 
    def test_annotate_merge_9(self):
817
 
        k = self.make_test_knit(True)
818
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
819
 
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
820
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['k\n', 'y\n', 'c\n'])
821
 
        origins = k.annotate('text-am')
822
 
        self.assertEquals(origins[0], ('text-am', 'k\n'))
823
 
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
824
 
        self.assertEquals(origins[2], ('text-a1', 'c\n'))
825
 
 
826
 
    def test_annotate_merge_3(self):
827
 
        k = self.make_test_knit(True)
828
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
829
 
        k.add_lines('text-a2', [] ,['x\n', 'y\n', 'z\n'])
830
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['k\n', 'y\n', 'z\n'])
831
 
        origins = k.annotate('text-am')
832
 
        self.assertEquals(origins[0], ('text-am', 'k\n'))
833
 
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
834
 
        self.assertEquals(origins[2], ('text-a2', 'z\n'))
835
 
 
836
 
    def test_annotate_merge_4(self):
837
 
        k = self.make_test_knit(True)
838
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
839
 
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
840
 
        k.add_lines('text-a3', ['text-a1'], ['a\n', 'b\n', 'p\n'])
841
 
        k.add_lines('text-am', ['text-a2', 'text-a3'], ['a\n', 'b\n', 'z\n'])
842
 
        origins = k.annotate('text-am')
843
 
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
844
 
        self.assertEquals(origins[1], ('text-a1', 'b\n'))
845
 
        self.assertEquals(origins[2], ('text-a2', 'z\n'))
846
 
 
847
 
    def test_annotate_merge_5(self):
848
 
        k = self.make_test_knit(True)
849
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
850
 
        k.add_lines('text-a2', [], ['d\n', 'e\n', 'f\n'])
851
 
        k.add_lines('text-a3', [], ['x\n', 'y\n', 'z\n'])
852
 
        k.add_lines('text-am',
853
 
                    ['text-a1', 'text-a2', 'text-a3'],
854
 
                    ['a\n', 'e\n', 'z\n'])
855
 
        origins = k.annotate('text-am')
856
 
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
857
 
        self.assertEquals(origins[1], ('text-a2', 'e\n'))
858
 
        self.assertEquals(origins[2], ('text-a3', 'z\n'))
859
 
 
860
 
    def test_annotate_file_cherry_pick(self):
861
 
        k = self.make_test_knit(True)
862
 
        k.add_lines('text-1', [], ['a\n', 'b\n', 'c\n'])
863
 
        k.add_lines('text-2', ['text-1'], ['d\n', 'e\n', 'f\n'])
864
 
        k.add_lines('text-3', ['text-2', 'text-1'], ['a\n', 'b\n', 'c\n'])
865
 
        origins = k.annotate('text-3')
866
 
        self.assertEquals(origins[0], ('text-1', 'a\n'))
867
 
        self.assertEquals(origins[1], ('text-1', 'b\n'))
868
 
        self.assertEquals(origins[2], ('text-1', 'c\n'))
869
 
 
870
 
    def test_knit_join(self):
871
 
        """Store in knit with parents"""
872
 
        k1 = KnitVersionedFile('test1', get_transport('.'), factory=KnitPlainFactory(), create=True)
873
 
        k1.add_lines('text-a', [], split_lines(TEXT_1))
874
 
        k1.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
875
 
 
876
 
        k1.add_lines('text-c', [], split_lines(TEXT_1))
877
 
        k1.add_lines('text-d', ['text-c'], split_lines(TEXT_1))
878
 
 
879
 
        k1.add_lines('text-m', ['text-b', 'text-d'], split_lines(TEXT_1))
880
 
 
881
 
        k2 = KnitVersionedFile('test2', get_transport('.'), factory=KnitPlainFactory(), create=True)
882
 
        count = k2.join(k1, version_ids=['text-m'])
883
 
        self.assertEquals(count, 5)
884
 
        self.assertTrue(k2.has_version('text-a'))
885
 
        self.assertTrue(k2.has_version('text-c'))
886
 
 
887
 
    def test_reannotate(self):
888
 
        k1 = KnitVersionedFile('knit1', get_transport('.'),
889
 
                               factory=KnitAnnotateFactory(), create=True)
890
 
        # 0
891
 
        k1.add_lines('text-a', [], ['a\n', 'b\n'])
892
 
        # 1
893
 
        k1.add_lines('text-b', ['text-a'], ['a\n', 'c\n'])
894
 
 
895
 
        k2 = KnitVersionedFile('test2', get_transport('.'),
896
 
                               factory=KnitAnnotateFactory(), create=True)
897
 
        k2.join(k1, version_ids=['text-b'])
898
 
 
899
 
        # 2
900
 
        k1.add_lines('text-X', ['text-b'], ['a\n', 'b\n'])
901
 
        # 2
902
 
        k2.add_lines('text-c', ['text-b'], ['z\n', 'c\n'])
903
 
        # 3
904
 
        k2.add_lines('text-Y', ['text-b'], ['b\n', 'c\n'])
905
 
 
906
 
        # test-c will have index 3
907
 
        k1.join(k2, version_ids=['text-c'])
908
 
 
909
 
        lines = k1.get_lines('text-c')
910
 
        self.assertEquals(lines, ['z\n', 'c\n'])
911
 
 
912
 
        origins = k1.annotate('text-c')
913
 
        self.assertEquals(origins[0], ('text-c', 'z\n'))
914
 
        self.assertEquals(origins[1], ('text-b', 'c\n'))
915
 
 
916
 
    def test_get_line_delta_texts(self):
917
 
        """Make sure we can call get_texts on text with reused line deltas"""
918
 
        k1 = KnitVersionedFile('test1', get_transport('.'), 
919
 
                               factory=KnitPlainFactory(), create=True)
920
 
        for t in range(3):
921
 
            if t == 0:
922
 
                parents = []
923
 
            else:
924
 
                parents = ['%d' % (t-1)]
925
 
            k1.add_lines('%d' % t, parents, ['hello\n'] * t)
926
 
        k1.get_texts(('%d' % t) for t in range(3))
927
 
        
928
 
    def test_iter_lines_reads_in_order(self):
929
 
        t = MemoryTransport()
930
 
        instrumented_t = TransportLogger(t)
931
 
        k1 = KnitVersionedFile('id', instrumented_t, create=True, delta=True)
932
 
        self.assertEqual([('id.kndx',)], instrumented_t._calls)
933
 
        # add texts with no required ordering
934
 
        k1.add_lines('base', [], ['text\n'])
935
 
        k1.add_lines('base2', [], ['text2\n'])
936
 
        k1.clear_cache()
937
 
        instrumented_t._calls = []
938
 
        # request a last-first iteration
939
 
        results = list(k1.iter_lines_added_or_present_in_versions(['base2', 'base']))
940
 
        self.assertEqual([('id.knit', [(0, 87), (87, 89)])], instrumented_t._calls)
941
 
        self.assertEqual(['text\n', 'text2\n'], results)
942
 
 
943
 
    def test_create_empty_annotated(self):
944
 
        k1 = self.make_test_knit(True)
945
 
        # 0
946
 
        k1.add_lines('text-a', [], ['a\n', 'b\n'])
947
 
        k2 = k1.create_empty('t', MemoryTransport())
948
 
        self.assertTrue(isinstance(k2.factory, KnitAnnotateFactory))
949
 
        self.assertEqual(k1.delta, k2.delta)
950
 
        # the generic test checks for empty content and file class
951
 
 
952
 
    def test_knit_format(self):
953
 
        # this tests that a new knit index file has the expected content
954
 
        # and that is writes the data we expect as records are added.
955
 
        knit = self.make_test_knit(True)
956
 
        # Now knit files are not created until we first add data to them
957
 
        self.assertFileEqual("# bzr knit index 8\n", 'test.kndx')
958
 
        knit.add_lines_with_ghosts('revid', ['a_ghost'], ['a\n'])
959
 
        self.assertFileEqual(
960
 
            "# bzr knit index 8\n"
961
 
            "\n"
962
 
            "revid fulltext 0 84 .a_ghost :",
963
 
            'test.kndx')
964
 
        knit.add_lines_with_ghosts('revid2', ['revid'], ['a\n'])
965
 
        self.assertFileEqual(
966
 
            "# bzr knit index 8\n"
967
 
            "\nrevid fulltext 0 84 .a_ghost :"
968
 
            "\nrevid2 line-delta 84 82 0 :",
969
 
            'test.kndx')
970
 
        # we should be able to load this file again
971
 
        knit = KnitVersionedFile('test', get_transport('.'), access_mode='r')
972
 
        self.assertEqual(['revid', 'revid2'], knit.versions())
973
 
        # write a short write to the file and ensure that its ignored
974
 
        indexfile = file('test.kndx', 'at')
975
 
        indexfile.write('\nrevid3 line-delta 166 82 1 2 3 4 5 .phwoar:demo ')
976
 
        indexfile.close()
977
 
        # we should be able to load this file again
978
 
        knit = KnitVersionedFile('test', get_transport('.'), access_mode='w')
979
 
        self.assertEqual(['revid', 'revid2'], knit.versions())
980
 
        # and add a revision with the same id the failed write had
981
 
        knit.add_lines('revid3', ['revid2'], ['a\n'])
982
 
        # and when reading it revid3 should now appear.
983
 
        knit = KnitVersionedFile('test', get_transport('.'), access_mode='r')
984
 
        self.assertEqual(['revid', 'revid2', 'revid3'], knit.versions())
985
 
        self.assertEqual(['revid2'], knit.get_parents('revid3'))
986
 
 
987
 
    def test_delay_create(self):
988
 
        """Test that passing delay_create=True creates files late"""
989
 
        knit = self.make_test_knit(annotate=True, delay_create=True)
990
 
        self.failIfExists('test.knit')
991
 
        self.failIfExists('test.kndx')
992
 
        knit.add_lines_with_ghosts('revid', ['a_ghost'], ['a\n'])
993
 
        self.failUnlessExists('test.knit')
994
 
        self.assertFileEqual(
995
 
            "# bzr knit index 8\n"
996
 
            "\n"
997
 
            "revid fulltext 0 84 .a_ghost :",
998
 
            'test.kndx')
999
 
 
1000
 
    def test_create_parent_dir(self):
1001
 
        """create_parent_dir can create knits in nonexistant dirs"""
1002
 
        # Has no effect if we don't set 'delay_create'
1003
 
        trans = get_transport('.')
1004
 
        self.assertRaises(NoSuchFile, KnitVersionedFile, 'dir/test',
1005
 
                          trans, access_mode='w', factory=None,
1006
 
                          create=True, create_parent_dir=True)
1007
 
        # Nothing should have changed yet
1008
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1009
 
                                 factory=None, create=True,
1010
 
                                 create_parent_dir=True,
1011
 
                                 delay_create=True)
1012
 
        self.failIfExists('dir/test.knit')
1013
 
        self.failIfExists('dir/test.kndx')
1014
 
        self.failIfExists('dir')
1015
 
        knit.add_lines('revid', [], ['a\n'])
1016
 
        self.failUnlessExists('dir')
1017
 
        self.failUnlessExists('dir/test.knit')
1018
 
        self.assertFileEqual(
1019
 
            "# bzr knit index 8\n"
1020
 
            "\n"
1021
 
            "revid fulltext 0 84  :",
1022
 
            'dir/test.kndx')
1023
 
 
1024
 
    def test_create_mode_700(self):
1025
 
        trans = get_transport('.')
1026
 
        if not trans._can_roundtrip_unix_modebits():
1027
 
            # Can't roundtrip, so no need to run this test
1028
 
            return
1029
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1030
 
                                 factory=None, create=True,
1031
 
                                 create_parent_dir=True,
1032
 
                                 delay_create=True,
1033
 
                                 file_mode=0600,
1034
 
                                 dir_mode=0700)
1035
 
        knit.add_lines('revid', [], ['a\n'])
1036
 
        self.assertTransportMode(trans, 'dir', 0700)
1037
 
        self.assertTransportMode(trans, 'dir/test.knit', 0600)
1038
 
        self.assertTransportMode(trans, 'dir/test.kndx', 0600)
1039
 
 
1040
 
    def test_create_mode_770(self):
1041
 
        trans = get_transport('.')
1042
 
        if not trans._can_roundtrip_unix_modebits():
1043
 
            # Can't roundtrip, so no need to run this test
1044
 
            return
1045
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1046
 
                                 factory=None, create=True,
1047
 
                                 create_parent_dir=True,
1048
 
                                 delay_create=True,
1049
 
                                 file_mode=0660,
1050
 
                                 dir_mode=0770)
1051
 
        knit.add_lines('revid', [], ['a\n'])
1052
 
        self.assertTransportMode(trans, 'dir', 0770)
1053
 
        self.assertTransportMode(trans, 'dir/test.knit', 0660)
1054
 
        self.assertTransportMode(trans, 'dir/test.kndx', 0660)
1055
 
 
1056
 
    def test_create_mode_777(self):
1057
 
        trans = get_transport('.')
1058
 
        if not trans._can_roundtrip_unix_modebits():
1059
 
            # Can't roundtrip, so no need to run this test
1060
 
            return
1061
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1062
 
                                 factory=None, create=True,
1063
 
                                 create_parent_dir=True,
1064
 
                                 delay_create=True,
1065
 
                                 file_mode=0666,
1066
 
                                 dir_mode=0777)
1067
 
        knit.add_lines('revid', [], ['a\n'])
1068
 
        self.assertTransportMode(trans, 'dir', 0777)
1069
 
        self.assertTransportMode(trans, 'dir/test.knit', 0666)
1070
 
        self.assertTransportMode(trans, 'dir/test.kndx', 0666)
1071
 
 
1072
 
    def test_plan_merge(self):
1073
 
        my_knit = self.make_test_knit(annotate=True)
1074
 
        my_knit.add_lines('text1', [], split_lines(TEXT_1))
1075
 
        my_knit.add_lines('text1a', ['text1'], split_lines(TEXT_1A))
1076
 
        my_knit.add_lines('text1b', ['text1'], split_lines(TEXT_1B))
1077
 
        plan = list(my_knit.plan_merge('text1a', 'text1b'))
1078
 
        for plan_line, expected_line in zip(plan, AB_MERGE):
1079
 
            self.assertEqual(plan_line, expected_line)
1080
 
 
1081
 
 
1082
 
TEXT_1 = """\
1083
 
Banana cup cakes:
1084
 
 
1085
 
- bananas
1086
 
- eggs
1087
 
- broken tea cups
1088
 
"""
1089
 
 
1090
 
TEXT_1A = """\
1091
 
Banana cup cake recipe
1092
 
(serves 6)
1093
 
 
1094
 
- bananas
1095
 
- eggs
1096
 
- broken tea cups
1097
 
- self-raising flour
1098
 
"""
1099
 
 
1100
 
TEXT_1B = """\
1101
 
Banana cup cake recipe
1102
 
 
1103
 
- bananas (do not use plantains!!!)
1104
 
- broken tea cups
1105
 
- flour
1106
 
"""
1107
 
 
1108
 
delta_1_1a = """\
1109
 
0,1,2
1110
 
Banana cup cake recipe
1111
 
(serves 6)
1112
 
5,5,1
1113
 
- self-raising flour
1114
 
"""
1115
 
 
1116
 
TEXT_2 = """\
1117
 
Boeuf bourguignon
1118
 
 
1119
 
- beef
1120
 
- red wine
1121
 
- small onions
1122
 
- carrot
1123
 
- mushrooms
1124
 
"""
1125
 
 
1126
 
AB_MERGE_TEXT="""unchanged|Banana cup cake recipe
1127
 
new-a|(serves 6)
1128
 
unchanged|
1129
 
killed-b|- bananas
1130
 
killed-b|- eggs
1131
 
new-b|- bananas (do not use plantains!!!)
1132
 
unchanged|- broken tea cups
1133
 
new-a|- self-raising flour
1134
 
new-b|- flour
1135
 
"""
1136
 
AB_MERGE=[tuple(l.split('|')) for l in AB_MERGE_TEXT.splitlines(True)]
1137
 
 
1138
 
 
1139
 
def line_delta(from_lines, to_lines):
1140
 
    """Generate line-based delta from one text to another"""
1141
 
    s = difflib.SequenceMatcher(None, from_lines, to_lines)
1142
 
    for op in s.get_opcodes():
1143
 
        if op[0] == 'equal':
1144
 
            continue
1145
 
        yield '%d,%d,%d\n' % (op[1], op[2], op[4]-op[3])
1146
 
        for i in range(op[3], op[4]):
1147
 
            yield to_lines[i]
1148
 
 
1149
 
 
1150
 
def apply_line_delta(basis_lines, delta_lines):
1151
 
    """Apply a line-based perfect diff
1152
 
    
1153
 
    basis_lines -- text to apply the patch to
1154
 
    delta_lines -- diff instructions and content
1155
 
    """
1156
 
    out = basis_lines[:]
1157
 
    i = 0
1158
 
    offset = 0
1159
 
    while i < len(delta_lines):
1160
 
        l = delta_lines[i]
1161
 
        a, b, c = map(long, l.split(','))
1162
 
        i = i + 1
1163
 
        out[offset+a:offset+b] = delta_lines[i:i+c]
1164
 
        i = i + c
1165
 
        offset = offset + (b - a) + c
1166
 
    return out
1167
 
 
1168
 
 
1169
 
class TestWeaveToKnit(KnitTests):
1170
 
 
1171
 
    def test_weave_to_knit_matches(self):
1172
 
        # check that the WeaveToKnit is_compatible function
1173
 
        # registers True for a Weave to a Knit.
1174
 
        w = Weave()
1175
 
        k = self.make_test_knit()
1176
 
        self.failUnless(WeaveToKnit.is_compatible(w, k))
1177
 
        self.failIf(WeaveToKnit.is_compatible(k, w))
1178
 
        self.failIf(WeaveToKnit.is_compatible(w, w))
1179
 
        self.failIf(WeaveToKnit.is_compatible(k, k))
1180
 
 
1181
 
 
1182
 
class TestKnitCaching(KnitTests):
1183
 
    
1184
 
    def create_knit(self, cache_add=False):
1185
 
        k = self.make_test_knit(True)
1186
 
        if cache_add:
1187
 
            k.enable_cache()
1188
 
 
1189
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
1190
 
        k.add_lines('text-2', [], split_lines(TEXT_2))
1191
 
        return k
1192
 
 
1193
 
    def test_no_caching(self):
1194
 
        k = self.create_knit()
1195
 
        # Nothing should be cached without setting 'enable_cache'
1196
 
        self.assertEqual({}, k._data._cache)
1197
 
 
1198
 
    def test_cache_add_and_clear(self):
1199
 
        k = self.create_knit(True)
1200
 
 
1201
 
        self.assertEqual(['text-1', 'text-2'], sorted(k._data._cache.keys()))
1202
 
 
1203
 
        k.clear_cache()
1204
 
        self.assertEqual({}, k._data._cache)
1205
 
 
1206
 
    def test_cache_data_read_raw(self):
1207
 
        k = self.create_knit()
1208
 
 
1209
 
        # Now cache and read
1210
 
        k.enable_cache()
1211
 
 
1212
 
        def read_one_raw(version):
1213
 
            pos_map = k._get_components_positions([version])
1214
 
            method, pos, size, next = pos_map[version]
1215
 
            lst = list(k._data.read_records_iter_raw([(version, pos, size)]))
1216
 
            self.assertEqual(1, len(lst))
1217
 
            return lst[0]
1218
 
 
1219
 
        val = read_one_raw('text-1')
1220
 
        self.assertEqual({'text-1':val[1]}, k._data._cache)
1221
 
 
1222
 
        k.clear_cache()
1223
 
        # After clear, new reads are not cached
1224
 
        self.assertEqual({}, k._data._cache)
1225
 
 
1226
 
        val2 = read_one_raw('text-1')
1227
 
        self.assertEqual(val, val2)
1228
 
        self.assertEqual({}, k._data._cache)
1229
 
 
1230
 
    def test_cache_data_read(self):
1231
 
        k = self.create_knit()
1232
 
 
1233
 
        def read_one(version):
1234
 
            pos_map = k._get_components_positions([version])
1235
 
            method, pos, size, next = pos_map[version]
1236
 
            lst = list(k._data.read_records_iter([(version, pos, size)]))
1237
 
            self.assertEqual(1, len(lst))
1238
 
            return lst[0]
1239
 
 
1240
 
        # Now cache and read
1241
 
        k.enable_cache()
1242
 
 
1243
 
        val = read_one('text-2')
1244
 
        self.assertEqual(['text-2'], k._data._cache.keys())
1245
 
        self.assertEqual('text-2', val[0])
1246
 
        content, digest = k._data._parse_record('text-2',
1247
 
                                                k._data._cache['text-2'])
1248
 
        self.assertEqual(content, val[1])
1249
 
        self.assertEqual(digest, val[2])
1250
 
 
1251
 
        k.clear_cache()
1252
 
        self.assertEqual({}, k._data._cache)
1253
 
 
1254
 
        val2 = read_one('text-2')
1255
 
        self.assertEqual(val, val2)
1256
 
        self.assertEqual({}, k._data._cache)
1257
 
 
1258
 
    def test_cache_read(self):
1259
 
        k = self.create_knit()
1260
 
        k.enable_cache()
1261
 
 
1262
 
        text = k.get_text('text-1')
1263
 
        self.assertEqual(TEXT_1, text)
1264
 
        self.assertEqual(['text-1'], k._data._cache.keys())
1265
 
 
1266
 
        k.clear_cache()
1267
 
        self.assertEqual({}, k._data._cache)
1268
 
 
1269
 
        text = k.get_text('text-1')
1270
 
        self.assertEqual(TEXT_1, text)
1271
 
        self.assertEqual({}, k._data._cache)
 
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 ")
1272
1349
 
1273
1350
 
1274
1351
class TestKnitIndex(KnitTests):
1277
1354
        """Adding versions to the index should update the lookup dict"""
1278
1355
        knit = self.make_test_knit()
1279
1356
        idx = knit._index
1280
 
        idx.add_version('a-1', ['fulltext'], 0, 0, [])
 
1357
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
1281
1358
        self.check_file_contents('test.kndx',
1282
1359
            '# bzr knit index 8\n'
1283
1360
            '\n'
1284
1361
            'a-1 fulltext 0 0  :'
1285
1362
            )
1286
 
        idx.add_versions([('a-2', ['fulltext'], 0, 0, ['a-1']),
1287
 
                          ('a-3', ['fulltext'], 0, 0, ['a-2']),
1288
 
                         ])
 
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
            ])
1289
1367
        self.check_file_contents('test.kndx',
1290
1368
            '# bzr knit index 8\n'
1291
1369
            '\n'
1293
1371
            'a-2 fulltext 0 0 0 :\n'
1294
1372
            'a-3 fulltext 0 0 1 :'
1295
1373
            )
1296
 
        self.assertEqual(['a-1', 'a-2', 'a-3'], idx._history)
1297
 
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, [], 0),
1298
 
                          'a-2':('a-2', ['fulltext'], 0, 0, ['a-1'], 1),
1299
 
                          'a-3':('a-3', ['fulltext'], 0, 0, ['a-2'], 2),
1300
 
                         }, idx._cache)
 
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()))
1301
1384
 
1302
1385
    def test_add_versions_fails_clean(self):
1303
1386
        """If add_versions fails in the middle, it restores a pristine state.
1313
1396
 
1314
1397
        knit = self.make_test_knit()
1315
1398
        idx = knit._index
1316
 
        idx.add_version('a-1', ['fulltext'], 0, 0, [])
 
1399
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
1317
1400
 
1318
1401
        class StopEarly(Exception):
1319
1402
            pass
1320
1403
 
1321
1404
        def generate_failure():
1322
1405
            """Add some entries and then raise an exception"""
1323
 
            yield ('a-2', ['fulltext'], 0, 0, ['a-1'])
1324
 
            yield ('a-3', ['fulltext'], 0, 0, ['a-2'])
 
1406
            yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
 
1407
            yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
1325
1408
            raise StopEarly()
1326
1409
 
1327
1410
        # Assert the pre-condition
1328
 
        self.assertEqual(['a-1'], idx._history)
1329
 
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, [], 0)}, idx._cache)
1330
 
 
1331
 
        self.assertRaises(StopEarly, idx.add_versions, generate_failure())
1332
 
 
 
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())
1333
1420
        # And it shouldn't be modified
1334
 
        self.assertEqual(['a-1'], idx._history)
1335
 
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, [], 0)}, idx._cache)
 
1421
        assertA1Only()
1336
1422
 
1337
1423
    def test_knit_index_ignores_empty_files(self):
1338
1424
        # There was a race condition in older bzr, where a ^C at the right time
1347
1433
    def test_knit_index_checks_header(self):
1348
1434
        t = get_transport('.')
1349
1435
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
1350
 
 
1351
 
        self.assertRaises(KnitHeaderError, self.make_test_knit)
 
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)