104
115
f = self.reopen_file()
118
def test_add_unicode_content(self):
119
# unicode content is not permitted in versioned files.
120
# versioned files version sequences of bytes only.
122
self.assertRaises(errors.BzrBadParameterUnicode,
123
vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
125
(errors.BzrBadParameterUnicode, NotImplementedError),
126
vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
128
def test_inline_newline_throws(self):
129
# \r characters are not permitted in lines being added
131
self.assertRaises(errors.BzrBadParameterContainsNewline,
132
vf.add_lines, 'a', [], ['a\n\n'])
134
(errors.BzrBadParameterContainsNewline, NotImplementedError),
135
vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
136
# but inline CR's are allowed
137
vf.add_lines('a', [], ['a\r\n'])
139
vf.add_lines_with_ghosts('b', [], ['a\r\n'])
140
except NotImplementedError:
107
143
def test_get_delta(self):
108
144
f = self.get_file()
109
145
sha1s = self._setup_for_deltas(f)
391
427
# and should be a list
392
428
self.assertTrue(isinstance(f.__class__.get_suffixes(), list))
430
def build_graph(self, file, graph):
431
for node in topo_sort(graph.items()):
432
file.add_lines(node, graph[node], [])
394
434
def test_get_graph(self):
395
435
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': [],
440
self.build_graph(f, graph)
441
self.assertEqual(graph, f.get_graph())
443
def test_get_graph_partial(self):
451
complex_graph.update(simple_a)
456
complex_graph.update(simple_b)
463
complex_graph.update(simple_gam)
465
simple_b_gam.update(simple_gam)
466
simple_b_gam.update(simple_b)
467
self.build_graph(f, complex_graph)
468
self.assertEqual(simple_a, f.get_graph(['a']))
469
self.assertEqual(simple_b, f.get_graph(['b']))
470
self.assertEqual(simple_gam, f.get_graph(['gam']))
471
self.assertEqual(simple_b_gam, f.get_graph(['b', 'gam']))
404
473
def test_get_parents(self):
405
474
f = self.get_file()
499
579
'otherchild\n':0,
581
progress = InstrumentedProgress()
501
582
# iterate over the lines
502
for line in vf.iter_lines_added_or_present_in_versions(versions):
583
for line in vf.iter_lines_added_or_present_in_versions(versions,
586
if []!= progress.updates:
587
self.assertEqual(expected, progress.updates)
505
lines = iter_with_versions(['child', 'otherchild'])
589
lines = iter_with_versions(['child', 'otherchild'],
590
[('Walking content.', 0, 2),
591
('Walking content.', 0, 2),
592
('Walking content.', 3, 2),
593
('Walking content.', 2, 2)])
506
594
# we must see child and otherchild
507
595
self.assertTrue(lines['child\n'] > 0)
508
596
self.assertTrue(lines['otherchild\n'] > 0)
509
597
# we dont care if we got more than that.
512
lines = iter_with_versions(None)
600
lines = iter_with_versions(None, [('Walking content.', 0, 5),
601
('Walking content.', 0, 5),
602
('Walking content.', 1, 5),
603
('Walking content.', 2, 5),
604
('Walking content.', 2, 5),
605
('Walking content.', 3, 5),
606
('Walking content.', 5, 5)])
513
607
# all lines must be seen at least once
514
608
self.assertTrue(lines['base\n'] > 0)
515
609
self.assertTrue(lines['lancestor\n'] > 0)
783
894
versionedfile.InterVersionedFile.unregister_optimiser(InterString)
784
895
# now we should get the default InterVersionedFile object again.
785
896
self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
899
class TestReadonlyHttpMixin(object):
901
def test_readonly_http_works(self):
902
# we should be able to read from http with a versioned file.
904
# try an empty file access
905
readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
906
self.assertEqual([], readonly_vf.versions())
908
vf.add_lines('1', [], ['a\n'])
909
vf.add_lines('2', ['1'], ['b\n', 'a\n'])
910
readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
911
self.assertEqual(['1', '2'], vf.versions())
912
for version in readonly_vf.versions():
913
readonly_vf.get_lines(version)
916
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
919
return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
921
def get_factory(self):
925
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
928
return KnitVersionedFile('foo', get_transport(self.get_url('.')),
929
delta=True, create=True)
931
def get_factory(self):
932
return KnitVersionedFile
935
class MergeCasesMixin(object):
937
def doMerge(self, base, a, b, mp):
938
from cStringIO import StringIO
939
from textwrap import dedent
945
w.add_lines('text0', [], map(addcrlf, base))
946
w.add_lines('text1', ['text0'], map(addcrlf, a))
947
w.add_lines('text2', ['text0'], map(addcrlf, b))
951
self.log('merge plan:')
952
p = list(w.plan_merge('text1', 'text2'))
953
for state, line in p:
955
self.log('%12s | %s' % (state, line[:-1]))
959
mt.writelines(w.weave_merge(p))
961
self.log(mt.getvalue())
963
mp = map(addcrlf, mp)
964
self.assertEqual(mt.readlines(), mp)
967
def testOneInsert(self):
973
def testSeparateInserts(self):
974
self.doMerge(['aaa', 'bbb', 'ccc'],
975
['aaa', 'xxx', 'bbb', 'ccc'],
976
['aaa', 'bbb', 'yyy', 'ccc'],
977
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
979
def testSameInsert(self):
980
self.doMerge(['aaa', 'bbb', 'ccc'],
981
['aaa', 'xxx', 'bbb', 'ccc'],
982
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
983
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
984
overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
985
def testOverlappedInsert(self):
986
self.doMerge(['aaa', 'bbb'],
987
['aaa', 'xxx', 'yyy', 'bbb'],
988
['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
990
# really it ought to reduce this to
991
# ['aaa', 'xxx', 'yyy', 'bbb']
994
def testClashReplace(self):
995
self.doMerge(['aaa'],
998
['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz',
1001
def testNonClashInsert1(self):
1002
self.doMerge(['aaa'],
1005
['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz',
1008
def testNonClashInsert2(self):
1009
self.doMerge(['aaa'],
1015
def testDeleteAndModify(self):
1016
"""Clashing delete and modification.
1018
If one side modifies a region and the other deletes it then
1019
there should be a conflict with one side blank.
1022
#######################################
1023
# skippd, not working yet
1026
self.doMerge(['aaa', 'bbb', 'ccc'],
1027
['aaa', 'ddd', 'ccc'],
1029
['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
1031
def _test_merge_from_strings(self, base, a, b, expected):
1033
w.add_lines('text0', [], base.splitlines(True))
1034
w.add_lines('text1', ['text0'], a.splitlines(True))
1035
w.add_lines('text2', ['text0'], b.splitlines(True))
1036
self.log('merge plan:')
1037
p = list(w.plan_merge('text1', 'text2'))
1038
for state, line in p:
1040
self.log('%12s | %s' % (state, line[:-1]))
1041
self.log('merge result:')
1042
result_text = ''.join(w.weave_merge(p))
1043
self.log(result_text)
1044
self.assertEqualDiff(result_text, expected)
1046
def test_weave_merge_conflicts(self):
1047
# does weave merge properly handle plans that end with unchanged?
1048
result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
1049
self.assertEqual(result, 'hello\n')
1051
def test_deletion_extended(self):
1052
"""One side deletes, the other deletes more.
1069
self._test_merge_from_strings(base, a, b, result)
1071
def test_deletion_overlap(self):
1072
"""Delete overlapping regions with no other conflict.
1074
Arguably it'd be better to treat these as agreement, rather than
1075
conflict, but for now conflict is safer.
1103
self._test_merge_from_strings(base, a, b, result)
1105
def test_agreement_deletion(self):
1106
"""Agree to delete some lines, without conflicts."""
1128
self._test_merge_from_strings(base, a, b, result)
1130
def test_sync_on_deletion(self):
1131
"""Specific case of merge where we can synchronize incorrectly.
1133
A previous version of the weave merge concluded that the two versions
1134
agreed on deleting line 2, and this could be a synchronization point.
1135
Line 1 was then considered in isolation, and thought to be deleted on
1138
It's better to consider the whole thing as a disagreement region.
1149
a's replacement line 2
1162
a's replacement line 2
1169
self._test_merge_from_strings(base, a, b, result)
1172
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
1174
def get_file(self, name='foo'):
1175
return KnitVersionedFile(name, get_transport(self.get_url('.')),
1176
delta=True, create=True)
1178
def log_contents(self, w):
1182
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
1184
def get_file(self, name='foo'):
1185
return WeaveFile(name, get_transport(self.get_url('.')), create=True)
1187
def log_contents(self, w):
1188
self.log('weave is:')
1190
write_weave(w, tmpf)
1191
self.log(tmpf.getvalue())
1193
overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======',
1194
'xxx', '>>>>>>> ', 'bbb']