1
# Copyright (C) 2005 Canonical Ltd
3
# Copyright (C) 2005 by Canonical Ltd
3
5
# This program is free software; you can redistribute it and/or modify
4
6
# it under the terms of the GNU General Public License as published by
5
7
# the Free Software Foundation; either version 2 of the License, or
6
8
# (at your option) any later version.
8
10
# This program is distributed in the hope that it will be useful,
9
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
13
# GNU General Public License for more details.
13
15
# You should have received a copy of the GNU General Public License
14
16
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
20
# TODO: tests regarding version names
19
# TODO: rbc 20050108 test that join does not leave an inconsistent weave
21
# TODO: rbc 20050108 test that join does not leave an inconsistent weave
22
24
"""test suite for weave algorithm"""
24
26
from pprint import pformat
28
import bzrlib.errors as errors
29
from bzrlib.weave import Weave, WeaveFormatError, WeaveError, reweave
30
from bzrlib.weavefile import write_weave, read_weave
31
from bzrlib.tests import TestCase
29
32
from bzrlib.osutils import sha_string
30
from bzrlib.tests import TestCase, TestCaseInTempDir
31
from bzrlib.weave import Weave, WeaveFormatError, WeaveError
32
from bzrlib.weavefile import write_weave, read_weave
35
35
# texts for use in testing
78
class StoreText(TestBase):
79
"""Store and retrieve a simple text."""
82
idx = k.add('text0', [], TEXT_0)
83
self.assertEqual(k.get(idx), TEXT_0)
84
self.assertEqual(idx, 0)
79
87
class AnnotateOne(TestBase):
82
k.add_lines('text0', [], TEXT_0)
83
self.assertEqual(k.annotate('text0'),
84
[('text0', TEXT_0[0])])
90
k.add('text0', [], TEXT_0)
91
self.assertEqual(k.annotate(0),
95
class StoreTwo(TestBase):
99
idx = k.add('text0', [], TEXT_0)
100
self.assertEqual(idx, 0)
102
idx = k.add('text1', [], TEXT_1)
103
self.assertEqual(idx, 1)
105
self.assertEqual(k.get(0), TEXT_0)
106
self.assertEqual(k.get(1), TEXT_1)
109
class AddWithGivenSha(TestBase):
111
"""Add with caller-supplied SHA-1"""
115
k.add('text0', [], [t], sha1=sha_string(t))
118
class GetSha1(TestBase):
119
def test_get_sha1(self):
121
k.add('text0', [], 'text0')
122
self.assertEqual('34dc0e430c642a26c3dd1c2beb7a8b4f4445eb79',
124
self.assertRaises(errors.WeaveRevisionNotPresent,
126
self.assertRaises(errors.WeaveRevisionNotPresent,
87
130
class InvalidAdd(TestBase):
88
131
"""Try to use invalid version number during add."""
89
132
def runTest(self):
92
self.assertRaises(errors.RevisionNotPresent,
135
self.assertRaises(IndexError,
99
142
class RepeatedAdd(TestBase):
100
143
"""Add the same version twice; harmless."""
102
def test_duplicate_add(self):
104
idx = k.add_lines('text0', [], TEXT_0)
105
idx2 = k.add_lines('text0', [], TEXT_0)
146
idx = k.add('text0', [], TEXT_0)
147
idx2 = k.add('text0', [], TEXT_0)
106
148
self.assertEqual(idx, idx2)
109
151
class InvalidRepeatedAdd(TestBase):
110
152
def runTest(self):
112
k.add_lines('basis', [], TEXT_0)
113
idx = k.add_lines('text0', [], TEXT_0)
114
self.assertRaises(errors.RevisionAlreadyPresent,
154
idx = k.add('text0', [], TEXT_0)
155
self.assertRaises(WeaveError,
118
159
['not the same text'])
119
self.assertRaises(errors.RevisionAlreadyPresent,
160
self.assertRaises(WeaveError,
122
['basis'], # not the right parents
163
[12], # not the right parents
126
167
class InsertLines(TestBase):
127
168
"""Store a revision that adds one line to the original.
131
172
def runTest(self):
134
k.add_lines('text0', [], ['line 1'])
135
k.add_lines('text1', ['text0'], ['line 1', 'line 2'])
137
self.assertEqual(k.annotate('text0'),
138
[('text0', 'line 1')])
140
self.assertEqual(k.get_lines(1),
175
k.add('text0', [], ['line 1'])
176
k.add('text1', [0], ['line 1', 'line 2'])
178
self.assertEqual(k.annotate(0),
181
self.assertEqual(k.get(1),
144
self.assertEqual(k.annotate('text1'),
145
[('text0', 'line 1'),
146
('text1', 'line 2')])
148
k.add_lines('text2', ['text0'], ['line 1', 'diverged line'])
150
self.assertEqual(k.annotate('text2'),
151
[('text0', 'line 1'),
152
('text2', 'diverged line')])
185
self.assertEqual(k.annotate(1),
189
k.add('text2', [0], ['line 1', 'diverged line'])
191
self.assertEqual(k.annotate(2),
193
(2, 'diverged line')])
154
195
text3 = ['line 1', 'middle line', 'line 2']
159
200
# self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3))))
161
202
self.log("k._weave=" + pformat(k._weave))
163
self.assertEqual(k.annotate('text3'),
164
[('text0', 'line 1'),
165
('text3', 'middle line'),
166
('text1', 'line 2')])
204
self.assertEqual(k.annotate(3),
168
209
# now multiple insertions at different places
170
['text0', 'text1', 'text3'],
171
212
['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc'])
173
self.assertEqual(k.annotate('text4'),
174
[('text0', 'line 1'),
176
('text3', 'middle line'),
214
self.assertEqual(k.annotate(4),
182
223
class DeleteLines(TestBase):
542
580
['header', '', 'line from 1', 'fixup line', 'line from 2'],
545
k.add_lines('text0', [], texts[0])
546
k.add_lines('text1', ['text0'], texts[1])
547
k.add_lines('text2', ['text0'], texts[2])
548
k.add_lines('merge', ['text0', 'text1', 'text2'], texts[3])
583
k.add('text0', [], texts[0])
584
k.add('text1', [0], texts[1])
585
k.add('text2', [0], texts[2])
586
k.add('merge', [0, 1, 2], texts[3])
550
588
for i, t in enumerate(texts):
551
self.assertEqual(k.get_lines(i), t)
589
self.assertEqual(k.get(i), t)
553
self.assertEqual(k.annotate('merge'),
554
[('text0', 'header'),
556
('text1', 'line from 1'),
557
('merge', 'fixup line'),
558
('text2', 'line from 2'),
591
self.assertEqual(k.annotate(3),
561
self.assertEqual(list(k.get_ancestry(['merge'])),
562
['text0', 'text1', 'text2', 'merge'])
599
self.assertEqual(list(k.inclusions([3])),
564
602
self.log('k._weave=' + pformat(k._weave))
598
k.add_lines([], ['aaa', 'bbb'])
599
k.add_lines([0], ['111', 'aaa', 'ccc', 'bbb'])
600
k.add_lines([1], ['aaa', 'ccc', 'bbb', '222'])
636
k.add([], ['aaa', 'bbb'])
637
k.add([0], ['111', 'aaa', 'ccc', 'bbb'])
638
k.add([1], ['aaa', 'ccc', 'bbb', '222'])
641
class AutoMerge(TestBase):
645
texts = [['header', 'aaa', 'bbb'],
646
['header', 'aaa', 'line from 1', 'bbb'],
647
['header', 'aaa', 'bbb', 'line from 2', 'more from 2'],
650
k.add('text0', [], texts[0])
651
k.add('text1', [0], texts[1])
652
k.add('text2', [0], texts[2])
654
self.log('k._weave=' + pformat(k._weave))
656
m = list(k.mash_iter([0, 1, 2]))
662
'line from 2', 'more from 2'])
603
665
class Khayyam(TestBase):
604
666
"""Test changes to multi-line texts, and read/write"""
606
def test_multi_line_merge(self):
608
669
"""A Book of Verses underneath the Bough,
609
670
A Jug of Wine, a Loaf of Bread, -- and Thou
610
671
Beside me singing in the Wilderness --
611
672
Oh, Wilderness were Paradise enow!""",
613
674
"""A Book of Verses underneath the Bough,
614
675
A Jug of Wine, a Loaf of Bread, -- and Thou
615
676
Beside me singing in the Wilderness --
638
ver = k.add_lines('text%d' % i,
699
ver = k.add('text%d' % i,
639
700
list(parents), t)
640
parents.add('text%d' % i)
643
704
self.log("k._weave=" + pformat(k._weave))
645
706
for i, t in enumerate(texts):
646
self.assertEqual(k.get_lines(i), t)
707
self.assertEqual(k.get(i), t)
648
709
self.check_read_write(k)
712
class MergeCases(TestBase):
713
def doMerge(self, base, a, b, mp):
714
from cStringIO import StringIO
715
from textwrap import dedent
721
w.add('text0', [], map(addcrlf, base))
722
w.add('text1', [0], map(addcrlf, a))
723
w.add('text2', [0], map(addcrlf, b))
725
self.log('weave is:')
728
self.log(tmpf.getvalue())
730
self.log('merge plan:')
731
p = list(w.plan_merge(1, 2))
732
for state, line in p:
734
self.log('%12s | %s' % (state, line[:-1]))
738
mt.writelines(w.weave_merge(p))
740
self.log(mt.getvalue())
742
mp = map(addcrlf, mp)
743
self.assertEqual(mt.readlines(), mp)
746
def testOneInsert(self):
752
def testSeparateInserts(self):
753
self.doMerge(['aaa', 'bbb', 'ccc'],
754
['aaa', 'xxx', 'bbb', 'ccc'],
755
['aaa', 'bbb', 'yyy', 'ccc'],
756
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
758
def testSameInsert(self):
759
self.doMerge(['aaa', 'bbb', 'ccc'],
760
['aaa', 'xxx', 'bbb', 'ccc'],
761
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
762
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
764
def testOverlappedInsert(self):
765
self.doMerge(['aaa', 'bbb'],
766
['aaa', 'xxx', 'yyy', 'bbb'],
767
['aaa', 'xxx', 'bbb'],
768
['aaa', '<<<<<<<', 'xxx', 'yyy', '=======', 'xxx',
771
# really it ought to reduce this to
772
# ['aaa', 'xxx', 'yyy', 'bbb']
775
def testClashReplace(self):
776
self.doMerge(['aaa'],
779
['<<<<<<<', 'xxx', '=======', 'yyy', 'zzz',
782
def testNonClashInsert(self):
783
self.doMerge(['aaa'],
786
['<<<<<<<', 'xxx', 'aaa', '=======', 'yyy', 'zzz',
789
self.doMerge(['aaa'],
795
def testDeleteAndModify(self):
796
"""Clashing delete and modification.
798
If one side modifies a region and the other deletes it then
799
there should be a conflict with one side blank.
802
#######################################
803
# skippd, not working yet
806
self.doMerge(['aaa', 'bbb', 'ccc'],
807
['aaa', 'ddd', 'ccc'],
809
['<<<<<<<<', 'aaa', '=======', '>>>>>>>', 'ccc'])
651
812
class JoinWeavesTests(TestBase):
653
814
super(JoinWeavesTests, self).setUp()
654
815
self.weave1 = Weave()
655
816
self.lines1 = ['hello\n']
656
817
self.lines3 = ['hello\n', 'cruel\n', 'world\n']
657
self.weave1.add_lines('v1', [], self.lines1)
658
self.weave1.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
659
self.weave1.add_lines('v3', ['v2'], self.lines3)
818
self.weave1.add('v1', [], self.lines1)
819
self.weave1.add('v2', [0], ['hello\n', 'world\n'])
820
self.weave1.add('v3', [1], self.lines3)
822
def test_join_empty(self):
823
"""Join two empty weaves."""
824
eq = self.assertEqual
828
eq(w1.numversions(), 0)
830
def test_join_empty_to_nonempty(self):
831
"""Join empty weave onto nonempty."""
832
self.weave1.join(Weave())
833
self.assertEqual(len(self.weave1), 3)
835
def test_join_unrelated(self):
836
"""Join two weaves with no history in common."""
838
wb.add('b1', [], ['line from b\n'])
841
eq = self.assertEqual
843
eq(sorted(list(w1.iter_names())),
844
['b1', 'v1', 'v2', 'v3'])
846
def test_join_related(self):
847
wa = self.weave1.copy()
848
wb = self.weave1.copy()
849
wa.add('a1', ['v3'], ['hello\n', 'sweet\n', 'world\n'])
850
wb.add('b1', ['v3'], ['hello\n', 'pale blue\n', 'world\n'])
851
eq = self.assertEquals
856
eq(wa.get_lines('b1'),
857
['hello\n', 'pale blue\n', 'world\n'])
859
def test_join_parent_disagreement(self):
860
"""Cannot join weaves with different parents for a version."""
863
wa.add('v1', [], ['hello\n'])
865
wb.add('v1', ['v0'], ['hello\n'])
866
self.assertRaises(WeaveError,
869
def test_join_text_disagreement(self):
870
"""Cannot join weaves with different texts for a version."""
873
wa.add('v1', [], ['hello\n'])
874
wb.add('v1', [], ['not\n', 'hello\n'])
875
self.assertRaises(WeaveError,
878
def test_join_unordered(self):
879
"""Join weaves where indexes differ.
881
The source weave contains a different version at index 0."""
882
wa = self.weave1.copy()
884
wb.add('x1', [], ['line from x1\n'])
885
wb.add('v1', [], ['hello\n'])
886
wb.add('v2', ['v1'], ['hello\n', 'world\n'])
888
eq = self.assertEquals
889
eq(sorted(wa.iter_names()), ['v1', 'v2', 'v3', 'x1',])
890
eq(wa.get_text('x1'), 'line from x1\n')
893
class Corruption(TestCase):
895
def test_detection(self):
896
# Test weaves detect corruption.
898
# Weaves contain a checksum of their texts.
899
# When a text is extracted, this checksum should be
903
w.add('v1', [], ['hello\n'])
904
w.add('v2', ['v1'], ['hello\n', 'there\n'])
906
# We are going to invasively corrupt the text
907
# Make sure the internals of weave are the same
908
self.assertEqual([('{', 0)
916
self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
917
, '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
922
w._weave[4] = 'There\n'
924
self.assertEqual('hello\n', w.get_text('v1'))
925
self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
926
self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
927
self.assertRaises(errors.WeaveInvalidChecksum, list, w.get_iter('v2'))
928
self.assertRaises(errors.WeaveInvalidChecksum, w.check)
931
w._weave[4] = 'there\n'
932
self.assertEqual('hello\nthere\n', w.get_text('v2'))
934
#Invalid checksum, first digit changed
935
w._sha1s[1] = 'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
937
self.assertEqual('hello\n', w.get_text('v1'))
938
self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
939
self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
940
self.assertRaises(errors.WeaveInvalidChecksum, list, w.get_iter('v2'))
941
self.assertRaises(errors.WeaveInvalidChecksum, w.check)
661
943
def test_written_detection(self):
662
944
# Test detection of weave file corruption.
734
1003
return Weave._extract(self, versions)
737
class TestNeedsReweave(TestCase):
738
"""Internal corner cases for when reweave is needed."""
740
def test_compatible_parents(self):
1006
class JoinOptimization(TestCase):
1007
"""Test that Weave.join() doesn't extract all texts, only what must be done."""
1009
def test_join(self):
1010
w1 = InstrumentedWeave()
1011
w2 = InstrumentedWeave()
1014
txt1 = ['a\n', 'b\n']
1015
txt2 = ['a\n', 'c\n']
1016
txt3 = ['a\n', 'b\n', 'c\n']
1018
w1.add('txt0', [], txt0) # extract 1a
1019
w2.add('txt0', [], txt0) # extract 1b
1020
w1.add('txt1', [0], txt1)# extract 2a
1021
w2.add('txt2', [0], txt2)# extract 2b
1022
w1.join(w2) # extract 3a to add txt2
1023
w2.join(w1) # extract 3b to add txt1
1025
w1.add('txt3', [1, 2], txt3) # extract 4a
1026
w2.add('txt3', [1, 2], txt3) # extract 4b
1027
# These secretly have inverted parents
1029
# This should not have to do any extractions
1030
w1.join(w2) # NO extract, texts already present with same parents
1031
w2.join(w1) # NO extract, texts already present with same parents
1033
self.assertEqual(4, w1._extract_count)
1034
self.assertEqual(4, w2._extract_count)
1036
def test_double_parent(self):
1037
# It should not be considered illegal to add
1038
# a revision with the same parent twice
1039
w1 = InstrumentedWeave()
1040
w2 = InstrumentedWeave()
1043
txt1 = ['a\n', 'b\n']
1044
txt2 = ['a\n', 'c\n']
1045
txt3 = ['a\n', 'b\n', 'c\n']
1047
w1.add('txt0', [], txt0)
1048
w2.add('txt0', [], txt0)
1049
w1.add('txt1', [0], txt1)
1050
w2.add('txt1', [0,0], txt1)
1051
# Same text, effectively the same, because the
1052
# parent is only repeated
1053
w1.join(w2) # extract 3a to add txt2
1054
w2.join(w1) # extract 3b to add txt1
1057
class MismatchedTexts(TestCase):
1058
"""Test that merging two weaves with different texts fails."""
1060
def test_reweave(self):
742
my_parents = set([1, 2, 3])
744
self.assertTrue(w1._compatible_parents(my_parents, set([3])))
746
self.assertTrue(w1._compatible_parents(my_parents, set(my_parents)))
747
# same empty corner case
748
self.assertTrue(w1._compatible_parents(set(), set()))
749
# other cannot contain stuff my_parents does not
750
self.assertFalse(w1._compatible_parents(set(), set([1])))
751
self.assertFalse(w1._compatible_parents(my_parents, set([1, 2, 3, 4])))
752
self.assertFalse(w1._compatible_parents(my_parents, set([4])))
755
class TestWeaveFile(TestCaseInTempDir):
757
def test_empty_file(self):
758
f = open('empty.weave', 'wb+')
760
self.assertRaises(errors.WeaveFormatError,
1064
w1.add('txt0', [], ['a\n'])
1065
w2.add('txt0', [], ['a\n'])
1066
w1.add('txt1', [0], ['a\n', 'b\n'])
1067
w2.add('txt1', [0], ['a\n', 'c\n'])
1069
self.assertRaises(errors.WeaveTextDiffers, w1.reweave, w2)