~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: Martin Pool
  • Date: 2006-03-21 12:26:54 UTC
  • mto: This revision was merged to the branch mainline in revision 1621.
  • Revision ID: mbp@sourcefrog.net-20060321122654-514047ed65795a17
New developer commands 'weave-list' and 'weave-join'.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
 
1
# Copyright (C) 2005 by Canonical Ltd
2
2
#
3
3
# Authors:
4
4
#   Johan Rydberg <jrydberg@gnu.org>
7
7
# it under the terms of the GNU General Public License as published by
8
8
# the Free Software Foundation; either version 2 of the License, or
9
9
# (at your option) any later version.
10
 
#
 
10
 
11
11
# This program is distributed in the hope that it will be useful,
12
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
14
# GNU General Public License for more details.
15
 
#
 
15
 
16
16
# You should have received a copy of the GNU General Public License
17
17
# along with this program; if not, write to the Free Software
18
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
 
20
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
21
import bzrlib
27
 
from bzrlib import (
28
 
    errors,
29
 
    osutils,
30
 
    progress,
31
 
    )
 
22
import bzrlib.errors as errors
32
23
from bzrlib.errors import (
33
24
                           RevisionNotPresent, 
34
25
                           RevisionAlreadyPresent,
37
28
from bzrlib.knit import KnitVersionedFile, \
38
29
     KnitAnnotateFactory
39
30
from bzrlib.tests import TestCaseWithTransport
40
 
from bzrlib.tests.HTTPTestUtil import TestCaseWithWebserver
41
31
from bzrlib.trace import mutter
42
32
from bzrlib.transport import get_transport
43
33
from bzrlib.transport.memory import MemoryTransport
44
 
from bzrlib.tsort import topo_sort
45
34
import bzrlib.versionedfile as versionedfile
46
35
from bzrlib.weave import WeaveFile
47
 
from bzrlib.weavefile import read_weave, write_weave
 
36
from bzrlib.weavefile import read_weave
48
37
 
49
38
 
50
39
class VersionedFileTestMixIn(object):
74
63
            self.assertRaises(RevisionAlreadyPresent,
75
64
                f.add_lines, 'r1', [], [])
76
65
        verify_file(f)
77
 
        # this checks that reopen with create=True does not break anything.
78
 
        f = self.reopen_file(create=True)
 
66
        f = self.reopen_file()
79
67
        verify_file(f)
80
68
 
81
69
    def test_adds_with_parent_texts(self):
116
104
        f = self.reopen_file()
117
105
        verify_file(f)
118
106
 
119
 
    def test_add_unicode_content(self):
120
 
        # unicode content is not permitted in versioned files. 
121
 
        # versioned files version sequences of bytes only.
122
 
        vf = self.get_file()
123
 
        self.assertRaises(errors.BzrBadParameterUnicode,
124
 
            vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
125
 
        self.assertRaises(
126
 
            (errors.BzrBadParameterUnicode, NotImplementedError),
127
 
            vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
128
 
 
129
 
    def test_inline_newline_throws(self):
130
 
        # \r characters are not permitted in lines being added
131
 
        vf = self.get_file()
132
 
        self.assertRaises(errors.BzrBadParameterContainsNewline, 
133
 
            vf.add_lines, 'a', [], ['a\n\n'])
134
 
        self.assertRaises(
135
 
            (errors.BzrBadParameterContainsNewline, NotImplementedError),
136
 
            vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
137
 
        # but inline CR's are allowed
138
 
        vf.add_lines('a', [], ['a\r\n'])
139
 
        try:
140
 
            vf.add_lines_with_ghosts('b', [], ['a\r\n'])
141
 
        except NotImplementedError:
142
 
            pass
143
 
 
144
 
    def test_add_reserved(self):
145
 
        vf = self.get_file()
146
 
        self.assertRaises(errors.ReservedId,
147
 
            vf.add_lines, 'a:', [], ['a\n', 'b\n', 'c\n'])
148
 
 
149
 
        self.assertRaises(errors.ReservedId,
150
 
            vf.add_delta, 'a:', [], None, 'sha1', False, ((0, 0, 0, []),))
151
 
 
152
 
    def test_get_reserved(self):
153
 
        vf = self.get_file()
154
 
        self.assertRaises(errors.ReservedId, vf.get_delta, 'b:')
155
 
        self.assertRaises(errors.ReservedId, vf.get_texts, ['b:'])
156
 
        self.assertRaises(errors.ReservedId, vf.get_lines, 'b:')
157
 
        self.assertRaises(errors.ReservedId, vf.get_text, 'b:')
158
 
 
159
107
    def test_get_delta(self):
160
108
        f = self.get_file()
161
109
        sha1s = self._setup_for_deltas(f)
214
162
        self.assertEqual(expected_delta, deltas['noeol'])
215
163
        # smoke tests for eol support - two noeol in a row same content
216
164
        expected_deltas = (('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
217
 
                          [(0, 1, 2, [('noeolsecond', 'line\n'), ('noeolsecond', 'line\n')])]),
 
165
                          [(0, 1, 2, [(u'noeolsecond', 'line\n'), (u'noeolsecond', 'line\n')])]),
218
166
                          ('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
219
167
                           [(0, 0, 1, [('noeolsecond', 'line\n')]), (1, 1, 0, [])]))
220
168
        self.assertEqual(['line\n', 'line'], f.get_lines('noeolsecond'))
221
169
        self.assertTrue(deltas['noeolsecond'] in expected_deltas)
222
170
        # two no-eol in a row, different content
223
171
        expected_delta = ('noeolsecond', '8bb553a84e019ef1149db082d65f3133b195223b', True, 
224
 
                          [(1, 2, 1, [('noeolnotshared', 'phone\n')])])
 
172
                          [(1, 2, 1, [(u'noeolnotshared', 'phone\n')])])
225
173
        self.assertEqual(['line\n', 'phone'], f.get_lines('noeolnotshared'))
226
174
        self.assertEqual(expected_delta, deltas['noeolnotshared'])
227
175
        # eol folling a no-eol with content change
228
176
        expected_delta = ('noeol', 'a61f6fb6cfc4596e8d88c34a308d1e724caf8977', False, 
229
 
                          [(0, 1, 1, [('eol', 'phone\n')])])
 
177
                          [(0, 1, 1, [(u'eol', 'phone\n')])])
230
178
        self.assertEqual(['phone\n'], f.get_lines('eol'))
231
179
        self.assertEqual(expected_delta, deltas['eol'])
232
180
        # eol folling a no-eol with content change
233
181
        expected_delta = ('noeol', '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
234
 
                          [(0, 1, 1, [('eolline', 'line\n')])])
 
182
                          [(0, 1, 1, [(u'eolline', 'line\n')])])
235
183
        self.assertEqual(['line\n'], f.get_lines('eolline'))
236
184
        self.assertEqual(expected_delta, deltas['eolline'])
237
185
        # eol with no parents
238
186
        expected_delta = (None, '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, 
239
 
                          [(0, 0, 1, [('noeolbase', 'line\n')])])
 
187
                          [(0, 0, 1, [(u'noeolbase', 'line\n')])])
240
188
        self.assertEqual(['line'], f.get_lines('noeolbase'))
241
189
        self.assertEqual(expected_delta, deltas['noeolbase'])
242
190
        # eol with two parents, in inverse insertion order
243
191
        expected_deltas = (('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
244
 
                            [(0, 1, 1, [('eolbeforefirstparent', 'line\n')])]),
 
192
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]),
245
193
                           ('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
246
 
                            [(0, 1, 1, [('eolbeforefirstparent', 'line\n')])]))
 
194
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]))
247
195
        self.assertEqual(['line'], f.get_lines('eolbeforefirstparent'))
248
196
        #self.assertTrue(deltas['eolbeforefirstparent'] in expected_deltas)
249
197
 
384
332
        self.assertRaises(RevisionNotPresent,
385
333
            f.get_ancestry, ['rM', 'rX'])
386
334
 
387
 
        self.assertEqual(set(f.get_ancestry('rM')),
388
 
            set(f.get_ancestry('rM', topo_sorted=False)))
389
 
 
390
335
    def test_mutate_after_finish(self):
391
336
        f = self.get_file()
392
337
        f.transaction_finished()
446
391
        # and should be a list
447
392
        self.assertTrue(isinstance(f.__class__.get_suffixes(), list))
448
393
 
449
 
    def build_graph(self, file, graph):
450
 
        for node in topo_sort(graph.items()):
451
 
            file.add_lines(node, graph[node], [])
452
 
 
453
394
    def test_get_graph(self):
454
395
        f = self.get_file()
455
 
        graph = {
456
 
            'v1': [],
457
 
            'v2': ['v1'],
458
 
            'v3': ['v2']}
459
 
        self.build_graph(f, graph)
460
 
        self.assertEqual(graph, f.get_graph())
461
 
    
462
 
    def test_get_graph_partial(self):
463
 
        f = self.get_file()
464
 
        complex_graph = {}
465
 
        simple_a = {
466
 
            'c': [],
467
 
            'b': ['c'],
468
 
            'a': ['b'],
469
 
            }
470
 
        complex_graph.update(simple_a)
471
 
        simple_b = {
472
 
            'c': [],
473
 
            'b': ['c'],
474
 
            }
475
 
        complex_graph.update(simple_b)
476
 
        simple_gam = {
477
 
            'c': [],
478
 
            'oo': [],
479
 
            'bar': ['oo', 'c'],
480
 
            'gam': ['bar'],
481
 
            }
482
 
        complex_graph.update(simple_gam)
483
 
        simple_b_gam = {}
484
 
        simple_b_gam.update(simple_gam)
485
 
        simple_b_gam.update(simple_b)
486
 
        self.build_graph(f, complex_graph)
487
 
        self.assertEqual(simple_a, f.get_graph(['a']))
488
 
        self.assertEqual(simple_b, f.get_graph(['b']))
489
 
        self.assertEqual(simple_gam, f.get_graph(['gam']))
490
 
        self.assertEqual(simple_b_gam, f.get_graph(['b', 'gam']))
 
396
        f.add_lines('v1', [], ['hello\n'])
 
397
        f.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
 
398
        f.add_lines('v3', ['v2'], ['hello\n', 'cruel\n', 'world\n'])
 
399
        self.assertEqual({'v1': [],
 
400
                          'v2': ['v1'],
 
401
                          'v3': ['v2']},
 
402
                         f.get_graph())
491
403
 
492
404
    def test_get_parents(self):
493
405
        f = self.get_file()
565
477
        # versions in the weave 
566
478
        # the ordering here is to make a tree so that dumb searches have
567
479
        # more changes to muck up.
568
 
 
569
 
        class InstrumentedProgress(progress.DummyProgress):
570
 
 
571
 
            def __init__(self):
572
 
 
573
 
                progress.DummyProgress.__init__(self)
574
 
                self.updates = []
575
 
 
576
 
            def update(self, msg=None, current=None, total=None):
577
 
                self.updates.append((msg, current, total))
578
 
 
579
480
        vf = self.get_file()
580
481
        # add a base to get included
581
482
        vf.add_lines('base', [], ['base\n'])
589
490
        vf.add_lines('otherchild',
590
491
                     ['lancestor', 'base'],
591
492
                     ['base\n', 'lancestor\n', 'otherchild\n'])
592
 
        def iter_with_versions(versions, expected):
 
493
        def iter_with_versions(versions):
593
494
            # now we need to see what lines are returned, and how often.
594
495
            lines = {'base\n':0,
595
496
                     'lancestor\n':0,
597
498
                     'child\n':0,
598
499
                     'otherchild\n':0,
599
500
                     }
600
 
            progress = InstrumentedProgress()
601
501
            # iterate over the lines
602
 
            for line in vf.iter_lines_added_or_present_in_versions(versions, 
603
 
                pb=progress):
 
502
            for line in vf.iter_lines_added_or_present_in_versions(versions):
604
503
                lines[line] += 1
605
 
            if []!= progress.updates: 
606
 
                self.assertEqual(expected, progress.updates)
607
504
            return lines
608
 
        lines = iter_with_versions(['child', 'otherchild'],
609
 
                                   [('Walking content.', 0, 2),
610
 
                                    ('Walking content.', 1, 2),
611
 
                                    ('Walking content.', 2, 2)])
 
505
        lines = iter_with_versions(['child', 'otherchild'])
612
506
        # we must see child and otherchild
613
507
        self.assertTrue(lines['child\n'] > 0)
614
508
        self.assertTrue(lines['otherchild\n'] > 0)
615
509
        # we dont care if we got more than that.
616
510
        
617
511
        # test all lines
618
 
        lines = iter_with_versions(None, [('Walking content.', 0, 5),
619
 
                                          ('Walking content.', 1, 5),
620
 
                                          ('Walking content.', 2, 5),
621
 
                                          ('Walking content.', 3, 5),
622
 
                                          ('Walking content.', 4, 5),
623
 
                                          ('Walking content.', 5, 5)])
 
512
        lines = iter_with_versions(None)
624
513
        # all lines must be seen at least once
625
514
        self.assertTrue(lines['base\n'] > 0)
626
515
        self.assertTrue(lines['lancestor\n'] > 0)
672
561
        # add_lines_with_ghosts api.
673
562
        vf = self.get_file()
674
563
        # add a revision with ghost parents
675
 
        # The preferred form is utf8, but we should translate when needed
676
 
        parent_id_unicode = u'b\xbfse'
677
 
        parent_id_utf8 = parent_id_unicode.encode('utf8')
678
564
        try:
679
 
            vf.add_lines_with_ghosts('notbxbfse', [parent_id_utf8], [])
 
565
            vf.add_lines_with_ghosts(u'notbxbfse', [u'b\xbfse'], [])
680
566
        except NotImplementedError:
681
567
            # check the other ghost apis are also not implemented
682
568
            self.assertRaises(NotImplementedError, vf.has_ghost, 'foo')
684
570
            self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
685
571
            self.assertRaises(NotImplementedError, vf.get_graph_with_ghosts)
686
572
            return
687
 
        vf = self.reopen_file()
688
573
        # test key graph related apis: getncestry, _graph, get_parents
689
574
        # has_version
690
575
        # - these are ghost unaware and must not be reflect ghosts
691
 
        self.assertEqual(['notbxbfse'], vf.get_ancestry('notbxbfse'))
692
 
        self.assertEqual([], vf.get_parents('notbxbfse'))
693
 
        self.assertEqual({'notbxbfse':[]}, vf.get_graph())
694
 
        self.assertFalse(self.callDeprecated([osutils._revision_id_warning],
695
 
                         vf.has_version, parent_id_unicode))
696
 
        self.assertFalse(vf.has_version(parent_id_utf8))
 
576
        self.assertEqual([u'notbxbfse'], vf.get_ancestry(u'notbxbfse'))
 
577
        self.assertEqual([], vf.get_parents(u'notbxbfse'))
 
578
        self.assertEqual({u'notbxbfse':[]}, vf.get_graph())
 
579
        self.assertFalse(vf.has_version(u'b\xbfse'))
697
580
        # we have _with_ghost apis to give us ghost information.
698
 
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
699
 
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
700
 
        self.assertEqual({'notbxbfse':[parent_id_utf8]}, vf.get_graph_with_ghosts())
701
 
        self.assertTrue(self.callDeprecated([osutils._revision_id_warning],
702
 
                        vf.has_ghost, parent_id_unicode))
703
 
        self.assertTrue(vf.has_ghost(parent_id_utf8))
 
581
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
 
582
        self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
 
583
        self.assertEqual({u'notbxbfse':[u'b\xbfse']}, vf.get_graph_with_ghosts())
 
584
        self.assertTrue(vf.has_ghost(u'b\xbfse'))
704
585
        # if we add something that is a ghost of another, it should correct the
705
586
        # results of the prior apis
706
 
        self.callDeprecated([osutils._revision_id_warning],
707
 
                            vf.add_lines, parent_id_unicode, [], [])
708
 
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry(['notbxbfse']))
709
 
        self.assertEqual([parent_id_utf8], vf.get_parents('notbxbfse'))
710
 
        self.assertEqual({parent_id_utf8:[],
711
 
                          'notbxbfse':[parent_id_utf8],
 
587
        vf.add_lines(u'b\xbfse', [], [])
 
588
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry([u'notbxbfse']))
 
589
        self.assertEqual([u'b\xbfse'], vf.get_parents(u'notbxbfse'))
 
590
        self.assertEqual({u'b\xbfse':[],
 
591
                          u'notbxbfse':[u'b\xbfse'],
712
592
                          },
713
593
                         vf.get_graph())
714
 
        self.assertTrue(self.callDeprecated([osutils._revision_id_warning],
715
 
                        vf.has_version, parent_id_unicode))
716
 
        self.assertTrue(vf.has_version(parent_id_utf8))
 
594
        self.assertTrue(vf.has_version(u'b\xbfse'))
717
595
        # we have _with_ghost apis to give us ghost information.
718
 
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
719
 
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
720
 
        self.assertEqual({parent_id_utf8:[],
721
 
                          'notbxbfse':[parent_id_utf8],
 
596
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
 
597
        self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
 
598
        self.assertEqual({u'b\xbfse':[],
 
599
                          u'notbxbfse':[u'b\xbfse'],
722
600
                          },
723
601
                         vf.get_graph_with_ghosts())
724
 
        self.assertFalse(self.callDeprecated([osutils._revision_id_warning],
725
 
                         vf.has_ghost, parent_id_unicode))
726
 
        self.assertFalse(vf.has_ghost(parent_id_utf8))
 
602
        self.assertFalse(vf.has_ghost(u'b\xbfse'))
727
603
 
728
604
    def test_add_lines_with_ghosts_after_normal_revs(self):
729
605
        # some versioned file formats allow lines to be added with parent
760
636
        self.assertRaises(errors.ReadOnlyError, vf.fix_parents, 'base', [])
761
637
        self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
762
638
        self.assertRaises(errors.ReadOnlyError, vf.clone_text, 'base', 'bar', ['foo'])
763
 
    
764
 
    def test_get_sha1(self):
765
 
        # check the sha1 data is available
766
 
        vf = self.get_file()
767
 
        # a simple file
768
 
        vf.add_lines('a', [], ['a\n'])
769
 
        # the same file, different metadata
770
 
        vf.add_lines('b', ['a'], ['a\n'])
771
 
        # a file differing only in last newline.
772
 
        vf.add_lines('c', [], ['a'])
773
 
        self.assertEqual(
774
 
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('a'))
775
 
        self.assertEqual(
776
 
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('b'))
777
 
        self.assertEqual(
778
 
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', vf.get_sha1('c'))
779
639
        
780
640
 
781
641
class TestWeave(TestCaseWithTransport, VersionedFileTestMixIn):
817
677
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
818
678
        return w
819
679
 
820
 
    def reopen_file(self, name='foo', create=False):
821
 
        return WeaveFile(name, get_transport(self.get_url('.')), create=create)
 
680
    def reopen_file(self, name='foo'):
 
681
        return WeaveFile(name, get_transport(self.get_url('.')))
822
682
 
823
683
    def test_no_implicit_create(self):
824
684
        self.assertRaises(errors.NoSuchFile,
845
705
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
846
706
        return knit
847
707
 
848
 
    def reopen_file(self, name='foo', create=False):
849
 
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
850
 
            delta=True,
851
 
            create=create)
 
708
    def reopen_file(self, name='foo'):
 
709
        return KnitVersionedFile(name, get_transport(self.get_url('.')), delta=True)
852
710
 
853
711
    def test_detection(self):
 
712
        print "TODO for merging: create a corrupted knit."
854
713
        knit = self.get_file()
855
714
        knit.check()
856
715
 
924
783
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
925
784
        # now we should get the default InterVersionedFile object again.
926
785
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
927
 
 
928
 
 
929
 
class TestReadonlyHttpMixin(object):
930
 
 
931
 
    def test_readonly_http_works(self):
932
 
        # we should be able to read from http with a versioned file.
933
 
        vf = self.get_file()
934
 
        # try an empty file access
935
 
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
936
 
        self.assertEqual([], readonly_vf.versions())
937
 
        # now with feeling.
938
 
        vf.add_lines('1', [], ['a\n'])
939
 
        vf.add_lines('2', ['1'], ['b\n', 'a\n'])
940
 
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
941
 
        self.assertEqual(['1', '2'], vf.versions())
942
 
        for version in readonly_vf.versions():
943
 
            readonly_vf.get_lines(version)
944
 
 
945
 
 
946
 
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
947
 
 
948
 
    def get_file(self):
949
 
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
950
 
 
951
 
    def get_factory(self):
952
 
        return WeaveFile
953
 
 
954
 
 
955
 
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
956
 
 
957
 
    def get_file(self):
958
 
        return KnitVersionedFile('foo', get_transport(self.get_url('.')),
959
 
                                 delta=True, create=True)
960
 
 
961
 
    def get_factory(self):
962
 
        return KnitVersionedFile
963
 
 
964
 
 
965
 
class MergeCasesMixin(object):
966
 
 
967
 
    def doMerge(self, base, a, b, mp):
968
 
        from cStringIO import StringIO
969
 
        from textwrap import dedent
970
 
 
971
 
        def addcrlf(x):
972
 
            return x + '\n'
973
 
        
974
 
        w = self.get_file()
975
 
        w.add_lines('text0', [], map(addcrlf, base))
976
 
        w.add_lines('text1', ['text0'], map(addcrlf, a))
977
 
        w.add_lines('text2', ['text0'], map(addcrlf, b))
978
 
 
979
 
        self.log_contents(w)
980
 
 
981
 
        self.log('merge plan:')
982
 
        p = list(w.plan_merge('text1', 'text2'))
983
 
        for state, line in p:
984
 
            if line:
985
 
                self.log('%12s | %s' % (state, line[:-1]))
986
 
 
987
 
        self.log('merge:')
988
 
        mt = StringIO()
989
 
        mt.writelines(w.weave_merge(p))
990
 
        mt.seek(0)
991
 
        self.log(mt.getvalue())
992
 
 
993
 
        mp = map(addcrlf, mp)
994
 
        self.assertEqual(mt.readlines(), mp)
995
 
        
996
 
        
997
 
    def testOneInsert(self):
998
 
        self.doMerge([],
999
 
                     ['aa'],
1000
 
                     [],
1001
 
                     ['aa'])
1002
 
 
1003
 
    def testSeparateInserts(self):
1004
 
        self.doMerge(['aaa', 'bbb', 'ccc'],
1005
 
                     ['aaa', 'xxx', 'bbb', 'ccc'],
1006
 
                     ['aaa', 'bbb', 'yyy', 'ccc'],
1007
 
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
1008
 
 
1009
 
    def testSameInsert(self):
1010
 
        self.doMerge(['aaa', 'bbb', 'ccc'],
1011
 
                     ['aaa', 'xxx', 'bbb', 'ccc'],
1012
 
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
1013
 
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
1014
 
    overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
1015
 
    def testOverlappedInsert(self):
1016
 
        self.doMerge(['aaa', 'bbb'],
1017
 
                     ['aaa', 'xxx', 'yyy', 'bbb'],
1018
 
                     ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
1019
 
 
1020
 
        # really it ought to reduce this to 
1021
 
        # ['aaa', 'xxx', 'yyy', 'bbb']
1022
 
 
1023
 
 
1024
 
    def testClashReplace(self):
1025
 
        self.doMerge(['aaa'],
1026
 
                     ['xxx'],
1027
 
                     ['yyy', 'zzz'],
1028
 
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
1029
 
                      '>>>>>>> '])
1030
 
 
1031
 
    def testNonClashInsert1(self):
1032
 
        self.doMerge(['aaa'],
1033
 
                     ['xxx', 'aaa'],
1034
 
                     ['yyy', 'zzz'],
1035
 
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
1036
 
                      '>>>>>>> '])
1037
 
 
1038
 
    def testNonClashInsert2(self):
1039
 
        self.doMerge(['aaa'],
1040
 
                     ['aaa'],
1041
 
                     ['yyy', 'zzz'],
1042
 
                     ['yyy', 'zzz'])
1043
 
 
1044
 
 
1045
 
    def testDeleteAndModify(self):
1046
 
        """Clashing delete and modification.
1047
 
 
1048
 
        If one side modifies a region and the other deletes it then
1049
 
        there should be a conflict with one side blank.
1050
 
        """
1051
 
 
1052
 
        #######################################
1053
 
        # skippd, not working yet
1054
 
        return
1055
 
        
1056
 
        self.doMerge(['aaa', 'bbb', 'ccc'],
1057
 
                     ['aaa', 'ddd', 'ccc'],
1058
 
                     ['aaa', 'ccc'],
1059
 
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
1060
 
 
1061
 
    def _test_merge_from_strings(self, base, a, b, expected):
1062
 
        w = self.get_file()
1063
 
        w.add_lines('text0', [], base.splitlines(True))
1064
 
        w.add_lines('text1', ['text0'], a.splitlines(True))
1065
 
        w.add_lines('text2', ['text0'], b.splitlines(True))
1066
 
        self.log('merge plan:')
1067
 
        p = list(w.plan_merge('text1', 'text2'))
1068
 
        for state, line in p:
1069
 
            if line:
1070
 
                self.log('%12s | %s' % (state, line[:-1]))
1071
 
        self.log('merge result:')
1072
 
        result_text = ''.join(w.weave_merge(p))
1073
 
        self.log(result_text)
1074
 
        self.assertEqualDiff(result_text, expected)
1075
 
 
1076
 
    def test_weave_merge_conflicts(self):
1077
 
        # does weave merge properly handle plans that end with unchanged?
1078
 
        result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
1079
 
        self.assertEqual(result, 'hello\n')
1080
 
 
1081
 
    def test_deletion_extended(self):
1082
 
        """One side deletes, the other deletes more.
1083
 
        """
1084
 
        base = """\
1085
 
            line 1
1086
 
            line 2
1087
 
            line 3
1088
 
            """
1089
 
        a = """\
1090
 
            line 1
1091
 
            line 2
1092
 
            """
1093
 
        b = """\
1094
 
            line 1
1095
 
            """
1096
 
        result = """\
1097
 
            line 1
1098
 
            """
1099
 
        self._test_merge_from_strings(base, a, b, result)
1100
 
 
1101
 
    def test_deletion_overlap(self):
1102
 
        """Delete overlapping regions with no other conflict.
1103
 
 
1104
 
        Arguably it'd be better to treat these as agreement, rather than 
1105
 
        conflict, but for now conflict is safer.
1106
 
        """
1107
 
        base = """\
1108
 
            start context
1109
 
            int a() {}
1110
 
            int b() {}
1111
 
            int c() {}
1112
 
            end context
1113
 
            """
1114
 
        a = """\
1115
 
            start context
1116
 
            int a() {}
1117
 
            end context
1118
 
            """
1119
 
        b = """\
1120
 
            start context
1121
 
            int c() {}
1122
 
            end context
1123
 
            """
1124
 
        result = """\
1125
 
            start context
1126
 
<<<<<<< 
1127
 
            int a() {}
1128
 
=======
1129
 
            int c() {}
1130
 
>>>>>>> 
1131
 
            end context
1132
 
            """
1133
 
        self._test_merge_from_strings(base, a, b, result)
1134
 
 
1135
 
    def test_agreement_deletion(self):
1136
 
        """Agree to delete some lines, without conflicts."""
1137
 
        base = """\
1138
 
            start context
1139
 
            base line 1
1140
 
            base line 2
1141
 
            end context
1142
 
            """
1143
 
        a = """\
1144
 
            start context
1145
 
            base line 1
1146
 
            end context
1147
 
            """
1148
 
        b = """\
1149
 
            start context
1150
 
            base line 1
1151
 
            end context
1152
 
            """
1153
 
        result = """\
1154
 
            start context
1155
 
            base line 1
1156
 
            end context
1157
 
            """
1158
 
        self._test_merge_from_strings(base, a, b, result)
1159
 
 
1160
 
    def test_sync_on_deletion(self):
1161
 
        """Specific case of merge where we can synchronize incorrectly.
1162
 
        
1163
 
        A previous version of the weave merge concluded that the two versions
1164
 
        agreed on deleting line 2, and this could be a synchronization point.
1165
 
        Line 1 was then considered in isolation, and thought to be deleted on 
1166
 
        both sides.
1167
 
 
1168
 
        It's better to consider the whole thing as a disagreement region.
1169
 
        """
1170
 
        base = """\
1171
 
            start context
1172
 
            base line 1
1173
 
            base line 2
1174
 
            end context
1175
 
            """
1176
 
        a = """\
1177
 
            start context
1178
 
            base line 1
1179
 
            a's replacement line 2
1180
 
            end context
1181
 
            """
1182
 
        b = """\
1183
 
            start context
1184
 
            b replaces
1185
 
            both lines
1186
 
            end context
1187
 
            """
1188
 
        result = """\
1189
 
            start context
1190
 
<<<<<<< 
1191
 
            base line 1
1192
 
            a's replacement line 2
1193
 
=======
1194
 
            b replaces
1195
 
            both lines
1196
 
>>>>>>> 
1197
 
            end context
1198
 
            """
1199
 
        self._test_merge_from_strings(base, a, b, result)
1200
 
 
1201
 
 
1202
 
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
1203
 
 
1204
 
    def get_file(self, name='foo'):
1205
 
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
1206
 
                                 delta=True, create=True)
1207
 
 
1208
 
    def log_contents(self, w):
1209
 
        pass
1210
 
 
1211
 
 
1212
 
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
1213
 
 
1214
 
    def get_file(self, name='foo'):
1215
 
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
1216
 
 
1217
 
    def log_contents(self, w):
1218
 
        self.log('weave is:')
1219
 
        tmpf = StringIO()
1220
 
        write_weave(w, tmpf)
1221
 
        self.log(tmpf.getvalue())
1222
 
 
1223
 
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
1224
 
                                'xxx', '>>>>>>> ', 'bbb']