~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: abentley
  • Date: 2006-04-20 23:47:53 UTC
  • mfrom: (1681 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1683.
  • Revision ID: abentley@lappy-20060420234753-6a6874b76f09f86d
Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
 
20
20
 
 
21
from StringIO import StringIO
 
22
 
21
23
import bzrlib
22
24
import bzrlib.errors as errors
23
25
from bzrlib.errors import (
28
30
from bzrlib.knit import KnitVersionedFile, \
29
31
     KnitAnnotateFactory
30
32
from bzrlib.tests import TestCaseWithTransport
 
33
from bzrlib.tests.HTTPTestUtil import TestCaseWithWebserver
31
34
from bzrlib.trace import mutter
32
35
from bzrlib.transport import get_transport
33
36
from bzrlib.transport.memory import MemoryTransport
34
37
import bzrlib.versionedfile as versionedfile
35
38
from bzrlib.weave import WeaveFile
36
 
from bzrlib.weavefile import read_weave
 
39
from bzrlib.weavefile import read_weave, write_weave
37
40
 
38
41
 
39
42
class VersionedFileTestMixIn(object):
63
66
            self.assertRaises(RevisionAlreadyPresent,
64
67
                f.add_lines, 'r1', [], [])
65
68
        verify_file(f)
 
69
        # this checks that reopen with create=True does not break anything.
 
70
        f = self.reopen_file(create=True)
 
71
        verify_file(f)
 
72
 
 
73
    def test_adds_with_parent_texts(self):
 
74
        f = self.get_file()
 
75
        parent_texts = {}
 
76
        parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
 
77
        try:
 
78
            parent_texts['r1'] = f.add_lines_with_ghosts('r1',
 
79
                                                         ['r0', 'ghost'], 
 
80
                                                         ['b\n', 'c\n'],
 
81
                                                         parent_texts=parent_texts)
 
82
        except NotImplementedError:
 
83
            # if the format doesn't support ghosts, just add normally.
 
84
            parent_texts['r1'] = f.add_lines('r1',
 
85
                                             ['r0'], 
 
86
                                             ['b\n', 'c\n'],
 
87
                                             parent_texts=parent_texts)
 
88
        f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
 
89
        self.assertNotEqual(None, parent_texts['r0'])
 
90
        self.assertNotEqual(None, parent_texts['r1'])
 
91
        def verify_file(f):
 
92
            versions = f.versions()
 
93
            self.assertTrue('r0' in versions)
 
94
            self.assertTrue('r1' in versions)
 
95
            self.assertTrue('r2' in versions)
 
96
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
 
97
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
 
98
            self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
 
99
            self.assertEqual(3, f.num_versions())
 
100
            origins = f.annotate('r1')
 
101
            self.assertEquals(origins[0][0], 'r0')
 
102
            self.assertEquals(origins[1][0], 'r1')
 
103
            origins = f.annotate('r2')
 
104
            self.assertEquals(origins[0][0], 'r1')
 
105
            self.assertEquals(origins[1][0], 'r2')
 
106
 
 
107
        verify_file(f)
66
108
        f = self.reopen_file()
67
109
        verify_file(f)
68
110
 
 
111
    def test_add_unicode_content(self):
 
112
        # unicode content is not permitted in versioned files. 
 
113
        # versioned files version sequences of bytes only.
 
114
        vf = self.get_file()
 
115
        self.assertRaises(errors.BzrBadParameterUnicode,
 
116
            vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
 
117
        self.assertRaises(
 
118
            (errors.BzrBadParameterUnicode, NotImplementedError),
 
119
            vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
 
120
 
 
121
    def test_inline_newline_throws(self):
 
122
        # \r characters are not permitted in lines being added
 
123
        vf = self.get_file()
 
124
        self.assertRaises(errors.BzrBadParameterContainsNewline, 
 
125
            vf.add_lines, 'a', [], ['a\n\n'])
 
126
        self.assertRaises(
 
127
            (errors.BzrBadParameterContainsNewline, NotImplementedError),
 
128
            vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
 
129
        # but inline CR's are allowed
 
130
        vf.add_lines('a', [], ['a\r\n'])
 
131
        try:
 
132
            vf.add_lines_with_ghosts('b', [], ['a\r\n'])
 
133
        except NotImplementedError:
 
134
            pass
 
135
 
 
136
    def test_get_delta(self):
 
137
        f = self.get_file()
 
138
        sha1s = self._setup_for_deltas(f)
 
139
        expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
 
140
                          [(0, 0, 1, [('base', 'line\n')])])
 
141
        self.assertEqual(expected_delta, f.get_delta('base'))
 
142
        next_parent = 'base'
 
143
        text_name = 'chain1-'
 
144
        for depth in range(26):
 
145
            new_version = text_name + '%s' % depth
 
146
            expected_delta = (next_parent, sha1s[depth], 
 
147
                              False,
 
148
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
149
            self.assertEqual(expected_delta, f.get_delta(new_version))
 
150
            next_parent = new_version
 
151
        next_parent = 'base'
 
152
        text_name = 'chain2-'
 
153
        for depth in range(26):
 
154
            new_version = text_name + '%s' % depth
 
155
            expected_delta = (next_parent, sha1s[depth], False,
 
156
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
157
            self.assertEqual(expected_delta, f.get_delta(new_version))
 
158
            next_parent = new_version
 
159
        # smoke test for eol support
 
160
        expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
 
161
        self.assertEqual(['line'], f.get_lines('noeol'))
 
162
        self.assertEqual(expected_delta, f.get_delta('noeol'))
 
163
 
 
164
    def test_get_deltas(self):
 
165
        f = self.get_file()
 
166
        sha1s = self._setup_for_deltas(f)
 
167
        deltas = f.get_deltas(f.versions())
 
168
        expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
 
169
                          [(0, 0, 1, [('base', 'line\n')])])
 
170
        self.assertEqual(expected_delta, deltas['base'])
 
171
        next_parent = 'base'
 
172
        text_name = 'chain1-'
 
173
        for depth in range(26):
 
174
            new_version = text_name + '%s' % depth
 
175
            expected_delta = (next_parent, sha1s[depth], 
 
176
                              False,
 
177
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
178
            self.assertEqual(expected_delta, deltas[new_version])
 
179
            next_parent = new_version
 
180
        next_parent = 'base'
 
181
        text_name = 'chain2-'
 
182
        for depth in range(26):
 
183
            new_version = text_name + '%s' % depth
 
184
            expected_delta = (next_parent, sha1s[depth], False,
 
185
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
186
            self.assertEqual(expected_delta, deltas[new_version])
 
187
            next_parent = new_version
 
188
        # smoke tests for eol support
 
189
        expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
 
190
        self.assertEqual(['line'], f.get_lines('noeol'))
 
191
        self.assertEqual(expected_delta, deltas['noeol'])
 
192
        # smoke tests for eol support - two noeol in a row same content
 
193
        expected_deltas = (('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
 
194
                          [(0, 1, 2, [(u'noeolsecond', 'line\n'), (u'noeolsecond', 'line\n')])]),
 
195
                          ('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
 
196
                           [(0, 0, 1, [('noeolsecond', 'line\n')]), (1, 1, 0, [])]))
 
197
        self.assertEqual(['line\n', 'line'], f.get_lines('noeolsecond'))
 
198
        self.assertTrue(deltas['noeolsecond'] in expected_deltas)
 
199
        # two no-eol in a row, different content
 
200
        expected_delta = ('noeolsecond', '8bb553a84e019ef1149db082d65f3133b195223b', True, 
 
201
                          [(1, 2, 1, [(u'noeolnotshared', 'phone\n')])])
 
202
        self.assertEqual(['line\n', 'phone'], f.get_lines('noeolnotshared'))
 
203
        self.assertEqual(expected_delta, deltas['noeolnotshared'])
 
204
        # eol folling a no-eol with content change
 
205
        expected_delta = ('noeol', 'a61f6fb6cfc4596e8d88c34a308d1e724caf8977', False, 
 
206
                          [(0, 1, 1, [(u'eol', 'phone\n')])])
 
207
        self.assertEqual(['phone\n'], f.get_lines('eol'))
 
208
        self.assertEqual(expected_delta, deltas['eol'])
 
209
        # eol folling a no-eol with content change
 
210
        expected_delta = ('noeol', '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
 
211
                          [(0, 1, 1, [(u'eolline', 'line\n')])])
 
212
        self.assertEqual(['line\n'], f.get_lines('eolline'))
 
213
        self.assertEqual(expected_delta, deltas['eolline'])
 
214
        # eol with no parents
 
215
        expected_delta = (None, '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, 
 
216
                          [(0, 0, 1, [(u'noeolbase', 'line\n')])])
 
217
        self.assertEqual(['line'], f.get_lines('noeolbase'))
 
218
        self.assertEqual(expected_delta, deltas['noeolbase'])
 
219
        # eol with two parents, in inverse insertion order
 
220
        expected_deltas = (('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
 
221
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]),
 
222
                           ('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
 
223
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]))
 
224
        self.assertEqual(['line'], f.get_lines('eolbeforefirstparent'))
 
225
        #self.assertTrue(deltas['eolbeforefirstparent'] in expected_deltas)
 
226
 
 
227
    def _setup_for_deltas(self, f):
 
228
        self.assertRaises(errors.RevisionNotPresent, f.get_delta, 'base')
 
229
        # add texts that should trip the knit maximum delta chain threshold
 
230
        # as well as doing parallel chains of data in knits.
 
231
        # this is done by two chains of 25 insertions
 
232
        f.add_lines('base', [], ['line\n'])
 
233
        f.add_lines('noeol', ['base'], ['line'])
 
234
        # detailed eol tests:
 
235
        # shared last line with parent no-eol
 
236
        f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
 
237
        # differing last line with parent, both no-eol
 
238
        f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
 
239
        # add eol following a noneol parent, change content
 
240
        f.add_lines('eol', ['noeol'], ['phone\n'])
 
241
        # add eol following a noneol parent, no change content
 
242
        f.add_lines('eolline', ['noeol'], ['line\n'])
 
243
        # noeol with no parents:
 
244
        f.add_lines('noeolbase', [], ['line'])
 
245
        # noeol preceeding its leftmost parent in the output:
 
246
        # this is done by making it a merge of two parents with no common
 
247
        # anestry: noeolbase and noeol with the 
 
248
        # later-inserted parent the leftmost.
 
249
        f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
 
250
        # two identical eol texts
 
251
        f.add_lines('noeoldup', ['noeol'], ['line'])
 
252
        next_parent = 'base'
 
253
        text_name = 'chain1-'
 
254
        text = ['line\n']
 
255
        sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
 
256
                 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
 
257
                 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
 
258
                 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
 
259
                 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
 
260
                 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
 
261
                 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
 
262
                 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
 
263
                 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
 
264
                 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
 
265
                 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
 
266
                 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
 
267
                 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
 
268
                 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
 
269
                 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
 
270
                 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
 
271
                 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
 
272
                 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
 
273
                 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
 
274
                 19:'1ebed371807ba5935958ad0884595126e8c4e823',
 
275
                 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
 
276
                 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
 
277
                 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
 
278
                 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
 
279
                 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
 
280
                 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
 
281
                 }
 
282
        for depth in range(26):
 
283
            new_version = text_name + '%s' % depth
 
284
            text = text + ['line\n']
 
285
            f.add_lines(new_version, [next_parent], text)
 
286
            next_parent = new_version
 
287
        next_parent = 'base'
 
288
        text_name = 'chain2-'
 
289
        text = ['line\n']
 
290
        for depth in range(26):
 
291
            new_version = text_name + '%s' % depth
 
292
            text = text + ['line\n']
 
293
            f.add_lines(new_version, [next_parent], text)
 
294
            next_parent = new_version
 
295
        return sha1s
 
296
 
 
297
    def test_add_delta(self):
 
298
        # tests for the add-delta facility.
 
299
        # at this point, optimising for speed, we assume no checks when deltas are inserted.
 
300
        # this may need to be revisited.
 
301
        source = self.get_file('source')
 
302
        source.add_lines('base', [], ['line\n'])
 
303
        next_parent = 'base'
 
304
        text_name = 'chain1-'
 
305
        text = ['line\n']
 
306
        for depth in range(26):
 
307
            new_version = text_name + '%s' % depth
 
308
            text = text + ['line\n']
 
309
            source.add_lines(new_version, [next_parent], text)
 
310
            next_parent = new_version
 
311
        next_parent = 'base'
 
312
        text_name = 'chain2-'
 
313
        text = ['line\n']
 
314
        for depth in range(26):
 
315
            new_version = text_name + '%s' % depth
 
316
            text = text + ['line\n']
 
317
            source.add_lines(new_version, [next_parent], text)
 
318
            next_parent = new_version
 
319
        source.add_lines('noeol', ['base'], ['line'])
 
320
        
 
321
        target = self.get_file('target')
 
322
        for version in source.versions():
 
323
            parent, sha1, noeol, delta = source.get_delta(version)
 
324
            target.add_delta(version,
 
325
                             source.get_parents(version),
 
326
                             parent,
 
327
                             sha1,
 
328
                             noeol,
 
329
                             delta)
 
330
        self.assertRaises(RevisionAlreadyPresent,
 
331
                          target.add_delta, 'base', [], None, '', False, [])
 
332
        for version in source.versions():
 
333
            self.assertEqual(source.get_lines(version),
 
334
                             target.get_lines(version))
 
335
 
69
336
    def test_ancestry(self):
70
337
        f = self.get_file()
71
338
        self.assertEqual([], f.get_ancestry([]))
97
364
    def test_mutate_after_finish(self):
98
365
        f = self.get_file()
99
366
        f.transaction_finished()
 
367
        self.assertRaises(errors.OutSideTransaction, f.add_delta, '', [], '', '', False, [])
100
368
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
101
369
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
102
370
        self.assertRaises(errors.OutSideTransaction, f.fix_parents, '', [])
387
655
        factory = self.get_factory()
388
656
        vf = factory('id', transport, 0777, create=True, access_mode='w')
389
657
        vf = factory('id', transport, access_mode='r')
 
658
        self.assertRaises(errors.ReadOnlyError, vf.add_delta, '', [], '', '', False, [])
390
659
        self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
391
660
        self.assertRaises(errors.ReadOnlyError,
392
661
                          vf.add_lines_with_ghosts,
396
665
        self.assertRaises(errors.ReadOnlyError, vf.fix_parents, 'base', [])
397
666
        self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
398
667
        self.assertRaises(errors.ReadOnlyError, vf.clone_text, 'base', 'bar', ['foo'])
 
668
    
 
669
    def test_get_sha1(self):
 
670
        # check the sha1 data is available
 
671
        vf = self.get_file()
 
672
        # a simple file
 
673
        vf.add_lines('a', [], ['a\n'])
 
674
        # the same file, different metadata
 
675
        vf.add_lines('b', ['a'], ['a\n'])
 
676
        # a file differing only in last newline.
 
677
        vf.add_lines('c', [], ['a'])
 
678
        self.assertEqual(
 
679
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('a'))
 
680
        self.assertEqual(
 
681
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('b'))
 
682
        self.assertEqual(
 
683
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', vf.get_sha1('c'))
399
684
        
400
685
 
401
686
class TestWeave(TestCaseWithTransport, VersionedFileTestMixIn):
437
722
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
438
723
        return w
439
724
 
440
 
    def reopen_file(self, name='foo'):
441
 
        return WeaveFile(name, get_transport(self.get_url('.')))
 
725
    def reopen_file(self, name='foo', create=False):
 
726
        return WeaveFile(name, get_transport(self.get_url('.')), create=create)
442
727
 
443
728
    def test_no_implicit_create(self):
444
729
        self.assertRaises(errors.NoSuchFile,
465
750
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
466
751
        return knit
467
752
 
468
 
    def reopen_file(self, name='foo'):
469
 
        return KnitVersionedFile(name, get_transport(self.get_url('.')), delta=True)
 
753
    def reopen_file(self, name='foo', create=False):
 
754
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
755
            delta=True,
 
756
            create=create)
470
757
 
471
758
    def test_detection(self):
472
759
        print "TODO for merging: create a corrupted knit."
543
830
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
544
831
        # now we should get the default InterVersionedFile object again.
545
832
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
833
 
 
834
 
 
835
class TestReadonlyHttpMixin(object):
 
836
 
 
837
    def test_readonly_http_works(self):
 
838
        # we should be able to read from http with a versioned file.
 
839
        vf = self.get_file()
 
840
        # try an empty file access
 
841
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
842
        self.assertEqual([], readonly_vf.versions())
 
843
        # now with feeling.
 
844
        vf.add_lines('1', [], ['a\n'])
 
845
        vf.add_lines('2', ['1'], ['b\n', 'a\n'])
 
846
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
847
        self.assertEqual(['1', '2'], vf.versions())
 
848
        for version in readonly_vf.versions():
 
849
            readonly_vf.get_lines(version)
 
850
 
 
851
 
 
852
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
853
 
 
854
    def get_file(self):
 
855
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
 
856
 
 
857
    def get_factory(self):
 
858
        return WeaveFile
 
859
 
 
860
 
 
861
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
862
 
 
863
    def get_file(self):
 
864
        return KnitVersionedFile('foo', get_transport(self.get_url('.')),
 
865
                                 delta=True, create=True)
 
866
 
 
867
    def get_factory(self):
 
868
        return KnitVersionedFile
 
869
 
 
870
 
 
871
class MergeCasesMixin(object):
 
872
 
 
873
    def doMerge(self, base, a, b, mp):
 
874
        from cStringIO import StringIO
 
875
        from textwrap import dedent
 
876
 
 
877
        def addcrlf(x):
 
878
            return x + '\n'
 
879
        
 
880
        w = self.get_file()
 
881
        w.add_lines('text0', [], map(addcrlf, base))
 
882
        w.add_lines('text1', ['text0'], map(addcrlf, a))
 
883
        w.add_lines('text2', ['text0'], map(addcrlf, b))
 
884
 
 
885
        self.log_contents(w)
 
886
 
 
887
        self.log('merge plan:')
 
888
        p = list(w.plan_merge('text1', 'text2'))
 
889
        for state, line in p:
 
890
            if line:
 
891
                self.log('%12s | %s' % (state, line[:-1]))
 
892
 
 
893
        self.log('merge:')
 
894
        mt = StringIO()
 
895
        mt.writelines(w.weave_merge(p))
 
896
        mt.seek(0)
 
897
        self.log(mt.getvalue())
 
898
 
 
899
        mp = map(addcrlf, mp)
 
900
        self.assertEqual(mt.readlines(), mp)
 
901
        
 
902
        
 
903
    def testOneInsert(self):
 
904
        self.doMerge([],
 
905
                     ['aa'],
 
906
                     [],
 
907
                     ['aa'])
 
908
 
 
909
    def testSeparateInserts(self):
 
910
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
911
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
912
                     ['aaa', 'bbb', 'yyy', 'ccc'],
 
913
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
914
 
 
915
    def testSameInsert(self):
 
916
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
917
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
918
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
 
919
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
920
    overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
 
921
    def testOverlappedInsert(self):
 
922
        self.doMerge(['aaa', 'bbb'],
 
923
                     ['aaa', 'xxx', 'yyy', 'bbb'],
 
924
                     ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
 
925
 
 
926
        # really it ought to reduce this to 
 
927
        # ['aaa', 'xxx', 'yyy', 'bbb']
 
928
 
 
929
 
 
930
    def testClashReplace(self):
 
931
        self.doMerge(['aaa'],
 
932
                     ['xxx'],
 
933
                     ['yyy', 'zzz'],
 
934
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
 
935
                      '>>>>>>> '])
 
936
 
 
937
    def testNonClashInsert1(self):
 
938
        self.doMerge(['aaa'],
 
939
                     ['xxx', 'aaa'],
 
940
                     ['yyy', 'zzz'],
 
941
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
 
942
                      '>>>>>>> '])
 
943
 
 
944
    def testNonClashInsert2(self):
 
945
        self.doMerge(['aaa'],
 
946
                     ['aaa'],
 
947
                     ['yyy', 'zzz'],
 
948
                     ['yyy', 'zzz'])
 
949
 
 
950
 
 
951
    def testDeleteAndModify(self):
 
952
        """Clashing delete and modification.
 
953
 
 
954
        If one side modifies a region and the other deletes it then
 
955
        there should be a conflict with one side blank.
 
956
        """
 
957
 
 
958
        #######################################
 
959
        # skippd, not working yet
 
960
        return
 
961
        
 
962
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
963
                     ['aaa', 'ddd', 'ccc'],
 
964
                     ['aaa', 'ccc'],
 
965
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
 
966
 
 
967
    def _test_merge_from_strings(self, base, a, b, expected):
 
968
        w = self.get_file()
 
969
        w.add_lines('text0', [], base.splitlines(True))
 
970
        w.add_lines('text1', ['text0'], a.splitlines(True))
 
971
        w.add_lines('text2', ['text0'], b.splitlines(True))
 
972
        self.log('merge plan:')
 
973
        p = list(w.plan_merge('text1', 'text2'))
 
974
        for state, line in p:
 
975
            if line:
 
976
                self.log('%12s | %s' % (state, line[:-1]))
 
977
        self.log('merge result:')
 
978
        result_text = ''.join(w.weave_merge(p))
 
979
        self.log(result_text)
 
980
        self.assertEqualDiff(result_text, expected)
 
981
 
 
982
    def test_weave_merge_conflicts(self):
 
983
        # does weave merge properly handle plans that end with unchanged?
 
984
        result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
 
985
        self.assertEqual(result, 'hello\n')
 
986
 
 
987
    def test_deletion_extended(self):
 
988
        """One side deletes, the other deletes more.
 
989
        """
 
990
        base = """\
 
991
            line 1
 
992
            line 2
 
993
            line 3
 
994
            """
 
995
        a = """\
 
996
            line 1
 
997
            line 2
 
998
            """
 
999
        b = """\
 
1000
            line 1
 
1001
            """
 
1002
        result = """\
 
1003
            line 1
 
1004
            """
 
1005
        self._test_merge_from_strings(base, a, b, result)
 
1006
 
 
1007
    def test_deletion_overlap(self):
 
1008
        """Delete overlapping regions with no other conflict.
 
1009
 
 
1010
        Arguably it'd be better to treat these as agreement, rather than 
 
1011
        conflict, but for now conflict is safer.
 
1012
        """
 
1013
        base = """\
 
1014
            start context
 
1015
            int a() {}
 
1016
            int b() {}
 
1017
            int c() {}
 
1018
            end context
 
1019
            """
 
1020
        a = """\
 
1021
            start context
 
1022
            int a() {}
 
1023
            end context
 
1024
            """
 
1025
        b = """\
 
1026
            start context
 
1027
            int c() {}
 
1028
            end context
 
1029
            """
 
1030
        result = """\
 
1031
            start context
 
1032
<<<<<<< 
 
1033
            int a() {}
 
1034
=======
 
1035
            int c() {}
 
1036
>>>>>>> 
 
1037
            end context
 
1038
            """
 
1039
        self._test_merge_from_strings(base, a, b, result)
 
1040
 
 
1041
    def test_agreement_deletion(self):
 
1042
        """Agree to delete some lines, without conflicts."""
 
1043
        base = """\
 
1044
            start context
 
1045
            base line 1
 
1046
            base line 2
 
1047
            end context
 
1048
            """
 
1049
        a = """\
 
1050
            start context
 
1051
            base line 1
 
1052
            end context
 
1053
            """
 
1054
        b = """\
 
1055
            start context
 
1056
            base line 1
 
1057
            end context
 
1058
            """
 
1059
        result = """\
 
1060
            start context
 
1061
            base line 1
 
1062
            end context
 
1063
            """
 
1064
        self._test_merge_from_strings(base, a, b, result)
 
1065
 
 
1066
    def test_sync_on_deletion(self):
 
1067
        """Specific case of merge where we can synchronize incorrectly.
 
1068
        
 
1069
        A previous version of the weave merge concluded that the two versions
 
1070
        agreed on deleting line 2, and this could be a synchronization point.
 
1071
        Line 1 was then considered in isolation, and thought to be deleted on 
 
1072
        both sides.
 
1073
 
 
1074
        It's better to consider the whole thing as a disagreement region.
 
1075
        """
 
1076
        base = """\
 
1077
            start context
 
1078
            base line 1
 
1079
            base line 2
 
1080
            end context
 
1081
            """
 
1082
        a = """\
 
1083
            start context
 
1084
            base line 1
 
1085
            a's replacement line 2
 
1086
            end context
 
1087
            """
 
1088
        b = """\
 
1089
            start context
 
1090
            b replaces
 
1091
            both lines
 
1092
            end context
 
1093
            """
 
1094
        result = """\
 
1095
            start context
 
1096
<<<<<<< 
 
1097
            base line 1
 
1098
            a's replacement line 2
 
1099
=======
 
1100
            b replaces
 
1101
            both lines
 
1102
>>>>>>> 
 
1103
            end context
 
1104
            """
 
1105
        self._test_merge_from_strings(base, a, b, result)
 
1106
 
 
1107
 
 
1108
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
 
1109
 
 
1110
    def get_file(self, name='foo'):
 
1111
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
1112
                                 delta=True, create=True)
 
1113
 
 
1114
    def log_contents(self, w):
 
1115
        pass
 
1116
 
 
1117
 
 
1118
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
 
1119
 
 
1120
    def get_file(self, name='foo'):
 
1121
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
1122
 
 
1123
    def log_contents(self, w):
 
1124
        self.log('weave is:')
 
1125
        tmpf = StringIO()
 
1126
        write_weave(w, tmpf)
 
1127
        self.log(tmpf.getvalue())
 
1128
 
 
1129
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
 
1130
                                'xxx', '>>>>>>> ', 'bbb']