~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-02-10 04:54:18 UTC
  • mfrom: (3988.1.3 bzr.dev)
  • Revision ID: pqm@pqm.ubuntu.com-20090210045418-u1c0p4zpnp6nna3n
(Jelmer) Add specification for colocated branches.

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