/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

  • Committer: Robert Collins
  • Date: 2007-04-19 05:21:37 UTC
  • mto: (2425.1.2 integration)
  • mto: This revision was merged to the branch mainline in revision 2427.
  • Revision ID: robertc@robertcollins.net-20070419052137-vsncwlmi8epl5eel
Update existing builtin commands help text to use _see_also. (Robert Collins)

Show diffs side-by-side

added added

removed removed

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