~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Vincent Ladeuil
  • Date: 2009-04-10 18:56:00 UTC
  • mto: (4304.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 4305.
  • Revision ID: v.ladeuil+lp@free.fr-20090410185600-4aje05xaycmofem8
Make built-in plugins display the same version than bzrlib.

* bzrlib/plugins/netrc_credential_store/__init__.py: 
Import version_info.

* bzrlib/plugins/launchpad/__init__.py: 
Import version_info.

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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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
    _VFContentMapGenerator,
 
46
    _DirectPackAccess,
 
47
    _KndxIndex,
 
48
    _KnitGraphIndex,
 
49
    _KnitKeyAccess,
 
50
    make_file_factory,
 
51
    )
 
52
from bzrlib.repofmt import pack_repo
 
53
from bzrlib.tests import (
 
54
    Feature,
 
55
    KnownFailure,
 
56
    TestCase,
 
57
    TestCaseWithMemoryTransport,
 
58
    TestCaseWithTransport,
 
59
    TestNotApplicable,
 
60
    )
 
61
from bzrlib.transport import get_transport
 
62
from bzrlib.transport.memory import MemoryTransport
 
63
from bzrlib.tuned_gzip import GzipFile
 
64
from bzrlib.versionedfile import (
 
65
    AbsentContentFactory,
 
66
    ConstantMapper,
 
67
    network_bytes_to_kind_and_offset,
 
68
    RecordingVersionedFilesDecorator,
 
69
    )
 
70
 
 
71
 
 
72
class _CompiledKnitFeature(Feature):
 
73
 
 
74
    def _probe(self):
 
75
        try:
 
76
            import bzrlib._knit_load_data_c
 
77
        except ImportError:
 
78
            return False
 
79
        return True
 
80
 
 
81
    def feature_name(self):
 
82
        return 'bzrlib._knit_load_data_c'
 
83
 
 
84
CompiledKnitFeature = _CompiledKnitFeature()
 
85
 
 
86
 
 
87
class KnitContentTestsMixin(object):
 
88
 
 
89
    def test_constructor(self):
 
90
        content = self._make_content([])
 
91
 
 
92
    def test_text(self):
 
93
        content = self._make_content([])
 
94
        self.assertEqual(content.text(), [])
 
95
 
 
96
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
97
        self.assertEqual(content.text(), ["text1", "text2"])
 
98
 
 
99
    def test_copy(self):
 
100
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
101
        copy = content.copy()
 
102
        self.assertIsInstance(copy, content.__class__)
 
103
        self.assertEqual(copy.annotate(), content.annotate())
 
104
 
 
105
    def assertDerivedBlocksEqual(self, source, target, noeol=False):
 
106
        """Assert that the derived matching blocks match real output"""
 
107
        source_lines = source.splitlines(True)
 
108
        target_lines = target.splitlines(True)
 
109
        def nl(line):
 
110
            if noeol and not line.endswith('\n'):
 
111
                return line + '\n'
 
112
            else:
 
113
                return line
 
114
        source_content = self._make_content([(None, nl(l)) for l in source_lines])
 
115
        target_content = self._make_content([(None, nl(l)) for l in target_lines])
 
116
        line_delta = source_content.line_delta(target_content)
 
117
        delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
 
118
            source_lines, target_lines))
 
119
        matcher = KnitSequenceMatcher(None, source_lines, target_lines)
 
120
        matcher_blocks = list(list(matcher.get_matching_blocks()))
 
121
        self.assertEqual(matcher_blocks, delta_blocks)
 
122
 
 
123
    def test_get_line_delta_blocks(self):
 
124
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
 
125
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
 
126
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
 
127
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
 
128
        self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
 
129
        self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
 
130
        self.assertDerivedBlocksEqual(TEXT_1A, '')
 
131
        self.assertDerivedBlocksEqual('', TEXT_1A)
 
132
        self.assertDerivedBlocksEqual('', '')
 
133
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
 
134
 
 
135
    def test_get_line_delta_blocks_noeol(self):
 
136
        """Handle historical knit deltas safely
 
137
 
 
138
        Some existing knit deltas don't consider the last line to differ
 
139
        when the only difference whether it has a final newline.
 
140
 
 
141
        New knit deltas appear to always consider the last line to differ
 
142
        in this case.
 
143
        """
 
144
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
 
145
        self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
 
146
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
 
147
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
 
148
 
 
149
 
 
150
TEXT_1 = """\
 
151
Banana cup cakes:
 
152
 
 
153
- bananas
 
154
- eggs
 
155
- broken tea cups
 
156
"""
 
157
 
 
158
TEXT_1A = """\
 
159
Banana cup cake recipe
 
160
(serves 6)
 
161
 
 
162
- bananas
 
163
- eggs
 
164
- broken tea cups
 
165
- self-raising flour
 
166
"""
 
167
 
 
168
TEXT_1B = """\
 
169
Banana cup cake recipe
 
170
 
 
171
- bananas (do not use plantains!!!)
 
172
- broken tea cups
 
173
- flour
 
174
"""
 
175
 
 
176
delta_1_1a = """\
 
177
0,1,2
 
178
Banana cup cake recipe
 
179
(serves 6)
 
180
5,5,1
 
181
- self-raising flour
 
182
"""
 
183
 
 
184
TEXT_2 = """\
 
185
Boeuf bourguignon
 
186
 
 
187
- beef
 
188
- red wine
 
189
- small onions
 
190
- carrot
 
191
- mushrooms
 
192
"""
 
193
 
 
194
 
 
195
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
 
196
 
 
197
    def _make_content(self, lines):
 
198
        annotated_content = AnnotatedKnitContent(lines)
 
199
        return PlainKnitContent(annotated_content.text(), 'bogus')
 
200
 
 
201
    def test_annotate(self):
 
202
        content = self._make_content([])
 
203
        self.assertEqual(content.annotate(), [])
 
204
 
 
205
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
206
        self.assertEqual(content.annotate(),
 
207
            [("bogus", "text1"), ("bogus", "text2")])
 
208
 
 
209
    def test_line_delta(self):
 
210
        content1 = self._make_content([("", "a"), ("", "b")])
 
211
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
212
        self.assertEqual(content1.line_delta(content2),
 
213
            [(1, 2, 2, ["a", "c"])])
 
214
 
 
215
    def test_line_delta_iter(self):
 
216
        content1 = self._make_content([("", "a"), ("", "b")])
 
217
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
218
        it = content1.line_delta_iter(content2)
 
219
        self.assertEqual(it.next(), (1, 2, 2, ["a", "c"]))
 
220
        self.assertRaises(StopIteration, it.next)
 
221
 
 
222
 
 
223
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
 
224
 
 
225
    def _make_content(self, lines):
 
226
        return AnnotatedKnitContent(lines)
 
227
 
 
228
    def test_annotate(self):
 
229
        content = self._make_content([])
 
230
        self.assertEqual(content.annotate(), [])
 
231
 
 
232
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
233
        self.assertEqual(content.annotate(),
 
234
            [("origin1", "text1"), ("origin2", "text2")])
 
235
 
 
236
    def test_line_delta(self):
 
237
        content1 = self._make_content([("", "a"), ("", "b")])
 
238
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
239
        self.assertEqual(content1.line_delta(content2),
 
240
            [(1, 2, 2, [("", "a"), ("", "c")])])
 
241
 
 
242
    def test_line_delta_iter(self):
 
243
        content1 = self._make_content([("", "a"), ("", "b")])
 
244
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
245
        it = content1.line_delta_iter(content2)
 
246
        self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
 
247
        self.assertRaises(StopIteration, it.next)
 
248
 
 
249
 
 
250
class MockTransport(object):
 
251
 
 
252
    def __init__(self, file_lines=None):
 
253
        self.file_lines = file_lines
 
254
        self.calls = []
 
255
        # We have no base directory for the MockTransport
 
256
        self.base = ''
 
257
 
 
258
    def get(self, filename):
 
259
        if self.file_lines is None:
 
260
            raise NoSuchFile(filename)
 
261
        else:
 
262
            return StringIO("\n".join(self.file_lines))
 
263
 
 
264
    def readv(self, relpath, offsets):
 
265
        fp = self.get(relpath)
 
266
        for offset, size in offsets:
 
267
            fp.seek(offset)
 
268
            yield offset, fp.read(size)
 
269
 
 
270
    def __getattr__(self, name):
 
271
        def queue_call(*args, **kwargs):
 
272
            self.calls.append((name, args, kwargs))
 
273
        return queue_call
 
274
 
 
275
 
 
276
class MockReadvFailingTransport(MockTransport):
 
277
    """Fail in the middle of a readv() result.
 
278
 
 
279
    This Transport will successfully yield the first two requested hunks, but
 
280
    raise NoSuchFile for the rest.
 
281
    """
 
282
 
 
283
    def readv(self, relpath, offsets):
 
284
        count = 0
 
285
        for result in MockTransport.readv(self, relpath, offsets):
 
286
            count += 1
 
287
            # we use 2 because the first offset is the pack header, the second
 
288
            # is the first actual content requset
 
289
            if count > 2:
 
290
                raise errors.NoSuchFile(relpath)
 
291
            yield result
 
292
 
 
293
 
 
294
class KnitRecordAccessTestsMixin(object):
 
295
    """Tests for getting and putting knit records."""
 
296
 
 
297
    def test_add_raw_records(self):
 
298
        """Add_raw_records adds records retrievable later."""
 
299
        access = self.get_access()
 
300
        memos = access.add_raw_records([('key', 10)], '1234567890')
 
301
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
 
302
 
 
303
    def test_add_several_raw_records(self):
 
304
        """add_raw_records with many records and read some back."""
 
305
        access = self.get_access()
 
306
        memos = access.add_raw_records([('key', 10), ('key2', 2), ('key3', 5)],
 
307
            '12345678901234567')
 
308
        self.assertEqual(['1234567890', '12', '34567'],
 
309
            list(access.get_raw_records(memos)))
 
310
        self.assertEqual(['1234567890'],
 
311
            list(access.get_raw_records(memos[0:1])))
 
312
        self.assertEqual(['12'],
 
313
            list(access.get_raw_records(memos[1:2])))
 
314
        self.assertEqual(['34567'],
 
315
            list(access.get_raw_records(memos[2:3])))
 
316
        self.assertEqual(['1234567890', '34567'],
 
317
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
318
 
 
319
 
 
320
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
321
    """Tests for the .kndx implementation."""
 
322
 
 
323
    def get_access(self):
 
324
        """Get a .knit style access instance."""
 
325
        mapper = ConstantMapper("foo")
 
326
        access = _KnitKeyAccess(self.get_transport(), mapper)
 
327
        return access
 
328
 
 
329
 
 
330
class _TestException(Exception):
 
331
    """Just an exception for local tests to use."""
 
332
 
 
333
 
 
334
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
335
    """Tests for the pack based access."""
 
336
 
 
337
    def get_access(self):
 
338
        return self._get_access()[0]
 
339
 
 
340
    def _get_access(self, packname='packfile', index='FOO'):
 
341
        transport = self.get_transport()
 
342
        def write_data(bytes):
 
343
            transport.append_bytes(packname, bytes)
 
344
        writer = pack.ContainerWriter(write_data)
 
345
        writer.begin()
 
346
        access = _DirectPackAccess({})
 
347
        access.set_writer(writer, index, (transport, packname))
 
348
        return access, writer
 
349
 
 
350
    def make_pack_file(self):
 
351
        """Create a pack file with 2 records."""
 
352
        access, writer = self._get_access(packname='packname', index='foo')
 
353
        memos = []
 
354
        memos.extend(access.add_raw_records([('key1', 10)], '1234567890'))
 
355
        memos.extend(access.add_raw_records([('key2', 5)], '12345'))
 
356
        writer.end()
 
357
        return memos
 
358
 
 
359
    def make_vf_for_retrying(self):
 
360
        """Create 3 packs and a reload function.
 
361
 
 
362
        Originally, 2 pack files will have the data, but one will be missing.
 
363
        And then the third will be used in place of the first two if reload()
 
364
        is called.
 
365
 
 
366
        :return: (versioned_file, reload_counter)
 
367
            versioned_file  a KnitVersionedFiles using the packs for access
 
368
        """
 
369
        tree = self.make_branch_and_memory_tree('tree')
 
370
        tree.lock_write()
 
371
        self.addCleanup(tree.branch.repository.unlock)
 
372
        tree.add([''], ['root-id'])
 
373
        tree.commit('one', rev_id='rev-1')
 
374
        tree.commit('two', rev_id='rev-2')
 
375
        tree.commit('three', rev_id='rev-3')
 
376
        # Pack these three revisions into another pack file, but don't remove
 
377
        # the originals
 
378
        repo = tree.branch.repository
 
379
        collection = repo._pack_collection
 
380
        collection.ensure_loaded()
 
381
        orig_packs = collection.packs
 
382
        packer = pack_repo.Packer(collection, orig_packs, '.testpack')
 
383
        new_pack = packer.pack()
 
384
        # forget about the new pack
 
385
        collection.reset()
 
386
        repo.refresh_data()
 
387
        vf = tree.branch.repository.revisions
 
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 assertTotalBuildSize(self, size, keys, positions):
 
1095
        self.assertEqual(size,
 
1096
                         knit._get_total_build_size(None, keys, positions))
 
1097
 
 
1098
    def test__get_total_build_size(self):
 
1099
        positions = {
 
1100
            ('a',): (('fulltext', False), (('a',), 0, 100), None),
 
1101
            ('b',): (('line-delta', False), (('b',), 100, 21), ('a',)),
 
1102
            ('c',): (('line-delta', False), (('c',), 121, 35), ('b',)),
 
1103
            ('d',): (('line-delta', False), (('d',), 156, 12), ('b',)),
 
1104
            }
 
1105
        self.assertTotalBuildSize(100, [('a',)], positions)
 
1106
        self.assertTotalBuildSize(121, [('b',)], positions)
 
1107
        # c needs both a & b
 
1108
        self.assertTotalBuildSize(156, [('c',)], positions)
 
1109
        # we shouldn't count 'b' twice
 
1110
        self.assertTotalBuildSize(156, [('b',), ('c',)], positions)
 
1111
        self.assertTotalBuildSize(133, [('d',)], positions)
 
1112
        self.assertTotalBuildSize(168, [('c',), ('d',)], positions)
 
1113
 
 
1114
    def test_get_position(self):
 
1115
        transport = MockTransport([
 
1116
            _KndxIndex.HEADER,
 
1117
            "a option 0 1 :",
 
1118
            "b option 1 2 :"
 
1119
            ])
 
1120
        index = self.get_knit_index(transport, "filename", "r")
 
1121
 
 
1122
        self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
 
1123
        self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
 
1124
 
 
1125
    def test_get_method(self):
 
1126
        transport = MockTransport([
 
1127
            _KndxIndex.HEADER,
 
1128
            "a fulltext,unknown 0 1 :",
 
1129
            "b unknown,line-delta 1 2 :",
 
1130
            "c bad 3 4 :"
 
1131
            ])
 
1132
        index = self.get_knit_index(transport, "filename", "r")
 
1133
 
 
1134
        self.assertEqual("fulltext", index.get_method("a"))
 
1135
        self.assertEqual("line-delta", index.get_method("b"))
 
1136
        self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
 
1137
 
 
1138
    def test_get_options(self):
 
1139
        transport = MockTransport([
 
1140
            _KndxIndex.HEADER,
 
1141
            "a opt1 0 1 :",
 
1142
            "b opt2,opt3 1 2 :"
 
1143
            ])
 
1144
        index = self.get_knit_index(transport, "filename", "r")
 
1145
 
 
1146
        self.assertEqual(["opt1"], index.get_options("a"))
 
1147
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
 
1148
 
 
1149
    def test_get_parent_map(self):
 
1150
        transport = MockTransport([
 
1151
            _KndxIndex.HEADER,
 
1152
            "a option 0 1 :",
 
1153
            "b option 1 2 0 .c :",
 
1154
            "c option 1 2 1 0 .e :"
 
1155
            ])
 
1156
        index = self.get_knit_index(transport, "filename", "r")
 
1157
 
 
1158
        self.assertEqual({
 
1159
            ("a",):(),
 
1160
            ("b",):(("a",), ("c",)),
 
1161
            ("c",):(("b",), ("a",), ("e",)),
 
1162
            }, index.get_parent_map(index.keys()))
 
1163
 
 
1164
    def test_impossible_parent(self):
 
1165
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
 
1166
        transport = MockTransport([
 
1167
            _KndxIndex.HEADER,
 
1168
            "a option 0 1 :",
 
1169
            "b option 0 1 4 :"  # We don't have a 4th record
 
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.IndexError')
 
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(self):
 
1185
        transport = MockTransport([
 
1186
            _KndxIndex.HEADER,
 
1187
            "a option 0 1 :",
 
1188
            "b option 0 1 :",
 
1189
            "c option 0 1 1v :", # Can't have a parent of '1v'
 
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_corrupted_parent_in_list(self):
 
1205
        transport = MockTransport([
 
1206
            _KndxIndex.HEADER,
 
1207
            "a option 0 1 :",
 
1208
            "b option 0 1 :",
 
1209
            "c option 0 1 1 v :", # Can't have a parent of 'v'
 
1210
            ])
 
1211
        index = self.get_knit_index(transport, 'filename', 'r')
 
1212
        try:
 
1213
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1214
        except TypeError, e:
 
1215
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1216
                           ' not exceptions.ValueError')
 
1217
                and sys.version_info[0:2] >= (2,5)):
 
1218
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1219
                                  ' raising new style exceptions with python'
 
1220
                                  ' >=2.5')
 
1221
            else:
 
1222
                raise
 
1223
 
 
1224
    def test_invalid_position(self):
 
1225
        transport = MockTransport([
 
1226
            _KndxIndex.HEADER,
 
1227
            "a option 1v 1 :",
 
1228
            ])
 
1229
        index = self.get_knit_index(transport, 'filename', 'r')
 
1230
        try:
 
1231
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1232
        except TypeError, e:
 
1233
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1234
                           ' not exceptions.ValueError')
 
1235
                and sys.version_info[0:2] >= (2,5)):
 
1236
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1237
                                  ' raising new style exceptions with python'
 
1238
                                  ' >=2.5')
 
1239
            else:
 
1240
                raise
 
1241
 
 
1242
    def test_invalid_size(self):
 
1243
        transport = MockTransport([
 
1244
            _KndxIndex.HEADER,
 
1245
            "a option 1 1v :",
 
1246
            ])
 
1247
        index = self.get_knit_index(transport, 'filename', 'r')
 
1248
        try:
 
1249
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1250
        except TypeError, e:
 
1251
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1252
                           ' not exceptions.ValueError')
 
1253
                and sys.version_info[0:2] >= (2,5)):
 
1254
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1255
                                  ' raising new style exceptions with python'
 
1256
                                  ' >=2.5')
 
1257
            else:
 
1258
                raise
 
1259
 
 
1260
    def test_scan_unvalidated_index_not_implemented(self):
 
1261
        transport = MockTransport()
 
1262
        index = self.get_knit_index(transport, 'filename', 'r')
 
1263
        self.assertRaises(
 
1264
            NotImplementedError, index.scan_unvalidated_index,
 
1265
            'dummy graph_index')
 
1266
        self.assertRaises(
 
1267
            NotImplementedError, index.get_missing_compression_parents)
 
1268
 
 
1269
    def test_short_line(self):
 
1270
        transport = MockTransport([
 
1271
            _KndxIndex.HEADER,
 
1272
            "a option 0 10  :",
 
1273
            "b option 10 10 0", # This line isn't terminated, ignored
 
1274
            ])
 
1275
        index = self.get_knit_index(transport, "filename", "r")
 
1276
        self.assertEqual(set([('a',)]), index.keys())
 
1277
 
 
1278
    def test_skip_incomplete_record(self):
 
1279
        # A line with bogus data should just be skipped
 
1280
        transport = MockTransport([
 
1281
            _KndxIndex.HEADER,
 
1282
            "a option 0 10  :",
 
1283
            "b option 10 10 0", # This line isn't terminated, ignored
 
1284
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1285
            ])
 
1286
        index = self.get_knit_index(transport, "filename", "r")
 
1287
        self.assertEqual(set([('a',), ('c',)]), index.keys())
 
1288
 
 
1289
    def test_trailing_characters(self):
 
1290
        # A line with bogus data should just be skipped
 
1291
        transport = MockTransport([
 
1292
            _KndxIndex.HEADER,
 
1293
            "a option 0 10  :",
 
1294
            "b option 10 10 0 :a", # This line has extra trailing characters
 
1295
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1296
            ])
 
1297
        index = self.get_knit_index(transport, "filename", "r")
 
1298
        self.assertEqual(set([('a',), ('c',)]), index.keys())
 
1299
 
 
1300
 
 
1301
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
 
1302
 
 
1303
    _test_needs_features = [CompiledKnitFeature]
 
1304
 
 
1305
    def get_knit_index(self, transport, name, mode):
 
1306
        mapper = ConstantMapper(name)
 
1307
        orig = knit._load_data
 
1308
        def reset():
 
1309
            knit._load_data = orig
 
1310
        self.addCleanup(reset)
 
1311
        from bzrlib._knit_load_data_c import _load_data_c
 
1312
        knit._load_data = _load_data_c
 
1313
        allow_writes = lambda: mode == 'w'
 
1314
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
 
1315
 
 
1316
 
 
1317
class KnitTests(TestCaseWithTransport):
 
1318
    """Class containing knit test helper routines."""
 
1319
 
 
1320
    def make_test_knit(self, annotate=False, name='test'):
 
1321
        mapper = ConstantMapper(name)
 
1322
        return make_file_factory(annotate, mapper)(self.get_transport())
 
1323
 
 
1324
 
 
1325
class TestBadShaError(KnitTests):
 
1326
    """Tests for handling of sha errors."""
 
1327
 
 
1328
    def test_sha_exception_has_text(self):
 
1329
        # having the failed text included in the error allows for recovery.
 
1330
        source = self.make_test_knit()
 
1331
        target = self.make_test_knit(name="target")
 
1332
        if not source._max_delta_chain:
 
1333
            raise TestNotApplicable(
 
1334
                "cannot get delta-caused sha failures without deltas.")
 
1335
        # create a basis
 
1336
        basis = ('basis',)
 
1337
        broken = ('broken',)
 
1338
        source.add_lines(basis, (), ['foo\n'])
 
1339
        source.add_lines(broken, (basis,), ['foo\n', 'bar\n'])
 
1340
        # Seed target with a bad basis text
 
1341
        target.add_lines(basis, (), ['gam\n'])
 
1342
        target.insert_record_stream(
 
1343
            source.get_record_stream([broken], 'unordered', False))
 
1344
        err = self.assertRaises(errors.KnitCorrupt,
 
1345
            target.get_record_stream([broken], 'unordered', True
 
1346
            ).next().get_bytes_as, 'chunked')
 
1347
        self.assertEqual(['gam\n', 'bar\n'], err.content)
 
1348
        # Test for formatting with live data
 
1349
        self.assertStartsWith(str(err), "Knit ")
 
1350
 
 
1351
 
 
1352
class TestKnitIndex(KnitTests):
 
1353
 
 
1354
    def test_add_versions_dictionary_compresses(self):
 
1355
        """Adding versions to the index should update the lookup dict"""
 
1356
        knit = self.make_test_knit()
 
1357
        idx = knit._index
 
1358
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
 
1359
        self.check_file_contents('test.kndx',
 
1360
            '# bzr knit index 8\n'
 
1361
            '\n'
 
1362
            'a-1 fulltext 0 0  :'
 
1363
            )
 
1364
        idx.add_records([
 
1365
            (('a-2',), ['fulltext'], (('a-2',), 0, 0), [('a-1',)]),
 
1366
            (('a-3',), ['fulltext'], (('a-3',), 0, 0), [('a-2',)]),
 
1367
            ])
 
1368
        self.check_file_contents('test.kndx',
 
1369
            '# bzr knit index 8\n'
 
1370
            '\n'
 
1371
            'a-1 fulltext 0 0  :\n'
 
1372
            'a-2 fulltext 0 0 0 :\n'
 
1373
            'a-3 fulltext 0 0 1 :'
 
1374
            )
 
1375
        self.assertEqual(set([('a-3',), ('a-1',), ('a-2',)]), idx.keys())
 
1376
        self.assertEqual({
 
1377
            ('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False)),
 
1378
            ('a-2',): ((('a-2',), 0, 0), None, (('a-1',),), ('fulltext', False)),
 
1379
            ('a-3',): ((('a-3',), 0, 0), None, (('a-2',),), ('fulltext', False)),
 
1380
            }, idx.get_build_details(idx.keys()))
 
1381
        self.assertEqual({('a-1',):(),
 
1382
            ('a-2',):(('a-1',),),
 
1383
            ('a-3',):(('a-2',),),},
 
1384
            idx.get_parent_map(idx.keys()))
 
1385
 
 
1386
    def test_add_versions_fails_clean(self):
 
1387
        """If add_versions fails in the middle, it restores a pristine state.
 
1388
 
 
1389
        Any modifications that are made to the index are reset if all versions
 
1390
        cannot be added.
 
1391
        """
 
1392
        # This cheats a little bit by passing in a generator which will
 
1393
        # raise an exception before the processing finishes
 
1394
        # Other possibilities would be to have an version with the wrong number
 
1395
        # of entries, or to make the backing transport unable to write any
 
1396
        # files.
 
1397
 
 
1398
        knit = self.make_test_knit()
 
1399
        idx = knit._index
 
1400
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
 
1401
 
 
1402
        class StopEarly(Exception):
 
1403
            pass
 
1404
 
 
1405
        def generate_failure():
 
1406
            """Add some entries and then raise an exception"""
 
1407
            yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
 
1408
            yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
 
1409
            raise StopEarly()
 
1410
 
 
1411
        # Assert the pre-condition
 
1412
        def assertA1Only():
 
1413
            self.assertEqual(set([('a-1',)]), set(idx.keys()))
 
1414
            self.assertEqual(
 
1415
                {('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False))},
 
1416
                idx.get_build_details([('a-1',)]))
 
1417
            self.assertEqual({('a-1',):()}, idx.get_parent_map(idx.keys()))
 
1418
 
 
1419
        assertA1Only()
 
1420
        self.assertRaises(StopEarly, idx.add_records, generate_failure())
 
1421
        # And it shouldn't be modified
 
1422
        assertA1Only()
 
1423
 
 
1424
    def test_knit_index_ignores_empty_files(self):
 
1425
        # There was a race condition in older bzr, where a ^C at the right time
 
1426
        # could leave an empty .kndx file, which bzr would later claim was a
 
1427
        # corrupted file since the header was not present. In reality, the file
 
1428
        # just wasn't created, so it should be ignored.
 
1429
        t = get_transport('.')
 
1430
        t.put_bytes('test.kndx', '')
 
1431
 
 
1432
        knit = self.make_test_knit()
 
1433
 
 
1434
    def test_knit_index_checks_header(self):
 
1435
        t = get_transport('.')
 
1436
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
 
1437
        k = self.make_test_knit()
 
1438
        self.assertRaises(KnitHeaderError, k.keys)
 
1439
 
 
1440
 
 
1441
class TestGraphIndexKnit(KnitTests):
 
1442
    """Tests for knits using a GraphIndex rather than a KnitIndex."""
 
1443
 
 
1444
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1445
        builder = GraphIndexBuilder(ref_lists)
 
1446
        for node, references, value in nodes:
 
1447
            builder.add_node(node, references, value)
 
1448
        stream = builder.finish()
 
1449
        trans = self.get_transport()
 
1450
        size = trans.put_file(name, stream)
 
1451
        return GraphIndex(trans, name, size)
 
1452
 
 
1453
    def two_graph_index(self, deltas=False, catch_adds=False):
 
1454
        """Build a two-graph index.
 
1455
 
 
1456
        :param deltas: If true, use underlying indices with two node-ref
 
1457
            lists and 'parent' set to a delta-compressed against tail.
 
1458
        """
 
1459
        # build a complex graph across several indices.
 
1460
        if deltas:
 
1461
            # delta compression inn the index
 
1462
            index1 = self.make_g_index('1', 2, [
 
1463
                (('tip', ), 'N0 100', ([('parent', )], [], )),
 
1464
                (('tail', ), '', ([], []))])
 
1465
            index2 = self.make_g_index('2', 2, [
 
1466
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
 
1467
                (('separate', ), '', ([], []))])
 
1468
        else:
 
1469
            # just blob location and graph in the index.
 
1470
            index1 = self.make_g_index('1', 1, [
 
1471
                (('tip', ), 'N0 100', ([('parent', )], )),
 
1472
                (('tail', ), '', ([], ))])
 
1473
            index2 = self.make_g_index('2', 1, [
 
1474
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
 
1475
                (('separate', ), '', ([], ))])
 
1476
        combined_index = CombinedGraphIndex([index1, index2])
 
1477
        if catch_adds:
 
1478
            self.combined_index = combined_index
 
1479
            self.caught_entries = []
 
1480
            add_callback = self.catch_add
 
1481
        else:
 
1482
            add_callback = None
 
1483
        return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
 
1484
            add_callback=add_callback)
 
1485
 
 
1486
    def test_keys(self):
 
1487
        index = self.two_graph_index()
 
1488
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1489
            set(index.keys()))
 
1490
 
 
1491
    def test_get_position(self):
 
1492
        index = self.two_graph_index()
 
1493
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position(('tip',)))
 
1494
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position(('parent',)))
 
1495
 
 
1496
    def test_get_method_deltas(self):
 
1497
        index = self.two_graph_index(deltas=True)
 
1498
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1499
        self.assertEqual('line-delta', index.get_method(('parent',)))
 
1500
 
 
1501
    def test_get_method_no_deltas(self):
 
1502
        # check that the parent-history lookup is ignored with deltas=False.
 
1503
        index = self.two_graph_index(deltas=False)
 
1504
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1505
        self.assertEqual('fulltext', index.get_method(('parent',)))
 
1506
 
 
1507
    def test_get_options_deltas(self):
 
1508
        index = self.two_graph_index(deltas=True)
 
1509
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1510
        self.assertEqual(['line-delta'], index.get_options(('parent',)))
 
1511
 
 
1512
    def test_get_options_no_deltas(self):
 
1513
        # check that the parent-history lookup is ignored with deltas=False.
 
1514
        index = self.two_graph_index(deltas=False)
 
1515
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1516
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1517
 
 
1518
    def test_get_parent_map(self):
 
1519
        index = self.two_graph_index()
 
1520
        self.assertEqual({('parent',):(('tail',), ('ghost',))},
 
1521
            index.get_parent_map([('parent',), ('ghost',)]))
 
1522
 
 
1523
    def catch_add(self, entries):
 
1524
        self.caught_entries.append(entries)
 
1525
 
 
1526
    def test_add_no_callback_errors(self):
 
1527
        index = self.two_graph_index()
 
1528
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1529
            [(('new',), 'fulltext,no-eol', (None, 50, 60), ['separate'])])
 
1530
 
 
1531
    def test_add_version_smoke(self):
 
1532
        index = self.two_graph_index(catch_adds=True)
 
1533
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60),
 
1534
            [('separate',)])])
 
1535
        self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
 
1536
            self.caught_entries)
 
1537
 
 
1538
    def test_add_version_delta_not_delta_index(self):
 
1539
        index = self.two_graph_index(catch_adds=True)
 
1540
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1541
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1542
        self.assertEqual([], self.caught_entries)
 
1543
 
 
1544
    def test_add_version_same_dup(self):
 
1545
        index = self.two_graph_index(catch_adds=True)
 
1546
        # options can be spelt two different ways
 
1547
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1548
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
 
1549
        # position/length are ignored (because each pack could have fulltext or
 
1550
        # delta, and be at a different position.
 
1551
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1552
            [('parent',)])])
 
1553
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1554
            [('parent',)])])
 
1555
        # but neither should have added data:
 
1556
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1557
 
 
1558
    def test_add_version_different_dup(self):
 
1559
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1560
        # change options
 
1561
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1562
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1563
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1564
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
 
1565
        # parents
 
1566
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1567
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1568
        self.assertEqual([], self.caught_entries)
 
1569
 
 
1570
    def test_add_versions_nodeltas(self):
 
1571
        index = self.two_graph_index(catch_adds=True)
 
1572
        index.add_records([
 
1573
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1574
                (('new2',), 'fulltext', (None, 0, 6), [('new',)]),
 
1575
                ])
 
1576
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
 
1577
            (('new2', ), ' 0 6', ((('new',),),))],
 
1578
            sorted(self.caught_entries[0]))
 
1579
        self.assertEqual(1, len(self.caught_entries))
 
1580
 
 
1581
    def test_add_versions_deltas(self):
 
1582
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1583
        index.add_records([
 
1584
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1585
                (('new2',), 'line-delta', (None, 0, 6), [('new',)]),
 
1586
                ])
 
1587
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
 
1588
            (('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
 
1589
            sorted(self.caught_entries[0]))
 
1590
        self.assertEqual(1, len(self.caught_entries))
 
1591
 
 
1592
    def test_add_versions_delta_not_delta_index(self):
 
1593
        index = self.two_graph_index(catch_adds=True)
 
1594
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1595
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1596
        self.assertEqual([], self.caught_entries)
 
1597
 
 
1598
    def test_add_versions_random_id_accepted(self):
 
1599
        index = self.two_graph_index(catch_adds=True)
 
1600
        index.add_records([], random_id=True)
 
1601
 
 
1602
    def test_add_versions_same_dup(self):
 
1603
        index = self.two_graph_index(catch_adds=True)
 
1604
        # options can be spelt two different ways
 
1605
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100),
 
1606
            [('parent',)])])
 
1607
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100),
 
1608
            [('parent',)])])
 
1609
        # position/length are ignored (because each pack could have fulltext or
 
1610
        # delta, and be at a different position.
 
1611
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1612
            [('parent',)])])
 
1613
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1614
            [('parent',)])])
 
1615
        # but neither should have added data.
 
1616
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1617
 
 
1618
    def test_add_versions_different_dup(self):
 
1619
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1620
        # change options
 
1621
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1622
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1623
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1624
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
 
1625
        # parents
 
1626
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1627
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1628
        # change options in the second record
 
1629
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1630
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)]),
 
1631
             (('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1632
        self.assertEqual([], self.caught_entries)
 
1633
 
 
1634
    def make_g_index_missing_compression_parent(self):
 
1635
        graph_index = self.make_g_index('missing_comp', 2,
 
1636
            [(('tip', ), ' 100 78',
 
1637
              ([('missing-parent', ), ('ghost', )], [('missing-parent', )]))])
 
1638
        return graph_index
 
1639
 
 
1640
    def make_g_index_no_external_refs(self):
 
1641
        graph_index = self.make_g_index('no_external_refs', 2,
 
1642
            [(('rev', ), ' 100 78',
 
1643
              ([('parent', ), ('ghost', )], []))])
 
1644
        return graph_index
 
1645
 
 
1646
    def test_add_good_unvalidated_index(self):
 
1647
        unvalidated = self.make_g_index_no_external_refs()
 
1648
        combined = CombinedGraphIndex([unvalidated])
 
1649
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1650
        index.scan_unvalidated_index(unvalidated)
 
1651
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1652
 
 
1653
    def test_add_incomplete_unvalidated_index(self):
 
1654
        unvalidated = self.make_g_index_missing_compression_parent()
 
1655
        combined = CombinedGraphIndex([unvalidated])
 
1656
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1657
        index.scan_unvalidated_index(unvalidated)
 
1658
        # This also checks that its only the compression parent that is
 
1659
        # examined, otherwise 'ghost' would also be reported as a missing
 
1660
        # parent.
 
1661
        self.assertEqual(
 
1662
            frozenset([('missing-parent',)]),
 
1663
            index.get_missing_compression_parents())
 
1664
 
 
1665
    def test_add_unvalidated_index_with_present_external_references(self):
 
1666
        index = self.two_graph_index(deltas=True)
 
1667
        # Ugly hack to get at one of the underlying GraphIndex objects that
 
1668
        # two_graph_index built.
 
1669
        unvalidated = index._graph_index._indices[1]
 
1670
        # 'parent' is an external ref of _indices[1] (unvalidated), but is
 
1671
        # present in _indices[0].
 
1672
        index.scan_unvalidated_index(unvalidated)
 
1673
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1674
 
 
1675
    def make_new_missing_parent_g_index(self, name):
 
1676
        missing_parent = name + '-missing-parent'
 
1677
        graph_index = self.make_g_index(name, 2,
 
1678
            [((name + 'tip', ), ' 100 78',
 
1679
              ([(missing_parent, ), ('ghost', )], [(missing_parent, )]))])
 
1680
        return graph_index
 
1681
 
 
1682
    def test_add_mulitiple_unvalidated_indices_with_missing_parents(self):
 
1683
        g_index_1 = self.make_new_missing_parent_g_index('one')
 
1684
        g_index_2 = self.make_new_missing_parent_g_index('two')
 
1685
        combined = CombinedGraphIndex([g_index_1, g_index_2])
 
1686
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1687
        index.scan_unvalidated_index(g_index_1)
 
1688
        index.scan_unvalidated_index(g_index_2)
 
1689
        self.assertEqual(
 
1690
            frozenset([('one-missing-parent',), ('two-missing-parent',)]),
 
1691
            index.get_missing_compression_parents())
 
1692
 
 
1693
    def test_add_mulitiple_unvalidated_indices_with_mutual_dependencies(self):
 
1694
        graph_index_a = self.make_g_index('one', 2,
 
1695
            [(('parent-one', ), ' 100 78', ([('non-compression-parent',)], [])),
 
1696
             (('child-of-two', ), ' 100 78',
 
1697
              ([('parent-two',)], [('parent-two',)]))])
 
1698
        graph_index_b = self.make_g_index('two', 2,
 
1699
            [(('parent-two', ), ' 100 78', ([('non-compression-parent',)], [])),
 
1700
             (('child-of-one', ), ' 100 78',
 
1701
              ([('parent-one',)], [('parent-one',)]))])
 
1702
        combined = CombinedGraphIndex([graph_index_a, graph_index_b])
 
1703
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1704
        index.scan_unvalidated_index(graph_index_a)
 
1705
        index.scan_unvalidated_index(graph_index_b)
 
1706
        self.assertEqual(
 
1707
            frozenset([]), index.get_missing_compression_parents())
 
1708
 
 
1709
 
 
1710
class TestNoParentsGraphIndexKnit(KnitTests):
 
1711
    """Tests for knits using _KnitGraphIndex with no parents."""
 
1712
 
 
1713
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1714
        builder = GraphIndexBuilder(ref_lists)
 
1715
        for node, references in nodes:
 
1716
            builder.add_node(node, references)
 
1717
        stream = builder.finish()
 
1718
        trans = self.get_transport()
 
1719
        size = trans.put_file(name, stream)
 
1720
        return GraphIndex(trans, name, size)
 
1721
 
 
1722
    def test_add_good_unvalidated_index(self):
 
1723
        unvalidated = self.make_g_index('unvalidated')
 
1724
        combined = CombinedGraphIndex([unvalidated])
 
1725
        index = _KnitGraphIndex(combined, lambda: True, parents=False)
 
1726
        index.scan_unvalidated_index(unvalidated)
 
1727
        self.assertEqual(frozenset(),
 
1728
            index.get_missing_compression_parents())
 
1729
 
 
1730
    def test_parents_deltas_incompatible(self):
 
1731
        index = CombinedGraphIndex([])
 
1732
        self.assertRaises(errors.KnitError, _KnitGraphIndex, lambda:True,
 
1733
            index, deltas=True, parents=False)
 
1734
 
 
1735
    def two_graph_index(self, catch_adds=False):
 
1736
        """Build a two-graph index.
 
1737
 
 
1738
        :param deltas: If true, use underlying indices with two node-ref
 
1739
            lists and 'parent' set to a delta-compressed against tail.
 
1740
        """
 
1741
        # put several versions in the index.
 
1742
        index1 = self.make_g_index('1', 0, [
 
1743
            (('tip', ), 'N0 100'),
 
1744
            (('tail', ), '')])
 
1745
        index2 = self.make_g_index('2', 0, [
 
1746
            (('parent', ), ' 100 78'),
 
1747
            (('separate', ), '')])
 
1748
        combined_index = CombinedGraphIndex([index1, index2])
 
1749
        if catch_adds:
 
1750
            self.combined_index = combined_index
 
1751
            self.caught_entries = []
 
1752
            add_callback = self.catch_add
 
1753
        else:
 
1754
            add_callback = None
 
1755
        return _KnitGraphIndex(combined_index, lambda:True, parents=False,
 
1756
            add_callback=add_callback)
 
1757
 
 
1758
    def test_keys(self):
 
1759
        index = self.two_graph_index()
 
1760
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1761
            set(index.keys()))
 
1762
 
 
1763
    def test_get_position(self):
 
1764
        index = self.two_graph_index()
 
1765
        self.assertEqual((index._graph_index._indices[0], 0, 100),
 
1766
            index.get_position(('tip',)))
 
1767
        self.assertEqual((index._graph_index._indices[1], 100, 78),
 
1768
            index.get_position(('parent',)))
 
1769
 
 
1770
    def test_get_method(self):
 
1771
        index = self.two_graph_index()
 
1772
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1773
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1774
 
 
1775
    def test_get_options(self):
 
1776
        index = self.two_graph_index()
 
1777
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1778
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1779
 
 
1780
    def test_get_parent_map(self):
 
1781
        index = self.two_graph_index()
 
1782
        self.assertEqual({('parent',):None},
 
1783
            index.get_parent_map([('parent',), ('ghost',)]))
 
1784
 
 
1785
    def catch_add(self, entries):
 
1786
        self.caught_entries.append(entries)
 
1787
 
 
1788
    def test_add_no_callback_errors(self):
 
1789
        index = self.two_graph_index()
 
1790
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1791
            [(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)])])
 
1792
 
 
1793
    def test_add_version_smoke(self):
 
1794
        index = self.two_graph_index(catch_adds=True)
 
1795
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60), [])])
 
1796
        self.assertEqual([[(('new', ), 'N50 60')]],
 
1797
            self.caught_entries)
 
1798
 
 
1799
    def test_add_version_delta_not_delta_index(self):
 
1800
        index = self.two_graph_index(catch_adds=True)
 
1801
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1802
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1803
        self.assertEqual([], self.caught_entries)
 
1804
 
 
1805
    def test_add_version_same_dup(self):
 
1806
        index = self.two_graph_index(catch_adds=True)
 
1807
        # options can be spelt two different ways
 
1808
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1809
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
1810
        # position/length are ignored (because each pack could have fulltext or
 
1811
        # delta, and be at a different position.
 
1812
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
1813
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
 
1814
        # but neither should have added data.
 
1815
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1816
 
 
1817
    def test_add_version_different_dup(self):
 
1818
        index = self.two_graph_index(catch_adds=True)
 
1819
        # change options
 
1820
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1821
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1822
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1823
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
1824
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1825
            [(('tip',), 'fulltext', (None, 0, 100), [])])
 
1826
        # parents
 
1827
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1828
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1829
        self.assertEqual([], self.caught_entries)
 
1830
 
 
1831
    def test_add_versions(self):
 
1832
        index = self.two_graph_index(catch_adds=True)
 
1833
        index.add_records([
 
1834
                (('new',), 'fulltext,no-eol', (None, 50, 60), []),
 
1835
                (('new2',), 'fulltext', (None, 0, 6), []),
 
1836
                ])
 
1837
        self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
 
1838
            sorted(self.caught_entries[0]))
 
1839
        self.assertEqual(1, len(self.caught_entries))
 
1840
 
 
1841
    def test_add_versions_delta_not_delta_index(self):
 
1842
        index = self.two_graph_index(catch_adds=True)
 
1843
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1844
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1845
        self.assertEqual([], self.caught_entries)
 
1846
 
 
1847
    def test_add_versions_parents_not_parents_index(self):
 
1848
        index = self.two_graph_index(catch_adds=True)
 
1849
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1850
            [(('new',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
 
1851
        self.assertEqual([], self.caught_entries)
 
1852
 
 
1853
    def test_add_versions_random_id_accepted(self):
 
1854
        index = self.two_graph_index(catch_adds=True)
 
1855
        index.add_records([], random_id=True)
 
1856
 
 
1857
    def test_add_versions_same_dup(self):
 
1858
        index = self.two_graph_index(catch_adds=True)
 
1859
        # options can be spelt two different ways
 
1860
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1861
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
1862
        # position/length are ignored (because each pack could have fulltext or
 
1863
        # delta, and be at a different position.
 
1864
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
1865
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
 
1866
        # but neither should have added data.
 
1867
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1868
 
 
1869
    def test_add_versions_different_dup(self):
 
1870
        index = self.two_graph_index(catch_adds=True)
 
1871
        # change options
 
1872
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1873
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1874
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1875
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
1876
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1877
            [(('tip',), 'fulltext', (None, 0, 100), [])])
 
1878
        # parents
 
1879
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1880
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1881
        # change options in the second record
 
1882
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1883
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), []),
 
1884
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1885
        self.assertEqual([], self.caught_entries)
 
1886
 
 
1887
 
 
1888
class TestKnitVersionedFiles(KnitTests):
 
1889
 
 
1890
    def assertGroupKeysForIo(self, exp_groups, keys, non_local_keys,
 
1891
                             positions, _min_buffer_size=None):
 
1892
        kvf = self.make_test_knit()
 
1893
        if _min_buffer_size is None:
 
1894
            _min_buffer_size = knit._STREAM_MIN_BUFFER_SIZE
 
1895
        self.assertEqual(exp_groups, kvf._group_keys_for_io(keys,
 
1896
                                        non_local_keys, positions,
 
1897
                                        _min_buffer_size=_min_buffer_size))
 
1898
 
 
1899
    def assertSplitByPrefix(self, expected_map, expected_prefix_order,
 
1900
                            keys):
 
1901
        split, prefix_order = KnitVersionedFiles._split_by_prefix(keys)
 
1902
        self.assertEqual(expected_map, split)
 
1903
        self.assertEqual(expected_prefix_order, prefix_order)
 
1904
 
 
1905
    def test__group_keys_for_io(self):
 
1906
        ft_detail = ('fulltext', False)
 
1907
        ld_detail = ('line-delta', False)
 
1908
        f_a = ('f', 'a')
 
1909
        f_b = ('f', 'b')
 
1910
        f_c = ('f', 'c')
 
1911
        g_a = ('g', 'a')
 
1912
        g_b = ('g', 'b')
 
1913
        g_c = ('g', 'c')
 
1914
        positions = {
 
1915
            f_a: (ft_detail, (f_a, 0, 100), None),
 
1916
            f_b: (ld_detail, (f_b, 100, 21), f_a),
 
1917
            f_c: (ld_detail, (f_c, 180, 15), f_b),
 
1918
            g_a: (ft_detail, (g_a, 121, 35), None),
 
1919
            g_b: (ld_detail, (g_b, 156, 12), g_a),
 
1920
            g_c: (ld_detail, (g_c, 195, 13), g_a),
 
1921
            }
 
1922
        self.assertGroupKeysForIo([([f_a], set())],
 
1923
                                  [f_a], [], positions)
 
1924
        self.assertGroupKeysForIo([([f_a], set([f_a]))],
 
1925
                                  [f_a], [f_a], positions)
 
1926
        self.assertGroupKeysForIo([([f_a, f_b], set([]))],
 
1927
                                  [f_a, f_b], [], positions)
 
1928
        self.assertGroupKeysForIo([([f_a, f_b], set([f_b]))],
 
1929
                                  [f_a, f_b], [f_b], positions)
 
1930
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
1931
                                  [f_a, g_a, f_b, g_b], [], positions)
 
1932
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
1933
                                  [f_a, g_a, f_b, g_b], [], positions,
 
1934
                                  _min_buffer_size=150)
 
1935
        self.assertGroupKeysForIo([([f_a, f_b], set()), ([g_a, g_b], set())],
 
1936
                                  [f_a, g_a, f_b, g_b], [], positions,
 
1937
                                  _min_buffer_size=100)
 
1938
        self.assertGroupKeysForIo([([f_c], set()), ([g_b], set())],
 
1939
                                  [f_c, g_b], [], positions,
 
1940
                                  _min_buffer_size=125)
 
1941
        self.assertGroupKeysForIo([([g_b, f_c], set())],
 
1942
                                  [g_b, f_c], [], positions,
 
1943
                                  _min_buffer_size=125)
 
1944
 
 
1945
    def test__split_by_prefix(self):
 
1946
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
1947
                                  'g': [('g', 'b'), ('g', 'a')],
 
1948
                                 }, ['f', 'g'],
 
1949
                                 [('f', 'a'), ('g', 'b'),
 
1950
                                  ('g', 'a'), ('f', 'b')])
 
1951
 
 
1952
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
1953
                                  'g': [('g', 'b'), ('g', 'a')],
 
1954
                                 }, ['f', 'g'],
 
1955
                                 [('f', 'a'), ('f', 'b'),
 
1956
                                  ('g', 'b'), ('g', 'a')])
 
1957
 
 
1958
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
1959
                                  'g': [('g', 'b'), ('g', 'a')],
 
1960
                                 }, ['f', 'g'],
 
1961
                                 [('f', 'a'), ('f', 'b'),
 
1962
                                  ('g', 'b'), ('g', 'a')])
 
1963
 
 
1964
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
1965
                                  'g': [('g', 'b'), ('g', 'a')],
 
1966
                                  '': [('a',), ('b',)]
 
1967
                                 }, ['f', 'g', ''],
 
1968
                                 [('f', 'a'), ('g', 'b'),
 
1969
                                  ('a',), ('b',),
 
1970
                                  ('g', 'a'), ('f', 'b')])
 
1971
 
 
1972
 
 
1973
class TestStacking(KnitTests):
 
1974
 
 
1975
    def get_basis_and_test_knit(self):
 
1976
        basis = self.make_test_knit(name='basis')
 
1977
        basis = RecordingVersionedFilesDecorator(basis)
 
1978
        test = self.make_test_knit(name='test')
 
1979
        test.add_fallback_versioned_files(basis)
 
1980
        return basis, test
 
1981
 
 
1982
    def test_add_fallback_versioned_files(self):
 
1983
        basis = self.make_test_knit(name='basis')
 
1984
        test = self.make_test_knit(name='test')
 
1985
        # It must not error; other tests test that the fallback is referred to
 
1986
        # when accessing data.
 
1987
        test.add_fallback_versioned_files(basis)
 
1988
 
 
1989
    def test_add_lines(self):
 
1990
        # lines added to the test are not added to the basis
 
1991
        basis, test = self.get_basis_and_test_knit()
 
1992
        key = ('foo',)
 
1993
        key_basis = ('bar',)
 
1994
        key_cross_border = ('quux',)
 
1995
        key_delta = ('zaphod',)
 
1996
        test.add_lines(key, (), ['foo\n'])
 
1997
        self.assertEqual({}, basis.get_parent_map([key]))
 
1998
        # lines added to the test that reference across the stack do a
 
1999
        # fulltext.
 
2000
        basis.add_lines(key_basis, (), ['foo\n'])
 
2001
        basis.calls = []
 
2002
        test.add_lines(key_cross_border, (key_basis,), ['foo\n'])
 
2003
        self.assertEqual('fulltext', test._index.get_method(key_cross_border))
 
2004
        # we don't even need to look at the basis to see that this should be
 
2005
        # stored as a fulltext
 
2006
        self.assertEqual([], basis.calls)
 
2007
        # Subsequent adds do delta.
 
2008
        basis.calls = []
 
2009
        test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
 
2010
        self.assertEqual('line-delta', test._index.get_method(key_delta))
 
2011
        self.assertEqual([], basis.calls)
 
2012
 
 
2013
    def test_annotate(self):
 
2014
        # annotations from the test knit are answered without asking the basis
 
2015
        basis, test = self.get_basis_and_test_knit()
 
2016
        key = ('foo',)
 
2017
        key_basis = ('bar',)
 
2018
        key_missing = ('missing',)
 
2019
        test.add_lines(key, (), ['foo\n'])
 
2020
        details = test.annotate(key)
 
2021
        self.assertEqual([(key, 'foo\n')], details)
 
2022
        self.assertEqual([], basis.calls)
 
2023
        # But texts that are not in the test knit are looked for in the basis
 
2024
        # directly.
 
2025
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2026
        basis.calls = []
 
2027
        details = test.annotate(key_basis)
 
2028
        self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
 
2029
        # Not optimised to date:
 
2030
        # self.assertEqual([("annotate", key_basis)], basis.calls)
 
2031
        self.assertEqual([('get_parent_map', set([key_basis])),
 
2032
            ('get_parent_map', set([key_basis])),
 
2033
            ('get_parent_map', set([key_basis])),
 
2034
            ('get_record_stream', [key_basis], 'unordered', True)],
 
2035
            basis.calls)
 
2036
 
 
2037
    def test_check(self):
 
2038
        # At the moment checking a stacked knit does implicitly check the
 
2039
        # fallback files.
 
2040
        basis, test = self.get_basis_and_test_knit()
 
2041
        test.check()
 
2042
 
 
2043
    def test_get_parent_map(self):
 
2044
        # parents in the test knit are answered without asking the basis
 
2045
        basis, test = self.get_basis_and_test_knit()
 
2046
        key = ('foo',)
 
2047
        key_basis = ('bar',)
 
2048
        key_missing = ('missing',)
 
2049
        test.add_lines(key, (), [])
 
2050
        parent_map = test.get_parent_map([key])
 
2051
        self.assertEqual({key: ()}, parent_map)
 
2052
        self.assertEqual([], basis.calls)
 
2053
        # But parents that are not in the test knit are looked for in the basis
 
2054
        basis.add_lines(key_basis, (), [])
 
2055
        basis.calls = []
 
2056
        parent_map = test.get_parent_map([key, key_basis, key_missing])
 
2057
        self.assertEqual({key: (),
 
2058
            key_basis: ()}, parent_map)
 
2059
        self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
 
2060
            basis.calls)
 
2061
 
 
2062
    def test_get_record_stream_unordered_fulltexts(self):
 
2063
        # records from the test knit are answered without asking the basis:
 
2064
        basis, test = self.get_basis_and_test_knit()
 
2065
        key = ('foo',)
 
2066
        key_basis = ('bar',)
 
2067
        key_missing = ('missing',)
 
2068
        test.add_lines(key, (), ['foo\n'])
 
2069
        records = list(test.get_record_stream([key], 'unordered', True))
 
2070
        self.assertEqual(1, len(records))
 
2071
        self.assertEqual([], basis.calls)
 
2072
        # Missing (from test knit) objects are retrieved from the basis:
 
2073
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2074
        basis.calls = []
 
2075
        records = list(test.get_record_stream([key_basis, key_missing],
 
2076
            'unordered', True))
 
2077
        self.assertEqual(2, len(records))
 
2078
        calls = list(basis.calls)
 
2079
        for record in records:
 
2080
            self.assertSubset([record.key], (key_basis, key_missing))
 
2081
            if record.key == key_missing:
 
2082
                self.assertIsInstance(record, AbsentContentFactory)
 
2083
            else:
 
2084
                reference = list(basis.get_record_stream([key_basis],
 
2085
                    'unordered', True))[0]
 
2086
                self.assertEqual(reference.key, record.key)
 
2087
                self.assertEqual(reference.sha1, record.sha1)
 
2088
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2089
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2090
                    record.get_bytes_as(record.storage_kind))
 
2091
                self.assertEqual(reference.get_bytes_as('fulltext'),
 
2092
                    record.get_bytes_as('fulltext'))
 
2093
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2094
        # ask which fallbacks have which parents.
 
2095
        self.assertEqual([
 
2096
            ("get_parent_map", set([key_basis, key_missing])),
 
2097
            ("get_record_stream", [key_basis], 'unordered', True)],
 
2098
            calls)
 
2099
 
 
2100
    def test_get_record_stream_ordered_fulltexts(self):
 
2101
        # ordering is preserved down into the fallback store.
 
2102
        basis, test = self.get_basis_and_test_knit()
 
2103
        key = ('foo',)
 
2104
        key_basis = ('bar',)
 
2105
        key_basis_2 = ('quux',)
 
2106
        key_missing = ('missing',)
 
2107
        test.add_lines(key, (key_basis,), ['foo\n'])
 
2108
        # Missing (from test knit) objects are retrieved from the basis:
 
2109
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
2110
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
2111
        basis.calls = []
 
2112
        # ask for in non-topological order
 
2113
        records = list(test.get_record_stream(
 
2114
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
 
2115
        self.assertEqual(4, len(records))
 
2116
        results = []
 
2117
        for record in records:
 
2118
            self.assertSubset([record.key],
 
2119
                (key_basis, key_missing, key_basis_2, key))
 
2120
            if record.key == key_missing:
 
2121
                self.assertIsInstance(record, AbsentContentFactory)
 
2122
            else:
 
2123
                results.append((record.key, record.sha1, record.storage_kind,
 
2124
                    record.get_bytes_as('fulltext')))
 
2125
        calls = list(basis.calls)
 
2126
        order = [record[0] for record in results]
 
2127
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2128
        for result in results:
 
2129
            if result[0] == key:
 
2130
                source = test
 
2131
            else:
 
2132
                source = basis
 
2133
            record = source.get_record_stream([result[0]], 'unordered',
 
2134
                True).next()
 
2135
            self.assertEqual(record.key, result[0])
 
2136
            self.assertEqual(record.sha1, result[1])
 
2137
            # We used to check that the storage kind matched, but actually it
 
2138
            # depends on whether it was sourced from the basis, or in a single
 
2139
            # group, because asking for full texts returns proxy objects to a
 
2140
            # _ContentMapGenerator object; so checking the kind is unneeded.
 
2141
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
 
2142
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2143
        # ask which fallbacks have which parents.
 
2144
        self.assertEqual([
 
2145
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
2146
            # unordered is asked for by the underlying worker as it still
 
2147
            # buffers everything while answering - which is a problem!
 
2148
            ("get_record_stream", [key_basis_2, key_basis], 'unordered', True)],
 
2149
            calls)
 
2150
 
 
2151
    def test_get_record_stream_unordered_deltas(self):
 
2152
        # records from the test knit are answered without asking the basis:
 
2153
        basis, test = self.get_basis_and_test_knit()
 
2154
        key = ('foo',)
 
2155
        key_basis = ('bar',)
 
2156
        key_missing = ('missing',)
 
2157
        test.add_lines(key, (), ['foo\n'])
 
2158
        records = list(test.get_record_stream([key], 'unordered', False))
 
2159
        self.assertEqual(1, len(records))
 
2160
        self.assertEqual([], basis.calls)
 
2161
        # Missing (from test knit) objects are retrieved from the basis:
 
2162
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2163
        basis.calls = []
 
2164
        records = list(test.get_record_stream([key_basis, key_missing],
 
2165
            'unordered', False))
 
2166
        self.assertEqual(2, len(records))
 
2167
        calls = list(basis.calls)
 
2168
        for record in records:
 
2169
            self.assertSubset([record.key], (key_basis, key_missing))
 
2170
            if record.key == key_missing:
 
2171
                self.assertIsInstance(record, AbsentContentFactory)
 
2172
            else:
 
2173
                reference = list(basis.get_record_stream([key_basis],
 
2174
                    'unordered', False))[0]
 
2175
                self.assertEqual(reference.key, record.key)
 
2176
                self.assertEqual(reference.sha1, record.sha1)
 
2177
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2178
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2179
                    record.get_bytes_as(record.storage_kind))
 
2180
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2181
        # ask which fallbacks have which parents.
 
2182
        self.assertEqual([
 
2183
            ("get_parent_map", set([key_basis, key_missing])),
 
2184
            ("get_record_stream", [key_basis], 'unordered', False)],
 
2185
            calls)
 
2186
 
 
2187
    def test_get_record_stream_ordered_deltas(self):
 
2188
        # ordering is preserved down into the fallback store.
 
2189
        basis, test = self.get_basis_and_test_knit()
 
2190
        key = ('foo',)
 
2191
        key_basis = ('bar',)
 
2192
        key_basis_2 = ('quux',)
 
2193
        key_missing = ('missing',)
 
2194
        test.add_lines(key, (key_basis,), ['foo\n'])
 
2195
        # Missing (from test knit) objects are retrieved from the basis:
 
2196
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
2197
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
2198
        basis.calls = []
 
2199
        # ask for in non-topological order
 
2200
        records = list(test.get_record_stream(
 
2201
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
 
2202
        self.assertEqual(4, len(records))
 
2203
        results = []
 
2204
        for record in records:
 
2205
            self.assertSubset([record.key],
 
2206
                (key_basis, key_missing, key_basis_2, key))
 
2207
            if record.key == key_missing:
 
2208
                self.assertIsInstance(record, AbsentContentFactory)
 
2209
            else:
 
2210
                results.append((record.key, record.sha1, record.storage_kind,
 
2211
                    record.get_bytes_as(record.storage_kind)))
 
2212
        calls = list(basis.calls)
 
2213
        order = [record[0] for record in results]
 
2214
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2215
        for result in results:
 
2216
            if result[0] == key:
 
2217
                source = test
 
2218
            else:
 
2219
                source = basis
 
2220
            record = source.get_record_stream([result[0]], 'unordered',
 
2221
                False).next()
 
2222
            self.assertEqual(record.key, result[0])
 
2223
            self.assertEqual(record.sha1, result[1])
 
2224
            self.assertEqual(record.storage_kind, result[2])
 
2225
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
 
2226
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2227
        # ask which fallbacks have which parents.
 
2228
        self.assertEqual([
 
2229
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
2230
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
 
2231
            calls)
 
2232
 
 
2233
    def test_get_sha1s(self):
 
2234
        # sha1's in the test knit are answered without asking the basis
 
2235
        basis, test = self.get_basis_and_test_knit()
 
2236
        key = ('foo',)
 
2237
        key_basis = ('bar',)
 
2238
        key_missing = ('missing',)
 
2239
        test.add_lines(key, (), ['foo\n'])
 
2240
        key_sha1sum = osutils.sha('foo\n').hexdigest()
 
2241
        sha1s = test.get_sha1s([key])
 
2242
        self.assertEqual({key: key_sha1sum}, sha1s)
 
2243
        self.assertEqual([], basis.calls)
 
2244
        # But texts that are not in the test knit are looked for in the basis
 
2245
        # directly (rather than via text reconstruction) so that remote servers
 
2246
        # etc don't have to answer with full content.
 
2247
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2248
        basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
2249
        basis.calls = []
 
2250
        sha1s = test.get_sha1s([key, key_missing, key_basis])
 
2251
        self.assertEqual({key: key_sha1sum,
 
2252
            key_basis: basis_sha1sum}, sha1s)
 
2253
        self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
 
2254
            basis.calls)
 
2255
 
 
2256
    def test_insert_record_stream(self):
 
2257
        # records are inserted as normal; insert_record_stream builds on
 
2258
        # add_lines, so a smoke test should be all that's needed:
 
2259
        key = ('foo',)
 
2260
        key_basis = ('bar',)
 
2261
        key_delta = ('zaphod',)
 
2262
        basis, test = self.get_basis_and_test_knit()
 
2263
        source = self.make_test_knit(name='source')
 
2264
        basis.add_lines(key_basis, (), ['foo\n'])
 
2265
        basis.calls = []
 
2266
        source.add_lines(key_basis, (), ['foo\n'])
 
2267
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2268
        stream = source.get_record_stream([key_delta], 'unordered', False)
 
2269
        test.insert_record_stream(stream)
 
2270
        # XXX: this does somewhat too many calls in making sure of whether it
 
2271
        # has to recreate the full text.
 
2272
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2273
             ('get_parent_map', set([key_basis])),
 
2274
             ('get_record_stream', [key_basis], 'unordered', True)],
 
2275
            basis.calls)
 
2276
        self.assertEqual({key_delta:(key_basis,)},
 
2277
            test.get_parent_map([key_delta]))
 
2278
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2279
            'unordered', True).next().get_bytes_as('fulltext'))
 
2280
 
 
2281
    def test_iter_lines_added_or_present_in_keys(self):
 
2282
        # Lines from the basis are returned, and lines for a given key are only
 
2283
        # returned once.
 
2284
        key1 = ('foo1',)
 
2285
        key2 = ('foo2',)
 
2286
        # all sources are asked for keys:
 
2287
        basis, test = self.get_basis_and_test_knit()
 
2288
        basis.add_lines(key1, (), ["foo"])
 
2289
        basis.calls = []
 
2290
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
 
2291
        self.assertEqual([("foo\n", key1)], lines)
 
2292
        self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
 
2293
            basis.calls)
 
2294
        # keys in both are not duplicated:
 
2295
        test.add_lines(key2, (), ["bar\n"])
 
2296
        basis.add_lines(key2, (), ["bar\n"])
 
2297
        basis.calls = []
 
2298
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
 
2299
        self.assertEqual([("bar\n", key2)], lines)
 
2300
        self.assertEqual([], basis.calls)
 
2301
 
 
2302
    def test_keys(self):
 
2303
        key1 = ('foo1',)
 
2304
        key2 = ('foo2',)
 
2305
        # all sources are asked for keys:
 
2306
        basis, test = self.get_basis_and_test_knit()
 
2307
        keys = test.keys()
 
2308
        self.assertEqual(set(), set(keys))
 
2309
        self.assertEqual([("keys",)], basis.calls)
 
2310
        # keys from a basis are returned:
 
2311
        basis.add_lines(key1, (), [])
 
2312
        basis.calls = []
 
2313
        keys = test.keys()
 
2314
        self.assertEqual(set([key1]), set(keys))
 
2315
        self.assertEqual([("keys",)], basis.calls)
 
2316
        # keys in both are not duplicated:
 
2317
        test.add_lines(key2, (), [])
 
2318
        basis.add_lines(key2, (), [])
 
2319
        basis.calls = []
 
2320
        keys = test.keys()
 
2321
        self.assertEqual(2, len(keys))
 
2322
        self.assertEqual(set([key1, key2]), set(keys))
 
2323
        self.assertEqual([("keys",)], basis.calls)
 
2324
 
 
2325
    def test_add_mpdiffs(self):
 
2326
        # records are inserted as normal; add_mpdiff builds on
 
2327
        # add_lines, so a smoke test should be all that's needed:
 
2328
        key = ('foo',)
 
2329
        key_basis = ('bar',)
 
2330
        key_delta = ('zaphod',)
 
2331
        basis, test = self.get_basis_and_test_knit()
 
2332
        source = self.make_test_knit(name='source')
 
2333
        basis.add_lines(key_basis, (), ['foo\n'])
 
2334
        basis.calls = []
 
2335
        source.add_lines(key_basis, (), ['foo\n'])
 
2336
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2337
        diffs = source.make_mpdiffs([key_delta])
 
2338
        test.add_mpdiffs([(key_delta, (key_basis,),
 
2339
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
 
2340
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2341
            ('get_record_stream', [key_basis], 'unordered', True),],
 
2342
            basis.calls)
 
2343
        self.assertEqual({key_delta:(key_basis,)},
 
2344
            test.get_parent_map([key_delta]))
 
2345
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2346
            'unordered', True).next().get_bytes_as('fulltext'))
 
2347
 
 
2348
    def test_make_mpdiffs(self):
 
2349
        # Generating an mpdiff across a stacking boundary should detect parent
 
2350
        # texts regions.
 
2351
        key = ('foo',)
 
2352
        key_left = ('bar',)
 
2353
        key_right = ('zaphod',)
 
2354
        basis, test = self.get_basis_and_test_knit()
 
2355
        basis.add_lines(key_left, (), ['bar\n'])
 
2356
        basis.add_lines(key_right, (), ['zaphod\n'])
 
2357
        basis.calls = []
 
2358
        test.add_lines(key, (key_left, key_right),
 
2359
            ['bar\n', 'foo\n', 'zaphod\n'])
 
2360
        diffs = test.make_mpdiffs([key])
 
2361
        self.assertEqual([
 
2362
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
 
2363
                multiparent.NewText(['foo\n']),
 
2364
                multiparent.ParentText(1, 0, 2, 1)])],
 
2365
            diffs)
 
2366
        self.assertEqual(3, len(basis.calls))
 
2367
        self.assertEqual([
 
2368
            ("get_parent_map", set([key_left, key_right])),
 
2369
            ("get_parent_map", set([key_left, key_right])),
 
2370
            ],
 
2371
            basis.calls[:-1])
 
2372
        last_call = basis.calls[-1]
 
2373
        self.assertEqual('get_record_stream', last_call[0])
 
2374
        self.assertEqual(set([key_left, key_right]), set(last_call[1]))
 
2375
        self.assertEqual('unordered', last_call[2])
 
2376
        self.assertEqual(True, last_call[3])
 
2377
 
 
2378
 
 
2379
class TestNetworkBehaviour(KnitTests):
 
2380
    """Tests for getting data out of/into knits over the network."""
 
2381
 
 
2382
    def test_include_delta_closure_generates_a_knit_delta_closure(self):
 
2383
        vf = self.make_test_knit(name='test')
 
2384
        # put in three texts, giving ft, delta, delta
 
2385
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2386
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2387
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2388
        # But heuristics could interfere, so check what happened:
 
2389
        self.assertEqual(['knit-ft-gz', 'knit-delta-gz', 'knit-delta-gz'],
 
2390
            [record.storage_kind for record in
 
2391
             vf.get_record_stream([('base',), ('d1',), ('d2',)],
 
2392
                'topological', False)])
 
2393
        # generate a stream of just the deltas include_delta_closure=True,
 
2394
        # serialise to the network, and check that we get a delta closure on the wire.
 
2395
        stream = vf.get_record_stream([('d1',), ('d2',)], 'topological', True)
 
2396
        netb = [record.get_bytes_as(record.storage_kind) for record in stream]
 
2397
        # The first bytes should be a memo from _ContentMapGenerator, and the
 
2398
        # second bytes should be empty (because its a API proxy not something
 
2399
        # for wire serialisation.
 
2400
        self.assertEqual('', netb[1])
 
2401
        bytes = netb[0]
 
2402
        kind, line_end = network_bytes_to_kind_and_offset(bytes)
 
2403
        self.assertEqual('knit-delta-closure', kind)
 
2404
 
 
2405
 
 
2406
class TestContentMapGenerator(KnitTests):
 
2407
    """Tests for ContentMapGenerator"""
 
2408
 
 
2409
    def test_get_record_stream_gives_records(self):
 
2410
        vf = self.make_test_knit(name='test')
 
2411
        # put in three texts, giving ft, delta, delta
 
2412
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2413
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2414
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2415
        keys = [('d1',), ('d2',)]
 
2416
        generator = _VFContentMapGenerator(vf, keys,
 
2417
            global_map=vf.get_parent_map(keys))
 
2418
        for record in generator.get_record_stream():
 
2419
            if record.key == ('d1',):
 
2420
                self.assertEqual('d1\n', record.get_bytes_as('fulltext'))
 
2421
            else:
 
2422
                self.assertEqual('d2\n', record.get_bytes_as('fulltext'))
 
2423
 
 
2424
    def test_get_record_stream_kinds_are_raw(self):
 
2425
        vf = self.make_test_knit(name='test')
 
2426
        # put in three texts, giving ft, delta, delta
 
2427
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2428
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2429
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2430
        keys = [('base',), ('d1',), ('d2',)]
 
2431
        generator = _VFContentMapGenerator(vf, keys,
 
2432
            global_map=vf.get_parent_map(keys))
 
2433
        kinds = {('base',): 'knit-delta-closure',
 
2434
            ('d1',): 'knit-delta-closure-ref',
 
2435
            ('d2',): 'knit-delta-closure-ref',
 
2436
            }
 
2437
        for record in generator.get_record_stream():
 
2438
            self.assertEqual(kinds[record.key], record.storage_kind)