~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: John Arbash Meinel
  • Date: 2009-02-23 15:29:35 UTC
  • mfrom: (3943.7.7 bzr.code_style_cleanup)
  • mto: This revision was merged to the branch mainline in revision 4033.
  • Revision ID: john@arbash-meinel.com-20090223152935-oel9m92mwcc6nb4h
Merge the removal of all trailing whitespace, and resolve conflicts.

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