~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Martin Pool
  • Date: 2005-07-22 18:05:47 UTC
  • Revision ID: mbp@sourcefrog.net-20050722180547-fbfa10a567eca667
- refactor imports and stats for hashcache

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