~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Martin Pool
  • Date: 2005-05-09 04:38:31 UTC
  • Revision ID: mbp@sourcefrog.net-20050509043831-d45f7832b7d4d5b0
- better message when refusing to add symlinks (from mpe)

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 sys
23
 
 
24
 
from bzrlib import (
25
 
    errors,
26
 
    generate_ids,
27
 
    knit,
28
 
    multiparent,
29
 
    osutils,
30
 
    pack,
31
 
    )
32
 
from bzrlib.errors import (
33
 
    RevisionAlreadyPresent,
34
 
    KnitHeaderError,
35
 
    RevisionNotPresent,
36
 
    NoSuchFile,
37
 
    )
38
 
from bzrlib.index import *
39
 
from bzrlib.knit import (
40
 
    AnnotatedKnitContent,
41
 
    KnitContent,
42
 
    KnitSequenceMatcher,
43
 
    KnitVersionedFiles,
44
 
    PlainKnitContent,
45
 
    _DirectPackAccess,
46
 
    _KndxIndex,
47
 
    _KnitGraphIndex,
48
 
    _KnitKeyAccess,
49
 
    make_file_factory,
50
 
    )
51
 
from bzrlib.tests import (
52
 
    Feature,
53
 
    KnownFailure,
54
 
    TestCase,
55
 
    TestCaseWithMemoryTransport,
56
 
    TestCaseWithTransport,
57
 
    TestNotApplicable,
58
 
    )
59
 
from bzrlib.transport import get_transport
60
 
from bzrlib.transport.memory import MemoryTransport
61
 
from bzrlib.tuned_gzip import GzipFile
62
 
from bzrlib.versionedfile import (
63
 
    AbsentContentFactory,
64
 
    ConstantMapper,
65
 
    RecordingVersionedFilesDecorator,
66
 
    )
67
 
 
68
 
 
69
 
class _CompiledKnitFeature(Feature):
70
 
 
71
 
    def _probe(self):
72
 
        try:
73
 
            import bzrlib._knit_load_data_c
74
 
        except ImportError:
75
 
            return False
76
 
        return True
77
 
 
78
 
    def feature_name(self):
79
 
        return 'bzrlib._knit_load_data_c'
80
 
 
81
 
CompiledKnitFeature = _CompiledKnitFeature()
82
 
 
83
 
 
84
 
class KnitContentTestsMixin(object):
85
 
 
86
 
    def test_constructor(self):
87
 
        content = self._make_content([])
88
 
 
89
 
    def test_text(self):
90
 
        content = self._make_content([])
91
 
        self.assertEqual(content.text(), [])
92
 
 
93
 
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
94
 
        self.assertEqual(content.text(), ["text1", "text2"])
95
 
 
96
 
    def test_copy(self):
97
 
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
98
 
        copy = content.copy()
99
 
        self.assertIsInstance(copy, content.__class__)
100
 
        self.assertEqual(copy.annotate(), content.annotate())
101
 
 
102
 
    def assertDerivedBlocksEqual(self, source, target, noeol=False):
103
 
        """Assert that the derived matching blocks match real output"""
104
 
        source_lines = source.splitlines(True)
105
 
        target_lines = target.splitlines(True)
106
 
        def nl(line):
107
 
            if noeol and not line.endswith('\n'):
108
 
                return line + '\n'
109
 
            else:
110
 
                return line
111
 
        source_content = self._make_content([(None, nl(l)) for l in source_lines])
112
 
        target_content = self._make_content([(None, nl(l)) for l in target_lines])
113
 
        line_delta = source_content.line_delta(target_content)
114
 
        delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
115
 
            source_lines, target_lines))
116
 
        matcher = KnitSequenceMatcher(None, source_lines, target_lines)
117
 
        matcher_blocks = list(list(matcher.get_matching_blocks()))
118
 
        self.assertEqual(matcher_blocks, delta_blocks)
119
 
 
120
 
    def test_get_line_delta_blocks(self):
121
 
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
122
 
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
123
 
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
124
 
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
125
 
        self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
126
 
        self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
127
 
        self.assertDerivedBlocksEqual(TEXT_1A, '')
128
 
        self.assertDerivedBlocksEqual('', TEXT_1A)
129
 
        self.assertDerivedBlocksEqual('', '')
130
 
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
131
 
 
132
 
    def test_get_line_delta_blocks_noeol(self):
133
 
        """Handle historical knit deltas safely
134
 
 
135
 
        Some existing knit deltas don't consider the last line to differ
136
 
        when the only difference whether it has a final newline.
137
 
 
138
 
        New knit deltas appear to always consider the last line to differ
139
 
        in this case.
140
 
        """
141
 
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
142
 
        self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
143
 
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
144
 
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
145
 
 
146
 
 
147
 
TEXT_1 = """\
148
 
Banana cup cakes:
149
 
 
150
 
- bananas
151
 
- eggs
152
 
- broken tea cups
153
 
"""
154
 
 
155
 
TEXT_1A = """\
156
 
Banana cup cake recipe
157
 
(serves 6)
158
 
 
159
 
- bananas
160
 
- eggs
161
 
- broken tea cups
162
 
- self-raising flour
163
 
"""
164
 
 
165
 
TEXT_1B = """\
166
 
Banana cup cake recipe
167
 
 
168
 
- bananas (do not use plantains!!!)
169
 
- broken tea cups
170
 
- flour
171
 
"""
172
 
 
173
 
delta_1_1a = """\
174
 
0,1,2
175
 
Banana cup cake recipe
176
 
(serves 6)
177
 
5,5,1
178
 
- self-raising flour
179
 
"""
180
 
 
181
 
TEXT_2 = """\
182
 
Boeuf bourguignon
183
 
 
184
 
- beef
185
 
- red wine
186
 
- small onions
187
 
- carrot
188
 
- mushrooms
189
 
"""
190
 
 
191
 
 
192
 
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
193
 
 
194
 
    def _make_content(self, lines):
195
 
        annotated_content = AnnotatedKnitContent(lines)
196
 
        return PlainKnitContent(annotated_content.text(), 'bogus')
197
 
 
198
 
    def test_annotate(self):
199
 
        content = self._make_content([])
200
 
        self.assertEqual(content.annotate(), [])
201
 
 
202
 
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
203
 
        self.assertEqual(content.annotate(),
204
 
            [("bogus", "text1"), ("bogus", "text2")])
205
 
 
206
 
    def test_line_delta(self):
207
 
        content1 = self._make_content([("", "a"), ("", "b")])
208
 
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
209
 
        self.assertEqual(content1.line_delta(content2),
210
 
            [(1, 2, 2, ["a", "c"])])
211
 
 
212
 
    def test_line_delta_iter(self):
213
 
        content1 = self._make_content([("", "a"), ("", "b")])
214
 
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
215
 
        it = content1.line_delta_iter(content2)
216
 
        self.assertEqual(it.next(), (1, 2, 2, ["a", "c"]))
217
 
        self.assertRaises(StopIteration, it.next)
218
 
 
219
 
 
220
 
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
221
 
 
222
 
    def _make_content(self, lines):
223
 
        return AnnotatedKnitContent(lines)
224
 
 
225
 
    def test_annotate(self):
226
 
        content = self._make_content([])
227
 
        self.assertEqual(content.annotate(), [])
228
 
 
229
 
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
230
 
        self.assertEqual(content.annotate(),
231
 
            [("origin1", "text1"), ("origin2", "text2")])
232
 
 
233
 
    def test_line_delta(self):
234
 
        content1 = self._make_content([("", "a"), ("", "b")])
235
 
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
236
 
        self.assertEqual(content1.line_delta(content2),
237
 
            [(1, 2, 2, [("", "a"), ("", "c")])])
238
 
 
239
 
    def test_line_delta_iter(self):
240
 
        content1 = self._make_content([("", "a"), ("", "b")])
241
 
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
242
 
        it = content1.line_delta_iter(content2)
243
 
        self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
244
 
        self.assertRaises(StopIteration, it.next)
245
 
 
246
 
 
247
 
class MockTransport(object):
248
 
 
249
 
    def __init__(self, file_lines=None):
250
 
        self.file_lines = file_lines
251
 
        self.calls = []
252
 
        # We have no base directory for the MockTransport
253
 
        self.base = ''
254
 
 
255
 
    def get(self, filename):
256
 
        if self.file_lines is None:
257
 
            raise NoSuchFile(filename)
258
 
        else:
259
 
            return StringIO("\n".join(self.file_lines))
260
 
 
261
 
    def readv(self, relpath, offsets):
262
 
        fp = self.get(relpath)
263
 
        for offset, size in offsets:
264
 
            fp.seek(offset)
265
 
            yield offset, fp.read(size)
266
 
 
267
 
    def __getattr__(self, name):
268
 
        def queue_call(*args, **kwargs):
269
 
            self.calls.append((name, args, kwargs))
270
 
        return queue_call
271
 
 
272
 
 
273
 
class KnitRecordAccessTestsMixin(object):
274
 
    """Tests for getting and putting knit records."""
275
 
 
276
 
    def test_add_raw_records(self):
277
 
        """Add_raw_records adds records retrievable later."""
278
 
        access = self.get_access()
279
 
        memos = access.add_raw_records([('key', 10)], '1234567890')
280
 
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
281
 
 
282
 
    def test_add_several_raw_records(self):
283
 
        """add_raw_records with many records and read some back."""
284
 
        access = self.get_access()
285
 
        memos = access.add_raw_records([('key', 10), ('key2', 2), ('key3', 5)],
286
 
            '12345678901234567')
287
 
        self.assertEqual(['1234567890', '12', '34567'],
288
 
            list(access.get_raw_records(memos)))
289
 
        self.assertEqual(['1234567890'],
290
 
            list(access.get_raw_records(memos[0:1])))
291
 
        self.assertEqual(['12'],
292
 
            list(access.get_raw_records(memos[1:2])))
293
 
        self.assertEqual(['34567'],
294
 
            list(access.get_raw_records(memos[2:3])))
295
 
        self.assertEqual(['1234567890', '34567'],
296
 
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
297
 
 
298
 
 
299
 
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
300
 
    """Tests for the .kndx implementation."""
301
 
 
302
 
    def get_access(self):
303
 
        """Get a .knit style access instance."""
304
 
        mapper = ConstantMapper("foo")
305
 
        access = _KnitKeyAccess(self.get_transport(), mapper)
306
 
        return access
307
 
    
308
 
 
309
 
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
310
 
    """Tests for the pack based access."""
311
 
 
312
 
    def get_access(self):
313
 
        return self._get_access()[0]
314
 
 
315
 
    def _get_access(self, packname='packfile', index='FOO'):
316
 
        transport = self.get_transport()
317
 
        def write_data(bytes):
318
 
            transport.append_bytes(packname, bytes)
319
 
        writer = pack.ContainerWriter(write_data)
320
 
        writer.begin()
321
 
        access = _DirectPackAccess({})
322
 
        access.set_writer(writer, index, (transport, packname))
323
 
        return access, writer
324
 
 
325
 
    def test_read_from_several_packs(self):
326
 
        access, writer = self._get_access()
327
 
        memos = []
328
 
        memos.extend(access.add_raw_records([('key', 10)], '1234567890'))
329
 
        writer.end()
330
 
        access, writer = self._get_access('pack2', 'FOOBAR')
331
 
        memos.extend(access.add_raw_records([('key', 5)], '12345'))
332
 
        writer.end()
333
 
        access, writer = self._get_access('pack3', 'BAZ')
334
 
        memos.extend(access.add_raw_records([('key', 5)], 'alpha'))
335
 
        writer.end()
336
 
        transport = self.get_transport()
337
 
        access = _DirectPackAccess({"FOO":(transport, 'packfile'),
338
 
            "FOOBAR":(transport, 'pack2'),
339
 
            "BAZ":(transport, 'pack3')})
340
 
        self.assertEqual(['1234567890', '12345', 'alpha'],
341
 
            list(access.get_raw_records(memos)))
342
 
        self.assertEqual(['1234567890'],
343
 
            list(access.get_raw_records(memos[0:1])))
344
 
        self.assertEqual(['12345'],
345
 
            list(access.get_raw_records(memos[1:2])))
346
 
        self.assertEqual(['alpha'],
347
 
            list(access.get_raw_records(memos[2:3])))
348
 
        self.assertEqual(['1234567890', 'alpha'],
349
 
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
350
 
 
351
 
    def test_set_writer(self):
352
 
        """The writer should be settable post construction."""
353
 
        access = _DirectPackAccess({})
354
 
        transport = self.get_transport()
355
 
        packname = 'packfile'
356
 
        index = 'foo'
357
 
        def write_data(bytes):
358
 
            transport.append_bytes(packname, bytes)
359
 
        writer = pack.ContainerWriter(write_data)
360
 
        writer.begin()
361
 
        access.set_writer(writer, index, (transport, packname))
362
 
        memos = access.add_raw_records([('key', 10)], '1234567890')
363
 
        writer.end()
364
 
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
365
 
 
366
 
 
367
 
class LowLevelKnitDataTests(TestCase):
368
 
 
369
 
    def create_gz_content(self, text):
370
 
        sio = StringIO()
371
 
        gz_file = gzip.GzipFile(mode='wb', fileobj=sio)
372
 
        gz_file.write(text)
373
 
        gz_file.close()
374
 
        return sio.getvalue()
375
 
 
376
 
    def test_valid_knit_data(self):
377
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
378
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
379
 
                                        'foo\n'
380
 
                                        'bar\n'
381
 
                                        'end rev-id-1\n'
382
 
                                        % (sha1sum,))
383
 
        transport = MockTransport([gz_txt])
384
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
385
 
        knit = KnitVersionedFiles(None, access)
386
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
387
 
 
388
 
        contents = list(knit._read_records_iter(records))
389
 
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'],
390
 
            '4e48e2c9a3d2ca8a708cb0cc545700544efb5021')], contents)
391
 
 
392
 
        raw_contents = list(knit._read_records_iter_raw(records))
393
 
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
394
 
 
395
 
    def test_not_enough_lines(self):
396
 
        sha1sum = osutils.sha('foo\n').hexdigest()
397
 
        # record says 2 lines data says 1
398
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
399
 
                                        'foo\n'
400
 
                                        'end rev-id-1\n'
401
 
                                        % (sha1sum,))
402
 
        transport = MockTransport([gz_txt])
403
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
404
 
        knit = KnitVersionedFiles(None, access)
405
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
406
 
        self.assertRaises(errors.KnitCorrupt, list,
407
 
            knit._read_records_iter(records))
408
 
 
409
 
        # read_records_iter_raw won't detect that sort of mismatch/corruption
410
 
        raw_contents = list(knit._read_records_iter_raw(records))
411
 
        self.assertEqual([(('rev-id-1',),  gz_txt, sha1sum)], raw_contents)
412
 
 
413
 
    def test_too_many_lines(self):
414
 
        sha1sum = osutils.sha('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 = _KnitKeyAccess(transport, ConstantMapper('filename'))
423
 
        knit = KnitVersionedFiles(None, access)
424
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
425
 
        self.assertRaises(errors.KnitCorrupt, list,
426
 
            knit._read_records_iter(records))
427
 
 
428
 
        # read_records_iter_raw won't detect that sort of mismatch/corruption
429
 
        raw_contents = list(knit._read_records_iter_raw(records))
430
 
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
431
 
 
432
 
    def test_mismatched_version_id(self):
433
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
434
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
435
 
                                        'foo\n'
436
 
                                        'bar\n'
437
 
                                        'end rev-id-1\n'
438
 
                                        % (sha1sum,))
439
 
        transport = MockTransport([gz_txt])
440
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
441
 
        knit = KnitVersionedFiles(None, access)
442
 
        # We are asking for rev-id-2, but the data is rev-id-1
443
 
        records = [(('rev-id-2',), (('rev-id-2',), 0, len(gz_txt)))]
444
 
        self.assertRaises(errors.KnitCorrupt, list,
445
 
            knit._read_records_iter(records))
446
 
 
447
 
        # read_records_iter_raw detects mismatches in the header
448
 
        self.assertRaises(errors.KnitCorrupt, list,
449
 
            knit._read_records_iter_raw(records))
450
 
 
451
 
    def test_uncompressed_data(self):
452
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
453
 
        txt = ('version rev-id-1 2 %s\n'
454
 
               'foo\n'
455
 
               'bar\n'
456
 
               'end rev-id-1\n'
457
 
               % (sha1sum,))
458
 
        transport = MockTransport([txt])
459
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
460
 
        knit = KnitVersionedFiles(None, access)
461
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(txt)))]
462
 
 
463
 
        # We don't have valid gzip data ==> corrupt
464
 
        self.assertRaises(errors.KnitCorrupt, list,
465
 
            knit._read_records_iter(records))
466
 
 
467
 
        # read_records_iter_raw will notice the bad data
468
 
        self.assertRaises(errors.KnitCorrupt, list,
469
 
            knit._read_records_iter_raw(records))
470
 
 
471
 
    def test_corrupted_data(self):
472
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
473
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
474
 
                                        'foo\n'
475
 
                                        'bar\n'
476
 
                                        'end rev-id-1\n'
477
 
                                        % (sha1sum,))
478
 
        # Change 2 bytes in the middle to \xff
479
 
        gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
480
 
        transport = MockTransport([gz_txt])
481
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
482
 
        knit = KnitVersionedFiles(None, access)
483
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
484
 
        self.assertRaises(errors.KnitCorrupt, list,
485
 
            knit._read_records_iter(records))
486
 
        # read_records_iter_raw will barf on bad gz data
487
 
        self.assertRaises(errors.KnitCorrupt, list,
488
 
            knit._read_records_iter_raw(records))
489
 
 
490
 
 
491
 
class LowLevelKnitIndexTests(TestCase):
492
 
 
493
 
    def get_knit_index(self, transport, name, mode):
494
 
        mapper = ConstantMapper(name)
495
 
        orig = knit._load_data
496
 
        def reset():
497
 
            knit._load_data = orig
498
 
        self.addCleanup(reset)
499
 
        from bzrlib._knit_load_data_py import _load_data_py
500
 
        knit._load_data = _load_data_py
501
 
        allow_writes = lambda: 'w' in mode
502
 
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
503
 
 
504
 
    def test_create_file(self):
505
 
        transport = MockTransport()
506
 
        index = self.get_knit_index(transport, "filename", "w")
507
 
        index.keys()
508
 
        call = transport.calls.pop(0)
509
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
510
 
        self.assertEqual('put_file_non_atomic', call[0])
511
 
        self.assertEqual('filename.kndx', call[1][0])
512
 
        # With no history, _KndxIndex writes a new index:
513
 
        self.assertEqual(_KndxIndex.HEADER,
514
 
            call[1][1].getvalue())
515
 
        self.assertEqual({'create_parent_dir': True}, call[2])
516
 
 
517
 
    def test_read_utf8_version_id(self):
518
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
519
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
520
 
        transport = MockTransport([
521
 
            _KndxIndex.HEADER,
522
 
            '%s option 0 1 :' % (utf8_revision_id,)
523
 
            ])
524
 
        index = self.get_knit_index(transport, "filename", "r")
525
 
        # _KndxIndex is a private class, and deals in utf8 revision_ids, not
526
 
        # Unicode revision_ids.
527
 
        self.assertEqual({(utf8_revision_id,):()},
528
 
            index.get_parent_map(index.keys()))
529
 
        self.assertFalse((unicode_revision_id,) in index.keys())
530
 
 
531
 
    def test_read_utf8_parents(self):
532
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
533
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
534
 
        transport = MockTransport([
535
 
            _KndxIndex.HEADER,
536
 
            "version option 0 1 .%s :" % (utf8_revision_id,)
537
 
            ])
538
 
        index = self.get_knit_index(transport, "filename", "r")
539
 
        self.assertEqual({("version",):((utf8_revision_id,),)},
540
 
            index.get_parent_map(index.keys()))
541
 
 
542
 
    def test_read_ignore_corrupted_lines(self):
543
 
        transport = MockTransport([
544
 
            _KndxIndex.HEADER,
545
 
            "corrupted",
546
 
            "corrupted options 0 1 .b .c ",
547
 
            "version options 0 1 :"
548
 
            ])
549
 
        index = self.get_knit_index(transport, "filename", "r")
550
 
        self.assertEqual(1, len(index.keys()))
551
 
        self.assertEqual(set([("version",)]), index.keys())
552
 
 
553
 
    def test_read_corrupted_header(self):
554
 
        transport = MockTransport(['not a bzr knit index header\n'])
555
 
        index = self.get_knit_index(transport, "filename", "r")
556
 
        self.assertRaises(KnitHeaderError, index.keys)
557
 
 
558
 
    def test_read_duplicate_entries(self):
559
 
        transport = MockTransport([
560
 
            _KndxIndex.HEADER,
561
 
            "parent options 0 1 :",
562
 
            "version options1 0 1 0 :",
563
 
            "version options2 1 2 .other :",
564
 
            "version options3 3 4 0 .other :"
565
 
            ])
566
 
        index = self.get_knit_index(transport, "filename", "r")
567
 
        self.assertEqual(2, len(index.keys()))
568
 
        # check that the index used is the first one written. (Specific
569
 
        # to KnitIndex style indices.
570
 
        self.assertEqual("1", index._dictionary_compress([("version",)]))
571
 
        self.assertEqual((("version",), 3, 4), index.get_position(("version",)))
572
 
        self.assertEqual(["options3"], index.get_options(("version",)))
573
 
        self.assertEqual({("version",):(("parent",), ("other",))},
574
 
            index.get_parent_map([("version",)]))
575
 
 
576
 
    def test_read_compressed_parents(self):
577
 
        transport = MockTransport([
578
 
            _KndxIndex.HEADER,
579
 
            "a option 0 1 :",
580
 
            "b option 0 1 0 :",
581
 
            "c option 0 1 1 0 :",
582
 
            ])
583
 
        index = self.get_knit_index(transport, "filename", "r")
584
 
        self.assertEqual({("b",):(("a",),), ("c",):(("b",), ("a",))},
585
 
            index.get_parent_map([("b",), ("c",)]))
586
 
 
587
 
    def test_write_utf8_version_id(self):
588
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
589
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
590
 
        transport = MockTransport([
591
 
            _KndxIndex.HEADER
592
 
            ])
593
 
        index = self.get_knit_index(transport, "filename", "r")
594
 
        index.add_records([
595
 
            ((utf8_revision_id,), ["option"], ((utf8_revision_id,), 0, 1), [])])
596
 
        call = transport.calls.pop(0)
597
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
598
 
        self.assertEqual('put_file_non_atomic', call[0])
599
 
        self.assertEqual('filename.kndx', call[1][0])
600
 
        # With no history, _KndxIndex writes a new index:
601
 
        self.assertEqual(_KndxIndex.HEADER +
602
 
            "\n%s option 0 1  :" % (utf8_revision_id,),
603
 
            call[1][1].getvalue())
604
 
        self.assertEqual({'create_parent_dir': True}, call[2])
605
 
 
606
 
    def test_write_utf8_parents(self):
607
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
608
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
609
 
        transport = MockTransport([
610
 
            _KndxIndex.HEADER
611
 
            ])
612
 
        index = self.get_knit_index(transport, "filename", "r")
613
 
        index.add_records([
614
 
            (("version",), ["option"], (("version",), 0, 1), [(utf8_revision_id,)])])
615
 
        call = transport.calls.pop(0)
616
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
617
 
        self.assertEqual('put_file_non_atomic', call[0])
618
 
        self.assertEqual('filename.kndx', call[1][0])
619
 
        # With no history, _KndxIndex writes a new index:
620
 
        self.assertEqual(_KndxIndex.HEADER +
621
 
            "\nversion option 0 1 .%s :" % (utf8_revision_id,),
622
 
            call[1][1].getvalue())
623
 
        self.assertEqual({'create_parent_dir': True}, call[2])
624
 
 
625
 
    def test_keys(self):
626
 
        transport = MockTransport([
627
 
            _KndxIndex.HEADER
628
 
            ])
629
 
        index = self.get_knit_index(transport, "filename", "r")
630
 
 
631
 
        self.assertEqual(set(), index.keys())
632
 
 
633
 
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
634
 
        self.assertEqual(set([("a",)]), index.keys())
635
 
 
636
 
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
637
 
        self.assertEqual(set([("a",)]), index.keys())
638
 
 
639
 
        index.add_records([(("b",), ["option"], (("b",), 0, 1), [])])
640
 
        self.assertEqual(set([("a",), ("b",)]), index.keys())
641
 
 
642
 
    def add_a_b(self, index, random_id=None):
643
 
        kwargs = {}
644
 
        if random_id is not None:
645
 
            kwargs["random_id"] = random_id
646
 
        index.add_records([
647
 
            (("a",), ["option"], (("a",), 0, 1), [("b",)]),
648
 
            (("a",), ["opt"], (("a",), 1, 2), [("c",)]),
649
 
            (("b",), ["option"], (("b",), 2, 3), [("a",)])
650
 
            ], **kwargs)
651
 
 
652
 
    def assertIndexIsAB(self, index):
653
 
        self.assertEqual({
654
 
            ('a',): (('c',),),
655
 
            ('b',): (('a',),),
656
 
            },
657
 
            index.get_parent_map(index.keys()))
658
 
        self.assertEqual((("a",), 1, 2), index.get_position(("a",)))
659
 
        self.assertEqual((("b",), 2, 3), index.get_position(("b",)))
660
 
        self.assertEqual(["opt"], index.get_options(("a",)))
661
 
 
662
 
    def test_add_versions(self):
663
 
        transport = MockTransport([
664
 
            _KndxIndex.HEADER
665
 
            ])
666
 
        index = self.get_knit_index(transport, "filename", "r")
667
 
 
668
 
        self.add_a_b(index)
669
 
        call = transport.calls.pop(0)
670
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
671
 
        self.assertEqual('put_file_non_atomic', call[0])
672
 
        self.assertEqual('filename.kndx', call[1][0])
673
 
        # With no history, _KndxIndex writes a new index:
674
 
        self.assertEqual(
675
 
            _KndxIndex.HEADER +
676
 
            "\na option 0 1 .b :"
677
 
            "\na opt 1 2 .c :"
678
 
            "\nb option 2 3 0 :",
679
 
            call[1][1].getvalue())
680
 
        self.assertEqual({'create_parent_dir': True}, call[2])
681
 
        self.assertIndexIsAB(index)
682
 
 
683
 
    def test_add_versions_random_id_is_accepted(self):
684
 
        transport = MockTransport([
685
 
            _KndxIndex.HEADER
686
 
            ])
687
 
        index = self.get_knit_index(transport, "filename", "r")
688
 
        self.add_a_b(index, random_id=True)
689
 
 
690
 
    def test_delay_create_and_add_versions(self):
691
 
        transport = MockTransport()
692
 
 
693
 
        index = self.get_knit_index(transport, "filename", "w")
694
 
        # dir_mode=0777)
695
 
        self.assertEqual([], transport.calls)
696
 
        self.add_a_b(index)
697
 
        #self.assertEqual(
698
 
        #[    {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
699
 
        #    kwargs)
700
 
        # Two calls: one during which we load the existing index (and when its
701
 
        # missing create it), then a second where we write the contents out.
702
 
        self.assertEqual(2, len(transport.calls))
703
 
        call = transport.calls.pop(0)
704
 
        self.assertEqual('put_file_non_atomic', call[0])
705
 
        self.assertEqual('filename.kndx', call[1][0])
706
 
        # With no history, _KndxIndex writes a new index:
707
 
        self.assertEqual(_KndxIndex.HEADER, call[1][1].getvalue())
708
 
        self.assertEqual({'create_parent_dir': True}, call[2])
709
 
        call = transport.calls.pop(0)
710
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
711
 
        self.assertEqual('put_file_non_atomic', call[0])
712
 
        self.assertEqual('filename.kndx', call[1][0])
713
 
        # With no history, _KndxIndex writes a new index:
714
 
        self.assertEqual(
715
 
            _KndxIndex.HEADER +
716
 
            "\na option 0 1 .b :"
717
 
            "\na opt 1 2 .c :"
718
 
            "\nb option 2 3 0 :",
719
 
            call[1][1].getvalue())
720
 
        self.assertEqual({'create_parent_dir': True}, call[2])
721
 
 
722
 
    def test_get_position(self):
723
 
        transport = MockTransport([
724
 
            _KndxIndex.HEADER,
725
 
            "a option 0 1 :",
726
 
            "b option 1 2 :"
727
 
            ])
728
 
        index = self.get_knit_index(transport, "filename", "r")
729
 
 
730
 
        self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
731
 
        self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
732
 
 
733
 
    def test_get_method(self):
734
 
        transport = MockTransport([
735
 
            _KndxIndex.HEADER,
736
 
            "a fulltext,unknown 0 1 :",
737
 
            "b unknown,line-delta 1 2 :",
738
 
            "c bad 3 4 :"
739
 
            ])
740
 
        index = self.get_knit_index(transport, "filename", "r")
741
 
 
742
 
        self.assertEqual("fulltext", index.get_method("a"))
743
 
        self.assertEqual("line-delta", index.get_method("b"))
744
 
        self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
745
 
 
746
 
    def test_get_options(self):
747
 
        transport = MockTransport([
748
 
            _KndxIndex.HEADER,
749
 
            "a opt1 0 1 :",
750
 
            "b opt2,opt3 1 2 :"
751
 
            ])
752
 
        index = self.get_knit_index(transport, "filename", "r")
753
 
 
754
 
        self.assertEqual(["opt1"], index.get_options("a"))
755
 
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
756
 
 
757
 
    def test_get_parent_map(self):
758
 
        transport = MockTransport([
759
 
            _KndxIndex.HEADER,
760
 
            "a option 0 1 :",
761
 
            "b option 1 2 0 .c :",
762
 
            "c option 1 2 1 0 .e :"
763
 
            ])
764
 
        index = self.get_knit_index(transport, "filename", "r")
765
 
 
766
 
        self.assertEqual({
767
 
            ("a",):(),
768
 
            ("b",):(("a",), ("c",)),
769
 
            ("c",):(("b",), ("a",), ("e",)),
770
 
            }, index.get_parent_map(index.keys()))
771
 
 
772
 
    def test_impossible_parent(self):
773
 
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
774
 
        transport = MockTransport([
775
 
            _KndxIndex.HEADER,
776
 
            "a option 0 1 :",
777
 
            "b option 0 1 4 :"  # We don't have a 4th record
778
 
            ])
779
 
        index = self.get_knit_index(transport, 'filename', 'r')
780
 
        try:
781
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
782
 
        except TypeError, e:
783
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
784
 
                           ' not exceptions.IndexError')
785
 
                and sys.version_info[0:2] >= (2,5)):
786
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
787
 
                                  ' raising new style exceptions with python'
788
 
                                  ' >=2.5')
789
 
            else:
790
 
                raise
791
 
 
792
 
    def test_corrupted_parent(self):
793
 
        transport = MockTransport([
794
 
            _KndxIndex.HEADER,
795
 
            "a option 0 1 :",
796
 
            "b option 0 1 :",
797
 
            "c option 0 1 1v :", # Can't have a parent of '1v'
798
 
            ])
799
 
        index = self.get_knit_index(transport, 'filename', 'r')
800
 
        try:
801
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
802
 
        except TypeError, e:
803
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
804
 
                           ' not exceptions.ValueError')
805
 
                and sys.version_info[0:2] >= (2,5)):
806
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
807
 
                                  ' raising new style exceptions with python'
808
 
                                  ' >=2.5')
809
 
            else:
810
 
                raise
811
 
 
812
 
    def test_corrupted_parent_in_list(self):
813
 
        transport = MockTransport([
814
 
            _KndxIndex.HEADER,
815
 
            "a option 0 1 :",
816
 
            "b option 0 1 :",
817
 
            "c option 0 1 1 v :", # Can't have a parent of 'v'
818
 
            ])
819
 
        index = self.get_knit_index(transport, 'filename', 'r')
820
 
        try:
821
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
822
 
        except TypeError, e:
823
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
824
 
                           ' not exceptions.ValueError')
825
 
                and sys.version_info[0:2] >= (2,5)):
826
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
827
 
                                  ' raising new style exceptions with python'
828
 
                                  ' >=2.5')
829
 
            else:
830
 
                raise
831
 
 
832
 
    def test_invalid_position(self):
833
 
        transport = MockTransport([
834
 
            _KndxIndex.HEADER,
835
 
            "a option 1v 1 :",
836
 
            ])
837
 
        index = self.get_knit_index(transport, 'filename', 'r')
838
 
        try:
839
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
840
 
        except TypeError, e:
841
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
842
 
                           ' not exceptions.ValueError')
843
 
                and sys.version_info[0:2] >= (2,5)):
844
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
845
 
                                  ' raising new style exceptions with python'
846
 
                                  ' >=2.5')
847
 
            else:
848
 
                raise
849
 
 
850
 
    def test_invalid_size(self):
851
 
        transport = MockTransport([
852
 
            _KndxIndex.HEADER,
853
 
            "a option 1 1v :",
854
 
            ])
855
 
        index = self.get_knit_index(transport, 'filename', 'r')
856
 
        try:
857
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
858
 
        except TypeError, e:
859
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
860
 
                           ' not exceptions.ValueError')
861
 
                and sys.version_info[0:2] >= (2,5)):
862
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
863
 
                                  ' raising new style exceptions with python'
864
 
                                  ' >=2.5')
865
 
            else:
866
 
                raise
867
 
 
868
 
    def test_short_line(self):
869
 
        transport = MockTransport([
870
 
            _KndxIndex.HEADER,
871
 
            "a option 0 10  :",
872
 
            "b option 10 10 0", # This line isn't terminated, ignored
873
 
            ])
874
 
        index = self.get_knit_index(transport, "filename", "r")
875
 
        self.assertEqual(set([('a',)]), index.keys())
876
 
 
877
 
    def test_skip_incomplete_record(self):
878
 
        # A line with bogus data should just be skipped
879
 
        transport = MockTransport([
880
 
            _KndxIndex.HEADER,
881
 
            "a option 0 10  :",
882
 
            "b option 10 10 0", # This line isn't terminated, ignored
883
 
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
884
 
            ])
885
 
        index = self.get_knit_index(transport, "filename", "r")
886
 
        self.assertEqual(set([('a',), ('c',)]), index.keys())
887
 
 
888
 
    def test_trailing_characters(self):
889
 
        # A line with bogus data should just be skipped
890
 
        transport = MockTransport([
891
 
            _KndxIndex.HEADER,
892
 
            "a option 0 10  :",
893
 
            "b option 10 10 0 :a", # This line has extra trailing characters
894
 
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
895
 
            ])
896
 
        index = self.get_knit_index(transport, "filename", "r")
897
 
        self.assertEqual(set([('a',), ('c',)]), index.keys())
898
 
 
899
 
 
900
 
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
901
 
 
902
 
    _test_needs_features = [CompiledKnitFeature]
903
 
 
904
 
    def get_knit_index(self, transport, name, mode):
905
 
        mapper = ConstantMapper(name)
906
 
        orig = knit._load_data
907
 
        def reset():
908
 
            knit._load_data = orig
909
 
        self.addCleanup(reset)
910
 
        from bzrlib._knit_load_data_c import _load_data_c
911
 
        knit._load_data = _load_data_c
912
 
        allow_writes = lambda: mode == 'w'
913
 
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
914
 
 
915
 
 
916
 
class KnitTests(TestCaseWithTransport):
917
 
    """Class containing knit test helper routines."""
918
 
 
919
 
    def make_test_knit(self, annotate=False, name='test'):
920
 
        mapper = ConstantMapper(name)
921
 
        return make_file_factory(annotate, mapper)(self.get_transport())
922
 
 
923
 
 
924
 
class TestBadShaError(KnitTests):
925
 
    """Tests for handling of sha errors."""
926
 
 
927
 
    def test_exception_has_text(self):
928
 
        # having the failed text included in the error allows for recovery.
929
 
        source = self.make_test_knit()
930
 
        target = self.make_test_knit(name="target")
931
 
        if not source._max_delta_chain:
932
 
            raise TestNotApplicable(
933
 
                "cannot get delta-caused sha failures without deltas.")
934
 
        # create a basis
935
 
        basis = ('basis',)
936
 
        broken = ('broken',)
937
 
        source.add_lines(basis, (), ['foo\n'])
938
 
        source.add_lines(broken, (basis,), ['foo\n', 'bar\n'])
939
 
        # Seed target with a bad basis text
940
 
        target.add_lines(basis, (), ['gam\n'])
941
 
        target.insert_record_stream(
942
 
            source.get_record_stream([broken], 'unordered', False))
943
 
        err = self.assertRaises(errors.KnitCorrupt,
944
 
            target.get_record_stream([broken], 'unordered', True).next)
945
 
        self.assertEqual(['gam\n', 'bar\n'], err.content)
946
 
        # Test for formatting with live data
947
 
        self.assertStartsWith(str(err), "Knit ")
948
 
 
949
 
 
950
 
class TestKnitIndex(KnitTests):
951
 
 
952
 
    def test_add_versions_dictionary_compresses(self):
953
 
        """Adding versions to the index should update the lookup dict"""
954
 
        knit = self.make_test_knit()
955
 
        idx = knit._index
956
 
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
957
 
        self.check_file_contents('test.kndx',
958
 
            '# bzr knit index 8\n'
959
 
            '\n'
960
 
            'a-1 fulltext 0 0  :'
961
 
            )
962
 
        idx.add_records([
963
 
            (('a-2',), ['fulltext'], (('a-2',), 0, 0), [('a-1',)]),
964
 
            (('a-3',), ['fulltext'], (('a-3',), 0, 0), [('a-2',)]),
965
 
            ])
966
 
        self.check_file_contents('test.kndx',
967
 
            '# bzr knit index 8\n'
968
 
            '\n'
969
 
            'a-1 fulltext 0 0  :\n'
970
 
            'a-2 fulltext 0 0 0 :\n'
971
 
            'a-3 fulltext 0 0 1 :'
972
 
            )
973
 
        self.assertEqual(set([('a-3',), ('a-1',), ('a-2',)]), idx.keys())
974
 
        self.assertEqual({
975
 
            ('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False)),
976
 
            ('a-2',): ((('a-2',), 0, 0), None, (('a-1',),), ('fulltext', False)),
977
 
            ('a-3',): ((('a-3',), 0, 0), None, (('a-2',),), ('fulltext', False)),
978
 
            }, idx.get_build_details(idx.keys()))
979
 
        self.assertEqual({('a-1',):(),
980
 
            ('a-2',):(('a-1',),),
981
 
            ('a-3',):(('a-2',),),},
982
 
            idx.get_parent_map(idx.keys()))
983
 
 
984
 
    def test_add_versions_fails_clean(self):
985
 
        """If add_versions fails in the middle, it restores a pristine state.
986
 
 
987
 
        Any modifications that are made to the index are reset if all versions
988
 
        cannot be added.
989
 
        """
990
 
        # This cheats a little bit by passing in a generator which will
991
 
        # raise an exception before the processing finishes
992
 
        # Other possibilities would be to have an version with the wrong number
993
 
        # of entries, or to make the backing transport unable to write any
994
 
        # files.
995
 
 
996
 
        knit = self.make_test_knit()
997
 
        idx = knit._index
998
 
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
999
 
 
1000
 
        class StopEarly(Exception):
1001
 
            pass
1002
 
 
1003
 
        def generate_failure():
1004
 
            """Add some entries and then raise an exception"""
1005
 
            yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
1006
 
            yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
1007
 
            raise StopEarly()
1008
 
 
1009
 
        # Assert the pre-condition
1010
 
        def assertA1Only():
1011
 
            self.assertEqual(set([('a-1',)]), set(idx.keys()))
1012
 
            self.assertEqual(
1013
 
                {('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False))},
1014
 
                idx.get_build_details([('a-1',)]))
1015
 
            self.assertEqual({('a-1',):()}, idx.get_parent_map(idx.keys()))
1016
 
 
1017
 
        assertA1Only()
1018
 
        self.assertRaises(StopEarly, idx.add_records, generate_failure())
1019
 
        # And it shouldn't be modified
1020
 
        assertA1Only()
1021
 
 
1022
 
    def test_knit_index_ignores_empty_files(self):
1023
 
        # There was a race condition in older bzr, where a ^C at the right time
1024
 
        # could leave an empty .kndx file, which bzr would later claim was a
1025
 
        # corrupted file since the header was not present. In reality, the file
1026
 
        # just wasn't created, so it should be ignored.
1027
 
        t = get_transport('.')
1028
 
        t.put_bytes('test.kndx', '')
1029
 
 
1030
 
        knit = self.make_test_knit()
1031
 
 
1032
 
    def test_knit_index_checks_header(self):
1033
 
        t = get_transport('.')
1034
 
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
1035
 
        k = self.make_test_knit()
1036
 
        self.assertRaises(KnitHeaderError, k.keys)
1037
 
 
1038
 
 
1039
 
class TestGraphIndexKnit(KnitTests):
1040
 
    """Tests for knits using a GraphIndex rather than a KnitIndex."""
1041
 
 
1042
 
    def make_g_index(self, name, ref_lists=0, nodes=[]):
1043
 
        builder = GraphIndexBuilder(ref_lists)
1044
 
        for node, references, value in nodes:
1045
 
            builder.add_node(node, references, value)
1046
 
        stream = builder.finish()
1047
 
        trans = self.get_transport()
1048
 
        size = trans.put_file(name, stream)
1049
 
        return GraphIndex(trans, name, size)
1050
 
 
1051
 
    def two_graph_index(self, deltas=False, catch_adds=False):
1052
 
        """Build a two-graph index.
1053
 
 
1054
 
        :param deltas: If true, use underlying indices with two node-ref
1055
 
            lists and 'parent' set to a delta-compressed against tail.
1056
 
        """
1057
 
        # build a complex graph across several indices.
1058
 
        if deltas:
1059
 
            # delta compression inn the index
1060
 
            index1 = self.make_g_index('1', 2, [
1061
 
                (('tip', ), 'N0 100', ([('parent', )], [], )),
1062
 
                (('tail', ), '', ([], []))])
1063
 
            index2 = self.make_g_index('2', 2, [
1064
 
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
1065
 
                (('separate', ), '', ([], []))])
1066
 
        else:
1067
 
            # just blob location and graph in the index.
1068
 
            index1 = self.make_g_index('1', 1, [
1069
 
                (('tip', ), 'N0 100', ([('parent', )], )),
1070
 
                (('tail', ), '', ([], ))])
1071
 
            index2 = self.make_g_index('2', 1, [
1072
 
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
1073
 
                (('separate', ), '', ([], ))])
1074
 
        combined_index = CombinedGraphIndex([index1, index2])
1075
 
        if catch_adds:
1076
 
            self.combined_index = combined_index
1077
 
            self.caught_entries = []
1078
 
            add_callback = self.catch_add
1079
 
        else:
1080
 
            add_callback = None
1081
 
        return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
1082
 
            add_callback=add_callback)
1083
 
 
1084
 
    def test_keys(self):
1085
 
        index = self.two_graph_index()
1086
 
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
1087
 
            set(index.keys()))
1088
 
 
1089
 
    def test_get_position(self):
1090
 
        index = self.two_graph_index()
1091
 
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position(('tip',)))
1092
 
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position(('parent',)))
1093
 
 
1094
 
    def test_get_method_deltas(self):
1095
 
        index = self.two_graph_index(deltas=True)
1096
 
        self.assertEqual('fulltext', index.get_method(('tip',)))
1097
 
        self.assertEqual('line-delta', index.get_method(('parent',)))
1098
 
 
1099
 
    def test_get_method_no_deltas(self):
1100
 
        # check that the parent-history lookup is ignored with deltas=False.
1101
 
        index = self.two_graph_index(deltas=False)
1102
 
        self.assertEqual('fulltext', index.get_method(('tip',)))
1103
 
        self.assertEqual('fulltext', index.get_method(('parent',)))
1104
 
 
1105
 
    def test_get_options_deltas(self):
1106
 
        index = self.two_graph_index(deltas=True)
1107
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1108
 
        self.assertEqual(['line-delta'], index.get_options(('parent',)))
1109
 
 
1110
 
    def test_get_options_no_deltas(self):
1111
 
        # check that the parent-history lookup is ignored with deltas=False.
1112
 
        index = self.two_graph_index(deltas=False)
1113
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1114
 
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
1115
 
 
1116
 
    def test_get_parent_map(self):
1117
 
        index = self.two_graph_index()
1118
 
        self.assertEqual({('parent',):(('tail',), ('ghost',))},
1119
 
            index.get_parent_map([('parent',), ('ghost',)]))
1120
 
 
1121
 
    def catch_add(self, entries):
1122
 
        self.caught_entries.append(entries)
1123
 
 
1124
 
    def test_add_no_callback_errors(self):
1125
 
        index = self.two_graph_index()
1126
 
        self.assertRaises(errors.ReadOnlyError, index.add_records,
1127
 
            [(('new',), 'fulltext,no-eol', (None, 50, 60), ['separate'])])
1128
 
 
1129
 
    def test_add_version_smoke(self):
1130
 
        index = self.two_graph_index(catch_adds=True)
1131
 
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60),
1132
 
            [('separate',)])])
1133
 
        self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
1134
 
            self.caught_entries)
1135
 
 
1136
 
    def test_add_version_delta_not_delta_index(self):
1137
 
        index = self.two_graph_index(catch_adds=True)
1138
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1139
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1140
 
        self.assertEqual([], self.caught_entries)
1141
 
 
1142
 
    def test_add_version_same_dup(self):
1143
 
        index = self.two_graph_index(catch_adds=True)
1144
 
        # options can be spelt two different ways
1145
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1146
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
1147
 
        # position/length are ignored (because each pack could have fulltext or
1148
 
        # delta, and be at a different position.
1149
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
1150
 
            [('parent',)])])
1151
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
1152
 
            [('parent',)])])
1153
 
        # but neither should have added data:
1154
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1155
 
        
1156
 
    def test_add_version_different_dup(self):
1157
 
        index = self.two_graph_index(deltas=True, catch_adds=True)
1158
 
        # change options
1159
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1160
 
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1161
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1162
 
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [('parent',)])])
1163
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1164
 
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
1165
 
        # parents
1166
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1167
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1168
 
        self.assertEqual([], self.caught_entries)
1169
 
        
1170
 
    def test_add_versions_nodeltas(self):
1171
 
        index = self.two_graph_index(catch_adds=True)
1172
 
        index.add_records([
1173
 
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
1174
 
                (('new2',), 'fulltext', (None, 0, 6), [('new',)]),
1175
 
                ])
1176
 
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
1177
 
            (('new2', ), ' 0 6', ((('new',),),))],
1178
 
            sorted(self.caught_entries[0]))
1179
 
        self.assertEqual(1, len(self.caught_entries))
1180
 
 
1181
 
    def test_add_versions_deltas(self):
1182
 
        index = self.two_graph_index(deltas=True, catch_adds=True)
1183
 
        index.add_records([
1184
 
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
1185
 
                (('new2',), 'line-delta', (None, 0, 6), [('new',)]),
1186
 
                ])
1187
 
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
1188
 
            (('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
1189
 
            sorted(self.caught_entries[0]))
1190
 
        self.assertEqual(1, len(self.caught_entries))
1191
 
 
1192
 
    def test_add_versions_delta_not_delta_index(self):
1193
 
        index = self.two_graph_index(catch_adds=True)
1194
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1195
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1196
 
        self.assertEqual([], self.caught_entries)
1197
 
 
1198
 
    def test_add_versions_random_id_accepted(self):
1199
 
        index = self.two_graph_index(catch_adds=True)
1200
 
        index.add_records([], random_id=True)
1201
 
 
1202
 
    def test_add_versions_same_dup(self):
1203
 
        index = self.two_graph_index(catch_adds=True)
1204
 
        # options can be spelt two different ways
1205
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100),
1206
 
            [('parent',)])])
1207
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100),
1208
 
            [('parent',)])])
1209
 
        # position/length are ignored (because each pack could have fulltext or
1210
 
        # delta, and be at a different position.
1211
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
1212
 
            [('parent',)])])
1213
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
1214
 
            [('parent',)])])
1215
 
        # but neither should have added data.
1216
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1217
 
        
1218
 
    def test_add_versions_different_dup(self):
1219
 
        index = self.two_graph_index(deltas=True, catch_adds=True)
1220
 
        # change options
1221
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1222
 
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1223
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1224
 
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [('parent',)])])
1225
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1226
 
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
1227
 
        # parents
1228
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1229
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1230
 
        # change options in the second record
1231
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1232
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)]),
1233
 
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1234
 
        self.assertEqual([], self.caught_entries)
1235
 
 
1236
 
 
1237
 
class TestNoParentsGraphIndexKnit(KnitTests):
1238
 
    """Tests for knits using _KnitGraphIndex with no parents."""
1239
 
 
1240
 
    def make_g_index(self, name, ref_lists=0, nodes=[]):
1241
 
        builder = GraphIndexBuilder(ref_lists)
1242
 
        for node, references in nodes:
1243
 
            builder.add_node(node, references)
1244
 
        stream = builder.finish()
1245
 
        trans = self.get_transport()
1246
 
        size = trans.put_file(name, stream)
1247
 
        return GraphIndex(trans, name, size)
1248
 
 
1249
 
    def test_parents_deltas_incompatible(self):
1250
 
        index = CombinedGraphIndex([])
1251
 
        self.assertRaises(errors.KnitError, _KnitGraphIndex, lambda:True,
1252
 
            index, deltas=True, parents=False)
1253
 
 
1254
 
    def two_graph_index(self, catch_adds=False):
1255
 
        """Build a two-graph index.
1256
 
 
1257
 
        :param deltas: If true, use underlying indices with two node-ref
1258
 
            lists and 'parent' set to a delta-compressed against tail.
1259
 
        """
1260
 
        # put several versions in the index.
1261
 
        index1 = self.make_g_index('1', 0, [
1262
 
            (('tip', ), 'N0 100'),
1263
 
            (('tail', ), '')])
1264
 
        index2 = self.make_g_index('2', 0, [
1265
 
            (('parent', ), ' 100 78'),
1266
 
            (('separate', ), '')])
1267
 
        combined_index = CombinedGraphIndex([index1, index2])
1268
 
        if catch_adds:
1269
 
            self.combined_index = combined_index
1270
 
            self.caught_entries = []
1271
 
            add_callback = self.catch_add
1272
 
        else:
1273
 
            add_callback = None
1274
 
        return _KnitGraphIndex(combined_index, lambda:True, parents=False,
1275
 
            add_callback=add_callback)
1276
 
 
1277
 
    def test_keys(self):
1278
 
        index = self.two_graph_index()
1279
 
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
1280
 
            set(index.keys()))
1281
 
 
1282
 
    def test_get_position(self):
1283
 
        index = self.two_graph_index()
1284
 
        self.assertEqual((index._graph_index._indices[0], 0, 100),
1285
 
            index.get_position(('tip',)))
1286
 
        self.assertEqual((index._graph_index._indices[1], 100, 78),
1287
 
            index.get_position(('parent',)))
1288
 
 
1289
 
    def test_get_method(self):
1290
 
        index = self.two_graph_index()
1291
 
        self.assertEqual('fulltext', index.get_method(('tip',)))
1292
 
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
1293
 
 
1294
 
    def test_get_options(self):
1295
 
        index = self.two_graph_index()
1296
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1297
 
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
1298
 
 
1299
 
    def test_get_parent_map(self):
1300
 
        index = self.two_graph_index()
1301
 
        self.assertEqual({('parent',):None},
1302
 
            index.get_parent_map([('parent',), ('ghost',)]))
1303
 
 
1304
 
    def catch_add(self, entries):
1305
 
        self.caught_entries.append(entries)
1306
 
 
1307
 
    def test_add_no_callback_errors(self):
1308
 
        index = self.two_graph_index()
1309
 
        self.assertRaises(errors.ReadOnlyError, index.add_records,
1310
 
            [(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)])])
1311
 
 
1312
 
    def test_add_version_smoke(self):
1313
 
        index = self.two_graph_index(catch_adds=True)
1314
 
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60), [])])
1315
 
        self.assertEqual([[(('new', ), 'N50 60')]],
1316
 
            self.caught_entries)
1317
 
 
1318
 
    def test_add_version_delta_not_delta_index(self):
1319
 
        index = self.two_graph_index(catch_adds=True)
1320
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1321
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [])])
1322
 
        self.assertEqual([], self.caught_entries)
1323
 
 
1324
 
    def test_add_version_same_dup(self):
1325
 
        index = self.two_graph_index(catch_adds=True)
1326
 
        # options can be spelt two different ways
1327
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1328
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
1329
 
        # position/length are ignored (because each pack could have fulltext or
1330
 
        # delta, and be at a different position.
1331
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
1332
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
1333
 
        # but neither should have added data.
1334
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1335
 
        
1336
 
    def test_add_version_different_dup(self):
1337
 
        index = self.two_graph_index(catch_adds=True)
1338
 
        # change options
1339
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1340
 
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1341
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1342
 
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
1343
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1344
 
            [(('tip',), 'fulltext', (None, 0, 100), [])])
1345
 
        # parents
1346
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1347
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1348
 
        self.assertEqual([], self.caught_entries)
1349
 
        
1350
 
    def test_add_versions(self):
1351
 
        index = self.two_graph_index(catch_adds=True)
1352
 
        index.add_records([
1353
 
                (('new',), 'fulltext,no-eol', (None, 50, 60), []),
1354
 
                (('new2',), 'fulltext', (None, 0, 6), []),
1355
 
                ])
1356
 
        self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
1357
 
            sorted(self.caught_entries[0]))
1358
 
        self.assertEqual(1, len(self.caught_entries))
1359
 
 
1360
 
    def test_add_versions_delta_not_delta_index(self):
1361
 
        index = self.two_graph_index(catch_adds=True)
1362
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1363
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1364
 
        self.assertEqual([], self.caught_entries)
1365
 
 
1366
 
    def test_add_versions_parents_not_parents_index(self):
1367
 
        index = self.two_graph_index(catch_adds=True)
1368
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1369
 
            [(('new',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
1370
 
        self.assertEqual([], self.caught_entries)
1371
 
 
1372
 
    def test_add_versions_random_id_accepted(self):
1373
 
        index = self.two_graph_index(catch_adds=True)
1374
 
        index.add_records([], random_id=True)
1375
 
 
1376
 
    def test_add_versions_same_dup(self):
1377
 
        index = self.two_graph_index(catch_adds=True)
1378
 
        # options can be spelt two different ways
1379
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1380
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
1381
 
        # position/length are ignored (because each pack could have fulltext or
1382
 
        # delta, and be at a different position.
1383
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
1384
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
1385
 
        # but neither should have added data.
1386
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1387
 
        
1388
 
    def test_add_versions_different_dup(self):
1389
 
        index = self.two_graph_index(catch_adds=True)
1390
 
        # change options
1391
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1392
 
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1393
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1394
 
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
1395
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1396
 
            [(('tip',), 'fulltext', (None, 0, 100), [])])
1397
 
        # parents
1398
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1399
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1400
 
        # change options in the second record
1401
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1402
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), []),
1403
 
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1404
 
        self.assertEqual([], self.caught_entries)
1405
 
 
1406
 
 
1407
 
class TestStacking(KnitTests):
1408
 
 
1409
 
    def get_basis_and_test_knit(self):
1410
 
        basis = self.make_test_knit(name='basis')
1411
 
        basis = RecordingVersionedFilesDecorator(basis)
1412
 
        test = self.make_test_knit(name='test')
1413
 
        test.add_fallback_versioned_files(basis)
1414
 
        return basis, test
1415
 
 
1416
 
    def test_add_fallback_versioned_files(self):
1417
 
        basis = self.make_test_knit(name='basis')
1418
 
        test = self.make_test_knit(name='test')
1419
 
        # It must not error; other tests test that the fallback is referred to
1420
 
        # when accessing data.
1421
 
        test.add_fallback_versioned_files(basis)
1422
 
 
1423
 
    def test_add_lines(self):
1424
 
        # lines added to the test are not added to the basis
1425
 
        basis, test = self.get_basis_and_test_knit()
1426
 
        key = ('foo',)
1427
 
        key_basis = ('bar',)
1428
 
        key_cross_border = ('quux',)
1429
 
        key_delta = ('zaphod',)
1430
 
        test.add_lines(key, (), ['foo\n'])
1431
 
        self.assertEqual({}, basis.get_parent_map([key]))
1432
 
        # lines added to the test that reference across the stack do a
1433
 
        # fulltext.
1434
 
        basis.add_lines(key_basis, (), ['foo\n'])
1435
 
        basis.calls = []
1436
 
        test.add_lines(key_cross_border, (key_basis,), ['foo\n'])
1437
 
        self.assertEqual('fulltext', test._index.get_method(key_cross_border))
1438
 
        self.assertEqual([("get_parent_map", set([key_basis]))], basis.calls)
1439
 
        # Subsequent adds do delta.
1440
 
        basis.calls = []
1441
 
        test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
1442
 
        self.assertEqual('line-delta', test._index.get_method(key_delta))
1443
 
        self.assertEqual([], basis.calls)
1444
 
 
1445
 
    def test_annotate(self):
1446
 
        # annotations from the test knit are answered without asking the basis
1447
 
        basis, test = self.get_basis_and_test_knit()
1448
 
        key = ('foo',)
1449
 
        key_basis = ('bar',)
1450
 
        key_missing = ('missing',)
1451
 
        test.add_lines(key, (), ['foo\n'])
1452
 
        details = test.annotate(key)
1453
 
        self.assertEqual([(key, 'foo\n')], details)
1454
 
        self.assertEqual([], basis.calls)
1455
 
        # But texts that are not in the test knit are looked for in the basis
1456
 
        # directly.
1457
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1458
 
        basis.calls = []
1459
 
        details = test.annotate(key_basis)
1460
 
        self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
1461
 
        # Not optimised to date:
1462
 
        # self.assertEqual([("annotate", key_basis)], basis.calls)
1463
 
        self.assertEqual([('get_parent_map', set([key_basis])),
1464
 
            ('get_parent_map', set([key_basis])),
1465
 
            ('get_parent_map', set([key_basis])),
1466
 
            ('get_record_stream', [key_basis], 'unordered', True)],
1467
 
            basis.calls)
1468
 
 
1469
 
    def test_check(self):
1470
 
        # At the moment checking a stacked knit does implicitly check the
1471
 
        # fallback files.  
1472
 
        basis, test = self.get_basis_and_test_knit()
1473
 
        test.check()
1474
 
 
1475
 
    def test_get_parent_map(self):
1476
 
        # parents in the test knit are answered without asking the basis
1477
 
        basis, test = self.get_basis_and_test_knit()
1478
 
        key = ('foo',)
1479
 
        key_basis = ('bar',)
1480
 
        key_missing = ('missing',)
1481
 
        test.add_lines(key, (), [])
1482
 
        parent_map = test.get_parent_map([key])
1483
 
        self.assertEqual({key: ()}, parent_map)
1484
 
        self.assertEqual([], basis.calls)
1485
 
        # But parents that are not in the test knit are looked for in the basis
1486
 
        basis.add_lines(key_basis, (), [])
1487
 
        basis.calls = []
1488
 
        parent_map = test.get_parent_map([key, key_basis, key_missing])
1489
 
        self.assertEqual({key: (),
1490
 
            key_basis: ()}, parent_map)
1491
 
        self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
1492
 
            basis.calls)
1493
 
 
1494
 
    def test_get_record_stream_unordered_fulltexts(self):
1495
 
        # records from the test knit are answered without asking the basis:
1496
 
        basis, test = self.get_basis_and_test_knit()
1497
 
        key = ('foo',)
1498
 
        key_basis = ('bar',)
1499
 
        key_missing = ('missing',)
1500
 
        test.add_lines(key, (), ['foo\n'])
1501
 
        records = list(test.get_record_stream([key], 'unordered', True))
1502
 
        self.assertEqual(1, len(records))
1503
 
        self.assertEqual([], basis.calls)
1504
 
        # Missing (from test knit) objects are retrieved from the basis:
1505
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1506
 
        basis.calls = []
1507
 
        records = list(test.get_record_stream([key_basis, key_missing],
1508
 
            'unordered', True))
1509
 
        self.assertEqual(2, len(records))
1510
 
        calls = list(basis.calls)
1511
 
        for record in records:
1512
 
            self.assertSubset([record.key], (key_basis, key_missing))
1513
 
            if record.key == key_missing:
1514
 
                self.assertIsInstance(record, AbsentContentFactory)
1515
 
            else:
1516
 
                reference = list(basis.get_record_stream([key_basis],
1517
 
                    'unordered', True))[0]
1518
 
                self.assertEqual(reference.key, record.key)
1519
 
                self.assertEqual(reference.sha1, record.sha1)
1520
 
                self.assertEqual(reference.storage_kind, record.storage_kind)
1521
 
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
1522
 
                    record.get_bytes_as(record.storage_kind))
1523
 
                self.assertEqual(reference.get_bytes_as('fulltext'),
1524
 
                    record.get_bytes_as('fulltext'))
1525
 
        # It's not strictly minimal, but it seems reasonable for now for it to
1526
 
        # ask which fallbacks have which parents.
1527
 
        self.assertEqual([
1528
 
            ("get_parent_map", set([key_basis, key_missing])),
1529
 
            ("get_record_stream", [key_basis], 'unordered', True)],
1530
 
            calls)
1531
 
 
1532
 
    def test_get_record_stream_ordered_fulltexts(self):
1533
 
        # ordering is preserved down into the fallback store.
1534
 
        basis, test = self.get_basis_and_test_knit()
1535
 
        key = ('foo',)
1536
 
        key_basis = ('bar',)
1537
 
        key_basis_2 = ('quux',)
1538
 
        key_missing = ('missing',)
1539
 
        test.add_lines(key, (key_basis,), ['foo\n'])
1540
 
        # Missing (from test knit) objects are retrieved from the basis:
1541
 
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
1542
 
        basis.add_lines(key_basis_2, (), ['quux\n'])
1543
 
        basis.calls = []
1544
 
        # ask for in non-topological order
1545
 
        records = list(test.get_record_stream(
1546
 
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
1547
 
        self.assertEqual(4, len(records))
1548
 
        results = []
1549
 
        for record in records:
1550
 
            self.assertSubset([record.key],
1551
 
                (key_basis, key_missing, key_basis_2, key))
1552
 
            if record.key == key_missing:
1553
 
                self.assertIsInstance(record, AbsentContentFactory)
1554
 
            else:
1555
 
                results.append((record.key, record.sha1, record.storage_kind,
1556
 
                    record.get_bytes_as('fulltext')))
1557
 
        calls = list(basis.calls)
1558
 
        order = [record[0] for record in results]
1559
 
        self.assertEqual([key_basis_2, key_basis, key], order)
1560
 
        for result in results:
1561
 
            if result[0] == key:
1562
 
                source = test
1563
 
            else:
1564
 
                source = basis
1565
 
            record = source.get_record_stream([result[0]], 'unordered',
1566
 
                True).next()
1567
 
            self.assertEqual(record.key, result[0])
1568
 
            self.assertEqual(record.sha1, result[1])
1569
 
            self.assertEqual(record.storage_kind, result[2])
1570
 
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
1571
 
        # It's not strictly minimal, but it seems reasonable for now for it to
1572
 
        # ask which fallbacks have which parents.
1573
 
        self.assertEqual([
1574
 
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
1575
 
            # unordered is asked for by the underlying worker as it still
1576
 
            # buffers everything while answering - which is a problem!
1577
 
            ("get_record_stream", [key_basis_2, key_basis], 'unordered', True)],
1578
 
            calls)
1579
 
 
1580
 
    def test_get_record_stream_unordered_deltas(self):
1581
 
        # records from the test knit are answered without asking the basis:
1582
 
        basis, test = self.get_basis_and_test_knit()
1583
 
        key = ('foo',)
1584
 
        key_basis = ('bar',)
1585
 
        key_missing = ('missing',)
1586
 
        test.add_lines(key, (), ['foo\n'])
1587
 
        records = list(test.get_record_stream([key], 'unordered', False))
1588
 
        self.assertEqual(1, len(records))
1589
 
        self.assertEqual([], basis.calls)
1590
 
        # Missing (from test knit) objects are retrieved from the basis:
1591
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1592
 
        basis.calls = []
1593
 
        records = list(test.get_record_stream([key_basis, key_missing],
1594
 
            'unordered', False))
1595
 
        self.assertEqual(2, len(records))
1596
 
        calls = list(basis.calls)
1597
 
        for record in records:
1598
 
            self.assertSubset([record.key], (key_basis, key_missing))
1599
 
            if record.key == key_missing:
1600
 
                self.assertIsInstance(record, AbsentContentFactory)
1601
 
            else:
1602
 
                reference = list(basis.get_record_stream([key_basis],
1603
 
                    'unordered', False))[0]
1604
 
                self.assertEqual(reference.key, record.key)
1605
 
                self.assertEqual(reference.sha1, record.sha1)
1606
 
                self.assertEqual(reference.storage_kind, record.storage_kind)
1607
 
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
1608
 
                    record.get_bytes_as(record.storage_kind))
1609
 
        # It's not strictly minimal, but it seems reasonable for now for it to
1610
 
        # ask which fallbacks have which parents.
1611
 
        self.assertEqual([
1612
 
            ("get_parent_map", set([key_basis, key_missing])),
1613
 
            ("get_record_stream", [key_basis], 'unordered', False)],
1614
 
            calls)
1615
 
 
1616
 
    def test_get_record_stream_ordered_deltas(self):
1617
 
        # ordering is preserved down into the fallback store.
1618
 
        basis, test = self.get_basis_and_test_knit()
1619
 
        key = ('foo',)
1620
 
        key_basis = ('bar',)
1621
 
        key_basis_2 = ('quux',)
1622
 
        key_missing = ('missing',)
1623
 
        test.add_lines(key, (key_basis,), ['foo\n'])
1624
 
        # Missing (from test knit) objects are retrieved from the basis:
1625
 
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
1626
 
        basis.add_lines(key_basis_2, (), ['quux\n'])
1627
 
        basis.calls = []
1628
 
        # ask for in non-topological order
1629
 
        records = list(test.get_record_stream(
1630
 
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
1631
 
        self.assertEqual(4, len(records))
1632
 
        results = []
1633
 
        for record in records:
1634
 
            self.assertSubset([record.key],
1635
 
                (key_basis, key_missing, key_basis_2, key))
1636
 
            if record.key == key_missing:
1637
 
                self.assertIsInstance(record, AbsentContentFactory)
1638
 
            else:
1639
 
                results.append((record.key, record.sha1, record.storage_kind,
1640
 
                    record.get_bytes_as(record.storage_kind)))
1641
 
        calls = list(basis.calls)
1642
 
        order = [record[0] for record in results]
1643
 
        self.assertEqual([key_basis_2, key_basis, key], order)
1644
 
        for result in results:
1645
 
            if result[0] == key:
1646
 
                source = test
1647
 
            else:
1648
 
                source = basis
1649
 
            record = source.get_record_stream([result[0]], 'unordered',
1650
 
                False).next()
1651
 
            self.assertEqual(record.key, result[0])
1652
 
            self.assertEqual(record.sha1, result[1])
1653
 
            self.assertEqual(record.storage_kind, result[2])
1654
 
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
1655
 
        # It's not strictly minimal, but it seems reasonable for now for it to
1656
 
        # ask which fallbacks have which parents.
1657
 
        self.assertEqual([
1658
 
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
1659
 
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
1660
 
            calls)
1661
 
 
1662
 
    def test_get_sha1s(self):
1663
 
        # sha1's in the test knit are answered without asking the basis
1664
 
        basis, test = self.get_basis_and_test_knit()
1665
 
        key = ('foo',)
1666
 
        key_basis = ('bar',)
1667
 
        key_missing = ('missing',)
1668
 
        test.add_lines(key, (), ['foo\n'])
1669
 
        key_sha1sum = osutils.sha('foo\n').hexdigest()
1670
 
        sha1s = test.get_sha1s([key])
1671
 
        self.assertEqual({key: key_sha1sum}, sha1s)
1672
 
        self.assertEqual([], basis.calls)
1673
 
        # But texts that are not in the test knit are looked for in the basis
1674
 
        # directly (rather than via text reconstruction) so that remote servers
1675
 
        # etc don't have to answer with full content.
1676
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1677
 
        basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
1678
 
        basis.calls = []
1679
 
        sha1s = test.get_sha1s([key, key_missing, key_basis])
1680
 
        self.assertEqual({key: key_sha1sum,
1681
 
            key_basis: basis_sha1sum}, sha1s)
1682
 
        self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
1683
 
            basis.calls)
1684
 
 
1685
 
    def test_insert_record_stream(self):
1686
 
        # records are inserted as normal; insert_record_stream builds on
1687
 
        # add_lines, so a smoke test should be all that's needed:
1688
 
        key = ('foo',)
1689
 
        key_basis = ('bar',)
1690
 
        key_delta = ('zaphod',)
1691
 
        basis, test = self.get_basis_and_test_knit()
1692
 
        source = self.make_test_knit(name='source')
1693
 
        basis.add_lines(key_basis, (), ['foo\n'])
1694
 
        basis.calls = []
1695
 
        source.add_lines(key_basis, (), ['foo\n'])
1696
 
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
1697
 
        stream = source.get_record_stream([key_delta], 'unordered', False)
1698
 
        test.insert_record_stream(stream)
1699
 
        self.assertEqual([("get_parent_map", set([key_basis]))],
1700
 
            basis.calls)
1701
 
        self.assertEqual({key_delta:(key_basis,)},
1702
 
            test.get_parent_map([key_delta]))
1703
 
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
1704
 
            'unordered', True).next().get_bytes_as('fulltext'))
1705
 
 
1706
 
    def test_iter_lines_added_or_present_in_keys(self):
1707
 
        # Lines from the basis are returned, and lines for a given key are only
1708
 
        # returned once. 
1709
 
        key1 = ('foo1',)
1710
 
        key2 = ('foo2',)
1711
 
        # all sources are asked for keys:
1712
 
        basis, test = self.get_basis_and_test_knit()
1713
 
        basis.add_lines(key1, (), ["foo"])
1714
 
        basis.calls = []
1715
 
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
1716
 
        self.assertEqual([("foo\n", key1)], lines)
1717
 
        self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
1718
 
            basis.calls)
1719
 
        # keys in both are not duplicated:
1720
 
        test.add_lines(key2, (), ["bar\n"])
1721
 
        basis.add_lines(key2, (), ["bar\n"])
1722
 
        basis.calls = []
1723
 
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
1724
 
        self.assertEqual([("bar\n", key2)], lines)
1725
 
        self.assertEqual([], basis.calls)
1726
 
 
1727
 
    def test_keys(self):
1728
 
        key1 = ('foo1',)
1729
 
        key2 = ('foo2',)
1730
 
        # all sources are asked for keys:
1731
 
        basis, test = self.get_basis_and_test_knit()
1732
 
        keys = test.keys()
1733
 
        self.assertEqual(set(), set(keys))
1734
 
        self.assertEqual([("keys",)], basis.calls)
1735
 
        # keys from a basis are returned:
1736
 
        basis.add_lines(key1, (), [])
1737
 
        basis.calls = []
1738
 
        keys = test.keys()
1739
 
        self.assertEqual(set([key1]), set(keys))
1740
 
        self.assertEqual([("keys",)], basis.calls)
1741
 
        # keys in both are not duplicated:
1742
 
        test.add_lines(key2, (), [])
1743
 
        basis.add_lines(key2, (), [])
1744
 
        basis.calls = []
1745
 
        keys = test.keys()
1746
 
        self.assertEqual(2, len(keys))
1747
 
        self.assertEqual(set([key1, key2]), set(keys))
1748
 
        self.assertEqual([("keys",)], basis.calls)
1749
 
 
1750
 
    def test_add_mpdiffs(self):
1751
 
        # records are inserted as normal; add_mpdiff builds on
1752
 
        # add_lines, so a smoke test should be all that's needed:
1753
 
        key = ('foo',)
1754
 
        key_basis = ('bar',)
1755
 
        key_delta = ('zaphod',)
1756
 
        basis, test = self.get_basis_and_test_knit()
1757
 
        source = self.make_test_knit(name='source')
1758
 
        basis.add_lines(key_basis, (), ['foo\n'])
1759
 
        basis.calls = []
1760
 
        source.add_lines(key_basis, (), ['foo\n'])
1761
 
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
1762
 
        diffs = source.make_mpdiffs([key_delta])
1763
 
        test.add_mpdiffs([(key_delta, (key_basis,),
1764
 
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
1765
 
        self.assertEqual([("get_parent_map", set([key_basis])),
1766
 
            ('get_record_stream', [key_basis], 'unordered', True),
1767
 
            ('get_parent_map', set([key_basis]))],
1768
 
            basis.calls)
1769
 
        self.assertEqual({key_delta:(key_basis,)},
1770
 
            test.get_parent_map([key_delta]))
1771
 
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
1772
 
            'unordered', True).next().get_bytes_as('fulltext'))
1773
 
 
1774
 
    def test_make_mpdiffs(self):
1775
 
        # Generating an mpdiff across a stacking boundary should detect parent
1776
 
        # texts regions.
1777
 
        key = ('foo',)
1778
 
        key_left = ('bar',)
1779
 
        key_right = ('zaphod',)
1780
 
        basis, test = self.get_basis_and_test_knit()
1781
 
        basis.add_lines(key_left, (), ['bar\n'])
1782
 
        basis.add_lines(key_right, (), ['zaphod\n'])
1783
 
        basis.calls = []
1784
 
        test.add_lines(key, (key_left, key_right),
1785
 
            ['bar\n', 'foo\n', 'zaphod\n'])
1786
 
        diffs = test.make_mpdiffs([key])
1787
 
        self.assertEqual([
1788
 
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
1789
 
                multiparent.NewText(['foo\n']),
1790
 
                multiparent.ParentText(1, 0, 2, 1)])],
1791
 
            diffs)
1792
 
        self.assertEqual(4, len(basis.calls))
1793
 
        self.assertEqual([
1794
 
            ("get_parent_map", set([key_left, key_right])),
1795
 
            ("get_parent_map", set([key_left, key_right])),
1796
 
            ("get_parent_map", set([key_left, key_right])),
1797
 
            ],
1798
 
            basis.calls[:3])
1799
 
        last_call = basis.calls[3]
1800
 
        self.assertEqual('get_record_stream', last_call[0])
1801
 
        self.assertEqual(set([key_left, key_right]), set(last_call[1]))
1802
 
        self.assertEqual('unordered', last_call[2])
1803
 
        self.assertEqual(True, last_call[3])