116
104
f = self.reopen_file()
119
def test_add_unicode_content(self):
120
# unicode content is not permitted in versioned files.
121
# versioned files version sequences of bytes only.
123
self.assertRaises(errors.BzrBadParameterUnicode,
124
vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
126
(errors.BzrBadParameterUnicode, NotImplementedError),
127
vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
129
def test_inline_newline_throws(self):
130
# \r characters are not permitted in lines being added
132
self.assertRaises(errors.BzrBadParameterContainsNewline,
133
vf.add_lines, 'a', [], ['a\n\n'])
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'])
140
vf.add_lines_with_ghosts('b', [], ['a\r\n'])
141
except NotImplementedError:
144
def test_add_reserved(self):
146
self.assertRaises(errors.ReservedId,
147
vf.add_lines, 'a:', [], ['a\n', 'b\n', 'c\n'])
149
self.assertRaises(errors.ReservedId,
150
vf.add_delta, 'a:', [], None, 'sha1', False, ((0, 0, 0, []),))
152
def test_get_reserved(self):
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:')
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)
446
391
# and should be a list
447
392
self.assertTrue(isinstance(f.__class__.get_suffixes(), list))
449
def build_graph(self, file, graph):
450
for node in topo_sort(graph.items()):
451
file.add_lines(node, graph[node], [])
453
394
def test_get_graph(self):
454
395
f = self.get_file()
459
self.build_graph(f, graph)
460
self.assertEqual(graph, f.get_graph())
462
def test_get_graph_partial(self):
470
complex_graph.update(simple_a)
475
complex_graph.update(simple_b)
482
complex_graph.update(simple_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': [],
492
404
def test_get_parents(self):
493
405
f = self.get_file()
598
499
'otherchild\n':0,
600
progress = InstrumentedProgress()
601
501
# iterate over the lines
602
for line in vf.iter_lines_added_or_present_in_versions(versions,
502
for line in vf.iter_lines_added_or_present_in_versions(versions):
605
if []!= progress.updates:
606
self.assertEqual(expected, progress.updates)
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.
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)
684
570
self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
685
571
self.assertRaises(NotImplementedError, vf.get_graph_with_ghosts)
687
vf = self.reopen_file()
688
573
# test key graph related apis: getncestry, _graph, get_parents
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'],
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'],
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'))
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
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)
929
class TestReadonlyHttpMixin(object):
931
def test_readonly_http_works(self):
932
# we should be able to read from http with a versioned 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())
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)
946
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
949
return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
951
def get_factory(self):
955
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
958
return KnitVersionedFile('foo', get_transport(self.get_url('.')),
959
delta=True, create=True)
961
def get_factory(self):
962
return KnitVersionedFile
965
class MergeCasesMixin(object):
967
def doMerge(self, base, a, b, mp):
968
from cStringIO import StringIO
969
from textwrap import dedent
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))
981
self.log('merge plan:')
982
p = list(w.plan_merge('text1', 'text2'))
983
for state, line in p:
985
self.log('%12s | %s' % (state, line[:-1]))
989
mt.writelines(w.weave_merge(p))
991
self.log(mt.getvalue())
993
mp = map(addcrlf, mp)
994
self.assertEqual(mt.readlines(), mp)
997
def testOneInsert(self):
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'])
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)
1020
# really it ought to reduce this to
1021
# ['aaa', 'xxx', 'yyy', 'bbb']
1024
def testClashReplace(self):
1025
self.doMerge(['aaa'],
1028
['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz',
1031
def testNonClashInsert1(self):
1032
self.doMerge(['aaa'],
1035
['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz',
1038
def testNonClashInsert2(self):
1039
self.doMerge(['aaa'],
1045
def testDeleteAndModify(self):
1046
"""Clashing delete and modification.
1048
If one side modifies a region and the other deletes it then
1049
there should be a conflict with one side blank.
1052
#######################################
1053
# skippd, not working yet
1056
self.doMerge(['aaa', 'bbb', 'ccc'],
1057
['aaa', 'ddd', 'ccc'],
1059
['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
1061
def _test_merge_from_strings(self, base, a, b, expected):
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:
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)
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')
1081
def test_deletion_extended(self):
1082
"""One side deletes, the other deletes more.
1099
self._test_merge_from_strings(base, a, b, result)
1101
def test_deletion_overlap(self):
1102
"""Delete overlapping regions with no other conflict.
1104
Arguably it'd be better to treat these as agreement, rather than
1105
conflict, but for now conflict is safer.
1133
self._test_merge_from_strings(base, a, b, result)
1135
def test_agreement_deletion(self):
1136
"""Agree to delete some lines, without conflicts."""
1158
self._test_merge_from_strings(base, a, b, result)
1160
def test_sync_on_deletion(self):
1161
"""Specific case of merge where we can synchronize incorrectly.
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
1168
It's better to consider the whole thing as a disagreement region.
1179
a's replacement line 2
1192
a's replacement line 2
1199
self._test_merge_from_strings(base, a, b, result)
1202
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
1204
def get_file(self, name='foo'):
1205
return KnitVersionedFile(name, get_transport(self.get_url('.')),
1206
delta=True, create=True)
1208
def log_contents(self, w):
1212
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
1214
def get_file(self, name='foo'):
1215
return WeaveFile(name, get_transport(self.get_url('.')), create=True)
1217
def log_contents(self, w):
1218
self.log('weave is:')
1220
write_weave(w, tmpf)
1221
self.log(tmpf.getvalue())
1223
overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======',
1224
'xxx', '>>>>>>> ', 'bbb']