/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: Marius Kruger
  • Date: 2008-11-30 19:51:17 UTC
  • mto: This revision was merged to the branch mainline in revision 3876.
  • Revision ID: amanic@gmail.com-20081130195117-cjjse2l5qqy42sdz
since the -c case seems to be fixed, add a test for it

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.repofmt import pack_repo
 
52
from bzrlib.tests import (
 
53
    Feature,
 
54
    KnownFailure,
 
55
    TestCase,
 
56
    TestCaseWithMemoryTransport,
 
57
    TestCaseWithTransport,
 
58
    TestNotApplicable,
 
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 MockReadvFailingTransport(MockTransport):
 
275
    """Fail in the middle of a readv() result.
 
276
 
 
277
    This Transport will successfully yield the first two requested hunks, but
 
278
    raise NoSuchFile for the rest.
 
279
    """
 
280
 
 
281
    def readv(self, relpath, offsets):
 
282
        count = 0
 
283
        for result in MockTransport.readv(self, relpath, offsets):
 
284
            count += 1
 
285
            # we use 2 because the first offset is the pack header, the second
 
286
            # is the first actual content requset
 
287
            if count > 2:
 
288
                raise errors.NoSuchFile(relpath)
 
289
            yield result
 
290
 
 
291
 
 
292
class KnitRecordAccessTestsMixin(object):
 
293
    """Tests for getting and putting knit records."""
 
294
 
 
295
    def test_add_raw_records(self):
 
296
        """Add_raw_records adds records retrievable later."""
 
297
        access = self.get_access()
 
298
        memos = access.add_raw_records([('key', 10)], '1234567890')
 
299
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
 
300
 
 
301
    def test_add_several_raw_records(self):
 
302
        """add_raw_records with many records and read some back."""
 
303
        access = self.get_access()
 
304
        memos = access.add_raw_records([('key', 10), ('key2', 2), ('key3', 5)],
 
305
            '12345678901234567')
 
306
        self.assertEqual(['1234567890', '12', '34567'],
 
307
            list(access.get_raw_records(memos)))
 
308
        self.assertEqual(['1234567890'],
 
309
            list(access.get_raw_records(memos[0:1])))
 
310
        self.assertEqual(['12'],
 
311
            list(access.get_raw_records(memos[1:2])))
 
312
        self.assertEqual(['34567'],
 
313
            list(access.get_raw_records(memos[2:3])))
 
314
        self.assertEqual(['1234567890', '34567'],
 
315
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
316
 
 
317
 
 
318
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
319
    """Tests for the .kndx implementation."""
 
320
 
 
321
    def get_access(self):
 
322
        """Get a .knit style access instance."""
 
323
        mapper = ConstantMapper("foo")
 
324
        access = _KnitKeyAccess(self.get_transport(), mapper)
 
325
        return access
 
326
 
 
327
 
 
328
class _TestException(Exception):
 
329
    """Just an exception for local tests to use."""
 
330
 
 
331
 
 
332
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
333
    """Tests for the pack based access."""
 
334
 
 
335
    def get_access(self):
 
336
        return self._get_access()[0]
 
337
 
 
338
    def _get_access(self, packname='packfile', index='FOO'):
 
339
        transport = self.get_transport()
 
340
        def write_data(bytes):
 
341
            transport.append_bytes(packname, bytes)
 
342
        writer = pack.ContainerWriter(write_data)
 
343
        writer.begin()
 
344
        access = _DirectPackAccess({})
 
345
        access.set_writer(writer, index, (transport, packname))
 
346
        return access, writer
 
347
 
 
348
    def make_pack_file(self):
 
349
        """Create a pack file with 2 records."""
 
350
        access, writer = self._get_access(packname='packname', index='foo')
 
351
        memos = []
 
352
        memos.extend(access.add_raw_records([('key1', 10)], '1234567890'))
 
353
        memos.extend(access.add_raw_records([('key2', 5)], '12345'))
 
354
        writer.end()
 
355
        return memos
 
356
 
 
357
    def make_vf_for_retrying(self):
 
358
        """Create 3 packs and a reload function.
 
359
 
 
360
        Originally, 2 pack files will have the data, but one will be missing.
 
361
        And then the third will be used in place of the first two if reload()
 
362
        is called.
 
363
 
 
364
        :return: (versioned_file, reload_counter)
 
365
            versioned_file  a KnitVersionedFiles using the packs for access
 
366
        """
 
367
        tree = self.make_branch_and_memory_tree('tree')
 
368
        tree.lock_write()
 
369
        try:
 
370
            tree.add([''], ['root-id'])
 
371
            tree.commit('one', rev_id='rev-1')
 
372
            tree.commit('two', rev_id='rev-2')
 
373
            tree.commit('three', rev_id='rev-3')
 
374
            # Pack these two revisions into another pack file, but don't remove
 
375
            # the originials
 
376
            repo = tree.branch.repository
 
377
            collection = repo._pack_collection
 
378
            collection.ensure_loaded()
 
379
            orig_packs = collection.packs
 
380
            packer = pack_repo.Packer(collection, orig_packs, '.testpack')
 
381
            new_pack = packer.pack()
 
382
 
 
383
            vf = tree.branch.repository.revisions
 
384
        finally:
 
385
            tree.unlock()
 
386
        tree.branch.repository.lock_read()
 
387
        self.addCleanup(tree.branch.repository.unlock)
 
388
        del tree
 
389
        # Set up a reload() function that switches to using the new pack file
 
390
        new_index = new_pack.revision_index
 
391
        access_tuple = new_pack.access_tuple()
 
392
        reload_counter = [0, 0, 0]
 
393
        def reload():
 
394
            reload_counter[0] += 1
 
395
            if reload_counter[1] > 0:
 
396
                # We already reloaded, nothing more to do
 
397
                reload_counter[2] += 1
 
398
                return False
 
399
            reload_counter[1] += 1
 
400
            vf._index._graph_index._indices[:] = [new_index]
 
401
            vf._access._indices.clear()
 
402
            vf._access._indices[new_index] = access_tuple
 
403
            return True
 
404
        # Delete one of the pack files so the data will need to be reloaded. We
 
405
        # will delete the file with 'rev-2' in it
 
406
        trans, name = orig_packs[1].access_tuple()
 
407
        trans.delete(name)
 
408
        # We don't have the index trigger reloading because we want to test
 
409
        # that we reload when the .pack disappears
 
410
        vf._access._reload_func = reload
 
411
        return vf, reload_counter
 
412
 
 
413
    def make_reload_func(self, return_val=True):
 
414
        reload_called = [0]
 
415
        def reload():
 
416
            reload_called[0] += 1
 
417
            return return_val
 
418
        return reload_called, reload
 
419
 
 
420
    def make_retry_exception(self):
 
421
        # We raise a real exception so that sys.exc_info() is properly
 
422
        # populated
 
423
        try:
 
424
            raise _TestException('foobar')
 
425
        except _TestException, e:
 
426
            retry_exc = errors.RetryWithNewPacks(reload_occurred=False,
 
427
                                                 exc_info=sys.exc_info())
 
428
        return retry_exc
 
429
 
 
430
    def test_read_from_several_packs(self):
 
431
        access, writer = self._get_access()
 
432
        memos = []
 
433
        memos.extend(access.add_raw_records([('key', 10)], '1234567890'))
 
434
        writer.end()
 
435
        access, writer = self._get_access('pack2', 'FOOBAR')
 
436
        memos.extend(access.add_raw_records([('key', 5)], '12345'))
 
437
        writer.end()
 
438
        access, writer = self._get_access('pack3', 'BAZ')
 
439
        memos.extend(access.add_raw_records([('key', 5)], 'alpha'))
 
440
        writer.end()
 
441
        transport = self.get_transport()
 
442
        access = _DirectPackAccess({"FOO":(transport, 'packfile'),
 
443
            "FOOBAR":(transport, 'pack2'),
 
444
            "BAZ":(transport, 'pack3')})
 
445
        self.assertEqual(['1234567890', '12345', 'alpha'],
 
446
            list(access.get_raw_records(memos)))
 
447
        self.assertEqual(['1234567890'],
 
448
            list(access.get_raw_records(memos[0:1])))
 
449
        self.assertEqual(['12345'],
 
450
            list(access.get_raw_records(memos[1:2])))
 
451
        self.assertEqual(['alpha'],
 
452
            list(access.get_raw_records(memos[2:3])))
 
453
        self.assertEqual(['1234567890', 'alpha'],
 
454
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
455
 
 
456
    def test_set_writer(self):
 
457
        """The writer should be settable post construction."""
 
458
        access = _DirectPackAccess({})
 
459
        transport = self.get_transport()
 
460
        packname = 'packfile'
 
461
        index = 'foo'
 
462
        def write_data(bytes):
 
463
            transport.append_bytes(packname, bytes)
 
464
        writer = pack.ContainerWriter(write_data)
 
465
        writer.begin()
 
466
        access.set_writer(writer, index, (transport, packname))
 
467
        memos = access.add_raw_records([('key', 10)], '1234567890')
 
468
        writer.end()
 
469
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
 
470
 
 
471
    def test_missing_index_raises_retry(self):
 
472
        memos = self.make_pack_file()
 
473
        transport = self.get_transport()
 
474
        reload_called, reload_func = self.make_reload_func()
 
475
        # Note that the index key has changed from 'foo' to 'bar'
 
476
        access = _DirectPackAccess({'bar':(transport, 'packname')},
 
477
                                   reload_func=reload_func)
 
478
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
479
                                  access.get_raw_records, memos)
 
480
        # Because a key was passed in which does not match our index list, we
 
481
        # assume that the listing was already reloaded
 
482
        self.assertTrue(e.reload_occurred)
 
483
        self.assertIsInstance(e.exc_info, tuple)
 
484
        self.assertIs(e.exc_info[0], KeyError)
 
485
        self.assertIsInstance(e.exc_info[1], KeyError)
 
486
 
 
487
    def test_missing_index_raises_key_error_with_no_reload(self):
 
488
        memos = self.make_pack_file()
 
489
        transport = self.get_transport()
 
490
        # Note that the index key has changed from 'foo' to 'bar'
 
491
        access = _DirectPackAccess({'bar':(transport, 'packname')})
 
492
        e = self.assertListRaises(KeyError, access.get_raw_records, memos)
 
493
 
 
494
    def test_missing_file_raises_retry(self):
 
495
        memos = self.make_pack_file()
 
496
        transport = self.get_transport()
 
497
        reload_called, reload_func = self.make_reload_func()
 
498
        # Note that the 'filename' has been changed to 'different-packname'
 
499
        access = _DirectPackAccess({'foo':(transport, 'different-packname')},
 
500
                                   reload_func=reload_func)
 
501
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
502
                                  access.get_raw_records, memos)
 
503
        # The file has gone missing, so we assume we need to reload
 
504
        self.assertFalse(e.reload_occurred)
 
505
        self.assertIsInstance(e.exc_info, tuple)
 
506
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
507
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
508
        self.assertEqual('different-packname', e.exc_info[1].path)
 
509
 
 
510
    def test_missing_file_raises_no_such_file_with_no_reload(self):
 
511
        memos = self.make_pack_file()
 
512
        transport = self.get_transport()
 
513
        # Note that the 'filename' has been changed to 'different-packname'
 
514
        access = _DirectPackAccess({'foo':(transport, 'different-packname')})
 
515
        e = self.assertListRaises(errors.NoSuchFile,
 
516
                                  access.get_raw_records, memos)
 
517
 
 
518
    def test_failing_readv_raises_retry(self):
 
519
        memos = self.make_pack_file()
 
520
        transport = self.get_transport()
 
521
        failing_transport = MockReadvFailingTransport(
 
522
                                [transport.get_bytes('packname')])
 
523
        reload_called, reload_func = self.make_reload_func()
 
524
        access = _DirectPackAccess({'foo':(failing_transport, 'packname')},
 
525
                                   reload_func=reload_func)
 
526
        # Asking for a single record will not trigger the Mock failure
 
527
        self.assertEqual(['1234567890'],
 
528
            list(access.get_raw_records(memos[:1])))
 
529
        self.assertEqual(['12345'],
 
530
            list(access.get_raw_records(memos[1:2])))
 
531
        # A multiple offset readv() will fail mid-way through
 
532
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
533
                                  access.get_raw_records, memos)
 
534
        # The file has gone missing, so we assume we need to reload
 
535
        self.assertFalse(e.reload_occurred)
 
536
        self.assertIsInstance(e.exc_info, tuple)
 
537
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
538
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
539
        self.assertEqual('packname', e.exc_info[1].path)
 
540
 
 
541
    def test_failing_readv_raises_no_such_file_with_no_reload(self):
 
542
        memos = self.make_pack_file()
 
543
        transport = self.get_transport()
 
544
        failing_transport = MockReadvFailingTransport(
 
545
                                [transport.get_bytes('packname')])
 
546
        reload_called, reload_func = self.make_reload_func()
 
547
        access = _DirectPackAccess({'foo':(failing_transport, 'packname')})
 
548
        # Asking for a single record will not trigger the Mock failure
 
549
        self.assertEqual(['1234567890'],
 
550
            list(access.get_raw_records(memos[:1])))
 
551
        self.assertEqual(['12345'],
 
552
            list(access.get_raw_records(memos[1:2])))
 
553
        # A multiple offset readv() will fail mid-way through
 
554
        e = self.assertListRaises(errors.NoSuchFile,
 
555
                                  access.get_raw_records, memos)
 
556
 
 
557
    def test_reload_or_raise_no_reload(self):
 
558
        access = _DirectPackAccess({}, reload_func=None)
 
559
        retry_exc = self.make_retry_exception()
 
560
        # Without a reload_func, we will just re-raise the original exception
 
561
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
562
 
 
563
    def test_reload_or_raise_reload_changed(self):
 
564
        reload_called, reload_func = self.make_reload_func(return_val=True)
 
565
        access = _DirectPackAccess({}, reload_func=reload_func)
 
566
        retry_exc = self.make_retry_exception()
 
567
        access.reload_or_raise(retry_exc)
 
568
        self.assertEqual([1], reload_called)
 
569
        retry_exc.reload_occurred=True
 
570
        access.reload_or_raise(retry_exc)
 
571
        self.assertEqual([2], reload_called)
 
572
 
 
573
    def test_reload_or_raise_reload_no_change(self):
 
574
        reload_called, reload_func = self.make_reload_func(return_val=False)
 
575
        access = _DirectPackAccess({}, reload_func=reload_func)
 
576
        retry_exc = self.make_retry_exception()
 
577
        # If reload_occurred is False, then we consider it an error to have
 
578
        # reload_func() return False (no changes).
 
579
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
580
        self.assertEqual([1], reload_called)
 
581
        retry_exc.reload_occurred=True
 
582
        # If reload_occurred is True, then we assume nothing changed because
 
583
        # it had changed earlier, but didn't change again
 
584
        access.reload_or_raise(retry_exc)
 
585
        self.assertEqual([2], reload_called)
 
586
 
 
587
    def test_annotate_retries(self):
 
588
        vf, reload_counter = self.make_vf_for_retrying()
 
589
        # It is a little bit bogus to annotate the Revision VF, but it works,
 
590
        # as we have ancestry stored there
 
591
        key = ('rev-3',)
 
592
        reload_lines = vf.annotate(key)
 
593
        self.assertEqual([1, 1, 0], reload_counter)
 
594
        plain_lines = vf.annotate(key)
 
595
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
596
        if reload_lines != plain_lines:
 
597
            self.fail('Annotation was not identical with reloading.')
 
598
        # Now delete the packs-in-use, which should trigger another reload, but
 
599
        # this time we just raise an exception because we can't recover
 
600
        for trans, name in vf._access._indices.itervalues():
 
601
            trans.delete(name)
 
602
        self.assertRaises(errors.NoSuchFile, vf.annotate, key)
 
603
        self.assertEqual([2, 1, 1], reload_counter)
 
604
 
 
605
    def test__get_record_map_retries(self):
 
606
        vf, reload_counter = self.make_vf_for_retrying()
 
607
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
608
        records = vf._get_record_map(keys)
 
609
        self.assertEqual(keys, sorted(records.keys()))
 
610
        self.assertEqual([1, 1, 0], reload_counter)
 
611
        # Now delete the packs-in-use, which should trigger another reload, but
 
612
        # this time we just raise an exception because we can't recover
 
613
        for trans, name in vf._access._indices.itervalues():
 
614
            trans.delete(name)
 
615
        self.assertRaises(errors.NoSuchFile, vf._get_record_map, keys)
 
616
        self.assertEqual([2, 1, 1], reload_counter)
 
617
 
 
618
    def test_get_record_stream_retries(self):
 
619
        vf, reload_counter = self.make_vf_for_retrying()
 
620
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
621
        record_stream = vf.get_record_stream(keys, 'topological', False)
 
622
        record = record_stream.next()
 
623
        self.assertEqual(('rev-1',), record.key)
 
624
        self.assertEqual([0, 0, 0], reload_counter)
 
625
        record = record_stream.next()
 
626
        self.assertEqual(('rev-2',), record.key)
 
627
        self.assertEqual([1, 1, 0], reload_counter)
 
628
        record = record_stream.next()
 
629
        self.assertEqual(('rev-3',), record.key)
 
630
        self.assertEqual([1, 1, 0], reload_counter)
 
631
        # Now delete all pack files, and see that we raise the right error
 
632
        for trans, name in vf._access._indices.itervalues():
 
633
            trans.delete(name)
 
634
        self.assertListRaises(errors.NoSuchFile,
 
635
            vf.get_record_stream, keys, 'topological', False)
 
636
 
 
637
    def test_iter_lines_added_or_present_in_keys_retries(self):
 
638
        vf, reload_counter = self.make_vf_for_retrying()
 
639
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
640
        # Unfortunately, iter_lines_added_or_present_in_keys iterates the
 
641
        # result in random order (determined by the iteration order from a
 
642
        # set()), so we don't have any solid way to trigger whether data is
 
643
        # read before or after. However we tried to delete the middle node to
 
644
        # exercise the code well.
 
645
        # What we care about is that all lines are always yielded, but not
 
646
        # duplicated
 
647
        count = 0
 
648
        reload_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
649
        self.assertEqual([1, 1, 0], reload_counter)
 
650
        # Now do it again, to make sure the result is equivalent
 
651
        plain_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
652
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
653
        self.assertEqual(plain_lines, reload_lines)
 
654
        self.assertEqual(21, len(plain_lines))
 
655
        # Now delete all pack files, and see that we raise the right error
 
656
        for trans, name in vf._access._indices.itervalues():
 
657
            trans.delete(name)
 
658
        self.assertListRaises(errors.NoSuchFile,
 
659
            vf.iter_lines_added_or_present_in_keys, keys)
 
660
        self.assertEqual([2, 1, 1], reload_counter)
 
661
 
 
662
 
 
663
class LowLevelKnitDataTests(TestCase):
 
664
 
 
665
    def create_gz_content(self, text):
 
666
        sio = StringIO()
 
667
        gz_file = gzip.GzipFile(mode='wb', fileobj=sio)
 
668
        gz_file.write(text)
 
669
        gz_file.close()
 
670
        return sio.getvalue()
 
671
 
 
672
    def make_multiple_records(self):
 
673
        """Create the content for multiple records."""
 
674
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
675
        total_txt = []
 
676
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
677
                                        'foo\n'
 
678
                                        'bar\n'
 
679
                                        'end rev-id-1\n'
 
680
                                        % (sha1sum,))
 
681
        record_1 = (0, len(gz_txt), sha1sum)
 
682
        total_txt.append(gz_txt)
 
683
        sha1sum = osutils.sha('baz\n').hexdigest()
 
684
        gz_txt = self.create_gz_content('version rev-id-2 1 %s\n'
 
685
                                        'baz\n'
 
686
                                        'end rev-id-2\n'
 
687
                                        % (sha1sum,))
 
688
        record_2 = (record_1[1], len(gz_txt), sha1sum)
 
689
        total_txt.append(gz_txt)
 
690
        return total_txt, record_1, record_2
 
691
 
 
692
    def test_valid_knit_data(self):
 
693
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
694
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
695
                                        'foo\n'
 
696
                                        'bar\n'
 
697
                                        'end rev-id-1\n'
 
698
                                        % (sha1sum,))
 
699
        transport = MockTransport([gz_txt])
 
700
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
701
        knit = KnitVersionedFiles(None, access)
 
702
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
703
 
 
704
        contents = list(knit._read_records_iter(records))
 
705
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'],
 
706
            '4e48e2c9a3d2ca8a708cb0cc545700544efb5021')], contents)
 
707
 
 
708
        raw_contents = list(knit._read_records_iter_raw(records))
 
709
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
 
710
 
 
711
    def test_multiple_records_valid(self):
 
712
        total_txt, record_1, record_2 = self.make_multiple_records()
 
713
        transport = MockTransport([''.join(total_txt)])
 
714
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
715
        knit = KnitVersionedFiles(None, access)
 
716
        records = [(('rev-id-1',), (('rev-id-1',), record_1[0], record_1[1])),
 
717
                   (('rev-id-2',), (('rev-id-2',), record_2[0], record_2[1]))]
 
718
 
 
719
        contents = list(knit._read_records_iter(records))
 
720
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'], record_1[2]),
 
721
                          (('rev-id-2',), ['baz\n'], record_2[2])],
 
722
                         contents)
 
723
 
 
724
        raw_contents = list(knit._read_records_iter_raw(records))
 
725
        self.assertEqual([(('rev-id-1',), total_txt[0], record_1[2]),
 
726
                          (('rev-id-2',), total_txt[1], record_2[2])],
 
727
                         raw_contents)
 
728
 
 
729
    def test_not_enough_lines(self):
 
730
        sha1sum = osutils.sha('foo\n').hexdigest()
 
731
        # record says 2 lines data says 1
 
732
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
733
                                        'foo\n'
 
734
                                        'end rev-id-1\n'
 
735
                                        % (sha1sum,))
 
736
        transport = MockTransport([gz_txt])
 
737
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
738
        knit = KnitVersionedFiles(None, access)
 
739
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
740
        self.assertRaises(errors.KnitCorrupt, list,
 
741
            knit._read_records_iter(records))
 
742
 
 
743
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
744
        raw_contents = list(knit._read_records_iter_raw(records))
 
745
        self.assertEqual([(('rev-id-1',),  gz_txt, sha1sum)], raw_contents)
 
746
 
 
747
    def test_too_many_lines(self):
 
748
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
749
        # record says 1 lines data says 2
 
750
        gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
 
751
                                        'foo\n'
 
752
                                        'bar\n'
 
753
                                        'end rev-id-1\n'
 
754
                                        % (sha1sum,))
 
755
        transport = MockTransport([gz_txt])
 
756
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
757
        knit = KnitVersionedFiles(None, access)
 
758
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
759
        self.assertRaises(errors.KnitCorrupt, list,
 
760
            knit._read_records_iter(records))
 
761
 
 
762
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
763
        raw_contents = list(knit._read_records_iter_raw(records))
 
764
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
 
765
 
 
766
    def test_mismatched_version_id(self):
 
767
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
768
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
769
                                        'foo\n'
 
770
                                        'bar\n'
 
771
                                        'end rev-id-1\n'
 
772
                                        % (sha1sum,))
 
773
        transport = MockTransport([gz_txt])
 
774
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
775
        knit = KnitVersionedFiles(None, access)
 
776
        # We are asking for rev-id-2, but the data is rev-id-1
 
777
        records = [(('rev-id-2',), (('rev-id-2',), 0, len(gz_txt)))]
 
778
        self.assertRaises(errors.KnitCorrupt, list,
 
779
            knit._read_records_iter(records))
 
780
 
 
781
        # read_records_iter_raw detects mismatches in the header
 
782
        self.assertRaises(errors.KnitCorrupt, list,
 
783
            knit._read_records_iter_raw(records))
 
784
 
 
785
    def test_uncompressed_data(self):
 
786
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
787
        txt = ('version rev-id-1 2 %s\n'
 
788
               'foo\n'
 
789
               'bar\n'
 
790
               'end rev-id-1\n'
 
791
               % (sha1sum,))
 
792
        transport = MockTransport([txt])
 
793
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
794
        knit = KnitVersionedFiles(None, access)
 
795
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(txt)))]
 
796
 
 
797
        # We don't have valid gzip data ==> corrupt
 
798
        self.assertRaises(errors.KnitCorrupt, list,
 
799
            knit._read_records_iter(records))
 
800
 
 
801
        # read_records_iter_raw will notice the bad data
 
802
        self.assertRaises(errors.KnitCorrupt, list,
 
803
            knit._read_records_iter_raw(records))
 
804
 
 
805
    def test_corrupted_data(self):
 
806
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
807
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
808
                                        'foo\n'
 
809
                                        'bar\n'
 
810
                                        'end rev-id-1\n'
 
811
                                        % (sha1sum,))
 
812
        # Change 2 bytes in the middle to \xff
 
813
        gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
 
814
        transport = MockTransport([gz_txt])
 
815
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
816
        knit = KnitVersionedFiles(None, access)
 
817
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
818
        self.assertRaises(errors.KnitCorrupt, list,
 
819
            knit._read_records_iter(records))
 
820
        # read_records_iter_raw will barf on bad gz data
 
821
        self.assertRaises(errors.KnitCorrupt, list,
 
822
            knit._read_records_iter_raw(records))
 
823
 
 
824
 
 
825
class LowLevelKnitIndexTests(TestCase):
 
826
 
 
827
    def get_knit_index(self, transport, name, mode):
 
828
        mapper = ConstantMapper(name)
 
829
        orig = knit._load_data
 
830
        def reset():
 
831
            knit._load_data = orig
 
832
        self.addCleanup(reset)
 
833
        from bzrlib._knit_load_data_py import _load_data_py
 
834
        knit._load_data = _load_data_py
 
835
        allow_writes = lambda: 'w' in mode
 
836
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
 
837
 
 
838
    def test_create_file(self):
 
839
        transport = MockTransport()
 
840
        index = self.get_knit_index(transport, "filename", "w")
 
841
        index.keys()
 
842
        call = transport.calls.pop(0)
 
843
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
844
        self.assertEqual('put_file_non_atomic', call[0])
 
845
        self.assertEqual('filename.kndx', call[1][0])
 
846
        # With no history, _KndxIndex writes a new index:
 
847
        self.assertEqual(_KndxIndex.HEADER,
 
848
            call[1][1].getvalue())
 
849
        self.assertEqual({'create_parent_dir': True}, call[2])
 
850
 
 
851
    def test_read_utf8_version_id(self):
 
852
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
853
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
854
        transport = MockTransport([
 
855
            _KndxIndex.HEADER,
 
856
            '%s option 0 1 :' % (utf8_revision_id,)
 
857
            ])
 
858
        index = self.get_knit_index(transport, "filename", "r")
 
859
        # _KndxIndex is a private class, and deals in utf8 revision_ids, not
 
860
        # Unicode revision_ids.
 
861
        self.assertEqual({(utf8_revision_id,):()},
 
862
            index.get_parent_map(index.keys()))
 
863
        self.assertFalse((unicode_revision_id,) in index.keys())
 
864
 
 
865
    def test_read_utf8_parents(self):
 
866
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
867
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
868
        transport = MockTransport([
 
869
            _KndxIndex.HEADER,
 
870
            "version option 0 1 .%s :" % (utf8_revision_id,)
 
871
            ])
 
872
        index = self.get_knit_index(transport, "filename", "r")
 
873
        self.assertEqual({("version",):((utf8_revision_id,),)},
 
874
            index.get_parent_map(index.keys()))
 
875
 
 
876
    def test_read_ignore_corrupted_lines(self):
 
877
        transport = MockTransport([
 
878
            _KndxIndex.HEADER,
 
879
            "corrupted",
 
880
            "corrupted options 0 1 .b .c ",
 
881
            "version options 0 1 :"
 
882
            ])
 
883
        index = self.get_knit_index(transport, "filename", "r")
 
884
        self.assertEqual(1, len(index.keys()))
 
885
        self.assertEqual(set([("version",)]), index.keys())
 
886
 
 
887
    def test_read_corrupted_header(self):
 
888
        transport = MockTransport(['not a bzr knit index header\n'])
 
889
        index = self.get_knit_index(transport, "filename", "r")
 
890
        self.assertRaises(KnitHeaderError, index.keys)
 
891
 
 
892
    def test_read_duplicate_entries(self):
 
893
        transport = MockTransport([
 
894
            _KndxIndex.HEADER,
 
895
            "parent options 0 1 :",
 
896
            "version options1 0 1 0 :",
 
897
            "version options2 1 2 .other :",
 
898
            "version options3 3 4 0 .other :"
 
899
            ])
 
900
        index = self.get_knit_index(transport, "filename", "r")
 
901
        self.assertEqual(2, len(index.keys()))
 
902
        # check that the index used is the first one written. (Specific
 
903
        # to KnitIndex style indices.
 
904
        self.assertEqual("1", index._dictionary_compress([("version",)]))
 
905
        self.assertEqual((("version",), 3, 4), index.get_position(("version",)))
 
906
        self.assertEqual(["options3"], index.get_options(("version",)))
 
907
        self.assertEqual({("version",):(("parent",), ("other",))},
 
908
            index.get_parent_map([("version",)]))
 
909
 
 
910
    def test_read_compressed_parents(self):
 
911
        transport = MockTransport([
 
912
            _KndxIndex.HEADER,
 
913
            "a option 0 1 :",
 
914
            "b option 0 1 0 :",
 
915
            "c option 0 1 1 0 :",
 
916
            ])
 
917
        index = self.get_knit_index(transport, "filename", "r")
 
918
        self.assertEqual({("b",):(("a",),), ("c",):(("b",), ("a",))},
 
919
            index.get_parent_map([("b",), ("c",)]))
 
920
 
 
921
    def test_write_utf8_version_id(self):
 
922
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
923
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
924
        transport = MockTransport([
 
925
            _KndxIndex.HEADER
 
926
            ])
 
927
        index = self.get_knit_index(transport, "filename", "r")
 
928
        index.add_records([
 
929
            ((utf8_revision_id,), ["option"], ((utf8_revision_id,), 0, 1), [])])
 
930
        call = transport.calls.pop(0)
 
931
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
932
        self.assertEqual('put_file_non_atomic', call[0])
 
933
        self.assertEqual('filename.kndx', call[1][0])
 
934
        # With no history, _KndxIndex writes a new index:
 
935
        self.assertEqual(_KndxIndex.HEADER +
 
936
            "\n%s option 0 1  :" % (utf8_revision_id,),
 
937
            call[1][1].getvalue())
 
938
        self.assertEqual({'create_parent_dir': True}, call[2])
 
939
 
 
940
    def test_write_utf8_parents(self):
 
941
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
942
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
943
        transport = MockTransport([
 
944
            _KndxIndex.HEADER
 
945
            ])
 
946
        index = self.get_knit_index(transport, "filename", "r")
 
947
        index.add_records([
 
948
            (("version",), ["option"], (("version",), 0, 1), [(utf8_revision_id,)])])
 
949
        call = transport.calls.pop(0)
 
950
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
951
        self.assertEqual('put_file_non_atomic', call[0])
 
952
        self.assertEqual('filename.kndx', call[1][0])
 
953
        # With no history, _KndxIndex writes a new index:
 
954
        self.assertEqual(_KndxIndex.HEADER +
 
955
            "\nversion option 0 1 .%s :" % (utf8_revision_id,),
 
956
            call[1][1].getvalue())
 
957
        self.assertEqual({'create_parent_dir': True}, call[2])
 
958
 
 
959
    def test_keys(self):
 
960
        transport = MockTransport([
 
961
            _KndxIndex.HEADER
 
962
            ])
 
963
        index = self.get_knit_index(transport, "filename", "r")
 
964
 
 
965
        self.assertEqual(set(), index.keys())
 
966
 
 
967
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
 
968
        self.assertEqual(set([("a",)]), index.keys())
 
969
 
 
970
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
 
971
        self.assertEqual(set([("a",)]), index.keys())
 
972
 
 
973
        index.add_records([(("b",), ["option"], (("b",), 0, 1), [])])
 
974
        self.assertEqual(set([("a",), ("b",)]), index.keys())
 
975
 
 
976
    def add_a_b(self, index, random_id=None):
 
977
        kwargs = {}
 
978
        if random_id is not None:
 
979
            kwargs["random_id"] = random_id
 
980
        index.add_records([
 
981
            (("a",), ["option"], (("a",), 0, 1), [("b",)]),
 
982
            (("a",), ["opt"], (("a",), 1, 2), [("c",)]),
 
983
            (("b",), ["option"], (("b",), 2, 3), [("a",)])
 
984
            ], **kwargs)
 
985
 
 
986
    def assertIndexIsAB(self, index):
 
987
        self.assertEqual({
 
988
            ('a',): (('c',),),
 
989
            ('b',): (('a',),),
 
990
            },
 
991
            index.get_parent_map(index.keys()))
 
992
        self.assertEqual((("a",), 1, 2), index.get_position(("a",)))
 
993
        self.assertEqual((("b",), 2, 3), index.get_position(("b",)))
 
994
        self.assertEqual(["opt"], index.get_options(("a",)))
 
995
 
 
996
    def test_add_versions(self):
 
997
        transport = MockTransport([
 
998
            _KndxIndex.HEADER
 
999
            ])
 
1000
        index = self.get_knit_index(transport, "filename", "r")
 
1001
 
 
1002
        self.add_a_b(index)
 
1003
        call = transport.calls.pop(0)
 
1004
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
1005
        self.assertEqual('put_file_non_atomic', call[0])
 
1006
        self.assertEqual('filename.kndx', call[1][0])
 
1007
        # With no history, _KndxIndex writes a new index:
 
1008
        self.assertEqual(
 
1009
            _KndxIndex.HEADER +
 
1010
            "\na option 0 1 .b :"
 
1011
            "\na opt 1 2 .c :"
 
1012
            "\nb option 2 3 0 :",
 
1013
            call[1][1].getvalue())
 
1014
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1015
        self.assertIndexIsAB(index)
 
1016
 
 
1017
    def test_add_versions_random_id_is_accepted(self):
 
1018
        transport = MockTransport([
 
1019
            _KndxIndex.HEADER
 
1020
            ])
 
1021
        index = self.get_knit_index(transport, "filename", "r")
 
1022
        self.add_a_b(index, random_id=True)
 
1023
 
 
1024
    def test_delay_create_and_add_versions(self):
 
1025
        transport = MockTransport()
 
1026
 
 
1027
        index = self.get_knit_index(transport, "filename", "w")
 
1028
        # dir_mode=0777)
 
1029
        self.assertEqual([], transport.calls)
 
1030
        self.add_a_b(index)
 
1031
        #self.assertEqual(
 
1032
        #[    {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
 
1033
        #    kwargs)
 
1034
        # Two calls: one during which we load the existing index (and when its
 
1035
        # missing create it), then a second where we write the contents out.
 
1036
        self.assertEqual(2, len(transport.calls))
 
1037
        call = transport.calls.pop(0)
 
1038
        self.assertEqual('put_file_non_atomic', call[0])
 
1039
        self.assertEqual('filename.kndx', call[1][0])
 
1040
        # With no history, _KndxIndex writes a new index:
 
1041
        self.assertEqual(_KndxIndex.HEADER, call[1][1].getvalue())
 
1042
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1043
        call = transport.calls.pop(0)
 
1044
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
1045
        self.assertEqual('put_file_non_atomic', call[0])
 
1046
        self.assertEqual('filename.kndx', call[1][0])
 
1047
        # With no history, _KndxIndex writes a new index:
 
1048
        self.assertEqual(
 
1049
            _KndxIndex.HEADER +
 
1050
            "\na option 0 1 .b :"
 
1051
            "\na opt 1 2 .c :"
 
1052
            "\nb option 2 3 0 :",
 
1053
            call[1][1].getvalue())
 
1054
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1055
 
 
1056
    def test_get_position(self):
 
1057
        transport = MockTransport([
 
1058
            _KndxIndex.HEADER,
 
1059
            "a option 0 1 :",
 
1060
            "b option 1 2 :"
 
1061
            ])
 
1062
        index = self.get_knit_index(transport, "filename", "r")
 
1063
 
 
1064
        self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
 
1065
        self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
 
1066
 
 
1067
    def test_get_method(self):
 
1068
        transport = MockTransport([
 
1069
            _KndxIndex.HEADER,
 
1070
            "a fulltext,unknown 0 1 :",
 
1071
            "b unknown,line-delta 1 2 :",
 
1072
            "c bad 3 4 :"
 
1073
            ])
 
1074
        index = self.get_knit_index(transport, "filename", "r")
 
1075
 
 
1076
        self.assertEqual("fulltext", index.get_method("a"))
 
1077
        self.assertEqual("line-delta", index.get_method("b"))
 
1078
        self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
 
1079
 
 
1080
    def test_get_options(self):
 
1081
        transport = MockTransport([
 
1082
            _KndxIndex.HEADER,
 
1083
            "a opt1 0 1 :",
 
1084
            "b opt2,opt3 1 2 :"
 
1085
            ])
 
1086
        index = self.get_knit_index(transport, "filename", "r")
 
1087
 
 
1088
        self.assertEqual(["opt1"], index.get_options("a"))
 
1089
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
 
1090
 
 
1091
    def test_get_parent_map(self):
 
1092
        transport = MockTransport([
 
1093
            _KndxIndex.HEADER,
 
1094
            "a option 0 1 :",
 
1095
            "b option 1 2 0 .c :",
 
1096
            "c option 1 2 1 0 .e :"
 
1097
            ])
 
1098
        index = self.get_knit_index(transport, "filename", "r")
 
1099
 
 
1100
        self.assertEqual({
 
1101
            ("a",):(),
 
1102
            ("b",):(("a",), ("c",)),
 
1103
            ("c",):(("b",), ("a",), ("e",)),
 
1104
            }, index.get_parent_map(index.keys()))
 
1105
 
 
1106
    def test_impossible_parent(self):
 
1107
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
 
1108
        transport = MockTransport([
 
1109
            _KndxIndex.HEADER,
 
1110
            "a option 0 1 :",
 
1111
            "b option 0 1 4 :"  # We don't have a 4th record
 
1112
            ])
 
1113
        index = self.get_knit_index(transport, 'filename', 'r')
 
1114
        try:
 
1115
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1116
        except TypeError, e:
 
1117
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1118
                           ' not exceptions.IndexError')
 
1119
                and sys.version_info[0:2] >= (2,5)):
 
1120
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1121
                                  ' raising new style exceptions with python'
 
1122
                                  ' >=2.5')
 
1123
            else:
 
1124
                raise
 
1125
 
 
1126
    def test_corrupted_parent(self):
 
1127
        transport = MockTransport([
 
1128
            _KndxIndex.HEADER,
 
1129
            "a option 0 1 :",
 
1130
            "b option 0 1 :",
 
1131
            "c option 0 1 1v :", # Can't have a parent of '1v'
 
1132
            ])
 
1133
        index = self.get_knit_index(transport, 'filename', 'r')
 
1134
        try:
 
1135
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1136
        except TypeError, e:
 
1137
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1138
                           ' not exceptions.ValueError')
 
1139
                and sys.version_info[0:2] >= (2,5)):
 
1140
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1141
                                  ' raising new style exceptions with python'
 
1142
                                  ' >=2.5')
 
1143
            else:
 
1144
                raise
 
1145
 
 
1146
    def test_corrupted_parent_in_list(self):
 
1147
        transport = MockTransport([
 
1148
            _KndxIndex.HEADER,
 
1149
            "a option 0 1 :",
 
1150
            "b option 0 1 :",
 
1151
            "c option 0 1 1 v :", # Can't have a parent of 'v'
 
1152
            ])
 
1153
        index = self.get_knit_index(transport, 'filename', 'r')
 
1154
        try:
 
1155
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1156
        except TypeError, e:
 
1157
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1158
                           ' not exceptions.ValueError')
 
1159
                and sys.version_info[0:2] >= (2,5)):
 
1160
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1161
                                  ' raising new style exceptions with python'
 
1162
                                  ' >=2.5')
 
1163
            else:
 
1164
                raise
 
1165
 
 
1166
    def test_invalid_position(self):
 
1167
        transport = MockTransport([
 
1168
            _KndxIndex.HEADER,
 
1169
            "a option 1v 1 :",
 
1170
            ])
 
1171
        index = self.get_knit_index(transport, 'filename', 'r')
 
1172
        try:
 
1173
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1174
        except TypeError, e:
 
1175
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1176
                           ' not exceptions.ValueError')
 
1177
                and sys.version_info[0:2] >= (2,5)):
 
1178
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1179
                                  ' raising new style exceptions with python'
 
1180
                                  ' >=2.5')
 
1181
            else:
 
1182
                raise
 
1183
 
 
1184
    def test_invalid_size(self):
 
1185
        transport = MockTransport([
 
1186
            _KndxIndex.HEADER,
 
1187
            "a option 1 1v :",
 
1188
            ])
 
1189
        index = self.get_knit_index(transport, 'filename', 'r')
 
1190
        try:
 
1191
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1192
        except TypeError, e:
 
1193
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1194
                           ' not exceptions.ValueError')
 
1195
                and sys.version_info[0:2] >= (2,5)):
 
1196
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1197
                                  ' raising new style exceptions with python'
 
1198
                                  ' >=2.5')
 
1199
            else:
 
1200
                raise
 
1201
 
 
1202
    def test_short_line(self):
 
1203
        transport = MockTransport([
 
1204
            _KndxIndex.HEADER,
 
1205
            "a option 0 10  :",
 
1206
            "b option 10 10 0", # This line isn't terminated, ignored
 
1207
            ])
 
1208
        index = self.get_knit_index(transport, "filename", "r")
 
1209
        self.assertEqual(set([('a',)]), index.keys())
 
1210
 
 
1211
    def test_skip_incomplete_record(self):
 
1212
        # A line with bogus data should just be skipped
 
1213
        transport = MockTransport([
 
1214
            _KndxIndex.HEADER,
 
1215
            "a option 0 10  :",
 
1216
            "b option 10 10 0", # This line isn't terminated, ignored
 
1217
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1218
            ])
 
1219
        index = self.get_knit_index(transport, "filename", "r")
 
1220
        self.assertEqual(set([('a',), ('c',)]), index.keys())
 
1221
 
 
1222
    def test_trailing_characters(self):
 
1223
        # A line with bogus data should just be skipped
 
1224
        transport = MockTransport([
 
1225
            _KndxIndex.HEADER,
 
1226
            "a option 0 10  :",
 
1227
            "b option 10 10 0 :a", # This line has extra trailing characters
 
1228
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1229
            ])
 
1230
        index = self.get_knit_index(transport, "filename", "r")
 
1231
        self.assertEqual(set([('a',), ('c',)]), index.keys())
 
1232
 
 
1233
 
 
1234
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
 
1235
 
 
1236
    _test_needs_features = [CompiledKnitFeature]
 
1237
 
 
1238
    def get_knit_index(self, transport, name, mode):
 
1239
        mapper = ConstantMapper(name)
 
1240
        orig = knit._load_data
 
1241
        def reset():
 
1242
            knit._load_data = orig
 
1243
        self.addCleanup(reset)
 
1244
        from bzrlib._knit_load_data_c import _load_data_c
 
1245
        knit._load_data = _load_data_c
 
1246
        allow_writes = lambda: mode == 'w'
 
1247
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
 
1248
 
 
1249
 
 
1250
class KnitTests(TestCaseWithTransport):
 
1251
    """Class containing knit test helper routines."""
 
1252
 
 
1253
    def make_test_knit(self, annotate=False, name='test'):
 
1254
        mapper = ConstantMapper(name)
 
1255
        return make_file_factory(annotate, mapper)(self.get_transport())
 
1256
 
 
1257
 
 
1258
class TestBadShaError(KnitTests):
 
1259
    """Tests for handling of sha errors."""
 
1260
 
 
1261
    def test_exception_has_text(self):
 
1262
        # having the failed text included in the error allows for recovery.
 
1263
        source = self.make_test_knit()
 
1264
        target = self.make_test_knit(name="target")
 
1265
        if not source._max_delta_chain:
 
1266
            raise TestNotApplicable(
 
1267
                "cannot get delta-caused sha failures without deltas.")
 
1268
        # create a basis
 
1269
        basis = ('basis',)
 
1270
        broken = ('broken',)
 
1271
        source.add_lines(basis, (), ['foo\n'])
 
1272
        source.add_lines(broken, (basis,), ['foo\n', 'bar\n'])
 
1273
        # Seed target with a bad basis text
 
1274
        target.add_lines(basis, (), ['gam\n'])
 
1275
        target.insert_record_stream(
 
1276
            source.get_record_stream([broken], 'unordered', False))
 
1277
        err = self.assertRaises(errors.KnitCorrupt,
 
1278
            target.get_record_stream([broken], 'unordered', True).next)
 
1279
        self.assertEqual(['gam\n', 'bar\n'], err.content)
 
1280
        # Test for formatting with live data
 
1281
        self.assertStartsWith(str(err), "Knit ")
 
1282
 
 
1283
 
 
1284
class TestKnitIndex(KnitTests):
 
1285
 
 
1286
    def test_add_versions_dictionary_compresses(self):
 
1287
        """Adding versions to the index should update the lookup dict"""
 
1288
        knit = self.make_test_knit()
 
1289
        idx = knit._index
 
1290
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
 
1291
        self.check_file_contents('test.kndx',
 
1292
            '# bzr knit index 8\n'
 
1293
            '\n'
 
1294
            'a-1 fulltext 0 0  :'
 
1295
            )
 
1296
        idx.add_records([
 
1297
            (('a-2',), ['fulltext'], (('a-2',), 0, 0), [('a-1',)]),
 
1298
            (('a-3',), ['fulltext'], (('a-3',), 0, 0), [('a-2',)]),
 
1299
            ])
 
1300
        self.check_file_contents('test.kndx',
 
1301
            '# bzr knit index 8\n'
 
1302
            '\n'
 
1303
            'a-1 fulltext 0 0  :\n'
 
1304
            'a-2 fulltext 0 0 0 :\n'
 
1305
            'a-3 fulltext 0 0 1 :'
 
1306
            )
 
1307
        self.assertEqual(set([('a-3',), ('a-1',), ('a-2',)]), idx.keys())
 
1308
        self.assertEqual({
 
1309
            ('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False)),
 
1310
            ('a-2',): ((('a-2',), 0, 0), None, (('a-1',),), ('fulltext', False)),
 
1311
            ('a-3',): ((('a-3',), 0, 0), None, (('a-2',),), ('fulltext', False)),
 
1312
            }, idx.get_build_details(idx.keys()))
 
1313
        self.assertEqual({('a-1',):(),
 
1314
            ('a-2',):(('a-1',),),
 
1315
            ('a-3',):(('a-2',),),},
 
1316
            idx.get_parent_map(idx.keys()))
 
1317
 
 
1318
    def test_add_versions_fails_clean(self):
 
1319
        """If add_versions fails in the middle, it restores a pristine state.
 
1320
 
 
1321
        Any modifications that are made to the index are reset if all versions
 
1322
        cannot be added.
 
1323
        """
 
1324
        # This cheats a little bit by passing in a generator which will
 
1325
        # raise an exception before the processing finishes
 
1326
        # Other possibilities would be to have an version with the wrong number
 
1327
        # of entries, or to make the backing transport unable to write any
 
1328
        # files.
 
1329
 
 
1330
        knit = self.make_test_knit()
 
1331
        idx = knit._index
 
1332
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
 
1333
 
 
1334
        class StopEarly(Exception):
 
1335
            pass
 
1336
 
 
1337
        def generate_failure():
 
1338
            """Add some entries and then raise an exception"""
 
1339
            yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
 
1340
            yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
 
1341
            raise StopEarly()
 
1342
 
 
1343
        # Assert the pre-condition
 
1344
        def assertA1Only():
 
1345
            self.assertEqual(set([('a-1',)]), set(idx.keys()))
 
1346
            self.assertEqual(
 
1347
                {('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False))},
 
1348
                idx.get_build_details([('a-1',)]))
 
1349
            self.assertEqual({('a-1',):()}, idx.get_parent_map(idx.keys()))
 
1350
 
 
1351
        assertA1Only()
 
1352
        self.assertRaises(StopEarly, idx.add_records, generate_failure())
 
1353
        # And it shouldn't be modified
 
1354
        assertA1Only()
 
1355
 
 
1356
    def test_knit_index_ignores_empty_files(self):
 
1357
        # There was a race condition in older bzr, where a ^C at the right time
 
1358
        # could leave an empty .kndx file, which bzr would later claim was a
 
1359
        # corrupted file since the header was not present. In reality, the file
 
1360
        # just wasn't created, so it should be ignored.
 
1361
        t = get_transport('.')
 
1362
        t.put_bytes('test.kndx', '')
 
1363
 
 
1364
        knit = self.make_test_knit()
 
1365
 
 
1366
    def test_knit_index_checks_header(self):
 
1367
        t = get_transport('.')
 
1368
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
 
1369
        k = self.make_test_knit()
 
1370
        self.assertRaises(KnitHeaderError, k.keys)
 
1371
 
 
1372
 
 
1373
class TestGraphIndexKnit(KnitTests):
 
1374
    """Tests for knits using a GraphIndex rather than a KnitIndex."""
 
1375
 
 
1376
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1377
        builder = GraphIndexBuilder(ref_lists)
 
1378
        for node, references, value in nodes:
 
1379
            builder.add_node(node, references, value)
 
1380
        stream = builder.finish()
 
1381
        trans = self.get_transport()
 
1382
        size = trans.put_file(name, stream)
 
1383
        return GraphIndex(trans, name, size)
 
1384
 
 
1385
    def two_graph_index(self, deltas=False, catch_adds=False):
 
1386
        """Build a two-graph index.
 
1387
 
 
1388
        :param deltas: If true, use underlying indices with two node-ref
 
1389
            lists and 'parent' set to a delta-compressed against tail.
 
1390
        """
 
1391
        # build a complex graph across several indices.
 
1392
        if deltas:
 
1393
            # delta compression inn the index
 
1394
            index1 = self.make_g_index('1', 2, [
 
1395
                (('tip', ), 'N0 100', ([('parent', )], [], )),
 
1396
                (('tail', ), '', ([], []))])
 
1397
            index2 = self.make_g_index('2', 2, [
 
1398
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
 
1399
                (('separate', ), '', ([], []))])
 
1400
        else:
 
1401
            # just blob location and graph in the index.
 
1402
            index1 = self.make_g_index('1', 1, [
 
1403
                (('tip', ), 'N0 100', ([('parent', )], )),
 
1404
                (('tail', ), '', ([], ))])
 
1405
            index2 = self.make_g_index('2', 1, [
 
1406
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
 
1407
                (('separate', ), '', ([], ))])
 
1408
        combined_index = CombinedGraphIndex([index1, index2])
 
1409
        if catch_adds:
 
1410
            self.combined_index = combined_index
 
1411
            self.caught_entries = []
 
1412
            add_callback = self.catch_add
 
1413
        else:
 
1414
            add_callback = None
 
1415
        return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
 
1416
            add_callback=add_callback)
 
1417
 
 
1418
    def test_keys(self):
 
1419
        index = self.two_graph_index()
 
1420
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1421
            set(index.keys()))
 
1422
 
 
1423
    def test_get_position(self):
 
1424
        index = self.two_graph_index()
 
1425
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position(('tip',)))
 
1426
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position(('parent',)))
 
1427
 
 
1428
    def test_get_method_deltas(self):
 
1429
        index = self.two_graph_index(deltas=True)
 
1430
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1431
        self.assertEqual('line-delta', index.get_method(('parent',)))
 
1432
 
 
1433
    def test_get_method_no_deltas(self):
 
1434
        # check that the parent-history lookup is ignored with deltas=False.
 
1435
        index = self.two_graph_index(deltas=False)
 
1436
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1437
        self.assertEqual('fulltext', index.get_method(('parent',)))
 
1438
 
 
1439
    def test_get_options_deltas(self):
 
1440
        index = self.two_graph_index(deltas=True)
 
1441
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1442
        self.assertEqual(['line-delta'], index.get_options(('parent',)))
 
1443
 
 
1444
    def test_get_options_no_deltas(self):
 
1445
        # check that the parent-history lookup is ignored with deltas=False.
 
1446
        index = self.two_graph_index(deltas=False)
 
1447
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1448
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1449
 
 
1450
    def test_get_parent_map(self):
 
1451
        index = self.two_graph_index()
 
1452
        self.assertEqual({('parent',):(('tail',), ('ghost',))},
 
1453
            index.get_parent_map([('parent',), ('ghost',)]))
 
1454
 
 
1455
    def catch_add(self, entries):
 
1456
        self.caught_entries.append(entries)
 
1457
 
 
1458
    def test_add_no_callback_errors(self):
 
1459
        index = self.two_graph_index()
 
1460
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1461
            [(('new',), 'fulltext,no-eol', (None, 50, 60), ['separate'])])
 
1462
 
 
1463
    def test_add_version_smoke(self):
 
1464
        index = self.two_graph_index(catch_adds=True)
 
1465
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60),
 
1466
            [('separate',)])])
 
1467
        self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
 
1468
            self.caught_entries)
 
1469
 
 
1470
    def test_add_version_delta_not_delta_index(self):
 
1471
        index = self.two_graph_index(catch_adds=True)
 
1472
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1473
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1474
        self.assertEqual([], self.caught_entries)
 
1475
 
 
1476
    def test_add_version_same_dup(self):
 
1477
        index = self.two_graph_index(catch_adds=True)
 
1478
        # options can be spelt two different ways
 
1479
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1480
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
 
1481
        # position/length are ignored (because each pack could have fulltext or
 
1482
        # delta, and be at a different position.
 
1483
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1484
            [('parent',)])])
 
1485
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1486
            [('parent',)])])
 
1487
        # but neither should have added data:
 
1488
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1489
        
 
1490
    def test_add_version_different_dup(self):
 
1491
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1492
        # change options
 
1493
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1494
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1495
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1496
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [('parent',)])])
 
1497
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1498
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
 
1499
        # parents
 
1500
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1501
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1502
        self.assertEqual([], self.caught_entries)
 
1503
        
 
1504
    def test_add_versions_nodeltas(self):
 
1505
        index = self.two_graph_index(catch_adds=True)
 
1506
        index.add_records([
 
1507
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1508
                (('new2',), 'fulltext', (None, 0, 6), [('new',)]),
 
1509
                ])
 
1510
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
 
1511
            (('new2', ), ' 0 6', ((('new',),),))],
 
1512
            sorted(self.caught_entries[0]))
 
1513
        self.assertEqual(1, len(self.caught_entries))
 
1514
 
 
1515
    def test_add_versions_deltas(self):
 
1516
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1517
        index.add_records([
 
1518
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1519
                (('new2',), 'line-delta', (None, 0, 6), [('new',)]),
 
1520
                ])
 
1521
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
 
1522
            (('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
 
1523
            sorted(self.caught_entries[0]))
 
1524
        self.assertEqual(1, len(self.caught_entries))
 
1525
 
 
1526
    def test_add_versions_delta_not_delta_index(self):
 
1527
        index = self.two_graph_index(catch_adds=True)
 
1528
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1529
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1530
        self.assertEqual([], self.caught_entries)
 
1531
 
 
1532
    def test_add_versions_random_id_accepted(self):
 
1533
        index = self.two_graph_index(catch_adds=True)
 
1534
        index.add_records([], random_id=True)
 
1535
 
 
1536
    def test_add_versions_same_dup(self):
 
1537
        index = self.two_graph_index(catch_adds=True)
 
1538
        # options can be spelt two different ways
 
1539
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100),
 
1540
            [('parent',)])])
 
1541
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100),
 
1542
            [('parent',)])])
 
1543
        # position/length are ignored (because each pack could have fulltext or
 
1544
        # delta, and be at a different position.
 
1545
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1546
            [('parent',)])])
 
1547
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1548
            [('parent',)])])
 
1549
        # but neither should have added data.
 
1550
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1551
        
 
1552
    def test_add_versions_different_dup(self):
 
1553
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1554
        # change options
 
1555
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1556
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1557
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1558
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [('parent',)])])
 
1559
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1560
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
 
1561
        # parents
 
1562
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1563
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1564
        # change options in the second record
 
1565
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1566
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)]),
 
1567
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1568
        self.assertEqual([], self.caught_entries)
 
1569
 
 
1570
 
 
1571
class TestNoParentsGraphIndexKnit(KnitTests):
 
1572
    """Tests for knits using _KnitGraphIndex with no parents."""
 
1573
 
 
1574
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1575
        builder = GraphIndexBuilder(ref_lists)
 
1576
        for node, references in nodes:
 
1577
            builder.add_node(node, references)
 
1578
        stream = builder.finish()
 
1579
        trans = self.get_transport()
 
1580
        size = trans.put_file(name, stream)
 
1581
        return GraphIndex(trans, name, size)
 
1582
 
 
1583
    def test_parents_deltas_incompatible(self):
 
1584
        index = CombinedGraphIndex([])
 
1585
        self.assertRaises(errors.KnitError, _KnitGraphIndex, lambda:True,
 
1586
            index, deltas=True, parents=False)
 
1587
 
 
1588
    def two_graph_index(self, catch_adds=False):
 
1589
        """Build a two-graph index.
 
1590
 
 
1591
        :param deltas: If true, use underlying indices with two node-ref
 
1592
            lists and 'parent' set to a delta-compressed against tail.
 
1593
        """
 
1594
        # put several versions in the index.
 
1595
        index1 = self.make_g_index('1', 0, [
 
1596
            (('tip', ), 'N0 100'),
 
1597
            (('tail', ), '')])
 
1598
        index2 = self.make_g_index('2', 0, [
 
1599
            (('parent', ), ' 100 78'),
 
1600
            (('separate', ), '')])
 
1601
        combined_index = CombinedGraphIndex([index1, index2])
 
1602
        if catch_adds:
 
1603
            self.combined_index = combined_index
 
1604
            self.caught_entries = []
 
1605
            add_callback = self.catch_add
 
1606
        else:
 
1607
            add_callback = None
 
1608
        return _KnitGraphIndex(combined_index, lambda:True, parents=False,
 
1609
            add_callback=add_callback)
 
1610
 
 
1611
    def test_keys(self):
 
1612
        index = self.two_graph_index()
 
1613
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1614
            set(index.keys()))
 
1615
 
 
1616
    def test_get_position(self):
 
1617
        index = self.two_graph_index()
 
1618
        self.assertEqual((index._graph_index._indices[0], 0, 100),
 
1619
            index.get_position(('tip',)))
 
1620
        self.assertEqual((index._graph_index._indices[1], 100, 78),
 
1621
            index.get_position(('parent',)))
 
1622
 
 
1623
    def test_get_method(self):
 
1624
        index = self.two_graph_index()
 
1625
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1626
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1627
 
 
1628
    def test_get_options(self):
 
1629
        index = self.two_graph_index()
 
1630
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1631
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1632
 
 
1633
    def test_get_parent_map(self):
 
1634
        index = self.two_graph_index()
 
1635
        self.assertEqual({('parent',):None},
 
1636
            index.get_parent_map([('parent',), ('ghost',)]))
 
1637
 
 
1638
    def catch_add(self, entries):
 
1639
        self.caught_entries.append(entries)
 
1640
 
 
1641
    def test_add_no_callback_errors(self):
 
1642
        index = self.two_graph_index()
 
1643
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1644
            [(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)])])
 
1645
 
 
1646
    def test_add_version_smoke(self):
 
1647
        index = self.two_graph_index(catch_adds=True)
 
1648
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60), [])])
 
1649
        self.assertEqual([[(('new', ), 'N50 60')]],
 
1650
            self.caught_entries)
 
1651
 
 
1652
    def test_add_version_delta_not_delta_index(self):
 
1653
        index = self.two_graph_index(catch_adds=True)
 
1654
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1655
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1656
        self.assertEqual([], self.caught_entries)
 
1657
 
 
1658
    def test_add_version_same_dup(self):
 
1659
        index = self.two_graph_index(catch_adds=True)
 
1660
        # options can be spelt two different ways
 
1661
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1662
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
1663
        # position/length are ignored (because each pack could have fulltext or
 
1664
        # delta, and be at a different position.
 
1665
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
1666
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
 
1667
        # but neither should have added data.
 
1668
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1669
        
 
1670
    def test_add_version_different_dup(self):
 
1671
        index = self.two_graph_index(catch_adds=True)
 
1672
        # change options
 
1673
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1674
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1675
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1676
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
1677
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1678
            [(('tip',), 'fulltext', (None, 0, 100), [])])
 
1679
        # parents
 
1680
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1681
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1682
        self.assertEqual([], self.caught_entries)
 
1683
        
 
1684
    def test_add_versions(self):
 
1685
        index = self.two_graph_index(catch_adds=True)
 
1686
        index.add_records([
 
1687
                (('new',), 'fulltext,no-eol', (None, 50, 60), []),
 
1688
                (('new2',), 'fulltext', (None, 0, 6), []),
 
1689
                ])
 
1690
        self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
 
1691
            sorted(self.caught_entries[0]))
 
1692
        self.assertEqual(1, len(self.caught_entries))
 
1693
 
 
1694
    def test_add_versions_delta_not_delta_index(self):
 
1695
        index = self.two_graph_index(catch_adds=True)
 
1696
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1697
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1698
        self.assertEqual([], self.caught_entries)
 
1699
 
 
1700
    def test_add_versions_parents_not_parents_index(self):
 
1701
        index = self.two_graph_index(catch_adds=True)
 
1702
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1703
            [(('new',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
 
1704
        self.assertEqual([], self.caught_entries)
 
1705
 
 
1706
    def test_add_versions_random_id_accepted(self):
 
1707
        index = self.two_graph_index(catch_adds=True)
 
1708
        index.add_records([], random_id=True)
 
1709
 
 
1710
    def test_add_versions_same_dup(self):
 
1711
        index = self.two_graph_index(catch_adds=True)
 
1712
        # options can be spelt two different ways
 
1713
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1714
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
1715
        # position/length are ignored (because each pack could have fulltext or
 
1716
        # delta, and be at a different position.
 
1717
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
1718
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
 
1719
        # but neither should have added data.
 
1720
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1721
        
 
1722
    def test_add_versions_different_dup(self):
 
1723
        index = self.two_graph_index(catch_adds=True)
 
1724
        # change options
 
1725
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1726
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1727
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1728
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
1729
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1730
            [(('tip',), 'fulltext', (None, 0, 100), [])])
 
1731
        # parents
 
1732
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1733
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1734
        # change options in the second record
 
1735
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1736
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), []),
 
1737
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1738
        self.assertEqual([], self.caught_entries)
 
1739
 
 
1740
 
 
1741
class TestStacking(KnitTests):
 
1742
 
 
1743
    def get_basis_and_test_knit(self):
 
1744
        basis = self.make_test_knit(name='basis')
 
1745
        basis = RecordingVersionedFilesDecorator(basis)
 
1746
        test = self.make_test_knit(name='test')
 
1747
        test.add_fallback_versioned_files(basis)
 
1748
        return basis, test
 
1749
 
 
1750
    def test_add_fallback_versioned_files(self):
 
1751
        basis = self.make_test_knit(name='basis')
 
1752
        test = self.make_test_knit(name='test')
 
1753
        # It must not error; other tests test that the fallback is referred to
 
1754
        # when accessing data.
 
1755
        test.add_fallback_versioned_files(basis)
 
1756
 
 
1757
    def test_add_lines(self):
 
1758
        # lines added to the test are not added to the basis
 
1759
        basis, test = self.get_basis_and_test_knit()
 
1760
        key = ('foo',)
 
1761
        key_basis = ('bar',)
 
1762
        key_cross_border = ('quux',)
 
1763
        key_delta = ('zaphod',)
 
1764
        test.add_lines(key, (), ['foo\n'])
 
1765
        self.assertEqual({}, basis.get_parent_map([key]))
 
1766
        # lines added to the test that reference across the stack do a
 
1767
        # fulltext.
 
1768
        basis.add_lines(key_basis, (), ['foo\n'])
 
1769
        basis.calls = []
 
1770
        test.add_lines(key_cross_border, (key_basis,), ['foo\n'])
 
1771
        self.assertEqual('fulltext', test._index.get_method(key_cross_border))
 
1772
        # we don't even need to look at the basis to see that this should be
 
1773
        # stored as a fulltext
 
1774
        self.assertEqual([], basis.calls)
 
1775
        # Subsequent adds do delta.
 
1776
        basis.calls = []
 
1777
        test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
 
1778
        self.assertEqual('line-delta', test._index.get_method(key_delta))
 
1779
        self.assertEqual([], basis.calls)
 
1780
 
 
1781
    def test_annotate(self):
 
1782
        # annotations from the test knit are answered without asking the basis
 
1783
        basis, test = self.get_basis_and_test_knit()
 
1784
        key = ('foo',)
 
1785
        key_basis = ('bar',)
 
1786
        key_missing = ('missing',)
 
1787
        test.add_lines(key, (), ['foo\n'])
 
1788
        details = test.annotate(key)
 
1789
        self.assertEqual([(key, 'foo\n')], details)
 
1790
        self.assertEqual([], basis.calls)
 
1791
        # But texts that are not in the test knit are looked for in the basis
 
1792
        # directly.
 
1793
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
1794
        basis.calls = []
 
1795
        details = test.annotate(key_basis)
 
1796
        self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
 
1797
        # Not optimised to date:
 
1798
        # self.assertEqual([("annotate", key_basis)], basis.calls)
 
1799
        self.assertEqual([('get_parent_map', set([key_basis])),
 
1800
            ('get_parent_map', set([key_basis])),
 
1801
            ('get_parent_map', set([key_basis])),
 
1802
            ('get_record_stream', [key_basis], 'unordered', True)],
 
1803
            basis.calls)
 
1804
 
 
1805
    def test_check(self):
 
1806
        # At the moment checking a stacked knit does implicitly check the
 
1807
        # fallback files.  
 
1808
        basis, test = self.get_basis_and_test_knit()
 
1809
        test.check()
 
1810
 
 
1811
    def test_get_parent_map(self):
 
1812
        # parents in the test knit are answered without asking the basis
 
1813
        basis, test = self.get_basis_and_test_knit()
 
1814
        key = ('foo',)
 
1815
        key_basis = ('bar',)
 
1816
        key_missing = ('missing',)
 
1817
        test.add_lines(key, (), [])
 
1818
        parent_map = test.get_parent_map([key])
 
1819
        self.assertEqual({key: ()}, parent_map)
 
1820
        self.assertEqual([], basis.calls)
 
1821
        # But parents that are not in the test knit are looked for in the basis
 
1822
        basis.add_lines(key_basis, (), [])
 
1823
        basis.calls = []
 
1824
        parent_map = test.get_parent_map([key, key_basis, key_missing])
 
1825
        self.assertEqual({key: (),
 
1826
            key_basis: ()}, parent_map)
 
1827
        self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
 
1828
            basis.calls)
 
1829
 
 
1830
    def test_get_record_stream_unordered_fulltexts(self):
 
1831
        # records from the test knit are answered without asking the basis:
 
1832
        basis, test = self.get_basis_and_test_knit()
 
1833
        key = ('foo',)
 
1834
        key_basis = ('bar',)
 
1835
        key_missing = ('missing',)
 
1836
        test.add_lines(key, (), ['foo\n'])
 
1837
        records = list(test.get_record_stream([key], 'unordered', True))
 
1838
        self.assertEqual(1, len(records))
 
1839
        self.assertEqual([], basis.calls)
 
1840
        # Missing (from test knit) objects are retrieved from the basis:
 
1841
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
1842
        basis.calls = []
 
1843
        records = list(test.get_record_stream([key_basis, key_missing],
 
1844
            'unordered', True))
 
1845
        self.assertEqual(2, len(records))
 
1846
        calls = list(basis.calls)
 
1847
        for record in records:
 
1848
            self.assertSubset([record.key], (key_basis, key_missing))
 
1849
            if record.key == key_missing:
 
1850
                self.assertIsInstance(record, AbsentContentFactory)
 
1851
            else:
 
1852
                reference = list(basis.get_record_stream([key_basis],
 
1853
                    'unordered', True))[0]
 
1854
                self.assertEqual(reference.key, record.key)
 
1855
                self.assertEqual(reference.sha1, record.sha1)
 
1856
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
1857
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
1858
                    record.get_bytes_as(record.storage_kind))
 
1859
                self.assertEqual(reference.get_bytes_as('fulltext'),
 
1860
                    record.get_bytes_as('fulltext'))
 
1861
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1862
        # ask which fallbacks have which parents.
 
1863
        self.assertEqual([
 
1864
            ("get_parent_map", set([key_basis, key_missing])),
 
1865
            ("get_record_stream", [key_basis], 'unordered', True)],
 
1866
            calls)
 
1867
 
 
1868
    def test_get_record_stream_ordered_fulltexts(self):
 
1869
        # ordering is preserved down into the fallback store.
 
1870
        basis, test = self.get_basis_and_test_knit()
 
1871
        key = ('foo',)
 
1872
        key_basis = ('bar',)
 
1873
        key_basis_2 = ('quux',)
 
1874
        key_missing = ('missing',)
 
1875
        test.add_lines(key, (key_basis,), ['foo\n'])
 
1876
        # Missing (from test knit) objects are retrieved from the basis:
 
1877
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
1878
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
1879
        basis.calls = []
 
1880
        # ask for in non-topological order
 
1881
        records = list(test.get_record_stream(
 
1882
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
 
1883
        self.assertEqual(4, len(records))
 
1884
        results = []
 
1885
        for record in records:
 
1886
            self.assertSubset([record.key],
 
1887
                (key_basis, key_missing, key_basis_2, key))
 
1888
            if record.key == key_missing:
 
1889
                self.assertIsInstance(record, AbsentContentFactory)
 
1890
            else:
 
1891
                results.append((record.key, record.sha1, record.storage_kind,
 
1892
                    record.get_bytes_as('fulltext')))
 
1893
        calls = list(basis.calls)
 
1894
        order = [record[0] for record in results]
 
1895
        self.assertEqual([key_basis_2, key_basis, key], order)
 
1896
        for result in results:
 
1897
            if result[0] == key:
 
1898
                source = test
 
1899
            else:
 
1900
                source = basis
 
1901
            record = source.get_record_stream([result[0]], 'unordered',
 
1902
                True).next()
 
1903
            self.assertEqual(record.key, result[0])
 
1904
            self.assertEqual(record.sha1, result[1])
 
1905
            self.assertEqual(record.storage_kind, result[2])
 
1906
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
 
1907
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1908
        # ask which fallbacks have which parents.
 
1909
        self.assertEqual([
 
1910
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
1911
            # unordered is asked for by the underlying worker as it still
 
1912
            # buffers everything while answering - which is a problem!
 
1913
            ("get_record_stream", [key_basis_2, key_basis], 'unordered', True)],
 
1914
            calls)
 
1915
 
 
1916
    def test_get_record_stream_unordered_deltas(self):
 
1917
        # records from the test knit are answered without asking the basis:
 
1918
        basis, test = self.get_basis_and_test_knit()
 
1919
        key = ('foo',)
 
1920
        key_basis = ('bar',)
 
1921
        key_missing = ('missing',)
 
1922
        test.add_lines(key, (), ['foo\n'])
 
1923
        records = list(test.get_record_stream([key], 'unordered', False))
 
1924
        self.assertEqual(1, len(records))
 
1925
        self.assertEqual([], basis.calls)
 
1926
        # Missing (from test knit) objects are retrieved from the basis:
 
1927
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
1928
        basis.calls = []
 
1929
        records = list(test.get_record_stream([key_basis, key_missing],
 
1930
            'unordered', False))
 
1931
        self.assertEqual(2, len(records))
 
1932
        calls = list(basis.calls)
 
1933
        for record in records:
 
1934
            self.assertSubset([record.key], (key_basis, key_missing))
 
1935
            if record.key == key_missing:
 
1936
                self.assertIsInstance(record, AbsentContentFactory)
 
1937
            else:
 
1938
                reference = list(basis.get_record_stream([key_basis],
 
1939
                    'unordered', False))[0]
 
1940
                self.assertEqual(reference.key, record.key)
 
1941
                self.assertEqual(reference.sha1, record.sha1)
 
1942
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
1943
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
1944
                    record.get_bytes_as(record.storage_kind))
 
1945
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1946
        # ask which fallbacks have which parents.
 
1947
        self.assertEqual([
 
1948
            ("get_parent_map", set([key_basis, key_missing])),
 
1949
            ("get_record_stream", [key_basis], 'unordered', False)],
 
1950
            calls)
 
1951
 
 
1952
    def test_get_record_stream_ordered_deltas(self):
 
1953
        # ordering is preserved down into the fallback store.
 
1954
        basis, test = self.get_basis_and_test_knit()
 
1955
        key = ('foo',)
 
1956
        key_basis = ('bar',)
 
1957
        key_basis_2 = ('quux',)
 
1958
        key_missing = ('missing',)
 
1959
        test.add_lines(key, (key_basis,), ['foo\n'])
 
1960
        # Missing (from test knit) objects are retrieved from the basis:
 
1961
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
1962
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
1963
        basis.calls = []
 
1964
        # ask for in non-topological order
 
1965
        records = list(test.get_record_stream(
 
1966
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
 
1967
        self.assertEqual(4, len(records))
 
1968
        results = []
 
1969
        for record in records:
 
1970
            self.assertSubset([record.key],
 
1971
                (key_basis, key_missing, key_basis_2, key))
 
1972
            if record.key == key_missing:
 
1973
                self.assertIsInstance(record, AbsentContentFactory)
 
1974
            else:
 
1975
                results.append((record.key, record.sha1, record.storage_kind,
 
1976
                    record.get_bytes_as(record.storage_kind)))
 
1977
        calls = list(basis.calls)
 
1978
        order = [record[0] for record in results]
 
1979
        self.assertEqual([key_basis_2, key_basis, key], order)
 
1980
        for result in results:
 
1981
            if result[0] == key:
 
1982
                source = test
 
1983
            else:
 
1984
                source = basis
 
1985
            record = source.get_record_stream([result[0]], 'unordered',
 
1986
                False).next()
 
1987
            self.assertEqual(record.key, result[0])
 
1988
            self.assertEqual(record.sha1, result[1])
 
1989
            self.assertEqual(record.storage_kind, result[2])
 
1990
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
 
1991
        # It's not strictly minimal, but it seems reasonable for now for it to
 
1992
        # ask which fallbacks have which parents.
 
1993
        self.assertEqual([
 
1994
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
1995
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
 
1996
            calls)
 
1997
 
 
1998
    def test_get_sha1s(self):
 
1999
        # sha1's in the test knit are answered without asking the basis
 
2000
        basis, test = self.get_basis_and_test_knit()
 
2001
        key = ('foo',)
 
2002
        key_basis = ('bar',)
 
2003
        key_missing = ('missing',)
 
2004
        test.add_lines(key, (), ['foo\n'])
 
2005
        key_sha1sum = osutils.sha('foo\n').hexdigest()
 
2006
        sha1s = test.get_sha1s([key])
 
2007
        self.assertEqual({key: key_sha1sum}, sha1s)
 
2008
        self.assertEqual([], basis.calls)
 
2009
        # But texts that are not in the test knit are looked for in the basis
 
2010
        # directly (rather than via text reconstruction) so that remote servers
 
2011
        # etc don't have to answer with full content.
 
2012
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2013
        basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
2014
        basis.calls = []
 
2015
        sha1s = test.get_sha1s([key, key_missing, key_basis])
 
2016
        self.assertEqual({key: key_sha1sum,
 
2017
            key_basis: basis_sha1sum}, sha1s)
 
2018
        self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
 
2019
            basis.calls)
 
2020
 
 
2021
    def test_insert_record_stream(self):
 
2022
        # records are inserted as normal; insert_record_stream builds on
 
2023
        # add_lines, so a smoke test should be all that's needed:
 
2024
        key = ('foo',)
 
2025
        key_basis = ('bar',)
 
2026
        key_delta = ('zaphod',)
 
2027
        basis, test = self.get_basis_and_test_knit()
 
2028
        source = self.make_test_knit(name='source')
 
2029
        basis.add_lines(key_basis, (), ['foo\n'])
 
2030
        basis.calls = []
 
2031
        source.add_lines(key_basis, (), ['foo\n'])
 
2032
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2033
        stream = source.get_record_stream([key_delta], 'unordered', False)
 
2034
        test.insert_record_stream(stream)
 
2035
        # XXX: this does somewhat too many calls in making sure of whether it
 
2036
        # has to recreate the full text.
 
2037
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2038
             ('get_parent_map', set([key_basis])),
 
2039
             ('get_record_stream', [key_basis], 'unordered', True)],
 
2040
            basis.calls)
 
2041
        self.assertEqual({key_delta:(key_basis,)},
 
2042
            test.get_parent_map([key_delta]))
 
2043
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2044
            'unordered', True).next().get_bytes_as('fulltext'))
 
2045
 
 
2046
    def test_iter_lines_added_or_present_in_keys(self):
 
2047
        # Lines from the basis are returned, and lines for a given key are only
 
2048
        # returned once. 
 
2049
        key1 = ('foo1',)
 
2050
        key2 = ('foo2',)
 
2051
        # all sources are asked for keys:
 
2052
        basis, test = self.get_basis_and_test_knit()
 
2053
        basis.add_lines(key1, (), ["foo"])
 
2054
        basis.calls = []
 
2055
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
 
2056
        self.assertEqual([("foo\n", key1)], lines)
 
2057
        self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
 
2058
            basis.calls)
 
2059
        # keys in both are not duplicated:
 
2060
        test.add_lines(key2, (), ["bar\n"])
 
2061
        basis.add_lines(key2, (), ["bar\n"])
 
2062
        basis.calls = []
 
2063
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
 
2064
        self.assertEqual([("bar\n", key2)], lines)
 
2065
        self.assertEqual([], basis.calls)
 
2066
 
 
2067
    def test_keys(self):
 
2068
        key1 = ('foo1',)
 
2069
        key2 = ('foo2',)
 
2070
        # all sources are asked for keys:
 
2071
        basis, test = self.get_basis_and_test_knit()
 
2072
        keys = test.keys()
 
2073
        self.assertEqual(set(), set(keys))
 
2074
        self.assertEqual([("keys",)], basis.calls)
 
2075
        # keys from a basis are returned:
 
2076
        basis.add_lines(key1, (), [])
 
2077
        basis.calls = []
 
2078
        keys = test.keys()
 
2079
        self.assertEqual(set([key1]), set(keys))
 
2080
        self.assertEqual([("keys",)], basis.calls)
 
2081
        # keys in both are not duplicated:
 
2082
        test.add_lines(key2, (), [])
 
2083
        basis.add_lines(key2, (), [])
 
2084
        basis.calls = []
 
2085
        keys = test.keys()
 
2086
        self.assertEqual(2, len(keys))
 
2087
        self.assertEqual(set([key1, key2]), set(keys))
 
2088
        self.assertEqual([("keys",)], basis.calls)
 
2089
 
 
2090
    def test_add_mpdiffs(self):
 
2091
        # records are inserted as normal; add_mpdiff builds on
 
2092
        # add_lines, so a smoke test should be all that's needed:
 
2093
        key = ('foo',)
 
2094
        key_basis = ('bar',)
 
2095
        key_delta = ('zaphod',)
 
2096
        basis, test = self.get_basis_and_test_knit()
 
2097
        source = self.make_test_knit(name='source')
 
2098
        basis.add_lines(key_basis, (), ['foo\n'])
 
2099
        basis.calls = []
 
2100
        source.add_lines(key_basis, (), ['foo\n'])
 
2101
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2102
        diffs = source.make_mpdiffs([key_delta])
 
2103
        test.add_mpdiffs([(key_delta, (key_basis,),
 
2104
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
 
2105
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2106
            ('get_record_stream', [key_basis], 'unordered', True),],
 
2107
            basis.calls)
 
2108
        self.assertEqual({key_delta:(key_basis,)},
 
2109
            test.get_parent_map([key_delta]))
 
2110
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2111
            'unordered', True).next().get_bytes_as('fulltext'))
 
2112
 
 
2113
    def test_make_mpdiffs(self):
 
2114
        # Generating an mpdiff across a stacking boundary should detect parent
 
2115
        # texts regions.
 
2116
        key = ('foo',)
 
2117
        key_left = ('bar',)
 
2118
        key_right = ('zaphod',)
 
2119
        basis, test = self.get_basis_and_test_knit()
 
2120
        basis.add_lines(key_left, (), ['bar\n'])
 
2121
        basis.add_lines(key_right, (), ['zaphod\n'])
 
2122
        basis.calls = []
 
2123
        test.add_lines(key, (key_left, key_right),
 
2124
            ['bar\n', 'foo\n', 'zaphod\n'])
 
2125
        diffs = test.make_mpdiffs([key])
 
2126
        self.assertEqual([
 
2127
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
 
2128
                multiparent.NewText(['foo\n']),
 
2129
                multiparent.ParentText(1, 0, 2, 1)])],
 
2130
            diffs)
 
2131
        self.assertEqual(3, len(basis.calls))
 
2132
        self.assertEqual([
 
2133
            ("get_parent_map", set([key_left, key_right])),
 
2134
            ("get_parent_map", set([key_left, key_right])),
 
2135
            ],
 
2136
            basis.calls[:-1])
 
2137
        last_call = basis.calls[-1]
 
2138
        self.assertEqual('get_record_stream', last_call[0])
 
2139
        self.assertEqual(set([key_left, key_right]), set(last_call[1]))
 
2140
        self.assertEqual('unordered', last_call[2])
 
2141
        self.assertEqual(True, last_call[3])