/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Robert Collins
  • Date: 2008-04-08 03:39:43 UTC
  • mto: This revision was merged to the branch mainline in revision 3350.
  • Revision ID: robertc@robertcollins.net-20080408033943-ihbgs5wyqnh61bit
 * ``VersionedFile.get_sha1`` is deprecated, please use
   ``VersionedFile.get_sha1s``. (Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Tests for Knit data structure"""
 
18
 
 
19
from cStringIO import StringIO
 
20
import difflib
 
21
import gzip
 
22
import sha
 
23
import sys
 
24
 
 
25
from bzrlib import (
 
26
    errors,
 
27
    generate_ids,
 
28
    knit,
 
29
    pack,
 
30
    )
 
31
from bzrlib.errors import (
 
32
    RevisionAlreadyPresent,
 
33
    KnitHeaderError,
 
34
    RevisionNotPresent,
 
35
    NoSuchFile,
 
36
    )
 
37
from bzrlib.index import *
 
38
from bzrlib.knit import (
 
39
    AnnotatedKnitContent,
 
40
    DATA_SUFFIX,
 
41
    INDEX_SUFFIX,
 
42
    KnitContent,
 
43
    KnitGraphIndex,
 
44
    KnitVersionedFile,
 
45
    KnitPlainFactory,
 
46
    KnitAnnotateFactory,
 
47
    _KnitAccess,
 
48
    _KnitData,
 
49
    _KnitIndex,
 
50
    make_file_knit,
 
51
    _PackAccess,
 
52
    PlainKnitContent,
 
53
    _StreamAccess,
 
54
    _StreamIndex,
 
55
    WeaveToKnit,
 
56
    KnitSequenceMatcher,
 
57
    )
 
58
from bzrlib.osutils import split_lines
 
59
from bzrlib.tests import (
 
60
    Feature,
 
61
    TestCase,
 
62
    TestCaseWithMemoryTransport,
 
63
    TestCaseWithTransport,
 
64
    )
 
65
from bzrlib.transport import get_transport
 
66
from bzrlib.transport.memory import MemoryTransport
 
67
from bzrlib.tuned_gzip import GzipFile
 
68
from bzrlib.util import bencode
 
69
from bzrlib.weave import Weave
 
70
 
 
71
 
 
72
class _CompiledKnitFeature(Feature):
 
73
 
 
74
    def _probe(self):
 
75
        try:
 
76
            import bzrlib._knit_load_data_c
 
77
        except ImportError:
 
78
            return False
 
79
        return True
 
80
 
 
81
    def feature_name(self):
 
82
        return 'bzrlib._knit_load_data_c'
 
83
 
 
84
CompiledKnitFeature = _CompiledKnitFeature()
 
85
 
 
86
 
 
87
class KnitContentTestsMixin(object):
 
88
 
 
89
    def test_constructor(self):
 
90
        content = self._make_content([])
 
91
 
 
92
    def test_text(self):
 
93
        content = self._make_content([])
 
94
        self.assertEqual(content.text(), [])
 
95
 
 
96
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
97
        self.assertEqual(content.text(), ["text1", "text2"])
 
98
 
 
99
    def test_copy(self):
 
100
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
101
        copy = content.copy()
 
102
        self.assertIsInstance(copy, content.__class__)
 
103
        self.assertEqual(copy.annotate(), content.annotate())
 
104
 
 
105
    def assertDerivedBlocksEqual(self, source, target, noeol=False):
 
106
        """Assert that the derived matching blocks match real output"""
 
107
        source_lines = source.splitlines(True)
 
108
        target_lines = target.splitlines(True)
 
109
        def nl(line):
 
110
            if noeol and not line.endswith('\n'):
 
111
                return line + '\n'
 
112
            else:
 
113
                return line
 
114
        source_content = self._make_content([(None, nl(l)) for l in source_lines])
 
115
        target_content = self._make_content([(None, nl(l)) for l in target_lines])
 
116
        line_delta = source_content.line_delta(target_content)
 
117
        delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
 
118
            source_lines, target_lines))
 
119
        matcher = KnitSequenceMatcher(None, source_lines, target_lines)
 
120
        matcher_blocks = list(list(matcher.get_matching_blocks()))
 
121
        self.assertEqual(matcher_blocks, delta_blocks)
 
122
 
 
123
    def test_get_line_delta_blocks(self):
 
124
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
 
125
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
 
126
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
 
127
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
 
128
        self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
 
129
        self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
 
130
        self.assertDerivedBlocksEqual(TEXT_1A, '')
 
131
        self.assertDerivedBlocksEqual('', TEXT_1A)
 
132
        self.assertDerivedBlocksEqual('', '')
 
133
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
 
134
 
 
135
    def test_get_line_delta_blocks_noeol(self):
 
136
        """Handle historical knit deltas safely
 
137
 
 
138
        Some existing knit deltas don't consider the last line to differ
 
139
        when the only difference whether it has a final newline.
 
140
 
 
141
        New knit deltas appear to always consider the last line to differ
 
142
        in this case.
 
143
        """
 
144
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
 
145
        self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
 
146
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
 
147
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
 
148
 
 
149
 
 
150
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
 
151
 
 
152
    def _make_content(self, lines):
 
153
        annotated_content = AnnotatedKnitContent(lines)
 
154
        return PlainKnitContent(annotated_content.text(), 'bogus')
 
155
 
 
156
    def test_annotate(self):
 
157
        content = self._make_content([])
 
158
        self.assertEqual(content.annotate(), [])
 
159
 
 
160
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
161
        self.assertEqual(content.annotate(),
 
162
            [("bogus", "text1"), ("bogus", "text2")])
 
163
 
 
164
    def test_annotate_iter(self):
 
165
        content = self._make_content([])
 
166
        it = content.annotate_iter()
 
167
        self.assertRaises(StopIteration, it.next)
 
168
 
 
169
        content = self._make_content([("bogus", "text1"), ("bogus", "text2")])
 
170
        it = content.annotate_iter()
 
171
        self.assertEqual(it.next(), ("bogus", "text1"))
 
172
        self.assertEqual(it.next(), ("bogus", "text2"))
 
173
        self.assertRaises(StopIteration, it.next)
 
174
 
 
175
    def test_line_delta(self):
 
176
        content1 = self._make_content([("", "a"), ("", "b")])
 
177
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
178
        self.assertEqual(content1.line_delta(content2),
 
179
            [(1, 2, 2, ["a", "c"])])
 
180
 
 
181
    def test_line_delta_iter(self):
 
182
        content1 = self._make_content([("", "a"), ("", "b")])
 
183
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
184
        it = content1.line_delta_iter(content2)
 
185
        self.assertEqual(it.next(), (1, 2, 2, ["a", "c"]))
 
186
        self.assertRaises(StopIteration, it.next)
 
187
 
 
188
 
 
189
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
 
190
 
 
191
    def _make_content(self, lines):
 
192
        return AnnotatedKnitContent(lines)
 
193
 
 
194
    def test_annotate(self):
 
195
        content = self._make_content([])
 
196
        self.assertEqual(content.annotate(), [])
 
197
 
 
198
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
199
        self.assertEqual(content.annotate(),
 
200
            [("origin1", "text1"), ("origin2", "text2")])
 
201
 
 
202
    def test_annotate_iter(self):
 
203
        content = self._make_content([])
 
204
        it = content.annotate_iter()
 
205
        self.assertRaises(StopIteration, it.next)
 
206
 
 
207
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
208
        it = content.annotate_iter()
 
209
        self.assertEqual(it.next(), ("origin1", "text1"))
 
210
        self.assertEqual(it.next(), ("origin2", "text2"))
 
211
        self.assertRaises(StopIteration, it.next)
 
212
 
 
213
    def test_line_delta(self):
 
214
        content1 = self._make_content([("", "a"), ("", "b")])
 
215
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
216
        self.assertEqual(content1.line_delta(content2),
 
217
            [(1, 2, 2, [("", "a"), ("", "c")])])
 
218
 
 
219
    def test_line_delta_iter(self):
 
220
        content1 = self._make_content([("", "a"), ("", "b")])
 
221
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
222
        it = content1.line_delta_iter(content2)
 
223
        self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
 
224
        self.assertRaises(StopIteration, it.next)
 
225
 
 
226
 
 
227
class MockTransport(object):
 
228
 
 
229
    def __init__(self, file_lines=None):
 
230
        self.file_lines = file_lines
 
231
        self.calls = []
 
232
        # We have no base directory for the MockTransport
 
233
        self.base = ''
 
234
 
 
235
    def get(self, filename):
 
236
        if self.file_lines is None:
 
237
            raise NoSuchFile(filename)
 
238
        else:
 
239
            return StringIO("\n".join(self.file_lines))
 
240
 
 
241
    def readv(self, relpath, offsets):
 
242
        fp = self.get(relpath)
 
243
        for offset, size in offsets:
 
244
            fp.seek(offset)
 
245
            yield offset, fp.read(size)
 
246
 
 
247
    def __getattr__(self, name):
 
248
        def queue_call(*args, **kwargs):
 
249
            self.calls.append((name, args, kwargs))
 
250
        return queue_call
 
251
 
 
252
 
 
253
class KnitRecordAccessTestsMixin(object):
 
254
    """Tests for getting and putting knit records."""
 
255
 
 
256
    def assertAccessExists(self, access):
 
257
        """Ensure the data area for access has been initialised/exists."""
 
258
        raise NotImplementedError(self.assertAccessExists)
 
259
 
 
260
    def test_add_raw_records(self):
 
261
        """Add_raw_records adds records retrievable later."""
 
262
        access = self.get_access()
 
263
        memos = access.add_raw_records([10], '1234567890')
 
264
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
 
265
 
 
266
    def test_add_several_raw_records(self):
 
267
        """add_raw_records with many records and read some back."""
 
268
        access = self.get_access()
 
269
        memos = access.add_raw_records([10, 2, 5], '12345678901234567')
 
270
        self.assertEqual(['1234567890', '12', '34567'],
 
271
            list(access.get_raw_records(memos)))
 
272
        self.assertEqual(['1234567890'],
 
273
            list(access.get_raw_records(memos[0:1])))
 
274
        self.assertEqual(['12'],
 
275
            list(access.get_raw_records(memos[1:2])))
 
276
        self.assertEqual(['34567'],
 
277
            list(access.get_raw_records(memos[2:3])))
 
278
        self.assertEqual(['1234567890', '34567'],
 
279
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
280
 
 
281
    def test_create(self):
 
282
        """create() should make a file on disk."""
 
283
        access = self.get_access()
 
284
        access.create()
 
285
        self.assertAccessExists(access)
 
286
 
 
287
    def test_open_file(self):
 
288
        """open_file never errors."""
 
289
        access = self.get_access()
 
290
        access.open_file()
 
291
 
 
292
 
 
293
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
294
    """Tests for the .kndx implementation."""
 
295
 
 
296
    def assertAccessExists(self, access):
 
297
        self.assertNotEqual(None, access.open_file())
 
298
 
 
299
    def get_access(self):
 
300
        """Get a .knit style access instance."""
 
301
        access = _KnitAccess(self.get_transport(), "foo.knit", None, None,
 
302
            False, False)
 
303
        return access
 
304
    
 
305
 
 
306
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
307
    """Tests for the pack based access."""
 
308
 
 
309
    def assertAccessExists(self, access):
 
310
        # as pack based access has no backing unless an index maps data, this
 
311
        # is a no-op.
 
312
        pass
 
313
 
 
314
    def get_access(self):
 
315
        return self._get_access()[0]
 
316
 
 
317
    def _get_access(self, packname='packfile', index='FOO'):
 
318
        transport = self.get_transport()
 
319
        def write_data(bytes):
 
320
            transport.append_bytes(packname, bytes)
 
321
        writer = pack.ContainerWriter(write_data)
 
322
        writer.begin()
 
323
        indices = {index:(transport, packname)}
 
324
        access = _PackAccess(indices, writer=(writer, index))
 
325
        return access, writer
 
326
 
 
327
    def test_read_from_several_packs(self):
 
328
        access, writer = self._get_access()
 
329
        memos = []
 
330
        memos.extend(access.add_raw_records([10], '1234567890'))
 
331
        writer.end()
 
332
        access, writer = self._get_access('pack2', 'FOOBAR')
 
333
        memos.extend(access.add_raw_records([5], '12345'))
 
334
        writer.end()
 
335
        access, writer = self._get_access('pack3', 'BAZ')
 
336
        memos.extend(access.add_raw_records([5], 'alpha'))
 
337
        writer.end()
 
338
        transport = self.get_transport()
 
339
        access = _PackAccess({"FOO":(transport, 'packfile'),
 
340
            "FOOBAR":(transport, 'pack2'),
 
341
            "BAZ":(transport, 'pack3')})
 
342
        self.assertEqual(['1234567890', '12345', 'alpha'],
 
343
            list(access.get_raw_records(memos)))
 
344
        self.assertEqual(['1234567890'],
 
345
            list(access.get_raw_records(memos[0:1])))
 
346
        self.assertEqual(['12345'],
 
347
            list(access.get_raw_records(memos[1:2])))
 
348
        self.assertEqual(['alpha'],
 
349
            list(access.get_raw_records(memos[2:3])))
 
350
        self.assertEqual(['1234567890', 'alpha'],
 
351
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
352
 
 
353
    def test_set_writer(self):
 
354
        """The writer should be settable post construction."""
 
355
        access = _PackAccess({})
 
356
        transport = self.get_transport()
 
357
        packname = 'packfile'
 
358
        index = 'foo'
 
359
        def write_data(bytes):
 
360
            transport.append_bytes(packname, bytes)
 
361
        writer = pack.ContainerWriter(write_data)
 
362
        writer.begin()
 
363
        access.set_writer(writer, index, (transport, packname))
 
364
        memos = access.add_raw_records([10], '1234567890')
 
365
        writer.end()
 
366
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
 
367
 
 
368
 
 
369
class LowLevelKnitDataTests(TestCase):
 
370
 
 
371
    def create_gz_content(self, text):
 
372
        sio = StringIO()
 
373
        gz_file = gzip.GzipFile(mode='wb', fileobj=sio)
 
374
        gz_file.write(text)
 
375
        gz_file.close()
 
376
        return sio.getvalue()
 
377
 
 
378
    def test_valid_knit_data(self):
 
379
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
380
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
381
                                        'foo\n'
 
382
                                        'bar\n'
 
383
                                        'end rev-id-1\n'
 
384
                                        % (sha1sum,))
 
385
        transport = MockTransport([gz_txt])
 
386
        access = _KnitAccess(transport, 'filename', None, None, False, False)
 
387
        data = _KnitData(access=access)
 
388
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
 
389
 
 
390
        contents = data.read_records(records)
 
391
        self.assertEqual({'rev-id-1':(['foo\n', 'bar\n'], sha1sum)}, contents)
 
392
 
 
393
        raw_contents = list(data.read_records_iter_raw(records))
 
394
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
395
 
 
396
    def test_not_enough_lines(self):
 
397
        sha1sum = sha.new('foo\n').hexdigest()
 
398
        # record says 2 lines data says 1
 
399
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
400
                                        'foo\n'
 
401
                                        'end rev-id-1\n'
 
402
                                        % (sha1sum,))
 
403
        transport = MockTransport([gz_txt])
 
404
        access = _KnitAccess(transport, 'filename', None, None, False, False)
 
405
        data = _KnitData(access=access)
 
406
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
 
407
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
408
 
 
409
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
410
        raw_contents = list(data.read_records_iter_raw(records))
 
411
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
412
 
 
413
    def test_too_many_lines(self):
 
414
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
415
        # record says 1 lines data says 2
 
416
        gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
 
417
                                        'foo\n'
 
418
                                        'bar\n'
 
419
                                        'end rev-id-1\n'
 
420
                                        % (sha1sum,))
 
421
        transport = MockTransport([gz_txt])
 
422
        access = _KnitAccess(transport, 'filename', None, None, False, False)
 
423
        data = _KnitData(access=access)
 
424
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
 
425
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
426
 
 
427
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
428
        raw_contents = list(data.read_records_iter_raw(records))
 
429
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
430
 
 
431
    def test_mismatched_version_id(self):
 
432
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
433
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
434
                                        'foo\n'
 
435
                                        'bar\n'
 
436
                                        'end rev-id-1\n'
 
437
                                        % (sha1sum,))
 
438
        transport = MockTransport([gz_txt])
 
439
        access = _KnitAccess(transport, 'filename', None, None, False, False)
 
440
        data = _KnitData(access=access)
 
441
        # We are asking for rev-id-2, but the data is rev-id-1
 
442
        records = [('rev-id-2', (None, 0, len(gz_txt)))]
 
443
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
444
 
 
445
        # read_records_iter_raw will notice if we request the wrong version.
 
446
        self.assertRaises(errors.KnitCorrupt, list,
 
447
                          data.read_records_iter_raw(records))
 
448
 
 
449
    def test_uncompressed_data(self):
 
450
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
451
        txt = ('version rev-id-1 2 %s\n'
 
452
               'foo\n'
 
453
               'bar\n'
 
454
               'end rev-id-1\n'
 
455
               % (sha1sum,))
 
456
        transport = MockTransport([txt])
 
457
        access = _KnitAccess(transport, 'filename', None, None, False, False)
 
458
        data = _KnitData(access=access)
 
459
        records = [('rev-id-1', (None, 0, len(txt)))]
 
460
 
 
461
        # We don't have valid gzip data ==> corrupt
 
462
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
463
 
 
464
        # read_records_iter_raw will notice the bad data
 
465
        self.assertRaises(errors.KnitCorrupt, list,
 
466
                          data.read_records_iter_raw(records))
 
467
 
 
468
    def test_corrupted_data(self):
 
469
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
470
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
471
                                        'foo\n'
 
472
                                        'bar\n'
 
473
                                        'end rev-id-1\n'
 
474
                                        % (sha1sum,))
 
475
        # Change 2 bytes in the middle to \xff
 
476
        gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
 
477
        transport = MockTransport([gz_txt])
 
478
        access = _KnitAccess(transport, 'filename', None, None, False, False)
 
479
        data = _KnitData(access=access)
 
480
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
 
481
 
 
482
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
483
 
 
484
        # read_records_iter_raw will notice if we request the wrong version.
 
485
        self.assertRaises(errors.KnitCorrupt, list,
 
486
                          data.read_records_iter_raw(records))
 
487
 
 
488
 
 
489
class LowLevelKnitIndexTests(TestCase):
 
490
 
 
491
    def get_knit_index(self, *args, **kwargs):
 
492
        orig = knit._load_data
 
493
        def reset():
 
494
            knit._load_data = orig
 
495
        self.addCleanup(reset)
 
496
        from bzrlib._knit_load_data_py import _load_data_py
 
497
        knit._load_data = _load_data_py
 
498
        return _KnitIndex(get_scope=lambda:None, *args, **kwargs)
 
499
 
 
500
    def test_no_such_file(self):
 
501
        transport = MockTransport()
 
502
 
 
503
        self.assertRaises(NoSuchFile, self.get_knit_index,
 
504
                          transport, "filename", "r")
 
505
        self.assertRaises(NoSuchFile, self.get_knit_index,
 
506
                          transport, "filename", "w", create=False)
 
507
 
 
508
    def test_create_file(self):
 
509
        transport = MockTransport()
 
510
 
 
511
        index = self.get_knit_index(transport, "filename", "w",
 
512
            file_mode="wb", create=True)
 
513
        self.assertEqual(
 
514
                ("put_bytes_non_atomic",
 
515
                    ("filename", index.HEADER), {"mode": "wb"}),
 
516
                transport.calls.pop(0))
 
517
 
 
518
    def test_delay_create_file(self):
 
519
        transport = MockTransport()
 
520
 
 
521
        index = self.get_knit_index(transport, "filename", "w",
 
522
            create=True, file_mode="wb", create_parent_dir=True,
 
523
            delay_create=True, dir_mode=0777)
 
524
        self.assertEqual([], transport.calls)
 
525
 
 
526
        index.add_versions([])
 
527
        name, (filename, f), kwargs = transport.calls.pop(0)
 
528
        self.assertEqual("put_file_non_atomic", name)
 
529
        self.assertEqual(
 
530
            {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
 
531
            kwargs)
 
532
        self.assertEqual("filename", filename)
 
533
        self.assertEqual(index.HEADER, f.read())
 
534
 
 
535
        index.add_versions([])
 
536
        self.assertEqual(("append_bytes", ("filename", ""), {}),
 
537
            transport.calls.pop(0))
 
538
 
 
539
    def test_read_utf8_version_id(self):
 
540
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
541
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
542
        transport = MockTransport([
 
543
            _KnitIndex.HEADER,
 
544
            '%s option 0 1 :' % (utf8_revision_id,)
 
545
            ])
 
546
        index = self.get_knit_index(transport, "filename", "r")
 
547
        # _KnitIndex is a private class, and deals in utf8 revision_ids, not
 
548
        # Unicode revision_ids.
 
549
        self.assertTrue(index.has_version(utf8_revision_id))
 
550
        self.assertFalse(index.has_version(unicode_revision_id))
 
551
 
 
552
    def test_read_utf8_parents(self):
 
553
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
554
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
555
        transport = MockTransport([
 
556
            _KnitIndex.HEADER,
 
557
            "version option 0 1 .%s :" % (utf8_revision_id,)
 
558
            ])
 
559
        index = self.get_knit_index(transport, "filename", "r")
 
560
        self.assertEqual((utf8_revision_id,),
 
561
            index.get_parents_with_ghosts("version"))
 
562
 
 
563
    def test_read_ignore_corrupted_lines(self):
 
564
        transport = MockTransport([
 
565
            _KnitIndex.HEADER,
 
566
            "corrupted",
 
567
            "corrupted options 0 1 .b .c ",
 
568
            "version options 0 1 :"
 
569
            ])
 
570
        index = self.get_knit_index(transport, "filename", "r")
 
571
        self.assertEqual(1, index.num_versions())
 
572
        self.assertTrue(index.has_version("version"))
 
573
 
 
574
    def test_read_corrupted_header(self):
 
575
        transport = MockTransport(['not a bzr knit index header\n'])
 
576
        self.assertRaises(KnitHeaderError,
 
577
            self.get_knit_index, transport, "filename", "r")
 
578
 
 
579
    def test_read_duplicate_entries(self):
 
580
        transport = MockTransport([
 
581
            _KnitIndex.HEADER,
 
582
            "parent options 0 1 :",
 
583
            "version options1 0 1 0 :",
 
584
            "version options2 1 2 .other :",
 
585
            "version options3 3 4 0 .other :"
 
586
            ])
 
587
        index = self.get_knit_index(transport, "filename", "r")
 
588
        self.assertEqual(2, index.num_versions())
 
589
        # check that the index used is the first one written. (Specific
 
590
        # to KnitIndex style indices.
 
591
        self.assertEqual("1", index._version_list_to_index(["version"]))
 
592
        self.assertEqual((None, 3, 4), index.get_position("version"))
 
593
        self.assertEqual(["options3"], index.get_options("version"))
 
594
        self.assertEqual(("parent", "other"),
 
595
            index.get_parents_with_ghosts("version"))
 
596
 
 
597
    def test_read_compressed_parents(self):
 
598
        transport = MockTransport([
 
599
            _KnitIndex.HEADER,
 
600
            "a option 0 1 :",
 
601
            "b option 0 1 0 :",
 
602
            "c option 0 1 1 0 :",
 
603
            ])
 
604
        index = self.get_knit_index(transport, "filename", "r")
 
605
        self.assertEqual({"b":("a",), "c":("b", "a")},
 
606
            index.get_parent_map(["b", "c"]))
 
607
 
 
608
    def test_write_utf8_version_id(self):
 
609
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
610
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
611
        transport = MockTransport([
 
612
            _KnitIndex.HEADER
 
613
            ])
 
614
        index = self.get_knit_index(transport, "filename", "r")
 
615
        index.add_version(utf8_revision_id, ["option"], (None, 0, 1), [])
 
616
        self.assertEqual(("append_bytes", ("filename",
 
617
            "\n%s option 0 1  :" % (utf8_revision_id,)),
 
618
            {}),
 
619
            transport.calls.pop(0))
 
620
 
 
621
    def test_write_utf8_parents(self):
 
622
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
623
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
624
        transport = MockTransport([
 
625
            _KnitIndex.HEADER
 
626
            ])
 
627
        index = self.get_knit_index(transport, "filename", "r")
 
628
        index.add_version("version", ["option"], (None, 0, 1), [utf8_revision_id])
 
629
        self.assertEqual(("append_bytes", ("filename",
 
630
            "\nversion option 0 1 .%s :" % (utf8_revision_id,)),
 
631
            {}),
 
632
            transport.calls.pop(0))
 
633
 
 
634
    def test_get_ancestry(self):
 
635
        transport = MockTransport([
 
636
            _KnitIndex.HEADER,
 
637
            "a option 0 1 :",
 
638
            "b option 0 1 0 .e :",
 
639
            "c option 0 1 1 0 :",
 
640
            "d option 0 1 2 .f :"
 
641
            ])
 
642
        index = self.get_knit_index(transport, "filename", "r")
 
643
 
 
644
        self.assertEqual([], index.get_ancestry([]))
 
645
        self.assertEqual(["a"], index.get_ancestry(["a"]))
 
646
        self.assertEqual(["a", "b"], index.get_ancestry(["b"]))
 
647
        self.assertEqual(["a", "b", "c"], index.get_ancestry(["c"]))
 
648
        self.assertEqual(["a", "b", "c", "d"], index.get_ancestry(["d"]))
 
649
        self.assertEqual(["a", "b"], index.get_ancestry(["a", "b"]))
 
650
        self.assertEqual(["a", "b", "c"], index.get_ancestry(["a", "c"]))
 
651
 
 
652
        self.assertRaises(RevisionNotPresent, index.get_ancestry, ["e"])
 
653
 
 
654
    def test_get_ancestry_with_ghosts(self):
 
655
        transport = MockTransport([
 
656
            _KnitIndex.HEADER,
 
657
            "a option 0 1 :",
 
658
            "b option 0 1 0 .e :",
 
659
            "c option 0 1 0 .f .g :",
 
660
            "d option 0 1 2 .h .j .k :"
 
661
            ])
 
662
        index = self.get_knit_index(transport, "filename", "r")
 
663
 
 
664
        self.assertEqual([], index.get_ancestry_with_ghosts([]))
 
665
        self.assertEqual(["a"], index.get_ancestry_with_ghosts(["a"]))
 
666
        self.assertEqual(["a", "e", "b"],
 
667
            index.get_ancestry_with_ghosts(["b"]))
 
668
        self.assertEqual(["a", "g", "f", "c"],
 
669
            index.get_ancestry_with_ghosts(["c"]))
 
670
        self.assertEqual(["a", "g", "f", "c", "k", "j", "h", "d"],
 
671
            index.get_ancestry_with_ghosts(["d"]))
 
672
        self.assertEqual(["a", "e", "b"],
 
673
            index.get_ancestry_with_ghosts(["a", "b"]))
 
674
        self.assertEqual(["a", "g", "f", "c"],
 
675
            index.get_ancestry_with_ghosts(["a", "c"]))
 
676
        self.assertEqual(
 
677
            ["a", "g", "f", "c", "e", "b", "k", "j", "h", "d"],
 
678
            index.get_ancestry_with_ghosts(["b", "d"]))
 
679
 
 
680
        self.assertRaises(RevisionNotPresent,
 
681
            index.get_ancestry_with_ghosts, ["e"])
 
682
 
 
683
    def test_num_versions(self):
 
684
        transport = MockTransport([
 
685
            _KnitIndex.HEADER
 
686
            ])
 
687
        index = self.get_knit_index(transport, "filename", "r")
 
688
 
 
689
        self.assertEqual(0, index.num_versions())
 
690
        self.assertEqual(0, len(index))
 
691
 
 
692
        index.add_version("a", ["option"], (None, 0, 1), [])
 
693
        self.assertEqual(1, index.num_versions())
 
694
        self.assertEqual(1, len(index))
 
695
 
 
696
        index.add_version("a", ["option2"], (None, 1, 2), [])
 
697
        self.assertEqual(1, index.num_versions())
 
698
        self.assertEqual(1, len(index))
 
699
 
 
700
        index.add_version("b", ["option"], (None, 0, 1), [])
 
701
        self.assertEqual(2, index.num_versions())
 
702
        self.assertEqual(2, len(index))
 
703
 
 
704
    def test_get_versions(self):
 
705
        transport = MockTransport([
 
706
            _KnitIndex.HEADER
 
707
            ])
 
708
        index = self.get_knit_index(transport, "filename", "r")
 
709
 
 
710
        self.assertEqual([], index.get_versions())
 
711
 
 
712
        index.add_version("a", ["option"], (None, 0, 1), [])
 
713
        self.assertEqual(["a"], index.get_versions())
 
714
 
 
715
        index.add_version("a", ["option"], (None, 0, 1), [])
 
716
        self.assertEqual(["a"], index.get_versions())
 
717
 
 
718
        index.add_version("b", ["option"], (None, 0, 1), [])
 
719
        self.assertEqual(["a", "b"], index.get_versions())
 
720
 
 
721
    def test_add_version(self):
 
722
        transport = MockTransport([
 
723
            _KnitIndex.HEADER
 
724
            ])
 
725
        index = self.get_knit_index(transport, "filename", "r")
 
726
 
 
727
        index.add_version("a", ["option"], (None, 0, 1), ["b"])
 
728
        self.assertEqual(("append_bytes",
 
729
            ("filename", "\na option 0 1 .b :"),
 
730
            {}), transport.calls.pop(0))
 
731
        self.assertTrue(index.has_version("a"))
 
732
        self.assertEqual(1, index.num_versions())
 
733
        self.assertEqual((None, 0, 1), index.get_position("a"))
 
734
        self.assertEqual(["option"], index.get_options("a"))
 
735
        self.assertEqual(("b",), index.get_parents_with_ghosts("a"))
 
736
 
 
737
        index.add_version("a", ["opt"], (None, 1, 2), ["c"])
 
738
        self.assertEqual(("append_bytes",
 
739
            ("filename", "\na opt 1 2 .c :"),
 
740
            {}), transport.calls.pop(0))
 
741
        self.assertTrue(index.has_version("a"))
 
742
        self.assertEqual(1, index.num_versions())
 
743
        self.assertEqual((None, 1, 2), index.get_position("a"))
 
744
        self.assertEqual(["opt"], index.get_options("a"))
 
745
        self.assertEqual(("c",), index.get_parents_with_ghosts("a"))
 
746
 
 
747
        index.add_version("b", ["option"], (None, 2, 3), ["a"])
 
748
        self.assertEqual(("append_bytes",
 
749
            ("filename", "\nb option 2 3 0 :"),
 
750
            {}), transport.calls.pop(0))
 
751
        self.assertTrue(index.has_version("b"))
 
752
        self.assertEqual(2, index.num_versions())
 
753
        self.assertEqual((None, 2, 3), index.get_position("b"))
 
754
        self.assertEqual(["option"], index.get_options("b"))
 
755
        self.assertEqual(("a",), index.get_parents_with_ghosts("b"))
 
756
 
 
757
    def test_add_versions(self):
 
758
        transport = MockTransport([
 
759
            _KnitIndex.HEADER
 
760
            ])
 
761
        index = self.get_knit_index(transport, "filename", "r")
 
762
 
 
763
        index.add_versions([
 
764
            ("a", ["option"], (None, 0, 1), ["b"]),
 
765
            ("a", ["opt"], (None, 1, 2), ["c"]),
 
766
            ("b", ["option"], (None, 2, 3), ["a"])
 
767
            ])
 
768
        self.assertEqual(("append_bytes", ("filename",
 
769
            "\na option 0 1 .b :"
 
770
            "\na opt 1 2 .c :"
 
771
            "\nb option 2 3 0 :"
 
772
            ), {}), transport.calls.pop(0))
 
773
        self.assertTrue(index.has_version("a"))
 
774
        self.assertTrue(index.has_version("b"))
 
775
        self.assertEqual(2, index.num_versions())
 
776
        self.assertEqual((None, 1, 2), index.get_position("a"))
 
777
        self.assertEqual((None, 2, 3), index.get_position("b"))
 
778
        self.assertEqual(["opt"], index.get_options("a"))
 
779
        self.assertEqual(["option"], index.get_options("b"))
 
780
        self.assertEqual(("c",), index.get_parents_with_ghosts("a"))
 
781
        self.assertEqual(("a",), index.get_parents_with_ghosts("b"))
 
782
 
 
783
    def test_add_versions_random_id_is_accepted(self):
 
784
        transport = MockTransport([
 
785
            _KnitIndex.HEADER
 
786
            ])
 
787
        index = self.get_knit_index(transport, "filename", "r")
 
788
 
 
789
        index.add_versions([
 
790
            ("a", ["option"], (None, 0, 1), ["b"]),
 
791
            ("a", ["opt"], (None, 1, 2), ["c"]),
 
792
            ("b", ["option"], (None, 2, 3), ["a"])
 
793
            ], random_id=True)
 
794
 
 
795
    def test_delay_create_and_add_versions(self):
 
796
        transport = MockTransport()
 
797
 
 
798
        index = self.get_knit_index(transport, "filename", "w",
 
799
            create=True, file_mode="wb", create_parent_dir=True,
 
800
            delay_create=True, dir_mode=0777)
 
801
        self.assertEqual([], transport.calls)
 
802
 
 
803
        index.add_versions([
 
804
            ("a", ["option"], (None, 0, 1), ["b"]),
 
805
            ("a", ["opt"], (None, 1, 2), ["c"]),
 
806
            ("b", ["option"], (None, 2, 3), ["a"])
 
807
            ])
 
808
        name, (filename, f), kwargs = transport.calls.pop(0)
 
809
        self.assertEqual("put_file_non_atomic", name)
 
810
        self.assertEqual(
 
811
            {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
 
812
            kwargs)
 
813
        self.assertEqual("filename", filename)
 
814
        self.assertEqual(
 
815
            index.HEADER +
 
816
            "\na option 0 1 .b :"
 
817
            "\na opt 1 2 .c :"
 
818
            "\nb option 2 3 0 :",
 
819
            f.read())
 
820
 
 
821
    def test_has_version(self):
 
822
        transport = MockTransport([
 
823
            _KnitIndex.HEADER,
 
824
            "a option 0 1 :"
 
825
            ])
 
826
        index = self.get_knit_index(transport, "filename", "r")
 
827
 
 
828
        self.assertTrue(index.has_version("a"))
 
829
        self.assertFalse(index.has_version("b"))
 
830
 
 
831
    def test_get_position(self):
 
832
        transport = MockTransport([
 
833
            _KnitIndex.HEADER,
 
834
            "a option 0 1 :",
 
835
            "b option 1 2 :"
 
836
            ])
 
837
        index = self.get_knit_index(transport, "filename", "r")
 
838
 
 
839
        self.assertEqual((None, 0, 1), index.get_position("a"))
 
840
        self.assertEqual((None, 1, 2), index.get_position("b"))
 
841
 
 
842
    def test_get_method(self):
 
843
        transport = MockTransport([
 
844
            _KnitIndex.HEADER,
 
845
            "a fulltext,unknown 0 1 :",
 
846
            "b unknown,line-delta 1 2 :",
 
847
            "c bad 3 4 :"
 
848
            ])
 
849
        index = self.get_knit_index(transport, "filename", "r")
 
850
 
 
851
        self.assertEqual("fulltext", index.get_method("a"))
 
852
        self.assertEqual("line-delta", index.get_method("b"))
 
853
        self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
 
854
 
 
855
    def test_get_options(self):
 
856
        transport = MockTransport([
 
857
            _KnitIndex.HEADER,
 
858
            "a opt1 0 1 :",
 
859
            "b opt2,opt3 1 2 :"
 
860
            ])
 
861
        index = self.get_knit_index(transport, "filename", "r")
 
862
 
 
863
        self.assertEqual(["opt1"], index.get_options("a"))
 
864
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
 
865
 
 
866
    def test_get_parent_map(self):
 
867
        transport = MockTransport([
 
868
            _KnitIndex.HEADER,
 
869
            "a option 0 1 :",
 
870
            "b option 1 2 0 .c :",
 
871
            "c option 1 2 1 0 .e :"
 
872
            ])
 
873
        index = self.get_knit_index(transport, "filename", "r")
 
874
 
 
875
        self.assertEqual({
 
876
            "a":(),
 
877
            "b":("a", "c"),
 
878
            "c":("b", "a", "e"),
 
879
            }, index.get_parent_map(["a", "b", "c"]))
 
880
 
 
881
    def test_get_parents_with_ghosts(self):
 
882
        transport = MockTransport([
 
883
            _KnitIndex.HEADER,
 
884
            "a option 0 1 :",
 
885
            "b option 1 2 0 .c :",
 
886
            "c option 1 2 1 0 .e :"
 
887
            ])
 
888
        index = self.get_knit_index(transport, "filename", "r")
 
889
 
 
890
        self.assertEqual((), index.get_parents_with_ghosts("a"))
 
891
        self.assertEqual(("a", "c"), index.get_parents_with_ghosts("b"))
 
892
        self.assertEqual(("b", "a", "e"),
 
893
            index.get_parents_with_ghosts("c"))
 
894
 
 
895
    def test_check_versions_present(self):
 
896
        transport = MockTransport([
 
897
            _KnitIndex.HEADER,
 
898
            "a option 0 1 :",
 
899
            "b option 0 1 :"
 
900
            ])
 
901
        index = self.get_knit_index(transport, "filename", "r")
 
902
 
 
903
        check = index.check_versions_present
 
904
 
 
905
        check([])
 
906
        check(["a"])
 
907
        check(["b"])
 
908
        check(["a", "b"])
 
909
        self.assertRaises(RevisionNotPresent, check, ["c"])
 
910
        self.assertRaises(RevisionNotPresent, check, ["a", "b", "c"])
 
911
 
 
912
    def test_impossible_parent(self):
 
913
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
 
914
        transport = MockTransport([
 
915
            _KnitIndex.HEADER,
 
916
            "a option 0 1 :",
 
917
            "b option 0 1 4 :"  # We don't have a 4th record
 
918
            ])
 
919
        try:
 
920
            self.assertRaises(errors.KnitCorrupt,
 
921
                              self.get_knit_index, transport, 'filename', 'r')
 
922
        except TypeError, e:
 
923
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
924
                           ' not exceptions.IndexError')
 
925
                and sys.version_info[0:2] >= (2,5)):
 
926
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
927
                                  ' raising new style exceptions with python'
 
928
                                  ' >=2.5')
 
929
            else:
 
930
                raise
 
931
 
 
932
    def test_corrupted_parent(self):
 
933
        transport = MockTransport([
 
934
            _KnitIndex.HEADER,
 
935
            "a option 0 1 :",
 
936
            "b option 0 1 :",
 
937
            "c option 0 1 1v :", # Can't have a parent of '1v'
 
938
            ])
 
939
        try:
 
940
            self.assertRaises(errors.KnitCorrupt,
 
941
                              self.get_knit_index, transport, 'filename', 'r')
 
942
        except TypeError, e:
 
943
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
944
                           ' not exceptions.ValueError')
 
945
                and sys.version_info[0:2] >= (2,5)):
 
946
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
947
                                  ' raising new style exceptions with python'
 
948
                                  ' >=2.5')
 
949
            else:
 
950
                raise
 
951
 
 
952
    def test_corrupted_parent_in_list(self):
 
953
        transport = MockTransport([
 
954
            _KnitIndex.HEADER,
 
955
            "a option 0 1 :",
 
956
            "b option 0 1 :",
 
957
            "c option 0 1 1 v :", # Can't have a parent of 'v'
 
958
            ])
 
959
        try:
 
960
            self.assertRaises(errors.KnitCorrupt,
 
961
                              self.get_knit_index, transport, 'filename', 'r')
 
962
        except TypeError, e:
 
963
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
964
                           ' not exceptions.ValueError')
 
965
                and sys.version_info[0:2] >= (2,5)):
 
966
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
967
                                  ' raising new style exceptions with python'
 
968
                                  ' >=2.5')
 
969
            else:
 
970
                raise
 
971
 
 
972
    def test_invalid_position(self):
 
973
        transport = MockTransport([
 
974
            _KnitIndex.HEADER,
 
975
            "a option 1v 1 :",
 
976
            ])
 
977
        try:
 
978
            self.assertRaises(errors.KnitCorrupt,
 
979
                              self.get_knit_index, transport, 'filename', 'r')
 
980
        except TypeError, e:
 
981
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
982
                           ' not exceptions.ValueError')
 
983
                and sys.version_info[0:2] >= (2,5)):
 
984
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
985
                                  ' raising new style exceptions with python'
 
986
                                  ' >=2.5')
 
987
            else:
 
988
                raise
 
989
 
 
990
    def test_invalid_size(self):
 
991
        transport = MockTransport([
 
992
            _KnitIndex.HEADER,
 
993
            "a option 1 1v :",
 
994
            ])
 
995
        try:
 
996
            self.assertRaises(errors.KnitCorrupt,
 
997
                              self.get_knit_index, transport, 'filename', 'r')
 
998
        except TypeError, e:
 
999
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1000
                           ' not exceptions.ValueError')
 
1001
                and sys.version_info[0:2] >= (2,5)):
 
1002
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1003
                                  ' raising new style exceptions with python'
 
1004
                                  ' >=2.5')
 
1005
            else:
 
1006
                raise
 
1007
 
 
1008
    def test_short_line(self):
 
1009
        transport = MockTransport([
 
1010
            _KnitIndex.HEADER,
 
1011
            "a option 0 10  :",
 
1012
            "b option 10 10 0", # This line isn't terminated, ignored
 
1013
            ])
 
1014
        index = self.get_knit_index(transport, "filename", "r")
 
1015
        self.assertEqual(['a'], index.get_versions())
 
1016
 
 
1017
    def test_skip_incomplete_record(self):
 
1018
        # A line with bogus data should just be skipped
 
1019
        transport = MockTransport([
 
1020
            _KnitIndex.HEADER,
 
1021
            "a option 0 10  :",
 
1022
            "b option 10 10 0", # This line isn't terminated, ignored
 
1023
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1024
            ])
 
1025
        index = self.get_knit_index(transport, "filename", "r")
 
1026
        self.assertEqual(['a', 'c'], index.get_versions())
 
1027
 
 
1028
    def test_trailing_characters(self):
 
1029
        # A line with bogus data should just be skipped
 
1030
        transport = MockTransport([
 
1031
            _KnitIndex.HEADER,
 
1032
            "a option 0 10  :",
 
1033
            "b option 10 10 0 :a", # This line has extra trailing characters
 
1034
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1035
            ])
 
1036
        index = self.get_knit_index(transport, "filename", "r")
 
1037
        self.assertEqual(['a', 'c'], index.get_versions())
 
1038
 
 
1039
 
 
1040
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
 
1041
 
 
1042
    _test_needs_features = [CompiledKnitFeature]
 
1043
 
 
1044
    def get_knit_index(self, *args, **kwargs):
 
1045
        orig = knit._load_data
 
1046
        def reset():
 
1047
            knit._load_data = orig
 
1048
        self.addCleanup(reset)
 
1049
        from bzrlib._knit_load_data_c import _load_data_c
 
1050
        knit._load_data = _load_data_c
 
1051
        return _KnitIndex(get_scope=lambda:None, *args, **kwargs)
 
1052
 
 
1053
 
 
1054
class KnitTests(TestCaseWithTransport):
 
1055
    """Class containing knit test helper routines."""
 
1056
 
 
1057
    def make_test_knit(self, annotate=False, delay_create=False, index=None,
 
1058
                       name='test', delta=True, access_mode='w'):
 
1059
        if not annotate:
 
1060
            factory = KnitPlainFactory()
 
1061
        else:
 
1062
            factory = None
 
1063
        if index is None:
 
1064
            index = _KnitIndex(get_transport('.'), name + INDEX_SUFFIX,
 
1065
                access_mode, create=True, file_mode=None,
 
1066
                create_parent_dir=False, delay_create=delay_create,
 
1067
                dir_mode=None, get_scope=lambda:None)
 
1068
        access = _KnitAccess(get_transport('.'), name + DATA_SUFFIX, None,
 
1069
            None, delay_create, False)
 
1070
        return KnitVersionedFile(name, get_transport('.'), factory=factory,
 
1071
            create=True, delay_create=delay_create, index=index,
 
1072
            access_method=access, delta=delta)
 
1073
 
 
1074
    def assertRecordContentEqual(self, knit, version_id, candidate_content):
 
1075
        """Assert that some raw record content matches the raw record content
 
1076
        for a particular version_id in the given knit.
 
1077
        """
 
1078
        index_memo = knit._index.get_position(version_id)
 
1079
        record = (version_id, index_memo)
 
1080
        [(_, expected_content)] = list(knit._data.read_records_iter_raw([record]))
 
1081
        self.assertEqual(expected_content, candidate_content)
 
1082
 
 
1083
 
 
1084
class BasicKnitTests(KnitTests):
 
1085
 
 
1086
    def add_stock_one_and_one_a(self, k):
 
1087
        k.add_lines('text-1', [], split_lines(TEXT_1))
 
1088
        k.add_lines('text-1a', ['text-1'], split_lines(TEXT_1A))
 
1089
 
 
1090
    def test_knit_constructor(self):
 
1091
        """Construct empty k"""
 
1092
        self.make_test_knit()
 
1093
 
 
1094
    def test_make_explicit_index(self):
 
1095
        """We can supply an index to use."""
 
1096
        knit = KnitVersionedFile('test', get_transport('.'),
 
1097
            index='strangelove', access_method="a")
 
1098
        self.assertEqual(knit._index, 'strangelove')
 
1099
 
 
1100
    def test_knit_add(self):
 
1101
        """Store one text in knit and retrieve"""
 
1102
        k = self.make_test_knit()
 
1103
        k.add_lines('text-1', [], split_lines(TEXT_1))
 
1104
        self.assertTrue(k.has_version('text-1'))
 
1105
        self.assertEqualDiff(''.join(k.get_lines('text-1')), TEXT_1)
 
1106
 
 
1107
    def test_newline_empty_lines(self):
 
1108
        # ensure that ["\n"] round trips ok.
 
1109
        knit = self.make_test_knit()
 
1110
        knit.add_lines('a', [], ["\n"])
 
1111
        knit.add_lines_with_ghosts('b', [], ["\n"])
 
1112
        self.assertEqual(["\n"], knit.get_lines('a'))
 
1113
        self.assertEqual(["\n"], knit.get_lines('b'))
 
1114
        self.assertEqual(['fulltext'], knit._index.get_options('a'))
 
1115
        self.assertEqual(['fulltext'], knit._index.get_options('b'))
 
1116
        knit.add_lines('c', ['a'], ["\n"])
 
1117
        knit.add_lines_with_ghosts('d', ['b'], ["\n"])
 
1118
        self.assertEqual(["\n"], knit.get_lines('c'))
 
1119
        self.assertEqual(["\n"], knit.get_lines('d'))
 
1120
        self.assertEqual(['line-delta'], knit._index.get_options('c'))
 
1121
        self.assertEqual(['line-delta'], knit._index.get_options('d'))
 
1122
 
 
1123
    def test_empty_lines(self):
 
1124
        # bizarrely, [] is not listed as having no-eol. 
 
1125
        knit = self.make_test_knit()
 
1126
        knit.add_lines('a', [], [])
 
1127
        knit.add_lines_with_ghosts('b', [], [])
 
1128
        self.assertEqual([], knit.get_lines('a'))
 
1129
        self.assertEqual([], knit.get_lines('b'))
 
1130
        self.assertEqual(['fulltext'], knit._index.get_options('a'))
 
1131
        self.assertEqual(['fulltext'], knit._index.get_options('b'))
 
1132
        knit.add_lines('c', ['a'], [])
 
1133
        knit.add_lines_with_ghosts('d', ['b'], [])
 
1134
        self.assertEqual([], knit.get_lines('c'))
 
1135
        self.assertEqual([], knit.get_lines('d'))
 
1136
        self.assertEqual(['line-delta'], knit._index.get_options('c'))
 
1137
        self.assertEqual(['line-delta'], knit._index.get_options('d'))
 
1138
 
 
1139
    def test_knit_reload(self):
 
1140
        # test that the content in a reloaded knit is correct
 
1141
        k = self.make_test_knit()
 
1142
        k.add_lines('text-1', [], split_lines(TEXT_1))
 
1143
        del k
 
1144
        k2 = make_file_knit('test', get_transport('.'), access_mode='r',
 
1145
            factory=KnitPlainFactory(), create=True)
 
1146
        self.assertTrue(k2.has_version('text-1'))
 
1147
        self.assertEqualDiff(''.join(k2.get_lines('text-1')), TEXT_1)
 
1148
 
 
1149
    def test_knit_several(self):
 
1150
        """Store several texts in a knit"""
 
1151
        k = self.make_test_knit()
 
1152
        k.add_lines('text-1', [], split_lines(TEXT_1))
 
1153
        k.add_lines('text-2', [], split_lines(TEXT_2))
 
1154
        self.assertEqualDiff(''.join(k.get_lines('text-1')), TEXT_1)
 
1155
        self.assertEqualDiff(''.join(k.get_lines('text-2')), TEXT_2)
 
1156
        
 
1157
    def test_repeated_add(self):
 
1158
        """Knit traps attempt to replace existing version"""
 
1159
        k = self.make_test_knit()
 
1160
        k.add_lines('text-1', [], split_lines(TEXT_1))
 
1161
        self.assertRaises(RevisionAlreadyPresent, 
 
1162
                k.add_lines,
 
1163
                'text-1', [], split_lines(TEXT_1))
 
1164
 
 
1165
    def test_empty(self):
 
1166
        k = self.make_test_knit(True)
 
1167
        k.add_lines('text-1', [], [])
 
1168
        self.assertEquals(k.get_lines('text-1'), [])
 
1169
 
 
1170
    def test_incomplete(self):
 
1171
        """Test if texts without a ending line-end can be inserted and
 
1172
        extracted."""
 
1173
        k = make_file_knit('test', get_transport('.'), delta=False, create=True)
 
1174
        k.add_lines('text-1', [], ['a\n',    'b'  ])
 
1175
        k.add_lines('text-2', ['text-1'], ['a\rb\n', 'b\n'])
 
1176
        # reopening ensures maximum room for confusion
 
1177
        k = make_file_knit('test', get_transport('.'), delta=False, create=True)
 
1178
        self.assertEquals(k.get_lines('text-1'), ['a\n',    'b'  ])
 
1179
        self.assertEquals(k.get_lines('text-2'), ['a\rb\n', 'b\n'])
 
1180
 
 
1181
    def test_delta(self):
 
1182
        """Expression of knit delta as lines"""
 
1183
        k = self.make_test_knit()
 
1184
        td = list(line_delta(TEXT_1.splitlines(True),
 
1185
                             TEXT_1A.splitlines(True)))
 
1186
        self.assertEqualDiff(''.join(td), delta_1_1a)
 
1187
        out = apply_line_delta(TEXT_1.splitlines(True), td)
 
1188
        self.assertEqualDiff(''.join(out), TEXT_1A)
 
1189
 
 
1190
    def test_add_with_parents(self):
 
1191
        """Store in knit with parents"""
 
1192
        k = self.make_test_knit()
 
1193
        self.add_stock_one_and_one_a(k)
 
1194
        self.assertEqual({'text-1':(), 'text-1a':('text-1',)},
 
1195
            k.get_parent_map(['text-1', 'text-1a']))
 
1196
 
 
1197
    def test_ancestry(self):
 
1198
        """Store in knit with parents"""
 
1199
        k = self.make_test_knit()
 
1200
        self.add_stock_one_and_one_a(k)
 
1201
        self.assertEquals(set(k.get_ancestry(['text-1a'])), set(['text-1a', 'text-1']))
 
1202
 
 
1203
    def test_add_delta(self):
 
1204
        """Store in knit with parents"""
 
1205
        k = self.make_test_knit(annotate=False)
 
1206
        self.add_stock_one_and_one_a(k)
 
1207
        k.clear_cache()
 
1208
        self.assertEqualDiff(''.join(k.get_lines('text-1a')), TEXT_1A)
 
1209
 
 
1210
    def test_add_delta_knit_graph_index(self):
 
1211
        """Does adding work with a KnitGraphIndex."""
 
1212
        index = InMemoryGraphIndex(2)
 
1213
        knit_index = KnitGraphIndex(index, add_callback=index.add_nodes,
 
1214
            deltas=True)
 
1215
        k = self.make_test_knit(annotate=True, index=knit_index)
 
1216
        self.add_stock_one_and_one_a(k)
 
1217
        k.clear_cache()
 
1218
        self.assertEqualDiff(''.join(k.get_lines('text-1a')), TEXT_1A)
 
1219
        # check the index had the right data added.
 
1220
        self.assertEqual(set([
 
1221
            (index, ('text-1', ), ' 0 127', ((), ())),
 
1222
            (index, ('text-1a', ), ' 127 140', ((('text-1', ),), (('text-1', ),))),
 
1223
            ]), set(index.iter_all_entries()))
 
1224
        # we should not have a .kndx file
 
1225
        self.assertFalse(get_transport('.').has('test.kndx'))
 
1226
 
 
1227
    def test_annotate(self):
 
1228
        """Annotations"""
 
1229
        k = self.make_test_knit(annotate=True, name='knit')
 
1230
        self.insert_and_test_small_annotate(k)
 
1231
 
 
1232
    def insert_and_test_small_annotate(self, k):
 
1233
        """test annotation with k works correctly."""
 
1234
        k.add_lines('text-1', [], ['a\n', 'b\n'])
 
1235
        k.add_lines('text-2', ['text-1'], ['a\n', 'c\n'])
 
1236
 
 
1237
        origins = k.annotate('text-2')
 
1238
        self.assertEquals(origins[0], ('text-1', 'a\n'))
 
1239
        self.assertEquals(origins[1], ('text-2', 'c\n'))
 
1240
 
 
1241
    def test_annotate_fulltext(self):
 
1242
        """Annotations"""
 
1243
        k = self.make_test_knit(annotate=True, name='knit', delta=False)
 
1244
        self.insert_and_test_small_annotate(k)
 
1245
 
 
1246
    def test_annotate_merge_1(self):
 
1247
        k = self.make_test_knit(True)
 
1248
        k.add_lines('text-a1', [], ['a\n', 'b\n'])
 
1249
        k.add_lines('text-a2', [], ['d\n', 'c\n'])
 
1250
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['d\n', 'b\n'])
 
1251
        origins = k.annotate('text-am')
 
1252
        self.assertEquals(origins[0], ('text-a2', 'd\n'))
 
1253
        self.assertEquals(origins[1], ('text-a1', 'b\n'))
 
1254
 
 
1255
    def test_annotate_merge_2(self):
 
1256
        k = self.make_test_knit(True)
 
1257
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
 
1258
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
 
1259
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['a\n', 'y\n', 'c\n'])
 
1260
        origins = k.annotate('text-am')
 
1261
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
 
1262
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
 
1263
        self.assertEquals(origins[2], ('text-a1', 'c\n'))
 
1264
 
 
1265
    def test_annotate_merge_9(self):
 
1266
        k = self.make_test_knit(True)
 
1267
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
 
1268
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
 
1269
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['k\n', 'y\n', 'c\n'])
 
1270
        origins = k.annotate('text-am')
 
1271
        self.assertEquals(origins[0], ('text-am', 'k\n'))
 
1272
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
 
1273
        self.assertEquals(origins[2], ('text-a1', 'c\n'))
 
1274
 
 
1275
    def test_annotate_merge_3(self):
 
1276
        k = self.make_test_knit(True)
 
1277
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
 
1278
        k.add_lines('text-a2', [] ,['x\n', 'y\n', 'z\n'])
 
1279
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['k\n', 'y\n', 'z\n'])
 
1280
        origins = k.annotate('text-am')
 
1281
        self.assertEquals(origins[0], ('text-am', 'k\n'))
 
1282
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
 
1283
        self.assertEquals(origins[2], ('text-a2', 'z\n'))
 
1284
 
 
1285
    def test_annotate_merge_4(self):
 
1286
        k = self.make_test_knit(True)
 
1287
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
 
1288
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
 
1289
        k.add_lines('text-a3', ['text-a1'], ['a\n', 'b\n', 'p\n'])
 
1290
        k.add_lines('text-am', ['text-a2', 'text-a3'], ['a\n', 'b\n', 'z\n'])
 
1291
        origins = k.annotate('text-am')
 
1292
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
 
1293
        self.assertEquals(origins[1], ('text-a1', 'b\n'))
 
1294
        self.assertEquals(origins[2], ('text-a2', 'z\n'))
 
1295
 
 
1296
    def test_annotate_merge_5(self):
 
1297
        k = self.make_test_knit(True)
 
1298
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
 
1299
        k.add_lines('text-a2', [], ['d\n', 'e\n', 'f\n'])
 
1300
        k.add_lines('text-a3', [], ['x\n', 'y\n', 'z\n'])
 
1301
        k.add_lines('text-am',
 
1302
                    ['text-a1', 'text-a2', 'text-a3'],
 
1303
                    ['a\n', 'e\n', 'z\n'])
 
1304
        origins = k.annotate('text-am')
 
1305
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
 
1306
        self.assertEquals(origins[1], ('text-a2', 'e\n'))
 
1307
        self.assertEquals(origins[2], ('text-a3', 'z\n'))
 
1308
 
 
1309
    def test_annotate_file_cherry_pick(self):
 
1310
        k = self.make_test_knit(True)
 
1311
        k.add_lines('text-1', [], ['a\n', 'b\n', 'c\n'])
 
1312
        k.add_lines('text-2', ['text-1'], ['d\n', 'e\n', 'f\n'])
 
1313
        k.add_lines('text-3', ['text-2', 'text-1'], ['a\n', 'b\n', 'c\n'])
 
1314
        origins = k.annotate('text-3')
 
1315
        self.assertEquals(origins[0], ('text-1', 'a\n'))
 
1316
        self.assertEquals(origins[1], ('text-1', 'b\n'))
 
1317
        self.assertEquals(origins[2], ('text-1', 'c\n'))
 
1318
 
 
1319
    def _test_join_with_factories(self, k1_factory, k2_factory):
 
1320
        k1 = make_file_knit('test1', get_transport('.'), factory=k1_factory, create=True)
 
1321
        k1.add_lines('text-a', [], ['a1\n', 'a2\n', 'a3\n'])
 
1322
        k1.add_lines('text-b', ['text-a'], ['a1\n', 'b2\n', 'a3\n'])
 
1323
        k1.add_lines('text-c', [], ['c1\n', 'c2\n', 'c3\n'])
 
1324
        k1.add_lines('text-d', ['text-c'], ['c1\n', 'd2\n', 'd3\n'])
 
1325
        k1.add_lines('text-m', ['text-b', 'text-d'], ['a1\n', 'b2\n', 'd3\n'])
 
1326
        k2 = make_file_knit('test2', get_transport('.'), factory=k2_factory, create=True)
 
1327
        count = k2.join(k1, version_ids=['text-m'])
 
1328
        self.assertEquals(count, 5)
 
1329
        self.assertTrue(k2.has_version('text-a'))
 
1330
        self.assertTrue(k2.has_version('text-c'))
 
1331
        origins = k2.annotate('text-m')
 
1332
        self.assertEquals(origins[0], ('text-a', 'a1\n'))
 
1333
        self.assertEquals(origins[1], ('text-b', 'b2\n'))
 
1334
        self.assertEquals(origins[2], ('text-d', 'd3\n'))
 
1335
 
 
1336
    def test_knit_join_plain_to_plain(self):
 
1337
        """Test joining a plain knit with a plain knit."""
 
1338
        self._test_join_with_factories(KnitPlainFactory(), KnitPlainFactory())
 
1339
 
 
1340
    def test_knit_join_anno_to_anno(self):
 
1341
        """Test joining an annotated knit with an annotated knit."""
 
1342
        self._test_join_with_factories(None, None)
 
1343
 
 
1344
    def test_knit_join_anno_to_plain(self):
 
1345
        """Test joining an annotated knit with a plain knit."""
 
1346
        self._test_join_with_factories(None, KnitPlainFactory())
 
1347
 
 
1348
    def test_knit_join_plain_to_anno(self):
 
1349
        """Test joining a plain knit with an annotated knit."""
 
1350
        self._test_join_with_factories(KnitPlainFactory(), None)
 
1351
 
 
1352
    def test_reannotate(self):
 
1353
        k1 = make_file_knit('knit1', get_transport('.'),
 
1354
                               factory=KnitAnnotateFactory(), create=True)
 
1355
        # 0
 
1356
        k1.add_lines('text-a', [], ['a\n', 'b\n'])
 
1357
        # 1
 
1358
        k1.add_lines('text-b', ['text-a'], ['a\n', 'c\n'])
 
1359
 
 
1360
        k2 = make_file_knit('test2', get_transport('.'),
 
1361
                               factory=KnitAnnotateFactory(), create=True)
 
1362
        k2.join(k1, version_ids=['text-b'])
 
1363
 
 
1364
        # 2
 
1365
        k1.add_lines('text-X', ['text-b'], ['a\n', 'b\n'])
 
1366
        # 2
 
1367
        k2.add_lines('text-c', ['text-b'], ['z\n', 'c\n'])
 
1368
        # 3
 
1369
        k2.add_lines('text-Y', ['text-b'], ['b\n', 'c\n'])
 
1370
 
 
1371
        # test-c will have index 3
 
1372
        k1.join(k2, version_ids=['text-c'])
 
1373
 
 
1374
        lines = k1.get_lines('text-c')
 
1375
        self.assertEquals(lines, ['z\n', 'c\n'])
 
1376
 
 
1377
        origins = k1.annotate('text-c')
 
1378
        self.assertEquals(origins[0], ('text-c', 'z\n'))
 
1379
        self.assertEquals(origins[1], ('text-b', 'c\n'))
 
1380
 
 
1381
    def test_get_line_delta_texts(self):
 
1382
        """Make sure we can call get_texts on text with reused line deltas"""
 
1383
        k1 = make_file_knit('test1', get_transport('.'),
 
1384
            factory=KnitPlainFactory(), create=True)
 
1385
        for t in range(3):
 
1386
            if t == 0:
 
1387
                parents = []
 
1388
            else:
 
1389
                parents = ['%d' % (t-1)]
 
1390
            k1.add_lines('%d' % t, parents, ['hello\n'] * t)
 
1391
        k1.get_texts(('%d' % t) for t in range(3))
 
1392
        
 
1393
    def test_iter_lines_reads_in_order(self):
 
1394
        instrumented_t = get_transport('trace+memory:///')
 
1395
        k1 = make_file_knit('id', instrumented_t, create=True, delta=True)
 
1396
        self.assertEqual([('get', 'id.kndx',)], instrumented_t._activity)
 
1397
        # add texts with no required ordering
 
1398
        k1.add_lines('base', [], ['text\n'])
 
1399
        k1.add_lines('base2', [], ['text2\n'])
 
1400
        k1.clear_cache()
 
1401
        # clear the logged activity, but preserve the list instance in case of
 
1402
        # clones pointing at it.
 
1403
        del instrumented_t._activity[:]
 
1404
        # request a last-first iteration
 
1405
        results = list(k1.iter_lines_added_or_present_in_versions(
 
1406
            ['base2', 'base']))
 
1407
        self.assertEqual(
 
1408
            [('readv', 'id.knit', [(0, 87), (87, 89)], False, None)],
 
1409
            instrumented_t._activity)
 
1410
        self.assertEqual([('text\n', 'base'), ('text2\n', 'base2')], results)
 
1411
 
 
1412
    def test_knit_format(self):
 
1413
        # this tests that a new knit index file has the expected content
 
1414
        # and that is writes the data we expect as records are added.
 
1415
        knit = self.make_test_knit(True)
 
1416
        # Now knit files are not created until we first add data to them
 
1417
        self.assertFileEqual("# bzr knit index 8\n", 'test.kndx')
 
1418
        knit.add_lines_with_ghosts('revid', ['a_ghost'], ['a\n'])
 
1419
        self.assertFileEqual(
 
1420
            "# bzr knit index 8\n"
 
1421
            "\n"
 
1422
            "revid fulltext 0 84 .a_ghost :",
 
1423
            'test.kndx')
 
1424
        knit.add_lines_with_ghosts('revid2', ['revid'], ['a\n'])
 
1425
        self.assertFileEqual(
 
1426
            "# bzr knit index 8\n"
 
1427
            "\nrevid fulltext 0 84 .a_ghost :"
 
1428
            "\nrevid2 line-delta 84 82 0 :",
 
1429
            'test.kndx')
 
1430
        # we should be able to load this file again
 
1431
        knit = make_file_knit('test', get_transport('.'), access_mode='r')
 
1432
        self.assertEqual(['revid', 'revid2'], knit.versions())
 
1433
        # write a short write to the file and ensure that its ignored
 
1434
        indexfile = file('test.kndx', 'ab')
 
1435
        indexfile.write('\nrevid3 line-delta 166 82 1 2 3 4 5 .phwoar:demo ')
 
1436
        indexfile.close()
 
1437
        # we should be able to load this file again
 
1438
        knit = make_file_knit('test', get_transport('.'), access_mode='w')
 
1439
        self.assertEqual(['revid', 'revid2'], knit.versions())
 
1440
        # and add a revision with the same id the failed write had
 
1441
        knit.add_lines('revid3', ['revid2'], ['a\n'])
 
1442
        # and when reading it revid3 should now appear.
 
1443
        knit = make_file_knit('test', get_transport('.'), access_mode='r')
 
1444
        self.assertEqual(['revid', 'revid2', 'revid3'], knit.versions())
 
1445
        self.assertEqual({'revid3':('revid2',)}, knit.get_parent_map(['revid3']))
 
1446
 
 
1447
    def test_delay_create(self):
 
1448
        """Test that passing delay_create=True creates files late"""
 
1449
        knit = self.make_test_knit(annotate=True, delay_create=True)
 
1450
        self.failIfExists('test.knit')
 
1451
        self.failIfExists('test.kndx')
 
1452
        knit.add_lines_with_ghosts('revid', ['a_ghost'], ['a\n'])
 
1453
        self.failUnlessExists('test.knit')
 
1454
        self.assertFileEqual(
 
1455
            "# bzr knit index 8\n"
 
1456
            "\n"
 
1457
            "revid fulltext 0 84 .a_ghost :",
 
1458
            'test.kndx')
 
1459
 
 
1460
    def test_create_parent_dir(self):
 
1461
        """create_parent_dir can create knits in nonexistant dirs"""
 
1462
        # Has no effect if we don't set 'delay_create'
 
1463
        trans = get_transport('.')
 
1464
        self.assertRaises(NoSuchFile, make_file_knit, 'dir/test',
 
1465
                          trans, access_mode='w', factory=None,
 
1466
                          create=True, create_parent_dir=True)
 
1467
        # Nothing should have changed yet
 
1468
        knit = make_file_knit('dir/test', trans, access_mode='w',
 
1469
                                 factory=None, create=True,
 
1470
                                 create_parent_dir=True,
 
1471
                                 delay_create=True)
 
1472
        self.failIfExists('dir/test.knit')
 
1473
        self.failIfExists('dir/test.kndx')
 
1474
        self.failIfExists('dir')
 
1475
        knit.add_lines('revid', [], ['a\n'])
 
1476
        self.failUnlessExists('dir')
 
1477
        self.failUnlessExists('dir/test.knit')
 
1478
        self.assertFileEqual(
 
1479
            "# bzr knit index 8\n"
 
1480
            "\n"
 
1481
            "revid fulltext 0 84  :",
 
1482
            'dir/test.kndx')
 
1483
 
 
1484
    def test_create_mode_700(self):
 
1485
        trans = get_transport('.')
 
1486
        if not trans._can_roundtrip_unix_modebits():
 
1487
            # Can't roundtrip, so no need to run this test
 
1488
            return
 
1489
        knit = make_file_knit('dir/test', trans, access_mode='w', factory=None,
 
1490
            create=True, create_parent_dir=True, delay_create=True,
 
1491
            file_mode=0600, dir_mode=0700)
 
1492
        knit.add_lines('revid', [], ['a\n'])
 
1493
        self.assertTransportMode(trans, 'dir', 0700)
 
1494
        self.assertTransportMode(trans, 'dir/test.knit', 0600)
 
1495
        self.assertTransportMode(trans, 'dir/test.kndx', 0600)
 
1496
 
 
1497
    def test_create_mode_770(self):
 
1498
        trans = get_transport('.')
 
1499
        if not trans._can_roundtrip_unix_modebits():
 
1500
            # Can't roundtrip, so no need to run this test
 
1501
            return
 
1502
        knit = make_file_knit('dir/test', trans, access_mode='w', factory=None,
 
1503
            create=True, create_parent_dir=True, delay_create=True,
 
1504
            file_mode=0660, dir_mode=0770)
 
1505
        knit.add_lines('revid', [], ['a\n'])
 
1506
        self.assertTransportMode(trans, 'dir', 0770)
 
1507
        self.assertTransportMode(trans, 'dir/test.knit', 0660)
 
1508
        self.assertTransportMode(trans, 'dir/test.kndx', 0660)
 
1509
 
 
1510
    def test_create_mode_777(self):
 
1511
        trans = get_transport('.')
 
1512
        if not trans._can_roundtrip_unix_modebits():
 
1513
            # Can't roundtrip, so no need to run this test
 
1514
            return
 
1515
        knit = make_file_knit('dir/test', trans, access_mode='w', factory=None,
 
1516
            create=True, create_parent_dir=True, delay_create=True,
 
1517
            file_mode=0666, dir_mode=0777)
 
1518
        knit.add_lines('revid', [], ['a\n'])
 
1519
        self.assertTransportMode(trans, 'dir', 0777)
 
1520
        self.assertTransportMode(trans, 'dir/test.knit', 0666)
 
1521
        self.assertTransportMode(trans, 'dir/test.kndx', 0666)
 
1522
 
 
1523
    def test_plan_merge(self):
 
1524
        my_knit = self.make_test_knit(annotate=True)
 
1525
        my_knit.add_lines('text1', [], split_lines(TEXT_1))
 
1526
        my_knit.add_lines('text1a', ['text1'], split_lines(TEXT_1A))
 
1527
        my_knit.add_lines('text1b', ['text1'], split_lines(TEXT_1B))
 
1528
        plan = list(my_knit.plan_merge('text1a', 'text1b'))
 
1529
        for plan_line, expected_line in zip(plan, AB_MERGE):
 
1530
            self.assertEqual(plan_line, expected_line)
 
1531
 
 
1532
    def test_get_stream_empty(self):
 
1533
        """Get a data stream for an empty knit file."""
 
1534
        k1 = self.make_test_knit()
 
1535
        format, data_list, reader_callable = k1.get_data_stream([])
 
1536
        self.assertEqual('knit-plain', format)
 
1537
        self.assertEqual([], data_list)
 
1538
        content = reader_callable(None)
 
1539
        self.assertEqual('', content)
 
1540
        self.assertIsInstance(content, str)
 
1541
 
 
1542
    def test_get_stream_one_version(self):
 
1543
        """Get a data stream for a single record out of a knit containing just
 
1544
        one record.
 
1545
        """
 
1546
        k1 = self.make_test_knit()
 
1547
        test_data = [
 
1548
            ('text-a', [], TEXT_1),
 
1549
            ]
 
1550
        expected_data_list = [
 
1551
            # version, options, length, parents
 
1552
            ('text-a', ['fulltext'], 122, ()),
 
1553
           ]
 
1554
        for version_id, parents, lines in test_data:
 
1555
            k1.add_lines(version_id, parents, split_lines(lines))
 
1556
 
 
1557
        format, data_list, reader_callable = k1.get_data_stream(['text-a'])
 
1558
        self.assertEqual('knit-plain', format)
 
1559
        self.assertEqual(expected_data_list, data_list)
 
1560
        # There's only one record in the knit, so the content should be the
 
1561
        # entire knit data file's contents.
 
1562
        self.assertEqual(k1.transport.get_bytes(k1._data._access._filename),
 
1563
                         reader_callable(None))
 
1564
        
 
1565
    def test_get_stream_get_one_version_of_many(self):
 
1566
        """Get a data stream for just one version out of a knit containing many
 
1567
        versions.
 
1568
        """
 
1569
        k1 = self.make_test_knit()
 
1570
        # Insert the same data as test_knit_join, as they seem to cover a range
 
1571
        # of cases (no parents, one parent, multiple parents).
 
1572
        test_data = [
 
1573
            ('text-a', [], TEXT_1),
 
1574
            ('text-b', ['text-a'], TEXT_1),
 
1575
            ('text-c', [], TEXT_1),
 
1576
            ('text-d', ['text-c'], TEXT_1),
 
1577
            ('text-m', ['text-b', 'text-d'], TEXT_1),
 
1578
            ]
 
1579
        expected_data_list = [
 
1580
            # version, options, length, parents
 
1581
            ('text-m', ['line-delta'], 84, ('text-b', 'text-d')),
 
1582
            ]
 
1583
        for version_id, parents, lines in test_data:
 
1584
            k1.add_lines(version_id, parents, split_lines(lines))
 
1585
 
 
1586
        format, data_list, reader_callable = k1.get_data_stream(['text-m'])
 
1587
        self.assertEqual('knit-plain', format)
 
1588
        self.assertEqual(expected_data_list, data_list)
 
1589
        self.assertRecordContentEqual(k1, 'text-m', reader_callable(None))
 
1590
        
 
1591
    def test_get_data_stream_unordered_index(self):
 
1592
        """Get a data stream when the knit index reports versions out of order.
 
1593
 
 
1594
        https://bugs.launchpad.net/bzr/+bug/164637
 
1595
        """
 
1596
        k1 = self.make_test_knit()
 
1597
        test_data = [
 
1598
            ('text-a', [], TEXT_1),
 
1599
            ('text-b', ['text-a'], TEXT_1),
 
1600
            ('text-c', [], TEXT_1),
 
1601
            ('text-d', ['text-c'], TEXT_1),
 
1602
            ('text-m', ['text-b', 'text-d'], TEXT_1),
 
1603
            ]
 
1604
        for version_id, parents, lines in test_data:
 
1605
            k1.add_lines(version_id, parents, split_lines(lines))
 
1606
        # monkey-patch versions method to return out of order, as if coming
 
1607
        # from multiple independently indexed packs
 
1608
        original_versions = k1.versions
 
1609
        k1.versions = lambda: reversed(original_versions())
 
1610
        expected_data_list = [
 
1611
            ('text-a', ['fulltext'], 122, ()),
 
1612
            ('text-b', ['line-delta'], 84, ('text-a',))]
 
1613
        # now check the fulltext is first and the delta second
 
1614
        format, data_list, _ = k1.get_data_stream(['text-a', 'text-b'])
 
1615
        self.assertEqual('knit-plain', format)
 
1616
        self.assertEqual(expected_data_list, data_list)
 
1617
        # and that's true if we ask for them in the opposite order too
 
1618
        format, data_list, _ = k1.get_data_stream(['text-b', 'text-a'])
 
1619
        self.assertEqual(expected_data_list, data_list)
 
1620
        # also try requesting more versions
 
1621
        format, data_list, _ = k1.get_data_stream([
 
1622
            'text-m', 'text-b', 'text-a'])
 
1623
        self.assertEqual([
 
1624
            ('text-a', ['fulltext'], 122, ()),
 
1625
            ('text-b', ['line-delta'], 84, ('text-a',)),
 
1626
            ('text-m', ['line-delta'], 84, ('text-b', 'text-d')),
 
1627
            ], data_list)
 
1628
 
 
1629
    def test_get_stream_ghost_parent(self):
 
1630
        """Get a data stream for a version with a ghost parent."""
 
1631
        k1 = self.make_test_knit()
 
1632
        # Test data
 
1633
        k1.add_lines('text-a', [], split_lines(TEXT_1))
 
1634
        k1.add_lines_with_ghosts('text-b', ['text-a', 'text-ghost'],
 
1635
                                 split_lines(TEXT_1))
 
1636
        # Expected data
 
1637
        expected_data_list = [
 
1638
            # version, options, length, parents
 
1639
            ('text-b', ['line-delta'], 84, ('text-a', 'text-ghost')),
 
1640
            ]
 
1641
        
 
1642
        format, data_list, reader_callable = k1.get_data_stream(['text-b'])
 
1643
        self.assertEqual('knit-plain', format)
 
1644
        self.assertEqual(expected_data_list, data_list)
 
1645
        self.assertRecordContentEqual(k1, 'text-b', reader_callable(None))
 
1646
    
 
1647
    def test_get_stream_get_multiple_records(self):
 
1648
        """Get a stream for multiple records of a knit."""
 
1649
        k1 = self.make_test_knit()
 
1650
        # Insert the same data as test_knit_join, as they seem to cover a range
 
1651
        # of cases (no parents, one parent, multiple parents).
 
1652
        test_data = [
 
1653
            ('text-a', [], TEXT_1),
 
1654
            ('text-b', ['text-a'], TEXT_1),
 
1655
            ('text-c', [], TEXT_1),
 
1656
            ('text-d', ['text-c'], TEXT_1),
 
1657
            ('text-m', ['text-b', 'text-d'], TEXT_1),
 
1658
            ]
 
1659
        for version_id, parents, lines in test_data:
 
1660
            k1.add_lines(version_id, parents, split_lines(lines))
 
1661
 
 
1662
        # This test is actually a bit strict as the order in which they're
 
1663
        # returned is not defined.  This matches the current (deterministic)
 
1664
        # behaviour.
 
1665
        expected_data_list = [
 
1666
            # version, options, length, parents
 
1667
            ('text-d', ['line-delta'], 84, ('text-c',)),
 
1668
            ('text-b', ['line-delta'], 84, ('text-a',)),
 
1669
            ]
 
1670
        # Note that even though we request the revision IDs in a particular
 
1671
        # order, the data stream may return them in any order it likes.  In this
 
1672
        # case, they'll be in the order they were inserted into the knit.
 
1673
        format, data_list, reader_callable = k1.get_data_stream(
 
1674
            ['text-d', 'text-b'])
 
1675
        self.assertEqual('knit-plain', format)
 
1676
        self.assertEqual(expected_data_list, data_list)
 
1677
        # must match order they're returned
 
1678
        self.assertRecordContentEqual(k1, 'text-d', reader_callable(84))
 
1679
        self.assertRecordContentEqual(k1, 'text-b', reader_callable(84))
 
1680
        self.assertEqual('', reader_callable(None),
 
1681
                         "There should be no more bytes left to read.")
 
1682
 
 
1683
    def test_get_stream_all(self):
 
1684
        """Get a data stream for all the records in a knit.
 
1685
 
 
1686
        This exercises fulltext records, line-delta records, records with
 
1687
        various numbers of parents, and reading multiple records out of the
 
1688
        callable.  These cases ought to all be exercised individually by the
 
1689
        other test_get_stream_* tests; this test is basically just paranoia.
 
1690
        """
 
1691
        k1 = self.make_test_knit()
 
1692
        # Insert the same data as test_knit_join, as they seem to cover a range
 
1693
        # of cases (no parents, one parent, multiple parents).
 
1694
        test_data = [
 
1695
            ('text-a', [], TEXT_1),
 
1696
            ('text-b', ['text-a'], TEXT_1),
 
1697
            ('text-c', [], TEXT_1),
 
1698
            ('text-d', ['text-c'], TEXT_1),
 
1699
            ('text-m', ['text-b', 'text-d'], TEXT_1),
 
1700
           ]
 
1701
        for version_id, parents, lines in test_data:
 
1702
            k1.add_lines(version_id, parents, split_lines(lines))
 
1703
 
 
1704
        # This test is actually a bit strict as the order in which they're
 
1705
        # returned is not defined.  This matches the current (deterministic)
 
1706
        # behaviour.
 
1707
        expected_data_list = [
 
1708
            # version, options, length, parents
 
1709
            ('text-a', ['fulltext'], 122, ()),
 
1710
            ('text-b', ['line-delta'], 84, ('text-a',)),
 
1711
            ('text-m', ['line-delta'], 84, ('text-b', 'text-d')),
 
1712
            ('text-c', ['fulltext'], 121, ()),
 
1713
            ('text-d', ['line-delta'], 84, ('text-c',)),
 
1714
            ]
 
1715
        format, data_list, reader_callable = k1.get_data_stream(
 
1716
            ['text-a', 'text-b', 'text-c', 'text-d', 'text-m'])
 
1717
        self.assertEqual('knit-plain', format)
 
1718
        self.assertEqual(expected_data_list, data_list)
 
1719
        for version_id, options, length, parents in expected_data_list:
 
1720
            bytes = reader_callable(length)
 
1721
            self.assertRecordContentEqual(k1, version_id, bytes)
 
1722
 
 
1723
    def assertKnitFilesEqual(self, knit1, knit2):
 
1724
        """Assert that the contents of the index and data files of two knits are
 
1725
        equal.
 
1726
        """
 
1727
        self.assertEqual(
 
1728
            knit1.transport.get_bytes(knit1._data._access._filename),
 
1729
            knit2.transport.get_bytes(knit2._data._access._filename))
 
1730
        self.assertEqual(
 
1731
            knit1.transport.get_bytes(knit1._index._filename),
 
1732
            knit2.transport.get_bytes(knit2._index._filename))
 
1733
 
 
1734
    def assertKnitValuesEqual(self, left, right):
 
1735
        """Assert that the texts, annotations and graph of left and right are
 
1736
        the same.
 
1737
        """
 
1738
        self.assertEqual(set(left.versions()), set(right.versions()))
 
1739
        for version in left.versions():
 
1740
            self.assertEqual(left.get_parents_with_ghosts(version),
 
1741
                right.get_parents_with_ghosts(version))
 
1742
            self.assertEqual(left.get_lines(version),
 
1743
                right.get_lines(version))
 
1744
            self.assertEqual(left.annotate(version),
 
1745
                right.annotate(version))
 
1746
 
 
1747
    def test_insert_data_stream_empty(self):
 
1748
        """Inserting a data stream with no records should not put any data into
 
1749
        the knit.
 
1750
        """
 
1751
        k1 = self.make_test_knit()
 
1752
        k1.insert_data_stream(
 
1753
            (k1.get_format_signature(), [], lambda ignored: ''))
 
1754
        self.assertEqual('', k1.transport.get_bytes(k1._data._access._filename),
 
1755
                         "The .knit should be completely empty.")
 
1756
        self.assertEqual(k1._index.HEADER,
 
1757
                         k1.transport.get_bytes(k1._index._filename),
 
1758
                         "The .kndx should have nothing apart from the header.")
 
1759
 
 
1760
    def test_insert_data_stream_one_record(self):
 
1761
        """Inserting a data stream with one record from a knit with one record
 
1762
        results in byte-identical files.
 
1763
        """
 
1764
        source = self.make_test_knit(name='source')
 
1765
        source.add_lines('text-a', [], split_lines(TEXT_1))
 
1766
        data_stream = source.get_data_stream(['text-a'])
 
1767
        target = self.make_test_knit(name='target')
 
1768
        target.insert_data_stream(data_stream)
 
1769
        self.assertKnitFilesEqual(source, target)
 
1770
 
 
1771
    def test_insert_data_stream_annotated_unannotated(self):
 
1772
        """Inserting an annotated datastream to an unannotated knit works."""
 
1773
        # case one - full texts.
 
1774
        source = self.make_test_knit(name='source', annotate=True)
 
1775
        target = self.make_test_knit(name='target', annotate=False)
 
1776
        source.add_lines('text-a', [], split_lines(TEXT_1))
 
1777
        target.insert_data_stream(source.get_data_stream(['text-a']))
 
1778
        self.assertKnitValuesEqual(source, target)
 
1779
        # case two - deltas.
 
1780
        source.add_lines('text-b', ['text-a'], split_lines(TEXT_2))
 
1781
        target.insert_data_stream(source.get_data_stream(['text-b']))
 
1782
        self.assertKnitValuesEqual(source, target)
 
1783
 
 
1784
    def test_insert_data_stream_unannotated_annotated(self):
 
1785
        """Inserting an unannotated datastream to an annotated knit works."""
 
1786
        # case one - full texts.
 
1787
        source = self.make_test_knit(name='source', annotate=False)
 
1788
        target = self.make_test_knit(name='target', annotate=True)
 
1789
        source.add_lines('text-a', [], split_lines(TEXT_1))
 
1790
        target.insert_data_stream(source.get_data_stream(['text-a']))
 
1791
        self.assertKnitValuesEqual(source, target)
 
1792
        # case two - deltas.
 
1793
        source.add_lines('text-b', ['text-a'], split_lines(TEXT_2))
 
1794
        target.insert_data_stream(source.get_data_stream(['text-b']))
 
1795
        self.assertKnitValuesEqual(source, target)
 
1796
 
 
1797
    def test_insert_data_stream_records_already_present(self):
 
1798
        """Insert a data stream where some records are alreday present in the
 
1799
        target, and some not.  Only the new records are inserted.
 
1800
        """
 
1801
        source = self.make_test_knit(name='source')
 
1802
        target = self.make_test_knit(name='target')
 
1803
        # Insert 'text-a' into both source and target
 
1804
        source.add_lines('text-a', [], split_lines(TEXT_1))
 
1805
        target.insert_data_stream(source.get_data_stream(['text-a']))
 
1806
        # Insert 'text-b' into just the source.
 
1807
        source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
 
1808
        # Get a data stream of both text-a and text-b, and insert it.
 
1809
        data_stream = source.get_data_stream(['text-a', 'text-b'])
 
1810
        target.insert_data_stream(data_stream)
 
1811
        # The source and target will now be identical.  This means the text-a
 
1812
        # record was not added a second time.
 
1813
        self.assertKnitFilesEqual(source, target)
 
1814
 
 
1815
    def test_insert_data_stream_multiple_records(self):
 
1816
        """Inserting a data stream of all records from a knit with multiple
 
1817
        records results in byte-identical files.
 
1818
        """
 
1819
        source = self.make_test_knit(name='source')
 
1820
        source.add_lines('text-a', [], split_lines(TEXT_1))
 
1821
        source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
 
1822
        source.add_lines('text-c', [], split_lines(TEXT_1))
 
1823
        data_stream = source.get_data_stream(['text-a', 'text-b', 'text-c'])
 
1824
        
 
1825
        target = self.make_test_knit(name='target')
 
1826
        target.insert_data_stream(data_stream)
 
1827
        
 
1828
        self.assertKnitFilesEqual(source, target)
 
1829
 
 
1830
    def test_insert_data_stream_ghost_parent(self):
 
1831
        """Insert a data stream with a record that has a ghost parent."""
 
1832
        # Make a knit with a record, text-a, that has a ghost parent.
 
1833
        source = self.make_test_knit(name='source')
 
1834
        source.add_lines_with_ghosts('text-a', ['text-ghost'],
 
1835
                                     split_lines(TEXT_1))
 
1836
        data_stream = source.get_data_stream(['text-a'])
 
1837
 
 
1838
        target = self.make_test_knit(name='target')
 
1839
        target.insert_data_stream(data_stream)
 
1840
 
 
1841
        self.assertKnitFilesEqual(source, target)
 
1842
 
 
1843
        # The target knit object is in a consistent state, i.e. the record we
 
1844
        # just added is immediately visible.
 
1845
        self.assertTrue(target.has_version('text-a'))
 
1846
        self.assertFalse(target.has_version('text-ghost'))
 
1847
        self.assertEqual({'text-a':('text-ghost',)},
 
1848
            target.get_parent_map(['text-a', 'text-ghost']))
 
1849
        self.assertEqual(split_lines(TEXT_1), target.get_lines('text-a'))
 
1850
 
 
1851
    def test_insert_data_stream_inconsistent_version_lines(self):
 
1852
        """Inserting a data stream which has different content for a version_id
 
1853
        than already exists in the knit will raise KnitCorrupt.
 
1854
        """
 
1855
        source = self.make_test_knit(name='source')
 
1856
        target = self.make_test_knit(name='target')
 
1857
        # Insert a different 'text-a' into both source and target
 
1858
        source.add_lines('text-a', [], split_lines(TEXT_1))
 
1859
        target.add_lines('text-a', [], split_lines(TEXT_2))
 
1860
        # Insert a data stream with conflicting content into the target
 
1861
        data_stream = source.get_data_stream(['text-a'])
 
1862
        self.assertRaises(
 
1863
            errors.KnitCorrupt, target.insert_data_stream, data_stream)
 
1864
 
 
1865
    def test_insert_data_stream_inconsistent_version_parents(self):
 
1866
        """Inserting a data stream which has different parents for a version_id
 
1867
        than already exists in the knit will raise KnitCorrupt.
 
1868
        """
 
1869
        source = self.make_test_knit(name='source')
 
1870
        target = self.make_test_knit(name='target')
 
1871
        # Insert a different 'text-a' into both source and target.  They differ
 
1872
        # only by the parents list, the content is the same.
 
1873
        source.add_lines_with_ghosts('text-a', [], split_lines(TEXT_1))
 
1874
        target.add_lines_with_ghosts('text-a', ['a-ghost'], split_lines(TEXT_1))
 
1875
        # Insert a data stream with conflicting content into the target
 
1876
        data_stream = source.get_data_stream(['text-a'])
 
1877
        self.assertRaises(
 
1878
            errors.KnitCorrupt, target.insert_data_stream, data_stream)
 
1879
 
 
1880
    def test_insert_data_stream_unknown_format(self):
 
1881
        """A data stream in a different format to the target knit cannot be
 
1882
        inserted.
 
1883
 
 
1884
        It will raise KnitDataStreamUnknown because the fallback code will fail
 
1885
        to make a knit. In future we may need KnitDataStreamIncompatible again,
 
1886
        for more exotic cases.
 
1887
        """
 
1888
        data_stream = ('fake-format-signature', [], lambda _: '')
 
1889
        target = self.make_test_knit(name='target')
 
1890
        self.assertRaises(
 
1891
            errors.KnitDataStreamUnknown,
 
1892
            target.insert_data_stream, data_stream)
 
1893
 
 
1894
    #  * test that a stream of "already present version, then new version"
 
1895
    #    inserts correctly.
 
1896
 
 
1897
 
 
1898
    def assertMadeStreamKnit(self, source_knit, versions, target_knit):
 
1899
        """Assert that a knit made from a stream is as expected."""
 
1900
        a_stream = source_knit.get_data_stream(versions)
 
1901
        expected_data = a_stream[2](None)
 
1902
        a_stream = source_knit.get_data_stream(versions)
 
1903
        a_knit = target_knit._knit_from_datastream(a_stream)
 
1904
        self.assertEqual(source_knit.factory.__class__,
 
1905
            a_knit.factory.__class__)
 
1906
        self.assertIsInstance(a_knit._data._access, _StreamAccess)
 
1907
        self.assertIsInstance(a_knit._index, _StreamIndex)
 
1908
        self.assertEqual(a_knit._index.data_list, a_stream[1])
 
1909
        self.assertEqual(a_knit._data._access.data, expected_data)
 
1910
        self.assertEqual(a_knit.filename, target_knit.filename)
 
1911
        self.assertEqual(a_knit.transport, target_knit.transport)
 
1912
        self.assertEqual(a_knit._index, a_knit._data._access.stream_index)
 
1913
        self.assertEqual(target_knit, a_knit._data._access.backing_knit)
 
1914
        self.assertIsInstance(a_knit._data._access.orig_factory,
 
1915
            source_knit.factory.__class__)
 
1916
 
 
1917
    def test__knit_from_data_stream_empty(self):
 
1918
        """Create a knit object from a datastream."""
 
1919
        annotated = self.make_test_knit(name='source', annotate=True)
 
1920
        plain = self.make_test_knit(name='target', annotate=False)
 
1921
        # case 1: annotated source
 
1922
        self.assertMadeStreamKnit(annotated, [], annotated)
 
1923
        self.assertMadeStreamKnit(annotated, [], plain)
 
1924
        # case 2: plain source
 
1925
        self.assertMadeStreamKnit(plain, [], annotated)
 
1926
        self.assertMadeStreamKnit(plain, [], plain)
 
1927
 
 
1928
    def test__knit_from_data_stream_unknown_format(self):
 
1929
        annotated = self.make_test_knit(name='source', annotate=True)
 
1930
        self.assertRaises(errors.KnitDataStreamUnknown,
 
1931
            annotated._knit_from_datastream, ("unknown", None, None))
 
1932
 
 
1933
 
 
1934
TEXT_1 = """\
 
1935
Banana cup cakes:
 
1936
 
 
1937
- bananas
 
1938
- eggs
 
1939
- broken tea cups
 
1940
"""
 
1941
 
 
1942
TEXT_1A = """\
 
1943
Banana cup cake recipe
 
1944
(serves 6)
 
1945
 
 
1946
- bananas
 
1947
- eggs
 
1948
- broken tea cups
 
1949
- self-raising flour
 
1950
"""
 
1951
 
 
1952
TEXT_1B = """\
 
1953
Banana cup cake recipe
 
1954
 
 
1955
- bananas (do not use plantains!!!)
 
1956
- broken tea cups
 
1957
- flour
 
1958
"""
 
1959
 
 
1960
delta_1_1a = """\
 
1961
0,1,2
 
1962
Banana cup cake recipe
 
1963
(serves 6)
 
1964
5,5,1
 
1965
- self-raising flour
 
1966
"""
 
1967
 
 
1968
TEXT_2 = """\
 
1969
Boeuf bourguignon
 
1970
 
 
1971
- beef
 
1972
- red wine
 
1973
- small onions
 
1974
- carrot
 
1975
- mushrooms
 
1976
"""
 
1977
 
 
1978
AB_MERGE_TEXT="""unchanged|Banana cup cake recipe
 
1979
new-a|(serves 6)
 
1980
unchanged|
 
1981
killed-b|- bananas
 
1982
killed-b|- eggs
 
1983
new-b|- bananas (do not use plantains!!!)
 
1984
unchanged|- broken tea cups
 
1985
new-a|- self-raising flour
 
1986
new-b|- flour
 
1987
"""
 
1988
AB_MERGE=[tuple(l.split('|')) for l in AB_MERGE_TEXT.splitlines(True)]
 
1989
 
 
1990
 
 
1991
def line_delta(from_lines, to_lines):
 
1992
    """Generate line-based delta from one text to another"""
 
1993
    s = difflib.SequenceMatcher(None, from_lines, to_lines)
 
1994
    for op in s.get_opcodes():
 
1995
        if op[0] == 'equal':
 
1996
            continue
 
1997
        yield '%d,%d,%d\n' % (op[1], op[2], op[4]-op[3])
 
1998
        for i in range(op[3], op[4]):
 
1999
            yield to_lines[i]
 
2000
 
 
2001
 
 
2002
def apply_line_delta(basis_lines, delta_lines):
 
2003
    """Apply a line-based perfect diff
 
2004
    
 
2005
    basis_lines -- text to apply the patch to
 
2006
    delta_lines -- diff instructions and content
 
2007
    """
 
2008
    out = basis_lines[:]
 
2009
    i = 0
 
2010
    offset = 0
 
2011
    while i < len(delta_lines):
 
2012
        l = delta_lines[i]
 
2013
        a, b, c = map(long, l.split(','))
 
2014
        i = i + 1
 
2015
        out[offset+a:offset+b] = delta_lines[i:i+c]
 
2016
        i = i + c
 
2017
        offset = offset + (b - a) + c
 
2018
    return out
 
2019
 
 
2020
 
 
2021
class TestWeaveToKnit(KnitTests):
 
2022
 
 
2023
    def test_weave_to_knit_matches(self):
 
2024
        # check that the WeaveToKnit is_compatible function
 
2025
        # registers True for a Weave to a Knit.
 
2026
        w = Weave(get_scope=lambda:None)
 
2027
        k = self.make_test_knit()
 
2028
        self.failUnless(WeaveToKnit.is_compatible(w, k))
 
2029
        self.failIf(WeaveToKnit.is_compatible(k, w))
 
2030
        self.failIf(WeaveToKnit.is_compatible(w, w))
 
2031
        self.failIf(WeaveToKnit.is_compatible(k, k))
 
2032
 
 
2033
 
 
2034
class TestKnitCaching(KnitTests):
 
2035
    
 
2036
    def create_knit(self):
 
2037
        k = self.make_test_knit(True)
 
2038
        k.add_lines('text-1', [], split_lines(TEXT_1))
 
2039
        k.add_lines('text-2', [], split_lines(TEXT_2))
 
2040
        return k
 
2041
 
 
2042
    def test_no_caching(self):
 
2043
        k = self.create_knit()
 
2044
        # Nothing should be cached without setting 'enable_cache'
 
2045
        self.assertEqual({}, k._data._cache)
 
2046
 
 
2047
    def test_cache_data_read_raw(self):
 
2048
        k = self.create_knit()
 
2049
 
 
2050
        # Now cache and read
 
2051
        k.enable_cache()
 
2052
 
 
2053
        def read_one_raw(version):
 
2054
            pos_map = k._get_components_positions([version])
 
2055
            method, index_memo, next = pos_map[version]
 
2056
            lst = list(k._data.read_records_iter_raw([(version, index_memo)]))
 
2057
            self.assertEqual(1, len(lst))
 
2058
            return lst[0]
 
2059
 
 
2060
        val = read_one_raw('text-1')
 
2061
        self.assertEqual({'text-1':val[1]}, k._data._cache)
 
2062
 
 
2063
        k.clear_cache()
 
2064
        # After clear, new reads are not cached
 
2065
        self.assertEqual({}, k._data._cache)
 
2066
 
 
2067
        val2 = read_one_raw('text-1')
 
2068
        self.assertEqual(val, val2)
 
2069
        self.assertEqual({}, k._data._cache)
 
2070
 
 
2071
    def test_cache_data_read(self):
 
2072
        k = self.create_knit()
 
2073
 
 
2074
        def read_one(version):
 
2075
            pos_map = k._get_components_positions([version])
 
2076
            method, index_memo, next = pos_map[version]
 
2077
            lst = list(k._data.read_records_iter([(version, index_memo)]))
 
2078
            self.assertEqual(1, len(lst))
 
2079
            return lst[0]
 
2080
 
 
2081
        # Now cache and read
 
2082
        k.enable_cache()
 
2083
 
 
2084
        val = read_one('text-2')
 
2085
        self.assertEqual(['text-2'], k._data._cache.keys())
 
2086
        self.assertEqual('text-2', val[0])
 
2087
        content, digest = k._data._parse_record('text-2',
 
2088
                                                k._data._cache['text-2'])
 
2089
        self.assertEqual(content, val[1])
 
2090
        self.assertEqual(digest, val[2])
 
2091
 
 
2092
        k.clear_cache()
 
2093
        self.assertEqual({}, k._data._cache)
 
2094
 
 
2095
        val2 = read_one('text-2')
 
2096
        self.assertEqual(val, val2)
 
2097
        self.assertEqual({}, k._data._cache)
 
2098
 
 
2099
    def test_cache_read(self):
 
2100
        k = self.create_knit()
 
2101
        k.enable_cache()
 
2102
 
 
2103
        text = k.get_text('text-1')
 
2104
        self.assertEqual(TEXT_1, text)
 
2105
        self.assertEqual(['text-1'], k._data._cache.keys())
 
2106
 
 
2107
        k.clear_cache()
 
2108
        self.assertEqual({}, k._data._cache)
 
2109
 
 
2110
        text = k.get_text('text-1')
 
2111
        self.assertEqual(TEXT_1, text)
 
2112
        self.assertEqual({}, k._data._cache)
 
2113
 
 
2114
 
 
2115
class TestKnitIndex(KnitTests):
 
2116
 
 
2117
    def test_add_versions_dictionary_compresses(self):
 
2118
        """Adding versions to the index should update the lookup dict"""
 
2119
        knit = self.make_test_knit()
 
2120
        idx = knit._index
 
2121
        idx.add_version('a-1', ['fulltext'], (None, 0, 0), [])
 
2122
        self.check_file_contents('test.kndx',
 
2123
            '# bzr knit index 8\n'
 
2124
            '\n'
 
2125
            'a-1 fulltext 0 0  :'
 
2126
            )
 
2127
        idx.add_versions([('a-2', ['fulltext'], (None, 0, 0), ['a-1']),
 
2128
                          ('a-3', ['fulltext'], (None, 0, 0), ['a-2']),
 
2129
                         ])
 
2130
        self.check_file_contents('test.kndx',
 
2131
            '# bzr knit index 8\n'
 
2132
            '\n'
 
2133
            'a-1 fulltext 0 0  :\n'
 
2134
            'a-2 fulltext 0 0 0 :\n'
 
2135
            'a-3 fulltext 0 0 1 :'
 
2136
            )
 
2137
        self.assertEqual(['a-1', 'a-2', 'a-3'], idx._history)
 
2138
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, (), 0),
 
2139
                          'a-2':('a-2', ['fulltext'], 0, 0, ('a-1',), 1),
 
2140
                          'a-3':('a-3', ['fulltext'], 0, 0, ('a-2',), 2),
 
2141
                         }, idx._cache)
 
2142
 
 
2143
    def test_add_versions_fails_clean(self):
 
2144
        """If add_versions fails in the middle, it restores a pristine state.
 
2145
 
 
2146
        Any modifications that are made to the index are reset if all versions
 
2147
        cannot be added.
 
2148
        """
 
2149
        # This cheats a little bit by passing in a generator which will
 
2150
        # raise an exception before the processing finishes
 
2151
        # Other possibilities would be to have an version with the wrong number
 
2152
        # of entries, or to make the backing transport unable to write any
 
2153
        # files.
 
2154
 
 
2155
        knit = self.make_test_knit()
 
2156
        idx = knit._index
 
2157
        idx.add_version('a-1', ['fulltext'], (None, 0, 0), [])
 
2158
 
 
2159
        class StopEarly(Exception):
 
2160
            pass
 
2161
 
 
2162
        def generate_failure():
 
2163
            """Add some entries and then raise an exception"""
 
2164
            yield ('a-2', ['fulltext'], (None, 0, 0), ('a-1',))
 
2165
            yield ('a-3', ['fulltext'], (None, 0, 0), ('a-2',))
 
2166
            raise StopEarly()
 
2167
 
 
2168
        # Assert the pre-condition
 
2169
        self.assertEqual(['a-1'], idx._history)
 
2170
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, (), 0)}, idx._cache)
 
2171
 
 
2172
        self.assertRaises(StopEarly, idx.add_versions, generate_failure())
 
2173
 
 
2174
        # And it shouldn't be modified
 
2175
        self.assertEqual(['a-1'], idx._history)
 
2176
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, (), 0)}, idx._cache)
 
2177
 
 
2178
    def test_knit_index_ignores_empty_files(self):
 
2179
        # There was a race condition in older bzr, where a ^C at the right time
 
2180
        # could leave an empty .kndx file, which bzr would later claim was a
 
2181
        # corrupted file since the header was not present. In reality, the file
 
2182
        # just wasn't created, so it should be ignored.
 
2183
        t = get_transport('.')
 
2184
        t.put_bytes('test.kndx', '')
 
2185
 
 
2186
        knit = self.make_test_knit()
 
2187
 
 
2188
    def test_knit_index_checks_header(self):
 
2189
        t = get_transport('.')
 
2190
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
 
2191
 
 
2192
        self.assertRaises(KnitHeaderError, self.make_test_knit)
 
2193
 
 
2194
 
 
2195
class TestGraphIndexKnit(KnitTests):
 
2196
    """Tests for knits using a GraphIndex rather than a KnitIndex."""
 
2197
 
 
2198
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
2199
        builder = GraphIndexBuilder(ref_lists)
 
2200
        for node, references, value in nodes:
 
2201
            builder.add_node(node, references, value)
 
2202
        stream = builder.finish()
 
2203
        trans = self.get_transport()
 
2204
        size = trans.put_file(name, stream)
 
2205
        return GraphIndex(trans, name, size)
 
2206
 
 
2207
    def two_graph_index(self, deltas=False, catch_adds=False):
 
2208
        """Build a two-graph index.
 
2209
 
 
2210
        :param deltas: If true, use underlying indices with two node-ref
 
2211
            lists and 'parent' set to a delta-compressed against tail.
 
2212
        """
 
2213
        # build a complex graph across several indices.
 
2214
        if deltas:
 
2215
            # delta compression inn the index
 
2216
            index1 = self.make_g_index('1', 2, [
 
2217
                (('tip', ), 'N0 100', ([('parent', )], [], )),
 
2218
                (('tail', ), '', ([], []))])
 
2219
            index2 = self.make_g_index('2', 2, [
 
2220
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
 
2221
                (('separate', ), '', ([], []))])
 
2222
        else:
 
2223
            # just blob location and graph in the index.
 
2224
            index1 = self.make_g_index('1', 1, [
 
2225
                (('tip', ), 'N0 100', ([('parent', )], )),
 
2226
                (('tail', ), '', ([], ))])
 
2227
            index2 = self.make_g_index('2', 1, [
 
2228
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
 
2229
                (('separate', ), '', ([], ))])
 
2230
        combined_index = CombinedGraphIndex([index1, index2])
 
2231
        if catch_adds:
 
2232
            self.combined_index = combined_index
 
2233
            self.caught_entries = []
 
2234
            add_callback = self.catch_add
 
2235
        else:
 
2236
            add_callback = None
 
2237
        return KnitGraphIndex(combined_index, deltas=deltas,
 
2238
            add_callback=add_callback)
 
2239
 
 
2240
    def test_get_ancestry(self):
 
2241
        # get_ancestry is defined as eliding ghosts, not erroring.
 
2242
        index = self.two_graph_index()
 
2243
        self.assertEqual([], index.get_ancestry([]))
 
2244
        self.assertEqual(['separate'], index.get_ancestry(['separate']))
 
2245
        self.assertEqual(['tail'], index.get_ancestry(['tail']))
 
2246
        self.assertEqual(['tail', 'parent'], index.get_ancestry(['parent']))
 
2247
        self.assertEqual(['tail', 'parent', 'tip'], index.get_ancestry(['tip']))
 
2248
        self.assertTrue(index.get_ancestry(['tip', 'separate']) in
 
2249
            (['tail', 'parent', 'tip', 'separate'],
 
2250
             ['separate', 'tail', 'parent', 'tip'],
 
2251
            ))
 
2252
        # and without topo_sort
 
2253
        self.assertEqual(set(['separate']),
 
2254
            set(index.get_ancestry(['separate'], topo_sorted=False)))
 
2255
        self.assertEqual(set(['tail']),
 
2256
            set(index.get_ancestry(['tail'], topo_sorted=False)))
 
2257
        self.assertEqual(set(['tail', 'parent']),
 
2258
            set(index.get_ancestry(['parent'], topo_sorted=False)))
 
2259
        self.assertEqual(set(['tail', 'parent', 'tip']),
 
2260
            set(index.get_ancestry(['tip'], topo_sorted=False)))
 
2261
        self.assertEqual(set(['separate', 'tail', 'parent', 'tip']),
 
2262
            set(index.get_ancestry(['tip', 'separate'])))
 
2263
        # asking for a ghost makes it go boom.
 
2264
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry, ['ghost'])
 
2265
 
 
2266
    def test_get_ancestry_with_ghosts(self):
 
2267
        index = self.two_graph_index()
 
2268
        self.assertEqual([], index.get_ancestry_with_ghosts([]))
 
2269
        self.assertEqual(['separate'], index.get_ancestry_with_ghosts(['separate']))
 
2270
        self.assertEqual(['tail'], index.get_ancestry_with_ghosts(['tail']))
 
2271
        self.assertTrue(index.get_ancestry_with_ghosts(['parent']) in
 
2272
            (['tail', 'ghost', 'parent'],
 
2273
             ['ghost', 'tail', 'parent'],
 
2274
            ))
 
2275
        self.assertTrue(index.get_ancestry_with_ghosts(['tip']) in
 
2276
            (['tail', 'ghost', 'parent', 'tip'],
 
2277
             ['ghost', 'tail', 'parent', 'tip'],
 
2278
            ))
 
2279
        self.assertTrue(index.get_ancestry_with_ghosts(['tip', 'separate']) in
 
2280
            (['tail', 'ghost', 'parent', 'tip', 'separate'],
 
2281
             ['ghost', 'tail', 'parent', 'tip', 'separate'],
 
2282
             ['separate', 'tail', 'ghost', 'parent', 'tip'],
 
2283
             ['separate', 'ghost', 'tail', 'parent', 'tip'],
 
2284
            ))
 
2285
        # asking for a ghost makes it go boom.
 
2286
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry_with_ghosts, ['ghost'])
 
2287
 
 
2288
    def test_num_versions(self):
 
2289
        index = self.two_graph_index()
 
2290
        self.assertEqual(4, index.num_versions())
 
2291
 
 
2292
    def test_get_versions(self):
 
2293
        index = self.two_graph_index()
 
2294
        self.assertEqual(set(['tail', 'tip', 'parent', 'separate']),
 
2295
            set(index.get_versions()))
 
2296
 
 
2297
    def test_has_version(self):
 
2298
        index = self.two_graph_index()
 
2299
        self.assertTrue(index.has_version('tail'))
 
2300
        self.assertFalse(index.has_version('ghost'))
 
2301
 
 
2302
    def test_get_position(self):
 
2303
        index = self.two_graph_index()
 
2304
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position('tip'))
 
2305
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position('parent'))
 
2306
 
 
2307
    def test_get_method_deltas(self):
 
2308
        index = self.two_graph_index(deltas=True)
 
2309
        self.assertEqual('fulltext', index.get_method('tip'))
 
2310
        self.assertEqual('line-delta', index.get_method('parent'))
 
2311
 
 
2312
    def test_get_method_no_deltas(self):
 
2313
        # check that the parent-history lookup is ignored with deltas=False.
 
2314
        index = self.two_graph_index(deltas=False)
 
2315
        self.assertEqual('fulltext', index.get_method('tip'))
 
2316
        self.assertEqual('fulltext', index.get_method('parent'))
 
2317
 
 
2318
    def test_get_options_deltas(self):
 
2319
        index = self.two_graph_index(deltas=True)
 
2320
        self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
 
2321
        self.assertEqual(['line-delta'], index.get_options('parent'))
 
2322
 
 
2323
    def test_get_options_no_deltas(self):
 
2324
        # check that the parent-history lookup is ignored with deltas=False.
 
2325
        index = self.two_graph_index(deltas=False)
 
2326
        self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
 
2327
        self.assertEqual(['fulltext'], index.get_options('parent'))
 
2328
 
 
2329
    def test_get_parents_with_ghosts(self):
 
2330
        index = self.two_graph_index()
 
2331
        self.assertEqual(('tail', 'ghost'), index.get_parents_with_ghosts('parent'))
 
2332
        # and errors on ghosts.
 
2333
        self.assertRaises(errors.RevisionNotPresent,
 
2334
            index.get_parents_with_ghosts, 'ghost')
 
2335
 
 
2336
    def test_check_versions_present(self):
 
2337
        # ghosts should not be considered present
 
2338
        index = self.two_graph_index()
 
2339
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
 
2340
            ['ghost'])
 
2341
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
 
2342
            ['tail', 'ghost'])
 
2343
        index.check_versions_present(['tail', 'separate'])
 
2344
 
 
2345
    def catch_add(self, entries):
 
2346
        self.caught_entries.append(entries)
 
2347
 
 
2348
    def test_add_no_callback_errors(self):
 
2349
        index = self.two_graph_index()
 
2350
        self.assertRaises(errors.ReadOnlyError, index.add_version,
 
2351
            'new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
 
2352
 
 
2353
    def test_add_version_smoke(self):
 
2354
        index = self.two_graph_index(catch_adds=True)
 
2355
        index.add_version('new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
 
2356
        self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
 
2357
            self.caught_entries)
 
2358
 
 
2359
    def test_add_version_delta_not_delta_index(self):
 
2360
        index = self.two_graph_index(catch_adds=True)
 
2361
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2362
            'new', 'no-eol,line-delta', (None, 0, 100), ['parent'])
 
2363
        self.assertEqual([], self.caught_entries)
 
2364
 
 
2365
    def test_add_version_same_dup(self):
 
2366
        index = self.two_graph_index(catch_adds=True)
 
2367
        # options can be spelt two different ways
 
2368
        index.add_version('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])
 
2369
        index.add_version('tip', 'no-eol,fulltext', (None, 0, 100), ['parent'])
 
2370
        # but neither should have added data.
 
2371
        self.assertEqual([[], []], self.caught_entries)
 
2372
        
 
2373
    def test_add_version_different_dup(self):
 
2374
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
2375
        # change options
 
2376
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2377
            'tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])
 
2378
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2379
            'tip', 'line-delta,no-eol', (None, 0, 100), ['parent'])
 
2380
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2381
            'tip', 'fulltext', (None, 0, 100), ['parent'])
 
2382
        # position/length
 
2383
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2384
            'tip', 'fulltext,no-eol', (None, 50, 100), ['parent'])
 
2385
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2386
            'tip', 'fulltext,no-eol', (None, 0, 1000), ['parent'])
 
2387
        # parents
 
2388
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2389
            'tip', 'fulltext,no-eol', (None, 0, 100), [])
 
2390
        self.assertEqual([], self.caught_entries)
 
2391
        
 
2392
    def test_add_versions_nodeltas(self):
 
2393
        index = self.two_graph_index(catch_adds=True)
 
2394
        index.add_versions([
 
2395
                ('new', 'fulltext,no-eol', (None, 50, 60), ['separate']),
 
2396
                ('new2', 'fulltext', (None, 0, 6), ['new']),
 
2397
                ])
 
2398
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
 
2399
            (('new2', ), ' 0 6', ((('new',),),))],
 
2400
            sorted(self.caught_entries[0]))
 
2401
        self.assertEqual(1, len(self.caught_entries))
 
2402
 
 
2403
    def test_add_versions_deltas(self):
 
2404
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
2405
        index.add_versions([
 
2406
                ('new', 'fulltext,no-eol', (None, 50, 60), ['separate']),
 
2407
                ('new2', 'line-delta', (None, 0, 6), ['new']),
 
2408
                ])
 
2409
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
 
2410
            (('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
 
2411
            sorted(self.caught_entries[0]))
 
2412
        self.assertEqual(1, len(self.caught_entries))
 
2413
 
 
2414
    def test_add_versions_delta_not_delta_index(self):
 
2415
        index = self.two_graph_index(catch_adds=True)
 
2416
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2417
            [('new', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
 
2418
        self.assertEqual([], self.caught_entries)
 
2419
 
 
2420
    def test_add_versions_random_id_accepted(self):
 
2421
        index = self.two_graph_index(catch_adds=True)
 
2422
        index.add_versions([], random_id=True)
 
2423
 
 
2424
    def test_add_versions_same_dup(self):
 
2425
        index = self.two_graph_index(catch_adds=True)
 
2426
        # options can be spelt two different ways
 
2427
        index.add_versions([('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])])
 
2428
        index.add_versions([('tip', 'no-eol,fulltext', (None, 0, 100), ['parent'])])
 
2429
        # but neither should have added data.
 
2430
        self.assertEqual([[], []], self.caught_entries)
 
2431
        
 
2432
    def test_add_versions_different_dup(self):
 
2433
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
2434
        # change options
 
2435
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2436
            [('tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
 
2437
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2438
            [('tip', 'line-delta,no-eol', (None, 0, 100), ['parent'])])
 
2439
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2440
            [('tip', 'fulltext', (None, 0, 100), ['parent'])])
 
2441
        # position/length
 
2442
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2443
            [('tip', 'fulltext,no-eol', (None, 50, 100), ['parent'])])
 
2444
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2445
            [('tip', 'fulltext,no-eol', (None, 0, 1000), ['parent'])])
 
2446
        # parents
 
2447
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2448
            [('tip', 'fulltext,no-eol', (None, 0, 100), [])])
 
2449
        # change options in the second record
 
2450
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2451
            [('tip', 'fulltext,no-eol', (None, 0, 100), ['parent']),
 
2452
             ('tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
 
2453
        self.assertEqual([], self.caught_entries)
 
2454
 
 
2455
class TestNoParentsGraphIndexKnit(KnitTests):
 
2456
    """Tests for knits using KnitGraphIndex with no parents."""
 
2457
 
 
2458
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
2459
        builder = GraphIndexBuilder(ref_lists)
 
2460
        for node, references in nodes:
 
2461
            builder.add_node(node, references)
 
2462
        stream = builder.finish()
 
2463
        trans = self.get_transport()
 
2464
        size = trans.put_file(name, stream)
 
2465
        return GraphIndex(trans, name, size)
 
2466
 
 
2467
    def test_parents_deltas_incompatible(self):
 
2468
        index = CombinedGraphIndex([])
 
2469
        self.assertRaises(errors.KnitError, KnitGraphIndex, index,
 
2470
            deltas=True, parents=False)
 
2471
 
 
2472
    def two_graph_index(self, catch_adds=False):
 
2473
        """Build a two-graph index.
 
2474
 
 
2475
        :param deltas: If true, use underlying indices with two node-ref
 
2476
            lists and 'parent' set to a delta-compressed against tail.
 
2477
        """
 
2478
        # put several versions in the index.
 
2479
        index1 = self.make_g_index('1', 0, [
 
2480
            (('tip', ), 'N0 100'),
 
2481
            (('tail', ), '')])
 
2482
        index2 = self.make_g_index('2', 0, [
 
2483
            (('parent', ), ' 100 78'),
 
2484
            (('separate', ), '')])
 
2485
        combined_index = CombinedGraphIndex([index1, index2])
 
2486
        if catch_adds:
 
2487
            self.combined_index = combined_index
 
2488
            self.caught_entries = []
 
2489
            add_callback = self.catch_add
 
2490
        else:
 
2491
            add_callback = None
 
2492
        return KnitGraphIndex(combined_index, parents=False,
 
2493
            add_callback=add_callback)
 
2494
 
 
2495
    def test_get_ancestry(self):
 
2496
        # with no parents, ancestry is always just the key.
 
2497
        index = self.two_graph_index()
 
2498
        self.assertEqual([], index.get_ancestry([]))
 
2499
        self.assertEqual(['separate'], index.get_ancestry(['separate']))
 
2500
        self.assertEqual(['tail'], index.get_ancestry(['tail']))
 
2501
        self.assertEqual(['parent'], index.get_ancestry(['parent']))
 
2502
        self.assertEqual(['tip'], index.get_ancestry(['tip']))
 
2503
        self.assertTrue(index.get_ancestry(['tip', 'separate']) in
 
2504
            (['tip', 'separate'],
 
2505
             ['separate', 'tip'],
 
2506
            ))
 
2507
        # asking for a ghost makes it go boom.
 
2508
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry, ['ghost'])
 
2509
 
 
2510
    def test_get_ancestry_with_ghosts(self):
 
2511
        index = self.two_graph_index()
 
2512
        self.assertEqual([], index.get_ancestry_with_ghosts([]))
 
2513
        self.assertEqual(['separate'], index.get_ancestry_with_ghosts(['separate']))
 
2514
        self.assertEqual(['tail'], index.get_ancestry_with_ghosts(['tail']))
 
2515
        self.assertEqual(['parent'], index.get_ancestry_with_ghosts(['parent']))
 
2516
        self.assertEqual(['tip'], index.get_ancestry_with_ghosts(['tip']))
 
2517
        self.assertTrue(index.get_ancestry_with_ghosts(['tip', 'separate']) in
 
2518
            (['tip', 'separate'],
 
2519
             ['separate', 'tip'],
 
2520
            ))
 
2521
        # asking for a ghost makes it go boom.
 
2522
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry_with_ghosts, ['ghost'])
 
2523
 
 
2524
    def test_num_versions(self):
 
2525
        index = self.two_graph_index()
 
2526
        self.assertEqual(4, index.num_versions())
 
2527
 
 
2528
    def test_get_versions(self):
 
2529
        index = self.two_graph_index()
 
2530
        self.assertEqual(set(['tail', 'tip', 'parent', 'separate']),
 
2531
            set(index.get_versions()))
 
2532
 
 
2533
    def test_has_version(self):
 
2534
        index = self.two_graph_index()
 
2535
        self.assertTrue(index.has_version('tail'))
 
2536
        self.assertFalse(index.has_version('ghost'))
 
2537
 
 
2538
    def test_get_position(self):
 
2539
        index = self.two_graph_index()
 
2540
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position('tip'))
 
2541
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position('parent'))
 
2542
 
 
2543
    def test_get_method(self):
 
2544
        index = self.two_graph_index()
 
2545
        self.assertEqual('fulltext', index.get_method('tip'))
 
2546
        self.assertEqual(['fulltext'], index.get_options('parent'))
 
2547
 
 
2548
    def test_get_options(self):
 
2549
        index = self.two_graph_index()
 
2550
        self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
 
2551
        self.assertEqual(['fulltext'], index.get_options('parent'))
 
2552
 
 
2553
    def test_get_parents_with_ghosts(self):
 
2554
        index = self.two_graph_index()
 
2555
        self.assertEqual((), index.get_parents_with_ghosts('parent'))
 
2556
        # and errors on ghosts.
 
2557
        self.assertRaises(errors.RevisionNotPresent,
 
2558
            index.get_parents_with_ghosts, 'ghost')
 
2559
 
 
2560
    def test_check_versions_present(self):
 
2561
        index = self.two_graph_index()
 
2562
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
 
2563
            ['missing'])
 
2564
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
 
2565
            ['tail', 'missing'])
 
2566
        index.check_versions_present(['tail', 'separate'])
 
2567
 
 
2568
    def catch_add(self, entries):
 
2569
        self.caught_entries.append(entries)
 
2570
 
 
2571
    def test_add_no_callback_errors(self):
 
2572
        index = self.two_graph_index()
 
2573
        self.assertRaises(errors.ReadOnlyError, index.add_version,
 
2574
            'new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
 
2575
 
 
2576
    def test_add_version_smoke(self):
 
2577
        index = self.two_graph_index(catch_adds=True)
 
2578
        index.add_version('new', 'fulltext,no-eol', (None, 50, 60), [])
 
2579
        self.assertEqual([[(('new', ), 'N50 60')]],
 
2580
            self.caught_entries)
 
2581
 
 
2582
    def test_add_version_delta_not_delta_index(self):
 
2583
        index = self.two_graph_index(catch_adds=True)
 
2584
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2585
            'new', 'no-eol,line-delta', (None, 0, 100), [])
 
2586
        self.assertEqual([], self.caught_entries)
 
2587
 
 
2588
    def test_add_version_same_dup(self):
 
2589
        index = self.two_graph_index(catch_adds=True)
 
2590
        # options can be spelt two different ways
 
2591
        index.add_version('tip', 'fulltext,no-eol', (None, 0, 100), [])
 
2592
        index.add_version('tip', 'no-eol,fulltext', (None, 0, 100), [])
 
2593
        # but neither should have added data.
 
2594
        self.assertEqual([[], []], self.caught_entries)
 
2595
        
 
2596
    def test_add_version_different_dup(self):
 
2597
        index = self.two_graph_index(catch_adds=True)
 
2598
        # change options
 
2599
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2600
            'tip', 'no-eol,line-delta', (None, 0, 100), [])
 
2601
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2602
            'tip', 'line-delta,no-eol', (None, 0, 100), [])
 
2603
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2604
            'tip', 'fulltext', (None, 0, 100), [])
 
2605
        # position/length
 
2606
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2607
            'tip', 'fulltext,no-eol', (None, 50, 100), [])
 
2608
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2609
            'tip', 'fulltext,no-eol', (None, 0, 1000), [])
 
2610
        # parents
 
2611
        self.assertRaises(errors.KnitCorrupt, index.add_version,
 
2612
            'tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])
 
2613
        self.assertEqual([], self.caught_entries)
 
2614
        
 
2615
    def test_add_versions(self):
 
2616
        index = self.two_graph_index(catch_adds=True)
 
2617
        index.add_versions([
 
2618
                ('new', 'fulltext,no-eol', (None, 50, 60), []),
 
2619
                ('new2', 'fulltext', (None, 0, 6), []),
 
2620
                ])
 
2621
        self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
 
2622
            sorted(self.caught_entries[0]))
 
2623
        self.assertEqual(1, len(self.caught_entries))
 
2624
 
 
2625
    def test_add_versions_delta_not_delta_index(self):
 
2626
        index = self.two_graph_index(catch_adds=True)
 
2627
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2628
            [('new', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
 
2629
        self.assertEqual([], self.caught_entries)
 
2630
 
 
2631
    def test_add_versions_parents_not_parents_index(self):
 
2632
        index = self.two_graph_index(catch_adds=True)
 
2633
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2634
            [('new', 'no-eol,fulltext', (None, 0, 100), ['parent'])])
 
2635
        self.assertEqual([], self.caught_entries)
 
2636
 
 
2637
    def test_add_versions_random_id_accepted(self):
 
2638
        index = self.two_graph_index(catch_adds=True)
 
2639
        index.add_versions([], random_id=True)
 
2640
 
 
2641
    def test_add_versions_same_dup(self):
 
2642
        index = self.two_graph_index(catch_adds=True)
 
2643
        # options can be spelt two different ways
 
2644
        index.add_versions([('tip', 'fulltext,no-eol', (None, 0, 100), [])])
 
2645
        index.add_versions([('tip', 'no-eol,fulltext', (None, 0, 100), [])])
 
2646
        # but neither should have added data.
 
2647
        self.assertEqual([[], []], self.caught_entries)
 
2648
        
 
2649
    def test_add_versions_different_dup(self):
 
2650
        index = self.two_graph_index(catch_adds=True)
 
2651
        # change options
 
2652
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2653
            [('tip', 'no-eol,line-delta', (None, 0, 100), [])])
 
2654
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2655
            [('tip', 'line-delta,no-eol', (None, 0, 100), [])])
 
2656
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2657
            [('tip', 'fulltext', (None, 0, 100), [])])
 
2658
        # position/length
 
2659
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2660
            [('tip', 'fulltext,no-eol', (None, 50, 100), [])])
 
2661
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2662
            [('tip', 'fulltext,no-eol', (None, 0, 1000), [])])
 
2663
        # parents
 
2664
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2665
            [('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])])
 
2666
        # change options in the second record
 
2667
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
 
2668
            [('tip', 'fulltext,no-eol', (None, 0, 100), []),
 
2669
             ('tip', 'no-eol,line-delta', (None, 0, 100), [])])
 
2670
        self.assertEqual([], self.caught_entries)
 
2671
 
 
2672
class TestPackKnits(KnitTests):
 
2673
    """Tests that use a _PackAccess and KnitGraphIndex."""
 
2674
 
 
2675
    def test_get_data_stream_packs_ignores_pack_overhead(self):
 
2676
        # Packs have an encoding overhead that should not be included in the
 
2677
        # 'size' field of a data stream, because it is not returned by the
 
2678
        # raw_reading functions - it is why index_memo's are opaque, and
 
2679
        # get_data_stream was abusing this.
 
2680
        packname = 'test.pack'
 
2681
        transport = self.get_transport()
 
2682
        def write_data(bytes):
 
2683
            transport.append_bytes(packname, bytes)
 
2684
        writer = pack.ContainerWriter(write_data)
 
2685
        writer.begin()
 
2686
        index = InMemoryGraphIndex(2)
 
2687
        knit_index = KnitGraphIndex(index, add_callback=index.add_nodes,
 
2688
            deltas=True)
 
2689
        indices = {index:(transport, packname)}
 
2690
        access = _PackAccess(indices, writer=(writer, index))
 
2691
        k = KnitVersionedFile('test', get_transport('.'),
 
2692
            delta=True, create=True, index=knit_index, access_method=access)
 
2693
        # insert something into the knit
 
2694
        k.add_lines('text-1', [], ["foo\n"])
 
2695
        # get a data stream for it
 
2696
        stream = k.get_data_stream(['text-1'])
 
2697
        # if the stream has been incorrectly assembled, we will get a short read
 
2698
        # reading from the stream (as streams have no trailer)
 
2699
        expected_length = stream[1][0][2]
 
2700
        # we use -1 to do the read, so that if a trailer is added this test
 
2701
        # will fail and we'll adjust it to handle that case correctly, rather
 
2702
        # than allowing an over-read that is bogus.
 
2703
        self.assertEqual(expected_length, len(stream[2](-1)))
 
2704
 
 
2705
 
 
2706
class Test_StreamIndex(KnitTests):
 
2707
 
 
2708
    def get_index(self, knit, stream):
 
2709
        """Get a _StreamIndex from knit and stream."""
 
2710
        return knit._knit_from_datastream(stream)._index
 
2711
 
 
2712
    def assertIndexVersions(self, knit, versions):
 
2713
        """Check that the _StreamIndex versions are those of the stream."""
 
2714
        index = self.get_index(knit, knit.get_data_stream(versions))
 
2715
        self.assertEqual(set(index.get_versions()), set(versions))
 
2716
        # check we didn't get duplicates
 
2717
        self.assertEqual(len(index.get_versions()), len(versions))
 
2718
 
 
2719
    def assertIndexAncestry(self, knit, ancestry_versions, versions, result):
 
2720
        """Check the result of a get_ancestry call on knit."""
 
2721
        index = self.get_index(knit, knit.get_data_stream(versions))
 
2722
        self.assertEqual(
 
2723
            set(result),
 
2724
            set(index.get_ancestry(ancestry_versions, False)))
 
2725
 
 
2726
    def assertGetMethod(self, knit, versions, version, result):
 
2727
        index = self.get_index(knit, knit.get_data_stream(versions))
 
2728
        self.assertEqual(result, index.get_method(version))
 
2729
 
 
2730
    def assertGetOptions(self, knit, version, options):
 
2731
        index = self.get_index(knit, knit.get_data_stream(version))
 
2732
        self.assertEqual(options, index.get_options(version))
 
2733
 
 
2734
    def assertGetPosition(self, knit, versions, version, result):
 
2735
        index = self.get_index(knit, knit.get_data_stream(versions))
 
2736
        if result[1] is None:
 
2737
            result = (result[0], index, result[2], result[3])
 
2738
        self.assertEqual(result, index.get_position(version))
 
2739
 
 
2740
    def assertGetParentsWithGhosts(self, knit, versions, version, parents):
 
2741
        index = self.get_index(knit, knit.get_data_stream(versions))
 
2742
        self.assertEqual(parents, index.get_parents_with_ghosts(version))
 
2743
 
 
2744
    def make_knit_with_4_versions_2_dags(self):
 
2745
        knit = self.make_test_knit()
 
2746
        knit.add_lines('a', [], ["foo"])
 
2747
        knit.add_lines('b', [], [])
 
2748
        knit.add_lines('c', ['b', 'a'], [])
 
2749
        knit.add_lines_with_ghosts('d', ['e', 'f'], [])
 
2750
        return knit
 
2751
 
 
2752
    def test_versions(self):
 
2753
        """The versions of a StreamIndex are those of the datastream."""
 
2754
        knit = self.make_knit_with_4_versions_2_dags()
 
2755
        # ask for most permutations, which catches bugs like falling back to the
 
2756
        # target knit, or showing ghosts, etc.
 
2757
        self.assertIndexVersions(knit, [])
 
2758
        self.assertIndexVersions(knit, ['a'])
 
2759
        self.assertIndexVersions(knit, ['b'])
 
2760
        self.assertIndexVersions(knit, ['c'])
 
2761
        self.assertIndexVersions(knit, ['d'])
 
2762
        self.assertIndexVersions(knit, ['a', 'b'])
 
2763
        self.assertIndexVersions(knit, ['b', 'c'])
 
2764
        self.assertIndexVersions(knit, ['a', 'c'])
 
2765
        self.assertIndexVersions(knit, ['a', 'b', 'c'])
 
2766
        self.assertIndexVersions(knit, ['a', 'b', 'c', 'd'])
 
2767
 
 
2768
    def test_construct(self):
 
2769
        """Constructing a StreamIndex generates index data."""
 
2770
        data_list = [('text-a', ['fulltext'], 127, []),
 
2771
            ('text-b', ['option'], 128, ['text-c'])]
 
2772
        index = _StreamIndex(data_list, None)
 
2773
        self.assertEqual({'text-a':(['fulltext'], (0, 127), []),
 
2774
            'text-b':(['option'], (127, 127 + 128), ['text-c'])},
 
2775
            index._by_version)
 
2776
 
 
2777
    def test_get_ancestry(self):
 
2778
        knit = self.make_knit_with_4_versions_2_dags()
 
2779
        self.assertIndexAncestry(knit, ['a'], ['a'], ['a'])
 
2780
        self.assertIndexAncestry(knit, ['b'], ['b'], ['b'])
 
2781
        self.assertIndexAncestry(knit, ['c'], ['c'], ['c'])
 
2782
        self.assertIndexAncestry(knit, ['c'], ['a', 'b', 'c'],
 
2783
            set(['a', 'b', 'c']))
 
2784
        self.assertIndexAncestry(knit, ['c', 'd'], ['a', 'b', 'c', 'd'],
 
2785
            set(['a', 'b', 'c', 'd']))
 
2786
 
 
2787
    def test_get_method(self):
 
2788
        knit = self.make_knit_with_4_versions_2_dags()
 
2789
        self.assertGetMethod(knit, ['a'], 'a', 'fulltext')
 
2790
        self.assertGetMethod(knit, ['c'], 'c', 'line-delta')
 
2791
        # get_method on a basis that is not in the datastream (but in the
 
2792
        # backing knit) returns 'fulltext', because thats what we'll create as
 
2793
        # we thunk across.
 
2794
        self.assertGetMethod(knit, ['c'], 'b', 'fulltext')
 
2795
 
 
2796
    def test_get_options(self):
 
2797
        knit = self.make_knit_with_4_versions_2_dags()
 
2798
        self.assertGetOptions(knit, 'a', ['no-eol', 'fulltext'])
 
2799
        self.assertGetOptions(knit, 'c', ['line-delta'])
 
2800
 
 
2801
    def test_get_parents_with_ghosts(self):
 
2802
        knit = self.make_knit_with_4_versions_2_dags()
 
2803
        self.assertGetParentsWithGhosts(knit, ['a'], 'a', ())
 
2804
        self.assertGetParentsWithGhosts(knit, ['c'], 'c', ('b', 'a'))
 
2805
        self.assertGetParentsWithGhosts(knit, ['d'], 'd', ('e', 'f'))
 
2806
 
 
2807
    def test_get_position(self):
 
2808
        knit = self.make_knit_with_4_versions_2_dags()
 
2809
        # get_position returns (thunk_flag, index(can be None), start, end) for
 
2810
        # _StreamAccess to use.
 
2811
        self.assertGetPosition(knit, ['a'], 'a', (False, None, 0, 78))
 
2812
        self.assertGetPosition(knit, ['a', 'c'], 'c', (False, None, 78, 156))
 
2813
        # get_position on a text that is not in the datastream (but in the
 
2814
        # backing knit) returns (True, 'versionid', None, None) - and then the
 
2815
        # access object can construct the relevant data as needed.
 
2816
        self.assertGetPosition(knit, ['a', 'c'], 'b', (True, 'b', None, None))
 
2817
 
 
2818
 
 
2819
class Test_StreamAccess(KnitTests):
 
2820
 
 
2821
    def get_index_access(self, knit, stream):
 
2822
        """Get a _StreamAccess from knit and stream."""
 
2823
        knit =  knit._knit_from_datastream(stream)
 
2824
        return knit._index, knit._data._access
 
2825
 
 
2826
    def assertGetRawRecords(self, knit, versions):
 
2827
        index, access = self.get_index_access(knit,
 
2828
            knit.get_data_stream(versions))
 
2829
        # check that every version asked for can be obtained from the resulting
 
2830
        # access object.
 
2831
        # batch
 
2832
        memos = []
 
2833
        for version in versions:
 
2834
            memos.append(knit._index.get_position(version))
 
2835
        original = {}
 
2836
        for version, data in zip(
 
2837
            versions, knit._data._access.get_raw_records(memos)):
 
2838
            original[version] = data
 
2839
        memos = []
 
2840
        for version in versions:
 
2841
            memos.append(index.get_position(version))
 
2842
        streamed = {}
 
2843
        for version, data in zip(versions, access.get_raw_records(memos)):
 
2844
            streamed[version] = data
 
2845
        self.assertEqual(original, streamed)
 
2846
        # individually
 
2847
        for version in versions:
 
2848
            data = list(access.get_raw_records(
 
2849
                [index.get_position(version)]))[0]
 
2850
            self.assertEqual(original[version], data)
 
2851
 
 
2852
    def make_knit_with_two_versions(self):
 
2853
        knit = self.make_test_knit()
 
2854
        knit.add_lines('a', [], ["foo"])
 
2855
        knit.add_lines('b', [], ["bar"])
 
2856
        return knit
 
2857
 
 
2858
    def test_get_raw_records(self):
 
2859
        knit = self.make_knit_with_two_versions()
 
2860
        self.assertGetRawRecords(knit, ['a', 'b'])
 
2861
        self.assertGetRawRecords(knit, ['a'])
 
2862
        self.assertGetRawRecords(knit, ['b'])
 
2863
    
 
2864
    def test_get_raw_record_from_backing_knit(self):
 
2865
        # the thunk layer should create an artificial A on-demand when needed.
 
2866
        source_knit = self.make_test_knit(name='plain', annotate=False)
 
2867
        target_knit = self.make_test_knit(name='annotated', annotate=True)
 
2868
        source_knit.add_lines("A", [], ["Foo\n"])
 
2869
        # Give the target A, so we can try to thunk across to it.
 
2870
        target_knit.join(source_knit)
 
2871
        index, access = self.get_index_access(target_knit,
 
2872
            source_knit.get_data_stream([]))
 
2873
        raw_data = list(access.get_raw_records([(True, "A", None, None)]))[0]
 
2874
        df = GzipFile(mode='rb', fileobj=StringIO(raw_data))
 
2875
        self.assertEqual(
 
2876
            'version A 1 5d36b88bb697a2d778f024048bafabd443d74503\n'
 
2877
            'Foo\nend A\n',
 
2878
            df.read())
 
2879
 
 
2880
    def test_asking_for_thunk_stream_is_not_plain_errors(self):
 
2881
        knit = self.make_test_knit(name='annotated', annotate=True)
 
2882
        knit.add_lines("A", [], ["Foo\n"])
 
2883
        index, access = self.get_index_access(knit,
 
2884
            knit.get_data_stream([]))
 
2885
        self.assertRaises(errors.KnitCorrupt,
 
2886
            list, access.get_raw_records([(True, "A", None, None)]))
 
2887
 
 
2888
 
 
2889
class TestFormatSignatures(KnitTests):
 
2890
 
 
2891
    def test_knit_format_signatures(self):
 
2892
        """Different formats of knit have different signature strings."""
 
2893
        knit = self.make_test_knit(name='a', annotate=True)
 
2894
        self.assertEqual('knit-annotated', knit.get_format_signature())
 
2895
        knit = self.make_test_knit(name='p', annotate=False)
 
2896
        self.assertEqual('knit-plain', knit.get_format_signature())