~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Martin Pool
  • Date: 2005-07-11 07:05:34 UTC
  • Revision ID: mbp@sourcefrog.net-20050711070534-5227696ab167ccde
- merge aaron's append_multiple.patch

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(None, 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
 
    def test_get_record_stream_yields_disk_sorted_order(self):
663
 
        # if we get 'unordered' pick a semi-optimal order for reading. The
664
 
        # order should be grouped by pack file, and then by position in file
665
 
        repo = self.make_repository('test', format='pack-0.92')
666
 
        repo.lock_write()
667
 
        self.addCleanup(repo.unlock)
668
 
        repo.start_write_group()
669
 
        vf = repo.texts
670
 
        vf.add_lines(('f-id', 'rev-5'), [('f-id', 'rev-4')], ['lines\n'])
671
 
        vf.add_lines(('f-id', 'rev-1'), [], ['lines\n'])
672
 
        vf.add_lines(('f-id', 'rev-2'), [('f-id', 'rev-1')], ['lines\n'])
673
 
        repo.commit_write_group()
674
 
        # We inserted them as rev-5, rev-1, rev-2, we should get them back in
675
 
        # the same order
676
 
        stream = vf.get_record_stream([('f-id', 'rev-1'), ('f-id', 'rev-5'),
677
 
                                       ('f-id', 'rev-2')], 'unordered', False)
678
 
        keys = [r.key for r in stream]
679
 
        self.assertEqual([('f-id', 'rev-5'), ('f-id', 'rev-1'),
680
 
                          ('f-id', 'rev-2')], keys)
681
 
        repo.start_write_group()
682
 
        vf.add_lines(('f-id', 'rev-4'), [('f-id', 'rev-3')], ['lines\n'])
683
 
        vf.add_lines(('f-id', 'rev-3'), [('f-id', 'rev-2')], ['lines\n'])
684
 
        vf.add_lines(('f-id', 'rev-6'), [('f-id', 'rev-5')], ['lines\n'])
685
 
        repo.commit_write_group()
686
 
        # Request in random order, to make sure the output order isn't based on
687
 
        # the request
688
 
        request_keys = set(('f-id', 'rev-%d' % i) for i in range(1, 7))
689
 
        stream = vf.get_record_stream(request_keys, 'unordered', False)
690
 
        keys = [r.key for r in stream]
691
 
        # We want to get the keys back in disk order, but it doesn't matter
692
 
        # which pack we read from first. So this can come back in 2 orders
693
 
        alt1 = [('f-id', 'rev-%d' % i) for i in [4, 3, 6, 5, 1, 2]]
694
 
        alt2 = [('f-id', 'rev-%d' % i) for i in [5, 1, 2, 4, 3, 6]]
695
 
        if keys != alt1 and keys != alt2:
696
 
            self.fail('Returned key order did not match either expected order.'
697
 
                      ' expected %s or %s, not %s'
698
 
                      % (alt1, alt2, keys))
699
 
 
700
 
 
701
 
class LowLevelKnitDataTests(TestCase):
702
 
 
703
 
    def create_gz_content(self, text):
704
 
        sio = StringIO()
705
 
        gz_file = gzip.GzipFile(mode='wb', fileobj=sio)
706
 
        gz_file.write(text)
707
 
        gz_file.close()
708
 
        return sio.getvalue()
709
 
 
710
 
    def make_multiple_records(self):
711
 
        """Create the content for multiple records."""
712
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
713
 
        total_txt = []
714
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
715
 
                                        'foo\n'
716
 
                                        'bar\n'
717
 
                                        'end rev-id-1\n'
718
 
                                        % (sha1sum,))
719
 
        record_1 = (0, len(gz_txt), sha1sum)
720
 
        total_txt.append(gz_txt)
721
 
        sha1sum = osutils.sha('baz\n').hexdigest()
722
 
        gz_txt = self.create_gz_content('version rev-id-2 1 %s\n'
723
 
                                        'baz\n'
724
 
                                        'end rev-id-2\n'
725
 
                                        % (sha1sum,))
726
 
        record_2 = (record_1[1], len(gz_txt), sha1sum)
727
 
        total_txt.append(gz_txt)
728
 
        return total_txt, record_1, record_2
729
 
 
730
 
    def test_valid_knit_data(self):
731
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
732
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
733
 
                                        'foo\n'
734
 
                                        'bar\n'
735
 
                                        'end rev-id-1\n'
736
 
                                        % (sha1sum,))
737
 
        transport = MockTransport([gz_txt])
738
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
739
 
        knit = KnitVersionedFiles(None, access)
740
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
741
 
 
742
 
        contents = list(knit._read_records_iter(records))
743
 
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'],
744
 
            '4e48e2c9a3d2ca8a708cb0cc545700544efb5021')], contents)
745
 
 
746
 
        raw_contents = list(knit._read_records_iter_raw(records))
747
 
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
748
 
 
749
 
    def test_multiple_records_valid(self):
750
 
        total_txt, record_1, record_2 = self.make_multiple_records()
751
 
        transport = MockTransport([''.join(total_txt)])
752
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
753
 
        knit = KnitVersionedFiles(None, access)
754
 
        records = [(('rev-id-1',), (('rev-id-1',), record_1[0], record_1[1])),
755
 
                   (('rev-id-2',), (('rev-id-2',), record_2[0], record_2[1]))]
756
 
 
757
 
        contents = list(knit._read_records_iter(records))
758
 
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'], record_1[2]),
759
 
                          (('rev-id-2',), ['baz\n'], record_2[2])],
760
 
                         contents)
761
 
 
762
 
        raw_contents = list(knit._read_records_iter_raw(records))
763
 
        self.assertEqual([(('rev-id-1',), total_txt[0], record_1[2]),
764
 
                          (('rev-id-2',), total_txt[1], record_2[2])],
765
 
                         raw_contents)
766
 
 
767
 
    def test_not_enough_lines(self):
768
 
        sha1sum = osutils.sha('foo\n').hexdigest()
769
 
        # record says 2 lines data says 1
770
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
771
 
                                        'foo\n'
772
 
                                        'end rev-id-1\n'
773
 
                                        % (sha1sum,))
774
 
        transport = MockTransport([gz_txt])
775
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
776
 
        knit = KnitVersionedFiles(None, access)
777
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
778
 
        self.assertRaises(errors.KnitCorrupt, list,
779
 
            knit._read_records_iter(records))
780
 
 
781
 
        # read_records_iter_raw won't detect that sort of mismatch/corruption
782
 
        raw_contents = list(knit._read_records_iter_raw(records))
783
 
        self.assertEqual([(('rev-id-1',),  gz_txt, sha1sum)], raw_contents)
784
 
 
785
 
    def test_too_many_lines(self):
786
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
787
 
        # record says 1 lines data says 2
788
 
        gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
789
 
                                        'foo\n'
790
 
                                        'bar\n'
791
 
                                        'end rev-id-1\n'
792
 
                                        % (sha1sum,))
793
 
        transport = MockTransport([gz_txt])
794
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
795
 
        knit = KnitVersionedFiles(None, access)
796
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
797
 
        self.assertRaises(errors.KnitCorrupt, list,
798
 
            knit._read_records_iter(records))
799
 
 
800
 
        # read_records_iter_raw won't detect that sort of mismatch/corruption
801
 
        raw_contents = list(knit._read_records_iter_raw(records))
802
 
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
803
 
 
804
 
    def test_mismatched_version_id(self):
805
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
806
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
807
 
                                        'foo\n'
808
 
                                        'bar\n'
809
 
                                        'end rev-id-1\n'
810
 
                                        % (sha1sum,))
811
 
        transport = MockTransport([gz_txt])
812
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
813
 
        knit = KnitVersionedFiles(None, access)
814
 
        # We are asking for rev-id-2, but the data is rev-id-1
815
 
        records = [(('rev-id-2',), (('rev-id-2',), 0, len(gz_txt)))]
816
 
        self.assertRaises(errors.KnitCorrupt, list,
817
 
            knit._read_records_iter(records))
818
 
 
819
 
        # read_records_iter_raw detects mismatches in the header
820
 
        self.assertRaises(errors.KnitCorrupt, list,
821
 
            knit._read_records_iter_raw(records))
822
 
 
823
 
    def test_uncompressed_data(self):
824
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
825
 
        txt = ('version rev-id-1 2 %s\n'
826
 
               'foo\n'
827
 
               'bar\n'
828
 
               'end rev-id-1\n'
829
 
               % (sha1sum,))
830
 
        transport = MockTransport([txt])
831
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
832
 
        knit = KnitVersionedFiles(None, access)
833
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(txt)))]
834
 
 
835
 
        # We don't have valid gzip data ==> corrupt
836
 
        self.assertRaises(errors.KnitCorrupt, list,
837
 
            knit._read_records_iter(records))
838
 
 
839
 
        # read_records_iter_raw will notice the bad data
840
 
        self.assertRaises(errors.KnitCorrupt, list,
841
 
            knit._read_records_iter_raw(records))
842
 
 
843
 
    def test_corrupted_data(self):
844
 
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
845
 
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
846
 
                                        'foo\n'
847
 
                                        'bar\n'
848
 
                                        'end rev-id-1\n'
849
 
                                        % (sha1sum,))
850
 
        # Change 2 bytes in the middle to \xff
851
 
        gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
852
 
        transport = MockTransport([gz_txt])
853
 
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
854
 
        knit = KnitVersionedFiles(None, access)
855
 
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
856
 
        self.assertRaises(errors.KnitCorrupt, list,
857
 
            knit._read_records_iter(records))
858
 
        # read_records_iter_raw will barf on bad gz data
859
 
        self.assertRaises(errors.KnitCorrupt, list,
860
 
            knit._read_records_iter_raw(records))
861
 
 
862
 
 
863
 
class LowLevelKnitIndexTests(TestCase):
864
 
 
865
 
    def get_knit_index(self, transport, name, mode):
866
 
        mapper = ConstantMapper(name)
867
 
        orig = knit._load_data
868
 
        def reset():
869
 
            knit._load_data = orig
870
 
        self.addCleanup(reset)
871
 
        from bzrlib._knit_load_data_py import _load_data_py
872
 
        knit._load_data = _load_data_py
873
 
        allow_writes = lambda: 'w' in mode
874
 
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
875
 
 
876
 
    def test_create_file(self):
877
 
        transport = MockTransport()
878
 
        index = self.get_knit_index(transport, "filename", "w")
879
 
        index.keys()
880
 
        call = transport.calls.pop(0)
881
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
882
 
        self.assertEqual('put_file_non_atomic', call[0])
883
 
        self.assertEqual('filename.kndx', call[1][0])
884
 
        # With no history, _KndxIndex writes a new index:
885
 
        self.assertEqual(_KndxIndex.HEADER,
886
 
            call[1][1].getvalue())
887
 
        self.assertEqual({'create_parent_dir': True}, call[2])
888
 
 
889
 
    def test_read_utf8_version_id(self):
890
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
891
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
892
 
        transport = MockTransport([
893
 
            _KndxIndex.HEADER,
894
 
            '%s option 0 1 :' % (utf8_revision_id,)
895
 
            ])
896
 
        index = self.get_knit_index(transport, "filename", "r")
897
 
        # _KndxIndex is a private class, and deals in utf8 revision_ids, not
898
 
        # Unicode revision_ids.
899
 
        self.assertEqual({(utf8_revision_id,):()},
900
 
            index.get_parent_map(index.keys()))
901
 
        self.assertFalse((unicode_revision_id,) in index.keys())
902
 
 
903
 
    def test_read_utf8_parents(self):
904
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
905
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
906
 
        transport = MockTransport([
907
 
            _KndxIndex.HEADER,
908
 
            "version option 0 1 .%s :" % (utf8_revision_id,)
909
 
            ])
910
 
        index = self.get_knit_index(transport, "filename", "r")
911
 
        self.assertEqual({("version",):((utf8_revision_id,),)},
912
 
            index.get_parent_map(index.keys()))
913
 
 
914
 
    def test_read_ignore_corrupted_lines(self):
915
 
        transport = MockTransport([
916
 
            _KndxIndex.HEADER,
917
 
            "corrupted",
918
 
            "corrupted options 0 1 .b .c ",
919
 
            "version options 0 1 :"
920
 
            ])
921
 
        index = self.get_knit_index(transport, "filename", "r")
922
 
        self.assertEqual(1, len(index.keys()))
923
 
        self.assertEqual(set([("version",)]), index.keys())
924
 
 
925
 
    def test_read_corrupted_header(self):
926
 
        transport = MockTransport(['not a bzr knit index header\n'])
927
 
        index = self.get_knit_index(transport, "filename", "r")
928
 
        self.assertRaises(KnitHeaderError, index.keys)
929
 
 
930
 
    def test_read_duplicate_entries(self):
931
 
        transport = MockTransport([
932
 
            _KndxIndex.HEADER,
933
 
            "parent options 0 1 :",
934
 
            "version options1 0 1 0 :",
935
 
            "version options2 1 2 .other :",
936
 
            "version options3 3 4 0 .other :"
937
 
            ])
938
 
        index = self.get_knit_index(transport, "filename", "r")
939
 
        self.assertEqual(2, len(index.keys()))
940
 
        # check that the index used is the first one written. (Specific
941
 
        # to KnitIndex style indices.
942
 
        self.assertEqual("1", index._dictionary_compress([("version",)]))
943
 
        self.assertEqual((("version",), 3, 4), index.get_position(("version",)))
944
 
        self.assertEqual(["options3"], index.get_options(("version",)))
945
 
        self.assertEqual({("version",):(("parent",), ("other",))},
946
 
            index.get_parent_map([("version",)]))
947
 
 
948
 
    def test_read_compressed_parents(self):
949
 
        transport = MockTransport([
950
 
            _KndxIndex.HEADER,
951
 
            "a option 0 1 :",
952
 
            "b option 0 1 0 :",
953
 
            "c option 0 1 1 0 :",
954
 
            ])
955
 
        index = self.get_knit_index(transport, "filename", "r")
956
 
        self.assertEqual({("b",):(("a",),), ("c",):(("b",), ("a",))},
957
 
            index.get_parent_map([("b",), ("c",)]))
958
 
 
959
 
    def test_write_utf8_version_id(self):
960
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
961
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
962
 
        transport = MockTransport([
963
 
            _KndxIndex.HEADER
964
 
            ])
965
 
        index = self.get_knit_index(transport, "filename", "r")
966
 
        index.add_records([
967
 
            ((utf8_revision_id,), ["option"], ((utf8_revision_id,), 0, 1), [])])
968
 
        call = transport.calls.pop(0)
969
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
970
 
        self.assertEqual('put_file_non_atomic', call[0])
971
 
        self.assertEqual('filename.kndx', call[1][0])
972
 
        # With no history, _KndxIndex writes a new index:
973
 
        self.assertEqual(_KndxIndex.HEADER +
974
 
            "\n%s option 0 1  :" % (utf8_revision_id,),
975
 
            call[1][1].getvalue())
976
 
        self.assertEqual({'create_parent_dir': True}, call[2])
977
 
 
978
 
    def test_write_utf8_parents(self):
979
 
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
980
 
        utf8_revision_id = unicode_revision_id.encode('utf-8')
981
 
        transport = MockTransport([
982
 
            _KndxIndex.HEADER
983
 
            ])
984
 
        index = self.get_knit_index(transport, "filename", "r")
985
 
        index.add_records([
986
 
            (("version",), ["option"], (("version",), 0, 1), [(utf8_revision_id,)])])
987
 
        call = transport.calls.pop(0)
988
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
989
 
        self.assertEqual('put_file_non_atomic', call[0])
990
 
        self.assertEqual('filename.kndx', call[1][0])
991
 
        # With no history, _KndxIndex writes a new index:
992
 
        self.assertEqual(_KndxIndex.HEADER +
993
 
            "\nversion option 0 1 .%s :" % (utf8_revision_id,),
994
 
            call[1][1].getvalue())
995
 
        self.assertEqual({'create_parent_dir': True}, call[2])
996
 
 
997
 
    def test_keys(self):
998
 
        transport = MockTransport([
999
 
            _KndxIndex.HEADER
1000
 
            ])
1001
 
        index = self.get_knit_index(transport, "filename", "r")
1002
 
 
1003
 
        self.assertEqual(set(), index.keys())
1004
 
 
1005
 
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
1006
 
        self.assertEqual(set([("a",)]), index.keys())
1007
 
 
1008
 
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
1009
 
        self.assertEqual(set([("a",)]), index.keys())
1010
 
 
1011
 
        index.add_records([(("b",), ["option"], (("b",), 0, 1), [])])
1012
 
        self.assertEqual(set([("a",), ("b",)]), index.keys())
1013
 
 
1014
 
    def add_a_b(self, index, random_id=None):
1015
 
        kwargs = {}
1016
 
        if random_id is not None:
1017
 
            kwargs["random_id"] = random_id
1018
 
        index.add_records([
1019
 
            (("a",), ["option"], (("a",), 0, 1), [("b",)]),
1020
 
            (("a",), ["opt"], (("a",), 1, 2), [("c",)]),
1021
 
            (("b",), ["option"], (("b",), 2, 3), [("a",)])
1022
 
            ], **kwargs)
1023
 
 
1024
 
    def assertIndexIsAB(self, index):
1025
 
        self.assertEqual({
1026
 
            ('a',): (('c',),),
1027
 
            ('b',): (('a',),),
1028
 
            },
1029
 
            index.get_parent_map(index.keys()))
1030
 
        self.assertEqual((("a",), 1, 2), index.get_position(("a",)))
1031
 
        self.assertEqual((("b",), 2, 3), index.get_position(("b",)))
1032
 
        self.assertEqual(["opt"], index.get_options(("a",)))
1033
 
 
1034
 
    def test_add_versions(self):
1035
 
        transport = MockTransport([
1036
 
            _KndxIndex.HEADER
1037
 
            ])
1038
 
        index = self.get_knit_index(transport, "filename", "r")
1039
 
 
1040
 
        self.add_a_b(index)
1041
 
        call = transport.calls.pop(0)
1042
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
1043
 
        self.assertEqual('put_file_non_atomic', call[0])
1044
 
        self.assertEqual('filename.kndx', call[1][0])
1045
 
        # With no history, _KndxIndex writes a new index:
1046
 
        self.assertEqual(
1047
 
            _KndxIndex.HEADER +
1048
 
            "\na option 0 1 .b :"
1049
 
            "\na opt 1 2 .c :"
1050
 
            "\nb option 2 3 0 :",
1051
 
            call[1][1].getvalue())
1052
 
        self.assertEqual({'create_parent_dir': True}, call[2])
1053
 
        self.assertIndexIsAB(index)
1054
 
 
1055
 
    def test_add_versions_random_id_is_accepted(self):
1056
 
        transport = MockTransport([
1057
 
            _KndxIndex.HEADER
1058
 
            ])
1059
 
        index = self.get_knit_index(transport, "filename", "r")
1060
 
        self.add_a_b(index, random_id=True)
1061
 
 
1062
 
    def test_delay_create_and_add_versions(self):
1063
 
        transport = MockTransport()
1064
 
 
1065
 
        index = self.get_knit_index(transport, "filename", "w")
1066
 
        # dir_mode=0777)
1067
 
        self.assertEqual([], transport.calls)
1068
 
        self.add_a_b(index)
1069
 
        #self.assertEqual(
1070
 
        #[    {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
1071
 
        #    kwargs)
1072
 
        # Two calls: one during which we load the existing index (and when its
1073
 
        # missing create it), then a second where we write the contents out.
1074
 
        self.assertEqual(2, len(transport.calls))
1075
 
        call = transport.calls.pop(0)
1076
 
        self.assertEqual('put_file_non_atomic', call[0])
1077
 
        self.assertEqual('filename.kndx', call[1][0])
1078
 
        # With no history, _KndxIndex writes a new index:
1079
 
        self.assertEqual(_KndxIndex.HEADER, call[1][1].getvalue())
1080
 
        self.assertEqual({'create_parent_dir': True}, call[2])
1081
 
        call = transport.calls.pop(0)
1082
 
        # call[1][1] is a StringIO - we can't test it by simple equality.
1083
 
        self.assertEqual('put_file_non_atomic', call[0])
1084
 
        self.assertEqual('filename.kndx', call[1][0])
1085
 
        # With no history, _KndxIndex writes a new index:
1086
 
        self.assertEqual(
1087
 
            _KndxIndex.HEADER +
1088
 
            "\na option 0 1 .b :"
1089
 
            "\na opt 1 2 .c :"
1090
 
            "\nb option 2 3 0 :",
1091
 
            call[1][1].getvalue())
1092
 
        self.assertEqual({'create_parent_dir': True}, call[2])
1093
 
 
1094
 
    def test_get_position(self):
1095
 
        transport = MockTransport([
1096
 
            _KndxIndex.HEADER,
1097
 
            "a option 0 1 :",
1098
 
            "b option 1 2 :"
1099
 
            ])
1100
 
        index = self.get_knit_index(transport, "filename", "r")
1101
 
 
1102
 
        self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
1103
 
        self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
1104
 
 
1105
 
    def test_get_method(self):
1106
 
        transport = MockTransport([
1107
 
            _KndxIndex.HEADER,
1108
 
            "a fulltext,unknown 0 1 :",
1109
 
            "b unknown,line-delta 1 2 :",
1110
 
            "c bad 3 4 :"
1111
 
            ])
1112
 
        index = self.get_knit_index(transport, "filename", "r")
1113
 
 
1114
 
        self.assertEqual("fulltext", index.get_method("a"))
1115
 
        self.assertEqual("line-delta", index.get_method("b"))
1116
 
        self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
1117
 
 
1118
 
    def test_get_options(self):
1119
 
        transport = MockTransport([
1120
 
            _KndxIndex.HEADER,
1121
 
            "a opt1 0 1 :",
1122
 
            "b opt2,opt3 1 2 :"
1123
 
            ])
1124
 
        index = self.get_knit_index(transport, "filename", "r")
1125
 
 
1126
 
        self.assertEqual(["opt1"], index.get_options("a"))
1127
 
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
1128
 
 
1129
 
    def test_get_parent_map(self):
1130
 
        transport = MockTransport([
1131
 
            _KndxIndex.HEADER,
1132
 
            "a option 0 1 :",
1133
 
            "b option 1 2 0 .c :",
1134
 
            "c option 1 2 1 0 .e :"
1135
 
            ])
1136
 
        index = self.get_knit_index(transport, "filename", "r")
1137
 
 
1138
 
        self.assertEqual({
1139
 
            ("a",):(),
1140
 
            ("b",):(("a",), ("c",)),
1141
 
            ("c",):(("b",), ("a",), ("e",)),
1142
 
            }, index.get_parent_map(index.keys()))
1143
 
 
1144
 
    def test_impossible_parent(self):
1145
 
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
1146
 
        transport = MockTransport([
1147
 
            _KndxIndex.HEADER,
1148
 
            "a option 0 1 :",
1149
 
            "b option 0 1 4 :"  # We don't have a 4th record
1150
 
            ])
1151
 
        index = self.get_knit_index(transport, 'filename', 'r')
1152
 
        try:
1153
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
1154
 
        except TypeError, e:
1155
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
1156
 
                           ' not exceptions.IndexError')
1157
 
                and sys.version_info[0:2] >= (2,5)):
1158
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
1159
 
                                  ' raising new style exceptions with python'
1160
 
                                  ' >=2.5')
1161
 
            else:
1162
 
                raise
1163
 
 
1164
 
    def test_corrupted_parent(self):
1165
 
        transport = MockTransport([
1166
 
            _KndxIndex.HEADER,
1167
 
            "a option 0 1 :",
1168
 
            "b option 0 1 :",
1169
 
            "c option 0 1 1v :", # Can't have a parent of '1v'
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_corrupted_parent_in_list(self):
1185
 
        transport = MockTransport([
1186
 
            _KndxIndex.HEADER,
1187
 
            "a option 0 1 :",
1188
 
            "b option 0 1 :",
1189
 
            "c option 0 1 1 v :", # Can't have a parent of 'v'
1190
 
            ])
1191
 
        index = self.get_knit_index(transport, 'filename', 'r')
1192
 
        try:
1193
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
1194
 
        except TypeError, e:
1195
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
1196
 
                           ' not exceptions.ValueError')
1197
 
                and sys.version_info[0:2] >= (2,5)):
1198
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
1199
 
                                  ' raising new style exceptions with python'
1200
 
                                  ' >=2.5')
1201
 
            else:
1202
 
                raise
1203
 
 
1204
 
    def test_invalid_position(self):
1205
 
        transport = MockTransport([
1206
 
            _KndxIndex.HEADER,
1207
 
            "a option 1v 1 :",
1208
 
            ])
1209
 
        index = self.get_knit_index(transport, 'filename', 'r')
1210
 
        try:
1211
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
1212
 
        except TypeError, e:
1213
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
1214
 
                           ' not exceptions.ValueError')
1215
 
                and sys.version_info[0:2] >= (2,5)):
1216
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
1217
 
                                  ' raising new style exceptions with python'
1218
 
                                  ' >=2.5')
1219
 
            else:
1220
 
                raise
1221
 
 
1222
 
    def test_invalid_size(self):
1223
 
        transport = MockTransport([
1224
 
            _KndxIndex.HEADER,
1225
 
            "a option 1 1v :",
1226
 
            ])
1227
 
        index = self.get_knit_index(transport, 'filename', 'r')
1228
 
        try:
1229
 
            self.assertRaises(errors.KnitCorrupt, index.keys)
1230
 
        except TypeError, e:
1231
 
            if (str(e) == ('exceptions must be strings, classes, or instances,'
1232
 
                           ' not exceptions.ValueError')
1233
 
                and sys.version_info[0:2] >= (2,5)):
1234
 
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
1235
 
                                  ' raising new style exceptions with python'
1236
 
                                  ' >=2.5')
1237
 
            else:
1238
 
                raise
1239
 
 
1240
 
    def test_short_line(self):
1241
 
        transport = MockTransport([
1242
 
            _KndxIndex.HEADER,
1243
 
            "a option 0 10  :",
1244
 
            "b option 10 10 0", # This line isn't terminated, ignored
1245
 
            ])
1246
 
        index = self.get_knit_index(transport, "filename", "r")
1247
 
        self.assertEqual(set([('a',)]), index.keys())
1248
 
 
1249
 
    def test_skip_incomplete_record(self):
1250
 
        # A line with bogus data should just be skipped
1251
 
        transport = MockTransport([
1252
 
            _KndxIndex.HEADER,
1253
 
            "a option 0 10  :",
1254
 
            "b option 10 10 0", # This line isn't terminated, ignored
1255
 
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
1256
 
            ])
1257
 
        index = self.get_knit_index(transport, "filename", "r")
1258
 
        self.assertEqual(set([('a',), ('c',)]), index.keys())
1259
 
 
1260
 
    def test_trailing_characters(self):
1261
 
        # A line with bogus data should just be skipped
1262
 
        transport = MockTransport([
1263
 
            _KndxIndex.HEADER,
1264
 
            "a option 0 10  :",
1265
 
            "b option 10 10 0 :a", # This line has extra trailing characters
1266
 
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
1267
 
            ])
1268
 
        index = self.get_knit_index(transport, "filename", "r")
1269
 
        self.assertEqual(set([('a',), ('c',)]), index.keys())
1270
 
 
1271
 
 
1272
 
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
1273
 
 
1274
 
    _test_needs_features = [CompiledKnitFeature]
1275
 
 
1276
 
    def get_knit_index(self, transport, name, mode):
1277
 
        mapper = ConstantMapper(name)
1278
 
        orig = knit._load_data
1279
 
        def reset():
1280
 
            knit._load_data = orig
1281
 
        self.addCleanup(reset)
1282
 
        from bzrlib._knit_load_data_c import _load_data_c
1283
 
        knit._load_data = _load_data_c
1284
 
        allow_writes = lambda: mode == 'w'
1285
 
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
1286
 
 
1287
 
 
1288
 
class KnitTests(TestCaseWithTransport):
1289
 
    """Class containing knit test helper routines."""
1290
 
 
1291
 
    def make_test_knit(self, annotate=False, name='test'):
1292
 
        mapper = ConstantMapper(name)
1293
 
        return make_file_factory(annotate, mapper)(self.get_transport())
1294
 
 
1295
 
 
1296
 
class TestBadShaError(KnitTests):
1297
 
    """Tests for handling of sha errors."""
1298
 
 
1299
 
    def test_exception_has_text(self):
1300
 
        # having the failed text included in the error allows for recovery.
1301
 
        source = self.make_test_knit()
1302
 
        target = self.make_test_knit(name="target")
1303
 
        if not source._max_delta_chain:
1304
 
            raise TestNotApplicable(
1305
 
                "cannot get delta-caused sha failures without deltas.")
1306
 
        # create a basis
1307
 
        basis = ('basis',)
1308
 
        broken = ('broken',)
1309
 
        source.add_lines(basis, (), ['foo\n'])
1310
 
        source.add_lines(broken, (basis,), ['foo\n', 'bar\n'])
1311
 
        # Seed target with a bad basis text
1312
 
        target.add_lines(basis, (), ['gam\n'])
1313
 
        target.insert_record_stream(
1314
 
            source.get_record_stream([broken], 'unordered', False))
1315
 
        err = self.assertRaises(errors.KnitCorrupt,
1316
 
            target.get_record_stream([broken], 'unordered', True).next)
1317
 
        self.assertEqual(['gam\n', 'bar\n'], err.content)
1318
 
        # Test for formatting with live data
1319
 
        self.assertStartsWith(str(err), "Knit ")
1320
 
 
1321
 
 
1322
 
class TestKnitIndex(KnitTests):
1323
 
 
1324
 
    def test_add_versions_dictionary_compresses(self):
1325
 
        """Adding versions to the index should update the lookup dict"""
1326
 
        knit = self.make_test_knit()
1327
 
        idx = knit._index
1328
 
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
1329
 
        self.check_file_contents('test.kndx',
1330
 
            '# bzr knit index 8\n'
1331
 
            '\n'
1332
 
            'a-1 fulltext 0 0  :'
1333
 
            )
1334
 
        idx.add_records([
1335
 
            (('a-2',), ['fulltext'], (('a-2',), 0, 0), [('a-1',)]),
1336
 
            (('a-3',), ['fulltext'], (('a-3',), 0, 0), [('a-2',)]),
1337
 
            ])
1338
 
        self.check_file_contents('test.kndx',
1339
 
            '# bzr knit index 8\n'
1340
 
            '\n'
1341
 
            'a-1 fulltext 0 0  :\n'
1342
 
            'a-2 fulltext 0 0 0 :\n'
1343
 
            'a-3 fulltext 0 0 1 :'
1344
 
            )
1345
 
        self.assertEqual(set([('a-3',), ('a-1',), ('a-2',)]), idx.keys())
1346
 
        self.assertEqual({
1347
 
            ('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False)),
1348
 
            ('a-2',): ((('a-2',), 0, 0), None, (('a-1',),), ('fulltext', False)),
1349
 
            ('a-3',): ((('a-3',), 0, 0), None, (('a-2',),), ('fulltext', False)),
1350
 
            }, idx.get_build_details(idx.keys()))
1351
 
        self.assertEqual({('a-1',):(),
1352
 
            ('a-2',):(('a-1',),),
1353
 
            ('a-3',):(('a-2',),),},
1354
 
            idx.get_parent_map(idx.keys()))
1355
 
 
1356
 
    def test_add_versions_fails_clean(self):
1357
 
        """If add_versions fails in the middle, it restores a pristine state.
1358
 
 
1359
 
        Any modifications that are made to the index are reset if all versions
1360
 
        cannot be added.
1361
 
        """
1362
 
        # This cheats a little bit by passing in a generator which will
1363
 
        # raise an exception before the processing finishes
1364
 
        # Other possibilities would be to have an version with the wrong number
1365
 
        # of entries, or to make the backing transport unable to write any
1366
 
        # files.
1367
 
 
1368
 
        knit = self.make_test_knit()
1369
 
        idx = knit._index
1370
 
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
1371
 
 
1372
 
        class StopEarly(Exception):
1373
 
            pass
1374
 
 
1375
 
        def generate_failure():
1376
 
            """Add some entries and then raise an exception"""
1377
 
            yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
1378
 
            yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
1379
 
            raise StopEarly()
1380
 
 
1381
 
        # Assert the pre-condition
1382
 
        def assertA1Only():
1383
 
            self.assertEqual(set([('a-1',)]), set(idx.keys()))
1384
 
            self.assertEqual(
1385
 
                {('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False))},
1386
 
                idx.get_build_details([('a-1',)]))
1387
 
            self.assertEqual({('a-1',):()}, idx.get_parent_map(idx.keys()))
1388
 
 
1389
 
        assertA1Only()
1390
 
        self.assertRaises(StopEarly, idx.add_records, generate_failure())
1391
 
        # And it shouldn't be modified
1392
 
        assertA1Only()
1393
 
 
1394
 
    def test_knit_index_ignores_empty_files(self):
1395
 
        # There was a race condition in older bzr, where a ^C at the right time
1396
 
        # could leave an empty .kndx file, which bzr would later claim was a
1397
 
        # corrupted file since the header was not present. In reality, the file
1398
 
        # just wasn't created, so it should be ignored.
1399
 
        t = get_transport('.')
1400
 
        t.put_bytes('test.kndx', '')
1401
 
 
1402
 
        knit = self.make_test_knit()
1403
 
 
1404
 
    def test_knit_index_checks_header(self):
1405
 
        t = get_transport('.')
1406
 
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
1407
 
        k = self.make_test_knit()
1408
 
        self.assertRaises(KnitHeaderError, k.keys)
1409
 
 
1410
 
 
1411
 
class TestGraphIndexKnit(KnitTests):
1412
 
    """Tests for knits using a GraphIndex rather than a KnitIndex."""
1413
 
 
1414
 
    def make_g_index(self, name, ref_lists=0, nodes=[]):
1415
 
        builder = GraphIndexBuilder(ref_lists)
1416
 
        for node, references, value in nodes:
1417
 
            builder.add_node(node, references, value)
1418
 
        stream = builder.finish()
1419
 
        trans = self.get_transport()
1420
 
        size = trans.put_file(name, stream)
1421
 
        return GraphIndex(trans, name, size)
1422
 
 
1423
 
    def two_graph_index(self, deltas=False, catch_adds=False):
1424
 
        """Build a two-graph index.
1425
 
 
1426
 
        :param deltas: If true, use underlying indices with two node-ref
1427
 
            lists and 'parent' set to a delta-compressed against tail.
1428
 
        """
1429
 
        # build a complex graph across several indices.
1430
 
        if deltas:
1431
 
            # delta compression inn the index
1432
 
            index1 = self.make_g_index('1', 2, [
1433
 
                (('tip', ), 'N0 100', ([('parent', )], [], )),
1434
 
                (('tail', ), '', ([], []))])
1435
 
            index2 = self.make_g_index('2', 2, [
1436
 
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
1437
 
                (('separate', ), '', ([], []))])
1438
 
        else:
1439
 
            # just blob location and graph in the index.
1440
 
            index1 = self.make_g_index('1', 1, [
1441
 
                (('tip', ), 'N0 100', ([('parent', )], )),
1442
 
                (('tail', ), '', ([], ))])
1443
 
            index2 = self.make_g_index('2', 1, [
1444
 
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
1445
 
                (('separate', ), '', ([], ))])
1446
 
        combined_index = CombinedGraphIndex([index1, index2])
1447
 
        if catch_adds:
1448
 
            self.combined_index = combined_index
1449
 
            self.caught_entries = []
1450
 
            add_callback = self.catch_add
1451
 
        else:
1452
 
            add_callback = None
1453
 
        return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
1454
 
            add_callback=add_callback)
1455
 
 
1456
 
    def test_keys(self):
1457
 
        index = self.two_graph_index()
1458
 
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
1459
 
            set(index.keys()))
1460
 
 
1461
 
    def test_get_position(self):
1462
 
        index = self.two_graph_index()
1463
 
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position(('tip',)))
1464
 
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position(('parent',)))
1465
 
 
1466
 
    def test_get_method_deltas(self):
1467
 
        index = self.two_graph_index(deltas=True)
1468
 
        self.assertEqual('fulltext', index.get_method(('tip',)))
1469
 
        self.assertEqual('line-delta', index.get_method(('parent',)))
1470
 
 
1471
 
    def test_get_method_no_deltas(self):
1472
 
        # check that the parent-history lookup is ignored with deltas=False.
1473
 
        index = self.two_graph_index(deltas=False)
1474
 
        self.assertEqual('fulltext', index.get_method(('tip',)))
1475
 
        self.assertEqual('fulltext', index.get_method(('parent',)))
1476
 
 
1477
 
    def test_get_options_deltas(self):
1478
 
        index = self.two_graph_index(deltas=True)
1479
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1480
 
        self.assertEqual(['line-delta'], index.get_options(('parent',)))
1481
 
 
1482
 
    def test_get_options_no_deltas(self):
1483
 
        # check that the parent-history lookup is ignored with deltas=False.
1484
 
        index = self.two_graph_index(deltas=False)
1485
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1486
 
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
1487
 
 
1488
 
    def test_get_parent_map(self):
1489
 
        index = self.two_graph_index()
1490
 
        self.assertEqual({('parent',):(('tail',), ('ghost',))},
1491
 
            index.get_parent_map([('parent',), ('ghost',)]))
1492
 
 
1493
 
    def catch_add(self, entries):
1494
 
        self.caught_entries.append(entries)
1495
 
 
1496
 
    def test_add_no_callback_errors(self):
1497
 
        index = self.two_graph_index()
1498
 
        self.assertRaises(errors.ReadOnlyError, index.add_records,
1499
 
            [(('new',), 'fulltext,no-eol', (None, 50, 60), ['separate'])])
1500
 
 
1501
 
    def test_add_version_smoke(self):
1502
 
        index = self.two_graph_index(catch_adds=True)
1503
 
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60),
1504
 
            [('separate',)])])
1505
 
        self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
1506
 
            self.caught_entries)
1507
 
 
1508
 
    def test_add_version_delta_not_delta_index(self):
1509
 
        index = self.two_graph_index(catch_adds=True)
1510
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1511
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1512
 
        self.assertEqual([], self.caught_entries)
1513
 
 
1514
 
    def test_add_version_same_dup(self):
1515
 
        index = self.two_graph_index(catch_adds=True)
1516
 
        # options can be spelt two different ways
1517
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1518
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
1519
 
        # position/length are ignored (because each pack could have fulltext or
1520
 
        # delta, and be at a different position.
1521
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
1522
 
            [('parent',)])])
1523
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
1524
 
            [('parent',)])])
1525
 
        # but neither should have added data:
1526
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1527
 
        
1528
 
    def test_add_version_different_dup(self):
1529
 
        index = self.two_graph_index(deltas=True, catch_adds=True)
1530
 
        # change options
1531
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1532
 
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
1533
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1534
 
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
1535
 
        # parents
1536
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1537
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1538
 
        self.assertEqual([], self.caught_entries)
1539
 
        
1540
 
    def test_add_versions_nodeltas(self):
1541
 
        index = self.two_graph_index(catch_adds=True)
1542
 
        index.add_records([
1543
 
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
1544
 
                (('new2',), 'fulltext', (None, 0, 6), [('new',)]),
1545
 
                ])
1546
 
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
1547
 
            (('new2', ), ' 0 6', ((('new',),),))],
1548
 
            sorted(self.caught_entries[0]))
1549
 
        self.assertEqual(1, len(self.caught_entries))
1550
 
 
1551
 
    def test_add_versions_deltas(self):
1552
 
        index = self.two_graph_index(deltas=True, catch_adds=True)
1553
 
        index.add_records([
1554
 
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
1555
 
                (('new2',), 'line-delta', (None, 0, 6), [('new',)]),
1556
 
                ])
1557
 
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
1558
 
            (('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
1559
 
            sorted(self.caught_entries[0]))
1560
 
        self.assertEqual(1, len(self.caught_entries))
1561
 
 
1562
 
    def test_add_versions_delta_not_delta_index(self):
1563
 
        index = self.two_graph_index(catch_adds=True)
1564
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1565
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1566
 
        self.assertEqual([], self.caught_entries)
1567
 
 
1568
 
    def test_add_versions_random_id_accepted(self):
1569
 
        index = self.two_graph_index(catch_adds=True)
1570
 
        index.add_records([], random_id=True)
1571
 
 
1572
 
    def test_add_versions_same_dup(self):
1573
 
        index = self.two_graph_index(catch_adds=True)
1574
 
        # options can be spelt two different ways
1575
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100),
1576
 
            [('parent',)])])
1577
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100),
1578
 
            [('parent',)])])
1579
 
        # position/length are ignored (because each pack could have fulltext or
1580
 
        # delta, and be at a different position.
1581
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
1582
 
            [('parent',)])])
1583
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
1584
 
            [('parent',)])])
1585
 
        # but neither should have added data.
1586
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1587
 
        
1588
 
    def test_add_versions_different_dup(self):
1589
 
        index = self.two_graph_index(deltas=True, catch_adds=True)
1590
 
        # change options
1591
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1592
 
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
1593
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1594
 
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
1595
 
        # parents
1596
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1597
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1598
 
        # change options in the second record
1599
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1600
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)]),
1601
 
             (('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
1602
 
        self.assertEqual([], self.caught_entries)
1603
 
 
1604
 
 
1605
 
class TestNoParentsGraphIndexKnit(KnitTests):
1606
 
    """Tests for knits using _KnitGraphIndex with no parents."""
1607
 
 
1608
 
    def make_g_index(self, name, ref_lists=0, nodes=[]):
1609
 
        builder = GraphIndexBuilder(ref_lists)
1610
 
        for node, references in nodes:
1611
 
            builder.add_node(node, references)
1612
 
        stream = builder.finish()
1613
 
        trans = self.get_transport()
1614
 
        size = trans.put_file(name, stream)
1615
 
        return GraphIndex(trans, name, size)
1616
 
 
1617
 
    def test_parents_deltas_incompatible(self):
1618
 
        index = CombinedGraphIndex([])
1619
 
        self.assertRaises(errors.KnitError, _KnitGraphIndex, lambda:True,
1620
 
            index, deltas=True, parents=False)
1621
 
 
1622
 
    def two_graph_index(self, catch_adds=False):
1623
 
        """Build a two-graph index.
1624
 
 
1625
 
        :param deltas: If true, use underlying indices with two node-ref
1626
 
            lists and 'parent' set to a delta-compressed against tail.
1627
 
        """
1628
 
        # put several versions in the index.
1629
 
        index1 = self.make_g_index('1', 0, [
1630
 
            (('tip', ), 'N0 100'),
1631
 
            (('tail', ), '')])
1632
 
        index2 = self.make_g_index('2', 0, [
1633
 
            (('parent', ), ' 100 78'),
1634
 
            (('separate', ), '')])
1635
 
        combined_index = CombinedGraphIndex([index1, index2])
1636
 
        if catch_adds:
1637
 
            self.combined_index = combined_index
1638
 
            self.caught_entries = []
1639
 
            add_callback = self.catch_add
1640
 
        else:
1641
 
            add_callback = None
1642
 
        return _KnitGraphIndex(combined_index, lambda:True, parents=False,
1643
 
            add_callback=add_callback)
1644
 
 
1645
 
    def test_keys(self):
1646
 
        index = self.two_graph_index()
1647
 
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
1648
 
            set(index.keys()))
1649
 
 
1650
 
    def test_get_position(self):
1651
 
        index = self.two_graph_index()
1652
 
        self.assertEqual((index._graph_index._indices[0], 0, 100),
1653
 
            index.get_position(('tip',)))
1654
 
        self.assertEqual((index._graph_index._indices[1], 100, 78),
1655
 
            index.get_position(('parent',)))
1656
 
 
1657
 
    def test_get_method(self):
1658
 
        index = self.two_graph_index()
1659
 
        self.assertEqual('fulltext', index.get_method(('tip',)))
1660
 
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
1661
 
 
1662
 
    def test_get_options(self):
1663
 
        index = self.two_graph_index()
1664
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1665
 
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
1666
 
 
1667
 
    def test_get_parent_map(self):
1668
 
        index = self.two_graph_index()
1669
 
        self.assertEqual({('parent',):None},
1670
 
            index.get_parent_map([('parent',), ('ghost',)]))
1671
 
 
1672
 
    def catch_add(self, entries):
1673
 
        self.caught_entries.append(entries)
1674
 
 
1675
 
    def test_add_no_callback_errors(self):
1676
 
        index = self.two_graph_index()
1677
 
        self.assertRaises(errors.ReadOnlyError, index.add_records,
1678
 
            [(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)])])
1679
 
 
1680
 
    def test_add_version_smoke(self):
1681
 
        index = self.two_graph_index(catch_adds=True)
1682
 
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60), [])])
1683
 
        self.assertEqual([[(('new', ), 'N50 60')]],
1684
 
            self.caught_entries)
1685
 
 
1686
 
    def test_add_version_delta_not_delta_index(self):
1687
 
        index = self.two_graph_index(catch_adds=True)
1688
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1689
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [])])
1690
 
        self.assertEqual([], self.caught_entries)
1691
 
 
1692
 
    def test_add_version_same_dup(self):
1693
 
        index = self.two_graph_index(catch_adds=True)
1694
 
        # options can be spelt two different ways
1695
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1696
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
1697
 
        # position/length are ignored (because each pack could have fulltext or
1698
 
        # delta, and be at a different position.
1699
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
1700
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
1701
 
        # but neither should have added data.
1702
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1703
 
        
1704
 
    def test_add_version_different_dup(self):
1705
 
        index = self.two_graph_index(catch_adds=True)
1706
 
        # change options
1707
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1708
 
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1709
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1710
 
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
1711
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1712
 
            [(('tip',), 'fulltext', (None, 0, 100), [])])
1713
 
        # parents
1714
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1715
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1716
 
        self.assertEqual([], self.caught_entries)
1717
 
        
1718
 
    def test_add_versions(self):
1719
 
        index = self.two_graph_index(catch_adds=True)
1720
 
        index.add_records([
1721
 
                (('new',), 'fulltext,no-eol', (None, 50, 60), []),
1722
 
                (('new2',), 'fulltext', (None, 0, 6), []),
1723
 
                ])
1724
 
        self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
1725
 
            sorted(self.caught_entries[0]))
1726
 
        self.assertEqual(1, len(self.caught_entries))
1727
 
 
1728
 
    def test_add_versions_delta_not_delta_index(self):
1729
 
        index = self.two_graph_index(catch_adds=True)
1730
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1731
 
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1732
 
        self.assertEqual([], self.caught_entries)
1733
 
 
1734
 
    def test_add_versions_parents_not_parents_index(self):
1735
 
        index = self.two_graph_index(catch_adds=True)
1736
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1737
 
            [(('new',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
1738
 
        self.assertEqual([], self.caught_entries)
1739
 
 
1740
 
    def test_add_versions_random_id_accepted(self):
1741
 
        index = self.two_graph_index(catch_adds=True)
1742
 
        index.add_records([], random_id=True)
1743
 
 
1744
 
    def test_add_versions_same_dup(self):
1745
 
        index = self.two_graph_index(catch_adds=True)
1746
 
        # options can be spelt two different ways
1747
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1748
 
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
1749
 
        # position/length are ignored (because each pack could have fulltext or
1750
 
        # delta, and be at a different position.
1751
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
1752
 
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
1753
 
        # but neither should have added data.
1754
 
        self.assertEqual([[], [], [], []], self.caught_entries)
1755
 
        
1756
 
    def test_add_versions_different_dup(self):
1757
 
        index = self.two_graph_index(catch_adds=True)
1758
 
        # change options
1759
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1760
 
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1761
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1762
 
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
1763
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1764
 
            [(('tip',), 'fulltext', (None, 0, 100), [])])
1765
 
        # parents
1766
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1767
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1768
 
        # change options in the second record
1769
 
        self.assertRaises(errors.KnitCorrupt, index.add_records,
1770
 
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), []),
1771
 
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1772
 
        self.assertEqual([], self.caught_entries)
1773
 
 
1774
 
 
1775
 
class TestStacking(KnitTests):
1776
 
 
1777
 
    def get_basis_and_test_knit(self):
1778
 
        basis = self.make_test_knit(name='basis')
1779
 
        basis = RecordingVersionedFilesDecorator(basis)
1780
 
        test = self.make_test_knit(name='test')
1781
 
        test.add_fallback_versioned_files(basis)
1782
 
        return basis, test
1783
 
 
1784
 
    def test_add_fallback_versioned_files(self):
1785
 
        basis = self.make_test_knit(name='basis')
1786
 
        test = self.make_test_knit(name='test')
1787
 
        # It must not error; other tests test that the fallback is referred to
1788
 
        # when accessing data.
1789
 
        test.add_fallback_versioned_files(basis)
1790
 
 
1791
 
    def test_add_lines(self):
1792
 
        # lines added to the test are not added to the basis
1793
 
        basis, test = self.get_basis_and_test_knit()
1794
 
        key = ('foo',)
1795
 
        key_basis = ('bar',)
1796
 
        key_cross_border = ('quux',)
1797
 
        key_delta = ('zaphod',)
1798
 
        test.add_lines(key, (), ['foo\n'])
1799
 
        self.assertEqual({}, basis.get_parent_map([key]))
1800
 
        # lines added to the test that reference across the stack do a
1801
 
        # fulltext.
1802
 
        basis.add_lines(key_basis, (), ['foo\n'])
1803
 
        basis.calls = []
1804
 
        test.add_lines(key_cross_border, (key_basis,), ['foo\n'])
1805
 
        self.assertEqual('fulltext', test._index.get_method(key_cross_border))
1806
 
        # we don't even need to look at the basis to see that this should be
1807
 
        # stored as a fulltext
1808
 
        self.assertEqual([], basis.calls)
1809
 
        # Subsequent adds do delta.
1810
 
        basis.calls = []
1811
 
        test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
1812
 
        self.assertEqual('line-delta', test._index.get_method(key_delta))
1813
 
        self.assertEqual([], basis.calls)
1814
 
 
1815
 
    def test_annotate(self):
1816
 
        # annotations from the test knit are answered without asking the basis
1817
 
        basis, test = self.get_basis_and_test_knit()
1818
 
        key = ('foo',)
1819
 
        key_basis = ('bar',)
1820
 
        key_missing = ('missing',)
1821
 
        test.add_lines(key, (), ['foo\n'])
1822
 
        details = test.annotate(key)
1823
 
        self.assertEqual([(key, 'foo\n')], details)
1824
 
        self.assertEqual([], basis.calls)
1825
 
        # But texts that are not in the test knit are looked for in the basis
1826
 
        # directly.
1827
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1828
 
        basis.calls = []
1829
 
        details = test.annotate(key_basis)
1830
 
        self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
1831
 
        # Not optimised to date:
1832
 
        # self.assertEqual([("annotate", key_basis)], basis.calls)
1833
 
        self.assertEqual([('get_parent_map', set([key_basis])),
1834
 
            ('get_parent_map', set([key_basis])),
1835
 
            ('get_parent_map', set([key_basis])),
1836
 
            ('get_record_stream', [key_basis], 'unordered', True)],
1837
 
            basis.calls)
1838
 
 
1839
 
    def test_check(self):
1840
 
        # At the moment checking a stacked knit does implicitly check the
1841
 
        # fallback files.  
1842
 
        basis, test = self.get_basis_and_test_knit()
1843
 
        test.check()
1844
 
 
1845
 
    def test_get_parent_map(self):
1846
 
        # parents in the test knit are answered without asking the basis
1847
 
        basis, test = self.get_basis_and_test_knit()
1848
 
        key = ('foo',)
1849
 
        key_basis = ('bar',)
1850
 
        key_missing = ('missing',)
1851
 
        test.add_lines(key, (), [])
1852
 
        parent_map = test.get_parent_map([key])
1853
 
        self.assertEqual({key: ()}, parent_map)
1854
 
        self.assertEqual([], basis.calls)
1855
 
        # But parents that are not in the test knit are looked for in the basis
1856
 
        basis.add_lines(key_basis, (), [])
1857
 
        basis.calls = []
1858
 
        parent_map = test.get_parent_map([key, key_basis, key_missing])
1859
 
        self.assertEqual({key: (),
1860
 
            key_basis: ()}, parent_map)
1861
 
        self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
1862
 
            basis.calls)
1863
 
 
1864
 
    def test_get_record_stream_unordered_fulltexts(self):
1865
 
        # records from the test knit are answered without asking the basis:
1866
 
        basis, test = self.get_basis_and_test_knit()
1867
 
        key = ('foo',)
1868
 
        key_basis = ('bar',)
1869
 
        key_missing = ('missing',)
1870
 
        test.add_lines(key, (), ['foo\n'])
1871
 
        records = list(test.get_record_stream([key], 'unordered', True))
1872
 
        self.assertEqual(1, len(records))
1873
 
        self.assertEqual([], basis.calls)
1874
 
        # Missing (from test knit) objects are retrieved from the basis:
1875
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1876
 
        basis.calls = []
1877
 
        records = list(test.get_record_stream([key_basis, key_missing],
1878
 
            'unordered', True))
1879
 
        self.assertEqual(2, len(records))
1880
 
        calls = list(basis.calls)
1881
 
        for record in records:
1882
 
            self.assertSubset([record.key], (key_basis, key_missing))
1883
 
            if record.key == key_missing:
1884
 
                self.assertIsInstance(record, AbsentContentFactory)
1885
 
            else:
1886
 
                reference = list(basis.get_record_stream([key_basis],
1887
 
                    'unordered', True))[0]
1888
 
                self.assertEqual(reference.key, record.key)
1889
 
                self.assertEqual(reference.sha1, record.sha1)
1890
 
                self.assertEqual(reference.storage_kind, record.storage_kind)
1891
 
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
1892
 
                    record.get_bytes_as(record.storage_kind))
1893
 
                self.assertEqual(reference.get_bytes_as('fulltext'),
1894
 
                    record.get_bytes_as('fulltext'))
1895
 
        # It's not strictly minimal, but it seems reasonable for now for it to
1896
 
        # ask which fallbacks have which parents.
1897
 
        self.assertEqual([
1898
 
            ("get_parent_map", set([key_basis, key_missing])),
1899
 
            ("get_record_stream", [key_basis], 'unordered', True)],
1900
 
            calls)
1901
 
 
1902
 
    def test_get_record_stream_ordered_fulltexts(self):
1903
 
        # ordering is preserved down into the fallback store.
1904
 
        basis, test = self.get_basis_and_test_knit()
1905
 
        key = ('foo',)
1906
 
        key_basis = ('bar',)
1907
 
        key_basis_2 = ('quux',)
1908
 
        key_missing = ('missing',)
1909
 
        test.add_lines(key, (key_basis,), ['foo\n'])
1910
 
        # Missing (from test knit) objects are retrieved from the basis:
1911
 
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
1912
 
        basis.add_lines(key_basis_2, (), ['quux\n'])
1913
 
        basis.calls = []
1914
 
        # ask for in non-topological order
1915
 
        records = list(test.get_record_stream(
1916
 
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
1917
 
        self.assertEqual(4, len(records))
1918
 
        results = []
1919
 
        for record in records:
1920
 
            self.assertSubset([record.key],
1921
 
                (key_basis, key_missing, key_basis_2, key))
1922
 
            if record.key == key_missing:
1923
 
                self.assertIsInstance(record, AbsentContentFactory)
1924
 
            else:
1925
 
                results.append((record.key, record.sha1, record.storage_kind,
1926
 
                    record.get_bytes_as('fulltext')))
1927
 
        calls = list(basis.calls)
1928
 
        order = [record[0] for record in results]
1929
 
        self.assertEqual([key_basis_2, key_basis, key], order)
1930
 
        for result in results:
1931
 
            if result[0] == key:
1932
 
                source = test
1933
 
            else:
1934
 
                source = basis
1935
 
            record = source.get_record_stream([result[0]], 'unordered',
1936
 
                True).next()
1937
 
            self.assertEqual(record.key, result[0])
1938
 
            self.assertEqual(record.sha1, result[1])
1939
 
            self.assertEqual(record.storage_kind, result[2])
1940
 
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
1941
 
        # It's not strictly minimal, but it seems reasonable for now for it to
1942
 
        # ask which fallbacks have which parents.
1943
 
        self.assertEqual([
1944
 
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
1945
 
            # unordered is asked for by the underlying worker as it still
1946
 
            # buffers everything while answering - which is a problem!
1947
 
            ("get_record_stream", [key_basis_2, key_basis], 'unordered', True)],
1948
 
            calls)
1949
 
 
1950
 
    def test_get_record_stream_unordered_deltas(self):
1951
 
        # records from the test knit are answered without asking the basis:
1952
 
        basis, test = self.get_basis_and_test_knit()
1953
 
        key = ('foo',)
1954
 
        key_basis = ('bar',)
1955
 
        key_missing = ('missing',)
1956
 
        test.add_lines(key, (), ['foo\n'])
1957
 
        records = list(test.get_record_stream([key], 'unordered', False))
1958
 
        self.assertEqual(1, len(records))
1959
 
        self.assertEqual([], basis.calls)
1960
 
        # Missing (from test knit) objects are retrieved from the basis:
1961
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1962
 
        basis.calls = []
1963
 
        records = list(test.get_record_stream([key_basis, key_missing],
1964
 
            'unordered', False))
1965
 
        self.assertEqual(2, len(records))
1966
 
        calls = list(basis.calls)
1967
 
        for record in records:
1968
 
            self.assertSubset([record.key], (key_basis, key_missing))
1969
 
            if record.key == key_missing:
1970
 
                self.assertIsInstance(record, AbsentContentFactory)
1971
 
            else:
1972
 
                reference = list(basis.get_record_stream([key_basis],
1973
 
                    'unordered', False))[0]
1974
 
                self.assertEqual(reference.key, record.key)
1975
 
                self.assertEqual(reference.sha1, record.sha1)
1976
 
                self.assertEqual(reference.storage_kind, record.storage_kind)
1977
 
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
1978
 
                    record.get_bytes_as(record.storage_kind))
1979
 
        # It's not strictly minimal, but it seems reasonable for now for it to
1980
 
        # ask which fallbacks have which parents.
1981
 
        self.assertEqual([
1982
 
            ("get_parent_map", set([key_basis, key_missing])),
1983
 
            ("get_record_stream", [key_basis], 'unordered', False)],
1984
 
            calls)
1985
 
 
1986
 
    def test_get_record_stream_ordered_deltas(self):
1987
 
        # ordering is preserved down into the fallback store.
1988
 
        basis, test = self.get_basis_and_test_knit()
1989
 
        key = ('foo',)
1990
 
        key_basis = ('bar',)
1991
 
        key_basis_2 = ('quux',)
1992
 
        key_missing = ('missing',)
1993
 
        test.add_lines(key, (key_basis,), ['foo\n'])
1994
 
        # Missing (from test knit) objects are retrieved from the basis:
1995
 
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
1996
 
        basis.add_lines(key_basis_2, (), ['quux\n'])
1997
 
        basis.calls = []
1998
 
        # ask for in non-topological order
1999
 
        records = list(test.get_record_stream(
2000
 
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
2001
 
        self.assertEqual(4, len(records))
2002
 
        results = []
2003
 
        for record in records:
2004
 
            self.assertSubset([record.key],
2005
 
                (key_basis, key_missing, key_basis_2, key))
2006
 
            if record.key == key_missing:
2007
 
                self.assertIsInstance(record, AbsentContentFactory)
2008
 
            else:
2009
 
                results.append((record.key, record.sha1, record.storage_kind,
2010
 
                    record.get_bytes_as(record.storage_kind)))
2011
 
        calls = list(basis.calls)
2012
 
        order = [record[0] for record in results]
2013
 
        self.assertEqual([key_basis_2, key_basis, key], order)
2014
 
        for result in results:
2015
 
            if result[0] == key:
2016
 
                source = test
2017
 
            else:
2018
 
                source = basis
2019
 
            record = source.get_record_stream([result[0]], 'unordered',
2020
 
                False).next()
2021
 
            self.assertEqual(record.key, result[0])
2022
 
            self.assertEqual(record.sha1, result[1])
2023
 
            self.assertEqual(record.storage_kind, result[2])
2024
 
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
2025
 
        # It's not strictly minimal, but it seems reasonable for now for it to
2026
 
        # ask which fallbacks have which parents.
2027
 
        self.assertEqual([
2028
 
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
2029
 
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
2030
 
            calls)
2031
 
 
2032
 
    def test_get_sha1s(self):
2033
 
        # sha1's in the test knit are answered without asking the basis
2034
 
        basis, test = self.get_basis_and_test_knit()
2035
 
        key = ('foo',)
2036
 
        key_basis = ('bar',)
2037
 
        key_missing = ('missing',)
2038
 
        test.add_lines(key, (), ['foo\n'])
2039
 
        key_sha1sum = osutils.sha('foo\n').hexdigest()
2040
 
        sha1s = test.get_sha1s([key])
2041
 
        self.assertEqual({key: key_sha1sum}, sha1s)
2042
 
        self.assertEqual([], basis.calls)
2043
 
        # But texts that are not in the test knit are looked for in the basis
2044
 
        # directly (rather than via text reconstruction) so that remote servers
2045
 
        # etc don't have to answer with full content.
2046
 
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
2047
 
        basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
2048
 
        basis.calls = []
2049
 
        sha1s = test.get_sha1s([key, key_missing, key_basis])
2050
 
        self.assertEqual({key: key_sha1sum,
2051
 
            key_basis: basis_sha1sum}, sha1s)
2052
 
        self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
2053
 
            basis.calls)
2054
 
 
2055
 
    def test_insert_record_stream(self):
2056
 
        # records are inserted as normal; insert_record_stream builds on
2057
 
        # add_lines, so a smoke test should be all that's needed:
2058
 
        key = ('foo',)
2059
 
        key_basis = ('bar',)
2060
 
        key_delta = ('zaphod',)
2061
 
        basis, test = self.get_basis_and_test_knit()
2062
 
        source = self.make_test_knit(name='source')
2063
 
        basis.add_lines(key_basis, (), ['foo\n'])
2064
 
        basis.calls = []
2065
 
        source.add_lines(key_basis, (), ['foo\n'])
2066
 
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
2067
 
        stream = source.get_record_stream([key_delta], 'unordered', False)
2068
 
        test.insert_record_stream(stream)
2069
 
        # XXX: this does somewhat too many calls in making sure of whether it
2070
 
        # has to recreate the full text.
2071
 
        self.assertEqual([("get_parent_map", set([key_basis])),
2072
 
             ('get_parent_map', set([key_basis])),
2073
 
             ('get_record_stream', [key_basis], 'unordered', True)],
2074
 
            basis.calls)
2075
 
        self.assertEqual({key_delta:(key_basis,)},
2076
 
            test.get_parent_map([key_delta]))
2077
 
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
2078
 
            'unordered', True).next().get_bytes_as('fulltext'))
2079
 
 
2080
 
    def test_iter_lines_added_or_present_in_keys(self):
2081
 
        # Lines from the basis are returned, and lines for a given key are only
2082
 
        # returned once. 
2083
 
        key1 = ('foo1',)
2084
 
        key2 = ('foo2',)
2085
 
        # all sources are asked for keys:
2086
 
        basis, test = self.get_basis_and_test_knit()
2087
 
        basis.add_lines(key1, (), ["foo"])
2088
 
        basis.calls = []
2089
 
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
2090
 
        self.assertEqual([("foo\n", key1)], lines)
2091
 
        self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
2092
 
            basis.calls)
2093
 
        # keys in both are not duplicated:
2094
 
        test.add_lines(key2, (), ["bar\n"])
2095
 
        basis.add_lines(key2, (), ["bar\n"])
2096
 
        basis.calls = []
2097
 
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
2098
 
        self.assertEqual([("bar\n", key2)], lines)
2099
 
        self.assertEqual([], basis.calls)
2100
 
 
2101
 
    def test_keys(self):
2102
 
        key1 = ('foo1',)
2103
 
        key2 = ('foo2',)
2104
 
        # all sources are asked for keys:
2105
 
        basis, test = self.get_basis_and_test_knit()
2106
 
        keys = test.keys()
2107
 
        self.assertEqual(set(), set(keys))
2108
 
        self.assertEqual([("keys",)], basis.calls)
2109
 
        # keys from a basis are returned:
2110
 
        basis.add_lines(key1, (), [])
2111
 
        basis.calls = []
2112
 
        keys = test.keys()
2113
 
        self.assertEqual(set([key1]), set(keys))
2114
 
        self.assertEqual([("keys",)], basis.calls)
2115
 
        # keys in both are not duplicated:
2116
 
        test.add_lines(key2, (), [])
2117
 
        basis.add_lines(key2, (), [])
2118
 
        basis.calls = []
2119
 
        keys = test.keys()
2120
 
        self.assertEqual(2, len(keys))
2121
 
        self.assertEqual(set([key1, key2]), set(keys))
2122
 
        self.assertEqual([("keys",)], basis.calls)
2123
 
 
2124
 
    def test_add_mpdiffs(self):
2125
 
        # records are inserted as normal; add_mpdiff builds on
2126
 
        # add_lines, so a smoke test should be all that's needed:
2127
 
        key = ('foo',)
2128
 
        key_basis = ('bar',)
2129
 
        key_delta = ('zaphod',)
2130
 
        basis, test = self.get_basis_and_test_knit()
2131
 
        source = self.make_test_knit(name='source')
2132
 
        basis.add_lines(key_basis, (), ['foo\n'])
2133
 
        basis.calls = []
2134
 
        source.add_lines(key_basis, (), ['foo\n'])
2135
 
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
2136
 
        diffs = source.make_mpdiffs([key_delta])
2137
 
        test.add_mpdiffs([(key_delta, (key_basis,),
2138
 
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
2139
 
        self.assertEqual([("get_parent_map", set([key_basis])),
2140
 
            ('get_record_stream', [key_basis], 'unordered', True),],
2141
 
            basis.calls)
2142
 
        self.assertEqual({key_delta:(key_basis,)},
2143
 
            test.get_parent_map([key_delta]))
2144
 
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
2145
 
            'unordered', True).next().get_bytes_as('fulltext'))
2146
 
 
2147
 
    def test_make_mpdiffs(self):
2148
 
        # Generating an mpdiff across a stacking boundary should detect parent
2149
 
        # texts regions.
2150
 
        key = ('foo',)
2151
 
        key_left = ('bar',)
2152
 
        key_right = ('zaphod',)
2153
 
        basis, test = self.get_basis_and_test_knit()
2154
 
        basis.add_lines(key_left, (), ['bar\n'])
2155
 
        basis.add_lines(key_right, (), ['zaphod\n'])
2156
 
        basis.calls = []
2157
 
        test.add_lines(key, (key_left, key_right),
2158
 
            ['bar\n', 'foo\n', 'zaphod\n'])
2159
 
        diffs = test.make_mpdiffs([key])
2160
 
        self.assertEqual([
2161
 
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
2162
 
                multiparent.NewText(['foo\n']),
2163
 
                multiparent.ParentText(1, 0, 2, 1)])],
2164
 
            diffs)
2165
 
        self.assertEqual(3, len(basis.calls))
2166
 
        self.assertEqual([
2167
 
            ("get_parent_map", set([key_left, key_right])),
2168
 
            ("get_parent_map", set([key_left, key_right])),
2169
 
            ],
2170
 
            basis.calls[:-1])
2171
 
        last_call = basis.calls[-1]
2172
 
        self.assertEqual('get_record_stream', last_call[0])
2173
 
        self.assertEqual(set([key_left, key_right]), set(last_call[1]))
2174
 
        self.assertEqual('unordered', last_call[2])
2175
 
        self.assertEqual(True, last_call[3])