63
66
self.assertRaises(RevisionAlreadyPresent,
64
67
f.add_lines, 'r1', [], [])
69
# this checks that reopen with create=True does not break anything.
70
f = self.reopen_file(create=True)
73
def test_adds_with_parent_texts(self):
76
parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
78
parent_texts['r1'] = f.add_lines_with_ghosts('r1',
81
parent_texts=parent_texts)
82
except NotImplementedError:
83
# if the format doesn't support ghosts, just add normally.
84
parent_texts['r1'] = f.add_lines('r1',
87
parent_texts=parent_texts)
88
f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
89
self.assertNotEqual(None, parent_texts['r0'])
90
self.assertNotEqual(None, parent_texts['r1'])
92
versions = f.versions()
93
self.assertTrue('r0' in versions)
94
self.assertTrue('r1' in versions)
95
self.assertTrue('r2' in versions)
96
self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
97
self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
98
self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
99
self.assertEqual(3, f.num_versions())
100
origins = f.annotate('r1')
101
self.assertEquals(origins[0][0], 'r0')
102
self.assertEquals(origins[1][0], 'r1')
103
origins = f.annotate('r2')
104
self.assertEquals(origins[0][0], 'r1')
105
self.assertEquals(origins[1][0], 'r2')
66
108
f = self.reopen_file()
111
def test_add_unicode_content(self):
112
# unicode content is not permitted in versioned files.
113
# versioned files version sequences of bytes only.
115
self.assertRaises(errors.BzrBadParameterUnicode,
116
vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
118
(errors.BzrBadParameterUnicode, NotImplementedError),
119
vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
121
def test_inline_newline_throws(self):
122
# \r characters are not permitted in lines being added
124
self.assertRaises(errors.BzrBadParameterContainsNewline,
125
vf.add_lines, 'a', [], ['a\n\n'])
127
(errors.BzrBadParameterContainsNewline, NotImplementedError),
128
vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
129
# but inline CR's are allowed
130
vf.add_lines('a', [], ['a\r\n'])
132
vf.add_lines_with_ghosts('b', [], ['a\r\n'])
133
except NotImplementedError:
136
def test_get_delta(self):
138
sha1s = self._setup_for_deltas(f)
139
expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False,
140
[(0, 0, 1, [('base', 'line\n')])])
141
self.assertEqual(expected_delta, f.get_delta('base'))
143
text_name = 'chain1-'
144
for depth in range(26):
145
new_version = text_name + '%s' % depth
146
expected_delta = (next_parent, sha1s[depth],
148
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
149
self.assertEqual(expected_delta, f.get_delta(new_version))
150
next_parent = new_version
152
text_name = 'chain2-'
153
for depth in range(26):
154
new_version = text_name + '%s' % depth
155
expected_delta = (next_parent, sha1s[depth], False,
156
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
157
self.assertEqual(expected_delta, f.get_delta(new_version))
158
next_parent = new_version
159
# smoke test for eol support
160
expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
161
self.assertEqual(['line'], f.get_lines('noeol'))
162
self.assertEqual(expected_delta, f.get_delta('noeol'))
164
def test_get_deltas(self):
166
sha1s = self._setup_for_deltas(f)
167
deltas = f.get_deltas(f.versions())
168
expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False,
169
[(0, 0, 1, [('base', 'line\n')])])
170
self.assertEqual(expected_delta, deltas['base'])
172
text_name = 'chain1-'
173
for depth in range(26):
174
new_version = text_name + '%s' % depth
175
expected_delta = (next_parent, sha1s[depth],
177
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
178
self.assertEqual(expected_delta, deltas[new_version])
179
next_parent = new_version
181
text_name = 'chain2-'
182
for depth in range(26):
183
new_version = text_name + '%s' % depth
184
expected_delta = (next_parent, sha1s[depth], False,
185
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
186
self.assertEqual(expected_delta, deltas[new_version])
187
next_parent = new_version
188
# smoke tests for eol support
189
expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
190
self.assertEqual(['line'], f.get_lines('noeol'))
191
self.assertEqual(expected_delta, deltas['noeol'])
192
# smoke tests for eol support - two noeol in a row same content
193
expected_deltas = (('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True,
194
[(0, 1, 2, [(u'noeolsecond', 'line\n'), (u'noeolsecond', 'line\n')])]),
195
('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True,
196
[(0, 0, 1, [('noeolsecond', 'line\n')]), (1, 1, 0, [])]))
197
self.assertEqual(['line\n', 'line'], f.get_lines('noeolsecond'))
198
self.assertTrue(deltas['noeolsecond'] in expected_deltas)
199
# two no-eol in a row, different content
200
expected_delta = ('noeolsecond', '8bb553a84e019ef1149db082d65f3133b195223b', True,
201
[(1, 2, 1, [(u'noeolnotshared', 'phone\n')])])
202
self.assertEqual(['line\n', 'phone'], f.get_lines('noeolnotshared'))
203
self.assertEqual(expected_delta, deltas['noeolnotshared'])
204
# eol folling a no-eol with content change
205
expected_delta = ('noeol', 'a61f6fb6cfc4596e8d88c34a308d1e724caf8977', False,
206
[(0, 1, 1, [(u'eol', 'phone\n')])])
207
self.assertEqual(['phone\n'], f.get_lines('eol'))
208
self.assertEqual(expected_delta, deltas['eol'])
209
# eol folling a no-eol with content change
210
expected_delta = ('noeol', '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False,
211
[(0, 1, 1, [(u'eolline', 'line\n')])])
212
self.assertEqual(['line\n'], f.get_lines('eolline'))
213
self.assertEqual(expected_delta, deltas['eolline'])
214
# eol with no parents
215
expected_delta = (None, '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
216
[(0, 0, 1, [(u'noeolbase', 'line\n')])])
217
self.assertEqual(['line'], f.get_lines('noeolbase'))
218
self.assertEqual(expected_delta, deltas['noeolbase'])
219
# eol with two parents, in inverse insertion order
220
expected_deltas = (('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
221
[(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]),
222
('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
223
[(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]))
224
self.assertEqual(['line'], f.get_lines('eolbeforefirstparent'))
225
#self.assertTrue(deltas['eolbeforefirstparent'] in expected_deltas)
227
def _setup_for_deltas(self, f):
228
self.assertRaises(errors.RevisionNotPresent, f.get_delta, 'base')
229
# add texts that should trip the knit maximum delta chain threshold
230
# as well as doing parallel chains of data in knits.
231
# this is done by two chains of 25 insertions
232
f.add_lines('base', [], ['line\n'])
233
f.add_lines('noeol', ['base'], ['line'])
234
# detailed eol tests:
235
# shared last line with parent no-eol
236
f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
237
# differing last line with parent, both no-eol
238
f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
239
# add eol following a noneol parent, change content
240
f.add_lines('eol', ['noeol'], ['phone\n'])
241
# add eol following a noneol parent, no change content
242
f.add_lines('eolline', ['noeol'], ['line\n'])
243
# noeol with no parents:
244
f.add_lines('noeolbase', [], ['line'])
245
# noeol preceeding its leftmost parent in the output:
246
# this is done by making it a merge of two parents with no common
247
# anestry: noeolbase and noeol with the
248
# later-inserted parent the leftmost.
249
f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
250
# two identical eol texts
251
f.add_lines('noeoldup', ['noeol'], ['line'])
253
text_name = 'chain1-'
255
sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
256
1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
257
2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
258
3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
259
4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
260
5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
261
6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
262
7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
263
8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
264
9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
265
10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
266
11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
267
12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
268
13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
269
14:'2c4b1736566b8ca6051e668de68650686a3922f2',
270
15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
271
16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
272
17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
273
18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
274
19:'1ebed371807ba5935958ad0884595126e8c4e823',
275
20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
276
21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
277
22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
278
23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
279
24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
280
25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
282
for depth in range(26):
283
new_version = text_name + '%s' % depth
284
text = text + ['line\n']
285
f.add_lines(new_version, [next_parent], text)
286
next_parent = new_version
288
text_name = 'chain2-'
290
for depth in range(26):
291
new_version = text_name + '%s' % depth
292
text = text + ['line\n']
293
f.add_lines(new_version, [next_parent], text)
294
next_parent = new_version
297
def test_add_delta(self):
298
# tests for the add-delta facility.
299
# at this point, optimising for speed, we assume no checks when deltas are inserted.
300
# this may need to be revisited.
301
source = self.get_file('source')
302
source.add_lines('base', [], ['line\n'])
304
text_name = 'chain1-'
306
for depth in range(26):
307
new_version = text_name + '%s' % depth
308
text = text + ['line\n']
309
source.add_lines(new_version, [next_parent], text)
310
next_parent = new_version
312
text_name = 'chain2-'
314
for depth in range(26):
315
new_version = text_name + '%s' % depth
316
text = text + ['line\n']
317
source.add_lines(new_version, [next_parent], text)
318
next_parent = new_version
319
source.add_lines('noeol', ['base'], ['line'])
321
target = self.get_file('target')
322
for version in source.versions():
323
parent, sha1, noeol, delta = source.get_delta(version)
324
target.add_delta(version,
325
source.get_parents(version),
330
self.assertRaises(RevisionAlreadyPresent,
331
target.add_delta, 'base', [], None, '', False, [])
332
for version in source.versions():
333
self.assertEqual(source.get_lines(version),
334
target.get_lines(version))
69
336
def test_ancestry(self):
70
337
f = self.get_file()
71
338
self.assertEqual([], f.get_ancestry([]))
543
830
versionedfile.InterVersionedFile.unregister_optimiser(InterString)
544
831
# now we should get the default InterVersionedFile object again.
545
832
self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
835
class TestReadonlyHttpMixin(object):
837
def test_readonly_http_works(self):
838
# we should be able to read from http with a versioned file.
840
# try an empty file access
841
readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
842
self.assertEqual([], readonly_vf.versions())
844
vf.add_lines('1', [], ['a\n'])
845
vf.add_lines('2', ['1'], ['b\n', 'a\n'])
846
readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
847
self.assertEqual(['1', '2'], vf.versions())
848
for version in readonly_vf.versions():
849
readonly_vf.get_lines(version)
852
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
855
return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
857
def get_factory(self):
861
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
864
return KnitVersionedFile('foo', get_transport(self.get_url('.')),
865
delta=True, create=True)
867
def get_factory(self):
868
return KnitVersionedFile
871
class MergeCasesMixin(object):
873
def doMerge(self, base, a, b, mp):
874
from cStringIO import StringIO
875
from textwrap import dedent
881
w.add_lines('text0', [], map(addcrlf, base))
882
w.add_lines('text1', ['text0'], map(addcrlf, a))
883
w.add_lines('text2', ['text0'], map(addcrlf, b))
887
self.log('merge plan:')
888
p = list(w.plan_merge('text1', 'text2'))
889
for state, line in p:
891
self.log('%12s | %s' % (state, line[:-1]))
895
mt.writelines(w.weave_merge(p))
897
self.log(mt.getvalue())
899
mp = map(addcrlf, mp)
900
self.assertEqual(mt.readlines(), mp)
903
def testOneInsert(self):
909
def testSeparateInserts(self):
910
self.doMerge(['aaa', 'bbb', 'ccc'],
911
['aaa', 'xxx', 'bbb', 'ccc'],
912
['aaa', 'bbb', 'yyy', 'ccc'],
913
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
915
def testSameInsert(self):
916
self.doMerge(['aaa', 'bbb', 'ccc'],
917
['aaa', 'xxx', 'bbb', 'ccc'],
918
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
919
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
920
overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
921
def testOverlappedInsert(self):
922
self.doMerge(['aaa', 'bbb'],
923
['aaa', 'xxx', 'yyy', 'bbb'],
924
['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
926
# really it ought to reduce this to
927
# ['aaa', 'xxx', 'yyy', 'bbb']
930
def testClashReplace(self):
931
self.doMerge(['aaa'],
934
['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz',
937
def testNonClashInsert1(self):
938
self.doMerge(['aaa'],
941
['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz',
944
def testNonClashInsert2(self):
945
self.doMerge(['aaa'],
951
def testDeleteAndModify(self):
952
"""Clashing delete and modification.
954
If one side modifies a region and the other deletes it then
955
there should be a conflict with one side blank.
958
#######################################
959
# skippd, not working yet
962
self.doMerge(['aaa', 'bbb', 'ccc'],
963
['aaa', 'ddd', 'ccc'],
965
['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
967
def _test_merge_from_strings(self, base, a, b, expected):
969
w.add_lines('text0', [], base.splitlines(True))
970
w.add_lines('text1', ['text0'], a.splitlines(True))
971
w.add_lines('text2', ['text0'], b.splitlines(True))
972
self.log('merge plan:')
973
p = list(w.plan_merge('text1', 'text2'))
974
for state, line in p:
976
self.log('%12s | %s' % (state, line[:-1]))
977
self.log('merge result:')
978
result_text = ''.join(w.weave_merge(p))
979
self.log(result_text)
980
self.assertEqualDiff(result_text, expected)
982
def test_weave_merge_conflicts(self):
983
# does weave merge properly handle plans that end with unchanged?
984
result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
985
self.assertEqual(result, 'hello\n')
987
def test_deletion_extended(self):
988
"""One side deletes, the other deletes more.
1005
self._test_merge_from_strings(base, a, b, result)
1007
def test_deletion_overlap(self):
1008
"""Delete overlapping regions with no other conflict.
1010
Arguably it'd be better to treat these as agreement, rather than
1011
conflict, but for now conflict is safer.
1039
self._test_merge_from_strings(base, a, b, result)
1041
def test_agreement_deletion(self):
1042
"""Agree to delete some lines, without conflicts."""
1064
self._test_merge_from_strings(base, a, b, result)
1066
def test_sync_on_deletion(self):
1067
"""Specific case of merge where we can synchronize incorrectly.
1069
A previous version of the weave merge concluded that the two versions
1070
agreed on deleting line 2, and this could be a synchronization point.
1071
Line 1 was then considered in isolation, and thought to be deleted on
1074
It's better to consider the whole thing as a disagreement region.
1085
a's replacement line 2
1098
a's replacement line 2
1105
self._test_merge_from_strings(base, a, b, result)
1108
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
1110
def get_file(self, name='foo'):
1111
return KnitVersionedFile(name, get_transport(self.get_url('.')),
1112
delta=True, create=True)
1114
def log_contents(self, w):
1118
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
1120
def get_file(self, name='foo'):
1121
return WeaveFile(name, get_transport(self.get_url('.')), create=True)
1123
def log_contents(self, w):
1124
self.log('weave is:')
1126
write_weave(w, tmpf)
1127
self.log(tmpf.getvalue())
1129
overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======',
1130
'xxx', '>>>>>>> ', 'bbb']