~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Andrew Bennetts
  • Date: 2009-10-26 06:44:40 UTC
  • mfrom: (4634.77.9 2.0)
  • mto: This revision was merged to the branch mainline in revision 4770.
  • Revision ID: andrew.bennetts@canonical.com-20091026064440-06u7tpg7l6sjkh8h
Merge lp:bzr/2.0 into lp:bzr.

Show diffs side-by-side

added added

removed removed

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