~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Vincent Ladeuil
  • Date: 2010-04-23 08:51:52 UTC
  • mfrom: (5131.2.6 support_OO_flag)
  • mto: This revision was merged to the branch mainline in revision 5179.
  • Revision ID: v.ladeuil+lp@free.fr-20100423085152-uoewc1vnkwqhw0pj
Manually assign docstrings to command objects, so that they work with python -OO

Show diffs side-by-side

added added

removed removed

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