/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: John Arbash Meinel
  • Date: 2008-11-25 18:51:48 UTC
  • mto: This revision was merged to the branch mainline in revision 3854.
  • Revision ID: john@arbash-meinel.com-20081125185148-jsfkqnzfjjqsleds
It seems we have some direct tests that don't use strings and expect a value error as well.

They would be sanitized later on by Revision. We could use that code, but this test
depends on the serializer, which Revision wouldn't know about.

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
        # we don't even need to look at the basis to see that this should be
 
1439
        # stored as a fulltext
 
1440
        self.assertEqual([], basis.calls)
 
1441
        # Subsequent adds do delta.
 
1442
        basis.calls = []
 
1443
        test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
 
1444
        self.assertEqual('line-delta', test._index.get_method(key_delta))
 
1445
        self.assertEqual([], basis.calls)
 
1446
 
 
1447
    def test_annotate(self):
 
1448
        # annotations from the test knit are answered without asking the basis
 
1449
        basis, test = self.get_basis_and_test_knit()
 
1450
        key = ('foo',)
 
1451
        key_basis = ('bar',)
 
1452
        key_missing = ('missing',)
 
1453
        test.add_lines(key, (), ['foo\n'])
 
1454
        details = test.annotate(key)
 
1455
        self.assertEqual([(key, 'foo\n')], details)
 
1456
        self.assertEqual([], basis.calls)
 
1457
        # But texts that are not in the test knit are looked for in the basis
 
1458
        # directly.
 
1459
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
1460
        basis.calls = []
 
1461
        details = test.annotate(key_basis)
 
1462
        self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
 
1463
        # Not optimised to date:
 
1464
        # self.assertEqual([("annotate", key_basis)], basis.calls)
 
1465
        self.assertEqual([('get_parent_map', set([key_basis])),
 
1466
            ('get_parent_map', set([key_basis])),
 
1467
            ('get_parent_map', set([key_basis])),
 
1468
            ('get_record_stream', [key_basis], 'unordered', True)],
 
1469
            basis.calls)
 
1470
 
 
1471
    def test_check(self):
 
1472
        # At the moment checking a stacked knit does implicitly check the
 
1473
        # fallback files.  
 
1474
        basis, test = self.get_basis_and_test_knit()
 
1475
        test.check()
 
1476
 
 
1477
    def test_get_parent_map(self):
 
1478
        # parents in the test knit are answered without asking the basis
 
1479
        basis, test = self.get_basis_and_test_knit()
 
1480
        key = ('foo',)
 
1481
        key_basis = ('bar',)
 
1482
        key_missing = ('missing',)
 
1483
        test.add_lines(key, (), [])
 
1484
        parent_map = test.get_parent_map([key])
 
1485
        self.assertEqual({key: ()}, parent_map)
 
1486
        self.assertEqual([], basis.calls)
 
1487
        # But parents that are not in the test knit are looked for in the basis
 
1488
        basis.add_lines(key_basis, (), [])
 
1489
        basis.calls = []
 
1490
        parent_map = test.get_parent_map([key, key_basis, key_missing])
 
1491
        self.assertEqual({key: (),
 
1492
            key_basis: ()}, parent_map)
 
1493
        self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
 
1494
            basis.calls)
 
1495
 
 
1496
    def test_get_record_stream_unordered_fulltexts(self):
 
1497
        # records from the test knit are answered without asking the basis:
 
1498
        basis, test = self.get_basis_and_test_knit()
 
1499
        key = ('foo',)
 
1500
        key_basis = ('bar',)
 
1501
        key_missing = ('missing',)
 
1502
        test.add_lines(key, (), ['foo\n'])
 
1503
        records = list(test.get_record_stream([key], 'unordered', True))
 
1504
        self.assertEqual(1, len(records))
 
1505
        self.assertEqual([], basis.calls)
 
1506
        # Missing (from test knit) objects are retrieved from the basis:
 
1507
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
1508
        basis.calls = []
 
1509
        records = list(test.get_record_stream([key_basis, key_missing],
 
1510
            'unordered', True))
 
1511
        self.assertEqual(2, len(records))
 
1512
        calls = list(basis.calls)
 
1513
        for record in records:
 
1514
            self.assertSubset([record.key], (key_basis, key_missing))
 
1515
            if record.key == key_missing:
 
1516
                self.assertIsInstance(record, AbsentContentFactory)
 
1517
            else:
 
1518
                reference = list(basis.get_record_stream([key_basis],
 
1519
                    'unordered', True))[0]
 
1520
                self.assertEqual(reference.key, record.key)
 
1521
                self.assertEqual(reference.sha1, record.sha1)
 
1522
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
1523
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
1524
                    record.get_bytes_as(record.storage_kind))
 
1525
                self.assertEqual(reference.get_bytes_as('fulltext'),
 
1526
                    record.get_bytes_as('fulltext'))
 
1527
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1528
        # ask which fallbacks have which parents.
 
1529
        self.assertEqual([
 
1530
            ("get_parent_map", set([key_basis, key_missing])),
 
1531
            ("get_record_stream", [key_basis], 'unordered', True)],
 
1532
            calls)
 
1533
 
 
1534
    def test_get_record_stream_ordered_fulltexts(self):
 
1535
        # ordering is preserved down into the fallback store.
 
1536
        basis, test = self.get_basis_and_test_knit()
 
1537
        key = ('foo',)
 
1538
        key_basis = ('bar',)
 
1539
        key_basis_2 = ('quux',)
 
1540
        key_missing = ('missing',)
 
1541
        test.add_lines(key, (key_basis,), ['foo\n'])
 
1542
        # Missing (from test knit) objects are retrieved from the basis:
 
1543
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
1544
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
1545
        basis.calls = []
 
1546
        # ask for in non-topological order
 
1547
        records = list(test.get_record_stream(
 
1548
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
 
1549
        self.assertEqual(4, len(records))
 
1550
        results = []
 
1551
        for record in records:
 
1552
            self.assertSubset([record.key],
 
1553
                (key_basis, key_missing, key_basis_2, key))
 
1554
            if record.key == key_missing:
 
1555
                self.assertIsInstance(record, AbsentContentFactory)
 
1556
            else:
 
1557
                results.append((record.key, record.sha1, record.storage_kind,
 
1558
                    record.get_bytes_as('fulltext')))
 
1559
        calls = list(basis.calls)
 
1560
        order = [record[0] for record in results]
 
1561
        self.assertEqual([key_basis_2, key_basis, key], order)
 
1562
        for result in results:
 
1563
            if result[0] == key:
 
1564
                source = test
 
1565
            else:
 
1566
                source = basis
 
1567
            record = source.get_record_stream([result[0]], 'unordered',
 
1568
                True).next()
 
1569
            self.assertEqual(record.key, result[0])
 
1570
            self.assertEqual(record.sha1, result[1])
 
1571
            self.assertEqual(record.storage_kind, result[2])
 
1572
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
 
1573
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1574
        # ask which fallbacks have which parents.
 
1575
        self.assertEqual([
 
1576
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
1577
            # unordered is asked for by the underlying worker as it still
 
1578
            # buffers everything while answering - which is a problem!
 
1579
            ("get_record_stream", [key_basis_2, key_basis], 'unordered', True)],
 
1580
            calls)
 
1581
 
 
1582
    def test_get_record_stream_unordered_deltas(self):
 
1583
        # records from the test knit are answered without asking the basis:
 
1584
        basis, test = self.get_basis_and_test_knit()
 
1585
        key = ('foo',)
 
1586
        key_basis = ('bar',)
 
1587
        key_missing = ('missing',)
 
1588
        test.add_lines(key, (), ['foo\n'])
 
1589
        records = list(test.get_record_stream([key], 'unordered', False))
 
1590
        self.assertEqual(1, len(records))
 
1591
        self.assertEqual([], basis.calls)
 
1592
        # Missing (from test knit) objects are retrieved from the basis:
 
1593
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
1594
        basis.calls = []
 
1595
        records = list(test.get_record_stream([key_basis, key_missing],
 
1596
            'unordered', False))
 
1597
        self.assertEqual(2, len(records))
 
1598
        calls = list(basis.calls)
 
1599
        for record in records:
 
1600
            self.assertSubset([record.key], (key_basis, key_missing))
 
1601
            if record.key == key_missing:
 
1602
                self.assertIsInstance(record, AbsentContentFactory)
 
1603
            else:
 
1604
                reference = list(basis.get_record_stream([key_basis],
 
1605
                    'unordered', False))[0]
 
1606
                self.assertEqual(reference.key, record.key)
 
1607
                self.assertEqual(reference.sha1, record.sha1)
 
1608
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
1609
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
1610
                    record.get_bytes_as(record.storage_kind))
 
1611
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1612
        # ask which fallbacks have which parents.
 
1613
        self.assertEqual([
 
1614
            ("get_parent_map", set([key_basis, key_missing])),
 
1615
            ("get_record_stream", [key_basis], 'unordered', False)],
 
1616
            calls)
 
1617
 
 
1618
    def test_get_record_stream_ordered_deltas(self):
 
1619
        # ordering is preserved down into the fallback store.
 
1620
        basis, test = self.get_basis_and_test_knit()
 
1621
        key = ('foo',)
 
1622
        key_basis = ('bar',)
 
1623
        key_basis_2 = ('quux',)
 
1624
        key_missing = ('missing',)
 
1625
        test.add_lines(key, (key_basis,), ['foo\n'])
 
1626
        # Missing (from test knit) objects are retrieved from the basis:
 
1627
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
1628
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
1629
        basis.calls = []
 
1630
        # ask for in non-topological order
 
1631
        records = list(test.get_record_stream(
 
1632
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
 
1633
        self.assertEqual(4, len(records))
 
1634
        results = []
 
1635
        for record in records:
 
1636
            self.assertSubset([record.key],
 
1637
                (key_basis, key_missing, key_basis_2, key))
 
1638
            if record.key == key_missing:
 
1639
                self.assertIsInstance(record, AbsentContentFactory)
 
1640
            else:
 
1641
                results.append((record.key, record.sha1, record.storage_kind,
 
1642
                    record.get_bytes_as(record.storage_kind)))
 
1643
        calls = list(basis.calls)
 
1644
        order = [record[0] for record in results]
 
1645
        self.assertEqual([key_basis_2, key_basis, key], order)
 
1646
        for result in results:
 
1647
            if result[0] == key:
 
1648
                source = test
 
1649
            else:
 
1650
                source = basis
 
1651
            record = source.get_record_stream([result[0]], 'unordered',
 
1652
                False).next()
 
1653
            self.assertEqual(record.key, result[0])
 
1654
            self.assertEqual(record.sha1, result[1])
 
1655
            self.assertEqual(record.storage_kind, result[2])
 
1656
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
 
1657
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1658
        # ask which fallbacks have which parents.
 
1659
        self.assertEqual([
 
1660
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
1661
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
 
1662
            calls)
 
1663
 
 
1664
    def test_get_sha1s(self):
 
1665
        # sha1's in the test knit are answered without asking the basis
 
1666
        basis, test = self.get_basis_and_test_knit()
 
1667
        key = ('foo',)
 
1668
        key_basis = ('bar',)
 
1669
        key_missing = ('missing',)
 
1670
        test.add_lines(key, (), ['foo\n'])
 
1671
        key_sha1sum = osutils.sha('foo\n').hexdigest()
 
1672
        sha1s = test.get_sha1s([key])
 
1673
        self.assertEqual({key: key_sha1sum}, sha1s)
 
1674
        self.assertEqual([], basis.calls)
 
1675
        # But texts that are not in the test knit are looked for in the basis
 
1676
        # directly (rather than via text reconstruction) so that remote servers
 
1677
        # etc don't have to answer with full content.
 
1678
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
1679
        basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
1680
        basis.calls = []
 
1681
        sha1s = test.get_sha1s([key, key_missing, key_basis])
 
1682
        self.assertEqual({key: key_sha1sum,
 
1683
            key_basis: basis_sha1sum}, sha1s)
 
1684
        self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
 
1685
            basis.calls)
 
1686
 
 
1687
    def test_insert_record_stream(self):
 
1688
        # records are inserted as normal; insert_record_stream builds on
 
1689
        # add_lines, so a smoke test should be all that's needed:
 
1690
        key = ('foo',)
 
1691
        key_basis = ('bar',)
 
1692
        key_delta = ('zaphod',)
 
1693
        basis, test = self.get_basis_and_test_knit()
 
1694
        source = self.make_test_knit(name='source')
 
1695
        basis.add_lines(key_basis, (), ['foo\n'])
 
1696
        basis.calls = []
 
1697
        source.add_lines(key_basis, (), ['foo\n'])
 
1698
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
1699
        stream = source.get_record_stream([key_delta], 'unordered', False)
 
1700
        test.insert_record_stream(stream)
 
1701
        # XXX: this does somewhat too many calls in making sure of whether it
 
1702
        # has to recreate the full text.
 
1703
        self.assertEqual([("get_parent_map", set([key_basis])),
 
1704
             ('get_parent_map', set([key_basis])),
 
1705
             ('get_record_stream', [key_basis], 'unordered', True)],
 
1706
            basis.calls)
 
1707
        self.assertEqual({key_delta:(key_basis,)},
 
1708
            test.get_parent_map([key_delta]))
 
1709
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
1710
            'unordered', True).next().get_bytes_as('fulltext'))
 
1711
 
 
1712
    def test_iter_lines_added_or_present_in_keys(self):
 
1713
        # Lines from the basis are returned, and lines for a given key are only
 
1714
        # returned once. 
 
1715
        key1 = ('foo1',)
 
1716
        key2 = ('foo2',)
 
1717
        # all sources are asked for keys:
 
1718
        basis, test = self.get_basis_and_test_knit()
 
1719
        basis.add_lines(key1, (), ["foo"])
 
1720
        basis.calls = []
 
1721
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
 
1722
        self.assertEqual([("foo\n", key1)], lines)
 
1723
        self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
 
1724
            basis.calls)
 
1725
        # keys in both are not duplicated:
 
1726
        test.add_lines(key2, (), ["bar\n"])
 
1727
        basis.add_lines(key2, (), ["bar\n"])
 
1728
        basis.calls = []
 
1729
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
 
1730
        self.assertEqual([("bar\n", key2)], lines)
 
1731
        self.assertEqual([], basis.calls)
 
1732
 
 
1733
    def test_keys(self):
 
1734
        key1 = ('foo1',)
 
1735
        key2 = ('foo2',)
 
1736
        # all sources are asked for keys:
 
1737
        basis, test = self.get_basis_and_test_knit()
 
1738
        keys = test.keys()
 
1739
        self.assertEqual(set(), set(keys))
 
1740
        self.assertEqual([("keys",)], basis.calls)
 
1741
        # keys from a basis are returned:
 
1742
        basis.add_lines(key1, (), [])
 
1743
        basis.calls = []
 
1744
        keys = test.keys()
 
1745
        self.assertEqual(set([key1]), set(keys))
 
1746
        self.assertEqual([("keys",)], basis.calls)
 
1747
        # keys in both are not duplicated:
 
1748
        test.add_lines(key2, (), [])
 
1749
        basis.add_lines(key2, (), [])
 
1750
        basis.calls = []
 
1751
        keys = test.keys()
 
1752
        self.assertEqual(2, len(keys))
 
1753
        self.assertEqual(set([key1, key2]), set(keys))
 
1754
        self.assertEqual([("keys",)], basis.calls)
 
1755
 
 
1756
    def test_add_mpdiffs(self):
 
1757
        # records are inserted as normal; add_mpdiff builds on
 
1758
        # add_lines, so a smoke test should be all that's needed:
 
1759
        key = ('foo',)
 
1760
        key_basis = ('bar',)
 
1761
        key_delta = ('zaphod',)
 
1762
        basis, test = self.get_basis_and_test_knit()
 
1763
        source = self.make_test_knit(name='source')
 
1764
        basis.add_lines(key_basis, (), ['foo\n'])
 
1765
        basis.calls = []
 
1766
        source.add_lines(key_basis, (), ['foo\n'])
 
1767
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
1768
        diffs = source.make_mpdiffs([key_delta])
 
1769
        test.add_mpdiffs([(key_delta, (key_basis,),
 
1770
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
 
1771
        self.assertEqual([("get_parent_map", set([key_basis])),
 
1772
            ('get_record_stream', [key_basis], 'unordered', True),],
 
1773
            basis.calls)
 
1774
        self.assertEqual({key_delta:(key_basis,)},
 
1775
            test.get_parent_map([key_delta]))
 
1776
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
1777
            'unordered', True).next().get_bytes_as('fulltext'))
 
1778
 
 
1779
    def test_make_mpdiffs(self):
 
1780
        # Generating an mpdiff across a stacking boundary should detect parent
 
1781
        # texts regions.
 
1782
        key = ('foo',)
 
1783
        key_left = ('bar',)
 
1784
        key_right = ('zaphod',)
 
1785
        basis, test = self.get_basis_and_test_knit()
 
1786
        basis.add_lines(key_left, (), ['bar\n'])
 
1787
        basis.add_lines(key_right, (), ['zaphod\n'])
 
1788
        basis.calls = []
 
1789
        test.add_lines(key, (key_left, key_right),
 
1790
            ['bar\n', 'foo\n', 'zaphod\n'])
 
1791
        diffs = test.make_mpdiffs([key])
 
1792
        self.assertEqual([
 
1793
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
 
1794
                multiparent.NewText(['foo\n']),
 
1795
                multiparent.ParentText(1, 0, 2, 1)])],
 
1796
            diffs)
 
1797
        self.assertEqual(3, len(basis.calls))
 
1798
        self.assertEqual([
 
1799
            ("get_parent_map", set([key_left, key_right])),
 
1800
            ("get_parent_map", set([key_left, key_right])),
 
1801
            ],
 
1802
            basis.calls[:-1])
 
1803
        last_call = basis.calls[-1]
 
1804
        self.assertEqual('get_record_stream', last_call[0])
 
1805
        self.assertEqual(set([key_left, key_right]), set(last_call[1]))
 
1806
        self.assertEqual('unordered', last_call[2])
 
1807
        self.assertEqual(True, last_call[3])