/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: Martin Pool
  • Date: 2008-07-03 10:44:34 UTC
  • mfrom: (3512.3.2 setlocale.mini)
  • mto: This revision was merged to the branch mainline in revision 3518.
  • Revision ID: mbp@sourcefrog.net-20080703104434-v4qgzvxd2wxg8etl
Set locale from environment for third party libs and day of week.

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