/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: John Arbash Meinel
  • Date: 2009-12-22 16:28:47 UTC
  • mto: This revision was merged to the branch mainline in revision 4922.
  • Revision ID: john@arbash-meinel.com-20091222162847-tvnsc69to4l4uf5r
Implement a permute_for_extension helper.

Use it for all of the 'simple' extension permutations.
It basically permutes all tests in the current module, by setting TestCase.module.
Which works well for most of our extension tests. Some had more advanced
handling of permutations (extra permutations, custom vars, etc.)

Show diffs side-by-side

added added

removed removed

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