/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Martin
  • Date: 2011-04-15 21:22:52 UTC
  • mto: This revision was merged to the branch mainline in revision 5797.
  • Revision ID: gzlist@googlemail.com-20110415212252-lhqulomwg2y538xj
Add user encoding name to argv decoding error message per poolie in review

Show diffs side-by-side

added added

removed removed

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