/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: Aaron Bentley
  • Date: 2008-10-15 18:45:28 UTC
  • mto: (3777.1.9 launchpad-login)
  • mto: This revision was merged to the branch mainline in revision 3778.
  • Revision ID: aaron@aaronbentley.com-20081015184528-mcdgasht0vz5bo8k
Use SSH default username from authentication.conf

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