1
1
#! /usr/bin/python2.4
3
# Copyright (C) 2005 by Canonical Ltd
3
# Copyright (C) 2005 Canonical Ltd
5
5
# This program is free software; you can redistribute it and/or modify
6
6
# it under the terms of the GNU General Public License as published by
7
7
# the Free Software Foundation; either version 2 of the License, or
8
8
# (at your option) any later version.
10
10
# This program is distributed in the hope that it will be useful,
11
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
# GNU General Public License for more details.
15
15
# You should have received a copy of the GNU General Public License
16
16
# along with this program; if not, write to the Free Software
17
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
20
# TODO: tests regarding version names
21
# TODO: rbc 20050108 test that join does not leave an inconsistent weave
24
24
"""test suite for weave algorithm"""
26
26
from pprint import pformat
28
from bzrlib.weave import Weave, WeaveFormatError, WeaveError
31
from bzrlib.osutils import sha_string
32
from bzrlib.tests import TestCase, TestCaseInTempDir
33
from bzrlib.weave import Weave, WeaveFormatError, WeaveError, reweave
29
34
from bzrlib.weavefile import write_weave, read_weave
30
from bzrlib.selftest import TestCase
31
from bzrlib.osutils import sha_string
34
37
# texts for use in testing
73
80
class StoreText(TestBase):
74
81
"""Store and retrieve a simple text."""
83
def test_storing_text(self):
77
idx = k.add('text0', [], TEXT_0)
78
self.assertEqual(k.get(idx), TEXT_0)
85
idx = k.add_lines('text0', [], TEXT_0)
86
self.assertEqual(k.get_lines(idx), TEXT_0)
79
87
self.assertEqual(idx, 0)
83
90
class AnnotateOne(TestBase):
86
k.add('text0', [], TEXT_0)
87
self.assertEqual(k.annotate(0),
93
k.add_lines('text0', [], TEXT_0)
94
self.assertEqual(k.annotate('text0'),
95
[('text0', TEXT_0[0])])
91
98
class StoreTwo(TestBase):
95
idx = k.add('text0', [], TEXT_0)
102
idx = k.add_lines('text0', [], TEXT_0)
96
103
self.assertEqual(idx, 0)
98
idx = k.add('text1', [], TEXT_1)
105
idx = k.add_lines('text1', [], TEXT_1)
99
106
self.assertEqual(idx, 1)
101
self.assertEqual(k.get(0), TEXT_0)
102
self.assertEqual(k.get(1), TEXT_1)
106
class AddWithGivenSha(TestBase):
108
"""Add with caller-supplied SHA-1"""
108
self.assertEqual(k.get_lines(0), TEXT_0)
109
self.assertEqual(k.get_lines(1), TEXT_1)
112
class GetSha1(TestBase):
113
def test_get_sha1(self):
112
k.add('text0', [], [t], sha1=sha_string(t))
115
k.add_lines('text0', [], 'text0')
116
self.assertEqual('34dc0e430c642a26c3dd1c2beb7a8b4f4445eb79',
118
self.assertRaises(errors.RevisionNotPresent,
120
self.assertRaises(errors.RevisionNotPresent,
116
124
class InvalidAdd(TestBase):
117
125
"""Try to use invalid version number during add."""
118
126
def runTest(self):
121
self.assertRaises(IndexError,
129
self.assertRaises(errors.RevisionNotPresent,
160
167
def runTest(self):
163
k.add('text0', [], ['line 1'])
164
k.add('text1', [0], ['line 1', 'line 2'])
166
self.assertEqual(k.annotate(0),
169
self.assertEqual(k.get(1),
170
k.add_lines('text0', [], ['line 1'])
171
k.add_lines('text1', ['text0'], ['line 1', 'line 2'])
173
self.assertEqual(k.annotate('text0'),
174
[('text0', 'line 1')])
176
self.assertEqual(k.get_lines(1),
173
self.assertEqual(k.annotate(1),
177
k.add('text2', [0], ['line 1', 'diverged line'])
179
self.assertEqual(k.annotate(2),
181
(2, 'diverged line')])
180
self.assertEqual(k.annotate('text1'),
181
[('text0', 'line 1'),
182
('text1', 'line 2')])
184
k.add_lines('text2', ['text0'], ['line 1', 'diverged line'])
186
self.assertEqual(k.annotate('text2'),
187
[('text0', 'line 1'),
188
('text2', 'diverged line')])
183
190
text3 = ['line 1', 'middle line', 'line 2']
188
195
# self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3))))
190
197
self.log("k._weave=" + pformat(k._weave))
192
self.assertEqual(k.annotate(3),
199
self.assertEqual(k.annotate('text3'),
200
[('text0', 'line 1'),
201
('text3', 'middle line'),
202
('text1', 'line 2')])
197
204
# now multiple insertions at different places
206
['text0', 'text1', 'text3'],
200
207
['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc'])
202
self.assertEqual(k.annotate(4),
209
self.assertEqual(k.annotate('text4'),
210
[('text0', 'line 1'),
212
('text3', 'middle line'),
212
218
class DeleteLines(TestBase):
561
578
['header', '', 'line from 1', 'fixup line', 'line from 2'],
564
k.add('text0', [], texts[0])
565
k.add('text1', [0], texts[1])
566
k.add('text2', [0], texts[2])
567
k.add('merge', [0, 1, 2], texts[3])
581
k.add_lines('text0', [], texts[0])
582
k.add_lines('text1', ['text0'], texts[1])
583
k.add_lines('text2', ['text0'], texts[2])
584
k.add_lines('merge', ['text0', 'text1', 'text2'], texts[3])
569
586
for i, t in enumerate(texts):
570
self.assertEqual(k.get(i), t)
587
self.assertEqual(k.get_lines(i), t)
572
self.assertEqual(k.annotate(3),
589
self.assertEqual(k.annotate('merge'),
590
[('text0', 'header'),
592
('text1', 'line from 1'),
593
('merge', 'fixup line'),
594
('text2', 'line from 2'),
580
self.assertEqual(list(k.inclusions([3])),
597
self.assertEqual(list(k.get_ancestry(['merge'])),
598
['text0', 'text1', 'text2', 'merge'])
583
600
self.log('k._weave=' + pformat(k._weave))
618
k.add([], ['aaa', 'bbb'])
619
k.add([0], ['111', 'aaa', 'ccc', 'bbb'])
620
k.add([1], ['aaa', 'ccc', 'bbb', '222'])
626
class AutoMerge(TestBase):
630
texts = [['header', 'aaa', 'bbb'],
631
['header', 'aaa', 'line from 1', 'bbb'],
632
['header', 'aaa', 'bbb', 'line from 2', 'more from 2'],
635
k.add('text0', [], texts[0])
636
k.add('text1', [0], texts[1])
637
k.add('text2', [0], texts[2])
639
self.log('k._weave=' + pformat(k._weave))
641
m = list(k.mash_iter([0, 1, 2]))
647
'line from 2', 'more from 2'])
634
k.add_lines([], ['aaa', 'bbb'])
635
k.add_lines([0], ['111', 'aaa', 'ccc', 'bbb'])
636
k.add_lines([1], ['aaa', 'ccc', 'bbb', '222'])
651
639
class Khayyam(TestBase):
652
640
"""Test changes to multi-line texts, and read/write"""
642
def test_multi_line_merge(self):
655
644
"""A Book of Verses underneath the Bough,
656
645
A Jug of Wine, a Loaf of Bread, -- and Thou
685
ver = k.add('text%d' % i,
674
ver = k.add_lines('text%d' % i,
686
675
list(parents), t)
676
parents.add('text%d' % i)
690
679
self.log("k._weave=" + pformat(k._weave))
692
681
for i, t in enumerate(texts):
693
self.assertEqual(k.get(i), t)
682
self.assertEqual(k.get_lines(i), t)
695
684
self.check_read_write(k)
699
class MergeCases(TestBase):
700
def doMerge(self, base, a, b, mp):
701
from cStringIO import StringIO
702
from textwrap import dedent
708
w.add('text0', [], map(addcrlf, base))
709
w.add('text1', [0], map(addcrlf, a))
710
w.add('text2', [0], map(addcrlf, b))
712
self.log('weave is:')
715
self.log(tmpf.getvalue())
717
self.log('merge plan:')
718
p = list(w.plan_merge(1, 2))
719
for state, line in p:
721
self.log('%12s | %s' % (state, line[:-1]))
725
mt.writelines(w.weave_merge(p))
727
self.log(mt.getvalue())
729
mp = map(addcrlf, mp)
730
self.assertEqual(mt.readlines(), mp)
733
def testOneInsert(self):
739
def testSeparateInserts(self):
740
self.doMerge(['aaa', 'bbb', 'ccc'],
741
['aaa', 'xxx', 'bbb', 'ccc'],
742
['aaa', 'bbb', 'yyy', 'ccc'],
743
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
745
def testSameInsert(self):
746
self.doMerge(['aaa', 'bbb', 'ccc'],
747
['aaa', 'xxx', 'bbb', 'ccc'],
748
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
749
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
751
def testOverlappedInsert(self):
752
self.doMerge(['aaa', 'bbb'],
753
['aaa', 'xxx', 'yyy', 'bbb'],
754
['aaa', 'xxx', 'bbb'],
755
['aaa', '<<<<', 'xxx', 'yyy', '====', 'xxx', '>>>>', 'bbb'])
757
# really it ought to reduce this to
758
# ['aaa', 'xxx', 'yyy', 'bbb']
761
def testClashReplace(self):
762
self.doMerge(['aaa'],
765
['<<<<', 'xxx', '====', 'yyy', 'zzz', '>>>>'])
767
def testNonClashInsert(self):
768
self.doMerge(['aaa'],
771
['<<<<', 'xxx', 'aaa', '====', 'yyy', 'zzz', '>>>>'])
773
self.doMerge(['aaa'],
779
def testDeleteAndModify(self):
780
"""Clashing delete and modification.
782
If one side modifies a region and the other deletes it then
783
there should be a conflict with one side blank.
786
#######################################
787
# skippd, not working yet
790
self.doMerge(['aaa', 'bbb', 'ccc'],
791
['aaa', 'ddd', 'ccc'],
793
['<<<<', 'aaa', '====', '>>>>', 'ccc'])
796
687
class JoinWeavesTests(TestBase):
798
689
super(JoinWeavesTests, self).setUp()
799
690
self.weave1 = Weave()
800
691
self.lines1 = ['hello\n']
801
692
self.lines3 = ['hello\n', 'cruel\n', 'world\n']
802
self.weave1.add('v1', [], self.lines1)
803
self.weave1.add('v2', [0], ['hello\n', 'world\n'])
804
self.weave1.add('v3', [1], self.lines3)
693
self.weave1.add_lines('v1', [], self.lines1)
694
self.weave1.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
695
self.weave1.add_lines('v3', ['v2'], self.lines3)
806
697
def test_join_empty(self):
807
698
"""Join two empty weaves."""
841
732
['hello\n', 'pale blue\n', 'world\n'])
843
734
def test_join_parent_disagreement(self):
844
"""Cannot join weaves with different parents for a version."""
735
#join reconciles differening parents into a union.
847
wa.add('v1', [], ['hello\n'])
849
wb.add('v1', ['v0'], ['hello\n'])
850
self.assertRaises(WeaveError,
738
wa.add_lines('v1', [], ['hello\n'])
739
wb.add_lines('v0', [], [])
740
wb.add_lines('v1', ['v0'], ['hello\n'])
742
self.assertEqual(['v0'], wa.get_parents('v1'))
853
744
def test_join_text_disagreement(self):
854
745
"""Cannot join weaves with different texts for a version."""
857
wa.add('v1', [], ['hello\n'])
858
wb.add('v1', [], ['not\n', 'hello\n'])
748
wa.add_lines('v1', [], ['hello\n'])
749
wb.add_lines('v1', [], ['not\n', 'hello\n'])
859
750
self.assertRaises(WeaveError,
865
756
The source weave contains a different version at index 0."""
866
757
wa = self.weave1.copy()
868
wb.add('x1', [], ['line from x1\n'])
869
wb.add('v1', [], ['hello\n'])
870
wb.add('v2', ['v1'], ['hello\n', 'world\n'])
872
eq = self.assertEquals
873
eq(sorted(wa.iter_names()), ['v1', 'v2', 'v3', 'x1',])
874
eq(wa.get_text('x1'), 'line from x1\n')
876
def test_join_with_ghosts(self):
877
"""Join that inserts parents of an existing revision.
879
This can happen when merging from another branch who
880
knows about revisions the destination does not. In
881
this test the second weave knows of an additional parent of
882
v2. Any revisions which are in common still have to have the
884
return ###############################
885
wa = self.weave1.copy()
887
wb.add('x1', [], ['line from x1\n'])
888
wb.add('v1', [], ['hello\n'])
889
wb.add('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
891
eq = self.assertEquals
892
eq(sorted(wa.iter_names()), ['v1', 'v2', 'v3', 'x1',])
893
eq(wa.get_text('x1'), 'line from x1\n')
896
if __name__ == '__main__':
899
sys.exit(unittest.main())
759
wb.add_lines('x1', [], ['line from x1\n'])
760
wb.add_lines('v1', [], ['hello\n'])
761
wb.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
763
eq = self.assertEquals
764
eq(sorted(wa.versions()), ['v1', 'v2', 'v3', 'x1',])
765
eq(wa.get_text('x1'), 'line from x1\n')
767
def test_written_detection(self):
768
# Test detection of weave file corruption.
770
# Make sure that we can detect if a weave file has
771
# been corrupted. This doesn't test all forms of corruption,
772
# but it at least helps verify the data you get, is what you want.
773
from cStringIO import StringIO
776
w.add_lines('v1', [], ['hello\n'])
777
w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
782
# Because we are corrupting, we need to make sure we have the exact text
783
self.assertEquals('# bzr weave file v5\n'
784
'i\n1 f572d396fae9206628714fb2ce00f72e94f2258f\nn v1\n\n'
785
'i 0\n1 90f265c6e75f1c8f9ab76dcf85528352c5f215ef\nn v2\n\n'
786
'w\n{ 0\n. hello\n}\n{ 1\n. there\n}\nW\n',
789
# Change a single letter
790
tmpf = StringIO('# bzr weave file v5\n'
791
'i\n1 f572d396fae9206628714fb2ce00f72e94f2258f\nn v1\n\n'
792
'i 0\n1 90f265c6e75f1c8f9ab76dcf85528352c5f215ef\nn v2\n\n'
793
'w\n{ 0\n. hello\n}\n{ 1\n. There\n}\nW\n')
797
self.assertEqual('hello\n', w.get_text('v1'))
798
self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
799
self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
800
self.assertRaises(errors.WeaveInvalidChecksum, w.check)
802
# Change the sha checksum
803
tmpf = StringIO('# bzr weave file v5\n'
804
'i\n1 f572d396fae9206628714fb2ce00f72e94f2258f\nn v1\n\n'
805
'i 0\n1 f0f265c6e75f1c8f9ab76dcf85528352c5f215ef\nn v2\n\n'
806
'w\n{ 0\n. hello\n}\n{ 1\n. there\n}\nW\n')
810
self.assertEqual('hello\n', w.get_text('v1'))
811
self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
812
self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
813
self.assertRaises(errors.WeaveInvalidChecksum, w.check)
816
class InstrumentedWeave(Weave):
817
"""Keep track of how many times functions are called."""
819
def __init__(self, weave_name=None):
820
self._extract_count = 0
821
Weave.__init__(self, weave_name=weave_name)
823
def _extract(self, versions):
824
self._extract_count += 1
825
return Weave._extract(self, versions)
828
class JoinOptimization(TestCase):
829
"""Test that Weave.join() doesn't extract all texts, only what must be done."""
832
w1 = InstrumentedWeave()
833
w2 = InstrumentedWeave()
836
txt1 = ['a\n', 'b\n']
837
txt2 = ['a\n', 'c\n']
838
txt3 = ['a\n', 'b\n', 'c\n']
840
w1.add_lines('txt0', [], txt0) # extract 1a
841
w2.add_lines('txt0', [], txt0) # extract 1b
842
w1.add_lines('txt1', ['txt0'], txt1)# extract 2a
843
w2.add_lines('txt2', ['txt0'], txt2)# extract 2b
844
w1.join(w2) # extract 3a to add txt2
845
w2.join(w1) # extract 3b to add txt1
847
w1.add_lines('txt3', ['txt1', 'txt2'], txt3) # extract 4a
848
w2.add_lines('txt3', ['txt2', 'txt1'], txt3) # extract 4b
849
# These secretly have inverted parents
851
# This should not have to do any extractions
852
w1.join(w2) # NO extract, texts already present with same parents
853
w2.join(w1) # NO extract, texts already present with same parents
855
self.assertEqual(4, w1._extract_count)
856
self.assertEqual(4, w2._extract_count)
858
def test_double_parent(self):
859
# It should not be considered illegal to add
860
# a revision with the same parent twice
861
w1 = InstrumentedWeave()
862
w2 = InstrumentedWeave()
865
txt1 = ['a\n', 'b\n']
866
txt2 = ['a\n', 'c\n']
867
txt3 = ['a\n', 'b\n', 'c\n']
869
w1.add_lines('txt0', [], txt0)
870
w2.add_lines('txt0', [], txt0)
871
w1.add_lines('txt1', ['txt0'], txt1)
872
w2.add_lines('txt1', ['txt0', 'txt0'], txt1)
873
# Same text, effectively the same, because the
874
# parent is only repeated
875
w1.join(w2) # extract 3a to add txt2
876
w2.join(w1) # extract 3b to add txt1
879
class TestNeedsReweave(TestCase):
880
"""Internal corner cases for when reweave is needed."""
882
def test_compatible_parents(self):
884
my_parents = set([1, 2, 3])
886
self.assertTrue(w1._compatible_parents(my_parents, set([3])))
888
self.assertTrue(w1._compatible_parents(my_parents, set(my_parents)))
889
# same empty corner case
890
self.assertTrue(w1._compatible_parents(set(), set()))
891
# other cannot contain stuff my_parents does not
892
self.assertFalse(w1._compatible_parents(set(), set([1])))
893
self.assertFalse(w1._compatible_parents(my_parents, set([1, 2, 3, 4])))
894
self.assertFalse(w1._compatible_parents(my_parents, set([4])))
897
class TestWeaveFile(TestCaseInTempDir):
899
def test_empty_file(self):
900
f = open('empty.weave', 'wb+')
902
self.assertRaises(errors.WeaveFormatError,