/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

Deprecate VersionedFile.join.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
#
 
3
# Authors:
 
4
#   Johan Rydberg <jrydberg@gnu.org>
 
5
#
 
6
# This program is free software; you can redistribute it and/or modify
 
7
# it under the terms of the GNU General Public License as published by
 
8
# the Free Software Foundation; either version 2 of the License, or
 
9
# (at your option) any later version.
 
10
#
 
11
# This program is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
# GNU General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License
 
17
# along with this program; if not, write to the Free Software
 
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
19
 
 
20
 
 
21
# TODO: might be nice to create a versionedfile with some type of corruption
 
22
# considered typical and check that it can be detected/corrected.
 
23
 
 
24
from StringIO import StringIO
 
25
 
 
26
import bzrlib
 
27
from bzrlib import (
 
28
    errors,
 
29
    osutils,
 
30
    progress,
 
31
    )
 
32
from bzrlib.errors import (
 
33
                           RevisionNotPresent,
 
34
                           RevisionAlreadyPresent,
 
35
                           WeaveParentMismatch
 
36
                           )
 
37
from bzrlib import knit as _mod_knit
 
38
from bzrlib.knit import (
 
39
    make_file_knit,
 
40
    KnitAnnotateFactory,
 
41
    KnitPlainFactory,
 
42
    )
 
43
from bzrlib.symbol_versioning import one_four, one_five
 
44
from bzrlib.tests import TestCaseWithMemoryTransport, TestSkipped
 
45
from bzrlib.tests.http_utils import TestCaseWithWebserver
 
46
from bzrlib.trace import mutter
 
47
from bzrlib.transport import get_transport
 
48
from bzrlib.transport.memory import MemoryTransport
 
49
from bzrlib.tsort import topo_sort
 
50
from bzrlib.tuned_gzip import GzipFile
 
51
import bzrlib.versionedfile as versionedfile
 
52
from bzrlib.weave import WeaveFile
 
53
from bzrlib.weavefile import read_weave, write_weave
 
54
 
 
55
 
 
56
def get_diamond_vf(f, trailing_eol=True, left_only=False):
 
57
    """Get a diamond graph to exercise deltas and merges.
 
58
    
 
59
    :param trailing_eol: If True end the last line with \n.
 
60
    """
 
61
    parents = {
 
62
        'origin': (),
 
63
        'base': (('origin',),),
 
64
        'left': (('base',),),
 
65
        'right': (('base',),),
 
66
        'merged': (('left',), ('right',)),
 
67
        }
 
68
    # insert a diamond graph to exercise deltas and merges.
 
69
    if trailing_eol:
 
70
        last_char = '\n'
 
71
    else:
 
72
        last_char = ''
 
73
    f.add_lines('origin', [], ['origin' + last_char])
 
74
    f.add_lines('base', ['origin'], ['base' + last_char])
 
75
    f.add_lines('left', ['base'], ['base\n', 'left' + last_char])
 
76
    if not left_only:
 
77
        f.add_lines('right', ['base'],
 
78
            ['base\n', 'right' + last_char])
 
79
        f.add_lines('merged', ['left', 'right'],
 
80
            ['base\n', 'left\n', 'right\n', 'merged' + last_char])
 
81
    return f, parents
 
82
 
 
83
 
 
84
class VersionedFileTestMixIn(object):
 
85
    """A mixin test class for testing VersionedFiles.
 
86
 
 
87
    This is not an adaptor-style test at this point because
 
88
    theres no dynamic substitution of versioned file implementations,
 
89
    they are strictly controlled by their owning repositories.
 
90
    """
 
91
 
 
92
    def get_transaction(self):
 
93
        if not hasattr(self, '_transaction'):
 
94
            self._transaction = None
 
95
        return self._transaction
 
96
 
 
97
    def test_add(self):
 
98
        f = self.get_file()
 
99
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
100
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
 
101
        def verify_file(f):
 
102
            versions = f.versions()
 
103
            self.assertTrue('r0' in versions)
 
104
            self.assertTrue('r1' in versions)
 
105
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
 
106
            self.assertEquals(f.get_text('r0'), 'a\nb\n')
 
107
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
 
108
            self.assertEqual(2, len(f))
 
109
            self.assertEqual(2, f.num_versions())
 
110
    
 
111
            self.assertRaises(RevisionNotPresent,
 
112
                f.add_lines, 'r2', ['foo'], [])
 
113
            self.assertRaises(RevisionAlreadyPresent,
 
114
                f.add_lines, 'r1', [], [])
 
115
        verify_file(f)
 
116
        # this checks that reopen with create=True does not break anything.
 
117
        f = self.reopen_file(create=True)
 
118
        verify_file(f)
 
119
 
 
120
    def test_get_record_stream_empty(self):
 
121
        """get_record_stream is a replacement for get_data_stream."""
 
122
        f = self.get_file()
 
123
        entries = f.get_record_stream([], 'unordered', False)
 
124
        self.assertEqual([], list(entries))
 
125
 
 
126
    def assertValidStorageKind(self, storage_kind):
 
127
        """Assert that storage_kind is a valid storage_kind."""
 
128
        self.assertSubset([storage_kind],
 
129
            ['mpdiff', 'knit-annotated-ft', 'knit-annotated-delta',
 
130
             'knit-ft', 'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
 
131
             'knit-annotated-delta-gz', 'knit-ft-gz', 'knit-delta-gz'])
 
132
 
 
133
    def capture_stream(self, f, entries, on_seen, parents):
 
134
        """Capture a stream for testing."""
 
135
        for factory in entries:
 
136
            on_seen(factory.key)
 
137
            self.assertValidStorageKind(factory.storage_kind)
 
138
            self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
139
            self.assertEqual(parents[factory.key[0]], factory.parents)
 
140
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
141
                str)
 
142
 
 
143
    def test_get_record_stream_interface(self):
 
144
        """each item in a stream has to provide a regular interface."""
 
145
        f, parents = get_diamond_vf(self.get_file())
 
146
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
147
            'unordered', False)
 
148
        seen = set()
 
149
        self.capture_stream(f, entries, seen.add, parents)
 
150
        self.assertEqual(set([('base',), ('left',), ('right',), ('merged',)]),
 
151
            seen)
 
152
 
 
153
    def test_get_record_stream_interface_ordered(self):
 
154
        """each item in a stream has to provide a regular interface."""
 
155
        f, parents = get_diamond_vf(self.get_file())
 
156
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
157
            'topological', False)
 
158
        seen = []
 
159
        self.capture_stream(f, entries, seen.append, parents)
 
160
        self.assertSubset([tuple(seen)],
 
161
            (
 
162
             (('base',), ('left',), ('right',), ('merged',)),
 
163
             (('base',), ('right',), ('left',), ('merged',)),
 
164
            ))
 
165
 
 
166
    def test_get_record_stream_interface_ordered_with_delta_closure(self):
 
167
        """each item in a stream has to provide a regular interface."""
 
168
        f, parents = get_diamond_vf(self.get_file())
 
169
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
170
            'topological', True)
 
171
        seen = []
 
172
        for factory in entries:
 
173
            seen.append(factory.key)
 
174
            self.assertValidStorageKind(factory.storage_kind)
 
175
            self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
176
            self.assertEqual(parents[factory.key[0]], factory.parents)
 
177
            self.assertEqual(f.get_text(factory.key[0]),
 
178
                factory.get_bytes_as('fulltext'))
 
179
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
180
                str)
 
181
        self.assertSubset([tuple(seen)],
 
182
            (
 
183
             (('base',), ('left',), ('right',), ('merged',)),
 
184
             (('base',), ('right',), ('left',), ('merged',)),
 
185
            ))
 
186
 
 
187
    def test_get_record_stream_unknown_storage_kind_raises(self):
 
188
        """Asking for a storage kind that the stream cannot supply raises."""
 
189
        f, parents = get_diamond_vf(self.get_file())
 
190
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
191
            'unordered', False)
 
192
        # We track the contents because we should be able to try, fail a
 
193
        # particular kind and then ask for one that works and continue.
 
194
        seen = set()
 
195
        for factory in entries:
 
196
            seen.add(factory.key)
 
197
            self.assertValidStorageKind(factory.storage_kind)
 
198
            self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
199
            self.assertEqual(parents[factory.key[0]], factory.parents)
 
200
            # currently no stream emits mpdiff
 
201
            self.assertRaises(errors.UnavailableRepresentation,
 
202
                factory.get_bytes_as, 'mpdiff')
 
203
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
204
                str)
 
205
        self.assertEqual(set([('base',), ('left',), ('right',), ('merged',)]),
 
206
            seen)
 
207
 
 
208
    def test_get_record_stream_missing_records_are_absent(self):
 
209
        f, parents = get_diamond_vf(self.get_file())
 
210
        entries = f.get_record_stream(['merged', 'left', 'right', 'or', 'base'],
 
211
            'unordered', False)
 
212
        seen = set()
 
213
        for factory in entries:
 
214
            seen.add(factory.key)
 
215
            if factory.key == ('or',):
 
216
                self.assertEqual('absent', factory.storage_kind)
 
217
                self.assertEqual(None, factory.sha1)
 
218
                self.assertEqual(None, factory.parents)
 
219
            else:
 
220
                self.assertValidStorageKind(factory.storage_kind)
 
221
                self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
222
                self.assertEqual(parents[factory.key[0]], factory.parents)
 
223
                self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
224
                    str)
 
225
        self.assertEqual(
 
226
            set([('base',), ('left',), ('right',), ('merged',), ('or',)]),
 
227
            seen)
 
228
 
 
229
    def test_filter_absent_records(self):
 
230
        """Requested missing records can be filter trivially."""
 
231
        f, parents = get_diamond_vf(self.get_file())
 
232
        entries = f.get_record_stream(['merged', 'left', 'right', 'extra', 'base'],
 
233
            'unordered', False)
 
234
        seen = set()
 
235
        self.capture_stream(f, versionedfile.filter_absent(entries), seen.add,
 
236
            parents)
 
237
        self.assertEqual(set([('base',), ('left',), ('right',), ('merged',)]),
 
238
            seen)
 
239
 
 
240
    def test_insert_record_stream_empty(self):
 
241
        """Inserting an empty record stream should work."""
 
242
        f = self.get_file()
 
243
        stream = []
 
244
        f.insert_record_stream([])
 
245
 
 
246
    def assertIdenticalVersionedFile(self, left, right):
 
247
        """Assert that left and right have the same contents."""
 
248
        self.assertEqual(set(left.versions()), set(right.versions()))
 
249
        self.assertEqual(left.get_parent_map(left.versions()),
 
250
            right.get_parent_map(right.versions()))
 
251
        for v in left.versions():
 
252
            self.assertEqual(left.get_text(v), right.get_text(v))
 
253
 
 
254
    def test_insert_record_stream_fulltexts(self):
 
255
        """Any file should accept a stream of fulltexts."""
 
256
        f = self.get_file()
 
257
        weave_vf = WeaveFile('source', get_transport(self.get_url('.')),
 
258
            create=True, get_scope=self.get_transaction)
 
259
        source, _ = get_diamond_vf(weave_vf)
 
260
        stream = source.get_record_stream(source.versions(), 'topological',
 
261
            False)
 
262
        f.insert_record_stream(stream)
 
263
        self.assertIdenticalVersionedFile(f, source)
 
264
 
 
265
    def test_insert_record_stream_fulltexts_noeol(self):
 
266
        """Any file should accept a stream of fulltexts."""
 
267
        f = self.get_file()
 
268
        weave_vf = WeaveFile('source', get_transport(self.get_url('.')),
 
269
            create=True, get_scope=self.get_transaction)
 
270
        source, _ = get_diamond_vf(weave_vf, trailing_eol=False)
 
271
        stream = source.get_record_stream(source.versions(), 'topological',
 
272
            False)
 
273
        f.insert_record_stream(stream)
 
274
        self.assertIdenticalVersionedFile(f, source)
 
275
 
 
276
    def test_insert_record_stream_annotated_knits(self):
 
277
        """Any file should accept a stream from plain knits."""
 
278
        f = self.get_file()
 
279
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
280
            create=True)
 
281
        get_diamond_vf(source)
 
282
        stream = source.get_record_stream(source.versions(), 'topological',
 
283
            False)
 
284
        f.insert_record_stream(stream)
 
285
        self.assertIdenticalVersionedFile(f, source)
 
286
 
 
287
    def test_insert_record_stream_annotated_knits_noeol(self):
 
288
        """Any file should accept a stream from plain knits."""
 
289
        f = self.get_file()
 
290
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
291
            create=True)
 
292
        get_diamond_vf(source, trailing_eol=False)
 
293
        stream = source.get_record_stream(source.versions(), 'topological',
 
294
            False)
 
295
        f.insert_record_stream(stream)
 
296
        self.assertIdenticalVersionedFile(f, source)
 
297
 
 
298
    def test_insert_record_stream_plain_knits(self):
 
299
        """Any file should accept a stream from plain knits."""
 
300
        f = self.get_file()
 
301
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
302
            create=True, factory=KnitPlainFactory())
 
303
        get_diamond_vf(source)
 
304
        stream = source.get_record_stream(source.versions(), 'topological',
 
305
            False)
 
306
        f.insert_record_stream(stream)
 
307
        self.assertIdenticalVersionedFile(f, source)
 
308
 
 
309
    def test_insert_record_stream_plain_knits_noeol(self):
 
310
        """Any file should accept a stream from plain knits."""
 
311
        f = self.get_file()
 
312
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
313
            create=True, factory=KnitPlainFactory())
 
314
        get_diamond_vf(source, trailing_eol=False)
 
315
        stream = source.get_record_stream(source.versions(), 'topological',
 
316
            False)
 
317
        f.insert_record_stream(stream)
 
318
        self.assertIdenticalVersionedFile(f, source)
 
319
 
 
320
    def test_insert_record_stream_existing_keys(self):
 
321
        """Inserting keys already in a file should not error."""
 
322
        f = self.get_file()
 
323
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
324
            create=True, factory=KnitPlainFactory())
 
325
        get_diamond_vf(source)
 
326
        # insert some keys into f.
 
327
        get_diamond_vf(f, left_only=True)
 
328
        stream = source.get_record_stream(source.versions(), 'topological',
 
329
            False)
 
330
        f.insert_record_stream(stream)
 
331
        self.assertIdenticalVersionedFile(f, source)
 
332
 
 
333
    def test_adds_with_parent_texts(self):
 
334
        f = self.get_file()
 
335
        parent_texts = {}
 
336
        _, _, parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
 
337
        try:
 
338
            _, _, parent_texts['r1'] = f.add_lines_with_ghosts('r1',
 
339
                ['r0', 'ghost'], ['b\n', 'c\n'], parent_texts=parent_texts)
 
340
        except NotImplementedError:
 
341
            # if the format doesn't support ghosts, just add normally.
 
342
            _, _, parent_texts['r1'] = f.add_lines('r1',
 
343
                ['r0'], ['b\n', 'c\n'], parent_texts=parent_texts)
 
344
        f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
 
345
        self.assertNotEqual(None, parent_texts['r0'])
 
346
        self.assertNotEqual(None, parent_texts['r1'])
 
347
        def verify_file(f):
 
348
            versions = f.versions()
 
349
            self.assertTrue('r0' in versions)
 
350
            self.assertTrue('r1' in versions)
 
351
            self.assertTrue('r2' in versions)
 
352
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
 
353
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
 
354
            self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
 
355
            self.assertEqual(3, f.num_versions())
 
356
            origins = f.annotate('r1')
 
357
            self.assertEquals(origins[0][0], 'r0')
 
358
            self.assertEquals(origins[1][0], 'r1')
 
359
            origins = f.annotate('r2')
 
360
            self.assertEquals(origins[0][0], 'r1')
 
361
            self.assertEquals(origins[1][0], 'r2')
 
362
 
 
363
        verify_file(f)
 
364
        f = self.reopen_file()
 
365
        verify_file(f)
 
366
 
 
367
    def test_add_unicode_content(self):
 
368
        # unicode content is not permitted in versioned files. 
 
369
        # versioned files version sequences of bytes only.
 
370
        vf = self.get_file()
 
371
        self.assertRaises(errors.BzrBadParameterUnicode,
 
372
            vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
 
373
        self.assertRaises(
 
374
            (errors.BzrBadParameterUnicode, NotImplementedError),
 
375
            vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
 
376
 
 
377
    def test_add_follows_left_matching_blocks(self):
 
378
        """If we change left_matching_blocks, delta changes
 
379
 
 
380
        Note: There are multiple correct deltas in this case, because
 
381
        we start with 1 "a" and we get 3.
 
382
        """
 
383
        vf = self.get_file()
 
384
        if isinstance(vf, WeaveFile):
 
385
            raise TestSkipped("WeaveFile ignores left_matching_blocks")
 
386
        vf.add_lines('1', [], ['a\n'])
 
387
        vf.add_lines('2', ['1'], ['a\n', 'a\n', 'a\n'],
 
388
                     left_matching_blocks=[(0, 0, 1), (1, 3, 0)])
 
389
        self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('2'))
 
390
        vf.add_lines('3', ['1'], ['a\n', 'a\n', 'a\n'],
 
391
                     left_matching_blocks=[(0, 2, 1), (1, 3, 0)])
 
392
        self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('3'))
 
393
 
 
394
    def test_inline_newline_throws(self):
 
395
        # \r characters are not permitted in lines being added
 
396
        vf = self.get_file()
 
397
        self.assertRaises(errors.BzrBadParameterContainsNewline, 
 
398
            vf.add_lines, 'a', [], ['a\n\n'])
 
399
        self.assertRaises(
 
400
            (errors.BzrBadParameterContainsNewline, NotImplementedError),
 
401
            vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
 
402
        # but inline CR's are allowed
 
403
        vf.add_lines('a', [], ['a\r\n'])
 
404
        try:
 
405
            vf.add_lines_with_ghosts('b', [], ['a\r\n'])
 
406
        except NotImplementedError:
 
407
            pass
 
408
 
 
409
    def test_add_reserved(self):
 
410
        vf = self.get_file()
 
411
        self.assertRaises(errors.ReservedId,
 
412
            vf.add_lines, 'a:', [], ['a\n', 'b\n', 'c\n'])
 
413
 
 
414
    def test_add_lines_nostoresha(self):
 
415
        """When nostore_sha is supplied using old content raises."""
 
416
        vf = self.get_file()
 
417
        empty_text = ('a', [])
 
418
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
419
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
420
        shas = []
 
421
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
422
            sha, _, _ = vf.add_lines(version, [], lines)
 
423
            shas.append(sha)
 
424
        # we now have a copy of all the lines in the vf.
 
425
        for sha, (version, lines) in zip(
 
426
            shas, (empty_text, sample_text_nl, sample_text_no_nl)):
 
427
            self.assertRaises(errors.ExistingContent,
 
428
                vf.add_lines, version + "2", [], lines,
 
429
                nostore_sha=sha)
 
430
            # and no new version should have been added.
 
431
            self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
 
432
                version + "2")
 
433
 
 
434
    def test_add_lines_with_ghosts_nostoresha(self):
 
435
        """When nostore_sha is supplied using old content raises."""
 
436
        vf = self.get_file()
 
437
        empty_text = ('a', [])
 
438
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
439
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
440
        shas = []
 
441
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
442
            sha, _, _ = vf.add_lines(version, [], lines)
 
443
            shas.append(sha)
 
444
        # we now have a copy of all the lines in the vf.
 
445
        # is the test applicable to this vf implementation?
 
446
        try:
 
447
            vf.add_lines_with_ghosts('d', [], [])
 
448
        except NotImplementedError:
 
449
            raise TestSkipped("add_lines_with_ghosts is optional")
 
450
        for sha, (version, lines) in zip(
 
451
            shas, (empty_text, sample_text_nl, sample_text_no_nl)):
 
452
            self.assertRaises(errors.ExistingContent,
 
453
                vf.add_lines_with_ghosts, version + "2", [], lines,
 
454
                nostore_sha=sha)
 
455
            # and no new version should have been added.
 
456
            self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
 
457
                version + "2")
 
458
 
 
459
    def test_add_lines_return_value(self):
 
460
        # add_lines should return the sha1 and the text size.
 
461
        vf = self.get_file()
 
462
        empty_text = ('a', [])
 
463
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
464
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
465
        # check results for the three cases:
 
466
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
467
            # the first two elements are the same for all versioned files:
 
468
            # - the digest and the size of the text. For some versioned files
 
469
            #   additional data is returned in additional tuple elements.
 
470
            result = vf.add_lines(version, [], lines)
 
471
            self.assertEqual(3, len(result))
 
472
            self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
 
473
                result[0:2])
 
474
        # parents should not affect the result:
 
475
        lines = sample_text_nl[1]
 
476
        self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
 
477
            vf.add_lines('d', ['b', 'c'], lines)[0:2])
 
478
 
 
479
    def test_get_reserved(self):
 
480
        vf = self.get_file()
 
481
        self.assertRaises(errors.ReservedId, vf.get_texts, ['b:'])
 
482
        self.assertRaises(errors.ReservedId, vf.get_lines, 'b:')
 
483
        self.assertRaises(errors.ReservedId, vf.get_text, 'b:')
 
484
 
 
485
    def test_make_mpdiffs(self):
 
486
        from bzrlib import multiparent
 
487
        vf = self.get_file('foo')
 
488
        sha1s = self._setup_for_deltas(vf)
 
489
        new_vf = self.get_file('bar')
 
490
        for version in multiparent.topo_iter(vf):
 
491
            mpdiff = vf.make_mpdiffs([version])[0]
 
492
            new_vf.add_mpdiffs([(version, vf.get_parent_map([version])[version],
 
493
                                 vf.get_sha1s([version])[0], mpdiff)])
 
494
            self.assertEqualDiff(vf.get_text(version),
 
495
                                 new_vf.get_text(version))
 
496
 
 
497
    def _setup_for_deltas(self, f):
 
498
        self.assertFalse(f.has_version('base'))
 
499
        # add texts that should trip the knit maximum delta chain threshold
 
500
        # as well as doing parallel chains of data in knits.
 
501
        # this is done by two chains of 25 insertions
 
502
        f.add_lines('base', [], ['line\n'])
 
503
        f.add_lines('noeol', ['base'], ['line'])
 
504
        # detailed eol tests:
 
505
        # shared last line with parent no-eol
 
506
        f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
 
507
        # differing last line with parent, both no-eol
 
508
        f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
 
509
        # add eol following a noneol parent, change content
 
510
        f.add_lines('eol', ['noeol'], ['phone\n'])
 
511
        # add eol following a noneol parent, no change content
 
512
        f.add_lines('eolline', ['noeol'], ['line\n'])
 
513
        # noeol with no parents:
 
514
        f.add_lines('noeolbase', [], ['line'])
 
515
        # noeol preceeding its leftmost parent in the output:
 
516
        # this is done by making it a merge of two parents with no common
 
517
        # anestry: noeolbase and noeol with the 
 
518
        # later-inserted parent the leftmost.
 
519
        f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
 
520
        # two identical eol texts
 
521
        f.add_lines('noeoldup', ['noeol'], ['line'])
 
522
        next_parent = 'base'
 
523
        text_name = 'chain1-'
 
524
        text = ['line\n']
 
525
        sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
 
526
                 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
 
527
                 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
 
528
                 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
 
529
                 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
 
530
                 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
 
531
                 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
 
532
                 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
 
533
                 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
 
534
                 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
 
535
                 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
 
536
                 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
 
537
                 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
 
538
                 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
 
539
                 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
 
540
                 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
 
541
                 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
 
542
                 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
 
543
                 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
 
544
                 19:'1ebed371807ba5935958ad0884595126e8c4e823',
 
545
                 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
 
546
                 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
 
547
                 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
 
548
                 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
 
549
                 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
 
550
                 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
 
551
                 }
 
552
        for depth in range(26):
 
553
            new_version = text_name + '%s' % depth
 
554
            text = text + ['line\n']
 
555
            f.add_lines(new_version, [next_parent], text)
 
556
            next_parent = new_version
 
557
        next_parent = 'base'
 
558
        text_name = 'chain2-'
 
559
        text = ['line\n']
 
560
        for depth in range(26):
 
561
            new_version = text_name + '%s' % depth
 
562
            text = text + ['line\n']
 
563
            f.add_lines(new_version, [next_parent], text)
 
564
            next_parent = new_version
 
565
        return sha1s
 
566
 
 
567
    def test_ancestry(self):
 
568
        f = self.get_file()
 
569
        self.assertEqual([], f.get_ancestry([]))
 
570
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
571
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
 
572
        f.add_lines('r2', ['r0'], ['b\n', 'c\n'])
 
573
        f.add_lines('r3', ['r2'], ['b\n', 'c\n'])
 
574
        f.add_lines('rM', ['r1', 'r2'], ['b\n', 'c\n'])
 
575
        self.assertEqual([], f.get_ancestry([]))
 
576
        versions = f.get_ancestry(['rM'])
 
577
        # there are some possibilities:
 
578
        # r0 r1 r2 rM r3
 
579
        # r0 r1 r2 r3 rM
 
580
        # etc
 
581
        # so we check indexes
 
582
        r0 = versions.index('r0')
 
583
        r1 = versions.index('r1')
 
584
        r2 = versions.index('r2')
 
585
        self.assertFalse('r3' in versions)
 
586
        rM = versions.index('rM')
 
587
        self.assertTrue(r0 < r1)
 
588
        self.assertTrue(r0 < r2)
 
589
        self.assertTrue(r1 < rM)
 
590
        self.assertTrue(r2 < rM)
 
591
 
 
592
        self.assertRaises(RevisionNotPresent,
 
593
            f.get_ancestry, ['rM', 'rX'])
 
594
 
 
595
        self.assertEqual(set(f.get_ancestry('rM')),
 
596
            set(f.get_ancestry('rM', topo_sorted=False)))
 
597
 
 
598
    def test_mutate_after_finish(self):
 
599
        self._transaction = 'before'
 
600
        f = self.get_file()
 
601
        self._transaction = 'after'
 
602
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
 
603
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
 
604
        self.assertRaises(errors.OutSideTransaction, self.applyDeprecated,
 
605
            one_five, f.join, '')
 
606
        
 
607
    def test_clone_text(self):
 
608
        f = self.get_file()
 
609
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
610
        self.applyDeprecated(one_four, f.clone_text, 'r1', 'r0', ['r0'])
 
611
        def verify_file(f):
 
612
            self.assertEquals(f.get_lines('r1'), f.get_lines('r0'))
 
613
            self.assertEquals(f.get_lines('r1'), ['a\n', 'b\n'])
 
614
            self.assertEqual({'r1':('r0',)}, f.get_parent_map(['r1']))
 
615
            self.assertRaises(RevisionNotPresent,
 
616
                self.applyDeprecated, one_four, f.clone_text, 'r2', 'rX', [])
 
617
            self.assertRaises(RevisionAlreadyPresent,
 
618
                self.applyDeprecated, one_four, f.clone_text, 'r1', 'r0', [])
 
619
        verify_file(f)
 
620
        verify_file(self.reopen_file())
 
621
 
 
622
    def test_copy_to(self):
 
623
        f = self.get_file()
 
624
        f.add_lines('0', [], ['a\n'])
 
625
        t = MemoryTransport()
 
626
        f.copy_to('foo', t)
 
627
        for suffix in self.get_factory().get_suffixes():
 
628
            self.assertTrue(t.has('foo' + suffix))
 
629
 
 
630
    def test_get_suffixes(self):
 
631
        f = self.get_file()
 
632
        # and should be a list
 
633
        self.assertTrue(isinstance(self.get_factory().get_suffixes(), list))
 
634
 
 
635
    def build_graph(self, file, graph):
 
636
        for node in topo_sort(graph.items()):
 
637
            file.add_lines(node, graph[node], [])
 
638
 
 
639
    def test_get_graph(self):
 
640
        f = self.get_file()
 
641
        graph = {
 
642
            'v1': (),
 
643
            'v2': ('v1', ),
 
644
            'v3': ('v2', )}
 
645
        self.build_graph(f, graph)
 
646
        self.assertEqual(graph, self.applyDeprecated(one_four, f.get_graph))
 
647
    
 
648
    def test_get_graph_partial(self):
 
649
        f = self.get_file()
 
650
        complex_graph = {}
 
651
        simple_a = {
 
652
            'c': (),
 
653
            'b': ('c', ),
 
654
            'a': ('b', ),
 
655
            }
 
656
        complex_graph.update(simple_a)
 
657
        simple_b = {
 
658
            'c': (),
 
659
            'b': ('c', ),
 
660
            }
 
661
        complex_graph.update(simple_b)
 
662
        simple_gam = {
 
663
            'c': (),
 
664
            'oo': (),
 
665
            'bar': ('oo', 'c'),
 
666
            'gam': ('bar', ),
 
667
            }
 
668
        complex_graph.update(simple_gam)
 
669
        simple_b_gam = {}
 
670
        simple_b_gam.update(simple_gam)
 
671
        simple_b_gam.update(simple_b)
 
672
        self.build_graph(f, complex_graph)
 
673
        self.assertEqual(simple_a, self.applyDeprecated(one_four, f.get_graph,
 
674
            ['a']))
 
675
        self.assertEqual(simple_b, self.applyDeprecated(one_four, f.get_graph,
 
676
            ['b']))
 
677
        self.assertEqual(simple_gam, self.applyDeprecated(one_four,
 
678
            f.get_graph, ['gam']))
 
679
        self.assertEqual(simple_b_gam, self.applyDeprecated(one_four,
 
680
            f.get_graph, ['b', 'gam']))
 
681
 
 
682
    def test_get_parents(self):
 
683
        f = self.get_file()
 
684
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
685
        f.add_lines('r1', [], ['a\n', 'b\n'])
 
686
        f.add_lines('r2', [], ['a\n', 'b\n'])
 
687
        f.add_lines('r3', [], ['a\n', 'b\n'])
 
688
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
 
689
        self.assertEqual(['r0', 'r1', 'r2', 'r3'],
 
690
            self.applyDeprecated(one_four, f.get_parents, 'm'))
 
691
        self.assertRaises(RevisionNotPresent,
 
692
            self.applyDeprecated, one_four, f.get_parents, 'y')
 
693
 
 
694
    def test_get_parent_map(self):
 
695
        f = self.get_file()
 
696
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
697
        self.assertEqual(
 
698
            {'r0':()}, f.get_parent_map(['r0']))
 
699
        f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
 
700
        self.assertEqual(
 
701
            {'r1':('r0',)}, f.get_parent_map(['r1']))
 
702
        self.assertEqual(
 
703
            {'r0':(),
 
704
             'r1':('r0',)},
 
705
            f.get_parent_map(['r0', 'r1']))
 
706
        f.add_lines('r2', [], ['a\n', 'b\n'])
 
707
        f.add_lines('r3', [], ['a\n', 'b\n'])
 
708
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
 
709
        self.assertEqual(
 
710
            {'m':('r0', 'r1', 'r2', 'r3')}, f.get_parent_map(['m']))
 
711
        self.assertEqual({}, f.get_parent_map('y'))
 
712
        self.assertEqual(
 
713
            {'r0':(),
 
714
             'r1':('r0',)},
 
715
            f.get_parent_map(['r0', 'y', 'r1']))
 
716
 
 
717
    def test_annotate(self):
 
718
        f = self.get_file()
 
719
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
720
        f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
 
721
        origins = f.annotate('r1')
 
722
        self.assertEquals(origins[0][0], 'r1')
 
723
        self.assertEquals(origins[1][0], 'r0')
 
724
 
 
725
        self.assertRaises(RevisionNotPresent,
 
726
            f.annotate, 'foo')
 
727
 
 
728
    def test_detection(self):
 
729
        # Test weaves detect corruption.
 
730
        #
 
731
        # Weaves contain a checksum of their texts.
 
732
        # When a text is extracted, this checksum should be
 
733
        # verified.
 
734
 
 
735
        w = self.get_file_corrupted_text()
 
736
 
 
737
        self.assertEqual('hello\n', w.get_text('v1'))
 
738
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
739
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
740
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
741
 
 
742
        w = self.get_file_corrupted_checksum()
 
743
 
 
744
        self.assertEqual('hello\n', w.get_text('v1'))
 
745
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
746
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
747
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
748
 
 
749
    def get_file_corrupted_text(self):
 
750
        """Return a versioned file with corrupt text but valid metadata."""
 
751
        raise NotImplementedError(self.get_file_corrupted_text)
 
752
 
 
753
    def reopen_file(self, name='foo'):
 
754
        """Open the versioned file from disk again."""
 
755
        raise NotImplementedError(self.reopen_file)
 
756
 
 
757
    def test_iter_parents(self):
 
758
        """iter_parents returns the parents for many nodes."""
 
759
        f = self.get_file()
 
760
        # sample data:
 
761
        # no parents
 
762
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
763
        # 1 parents
 
764
        f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
 
765
        # 2 parents
 
766
        f.add_lines('r2', ['r1', 'r0'], ['a\n', 'b\n'])
 
767
        # XXX TODO a ghost
 
768
        # cases: each sample data individually:
 
769
        self.assertEqual(set([('r0', ())]),
 
770
            set(self.applyDeprecated(one_four, f.iter_parents, ['r0'])))
 
771
        self.assertEqual(set([('r1', ('r0', ))]),
 
772
            set(self.applyDeprecated(one_four, f.iter_parents, ['r1'])))
 
773
        self.assertEqual(set([('r2', ('r1', 'r0'))]),
 
774
            set(self.applyDeprecated(one_four, f.iter_parents, ['r2'])))
 
775
        # no nodes returned for a missing node
 
776
        self.assertEqual(set(),
 
777
            set(self.applyDeprecated(one_four, f.iter_parents, ['missing'])))
 
778
        # 1 node returned with missing nodes skipped
 
779
        self.assertEqual(set([('r1', ('r0', ))]),
 
780
            set(self.applyDeprecated(one_four, f.iter_parents, ['ghost1', 'r1',
 
781
                'ghost'])))
 
782
        # 2 nodes returned
 
783
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
784
            set(self.applyDeprecated(one_four, f.iter_parents, ['r0', 'r1'])))
 
785
        # 2 nodes returned, missing skipped
 
786
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
787
            set(self.applyDeprecated(one_four, f.iter_parents,
 
788
                ['a', 'r0', 'b', 'r1', 'c'])))
 
789
 
 
790
    def test_iter_lines_added_or_present_in_versions(self):
 
791
        # test that we get at least an equalset of the lines added by
 
792
        # versions in the weave 
 
793
        # the ordering here is to make a tree so that dumb searches have
 
794
        # more changes to muck up.
 
795
 
 
796
        class InstrumentedProgress(progress.DummyProgress):
 
797
 
 
798
            def __init__(self):
 
799
 
 
800
                progress.DummyProgress.__init__(self)
 
801
                self.updates = []
 
802
 
 
803
            def update(self, msg=None, current=None, total=None):
 
804
                self.updates.append((msg, current, total))
 
805
 
 
806
        vf = self.get_file()
 
807
        # add a base to get included
 
808
        vf.add_lines('base', [], ['base\n'])
 
809
        # add a ancestor to be included on one side
 
810
        vf.add_lines('lancestor', [], ['lancestor\n'])
 
811
        # add a ancestor to be included on the other side
 
812
        vf.add_lines('rancestor', ['base'], ['rancestor\n'])
 
813
        # add a child of rancestor with no eofile-nl
 
814
        vf.add_lines('child', ['rancestor'], ['base\n', 'child\n'])
 
815
        # add a child of lancestor and base to join the two roots
 
816
        vf.add_lines('otherchild',
 
817
                     ['lancestor', 'base'],
 
818
                     ['base\n', 'lancestor\n', 'otherchild\n'])
 
819
        def iter_with_versions(versions, expected):
 
820
            # now we need to see what lines are returned, and how often.
 
821
            lines = {}
 
822
            progress = InstrumentedProgress()
 
823
            # iterate over the lines
 
824
            for line in vf.iter_lines_added_or_present_in_versions(versions,
 
825
                pb=progress):
 
826
                lines.setdefault(line, 0)
 
827
                lines[line] += 1
 
828
            if []!= progress.updates:
 
829
                self.assertEqual(expected, progress.updates)
 
830
            return lines
 
831
        lines = iter_with_versions(['child', 'otherchild'],
 
832
                                   [('Walking content.', 0, 2),
 
833
                                    ('Walking content.', 1, 2),
 
834
                                    ('Walking content.', 2, 2)])
 
835
        # we must see child and otherchild
 
836
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
837
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
 
838
        # we dont care if we got more than that.
 
839
        
 
840
        # test all lines
 
841
        lines = iter_with_versions(None, [('Walking content.', 0, 5),
 
842
                                          ('Walking content.', 1, 5),
 
843
                                          ('Walking content.', 2, 5),
 
844
                                          ('Walking content.', 3, 5),
 
845
                                          ('Walking content.', 4, 5),
 
846
                                          ('Walking content.', 5, 5)])
 
847
        # all lines must be seen at least once
 
848
        self.assertTrue(lines[('base\n', 'base')] > 0)
 
849
        self.assertTrue(lines[('lancestor\n', 'lancestor')] > 0)
 
850
        self.assertTrue(lines[('rancestor\n', 'rancestor')] > 0)
 
851
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
852
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
 
853
 
 
854
    def test_add_lines_with_ghosts(self):
 
855
        # some versioned file formats allow lines to be added with parent
 
856
        # information that is > than that in the format. Formats that do
 
857
        # not support this need to raise NotImplementedError on the
 
858
        # add_lines_with_ghosts api.
 
859
        vf = self.get_file()
 
860
        # add a revision with ghost parents
 
861
        # The preferred form is utf8, but we should translate when needed
 
862
        parent_id_unicode = u'b\xbfse'
 
863
        parent_id_utf8 = parent_id_unicode.encode('utf8')
 
864
        try:
 
865
            vf.add_lines_with_ghosts('notbxbfse', [parent_id_utf8], [])
 
866
        except NotImplementedError:
 
867
            # check the other ghost apis are also not implemented
 
868
            self.assertRaises(NotImplementedError, vf.get_ancestry_with_ghosts, ['foo'])
 
869
            self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
 
870
            return
 
871
        vf = self.reopen_file()
 
872
        # test key graph related apis: getncestry, _graph, get_parents
 
873
        # has_version
 
874
        # - these are ghost unaware and must not be reflect ghosts
 
875
        self.assertEqual(['notbxbfse'], vf.get_ancestry('notbxbfse'))
 
876
        self.assertEqual([],
 
877
            self.applyDeprecated(one_four, vf.get_parents, 'notbxbfse'))
 
878
        self.assertEqual({'notbxbfse':()}, self.applyDeprecated(one_four,
 
879
            vf.get_graph))
 
880
        self.assertFalse(vf.has_version(parent_id_utf8))
 
881
        # we have _with_ghost apis to give us ghost information.
 
882
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
 
883
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
884
        self.assertEqual({'notbxbfse':(parent_id_utf8,)},
 
885
            self.applyDeprecated(one_four, vf.get_graph_with_ghosts))
 
886
        self.assertTrue(self.applyDeprecated(one_four, vf.has_ghost,
 
887
            parent_id_utf8))
 
888
        # if we add something that is a ghost of another, it should correct the
 
889
        # results of the prior apis
 
890
        vf.add_lines(parent_id_utf8, [], [])
 
891
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry(['notbxbfse']))
 
892
        self.assertEqual({'notbxbfse':(parent_id_utf8,)},
 
893
            vf.get_parent_map(['notbxbfse']))
 
894
        self.assertEqual({parent_id_utf8:(),
 
895
                          'notbxbfse':(parent_id_utf8, ),
 
896
                          },
 
897
                         self.applyDeprecated(one_four, vf.get_graph))
 
898
        self.assertTrue(vf.has_version(parent_id_utf8))
 
899
        # we have _with_ghost apis to give us ghost information.
 
900
        self.assertEqual([parent_id_utf8, 'notbxbfse'],
 
901
            vf.get_ancestry_with_ghosts(['notbxbfse']))
 
902
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
903
        self.assertEqual({parent_id_utf8:(),
 
904
                          'notbxbfse':(parent_id_utf8,),
 
905
                          },
 
906
            self.applyDeprecated(one_four, vf.get_graph_with_ghosts))
 
907
        self.assertFalse(self.applyDeprecated(one_four, vf.has_ghost,
 
908
            parent_id_utf8))
 
909
 
 
910
    def test_add_lines_with_ghosts_after_normal_revs(self):
 
911
        # some versioned file formats allow lines to be added with parent
 
912
        # information that is > than that in the format. Formats that do
 
913
        # not support this need to raise NotImplementedError on the
 
914
        # add_lines_with_ghosts api.
 
915
        vf = self.get_file()
 
916
        # probe for ghost support
 
917
        try:
 
918
            vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
 
919
        except NotImplementedError:
 
920
            return
 
921
        vf.add_lines_with_ghosts('references_ghost',
 
922
                                 ['base', 'a_ghost'],
 
923
                                 ['line\n', 'line_b\n', 'line_c\n'])
 
924
        origins = vf.annotate('references_ghost')
 
925
        self.assertEquals(('base', 'line\n'), origins[0])
 
926
        self.assertEquals(('base', 'line_b\n'), origins[1])
 
927
        self.assertEquals(('references_ghost', 'line_c\n'), origins[2])
 
928
 
 
929
    def test_readonly_mode(self):
 
930
        transport = get_transport(self.get_url('.'))
 
931
        factory = self.get_factory()
 
932
        vf = factory('id', transport, 0777, create=True, access_mode='w')
 
933
        vf = factory('id', transport, access_mode='r')
 
934
        self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
 
935
        self.assertRaises(errors.ReadOnlyError,
 
936
                          vf.add_lines_with_ghosts,
 
937
                          'base',
 
938
                          [],
 
939
                          [])
 
940
        self.assertRaises(errors.ReadOnlyError, self.applyDeprecated, one_five,
 
941
            vf.join, 'base')
 
942
    
 
943
    def test_get_sha1s(self):
 
944
        # check the sha1 data is available
 
945
        vf = self.get_file()
 
946
        # a simple file
 
947
        vf.add_lines('a', [], ['a\n'])
 
948
        # the same file, different metadata
 
949
        vf.add_lines('b', ['a'], ['a\n'])
 
950
        # a file differing only in last newline.
 
951
        vf.add_lines('c', [], ['a'])
 
952
        # Deprecasted single-version API.
 
953
        self.assertEqual(
 
954
            '3f786850e387550fdab836ed7e6dc881de23001b',
 
955
            self.applyDeprecated(one_four, vf.get_sha1, 'a'))
 
956
        self.assertEqual(
 
957
            '3f786850e387550fdab836ed7e6dc881de23001b',
 
958
            self.applyDeprecated(one_four, vf.get_sha1, 'b'))
 
959
        self.assertEqual(
 
960
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
961
            self.applyDeprecated(one_four, vf.get_sha1, 'c'))
 
962
        self.assertEqual(['3f786850e387550fdab836ed7e6dc881de23001b',
 
963
                          '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
964
                          '3f786850e387550fdab836ed7e6dc881de23001b'],
 
965
                          vf.get_sha1s(['a', 'c', 'b']))
 
966
        
 
967
 
 
968
class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
969
 
 
970
    def get_file(self, name='foo'):
 
971
        return WeaveFile(name, get_transport(self.get_url('.')), create=True,
 
972
            get_scope=self.get_transaction)
 
973
 
 
974
    def get_file_corrupted_text(self):
 
975
        w = WeaveFile('foo', get_transport(self.get_url('.')), create=True,
 
976
            get_scope=self.get_transaction)
 
977
        w.add_lines('v1', [], ['hello\n'])
 
978
        w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
979
        
 
980
        # We are going to invasively corrupt the text
 
981
        # Make sure the internals of weave are the same
 
982
        self.assertEqual([('{', 0)
 
983
                        , 'hello\n'
 
984
                        , ('}', None)
 
985
                        , ('{', 1)
 
986
                        , 'there\n'
 
987
                        , ('}', None)
 
988
                        ], w._weave)
 
989
        
 
990
        self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
 
991
                        , '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
992
                        ], w._sha1s)
 
993
        w.check()
 
994
        
 
995
        # Corrupted
 
996
        w._weave[4] = 'There\n'
 
997
        return w
 
998
 
 
999
    def get_file_corrupted_checksum(self):
 
1000
        w = self.get_file_corrupted_text()
 
1001
        # Corrected
 
1002
        w._weave[4] = 'there\n'
 
1003
        self.assertEqual('hello\nthere\n', w.get_text('v2'))
 
1004
        
 
1005
        #Invalid checksum, first digit changed
 
1006
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
1007
        return w
 
1008
 
 
1009
    def reopen_file(self, name='foo', create=False):
 
1010
        return WeaveFile(name, get_transport(self.get_url('.')), create=create,
 
1011
            get_scope=self.get_transaction)
 
1012
 
 
1013
    def test_no_implicit_create(self):
 
1014
        self.assertRaises(errors.NoSuchFile,
 
1015
                          WeaveFile,
 
1016
                          'foo',
 
1017
                          get_transport(self.get_url('.')),
 
1018
                          get_scope=self.get_transaction)
 
1019
 
 
1020
    def get_factory(self):
 
1021
        return WeaveFile
 
1022
 
 
1023
 
 
1024
class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
1025
 
 
1026
    def get_file(self, name='foo', create=True):
 
1027
        return make_file_knit(name, get_transport(self.get_url('.')),
 
1028
            delta=True, create=True, get_scope=self.get_transaction)
 
1029
 
 
1030
    def get_factory(self):
 
1031
        return make_file_knit
 
1032
 
 
1033
    def get_file_corrupted_text(self):
 
1034
        knit = self.get_file()
 
1035
        knit.add_lines('v1', [], ['hello\n'])
 
1036
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
1037
        return knit
 
1038
 
 
1039
    def reopen_file(self, name='foo', create=False):
 
1040
        return self.get_file(name, create)
 
1041
 
 
1042
    def test_detection(self):
 
1043
        knit = self.get_file()
 
1044
        knit.check()
 
1045
 
 
1046
    def test_no_implicit_create(self):
 
1047
        self.assertRaises(errors.NoSuchFile, self.get_factory(), 'foo',
 
1048
            get_transport(self.get_url('.')))
 
1049
 
 
1050
 
 
1051
class TestPlaintextKnit(TestKnit):
 
1052
    """Test a knit with no cached annotations"""
 
1053
 
 
1054
    def get_file(self, name='foo', create=True):
 
1055
        return make_file_knit(name, get_transport(self.get_url('.')),
 
1056
            delta=True, create=create, get_scope=self.get_transaction,
 
1057
            factory=_mod_knit.KnitPlainFactory())
 
1058
 
 
1059
 
 
1060
class TestPlanMergeVersionedFile(TestCaseWithMemoryTransport):
 
1061
 
 
1062
    def setUp(self):
 
1063
        TestCaseWithMemoryTransport.setUp(self)
 
1064
        self.vf1 = make_file_knit('root', self.get_transport(), create=True)
 
1065
        self.vf2 = make_file_knit('root', self.get_transport(), create=True)
 
1066
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root',
 
1067
            [self.vf1, self.vf2])
 
1068
 
 
1069
    def test_add_lines(self):
 
1070
        self.plan_merge_vf.add_lines('a:', [], [])
 
1071
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a', [],
 
1072
                          [])
 
1073
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', None,
 
1074
                          [])
 
1075
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', [],
 
1076
                          None)
 
1077
 
 
1078
    def test_ancestry(self):
 
1079
        self.vf1.add_lines('A', [], [])
 
1080
        self.vf1.add_lines('B', ['A'], [])
 
1081
        self.plan_merge_vf.add_lines('C:', ['B'], [])
 
1082
        self.plan_merge_vf.add_lines('D:', ['C:'], [])
 
1083
        self.assertEqual(set(['A', 'B', 'C:', 'D:']),
 
1084
            self.plan_merge_vf.get_ancestry('D:', topo_sorted=False))
 
1085
 
 
1086
    def setup_abcde(self):
 
1087
        self.vf1.add_lines('A', [], ['a'])
 
1088
        self.vf1.add_lines('B', ['A'], ['b'])
 
1089
        self.vf2.add_lines('C', [], ['c'])
 
1090
        self.vf2.add_lines('D', ['C'], ['d'])
 
1091
        self.plan_merge_vf.add_lines('E:', ['B', 'D'], ['e'])
 
1092
 
 
1093
    def test_ancestry_uses_all_versionedfiles(self):
 
1094
        self.setup_abcde()
 
1095
        self.assertEqual(set(['A', 'B', 'C', 'D', 'E:']),
 
1096
            self.plan_merge_vf.get_ancestry('E:', topo_sorted=False))
 
1097
 
 
1098
    def test_ancestry_raises_revision_not_present(self):
 
1099
        error = self.assertRaises(errors.RevisionNotPresent,
 
1100
                                  self.plan_merge_vf.get_ancestry, 'E:', False)
 
1101
        self.assertContainsRe(str(error), '{E:} not present in "root"')
 
1102
 
 
1103
    def test_get_parents(self):
 
1104
        self.setup_abcde()
 
1105
        self.assertEqual({'B':('A',)}, self.plan_merge_vf.get_parent_map(['B']))
 
1106
        self.assertEqual({'D':('C',)}, self.plan_merge_vf.get_parent_map(['D']))
 
1107
        self.assertEqual({'E:':('B', 'D')},
 
1108
            self.plan_merge_vf.get_parent_map(['E:']))
 
1109
        self.assertEqual({}, self.plan_merge_vf.get_parent_map(['F']))
 
1110
        self.assertEqual({
 
1111
                'B':('A',),
 
1112
                'D':('C',),
 
1113
                'E:':('B', 'D'),
 
1114
                }, self.plan_merge_vf.get_parent_map(['B', 'D', 'E:', 'F']))
 
1115
 
 
1116
    def test_get_lines(self):
 
1117
        self.setup_abcde()
 
1118
        self.assertEqual(['a'], self.plan_merge_vf.get_lines('A'))
 
1119
        self.assertEqual(['c'], self.plan_merge_vf.get_lines('C'))
 
1120
        self.assertEqual(['e'], self.plan_merge_vf.get_lines('E:'))
 
1121
        error = self.assertRaises(errors.RevisionNotPresent,
 
1122
                                  self.plan_merge_vf.get_lines, 'F')
 
1123
        self.assertContainsRe(str(error), '{F} not present in "root"')
 
1124
 
 
1125
 
 
1126
class InterString(versionedfile.InterVersionedFile):
 
1127
    """An inter-versionedfile optimised code path for strings.
 
1128
 
 
1129
    This is for use during testing where we use strings as versionedfiles
 
1130
    so that none of the default regsitered interversionedfile classes will
 
1131
    match - which lets us test the match logic.
 
1132
    """
 
1133
 
 
1134
    @staticmethod
 
1135
    def is_compatible(source, target):
 
1136
        """InterString is compatible with strings-as-versionedfiles."""
 
1137
        return isinstance(source, str) and isinstance(target, str)
 
1138
 
 
1139
 
 
1140
# TODO this and the InterRepository core logic should be consolidatable
 
1141
# if we make the registry a separate class though we still need to 
 
1142
# test the behaviour in the active registry to catch failure-to-handle-
 
1143
# stange-objects
 
1144
class TestInterVersionedFile(TestCaseWithMemoryTransport):
 
1145
 
 
1146
    def test_get_default_inter_versionedfile(self):
 
1147
        # test that the InterVersionedFile.get(a, b) probes
 
1148
        # for a class where is_compatible(a, b) returns
 
1149
        # true and returns a default interversionedfile otherwise.
 
1150
        # This also tests that the default registered optimised interversionedfile
 
1151
        # classes do not barf inappropriately when a surprising versionedfile type
 
1152
        # is handed to them.
 
1153
        dummy_a = "VersionedFile 1."
 
1154
        dummy_b = "VersionedFile 2."
 
1155
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
1156
 
 
1157
    def assertGetsDefaultInterVersionedFile(self, a, b):
 
1158
        """Asserts that InterVersionedFile.get(a, b) -> the default."""
 
1159
        inter = versionedfile.InterVersionedFile.get(a, b)
 
1160
        self.assertEqual(versionedfile.InterVersionedFile,
 
1161
                         inter.__class__)
 
1162
        self.assertEqual(a, inter.source)
 
1163
        self.assertEqual(b, inter.target)
 
1164
 
 
1165
    def test_register_inter_versionedfile_class(self):
 
1166
        # test that a optimised code path provider - a
 
1167
        # InterVersionedFile subclass can be registered and unregistered
 
1168
        # and that it is correctly selected when given a versionedfile
 
1169
        # pair that it returns true on for the is_compatible static method
 
1170
        # check
 
1171
        dummy_a = "VersionedFile 1."
 
1172
        dummy_b = "VersionedFile 2."
 
1173
        versionedfile.InterVersionedFile.register_optimiser(InterString)
 
1174
        try:
 
1175
            # we should get the default for something InterString returns False
 
1176
            # to
 
1177
            self.assertFalse(InterString.is_compatible(dummy_a, None))
 
1178
            self.assertGetsDefaultInterVersionedFile(dummy_a, None)
 
1179
            # and we should get an InterString for a pair it 'likes'
 
1180
            self.assertTrue(InterString.is_compatible(dummy_a, dummy_b))
 
1181
            inter = versionedfile.InterVersionedFile.get(dummy_a, dummy_b)
 
1182
            self.assertEqual(InterString, inter.__class__)
 
1183
            self.assertEqual(dummy_a, inter.source)
 
1184
            self.assertEqual(dummy_b, inter.target)
 
1185
        finally:
 
1186
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
 
1187
        # now we should get the default InterVersionedFile object again.
 
1188
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
1189
 
 
1190
 
 
1191
class TestReadonlyHttpMixin(object):
 
1192
 
 
1193
    def get_transaction(self):
 
1194
        return 1
 
1195
 
 
1196
    def test_readonly_http_works(self):
 
1197
        # we should be able to read from http with a versioned file.
 
1198
        vf = self.get_file()
 
1199
        # try an empty file access
 
1200
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
1201
        self.assertEqual([], readonly_vf.versions())
 
1202
        # now with feeling.
 
1203
        vf.add_lines('1', [], ['a\n'])
 
1204
        vf.add_lines('2', ['1'], ['b\n', 'a\n'])
 
1205
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
1206
        self.assertEqual(['1', '2'], vf.versions())
 
1207
        for version in readonly_vf.versions():
 
1208
            readonly_vf.get_lines(version)
 
1209
 
 
1210
 
 
1211
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
1212
 
 
1213
    def get_file(self):
 
1214
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True,
 
1215
            get_scope=self.get_transaction)
 
1216
 
 
1217
    def get_factory(self):
 
1218
        return WeaveFile
 
1219
 
 
1220
 
 
1221
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
1222
 
 
1223
    def get_file(self):
 
1224
        return make_file_knit('foo', get_transport(self.get_url('.')),
 
1225
            delta=True, create=True, get_scope=self.get_transaction)
 
1226
 
 
1227
    def get_factory(self):
 
1228
        return make_file_knit
 
1229
 
 
1230
 
 
1231
class MergeCasesMixin(object):
 
1232
 
 
1233
    def doMerge(self, base, a, b, mp):
 
1234
        from cStringIO import StringIO
 
1235
        from textwrap import dedent
 
1236
 
 
1237
        def addcrlf(x):
 
1238
            return x + '\n'
 
1239
        
 
1240
        w = self.get_file()
 
1241
        w.add_lines('text0', [], map(addcrlf, base))
 
1242
        w.add_lines('text1', ['text0'], map(addcrlf, a))
 
1243
        w.add_lines('text2', ['text0'], map(addcrlf, b))
 
1244
 
 
1245
        self.log_contents(w)
 
1246
 
 
1247
        self.log('merge plan:')
 
1248
        p = list(w.plan_merge('text1', 'text2'))
 
1249
        for state, line in p:
 
1250
            if line:
 
1251
                self.log('%12s | %s' % (state, line[:-1]))
 
1252
 
 
1253
        self.log('merge:')
 
1254
        mt = StringIO()
 
1255
        mt.writelines(w.weave_merge(p))
 
1256
        mt.seek(0)
 
1257
        self.log(mt.getvalue())
 
1258
 
 
1259
        mp = map(addcrlf, mp)
 
1260
        self.assertEqual(mt.readlines(), mp)
 
1261
        
 
1262
        
 
1263
    def testOneInsert(self):
 
1264
        self.doMerge([],
 
1265
                     ['aa'],
 
1266
                     [],
 
1267
                     ['aa'])
 
1268
 
 
1269
    def testSeparateInserts(self):
 
1270
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1271
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1272
                     ['aaa', 'bbb', 'yyy', 'ccc'],
 
1273
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1274
 
 
1275
    def testSameInsert(self):
 
1276
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1277
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1278
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
 
1279
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1280
    overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
 
1281
    def testOverlappedInsert(self):
 
1282
        self.doMerge(['aaa', 'bbb'],
 
1283
                     ['aaa', 'xxx', 'yyy', 'bbb'],
 
1284
                     ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
 
1285
 
 
1286
        # really it ought to reduce this to 
 
1287
        # ['aaa', 'xxx', 'yyy', 'bbb']
 
1288
 
 
1289
 
 
1290
    def testClashReplace(self):
 
1291
        self.doMerge(['aaa'],
 
1292
                     ['xxx'],
 
1293
                     ['yyy', 'zzz'],
 
1294
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
 
1295
                      '>>>>>>> '])
 
1296
 
 
1297
    def testNonClashInsert1(self):
 
1298
        self.doMerge(['aaa'],
 
1299
                     ['xxx', 'aaa'],
 
1300
                     ['yyy', 'zzz'],
 
1301
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
 
1302
                      '>>>>>>> '])
 
1303
 
 
1304
    def testNonClashInsert2(self):
 
1305
        self.doMerge(['aaa'],
 
1306
                     ['aaa'],
 
1307
                     ['yyy', 'zzz'],
 
1308
                     ['yyy', 'zzz'])
 
1309
 
 
1310
 
 
1311
    def testDeleteAndModify(self):
 
1312
        """Clashing delete and modification.
 
1313
 
 
1314
        If one side modifies a region and the other deletes it then
 
1315
        there should be a conflict with one side blank.
 
1316
        """
 
1317
 
 
1318
        #######################################
 
1319
        # skippd, not working yet
 
1320
        return
 
1321
        
 
1322
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1323
                     ['aaa', 'ddd', 'ccc'],
 
1324
                     ['aaa', 'ccc'],
 
1325
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
 
1326
 
 
1327
    def _test_merge_from_strings(self, base, a, b, expected):
 
1328
        w = self.get_file()
 
1329
        w.add_lines('text0', [], base.splitlines(True))
 
1330
        w.add_lines('text1', ['text0'], a.splitlines(True))
 
1331
        w.add_lines('text2', ['text0'], b.splitlines(True))
 
1332
        self.log('merge plan:')
 
1333
        p = list(w.plan_merge('text1', 'text2'))
 
1334
        for state, line in p:
 
1335
            if line:
 
1336
                self.log('%12s | %s' % (state, line[:-1]))
 
1337
        self.log('merge result:')
 
1338
        result_text = ''.join(w.weave_merge(p))
 
1339
        self.log(result_text)
 
1340
        self.assertEqualDiff(result_text, expected)
 
1341
 
 
1342
    def test_weave_merge_conflicts(self):
 
1343
        # does weave merge properly handle plans that end with unchanged?
 
1344
        result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
 
1345
        self.assertEqual(result, 'hello\n')
 
1346
 
 
1347
    def test_deletion_extended(self):
 
1348
        """One side deletes, the other deletes more.
 
1349
        """
 
1350
        base = """\
 
1351
            line 1
 
1352
            line 2
 
1353
            line 3
 
1354
            """
 
1355
        a = """\
 
1356
            line 1
 
1357
            line 2
 
1358
            """
 
1359
        b = """\
 
1360
            line 1
 
1361
            """
 
1362
        result = """\
 
1363
            line 1
 
1364
            """
 
1365
        self._test_merge_from_strings(base, a, b, result)
 
1366
 
 
1367
    def test_deletion_overlap(self):
 
1368
        """Delete overlapping regions with no other conflict.
 
1369
 
 
1370
        Arguably it'd be better to treat these as agreement, rather than 
 
1371
        conflict, but for now conflict is safer.
 
1372
        """
 
1373
        base = """\
 
1374
            start context
 
1375
            int a() {}
 
1376
            int b() {}
 
1377
            int c() {}
 
1378
            end context
 
1379
            """
 
1380
        a = """\
 
1381
            start context
 
1382
            int a() {}
 
1383
            end context
 
1384
            """
 
1385
        b = """\
 
1386
            start context
 
1387
            int c() {}
 
1388
            end context
 
1389
            """
 
1390
        result = """\
 
1391
            start context
 
1392
<<<<<<< 
 
1393
            int a() {}
 
1394
=======
 
1395
            int c() {}
 
1396
>>>>>>> 
 
1397
            end context
 
1398
            """
 
1399
        self._test_merge_from_strings(base, a, b, result)
 
1400
 
 
1401
    def test_agreement_deletion(self):
 
1402
        """Agree to delete some lines, without conflicts."""
 
1403
        base = """\
 
1404
            start context
 
1405
            base line 1
 
1406
            base line 2
 
1407
            end context
 
1408
            """
 
1409
        a = """\
 
1410
            start context
 
1411
            base line 1
 
1412
            end context
 
1413
            """
 
1414
        b = """\
 
1415
            start context
 
1416
            base line 1
 
1417
            end context
 
1418
            """
 
1419
        result = """\
 
1420
            start context
 
1421
            base line 1
 
1422
            end context
 
1423
            """
 
1424
        self._test_merge_from_strings(base, a, b, result)
 
1425
 
 
1426
    def test_sync_on_deletion(self):
 
1427
        """Specific case of merge where we can synchronize incorrectly.
 
1428
        
 
1429
        A previous version of the weave merge concluded that the two versions
 
1430
        agreed on deleting line 2, and this could be a synchronization point.
 
1431
        Line 1 was then considered in isolation, and thought to be deleted on 
 
1432
        both sides.
 
1433
 
 
1434
        It's better to consider the whole thing as a disagreement region.
 
1435
        """
 
1436
        base = """\
 
1437
            start context
 
1438
            base line 1
 
1439
            base line 2
 
1440
            end context
 
1441
            """
 
1442
        a = """\
 
1443
            start context
 
1444
            base line 1
 
1445
            a's replacement line 2
 
1446
            end context
 
1447
            """
 
1448
        b = """\
 
1449
            start context
 
1450
            b replaces
 
1451
            both lines
 
1452
            end context
 
1453
            """
 
1454
        result = """\
 
1455
            start context
 
1456
<<<<<<< 
 
1457
            base line 1
 
1458
            a's replacement line 2
 
1459
=======
 
1460
            b replaces
 
1461
            both lines
 
1462
>>>>>>> 
 
1463
            end context
 
1464
            """
 
1465
        self._test_merge_from_strings(base, a, b, result)
 
1466
 
 
1467
 
 
1468
class TestKnitMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1469
 
 
1470
    def get_file(self, name='foo'):
 
1471
        return make_file_knit(name, get_transport(self.get_url('.')),
 
1472
                                 delta=True, create=True)
 
1473
 
 
1474
    def log_contents(self, w):
 
1475
        pass
 
1476
 
 
1477
 
 
1478
class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1479
 
 
1480
    def get_file(self, name='foo'):
 
1481
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
1482
 
 
1483
    def log_contents(self, w):
 
1484
        self.log('weave is:')
 
1485
        tmpf = StringIO()
 
1486
        write_weave(w, tmpf)
 
1487
        self.log(tmpf.getvalue())
 
1488
 
 
1489
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
 
1490
                                'xxx', '>>>>>>> ', 'bbb']
 
1491
 
 
1492
 
 
1493
class TestContentFactoryAdaption(TestCaseWithMemoryTransport):
 
1494
 
 
1495
    def test_select_adaptor(self):
 
1496
        """Test expected adapters exist."""
 
1497
        # One scenario for each lookup combination we expect to use.
 
1498
        # Each is source_kind, requested_kind, adapter class
 
1499
        scenarios = [
 
1500
            ('knit-delta-gz', 'fulltext', _mod_knit.DeltaPlainToFullText),
 
1501
            ('knit-ft-gz', 'fulltext', _mod_knit.FTPlainToFullText),
 
1502
            ('knit-annotated-delta-gz', 'knit-delta-gz',
 
1503
                _mod_knit.DeltaAnnotatedToUnannotated),
 
1504
            ('knit-annotated-delta-gz', 'fulltext',
 
1505
                _mod_knit.DeltaAnnotatedToFullText),
 
1506
            ('knit-annotated-ft-gz', 'knit-ft-gz',
 
1507
                _mod_knit.FTAnnotatedToUnannotated),
 
1508
            ('knit-annotated-ft-gz', 'fulltext',
 
1509
                _mod_knit.FTAnnotatedToFullText),
 
1510
            ]
 
1511
        for source, requested, klass in scenarios:
 
1512
            adapter_factory = versionedfile.adapter_registry.get(
 
1513
                (source, requested))
 
1514
            adapter = adapter_factory(None)
 
1515
            self.assertIsInstance(adapter, klass)
 
1516
 
 
1517
    def get_knit(self, annotated=True):
 
1518
        if annotated:
 
1519
            factory = KnitAnnotateFactory()
 
1520
        else:
 
1521
            factory = KnitPlainFactory()
 
1522
        return make_file_knit('knit', self.get_transport('.'), delta=True,
 
1523
            create=True, factory=factory)
 
1524
 
 
1525
    def helpGetBytes(self, f, ft_adapter, delta_adapter):
 
1526
        """grab the interested adapted texts for tests."""
 
1527
        # origin is a fulltext
 
1528
        entries = f.get_record_stream(['origin'], 'unordered', False)
 
1529
        base = entries.next()
 
1530
        ft_data = ft_adapter.get_bytes(base, base.get_bytes_as(base.storage_kind))
 
1531
        # merged is both a delta and multiple parents.
 
1532
        entries = f.get_record_stream(['merged'], 'unordered', False)
 
1533
        merged = entries.next()
 
1534
        delta_data = delta_adapter.get_bytes(merged,
 
1535
            merged.get_bytes_as(merged.storage_kind))
 
1536
        return ft_data, delta_data
 
1537
 
 
1538
    def test_deannotation_noeol(self):
 
1539
        """Test converting annotated knits to unannotated knits."""
 
1540
        # we need a full text, and a delta
 
1541
        f, parents = get_diamond_vf(self.get_knit(), trailing_eol=False)
 
1542
        ft_data, delta_data = self.helpGetBytes(f,
 
1543
            _mod_knit.FTAnnotatedToUnannotated(None),
 
1544
            _mod_knit.DeltaAnnotatedToUnannotated(None))
 
1545
        self.assertEqual(
 
1546
            'version origin 1 b284f94827db1fa2970d9e2014f080413b547a7e\n'
 
1547
            'origin\n'
 
1548
            'end origin\n',
 
1549
            GzipFile(mode='rb', fileobj=StringIO(ft_data)).read())
 
1550
        self.assertEqual(
 
1551
            'version merged 4 32c2e79763b3f90e8ccde37f9710b6629c25a796\n'
 
1552
            '1,2,3\nleft\nright\nmerged\nend merged\n',
 
1553
            GzipFile(mode='rb', fileobj=StringIO(delta_data)).read())
 
1554
 
 
1555
    def test_deannotation(self):
 
1556
        """Test converting annotated knits to unannotated knits."""
 
1557
        # we need a full text, and a delta
 
1558
        f, parents = get_diamond_vf(self.get_knit())
 
1559
        ft_data, delta_data = self.helpGetBytes(f,
 
1560
            _mod_knit.FTAnnotatedToUnannotated(None),
 
1561
            _mod_knit.DeltaAnnotatedToUnannotated(None))
 
1562
        self.assertEqual(
 
1563
            'version origin 1 00e364d235126be43292ab09cb4686cf703ddc17\n'
 
1564
            'origin\n'
 
1565
            'end origin\n',
 
1566
            GzipFile(mode='rb', fileobj=StringIO(ft_data)).read())
 
1567
        self.assertEqual(
 
1568
            'version merged 3 ed8bce375198ea62444dc71952b22cfc2b09226d\n'
 
1569
            '2,2,2\nright\nmerged\nend merged\n',
 
1570
            GzipFile(mode='rb', fileobj=StringIO(delta_data)).read())
 
1571
 
 
1572
    def test_annotated_to_fulltext_no_eol(self):
 
1573
        """Test adapting annotated knits to full texts (for -> weaves)."""
 
1574
        # we need a full text, and a delta
 
1575
        f, parents = get_diamond_vf(self.get_knit(), trailing_eol=False)
 
1576
        # Reconstructing a full text requires a backing versioned file, and it
 
1577
        # must have the base lines requested from it.
 
1578
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1579
        ft_data, delta_data = self.helpGetBytes(f,
 
1580
            _mod_knit.FTAnnotatedToFullText(None),
 
1581
            _mod_knit.DeltaAnnotatedToFullText(logged_vf))
 
1582
        self.assertEqual('origin', ft_data)
 
1583
        self.assertEqual('base\nleft\nright\nmerged', delta_data)
 
1584
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
 
1585
 
 
1586
    def test_annotated_to_fulltext(self):
 
1587
        """Test adapting annotated knits to full texts (for -> weaves)."""
 
1588
        # we need a full text, and a delta
 
1589
        f, parents = get_diamond_vf(self.get_knit())
 
1590
        # Reconstructing a full text requires a backing versioned file, and it
 
1591
        # must have the base lines requested from it.
 
1592
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1593
        ft_data, delta_data = self.helpGetBytes(f,
 
1594
            _mod_knit.FTAnnotatedToFullText(None),
 
1595
            _mod_knit.DeltaAnnotatedToFullText(logged_vf))
 
1596
        self.assertEqual('origin\n', ft_data)
 
1597
        self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
 
1598
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
 
1599
 
 
1600
    def test_unannotated_to_fulltext(self):
 
1601
        """Test adapting unannotated knits to full texts.
 
1602
        
 
1603
        This is used for -> weaves, and for -> annotated knits.
 
1604
        """
 
1605
        # we need a full text, and a delta
 
1606
        f, parents = get_diamond_vf(self.get_knit(annotated=False))
 
1607
        # Reconstructing a full text requires a backing versioned file, and it
 
1608
        # must have the base lines requested from it.
 
1609
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1610
        ft_data, delta_data = self.helpGetBytes(f,
 
1611
            _mod_knit.FTPlainToFullText(None),
 
1612
            _mod_knit.DeltaPlainToFullText(logged_vf))
 
1613
        self.assertEqual('origin\n', ft_data)
 
1614
        self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
 
1615
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
 
1616
 
 
1617
    def test_unannotated_to_fulltext_no_eol(self):
 
1618
        """Test adapting unannotated knits to full texts.
 
1619
        
 
1620
        This is used for -> weaves, and for -> annotated knits.
 
1621
        """
 
1622
        # we need a full text, and a delta
 
1623
        f, parents = get_diamond_vf(self.get_knit(annotated=False),
 
1624
            trailing_eol=False)
 
1625
        # Reconstructing a full text requires a backing versioned file, and it
 
1626
        # must have the base lines requested from it.
 
1627
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1628
        ft_data, delta_data = self.helpGetBytes(f,
 
1629
            _mod_knit.FTPlainToFullText(None),
 
1630
            _mod_knit.DeltaPlainToFullText(logged_vf))
 
1631
        self.assertEqual('origin', ft_data)
 
1632
        self.assertEqual('base\nleft\nright\nmerged', delta_data)
 
1633
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
 
1634