~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-10-31 04:39:04 UTC
  • mfrom: (3565.6.16 switch_nick)
  • Revision ID: pqm@pqm.ubuntu.com-20081031043904-52fnbfrloojemvcc
(mbp) branch nickname documentation

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])