/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Ian Clatworthy
  • Date: 2010-01-03 04:07:55 UTC
  • mto: (4944.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 4945.
  • Revision ID: ian.clatworthy@canonical.com-20100103040755-2i24o4olhqbnhxnk
Better linking in global-options topic

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
    tests,
 
32
    )
 
33
from bzrlib.errors import (
 
34
    RevisionAlreadyPresent,
 
35
    KnitHeaderError,
 
36
    RevisionNotPresent,
 
37
    NoSuchFile,
 
38
    )
 
39
from bzrlib.index import *
 
40
from bzrlib.knit import (
 
41
    AnnotatedKnitContent,
 
42
    KnitContent,
 
43
    KnitSequenceMatcher,
 
44
    KnitVersionedFiles,
 
45
    PlainKnitContent,
 
46
    _VFContentMapGenerator,
 
47
    _DirectPackAccess,
 
48
    _KndxIndex,
 
49
    _KnitGraphIndex,
 
50
    _KnitKeyAccess,
 
51
    make_file_factory,
 
52
    )
 
53
from bzrlib.repofmt import pack_repo
 
54
from bzrlib.tests import (
 
55
    Feature,
 
56
    KnownFailure,
 
57
    TestCase,
 
58
    TestCaseWithMemoryTransport,
 
59
    TestCaseWithTransport,
 
60
    TestNotApplicable,
 
61
    )
 
62
from bzrlib.transport import get_transport
 
63
from bzrlib.transport.memory import MemoryTransport
 
64
from bzrlib.tuned_gzip import GzipFile
 
65
from bzrlib.versionedfile import (
 
66
    AbsentContentFactory,
 
67
    ConstantMapper,
 
68
    network_bytes_to_kind_and_offset,
 
69
    RecordingVersionedFilesDecorator,
 
70
    )
 
71
 
 
72
 
 
73
compiled_knit_feature = tests.ModuleAvailableFeature(
 
74
                            'bzrlib._knit_load_data_pyx')
 
75
 
 
76
 
 
77
class KnitContentTestsMixin(object):
 
78
 
 
79
    def test_constructor(self):
 
80
        content = self._make_content([])
 
81
 
 
82
    def test_text(self):
 
83
        content = self._make_content([])
 
84
        self.assertEqual(content.text(), [])
 
85
 
 
86
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
87
        self.assertEqual(content.text(), ["text1", "text2"])
 
88
 
 
89
    def test_copy(self):
 
90
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
91
        copy = content.copy()
 
92
        self.assertIsInstance(copy, content.__class__)
 
93
        self.assertEqual(copy.annotate(), content.annotate())
 
94
 
 
95
    def assertDerivedBlocksEqual(self, source, target, noeol=False):
 
96
        """Assert that the derived matching blocks match real output"""
 
97
        source_lines = source.splitlines(True)
 
98
        target_lines = target.splitlines(True)
 
99
        def nl(line):
 
100
            if noeol and not line.endswith('\n'):
 
101
                return line + '\n'
 
102
            else:
 
103
                return line
 
104
        source_content = self._make_content([(None, nl(l)) for l in source_lines])
 
105
        target_content = self._make_content([(None, nl(l)) for l in target_lines])
 
106
        line_delta = source_content.line_delta(target_content)
 
107
        delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
 
108
            source_lines, target_lines))
 
109
        matcher = KnitSequenceMatcher(None, source_lines, target_lines)
 
110
        matcher_blocks = list(list(matcher.get_matching_blocks()))
 
111
        self.assertEqual(matcher_blocks, delta_blocks)
 
112
 
 
113
    def test_get_line_delta_blocks(self):
 
114
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
 
115
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
 
116
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
 
117
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
 
118
        self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
 
119
        self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
 
120
        self.assertDerivedBlocksEqual(TEXT_1A, '')
 
121
        self.assertDerivedBlocksEqual('', TEXT_1A)
 
122
        self.assertDerivedBlocksEqual('', '')
 
123
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
 
124
 
 
125
    def test_get_line_delta_blocks_noeol(self):
 
126
        """Handle historical knit deltas safely
 
127
 
 
128
        Some existing knit deltas don't consider the last line to differ
 
129
        when the only difference whether it has a final newline.
 
130
 
 
131
        New knit deltas appear to always consider the last line to differ
 
132
        in this case.
 
133
        """
 
134
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
 
135
        self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
 
136
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
 
137
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
 
138
 
 
139
 
 
140
TEXT_1 = """\
 
141
Banana cup cakes:
 
142
 
 
143
- bananas
 
144
- eggs
 
145
- broken tea cups
 
146
"""
 
147
 
 
148
TEXT_1A = """\
 
149
Banana cup cake recipe
 
150
(serves 6)
 
151
 
 
152
- bananas
 
153
- eggs
 
154
- broken tea cups
 
155
- self-raising flour
 
156
"""
 
157
 
 
158
TEXT_1B = """\
 
159
Banana cup cake recipe
 
160
 
 
161
- bananas (do not use plantains!!!)
 
162
- broken tea cups
 
163
- flour
 
164
"""
 
165
 
 
166
delta_1_1a = """\
 
167
0,1,2
 
168
Banana cup cake recipe
 
169
(serves 6)
 
170
5,5,1
 
171
- self-raising flour
 
172
"""
 
173
 
 
174
TEXT_2 = """\
 
175
Boeuf bourguignon
 
176
 
 
177
- beef
 
178
- red wine
 
179
- small onions
 
180
- carrot
 
181
- mushrooms
 
182
"""
 
183
 
 
184
 
 
185
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
 
186
 
 
187
    def _make_content(self, lines):
 
188
        annotated_content = AnnotatedKnitContent(lines)
 
189
        return PlainKnitContent(annotated_content.text(), 'bogus')
 
190
 
 
191
    def test_annotate(self):
 
192
        content = self._make_content([])
 
193
        self.assertEqual(content.annotate(), [])
 
194
 
 
195
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
196
        self.assertEqual(content.annotate(),
 
197
            [("bogus", "text1"), ("bogus", "text2")])
 
198
 
 
199
    def test_line_delta(self):
 
200
        content1 = self._make_content([("", "a"), ("", "b")])
 
201
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
202
        self.assertEqual(content1.line_delta(content2),
 
203
            [(1, 2, 2, ["a", "c"])])
 
204
 
 
205
    def test_line_delta_iter(self):
 
206
        content1 = self._make_content([("", "a"), ("", "b")])
 
207
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
208
        it = content1.line_delta_iter(content2)
 
209
        self.assertEqual(it.next(), (1, 2, 2, ["a", "c"]))
 
210
        self.assertRaises(StopIteration, it.next)
 
211
 
 
212
 
 
213
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
 
214
 
 
215
    def _make_content(self, lines):
 
216
        return AnnotatedKnitContent(lines)
 
217
 
 
218
    def test_annotate(self):
 
219
        content = self._make_content([])
 
220
        self.assertEqual(content.annotate(), [])
 
221
 
 
222
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
223
        self.assertEqual(content.annotate(),
 
224
            [("origin1", "text1"), ("origin2", "text2")])
 
225
 
 
226
    def test_line_delta(self):
 
227
        content1 = self._make_content([("", "a"), ("", "b")])
 
228
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
229
        self.assertEqual(content1.line_delta(content2),
 
230
            [(1, 2, 2, [("", "a"), ("", "c")])])
 
231
 
 
232
    def test_line_delta_iter(self):
 
233
        content1 = self._make_content([("", "a"), ("", "b")])
 
234
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
235
        it = content1.line_delta_iter(content2)
 
236
        self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
 
237
        self.assertRaises(StopIteration, it.next)
 
238
 
 
239
 
 
240
class MockTransport(object):
 
241
 
 
242
    def __init__(self, file_lines=None):
 
243
        self.file_lines = file_lines
 
244
        self.calls = []
 
245
        # We have no base directory for the MockTransport
 
246
        self.base = ''
 
247
 
 
248
    def get(self, filename):
 
249
        if self.file_lines is None:
 
250
            raise NoSuchFile(filename)
 
251
        else:
 
252
            return StringIO("\n".join(self.file_lines))
 
253
 
 
254
    def readv(self, relpath, offsets):
 
255
        fp = self.get(relpath)
 
256
        for offset, size in offsets:
 
257
            fp.seek(offset)
 
258
            yield offset, fp.read(size)
 
259
 
 
260
    def __getattr__(self, name):
 
261
        def queue_call(*args, **kwargs):
 
262
            self.calls.append((name, args, kwargs))
 
263
        return queue_call
 
264
 
 
265
 
 
266
class MockReadvFailingTransport(MockTransport):
 
267
    """Fail in the middle of a readv() result.
 
268
 
 
269
    This Transport will successfully yield the first two requested hunks, but
 
270
    raise NoSuchFile for the rest.
 
271
    """
 
272
 
 
273
    def readv(self, relpath, offsets):
 
274
        count = 0
 
275
        for result in MockTransport.readv(self, relpath, offsets):
 
276
            count += 1
 
277
            # we use 2 because the first offset is the pack header, the second
 
278
            # is the first actual content requset
 
279
            if count > 2:
 
280
                raise errors.NoSuchFile(relpath)
 
281
            yield result
 
282
 
 
283
 
 
284
class KnitRecordAccessTestsMixin(object):
 
285
    """Tests for getting and putting knit records."""
 
286
 
 
287
    def test_add_raw_records(self):
 
288
        """Add_raw_records adds records retrievable later."""
 
289
        access = self.get_access()
 
290
        memos = access.add_raw_records([('key', 10)], '1234567890')
 
291
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
 
292
 
 
293
    def test_add_several_raw_records(self):
 
294
        """add_raw_records with many records and read some back."""
 
295
        access = self.get_access()
 
296
        memos = access.add_raw_records([('key', 10), ('key2', 2), ('key3', 5)],
 
297
            '12345678901234567')
 
298
        self.assertEqual(['1234567890', '12', '34567'],
 
299
            list(access.get_raw_records(memos)))
 
300
        self.assertEqual(['1234567890'],
 
301
            list(access.get_raw_records(memos[0:1])))
 
302
        self.assertEqual(['12'],
 
303
            list(access.get_raw_records(memos[1:2])))
 
304
        self.assertEqual(['34567'],
 
305
            list(access.get_raw_records(memos[2:3])))
 
306
        self.assertEqual(['1234567890', '34567'],
 
307
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
308
 
 
309
 
 
310
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
311
    """Tests for the .kndx implementation."""
 
312
 
 
313
    def get_access(self):
 
314
        """Get a .knit style access instance."""
 
315
        mapper = ConstantMapper("foo")
 
316
        access = _KnitKeyAccess(self.get_transport(), mapper)
 
317
        return access
 
318
 
 
319
 
 
320
class _TestException(Exception):
 
321
    """Just an exception for local tests to use."""
 
322
 
 
323
 
 
324
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
 
325
    """Tests for the pack based access."""
 
326
 
 
327
    def get_access(self):
 
328
        return self._get_access()[0]
 
329
 
 
330
    def _get_access(self, packname='packfile', index='FOO'):
 
331
        transport = self.get_transport()
 
332
        def write_data(bytes):
 
333
            transport.append_bytes(packname, bytes)
 
334
        writer = pack.ContainerWriter(write_data)
 
335
        writer.begin()
 
336
        access = _DirectPackAccess({})
 
337
        access.set_writer(writer, index, (transport, packname))
 
338
        return access, writer
 
339
 
 
340
    def make_pack_file(self):
 
341
        """Create a pack file with 2 records."""
 
342
        access, writer = self._get_access(packname='packname', index='foo')
 
343
        memos = []
 
344
        memos.extend(access.add_raw_records([('key1', 10)], '1234567890'))
 
345
        memos.extend(access.add_raw_records([('key2', 5)], '12345'))
 
346
        writer.end()
 
347
        return memos
 
348
 
 
349
    def make_vf_for_retrying(self):
 
350
        """Create 3 packs and a reload function.
 
351
 
 
352
        Originally, 2 pack files will have the data, but one will be missing.
 
353
        And then the third will be used in place of the first two if reload()
 
354
        is called.
 
355
 
 
356
        :return: (versioned_file, reload_counter)
 
357
            versioned_file  a KnitVersionedFiles using the packs for access
 
358
        """
 
359
        builder = self.make_branch_builder('.', format="1.9")
 
360
        builder.start_series()
 
361
        builder.build_snapshot('rev-1', None, [
 
362
            ('add', ('', 'root-id', 'directory', None)),
 
363
            ('add', ('file', 'file-id', 'file', 'content\nrev 1\n')),
 
364
            ])
 
365
        builder.build_snapshot('rev-2', ['rev-1'], [
 
366
            ('modify', ('file-id', 'content\nrev 2\n')),
 
367
            ])
 
368
        builder.build_snapshot('rev-3', ['rev-2'], [
 
369
            ('modify', ('file-id', 'content\nrev 3\n')),
 
370
            ])
 
371
        builder.finish_series()
 
372
        b = builder.get_branch()
 
373
        b.lock_write()
 
374
        self.addCleanup(b.unlock)
 
375
        # Pack these three revisions into another pack file, but don't remove
 
376
        # the originals
 
377
        repo = b.repository
 
378
        collection = repo._pack_collection
 
379
        collection.ensure_loaded()
 
380
        orig_packs = collection.packs
 
381
        packer = pack_repo.Packer(collection, orig_packs, '.testpack')
 
382
        new_pack = packer.pack()
 
383
        # forget about the new pack
 
384
        collection.reset()
 
385
        repo.refresh_data()
 
386
        vf = repo.revisions
 
387
        # Set up a reload() function that switches to using the new pack file
 
388
        new_index = new_pack.revision_index
 
389
        access_tuple = new_pack.access_tuple()
 
390
        reload_counter = [0, 0, 0]
 
391
        def reload():
 
392
            reload_counter[0] += 1
 
393
            if reload_counter[1] > 0:
 
394
                # We already reloaded, nothing more to do
 
395
                reload_counter[2] += 1
 
396
                return False
 
397
            reload_counter[1] += 1
 
398
            vf._index._graph_index._indices[:] = [new_index]
 
399
            vf._access._indices.clear()
 
400
            vf._access._indices[new_index] = access_tuple
 
401
            return True
 
402
        # Delete one of the pack files so the data will need to be reloaded. We
 
403
        # will delete the file with 'rev-2' in it
 
404
        trans, name = orig_packs[1].access_tuple()
 
405
        trans.delete(name)
 
406
        # We don't have the index trigger reloading because we want to test
 
407
        # that we reload when the .pack disappears
 
408
        vf._access._reload_func = reload
 
409
        return vf, reload_counter
 
410
 
 
411
    def make_reload_func(self, return_val=True):
 
412
        reload_called = [0]
 
413
        def reload():
 
414
            reload_called[0] += 1
 
415
            return return_val
 
416
        return reload_called, reload
 
417
 
 
418
    def make_retry_exception(self):
 
419
        # We raise a real exception so that sys.exc_info() is properly
 
420
        # populated
 
421
        try:
 
422
            raise _TestException('foobar')
 
423
        except _TestException, e:
 
424
            retry_exc = errors.RetryWithNewPacks(None, reload_occurred=False,
 
425
                                                 exc_info=sys.exc_info())
 
426
        return retry_exc
 
427
 
 
428
    def test_read_from_several_packs(self):
 
429
        access, writer = self._get_access()
 
430
        memos = []
 
431
        memos.extend(access.add_raw_records([('key', 10)], '1234567890'))
 
432
        writer.end()
 
433
        access, writer = self._get_access('pack2', 'FOOBAR')
 
434
        memos.extend(access.add_raw_records([('key', 5)], '12345'))
 
435
        writer.end()
 
436
        access, writer = self._get_access('pack3', 'BAZ')
 
437
        memos.extend(access.add_raw_records([('key', 5)], 'alpha'))
 
438
        writer.end()
 
439
        transport = self.get_transport()
 
440
        access = _DirectPackAccess({"FOO":(transport, 'packfile'),
 
441
            "FOOBAR":(transport, 'pack2'),
 
442
            "BAZ":(transport, 'pack3')})
 
443
        self.assertEqual(['1234567890', '12345', 'alpha'],
 
444
            list(access.get_raw_records(memos)))
 
445
        self.assertEqual(['1234567890'],
 
446
            list(access.get_raw_records(memos[0:1])))
 
447
        self.assertEqual(['12345'],
 
448
            list(access.get_raw_records(memos[1:2])))
 
449
        self.assertEqual(['alpha'],
 
450
            list(access.get_raw_records(memos[2:3])))
 
451
        self.assertEqual(['1234567890', 'alpha'],
 
452
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
 
453
 
 
454
    def test_set_writer(self):
 
455
        """The writer should be settable post construction."""
 
456
        access = _DirectPackAccess({})
 
457
        transport = self.get_transport()
 
458
        packname = 'packfile'
 
459
        index = 'foo'
 
460
        def write_data(bytes):
 
461
            transport.append_bytes(packname, bytes)
 
462
        writer = pack.ContainerWriter(write_data)
 
463
        writer.begin()
 
464
        access.set_writer(writer, index, (transport, packname))
 
465
        memos = access.add_raw_records([('key', 10)], '1234567890')
 
466
        writer.end()
 
467
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
 
468
 
 
469
    def test_missing_index_raises_retry(self):
 
470
        memos = self.make_pack_file()
 
471
        transport = self.get_transport()
 
472
        reload_called, reload_func = self.make_reload_func()
 
473
        # Note that the index key has changed from 'foo' to 'bar'
 
474
        access = _DirectPackAccess({'bar':(transport, 'packname')},
 
475
                                   reload_func=reload_func)
 
476
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
477
                                  access.get_raw_records, memos)
 
478
        # Because a key was passed in which does not match our index list, we
 
479
        # assume that the listing was already reloaded
 
480
        self.assertTrue(e.reload_occurred)
 
481
        self.assertIsInstance(e.exc_info, tuple)
 
482
        self.assertIs(e.exc_info[0], KeyError)
 
483
        self.assertIsInstance(e.exc_info[1], KeyError)
 
484
 
 
485
    def test_missing_index_raises_key_error_with_no_reload(self):
 
486
        memos = self.make_pack_file()
 
487
        transport = self.get_transport()
 
488
        # Note that the index key has changed from 'foo' to 'bar'
 
489
        access = _DirectPackAccess({'bar':(transport, 'packname')})
 
490
        e = self.assertListRaises(KeyError, access.get_raw_records, memos)
 
491
 
 
492
    def test_missing_file_raises_retry(self):
 
493
        memos = self.make_pack_file()
 
494
        transport = self.get_transport()
 
495
        reload_called, reload_func = self.make_reload_func()
 
496
        # Note that the 'filename' has been changed to 'different-packname'
 
497
        access = _DirectPackAccess({'foo':(transport, 'different-packname')},
 
498
                                   reload_func=reload_func)
 
499
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
500
                                  access.get_raw_records, memos)
 
501
        # The file has gone missing, so we assume we need to reload
 
502
        self.assertFalse(e.reload_occurred)
 
503
        self.assertIsInstance(e.exc_info, tuple)
 
504
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
505
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
506
        self.assertEqual('different-packname', e.exc_info[1].path)
 
507
 
 
508
    def test_missing_file_raises_no_such_file_with_no_reload(self):
 
509
        memos = self.make_pack_file()
 
510
        transport = self.get_transport()
 
511
        # Note that the 'filename' has been changed to 'different-packname'
 
512
        access = _DirectPackAccess({'foo':(transport, 'different-packname')})
 
513
        e = self.assertListRaises(errors.NoSuchFile,
 
514
                                  access.get_raw_records, memos)
 
515
 
 
516
    def test_failing_readv_raises_retry(self):
 
517
        memos = self.make_pack_file()
 
518
        transport = self.get_transport()
 
519
        failing_transport = MockReadvFailingTransport(
 
520
                                [transport.get_bytes('packname')])
 
521
        reload_called, reload_func = self.make_reload_func()
 
522
        access = _DirectPackAccess({'foo':(failing_transport, 'packname')},
 
523
                                   reload_func=reload_func)
 
524
        # Asking for a single record will not trigger the Mock failure
 
525
        self.assertEqual(['1234567890'],
 
526
            list(access.get_raw_records(memos[:1])))
 
527
        self.assertEqual(['12345'],
 
528
            list(access.get_raw_records(memos[1:2])))
 
529
        # A multiple offset readv() will fail mid-way through
 
530
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
531
                                  access.get_raw_records, memos)
 
532
        # The file has gone missing, so we assume we need to reload
 
533
        self.assertFalse(e.reload_occurred)
 
534
        self.assertIsInstance(e.exc_info, tuple)
 
535
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
536
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
537
        self.assertEqual('packname', e.exc_info[1].path)
 
538
 
 
539
    def test_failing_readv_raises_no_such_file_with_no_reload(self):
 
540
        memos = self.make_pack_file()
 
541
        transport = self.get_transport()
 
542
        failing_transport = MockReadvFailingTransport(
 
543
                                [transport.get_bytes('packname')])
 
544
        reload_called, reload_func = self.make_reload_func()
 
545
        access = _DirectPackAccess({'foo':(failing_transport, 'packname')})
 
546
        # Asking for a single record will not trigger the Mock failure
 
547
        self.assertEqual(['1234567890'],
 
548
            list(access.get_raw_records(memos[:1])))
 
549
        self.assertEqual(['12345'],
 
550
            list(access.get_raw_records(memos[1:2])))
 
551
        # A multiple offset readv() will fail mid-way through
 
552
        e = self.assertListRaises(errors.NoSuchFile,
 
553
                                  access.get_raw_records, memos)
 
554
 
 
555
    def test_reload_or_raise_no_reload(self):
 
556
        access = _DirectPackAccess({}, reload_func=None)
 
557
        retry_exc = self.make_retry_exception()
 
558
        # Without a reload_func, we will just re-raise the original exception
 
559
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
560
 
 
561
    def test_reload_or_raise_reload_changed(self):
 
562
        reload_called, reload_func = self.make_reload_func(return_val=True)
 
563
        access = _DirectPackAccess({}, reload_func=reload_func)
 
564
        retry_exc = self.make_retry_exception()
 
565
        access.reload_or_raise(retry_exc)
 
566
        self.assertEqual([1], reload_called)
 
567
        retry_exc.reload_occurred=True
 
568
        access.reload_or_raise(retry_exc)
 
569
        self.assertEqual([2], reload_called)
 
570
 
 
571
    def test_reload_or_raise_reload_no_change(self):
 
572
        reload_called, reload_func = self.make_reload_func(return_val=False)
 
573
        access = _DirectPackAccess({}, reload_func=reload_func)
 
574
        retry_exc = self.make_retry_exception()
 
575
        # If reload_occurred is False, then we consider it an error to have
 
576
        # reload_func() return False (no changes).
 
577
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
578
        self.assertEqual([1], reload_called)
 
579
        retry_exc.reload_occurred=True
 
580
        # If reload_occurred is True, then we assume nothing changed because
 
581
        # it had changed earlier, but didn't change again
 
582
        access.reload_or_raise(retry_exc)
 
583
        self.assertEqual([2], reload_called)
 
584
 
 
585
    def test_annotate_retries(self):
 
586
        vf, reload_counter = self.make_vf_for_retrying()
 
587
        # It is a little bit bogus to annotate the Revision VF, but it works,
 
588
        # as we have ancestry stored there
 
589
        key = ('rev-3',)
 
590
        reload_lines = vf.annotate(key)
 
591
        self.assertEqual([1, 1, 0], reload_counter)
 
592
        plain_lines = vf.annotate(key)
 
593
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
594
        if reload_lines != plain_lines:
 
595
            self.fail('Annotation was not identical with reloading.')
 
596
        # Now delete the packs-in-use, which should trigger another reload, but
 
597
        # this time we just raise an exception because we can't recover
 
598
        for trans, name in vf._access._indices.itervalues():
 
599
            trans.delete(name)
 
600
        self.assertRaises(errors.NoSuchFile, vf.annotate, key)
 
601
        self.assertEqual([2, 1, 1], reload_counter)
 
602
 
 
603
    def test__get_record_map_retries(self):
 
604
        vf, reload_counter = self.make_vf_for_retrying()
 
605
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
606
        records = vf._get_record_map(keys)
 
607
        self.assertEqual(keys, sorted(records.keys()))
 
608
        self.assertEqual([1, 1, 0], reload_counter)
 
609
        # Now delete the packs-in-use, which should trigger another reload, but
 
610
        # this time we just raise an exception because we can't recover
 
611
        for trans, name in vf._access._indices.itervalues():
 
612
            trans.delete(name)
 
613
        self.assertRaises(errors.NoSuchFile, vf._get_record_map, keys)
 
614
        self.assertEqual([2, 1, 1], reload_counter)
 
615
 
 
616
    def test_get_record_stream_retries(self):
 
617
        vf, reload_counter = self.make_vf_for_retrying()
 
618
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
619
        record_stream = vf.get_record_stream(keys, 'topological', False)
 
620
        record = record_stream.next()
 
621
        self.assertEqual(('rev-1',), record.key)
 
622
        self.assertEqual([0, 0, 0], reload_counter)
 
623
        record = record_stream.next()
 
624
        self.assertEqual(('rev-2',), record.key)
 
625
        self.assertEqual([1, 1, 0], reload_counter)
 
626
        record = record_stream.next()
 
627
        self.assertEqual(('rev-3',), record.key)
 
628
        self.assertEqual([1, 1, 0], reload_counter)
 
629
        # Now delete all pack files, and see that we raise the right error
 
630
        for trans, name in vf._access._indices.itervalues():
 
631
            trans.delete(name)
 
632
        self.assertListRaises(errors.NoSuchFile,
 
633
            vf.get_record_stream, keys, 'topological', False)
 
634
 
 
635
    def test_iter_lines_added_or_present_in_keys_retries(self):
 
636
        vf, reload_counter = self.make_vf_for_retrying()
 
637
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
638
        # Unfortunately, iter_lines_added_or_present_in_keys iterates the
 
639
        # result in random order (determined by the iteration order from a
 
640
        # set()), so we don't have any solid way to trigger whether data is
 
641
        # read before or after. However we tried to delete the middle node to
 
642
        # exercise the code well.
 
643
        # What we care about is that all lines are always yielded, but not
 
644
        # duplicated
 
645
        count = 0
 
646
        reload_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
647
        self.assertEqual([1, 1, 0], reload_counter)
 
648
        # Now do it again, to make sure the result is equivalent
 
649
        plain_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
650
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
651
        self.assertEqual(plain_lines, reload_lines)
 
652
        self.assertEqual(21, len(plain_lines))
 
653
        # Now delete all pack files, and see that we raise the right error
 
654
        for trans, name in vf._access._indices.itervalues():
 
655
            trans.delete(name)
 
656
        self.assertListRaises(errors.NoSuchFile,
 
657
            vf.iter_lines_added_or_present_in_keys, keys)
 
658
        self.assertEqual([2, 1, 1], reload_counter)
 
659
 
 
660
    def test_get_record_stream_yields_disk_sorted_order(self):
 
661
        # if we get 'unordered' pick a semi-optimal order for reading. The
 
662
        # order should be grouped by pack file, and then by position in file
 
663
        repo = self.make_repository('test', format='pack-0.92')
 
664
        repo.lock_write()
 
665
        self.addCleanup(repo.unlock)
 
666
        repo.start_write_group()
 
667
        vf = repo.texts
 
668
        vf.add_lines(('f-id', 'rev-5'), [('f-id', 'rev-4')], ['lines\n'])
 
669
        vf.add_lines(('f-id', 'rev-1'), [], ['lines\n'])
 
670
        vf.add_lines(('f-id', 'rev-2'), [('f-id', 'rev-1')], ['lines\n'])
 
671
        repo.commit_write_group()
 
672
        # We inserted them as rev-5, rev-1, rev-2, we should get them back in
 
673
        # the same order
 
674
        stream = vf.get_record_stream([('f-id', 'rev-1'), ('f-id', 'rev-5'),
 
675
                                       ('f-id', 'rev-2')], 'unordered', False)
 
676
        keys = [r.key for r in stream]
 
677
        self.assertEqual([('f-id', 'rev-5'), ('f-id', 'rev-1'),
 
678
                          ('f-id', 'rev-2')], keys)
 
679
        repo.start_write_group()
 
680
        vf.add_lines(('f-id', 'rev-4'), [('f-id', 'rev-3')], ['lines\n'])
 
681
        vf.add_lines(('f-id', 'rev-3'), [('f-id', 'rev-2')], ['lines\n'])
 
682
        vf.add_lines(('f-id', 'rev-6'), [('f-id', 'rev-5')], ['lines\n'])
 
683
        repo.commit_write_group()
 
684
        # Request in random order, to make sure the output order isn't based on
 
685
        # the request
 
686
        request_keys = set(('f-id', 'rev-%d' % i) for i in range(1, 7))
 
687
        stream = vf.get_record_stream(request_keys, 'unordered', False)
 
688
        keys = [r.key for r in stream]
 
689
        # We want to get the keys back in disk order, but it doesn't matter
 
690
        # which pack we read from first. So this can come back in 2 orders
 
691
        alt1 = [('f-id', 'rev-%d' % i) for i in [4, 3, 6, 5, 1, 2]]
 
692
        alt2 = [('f-id', 'rev-%d' % i) for i in [5, 1, 2, 4, 3, 6]]
 
693
        if keys != alt1 and keys != alt2:
 
694
            self.fail('Returned key order did not match either expected order.'
 
695
                      ' expected %s or %s, not %s'
 
696
                      % (alt1, alt2, keys))
 
697
 
 
698
 
 
699
class LowLevelKnitDataTests(TestCase):
 
700
 
 
701
    def create_gz_content(self, text):
 
702
        sio = StringIO()
 
703
        gz_file = gzip.GzipFile(mode='wb', fileobj=sio)
 
704
        gz_file.write(text)
 
705
        gz_file.close()
 
706
        return sio.getvalue()
 
707
 
 
708
    def make_multiple_records(self):
 
709
        """Create the content for multiple records."""
 
710
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
711
        total_txt = []
 
712
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
713
                                        'foo\n'
 
714
                                        'bar\n'
 
715
                                        'end rev-id-1\n'
 
716
                                        % (sha1sum,))
 
717
        record_1 = (0, len(gz_txt), sha1sum)
 
718
        total_txt.append(gz_txt)
 
719
        sha1sum = osutils.sha('baz\n').hexdigest()
 
720
        gz_txt = self.create_gz_content('version rev-id-2 1 %s\n'
 
721
                                        'baz\n'
 
722
                                        'end rev-id-2\n'
 
723
                                        % (sha1sum,))
 
724
        record_2 = (record_1[1], len(gz_txt), sha1sum)
 
725
        total_txt.append(gz_txt)
 
726
        return total_txt, record_1, record_2
 
727
 
 
728
    def test_valid_knit_data(self):
 
729
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
730
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
731
                                        'foo\n'
 
732
                                        'bar\n'
 
733
                                        'end rev-id-1\n'
 
734
                                        % (sha1sum,))
 
735
        transport = MockTransport([gz_txt])
 
736
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
737
        knit = KnitVersionedFiles(None, access)
 
738
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
739
 
 
740
        contents = list(knit._read_records_iter(records))
 
741
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'],
 
742
            '4e48e2c9a3d2ca8a708cb0cc545700544efb5021')], contents)
 
743
 
 
744
        raw_contents = list(knit._read_records_iter_raw(records))
 
745
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
 
746
 
 
747
    def test_multiple_records_valid(self):
 
748
        total_txt, record_1, record_2 = self.make_multiple_records()
 
749
        transport = MockTransport([''.join(total_txt)])
 
750
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
751
        knit = KnitVersionedFiles(None, access)
 
752
        records = [(('rev-id-1',), (('rev-id-1',), record_1[0], record_1[1])),
 
753
                   (('rev-id-2',), (('rev-id-2',), record_2[0], record_2[1]))]
 
754
 
 
755
        contents = list(knit._read_records_iter(records))
 
756
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'], record_1[2]),
 
757
                          (('rev-id-2',), ['baz\n'], record_2[2])],
 
758
                         contents)
 
759
 
 
760
        raw_contents = list(knit._read_records_iter_raw(records))
 
761
        self.assertEqual([(('rev-id-1',), total_txt[0], record_1[2]),
 
762
                          (('rev-id-2',), total_txt[1], record_2[2])],
 
763
                         raw_contents)
 
764
 
 
765
    def test_not_enough_lines(self):
 
766
        sha1sum = osutils.sha('foo\n').hexdigest()
 
767
        # record says 2 lines data says 1
 
768
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
769
                                        'foo\n'
 
770
                                        'end rev-id-1\n'
 
771
                                        % (sha1sum,))
 
772
        transport = MockTransport([gz_txt])
 
773
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
774
        knit = KnitVersionedFiles(None, access)
 
775
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
776
        self.assertRaises(errors.KnitCorrupt, list,
 
777
            knit._read_records_iter(records))
 
778
 
 
779
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
780
        raw_contents = list(knit._read_records_iter_raw(records))
 
781
        self.assertEqual([(('rev-id-1',),  gz_txt, sha1sum)], raw_contents)
 
782
 
 
783
    def test_too_many_lines(self):
 
784
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
785
        # record says 1 lines data says 2
 
786
        gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
 
787
                                        'foo\n'
 
788
                                        'bar\n'
 
789
                                        'end rev-id-1\n'
 
790
                                        % (sha1sum,))
 
791
        transport = MockTransport([gz_txt])
 
792
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
793
        knit = KnitVersionedFiles(None, access)
 
794
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
795
        self.assertRaises(errors.KnitCorrupt, list,
 
796
            knit._read_records_iter(records))
 
797
 
 
798
        # read_records_iter_raw won't detect that sort of mismatch/corruption
 
799
        raw_contents = list(knit._read_records_iter_raw(records))
 
800
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
 
801
 
 
802
    def test_mismatched_version_id(self):
 
803
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
804
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
805
                                        'foo\n'
 
806
                                        'bar\n'
 
807
                                        'end rev-id-1\n'
 
808
                                        % (sha1sum,))
 
809
        transport = MockTransport([gz_txt])
 
810
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
811
        knit = KnitVersionedFiles(None, access)
 
812
        # We are asking for rev-id-2, but the data is rev-id-1
 
813
        records = [(('rev-id-2',), (('rev-id-2',), 0, len(gz_txt)))]
 
814
        self.assertRaises(errors.KnitCorrupt, list,
 
815
            knit._read_records_iter(records))
 
816
 
 
817
        # read_records_iter_raw detects mismatches in the header
 
818
        self.assertRaises(errors.KnitCorrupt, list,
 
819
            knit._read_records_iter_raw(records))
 
820
 
 
821
    def test_uncompressed_data(self):
 
822
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
823
        txt = ('version rev-id-1 2 %s\n'
 
824
               'foo\n'
 
825
               'bar\n'
 
826
               'end rev-id-1\n'
 
827
               % (sha1sum,))
 
828
        transport = MockTransport([txt])
 
829
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
830
        knit = KnitVersionedFiles(None, access)
 
831
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(txt)))]
 
832
 
 
833
        # We don't have valid gzip data ==> corrupt
 
834
        self.assertRaises(errors.KnitCorrupt, list,
 
835
            knit._read_records_iter(records))
 
836
 
 
837
        # read_records_iter_raw will notice the bad data
 
838
        self.assertRaises(errors.KnitCorrupt, list,
 
839
            knit._read_records_iter_raw(records))
 
840
 
 
841
    def test_corrupted_data(self):
 
842
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
843
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
844
                                        'foo\n'
 
845
                                        'bar\n'
 
846
                                        'end rev-id-1\n'
 
847
                                        % (sha1sum,))
 
848
        # Change 2 bytes in the middle to \xff
 
849
        gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
 
850
        transport = MockTransport([gz_txt])
 
851
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
852
        knit = KnitVersionedFiles(None, access)
 
853
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
854
        self.assertRaises(errors.KnitCorrupt, list,
 
855
            knit._read_records_iter(records))
 
856
        # read_records_iter_raw will barf on bad gz data
 
857
        self.assertRaises(errors.KnitCorrupt, list,
 
858
            knit._read_records_iter_raw(records))
 
859
 
 
860
 
 
861
class LowLevelKnitIndexTests(TestCase):
 
862
 
 
863
    def get_knit_index(self, transport, name, mode):
 
864
        mapper = ConstantMapper(name)
 
865
        orig = knit._load_data
 
866
        def reset():
 
867
            knit._load_data = orig
 
868
        self.addCleanup(reset)
 
869
        from bzrlib._knit_load_data_py import _load_data_py
 
870
        knit._load_data = _load_data_py
 
871
        allow_writes = lambda: 'w' in mode
 
872
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
 
873
 
 
874
    def test_create_file(self):
 
875
        transport = MockTransport()
 
876
        index = self.get_knit_index(transport, "filename", "w")
 
877
        index.keys()
 
878
        call = transport.calls.pop(0)
 
879
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
880
        self.assertEqual('put_file_non_atomic', call[0])
 
881
        self.assertEqual('filename.kndx', call[1][0])
 
882
        # With no history, _KndxIndex writes a new index:
 
883
        self.assertEqual(_KndxIndex.HEADER,
 
884
            call[1][1].getvalue())
 
885
        self.assertEqual({'create_parent_dir': True}, call[2])
 
886
 
 
887
    def test_read_utf8_version_id(self):
 
888
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
889
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
890
        transport = MockTransport([
 
891
            _KndxIndex.HEADER,
 
892
            '%s option 0 1 :' % (utf8_revision_id,)
 
893
            ])
 
894
        index = self.get_knit_index(transport, "filename", "r")
 
895
        # _KndxIndex is a private class, and deals in utf8 revision_ids, not
 
896
        # Unicode revision_ids.
 
897
        self.assertEqual({(utf8_revision_id,):()},
 
898
            index.get_parent_map(index.keys()))
 
899
        self.assertFalse((unicode_revision_id,) in index.keys())
 
900
 
 
901
    def test_read_utf8_parents(self):
 
902
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
903
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
904
        transport = MockTransport([
 
905
            _KndxIndex.HEADER,
 
906
            "version option 0 1 .%s :" % (utf8_revision_id,)
 
907
            ])
 
908
        index = self.get_knit_index(transport, "filename", "r")
 
909
        self.assertEqual({("version",):((utf8_revision_id,),)},
 
910
            index.get_parent_map(index.keys()))
 
911
 
 
912
    def test_read_ignore_corrupted_lines(self):
 
913
        transport = MockTransport([
 
914
            _KndxIndex.HEADER,
 
915
            "corrupted",
 
916
            "corrupted options 0 1 .b .c ",
 
917
            "version options 0 1 :"
 
918
            ])
 
919
        index = self.get_knit_index(transport, "filename", "r")
 
920
        self.assertEqual(1, len(index.keys()))
 
921
        self.assertEqual(set([("version",)]), index.keys())
 
922
 
 
923
    def test_read_corrupted_header(self):
 
924
        transport = MockTransport(['not a bzr knit index header\n'])
 
925
        index = self.get_knit_index(transport, "filename", "r")
 
926
        self.assertRaises(KnitHeaderError, index.keys)
 
927
 
 
928
    def test_read_duplicate_entries(self):
 
929
        transport = MockTransport([
 
930
            _KndxIndex.HEADER,
 
931
            "parent options 0 1 :",
 
932
            "version options1 0 1 0 :",
 
933
            "version options2 1 2 .other :",
 
934
            "version options3 3 4 0 .other :"
 
935
            ])
 
936
        index = self.get_knit_index(transport, "filename", "r")
 
937
        self.assertEqual(2, len(index.keys()))
 
938
        # check that the index used is the first one written. (Specific
 
939
        # to KnitIndex style indices.
 
940
        self.assertEqual("1", index._dictionary_compress([("version",)]))
 
941
        self.assertEqual((("version",), 3, 4), index.get_position(("version",)))
 
942
        self.assertEqual(["options3"], index.get_options(("version",)))
 
943
        self.assertEqual({("version",):(("parent",), ("other",))},
 
944
            index.get_parent_map([("version",)]))
 
945
 
 
946
    def test_read_compressed_parents(self):
 
947
        transport = MockTransport([
 
948
            _KndxIndex.HEADER,
 
949
            "a option 0 1 :",
 
950
            "b option 0 1 0 :",
 
951
            "c option 0 1 1 0 :",
 
952
            ])
 
953
        index = self.get_knit_index(transport, "filename", "r")
 
954
        self.assertEqual({("b",):(("a",),), ("c",):(("b",), ("a",))},
 
955
            index.get_parent_map([("b",), ("c",)]))
 
956
 
 
957
    def test_write_utf8_version_id(self):
 
958
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
959
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
960
        transport = MockTransport([
 
961
            _KndxIndex.HEADER
 
962
            ])
 
963
        index = self.get_knit_index(transport, "filename", "r")
 
964
        index.add_records([
 
965
            ((utf8_revision_id,), ["option"], ((utf8_revision_id,), 0, 1), [])])
 
966
        call = transport.calls.pop(0)
 
967
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
968
        self.assertEqual('put_file_non_atomic', call[0])
 
969
        self.assertEqual('filename.kndx', call[1][0])
 
970
        # With no history, _KndxIndex writes a new index:
 
971
        self.assertEqual(_KndxIndex.HEADER +
 
972
            "\n%s option 0 1  :" % (utf8_revision_id,),
 
973
            call[1][1].getvalue())
 
974
        self.assertEqual({'create_parent_dir': True}, call[2])
 
975
 
 
976
    def test_write_utf8_parents(self):
 
977
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
 
978
        utf8_revision_id = unicode_revision_id.encode('utf-8')
 
979
        transport = MockTransport([
 
980
            _KndxIndex.HEADER
 
981
            ])
 
982
        index = self.get_knit_index(transport, "filename", "r")
 
983
        index.add_records([
 
984
            (("version",), ["option"], (("version",), 0, 1), [(utf8_revision_id,)])])
 
985
        call = transport.calls.pop(0)
 
986
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
987
        self.assertEqual('put_file_non_atomic', call[0])
 
988
        self.assertEqual('filename.kndx', call[1][0])
 
989
        # With no history, _KndxIndex writes a new index:
 
990
        self.assertEqual(_KndxIndex.HEADER +
 
991
            "\nversion option 0 1 .%s :" % (utf8_revision_id,),
 
992
            call[1][1].getvalue())
 
993
        self.assertEqual({'create_parent_dir': True}, call[2])
 
994
 
 
995
    def test_keys(self):
 
996
        transport = MockTransport([
 
997
            _KndxIndex.HEADER
 
998
            ])
 
999
        index = self.get_knit_index(transport, "filename", "r")
 
1000
 
 
1001
        self.assertEqual(set(), index.keys())
 
1002
 
 
1003
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
 
1004
        self.assertEqual(set([("a",)]), index.keys())
 
1005
 
 
1006
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
 
1007
        self.assertEqual(set([("a",)]), index.keys())
 
1008
 
 
1009
        index.add_records([(("b",), ["option"], (("b",), 0, 1), [])])
 
1010
        self.assertEqual(set([("a",), ("b",)]), index.keys())
 
1011
 
 
1012
    def add_a_b(self, index, random_id=None):
 
1013
        kwargs = {}
 
1014
        if random_id is not None:
 
1015
            kwargs["random_id"] = random_id
 
1016
        index.add_records([
 
1017
            (("a",), ["option"], (("a",), 0, 1), [("b",)]),
 
1018
            (("a",), ["opt"], (("a",), 1, 2), [("c",)]),
 
1019
            (("b",), ["option"], (("b",), 2, 3), [("a",)])
 
1020
            ], **kwargs)
 
1021
 
 
1022
    def assertIndexIsAB(self, index):
 
1023
        self.assertEqual({
 
1024
            ('a',): (('c',),),
 
1025
            ('b',): (('a',),),
 
1026
            },
 
1027
            index.get_parent_map(index.keys()))
 
1028
        self.assertEqual((("a",), 1, 2), index.get_position(("a",)))
 
1029
        self.assertEqual((("b",), 2, 3), index.get_position(("b",)))
 
1030
        self.assertEqual(["opt"], index.get_options(("a",)))
 
1031
 
 
1032
    def test_add_versions(self):
 
1033
        transport = MockTransport([
 
1034
            _KndxIndex.HEADER
 
1035
            ])
 
1036
        index = self.get_knit_index(transport, "filename", "r")
 
1037
 
 
1038
        self.add_a_b(index)
 
1039
        call = transport.calls.pop(0)
 
1040
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
1041
        self.assertEqual('put_file_non_atomic', call[0])
 
1042
        self.assertEqual('filename.kndx', call[1][0])
 
1043
        # With no history, _KndxIndex writes a new index:
 
1044
        self.assertEqual(
 
1045
            _KndxIndex.HEADER +
 
1046
            "\na option 0 1 .b :"
 
1047
            "\na opt 1 2 .c :"
 
1048
            "\nb option 2 3 0 :",
 
1049
            call[1][1].getvalue())
 
1050
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1051
        self.assertIndexIsAB(index)
 
1052
 
 
1053
    def test_add_versions_random_id_is_accepted(self):
 
1054
        transport = MockTransport([
 
1055
            _KndxIndex.HEADER
 
1056
            ])
 
1057
        index = self.get_knit_index(transport, "filename", "r")
 
1058
        self.add_a_b(index, random_id=True)
 
1059
 
 
1060
    def test_delay_create_and_add_versions(self):
 
1061
        transport = MockTransport()
 
1062
 
 
1063
        index = self.get_knit_index(transport, "filename", "w")
 
1064
        # dir_mode=0777)
 
1065
        self.assertEqual([], transport.calls)
 
1066
        self.add_a_b(index)
 
1067
        #self.assertEqual(
 
1068
        #[    {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
 
1069
        #    kwargs)
 
1070
        # Two calls: one during which we load the existing index (and when its
 
1071
        # missing create it), then a second where we write the contents out.
 
1072
        self.assertEqual(2, len(transport.calls))
 
1073
        call = transport.calls.pop(0)
 
1074
        self.assertEqual('put_file_non_atomic', call[0])
 
1075
        self.assertEqual('filename.kndx', call[1][0])
 
1076
        # With no history, _KndxIndex writes a new index:
 
1077
        self.assertEqual(_KndxIndex.HEADER, call[1][1].getvalue())
 
1078
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1079
        call = transport.calls.pop(0)
 
1080
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
1081
        self.assertEqual('put_file_non_atomic', call[0])
 
1082
        self.assertEqual('filename.kndx', call[1][0])
 
1083
        # With no history, _KndxIndex writes a new index:
 
1084
        self.assertEqual(
 
1085
            _KndxIndex.HEADER +
 
1086
            "\na option 0 1 .b :"
 
1087
            "\na opt 1 2 .c :"
 
1088
            "\nb option 2 3 0 :",
 
1089
            call[1][1].getvalue())
 
1090
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1091
 
 
1092
    def assertTotalBuildSize(self, size, keys, positions):
 
1093
        self.assertEqual(size,
 
1094
                         knit._get_total_build_size(None, keys, positions))
 
1095
 
 
1096
    def test__get_total_build_size(self):
 
1097
        positions = {
 
1098
            ('a',): (('fulltext', False), (('a',), 0, 100), None),
 
1099
            ('b',): (('line-delta', False), (('b',), 100, 21), ('a',)),
 
1100
            ('c',): (('line-delta', False), (('c',), 121, 35), ('b',)),
 
1101
            ('d',): (('line-delta', False), (('d',), 156, 12), ('b',)),
 
1102
            }
 
1103
        self.assertTotalBuildSize(100, [('a',)], positions)
 
1104
        self.assertTotalBuildSize(121, [('b',)], positions)
 
1105
        # c needs both a & b
 
1106
        self.assertTotalBuildSize(156, [('c',)], positions)
 
1107
        # we shouldn't count 'b' twice
 
1108
        self.assertTotalBuildSize(156, [('b',), ('c',)], positions)
 
1109
        self.assertTotalBuildSize(133, [('d',)], positions)
 
1110
        self.assertTotalBuildSize(168, [('c',), ('d',)], positions)
 
1111
 
 
1112
    def test_get_position(self):
 
1113
        transport = MockTransport([
 
1114
            _KndxIndex.HEADER,
 
1115
            "a option 0 1 :",
 
1116
            "b option 1 2 :"
 
1117
            ])
 
1118
        index = self.get_knit_index(transport, "filename", "r")
 
1119
 
 
1120
        self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
 
1121
        self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
 
1122
 
 
1123
    def test_get_method(self):
 
1124
        transport = MockTransport([
 
1125
            _KndxIndex.HEADER,
 
1126
            "a fulltext,unknown 0 1 :",
 
1127
            "b unknown,line-delta 1 2 :",
 
1128
            "c bad 3 4 :"
 
1129
            ])
 
1130
        index = self.get_knit_index(transport, "filename", "r")
 
1131
 
 
1132
        self.assertEqual("fulltext", index.get_method("a"))
 
1133
        self.assertEqual("line-delta", index.get_method("b"))
 
1134
        self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
 
1135
 
 
1136
    def test_get_options(self):
 
1137
        transport = MockTransport([
 
1138
            _KndxIndex.HEADER,
 
1139
            "a opt1 0 1 :",
 
1140
            "b opt2,opt3 1 2 :"
 
1141
            ])
 
1142
        index = self.get_knit_index(transport, "filename", "r")
 
1143
 
 
1144
        self.assertEqual(["opt1"], index.get_options("a"))
 
1145
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
 
1146
 
 
1147
    def test_get_parent_map(self):
 
1148
        transport = MockTransport([
 
1149
            _KndxIndex.HEADER,
 
1150
            "a option 0 1 :",
 
1151
            "b option 1 2 0 .c :",
 
1152
            "c option 1 2 1 0 .e :"
 
1153
            ])
 
1154
        index = self.get_knit_index(transport, "filename", "r")
 
1155
 
 
1156
        self.assertEqual({
 
1157
            ("a",):(),
 
1158
            ("b",):(("a",), ("c",)),
 
1159
            ("c",):(("b",), ("a",), ("e",)),
 
1160
            }, index.get_parent_map(index.keys()))
 
1161
 
 
1162
    def test_impossible_parent(self):
 
1163
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
 
1164
        transport = MockTransport([
 
1165
            _KndxIndex.HEADER,
 
1166
            "a option 0 1 :",
 
1167
            "b option 0 1 4 :"  # We don't have a 4th record
 
1168
            ])
 
1169
        index = self.get_knit_index(transport, 'filename', 'r')
 
1170
        try:
 
1171
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1172
        except TypeError, e:
 
1173
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1174
                           ' not exceptions.IndexError')
 
1175
                and sys.version_info[0:2] >= (2,5)):
 
1176
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1177
                                  ' raising new style exceptions with python'
 
1178
                                  ' >=2.5')
 
1179
            else:
 
1180
                raise
 
1181
 
 
1182
    def test_corrupted_parent(self):
 
1183
        transport = MockTransport([
 
1184
            _KndxIndex.HEADER,
 
1185
            "a option 0 1 :",
 
1186
            "b option 0 1 :",
 
1187
            "c option 0 1 1v :", # Can't have a parent of '1v'
 
1188
            ])
 
1189
        index = self.get_knit_index(transport, 'filename', 'r')
 
1190
        try:
 
1191
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1192
        except TypeError, e:
 
1193
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1194
                           ' not exceptions.ValueError')
 
1195
                and sys.version_info[0:2] >= (2,5)):
 
1196
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1197
                                  ' raising new style exceptions with python'
 
1198
                                  ' >=2.5')
 
1199
            else:
 
1200
                raise
 
1201
 
 
1202
    def test_corrupted_parent_in_list(self):
 
1203
        transport = MockTransport([
 
1204
            _KndxIndex.HEADER,
 
1205
            "a option 0 1 :",
 
1206
            "b option 0 1 :",
 
1207
            "c option 0 1 1 v :", # Can't have a parent of 'v'
 
1208
            ])
 
1209
        index = self.get_knit_index(transport, 'filename', 'r')
 
1210
        try:
 
1211
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1212
        except TypeError, e:
 
1213
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1214
                           ' not exceptions.ValueError')
 
1215
                and sys.version_info[0:2] >= (2,5)):
 
1216
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1217
                                  ' raising new style exceptions with python'
 
1218
                                  ' >=2.5')
 
1219
            else:
 
1220
                raise
 
1221
 
 
1222
    def test_invalid_position(self):
 
1223
        transport = MockTransport([
 
1224
            _KndxIndex.HEADER,
 
1225
            "a option 1v 1 :",
 
1226
            ])
 
1227
        index = self.get_knit_index(transport, 'filename', 'r')
 
1228
        try:
 
1229
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1230
        except TypeError, e:
 
1231
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1232
                           ' not exceptions.ValueError')
 
1233
                and sys.version_info[0:2] >= (2,5)):
 
1234
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1235
                                  ' raising new style exceptions with python'
 
1236
                                  ' >=2.5')
 
1237
            else:
 
1238
                raise
 
1239
 
 
1240
    def test_invalid_size(self):
 
1241
        transport = MockTransport([
 
1242
            _KndxIndex.HEADER,
 
1243
            "a option 1 1v :",
 
1244
            ])
 
1245
        index = self.get_knit_index(transport, 'filename', 'r')
 
1246
        try:
 
1247
            self.assertRaises(errors.KnitCorrupt, index.keys)
 
1248
        except TypeError, e:
 
1249
            if (str(e) == ('exceptions must be strings, classes, or instances,'
 
1250
                           ' not exceptions.ValueError')
 
1251
                and sys.version_info[0:2] >= (2,5)):
 
1252
                self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
 
1253
                                  ' raising new style exceptions with python'
 
1254
                                  ' >=2.5')
 
1255
            else:
 
1256
                raise
 
1257
 
 
1258
    def test_scan_unvalidated_index_not_implemented(self):
 
1259
        transport = MockTransport()
 
1260
        index = self.get_knit_index(transport, 'filename', 'r')
 
1261
        self.assertRaises(
 
1262
            NotImplementedError, index.scan_unvalidated_index,
 
1263
            'dummy graph_index')
 
1264
        self.assertRaises(
 
1265
            NotImplementedError, index.get_missing_compression_parents)
 
1266
 
 
1267
    def test_short_line(self):
 
1268
        transport = MockTransport([
 
1269
            _KndxIndex.HEADER,
 
1270
            "a option 0 10  :",
 
1271
            "b option 10 10 0", # This line isn't terminated, ignored
 
1272
            ])
 
1273
        index = self.get_knit_index(transport, "filename", "r")
 
1274
        self.assertEqual(set([('a',)]), index.keys())
 
1275
 
 
1276
    def test_skip_incomplete_record(self):
 
1277
        # A line with bogus data should just be skipped
 
1278
        transport = MockTransport([
 
1279
            _KndxIndex.HEADER,
 
1280
            "a option 0 10  :",
 
1281
            "b option 10 10 0", # This line isn't terminated, ignored
 
1282
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1283
            ])
 
1284
        index = self.get_knit_index(transport, "filename", "r")
 
1285
        self.assertEqual(set([('a',), ('c',)]), index.keys())
 
1286
 
 
1287
    def test_trailing_characters(self):
 
1288
        # A line with bogus data should just be skipped
 
1289
        transport = MockTransport([
 
1290
            _KndxIndex.HEADER,
 
1291
            "a option 0 10  :",
 
1292
            "b option 10 10 0 :a", # This line has extra trailing characters
 
1293
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
 
1294
            ])
 
1295
        index = self.get_knit_index(transport, "filename", "r")
 
1296
        self.assertEqual(set([('a',), ('c',)]), index.keys())
 
1297
 
 
1298
 
 
1299
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
 
1300
 
 
1301
    _test_needs_features = [compiled_knit_feature]
 
1302
 
 
1303
    def get_knit_index(self, transport, name, mode):
 
1304
        mapper = ConstantMapper(name)
 
1305
        orig = knit._load_data
 
1306
        def reset():
 
1307
            knit._load_data = orig
 
1308
        self.addCleanup(reset)
 
1309
        from bzrlib._knit_load_data_pyx import _load_data_c
 
1310
        knit._load_data = _load_data_c
 
1311
        allow_writes = lambda: mode == 'w'
 
1312
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
 
1313
 
 
1314
 
 
1315
class Test_KnitAnnotator(TestCaseWithMemoryTransport):
 
1316
 
 
1317
    def make_annotator(self):
 
1318
        factory = knit.make_pack_factory(True, True, 1)
 
1319
        vf = factory(self.get_transport())
 
1320
        return knit._KnitAnnotator(vf)
 
1321
 
 
1322
    def test__expand_fulltext(self):
 
1323
        ann = self.make_annotator()
 
1324
        rev_key = ('rev-id',)
 
1325
        ann._num_compression_children[rev_key] = 1
 
1326
        res = ann._expand_record(rev_key, (('parent-id',),), None,
 
1327
                           ['line1\n', 'line2\n'], ('fulltext', True))
 
1328
        # The content object and text lines should be cached appropriately
 
1329
        self.assertEqual(['line1\n', 'line2'], res)
 
1330
        content_obj = ann._content_objects[rev_key]
 
1331
        self.assertEqual(['line1\n', 'line2\n'], content_obj._lines)
 
1332
        self.assertEqual(res, content_obj.text())
 
1333
        self.assertEqual(res, ann._text_cache[rev_key])
 
1334
 
 
1335
    def test__expand_delta_comp_parent_not_available(self):
 
1336
        # Parent isn't available yet, so we return nothing, but queue up this
 
1337
        # node for later processing
 
1338
        ann = self.make_annotator()
 
1339
        rev_key = ('rev-id',)
 
1340
        parent_key = ('parent-id',)
 
1341
        record = ['0,1,1\n', 'new-line\n']
 
1342
        details = ('line-delta', False)
 
1343
        res = ann._expand_record(rev_key, (parent_key,), parent_key,
 
1344
                                 record, details)
 
1345
        self.assertEqual(None, res)
 
1346
        self.assertTrue(parent_key in ann._pending_deltas)
 
1347
        pending = ann._pending_deltas[parent_key]
 
1348
        self.assertEqual(1, len(pending))
 
1349
        self.assertEqual((rev_key, (parent_key,), record, details), pending[0])
 
1350
 
 
1351
    def test__expand_record_tracks_num_children(self):
 
1352
        ann = self.make_annotator()
 
1353
        rev_key = ('rev-id',)
 
1354
        rev2_key = ('rev2-id',)
 
1355
        parent_key = ('parent-id',)
 
1356
        record = ['0,1,1\n', 'new-line\n']
 
1357
        details = ('line-delta', False)
 
1358
        ann._num_compression_children[parent_key] = 2
 
1359
        ann._expand_record(parent_key, (), None, ['line1\n', 'line2\n'],
 
1360
                           ('fulltext', False))
 
1361
        res = ann._expand_record(rev_key, (parent_key,), parent_key,
 
1362
                                 record, details)
 
1363
        self.assertEqual({parent_key: 1}, ann._num_compression_children)
 
1364
        # Expanding the second child should remove the content object, and the
 
1365
        # num_compression_children entry
 
1366
        res = ann._expand_record(rev2_key, (parent_key,), parent_key,
 
1367
                                 record, details)
 
1368
        self.assertFalse(parent_key in ann._content_objects)
 
1369
        self.assertEqual({}, ann._num_compression_children)
 
1370
        # We should not cache the content_objects for rev2 and rev, because
 
1371
        # they do not have compression children of their own.
 
1372
        self.assertEqual({}, ann._content_objects)
 
1373
 
 
1374
    def test__expand_delta_records_blocks(self):
 
1375
        ann = self.make_annotator()
 
1376
        rev_key = ('rev-id',)
 
1377
        parent_key = ('parent-id',)
 
1378
        record = ['0,1,1\n', 'new-line\n']
 
1379
        details = ('line-delta', True)
 
1380
        ann._num_compression_children[parent_key] = 2
 
1381
        ann._expand_record(parent_key, (), None,
 
1382
                           ['line1\n', 'line2\n', 'line3\n'],
 
1383
                           ('fulltext', False))
 
1384
        ann._expand_record(rev_key, (parent_key,), parent_key, record, details)
 
1385
        self.assertEqual({(rev_key, parent_key): [(1, 1, 1), (3, 3, 0)]},
 
1386
                         ann._matching_blocks)
 
1387
        rev2_key = ('rev2-id',)
 
1388
        record = ['0,1,1\n', 'new-line\n']
 
1389
        details = ('line-delta', False)
 
1390
        ann._expand_record(rev2_key, (parent_key,), parent_key, record, details)
 
1391
        self.assertEqual([(1, 1, 2), (3, 3, 0)],
 
1392
                         ann._matching_blocks[(rev2_key, parent_key)])
 
1393
 
 
1394
    def test__get_parent_ann_uses_matching_blocks(self):
 
1395
        ann = self.make_annotator()
 
1396
        rev_key = ('rev-id',)
 
1397
        parent_key = ('parent-id',)
 
1398
        parent_ann = [(parent_key,)]*3
 
1399
        block_key = (rev_key, parent_key)
 
1400
        ann._annotations_cache[parent_key] = parent_ann
 
1401
        ann._matching_blocks[block_key] = [(0, 1, 1), (3, 3, 0)]
 
1402
        # We should not try to access any parent_lines content, because we know
 
1403
        # we already have the matching blocks
 
1404
        par_ann, blocks = ann._get_parent_annotations_and_matches(rev_key,
 
1405
                                        ['1\n', '2\n', '3\n'], parent_key)
 
1406
        self.assertEqual(parent_ann, par_ann)
 
1407
        self.assertEqual([(0, 1, 1), (3, 3, 0)], blocks)
 
1408
        self.assertEqual({}, ann._matching_blocks)
 
1409
 
 
1410
    def test__process_pending(self):
 
1411
        ann = self.make_annotator()
 
1412
        rev_key = ('rev-id',)
 
1413
        p1_key = ('p1-id',)
 
1414
        p2_key = ('p2-id',)
 
1415
        record = ['0,1,1\n', 'new-line\n']
 
1416
        details = ('line-delta', False)
 
1417
        p1_record = ['line1\n', 'line2\n']
 
1418
        ann._num_compression_children[p1_key] = 1
 
1419
        res = ann._expand_record(rev_key, (p1_key,p2_key), p1_key,
 
1420
                                 record, details)
 
1421
        self.assertEqual(None, res)
 
1422
        # self.assertTrue(p1_key in ann._pending_deltas)
 
1423
        self.assertEqual({}, ann._pending_annotation)
 
1424
        # Now insert p1, and we should be able to expand the delta
 
1425
        res = ann._expand_record(p1_key, (), None, p1_record,
 
1426
                                 ('fulltext', False))
 
1427
        self.assertEqual(p1_record, res)
 
1428
        ann._annotations_cache[p1_key] = [(p1_key,)]*2
 
1429
        res = ann._process_pending(p1_key)
 
1430
        self.assertEqual([], res)
 
1431
        self.assertFalse(p1_key in ann._pending_deltas)
 
1432
        self.assertTrue(p2_key in ann._pending_annotation)
 
1433
        self.assertEqual({p2_key: [(rev_key, (p1_key, p2_key))]},
 
1434
                         ann._pending_annotation)
 
1435
        # Now fill in parent 2, and pending annotation should be satisfied
 
1436
        res = ann._expand_record(p2_key, (), None, [], ('fulltext', False))
 
1437
        ann._annotations_cache[p2_key] = []
 
1438
        res = ann._process_pending(p2_key)
 
1439
        self.assertEqual([rev_key], res)
 
1440
        self.assertEqual({}, ann._pending_annotation)
 
1441
        self.assertEqual({}, ann._pending_deltas)
 
1442
 
 
1443
    def test_record_delta_removes_basis(self):
 
1444
        ann = self.make_annotator()
 
1445
        ann._expand_record(('parent-id',), (), None,
 
1446
                           ['line1\n', 'line2\n'], ('fulltext', False))
 
1447
        ann._num_compression_children['parent-id'] = 2
 
1448
 
 
1449
    def test_annotate_special_text(self):
 
1450
        ann = self.make_annotator()
 
1451
        vf = ann._vf
 
1452
        rev1_key = ('rev-1',)
 
1453
        rev2_key = ('rev-2',)
 
1454
        rev3_key = ('rev-3',)
 
1455
        spec_key = ('special:',)
 
1456
        vf.add_lines(rev1_key, [], ['initial content\n'])
 
1457
        vf.add_lines(rev2_key, [rev1_key], ['initial content\n',
 
1458
                                            'common content\n',
 
1459
                                            'content in 2\n'])
 
1460
        vf.add_lines(rev3_key, [rev1_key], ['initial content\n',
 
1461
                                            'common content\n',
 
1462
                                            'content in 3\n'])
 
1463
        spec_text = ('initial content\n'
 
1464
                     'common content\n'
 
1465
                     'content in 2\n'
 
1466
                     'content in 3\n')
 
1467
        ann.add_special_text(spec_key, [rev2_key, rev3_key], spec_text)
 
1468
        anns, lines = ann.annotate(spec_key)
 
1469
        self.assertEqual([(rev1_key,),
 
1470
                          (rev2_key, rev3_key),
 
1471
                          (rev2_key,),
 
1472
                          (rev3_key,),
 
1473
                         ], anns)
 
1474
        self.assertEqualDiff(spec_text, ''.join(lines))
 
1475
 
 
1476
 
 
1477
class KnitTests(TestCaseWithTransport):
 
1478
    """Class containing knit test helper routines."""
 
1479
 
 
1480
    def make_test_knit(self, annotate=False, name='test'):
 
1481
        mapper = ConstantMapper(name)
 
1482
        return make_file_factory(annotate, mapper)(self.get_transport())
 
1483
 
 
1484
 
 
1485
class TestBadShaError(KnitTests):
 
1486
    """Tests for handling of sha errors."""
 
1487
 
 
1488
    def test_sha_exception_has_text(self):
 
1489
        # having the failed text included in the error allows for recovery.
 
1490
        source = self.make_test_knit()
 
1491
        target = self.make_test_knit(name="target")
 
1492
        if not source._max_delta_chain:
 
1493
            raise TestNotApplicable(
 
1494
                "cannot get delta-caused sha failures without deltas.")
 
1495
        # create a basis
 
1496
        basis = ('basis',)
 
1497
        broken = ('broken',)
 
1498
        source.add_lines(basis, (), ['foo\n'])
 
1499
        source.add_lines(broken, (basis,), ['foo\n', 'bar\n'])
 
1500
        # Seed target with a bad basis text
 
1501
        target.add_lines(basis, (), ['gam\n'])
 
1502
        target.insert_record_stream(
 
1503
            source.get_record_stream([broken], 'unordered', False))
 
1504
        err = self.assertRaises(errors.KnitCorrupt,
 
1505
            target.get_record_stream([broken], 'unordered', True
 
1506
            ).next().get_bytes_as, 'chunked')
 
1507
        self.assertEqual(['gam\n', 'bar\n'], err.content)
 
1508
        # Test for formatting with live data
 
1509
        self.assertStartsWith(str(err), "Knit ")
 
1510
 
 
1511
 
 
1512
class TestKnitIndex(KnitTests):
 
1513
 
 
1514
    def test_add_versions_dictionary_compresses(self):
 
1515
        """Adding versions to the index should update the lookup dict"""
 
1516
        knit = self.make_test_knit()
 
1517
        idx = knit._index
 
1518
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
 
1519
        self.check_file_contents('test.kndx',
 
1520
            '# bzr knit index 8\n'
 
1521
            '\n'
 
1522
            'a-1 fulltext 0 0  :'
 
1523
            )
 
1524
        idx.add_records([
 
1525
            (('a-2',), ['fulltext'], (('a-2',), 0, 0), [('a-1',)]),
 
1526
            (('a-3',), ['fulltext'], (('a-3',), 0, 0), [('a-2',)]),
 
1527
            ])
 
1528
        self.check_file_contents('test.kndx',
 
1529
            '# bzr knit index 8\n'
 
1530
            '\n'
 
1531
            'a-1 fulltext 0 0  :\n'
 
1532
            'a-2 fulltext 0 0 0 :\n'
 
1533
            'a-3 fulltext 0 0 1 :'
 
1534
            )
 
1535
        self.assertEqual(set([('a-3',), ('a-1',), ('a-2',)]), idx.keys())
 
1536
        self.assertEqual({
 
1537
            ('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False)),
 
1538
            ('a-2',): ((('a-2',), 0, 0), None, (('a-1',),), ('fulltext', False)),
 
1539
            ('a-3',): ((('a-3',), 0, 0), None, (('a-2',),), ('fulltext', False)),
 
1540
            }, idx.get_build_details(idx.keys()))
 
1541
        self.assertEqual({('a-1',):(),
 
1542
            ('a-2',):(('a-1',),),
 
1543
            ('a-3',):(('a-2',),),},
 
1544
            idx.get_parent_map(idx.keys()))
 
1545
 
 
1546
    def test_add_versions_fails_clean(self):
 
1547
        """If add_versions fails in the middle, it restores a pristine state.
 
1548
 
 
1549
        Any modifications that are made to the index are reset if all versions
 
1550
        cannot be added.
 
1551
        """
 
1552
        # This cheats a little bit by passing in a generator which will
 
1553
        # raise an exception before the processing finishes
 
1554
        # Other possibilities would be to have an version with the wrong number
 
1555
        # of entries, or to make the backing transport unable to write any
 
1556
        # files.
 
1557
 
 
1558
        knit = self.make_test_knit()
 
1559
        idx = knit._index
 
1560
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
 
1561
 
 
1562
        class StopEarly(Exception):
 
1563
            pass
 
1564
 
 
1565
        def generate_failure():
 
1566
            """Add some entries and then raise an exception"""
 
1567
            yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
 
1568
            yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
 
1569
            raise StopEarly()
 
1570
 
 
1571
        # Assert the pre-condition
 
1572
        def assertA1Only():
 
1573
            self.assertEqual(set([('a-1',)]), set(idx.keys()))
 
1574
            self.assertEqual(
 
1575
                {('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False))},
 
1576
                idx.get_build_details([('a-1',)]))
 
1577
            self.assertEqual({('a-1',):()}, idx.get_parent_map(idx.keys()))
 
1578
 
 
1579
        assertA1Only()
 
1580
        self.assertRaises(StopEarly, idx.add_records, generate_failure())
 
1581
        # And it shouldn't be modified
 
1582
        assertA1Only()
 
1583
 
 
1584
    def test_knit_index_ignores_empty_files(self):
 
1585
        # There was a race condition in older bzr, where a ^C at the right time
 
1586
        # could leave an empty .kndx file, which bzr would later claim was a
 
1587
        # corrupted file since the header was not present. In reality, the file
 
1588
        # just wasn't created, so it should be ignored.
 
1589
        t = get_transport('.')
 
1590
        t.put_bytes('test.kndx', '')
 
1591
 
 
1592
        knit = self.make_test_knit()
 
1593
 
 
1594
    def test_knit_index_checks_header(self):
 
1595
        t = get_transport('.')
 
1596
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
 
1597
        k = self.make_test_knit()
 
1598
        self.assertRaises(KnitHeaderError, k.keys)
 
1599
 
 
1600
 
 
1601
class TestGraphIndexKnit(KnitTests):
 
1602
    """Tests for knits using a GraphIndex rather than a KnitIndex."""
 
1603
 
 
1604
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1605
        builder = GraphIndexBuilder(ref_lists)
 
1606
        for node, references, value in nodes:
 
1607
            builder.add_node(node, references, value)
 
1608
        stream = builder.finish()
 
1609
        trans = self.get_transport()
 
1610
        size = trans.put_file(name, stream)
 
1611
        return GraphIndex(trans, name, size)
 
1612
 
 
1613
    def two_graph_index(self, deltas=False, catch_adds=False):
 
1614
        """Build a two-graph index.
 
1615
 
 
1616
        :param deltas: If true, use underlying indices with two node-ref
 
1617
            lists and 'parent' set to a delta-compressed against tail.
 
1618
        """
 
1619
        # build a complex graph across several indices.
 
1620
        if deltas:
 
1621
            # delta compression inn the index
 
1622
            index1 = self.make_g_index('1', 2, [
 
1623
                (('tip', ), 'N0 100', ([('parent', )], [], )),
 
1624
                (('tail', ), '', ([], []))])
 
1625
            index2 = self.make_g_index('2', 2, [
 
1626
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
 
1627
                (('separate', ), '', ([], []))])
 
1628
        else:
 
1629
            # just blob location and graph in the index.
 
1630
            index1 = self.make_g_index('1', 1, [
 
1631
                (('tip', ), 'N0 100', ([('parent', )], )),
 
1632
                (('tail', ), '', ([], ))])
 
1633
            index2 = self.make_g_index('2', 1, [
 
1634
                (('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
 
1635
                (('separate', ), '', ([], ))])
 
1636
        combined_index = CombinedGraphIndex([index1, index2])
 
1637
        if catch_adds:
 
1638
            self.combined_index = combined_index
 
1639
            self.caught_entries = []
 
1640
            add_callback = self.catch_add
 
1641
        else:
 
1642
            add_callback = None
 
1643
        return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
 
1644
            add_callback=add_callback)
 
1645
 
 
1646
    def test_keys(self):
 
1647
        index = self.two_graph_index()
 
1648
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1649
            set(index.keys()))
 
1650
 
 
1651
    def test_get_position(self):
 
1652
        index = self.two_graph_index()
 
1653
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position(('tip',)))
 
1654
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position(('parent',)))
 
1655
 
 
1656
    def test_get_method_deltas(self):
 
1657
        index = self.two_graph_index(deltas=True)
 
1658
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1659
        self.assertEqual('line-delta', index.get_method(('parent',)))
 
1660
 
 
1661
    def test_get_method_no_deltas(self):
 
1662
        # check that the parent-history lookup is ignored with deltas=False.
 
1663
        index = self.two_graph_index(deltas=False)
 
1664
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1665
        self.assertEqual('fulltext', index.get_method(('parent',)))
 
1666
 
 
1667
    def test_get_options_deltas(self):
 
1668
        index = self.two_graph_index(deltas=True)
 
1669
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1670
        self.assertEqual(['line-delta'], index.get_options(('parent',)))
 
1671
 
 
1672
    def test_get_options_no_deltas(self):
 
1673
        # check that the parent-history lookup is ignored with deltas=False.
 
1674
        index = self.two_graph_index(deltas=False)
 
1675
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1676
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1677
 
 
1678
    def test_get_parent_map(self):
 
1679
        index = self.two_graph_index()
 
1680
        self.assertEqual({('parent',):(('tail',), ('ghost',))},
 
1681
            index.get_parent_map([('parent',), ('ghost',)]))
 
1682
 
 
1683
    def catch_add(self, entries):
 
1684
        self.caught_entries.append(entries)
 
1685
 
 
1686
    def test_add_no_callback_errors(self):
 
1687
        index = self.two_graph_index()
 
1688
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1689
            [(('new',), 'fulltext,no-eol', (None, 50, 60), ['separate'])])
 
1690
 
 
1691
    def test_add_version_smoke(self):
 
1692
        index = self.two_graph_index(catch_adds=True)
 
1693
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60),
 
1694
            [('separate',)])])
 
1695
        self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
 
1696
            self.caught_entries)
 
1697
 
 
1698
    def test_add_version_delta_not_delta_index(self):
 
1699
        index = self.two_graph_index(catch_adds=True)
 
1700
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1701
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1702
        self.assertEqual([], self.caught_entries)
 
1703
 
 
1704
    def test_add_version_same_dup(self):
 
1705
        index = self.two_graph_index(catch_adds=True)
 
1706
        # options can be spelt two different ways
 
1707
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1708
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
 
1709
        # position/length are ignored (because each pack could have fulltext or
 
1710
        # delta, and be at a different position.
 
1711
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1712
            [('parent',)])])
 
1713
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1714
            [('parent',)])])
 
1715
        # but neither should have added data:
 
1716
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1717
 
 
1718
    def test_add_version_different_dup(self):
 
1719
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1720
        # change options
 
1721
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1722
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1723
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1724
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
 
1725
        # parents
 
1726
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1727
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1728
        self.assertEqual([], self.caught_entries)
 
1729
 
 
1730
    def test_add_versions_nodeltas(self):
 
1731
        index = self.two_graph_index(catch_adds=True)
 
1732
        index.add_records([
 
1733
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1734
                (('new2',), 'fulltext', (None, 0, 6), [('new',)]),
 
1735
                ])
 
1736
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
 
1737
            (('new2', ), ' 0 6', ((('new',),),))],
 
1738
            sorted(self.caught_entries[0]))
 
1739
        self.assertEqual(1, len(self.caught_entries))
 
1740
 
 
1741
    def test_add_versions_deltas(self):
 
1742
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1743
        index.add_records([
 
1744
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1745
                (('new2',), 'line-delta', (None, 0, 6), [('new',)]),
 
1746
                ])
 
1747
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
 
1748
            (('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
 
1749
            sorted(self.caught_entries[0]))
 
1750
        self.assertEqual(1, len(self.caught_entries))
 
1751
 
 
1752
    def test_add_versions_delta_not_delta_index(self):
 
1753
        index = self.two_graph_index(catch_adds=True)
 
1754
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1755
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
1756
        self.assertEqual([], self.caught_entries)
 
1757
 
 
1758
    def test_add_versions_random_id_accepted(self):
 
1759
        index = self.two_graph_index(catch_adds=True)
 
1760
        index.add_records([], random_id=True)
 
1761
 
 
1762
    def test_add_versions_same_dup(self):
 
1763
        index = self.two_graph_index(catch_adds=True)
 
1764
        # options can be spelt two different ways
 
1765
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100),
 
1766
            [('parent',)])])
 
1767
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100),
 
1768
            [('parent',)])])
 
1769
        # position/length are ignored (because each pack could have fulltext or
 
1770
        # delta, and be at a different position.
 
1771
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1772
            [('parent',)])])
 
1773
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1774
            [('parent',)])])
 
1775
        # but neither should have added data.
 
1776
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1777
 
 
1778
    def test_add_versions_different_dup(self):
 
1779
        index = self.two_graph_index(deltas=True, catch_adds=True)
 
1780
        # change options
 
1781
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1782
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1783
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1784
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
 
1785
        # parents
 
1786
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1787
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1788
        # change options in the second record
 
1789
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1790
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)]),
 
1791
             (('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1792
        self.assertEqual([], self.caught_entries)
 
1793
 
 
1794
    def make_g_index_missing_compression_parent(self):
 
1795
        graph_index = self.make_g_index('missing_comp', 2,
 
1796
            [(('tip', ), ' 100 78',
 
1797
              ([('missing-parent', ), ('ghost', )], [('missing-parent', )]))])
 
1798
        return graph_index
 
1799
 
 
1800
    def make_g_index_missing_parent(self):
 
1801
        graph_index = self.make_g_index('missing_parent', 2,
 
1802
            [(('parent', ), ' 100 78', ([], [])),
 
1803
             (('tip', ), ' 100 78',
 
1804
              ([('parent', ), ('missing-parent', )], [('parent', )])),
 
1805
              ])
 
1806
        return graph_index
 
1807
 
 
1808
    def make_g_index_no_external_refs(self):
 
1809
        graph_index = self.make_g_index('no_external_refs', 2,
 
1810
            [(('rev', ), ' 100 78',
 
1811
              ([('parent', ), ('ghost', )], []))])
 
1812
        return graph_index
 
1813
 
 
1814
    def test_add_good_unvalidated_index(self):
 
1815
        unvalidated = self.make_g_index_no_external_refs()
 
1816
        combined = CombinedGraphIndex([unvalidated])
 
1817
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1818
        index.scan_unvalidated_index(unvalidated)
 
1819
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1820
 
 
1821
    def test_add_missing_compression_parent_unvalidated_index(self):
 
1822
        unvalidated = self.make_g_index_missing_compression_parent()
 
1823
        combined = CombinedGraphIndex([unvalidated])
 
1824
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1825
        index.scan_unvalidated_index(unvalidated)
 
1826
        # This also checks that its only the compression parent that is
 
1827
        # examined, otherwise 'ghost' would also be reported as a missing
 
1828
        # parent.
 
1829
        self.assertEqual(
 
1830
            frozenset([('missing-parent',)]),
 
1831
            index.get_missing_compression_parents())
 
1832
 
 
1833
    def test_add_missing_noncompression_parent_unvalidated_index(self):
 
1834
        unvalidated = self.make_g_index_missing_parent()
 
1835
        combined = CombinedGraphIndex([unvalidated])
 
1836
        index = _KnitGraphIndex(combined, lambda: True, deltas=True,
 
1837
            track_external_parent_refs=True)
 
1838
        index.scan_unvalidated_index(unvalidated)
 
1839
        self.assertEqual(
 
1840
            frozenset([('missing-parent',)]), index.get_missing_parents())
 
1841
 
 
1842
    def test_track_external_parent_refs(self):
 
1843
        g_index = self.make_g_index('empty', 2, [])
 
1844
        combined = CombinedGraphIndex([g_index])
 
1845
        index = _KnitGraphIndex(combined, lambda: True, deltas=True,
 
1846
            add_callback=self.catch_add, track_external_parent_refs=True)
 
1847
        self.caught_entries = []
 
1848
        index.add_records([
 
1849
            (('new-key',), 'fulltext,no-eol', (None, 50, 60),
 
1850
             [('parent-1',), ('parent-2',)])])
 
1851
        self.assertEqual(
 
1852
            frozenset([('parent-1',), ('parent-2',)]),
 
1853
            index.get_missing_parents())
 
1854
 
 
1855
    def test_add_unvalidated_index_with_present_external_references(self):
 
1856
        index = self.two_graph_index(deltas=True)
 
1857
        # Ugly hack to get at one of the underlying GraphIndex objects that
 
1858
        # two_graph_index built.
 
1859
        unvalidated = index._graph_index._indices[1]
 
1860
        # 'parent' is an external ref of _indices[1] (unvalidated), but is
 
1861
        # present in _indices[0].
 
1862
        index.scan_unvalidated_index(unvalidated)
 
1863
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1864
 
 
1865
    def make_new_missing_parent_g_index(self, name):
 
1866
        missing_parent = name + '-missing-parent'
 
1867
        graph_index = self.make_g_index(name, 2,
 
1868
            [((name + 'tip', ), ' 100 78',
 
1869
              ([(missing_parent, ), ('ghost', )], [(missing_parent, )]))])
 
1870
        return graph_index
 
1871
 
 
1872
    def test_add_mulitiple_unvalidated_indices_with_missing_parents(self):
 
1873
        g_index_1 = self.make_new_missing_parent_g_index('one')
 
1874
        g_index_2 = self.make_new_missing_parent_g_index('two')
 
1875
        combined = CombinedGraphIndex([g_index_1, g_index_2])
 
1876
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1877
        index.scan_unvalidated_index(g_index_1)
 
1878
        index.scan_unvalidated_index(g_index_2)
 
1879
        self.assertEqual(
 
1880
            frozenset([('one-missing-parent',), ('two-missing-parent',)]),
 
1881
            index.get_missing_compression_parents())
 
1882
 
 
1883
    def test_add_mulitiple_unvalidated_indices_with_mutual_dependencies(self):
 
1884
        graph_index_a = self.make_g_index('one', 2,
 
1885
            [(('parent-one', ), ' 100 78', ([('non-compression-parent',)], [])),
 
1886
             (('child-of-two', ), ' 100 78',
 
1887
              ([('parent-two',)], [('parent-two',)]))])
 
1888
        graph_index_b = self.make_g_index('two', 2,
 
1889
            [(('parent-two', ), ' 100 78', ([('non-compression-parent',)], [])),
 
1890
             (('child-of-one', ), ' 100 78',
 
1891
              ([('parent-one',)], [('parent-one',)]))])
 
1892
        combined = CombinedGraphIndex([graph_index_a, graph_index_b])
 
1893
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1894
        index.scan_unvalidated_index(graph_index_a)
 
1895
        index.scan_unvalidated_index(graph_index_b)
 
1896
        self.assertEqual(
 
1897
            frozenset([]), index.get_missing_compression_parents())
 
1898
 
 
1899
 
 
1900
class TestNoParentsGraphIndexKnit(KnitTests):
 
1901
    """Tests for knits using _KnitGraphIndex with no parents."""
 
1902
 
 
1903
    def make_g_index(self, name, ref_lists=0, nodes=[]):
 
1904
        builder = GraphIndexBuilder(ref_lists)
 
1905
        for node, references in nodes:
 
1906
            builder.add_node(node, references)
 
1907
        stream = builder.finish()
 
1908
        trans = self.get_transport()
 
1909
        size = trans.put_file(name, stream)
 
1910
        return GraphIndex(trans, name, size)
 
1911
 
 
1912
    def test_add_good_unvalidated_index(self):
 
1913
        unvalidated = self.make_g_index('unvalidated')
 
1914
        combined = CombinedGraphIndex([unvalidated])
 
1915
        index = _KnitGraphIndex(combined, lambda: True, parents=False)
 
1916
        index.scan_unvalidated_index(unvalidated)
 
1917
        self.assertEqual(frozenset(),
 
1918
            index.get_missing_compression_parents())
 
1919
 
 
1920
    def test_parents_deltas_incompatible(self):
 
1921
        index = CombinedGraphIndex([])
 
1922
        self.assertRaises(errors.KnitError, _KnitGraphIndex, lambda:True,
 
1923
            index, deltas=True, parents=False)
 
1924
 
 
1925
    def two_graph_index(self, catch_adds=False):
 
1926
        """Build a two-graph index.
 
1927
 
 
1928
        :param deltas: If true, use underlying indices with two node-ref
 
1929
            lists and 'parent' set to a delta-compressed against tail.
 
1930
        """
 
1931
        # put several versions in the index.
 
1932
        index1 = self.make_g_index('1', 0, [
 
1933
            (('tip', ), 'N0 100'),
 
1934
            (('tail', ), '')])
 
1935
        index2 = self.make_g_index('2', 0, [
 
1936
            (('parent', ), ' 100 78'),
 
1937
            (('separate', ), '')])
 
1938
        combined_index = CombinedGraphIndex([index1, index2])
 
1939
        if catch_adds:
 
1940
            self.combined_index = combined_index
 
1941
            self.caught_entries = []
 
1942
            add_callback = self.catch_add
 
1943
        else:
 
1944
            add_callback = None
 
1945
        return _KnitGraphIndex(combined_index, lambda:True, parents=False,
 
1946
            add_callback=add_callback)
 
1947
 
 
1948
    def test_keys(self):
 
1949
        index = self.two_graph_index()
 
1950
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1951
            set(index.keys()))
 
1952
 
 
1953
    def test_get_position(self):
 
1954
        index = self.two_graph_index()
 
1955
        self.assertEqual((index._graph_index._indices[0], 0, 100),
 
1956
            index.get_position(('tip',)))
 
1957
        self.assertEqual((index._graph_index._indices[1], 100, 78),
 
1958
            index.get_position(('parent',)))
 
1959
 
 
1960
    def test_get_method(self):
 
1961
        index = self.two_graph_index()
 
1962
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1963
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1964
 
 
1965
    def test_get_options(self):
 
1966
        index = self.two_graph_index()
 
1967
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1968
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1969
 
 
1970
    def test_get_parent_map(self):
 
1971
        index = self.two_graph_index()
 
1972
        self.assertEqual({('parent',):None},
 
1973
            index.get_parent_map([('parent',), ('ghost',)]))
 
1974
 
 
1975
    def catch_add(self, entries):
 
1976
        self.caught_entries.append(entries)
 
1977
 
 
1978
    def test_add_no_callback_errors(self):
 
1979
        index = self.two_graph_index()
 
1980
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1981
            [(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)])])
 
1982
 
 
1983
    def test_add_version_smoke(self):
 
1984
        index = self.two_graph_index(catch_adds=True)
 
1985
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60), [])])
 
1986
        self.assertEqual([[(('new', ), 'N50 60')]],
 
1987
            self.caught_entries)
 
1988
 
 
1989
    def test_add_version_delta_not_delta_index(self):
 
1990
        index = self.two_graph_index(catch_adds=True)
 
1991
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1992
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [])])
 
1993
        self.assertEqual([], self.caught_entries)
 
1994
 
 
1995
    def test_add_version_same_dup(self):
 
1996
        index = self.two_graph_index(catch_adds=True)
 
1997
        # options can be spelt two different ways
 
1998
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
1999
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
2000
        # position/length are ignored (because each pack could have fulltext or
 
2001
        # delta, and be at a different position.
 
2002
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
2003
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
 
2004
        # but neither should have added data.
 
2005
        self.assertEqual([[], [], [], []], self.caught_entries)
 
2006
 
 
2007
    def test_add_version_different_dup(self):
 
2008
        index = self.two_graph_index(catch_adds=True)
 
2009
        # change options
 
2010
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2011
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
2012
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2013
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
2014
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2015
            [(('tip',), 'fulltext', (None, 0, 100), [])])
 
2016
        # parents
 
2017
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2018
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
2019
        self.assertEqual([], self.caught_entries)
 
2020
 
 
2021
    def test_add_versions(self):
 
2022
        index = self.two_graph_index(catch_adds=True)
 
2023
        index.add_records([
 
2024
                (('new',), 'fulltext,no-eol', (None, 50, 60), []),
 
2025
                (('new2',), 'fulltext', (None, 0, 6), []),
 
2026
                ])
 
2027
        self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
 
2028
            sorted(self.caught_entries[0]))
 
2029
        self.assertEqual(1, len(self.caught_entries))
 
2030
 
 
2031
    def test_add_versions_delta_not_delta_index(self):
 
2032
        index = self.two_graph_index(catch_adds=True)
 
2033
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2034
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
 
2035
        self.assertEqual([], self.caught_entries)
 
2036
 
 
2037
    def test_add_versions_parents_not_parents_index(self):
 
2038
        index = self.two_graph_index(catch_adds=True)
 
2039
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2040
            [(('new',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
 
2041
        self.assertEqual([], self.caught_entries)
 
2042
 
 
2043
    def test_add_versions_random_id_accepted(self):
 
2044
        index = self.two_graph_index(catch_adds=True)
 
2045
        index.add_records([], random_id=True)
 
2046
 
 
2047
    def test_add_versions_same_dup(self):
 
2048
        index = self.two_graph_index(catch_adds=True)
 
2049
        # options can be spelt two different ways
 
2050
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
2051
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
2052
        # position/length are ignored (because each pack could have fulltext or
 
2053
        # delta, and be at a different position.
 
2054
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
2055
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
 
2056
        # but neither should have added data.
 
2057
        self.assertEqual([[], [], [], []], self.caught_entries)
 
2058
 
 
2059
    def test_add_versions_different_dup(self):
 
2060
        index = self.two_graph_index(catch_adds=True)
 
2061
        # change options
 
2062
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2063
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
2064
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2065
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
2066
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2067
            [(('tip',), 'fulltext', (None, 0, 100), [])])
 
2068
        # parents
 
2069
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2070
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
2071
        # change options in the second record
 
2072
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2073
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), []),
 
2074
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
2075
        self.assertEqual([], self.caught_entries)
 
2076
 
 
2077
 
 
2078
class TestKnitVersionedFiles(KnitTests):
 
2079
 
 
2080
    def assertGroupKeysForIo(self, exp_groups, keys, non_local_keys,
 
2081
                             positions, _min_buffer_size=None):
 
2082
        kvf = self.make_test_knit()
 
2083
        if _min_buffer_size is None:
 
2084
            _min_buffer_size = knit._STREAM_MIN_BUFFER_SIZE
 
2085
        self.assertEqual(exp_groups, kvf._group_keys_for_io(keys,
 
2086
                                        non_local_keys, positions,
 
2087
                                        _min_buffer_size=_min_buffer_size))
 
2088
 
 
2089
    def assertSplitByPrefix(self, expected_map, expected_prefix_order,
 
2090
                            keys):
 
2091
        split, prefix_order = KnitVersionedFiles._split_by_prefix(keys)
 
2092
        self.assertEqual(expected_map, split)
 
2093
        self.assertEqual(expected_prefix_order, prefix_order)
 
2094
 
 
2095
    def test__group_keys_for_io(self):
 
2096
        ft_detail = ('fulltext', False)
 
2097
        ld_detail = ('line-delta', False)
 
2098
        f_a = ('f', 'a')
 
2099
        f_b = ('f', 'b')
 
2100
        f_c = ('f', 'c')
 
2101
        g_a = ('g', 'a')
 
2102
        g_b = ('g', 'b')
 
2103
        g_c = ('g', 'c')
 
2104
        positions = {
 
2105
            f_a: (ft_detail, (f_a, 0, 100), None),
 
2106
            f_b: (ld_detail, (f_b, 100, 21), f_a),
 
2107
            f_c: (ld_detail, (f_c, 180, 15), f_b),
 
2108
            g_a: (ft_detail, (g_a, 121, 35), None),
 
2109
            g_b: (ld_detail, (g_b, 156, 12), g_a),
 
2110
            g_c: (ld_detail, (g_c, 195, 13), g_a),
 
2111
            }
 
2112
        self.assertGroupKeysForIo([([f_a], set())],
 
2113
                                  [f_a], [], positions)
 
2114
        self.assertGroupKeysForIo([([f_a], set([f_a]))],
 
2115
                                  [f_a], [f_a], positions)
 
2116
        self.assertGroupKeysForIo([([f_a, f_b], set([]))],
 
2117
                                  [f_a, f_b], [], positions)
 
2118
        self.assertGroupKeysForIo([([f_a, f_b], set([f_b]))],
 
2119
                                  [f_a, f_b], [f_b], positions)
 
2120
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
2121
                                  [f_a, g_a, f_b, g_b], [], positions)
 
2122
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
2123
                                  [f_a, g_a, f_b, g_b], [], positions,
 
2124
                                  _min_buffer_size=150)
 
2125
        self.assertGroupKeysForIo([([f_a, f_b], set()), ([g_a, g_b], set())],
 
2126
                                  [f_a, g_a, f_b, g_b], [], positions,
 
2127
                                  _min_buffer_size=100)
 
2128
        self.assertGroupKeysForIo([([f_c], set()), ([g_b], set())],
 
2129
                                  [f_c, g_b], [], positions,
 
2130
                                  _min_buffer_size=125)
 
2131
        self.assertGroupKeysForIo([([g_b, f_c], set())],
 
2132
                                  [g_b, f_c], [], positions,
 
2133
                                  _min_buffer_size=125)
 
2134
 
 
2135
    def test__split_by_prefix(self):
 
2136
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2137
                                  'g': [('g', 'b'), ('g', 'a')],
 
2138
                                 }, ['f', 'g'],
 
2139
                                 [('f', 'a'), ('g', 'b'),
 
2140
                                  ('g', 'a'), ('f', 'b')])
 
2141
 
 
2142
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2143
                                  'g': [('g', 'b'), ('g', 'a')],
 
2144
                                 }, ['f', 'g'],
 
2145
                                 [('f', 'a'), ('f', 'b'),
 
2146
                                  ('g', 'b'), ('g', 'a')])
 
2147
 
 
2148
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2149
                                  'g': [('g', 'b'), ('g', 'a')],
 
2150
                                 }, ['f', 'g'],
 
2151
                                 [('f', 'a'), ('f', 'b'),
 
2152
                                  ('g', 'b'), ('g', 'a')])
 
2153
 
 
2154
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2155
                                  'g': [('g', 'b'), ('g', 'a')],
 
2156
                                  '': [('a',), ('b',)]
 
2157
                                 }, ['f', 'g', ''],
 
2158
                                 [('f', 'a'), ('g', 'b'),
 
2159
                                  ('a',), ('b',),
 
2160
                                  ('g', 'a'), ('f', 'b')])
 
2161
 
 
2162
 
 
2163
class TestStacking(KnitTests):
 
2164
 
 
2165
    def get_basis_and_test_knit(self):
 
2166
        basis = self.make_test_knit(name='basis')
 
2167
        basis = RecordingVersionedFilesDecorator(basis)
 
2168
        test = self.make_test_knit(name='test')
 
2169
        test.add_fallback_versioned_files(basis)
 
2170
        return basis, test
 
2171
 
 
2172
    def test_add_fallback_versioned_files(self):
 
2173
        basis = self.make_test_knit(name='basis')
 
2174
        test = self.make_test_knit(name='test')
 
2175
        # It must not error; other tests test that the fallback is referred to
 
2176
        # when accessing data.
 
2177
        test.add_fallback_versioned_files(basis)
 
2178
 
 
2179
    def test_add_lines(self):
 
2180
        # lines added to the test are not added to the basis
 
2181
        basis, test = self.get_basis_and_test_knit()
 
2182
        key = ('foo',)
 
2183
        key_basis = ('bar',)
 
2184
        key_cross_border = ('quux',)
 
2185
        key_delta = ('zaphod',)
 
2186
        test.add_lines(key, (), ['foo\n'])
 
2187
        self.assertEqual({}, basis.get_parent_map([key]))
 
2188
        # lines added to the test that reference across the stack do a
 
2189
        # fulltext.
 
2190
        basis.add_lines(key_basis, (), ['foo\n'])
 
2191
        basis.calls = []
 
2192
        test.add_lines(key_cross_border, (key_basis,), ['foo\n'])
 
2193
        self.assertEqual('fulltext', test._index.get_method(key_cross_border))
 
2194
        # we don't even need to look at the basis to see that this should be
 
2195
        # stored as a fulltext
 
2196
        self.assertEqual([], basis.calls)
 
2197
        # Subsequent adds do delta.
 
2198
        basis.calls = []
 
2199
        test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
 
2200
        self.assertEqual('line-delta', test._index.get_method(key_delta))
 
2201
        self.assertEqual([], basis.calls)
 
2202
 
 
2203
    def test_annotate(self):
 
2204
        # annotations from the test knit are answered without asking the basis
 
2205
        basis, test = self.get_basis_and_test_knit()
 
2206
        key = ('foo',)
 
2207
        key_basis = ('bar',)
 
2208
        key_missing = ('missing',)
 
2209
        test.add_lines(key, (), ['foo\n'])
 
2210
        details = test.annotate(key)
 
2211
        self.assertEqual([(key, 'foo\n')], details)
 
2212
        self.assertEqual([], basis.calls)
 
2213
        # But texts that are not in the test knit are looked for in the basis
 
2214
        # directly.
 
2215
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2216
        basis.calls = []
 
2217
        details = test.annotate(key_basis)
 
2218
        self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
 
2219
        # Not optimised to date:
 
2220
        # self.assertEqual([("annotate", key_basis)], basis.calls)
 
2221
        self.assertEqual([('get_parent_map', set([key_basis])),
 
2222
            ('get_parent_map', set([key_basis])),
 
2223
            ('get_record_stream', [key_basis], 'topological', True)],
 
2224
            basis.calls)
 
2225
 
 
2226
    def test_check(self):
 
2227
        # At the moment checking a stacked knit does implicitly check the
 
2228
        # fallback files.
 
2229
        basis, test = self.get_basis_and_test_knit()
 
2230
        test.check()
 
2231
 
 
2232
    def test_get_parent_map(self):
 
2233
        # parents in the test knit are answered without asking the basis
 
2234
        basis, test = self.get_basis_and_test_knit()
 
2235
        key = ('foo',)
 
2236
        key_basis = ('bar',)
 
2237
        key_missing = ('missing',)
 
2238
        test.add_lines(key, (), [])
 
2239
        parent_map = test.get_parent_map([key])
 
2240
        self.assertEqual({key: ()}, parent_map)
 
2241
        self.assertEqual([], basis.calls)
 
2242
        # But parents that are not in the test knit are looked for in the basis
 
2243
        basis.add_lines(key_basis, (), [])
 
2244
        basis.calls = []
 
2245
        parent_map = test.get_parent_map([key, key_basis, key_missing])
 
2246
        self.assertEqual({key: (),
 
2247
            key_basis: ()}, parent_map)
 
2248
        self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
 
2249
            basis.calls)
 
2250
 
 
2251
    def test_get_record_stream_unordered_fulltexts(self):
 
2252
        # records from the test knit are answered without asking the basis:
 
2253
        basis, test = self.get_basis_and_test_knit()
 
2254
        key = ('foo',)
 
2255
        key_basis = ('bar',)
 
2256
        key_missing = ('missing',)
 
2257
        test.add_lines(key, (), ['foo\n'])
 
2258
        records = list(test.get_record_stream([key], 'unordered', True))
 
2259
        self.assertEqual(1, len(records))
 
2260
        self.assertEqual([], basis.calls)
 
2261
        # Missing (from test knit) objects are retrieved from the basis:
 
2262
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2263
        basis.calls = []
 
2264
        records = list(test.get_record_stream([key_basis, key_missing],
 
2265
            'unordered', True))
 
2266
        self.assertEqual(2, len(records))
 
2267
        calls = list(basis.calls)
 
2268
        for record in records:
 
2269
            self.assertSubset([record.key], (key_basis, key_missing))
 
2270
            if record.key == key_missing:
 
2271
                self.assertIsInstance(record, AbsentContentFactory)
 
2272
            else:
 
2273
                reference = list(basis.get_record_stream([key_basis],
 
2274
                    'unordered', True))[0]
 
2275
                self.assertEqual(reference.key, record.key)
 
2276
                self.assertEqual(reference.sha1, record.sha1)
 
2277
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2278
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2279
                    record.get_bytes_as(record.storage_kind))
 
2280
                self.assertEqual(reference.get_bytes_as('fulltext'),
 
2281
                    record.get_bytes_as('fulltext'))
 
2282
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2283
        # ask which fallbacks have which parents.
 
2284
        self.assertEqual([
 
2285
            ("get_parent_map", set([key_basis, key_missing])),
 
2286
            ("get_record_stream", [key_basis], 'unordered', True)],
 
2287
            calls)
 
2288
 
 
2289
    def test_get_record_stream_ordered_fulltexts(self):
 
2290
        # ordering is preserved down into the fallback store.
 
2291
        basis, test = self.get_basis_and_test_knit()
 
2292
        key = ('foo',)
 
2293
        key_basis = ('bar',)
 
2294
        key_basis_2 = ('quux',)
 
2295
        key_missing = ('missing',)
 
2296
        test.add_lines(key, (key_basis,), ['foo\n'])
 
2297
        # Missing (from test knit) objects are retrieved from the basis:
 
2298
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
2299
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
2300
        basis.calls = []
 
2301
        # ask for in non-topological order
 
2302
        records = list(test.get_record_stream(
 
2303
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
 
2304
        self.assertEqual(4, len(records))
 
2305
        results = []
 
2306
        for record in records:
 
2307
            self.assertSubset([record.key],
 
2308
                (key_basis, key_missing, key_basis_2, key))
 
2309
            if record.key == key_missing:
 
2310
                self.assertIsInstance(record, AbsentContentFactory)
 
2311
            else:
 
2312
                results.append((record.key, record.sha1, record.storage_kind,
 
2313
                    record.get_bytes_as('fulltext')))
 
2314
        calls = list(basis.calls)
 
2315
        order = [record[0] for record in results]
 
2316
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2317
        for result in results:
 
2318
            if result[0] == key:
 
2319
                source = test
 
2320
            else:
 
2321
                source = basis
 
2322
            record = source.get_record_stream([result[0]], 'unordered',
 
2323
                True).next()
 
2324
            self.assertEqual(record.key, result[0])
 
2325
            self.assertEqual(record.sha1, result[1])
 
2326
            # We used to check that the storage kind matched, but actually it
 
2327
            # depends on whether it was sourced from the basis, or in a single
 
2328
            # group, because asking for full texts returns proxy objects to a
 
2329
            # _ContentMapGenerator object; so checking the kind is unneeded.
 
2330
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
 
2331
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2332
        # ask which fallbacks have which parents.
 
2333
        self.assertEqual([
 
2334
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
2335
            # topological is requested from the fallback, because that is what
 
2336
            # was requested at the top level.
 
2337
            ("get_record_stream", [key_basis_2, key_basis], 'topological', True)],
 
2338
            calls)
 
2339
 
 
2340
    def test_get_record_stream_unordered_deltas(self):
 
2341
        # records from the test knit are answered without asking the basis:
 
2342
        basis, test = self.get_basis_and_test_knit()
 
2343
        key = ('foo',)
 
2344
        key_basis = ('bar',)
 
2345
        key_missing = ('missing',)
 
2346
        test.add_lines(key, (), ['foo\n'])
 
2347
        records = list(test.get_record_stream([key], 'unordered', False))
 
2348
        self.assertEqual(1, len(records))
 
2349
        self.assertEqual([], basis.calls)
 
2350
        # Missing (from test knit) objects are retrieved from the basis:
 
2351
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2352
        basis.calls = []
 
2353
        records = list(test.get_record_stream([key_basis, key_missing],
 
2354
            'unordered', False))
 
2355
        self.assertEqual(2, len(records))
 
2356
        calls = list(basis.calls)
 
2357
        for record in records:
 
2358
            self.assertSubset([record.key], (key_basis, key_missing))
 
2359
            if record.key == key_missing:
 
2360
                self.assertIsInstance(record, AbsentContentFactory)
 
2361
            else:
 
2362
                reference = list(basis.get_record_stream([key_basis],
 
2363
                    'unordered', False))[0]
 
2364
                self.assertEqual(reference.key, record.key)
 
2365
                self.assertEqual(reference.sha1, record.sha1)
 
2366
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2367
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2368
                    record.get_bytes_as(record.storage_kind))
 
2369
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2370
        # ask which fallbacks have which parents.
 
2371
        self.assertEqual([
 
2372
            ("get_parent_map", set([key_basis, key_missing])),
 
2373
            ("get_record_stream", [key_basis], 'unordered', False)],
 
2374
            calls)
 
2375
 
 
2376
    def test_get_record_stream_ordered_deltas(self):
 
2377
        # ordering is preserved down into the fallback store.
 
2378
        basis, test = self.get_basis_and_test_knit()
 
2379
        key = ('foo',)
 
2380
        key_basis = ('bar',)
 
2381
        key_basis_2 = ('quux',)
 
2382
        key_missing = ('missing',)
 
2383
        test.add_lines(key, (key_basis,), ['foo\n'])
 
2384
        # Missing (from test knit) objects are retrieved from the basis:
 
2385
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
2386
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
2387
        basis.calls = []
 
2388
        # ask for in non-topological order
 
2389
        records = list(test.get_record_stream(
 
2390
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
 
2391
        self.assertEqual(4, len(records))
 
2392
        results = []
 
2393
        for record in records:
 
2394
            self.assertSubset([record.key],
 
2395
                (key_basis, key_missing, key_basis_2, key))
 
2396
            if record.key == key_missing:
 
2397
                self.assertIsInstance(record, AbsentContentFactory)
 
2398
            else:
 
2399
                results.append((record.key, record.sha1, record.storage_kind,
 
2400
                    record.get_bytes_as(record.storage_kind)))
 
2401
        calls = list(basis.calls)
 
2402
        order = [record[0] for record in results]
 
2403
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2404
        for result in results:
 
2405
            if result[0] == key:
 
2406
                source = test
 
2407
            else:
 
2408
                source = basis
 
2409
            record = source.get_record_stream([result[0]], 'unordered',
 
2410
                False).next()
 
2411
            self.assertEqual(record.key, result[0])
 
2412
            self.assertEqual(record.sha1, result[1])
 
2413
            self.assertEqual(record.storage_kind, result[2])
 
2414
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
 
2415
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2416
        # ask which fallbacks have which parents.
 
2417
        self.assertEqual([
 
2418
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
2419
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
 
2420
            calls)
 
2421
 
 
2422
    def test_get_sha1s(self):
 
2423
        # sha1's in the test knit are answered without asking the basis
 
2424
        basis, test = self.get_basis_and_test_knit()
 
2425
        key = ('foo',)
 
2426
        key_basis = ('bar',)
 
2427
        key_missing = ('missing',)
 
2428
        test.add_lines(key, (), ['foo\n'])
 
2429
        key_sha1sum = osutils.sha('foo\n').hexdigest()
 
2430
        sha1s = test.get_sha1s([key])
 
2431
        self.assertEqual({key: key_sha1sum}, sha1s)
 
2432
        self.assertEqual([], basis.calls)
 
2433
        # But texts that are not in the test knit are looked for in the basis
 
2434
        # directly (rather than via text reconstruction) so that remote servers
 
2435
        # etc don't have to answer with full content.
 
2436
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2437
        basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
2438
        basis.calls = []
 
2439
        sha1s = test.get_sha1s([key, key_missing, key_basis])
 
2440
        self.assertEqual({key: key_sha1sum,
 
2441
            key_basis: basis_sha1sum}, sha1s)
 
2442
        self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
 
2443
            basis.calls)
 
2444
 
 
2445
    def test_insert_record_stream(self):
 
2446
        # records are inserted as normal; insert_record_stream builds on
 
2447
        # add_lines, so a smoke test should be all that's needed:
 
2448
        key = ('foo',)
 
2449
        key_basis = ('bar',)
 
2450
        key_delta = ('zaphod',)
 
2451
        basis, test = self.get_basis_and_test_knit()
 
2452
        source = self.make_test_knit(name='source')
 
2453
        basis.add_lines(key_basis, (), ['foo\n'])
 
2454
        basis.calls = []
 
2455
        source.add_lines(key_basis, (), ['foo\n'])
 
2456
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2457
        stream = source.get_record_stream([key_delta], 'unordered', False)
 
2458
        test.insert_record_stream(stream)
 
2459
        # XXX: this does somewhat too many calls in making sure of whether it
 
2460
        # has to recreate the full text.
 
2461
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2462
             ('get_parent_map', set([key_basis])),
 
2463
             ('get_record_stream', [key_basis], 'unordered', True)],
 
2464
            basis.calls)
 
2465
        self.assertEqual({key_delta:(key_basis,)},
 
2466
            test.get_parent_map([key_delta]))
 
2467
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2468
            'unordered', True).next().get_bytes_as('fulltext'))
 
2469
 
 
2470
    def test_iter_lines_added_or_present_in_keys(self):
 
2471
        # Lines from the basis are returned, and lines for a given key are only
 
2472
        # returned once.
 
2473
        key1 = ('foo1',)
 
2474
        key2 = ('foo2',)
 
2475
        # all sources are asked for keys:
 
2476
        basis, test = self.get_basis_and_test_knit()
 
2477
        basis.add_lines(key1, (), ["foo"])
 
2478
        basis.calls = []
 
2479
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
 
2480
        self.assertEqual([("foo\n", key1)], lines)
 
2481
        self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
 
2482
            basis.calls)
 
2483
        # keys in both are not duplicated:
 
2484
        test.add_lines(key2, (), ["bar\n"])
 
2485
        basis.add_lines(key2, (), ["bar\n"])
 
2486
        basis.calls = []
 
2487
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
 
2488
        self.assertEqual([("bar\n", key2)], lines)
 
2489
        self.assertEqual([], basis.calls)
 
2490
 
 
2491
    def test_keys(self):
 
2492
        key1 = ('foo1',)
 
2493
        key2 = ('foo2',)
 
2494
        # all sources are asked for keys:
 
2495
        basis, test = self.get_basis_and_test_knit()
 
2496
        keys = test.keys()
 
2497
        self.assertEqual(set(), set(keys))
 
2498
        self.assertEqual([("keys",)], basis.calls)
 
2499
        # keys from a basis are returned:
 
2500
        basis.add_lines(key1, (), [])
 
2501
        basis.calls = []
 
2502
        keys = test.keys()
 
2503
        self.assertEqual(set([key1]), set(keys))
 
2504
        self.assertEqual([("keys",)], basis.calls)
 
2505
        # keys in both are not duplicated:
 
2506
        test.add_lines(key2, (), [])
 
2507
        basis.add_lines(key2, (), [])
 
2508
        basis.calls = []
 
2509
        keys = test.keys()
 
2510
        self.assertEqual(2, len(keys))
 
2511
        self.assertEqual(set([key1, key2]), set(keys))
 
2512
        self.assertEqual([("keys",)], basis.calls)
 
2513
 
 
2514
    def test_add_mpdiffs(self):
 
2515
        # records are inserted as normal; add_mpdiff builds on
 
2516
        # add_lines, so a smoke test should be all that's needed:
 
2517
        key = ('foo',)
 
2518
        key_basis = ('bar',)
 
2519
        key_delta = ('zaphod',)
 
2520
        basis, test = self.get_basis_and_test_knit()
 
2521
        source = self.make_test_knit(name='source')
 
2522
        basis.add_lines(key_basis, (), ['foo\n'])
 
2523
        basis.calls = []
 
2524
        source.add_lines(key_basis, (), ['foo\n'])
 
2525
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2526
        diffs = source.make_mpdiffs([key_delta])
 
2527
        test.add_mpdiffs([(key_delta, (key_basis,),
 
2528
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
 
2529
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2530
            ('get_record_stream', [key_basis], 'unordered', True),],
 
2531
            basis.calls)
 
2532
        self.assertEqual({key_delta:(key_basis,)},
 
2533
            test.get_parent_map([key_delta]))
 
2534
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2535
            'unordered', True).next().get_bytes_as('fulltext'))
 
2536
 
 
2537
    def test_make_mpdiffs(self):
 
2538
        # Generating an mpdiff across a stacking boundary should detect parent
 
2539
        # texts regions.
 
2540
        key = ('foo',)
 
2541
        key_left = ('bar',)
 
2542
        key_right = ('zaphod',)
 
2543
        basis, test = self.get_basis_and_test_knit()
 
2544
        basis.add_lines(key_left, (), ['bar\n'])
 
2545
        basis.add_lines(key_right, (), ['zaphod\n'])
 
2546
        basis.calls = []
 
2547
        test.add_lines(key, (key_left, key_right),
 
2548
            ['bar\n', 'foo\n', 'zaphod\n'])
 
2549
        diffs = test.make_mpdiffs([key])
 
2550
        self.assertEqual([
 
2551
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
 
2552
                multiparent.NewText(['foo\n']),
 
2553
                multiparent.ParentText(1, 0, 2, 1)])],
 
2554
            diffs)
 
2555
        self.assertEqual(3, len(basis.calls))
 
2556
        self.assertEqual([
 
2557
            ("get_parent_map", set([key_left, key_right])),
 
2558
            ("get_parent_map", set([key_left, key_right])),
 
2559
            ],
 
2560
            basis.calls[:-1])
 
2561
        last_call = basis.calls[-1]
 
2562
        self.assertEqual('get_record_stream', last_call[0])
 
2563
        self.assertEqual(set([key_left, key_right]), set(last_call[1]))
 
2564
        self.assertEqual('topological', last_call[2])
 
2565
        self.assertEqual(True, last_call[3])
 
2566
 
 
2567
 
 
2568
class TestNetworkBehaviour(KnitTests):
 
2569
    """Tests for getting data out of/into knits over the network."""
 
2570
 
 
2571
    def test_include_delta_closure_generates_a_knit_delta_closure(self):
 
2572
        vf = self.make_test_knit(name='test')
 
2573
        # put in three texts, giving ft, delta, delta
 
2574
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2575
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2576
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2577
        # But heuristics could interfere, so check what happened:
 
2578
        self.assertEqual(['knit-ft-gz', 'knit-delta-gz', 'knit-delta-gz'],
 
2579
            [record.storage_kind for record in
 
2580
             vf.get_record_stream([('base',), ('d1',), ('d2',)],
 
2581
                'topological', False)])
 
2582
        # generate a stream of just the deltas include_delta_closure=True,
 
2583
        # serialise to the network, and check that we get a delta closure on the wire.
 
2584
        stream = vf.get_record_stream([('d1',), ('d2',)], 'topological', True)
 
2585
        netb = [record.get_bytes_as(record.storage_kind) for record in stream]
 
2586
        # The first bytes should be a memo from _ContentMapGenerator, and the
 
2587
        # second bytes should be empty (because its a API proxy not something
 
2588
        # for wire serialisation.
 
2589
        self.assertEqual('', netb[1])
 
2590
        bytes = netb[0]
 
2591
        kind, line_end = network_bytes_to_kind_and_offset(bytes)
 
2592
        self.assertEqual('knit-delta-closure', kind)
 
2593
 
 
2594
 
 
2595
class TestContentMapGenerator(KnitTests):
 
2596
    """Tests for ContentMapGenerator"""
 
2597
 
 
2598
    def test_get_record_stream_gives_records(self):
 
2599
        vf = self.make_test_knit(name='test')
 
2600
        # put in three texts, giving ft, delta, delta
 
2601
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2602
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2603
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2604
        keys = [('d1',), ('d2',)]
 
2605
        generator = _VFContentMapGenerator(vf, keys,
 
2606
            global_map=vf.get_parent_map(keys))
 
2607
        for record in generator.get_record_stream():
 
2608
            if record.key == ('d1',):
 
2609
                self.assertEqual('d1\n', record.get_bytes_as('fulltext'))
 
2610
            else:
 
2611
                self.assertEqual('d2\n', record.get_bytes_as('fulltext'))
 
2612
 
 
2613
    def test_get_record_stream_kinds_are_raw(self):
 
2614
        vf = self.make_test_knit(name='test')
 
2615
        # put in three texts, giving ft, delta, delta
 
2616
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2617
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2618
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2619
        keys = [('base',), ('d1',), ('d2',)]
 
2620
        generator = _VFContentMapGenerator(vf, keys,
 
2621
            global_map=vf.get_parent_map(keys))
 
2622
        kinds = {('base',): 'knit-delta-closure',
 
2623
            ('d1',): 'knit-delta-closure-ref',
 
2624
            ('d2',): 'knit-delta-closure-ref',
 
2625
            }
 
2626
        for record in generator.get_record_stream():
 
2627
            self.assertEqual(kinds[record.key], record.storage_kind)