1
# Copyright (C) 2005 by Canonical Ltd
4
# Johan Rydberg <jrydberg@gnu.org>
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from StringIO import StringIO
24
import bzrlib.errors as errors
25
from bzrlib.errors import (
27
RevisionAlreadyPresent,
30
from bzrlib.knit import KnitVersionedFile, \
32
from bzrlib.tests import TestCaseWithTransport
33
from bzrlib.tests.HTTPTestUtil import TestCaseWithWebserver
34
from bzrlib.trace import mutter
35
from bzrlib.transport import get_transport
36
from bzrlib.transport.memory import MemoryTransport
37
from bzrlib.tsort import topo_sort
38
import bzrlib.versionedfile as versionedfile
39
from bzrlib.weave import WeaveFile
40
from bzrlib.weavefile import read_weave, write_weave
43
class VersionedFileTestMixIn(object):
44
"""A mixin test class for testing VersionedFiles.
46
This is not an adaptor-style test at this point because
47
theres no dynamic substitution of versioned file implementations,
48
they are strictly controlled by their owning repositories.
53
f.add_lines('r0', [], ['a\n', 'b\n'])
54
f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
56
versions = f.versions()
57
self.assertTrue('r0' in versions)
58
self.assertTrue('r1' in versions)
59
self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
60
self.assertEquals(f.get_text('r0'), 'a\nb\n')
61
self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
62
self.assertEqual(2, len(f))
63
self.assertEqual(2, f.num_versions())
65
self.assertRaises(RevisionNotPresent,
66
f.add_lines, 'r2', ['foo'], [])
67
self.assertRaises(RevisionAlreadyPresent,
68
f.add_lines, 'r1', [], [])
70
# this checks that reopen with create=True does not break anything.
71
f = self.reopen_file(create=True)
74
def test_adds_with_parent_texts(self):
77
parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
79
parent_texts['r1'] = f.add_lines_with_ghosts('r1',
82
parent_texts=parent_texts)
83
except NotImplementedError:
84
# if the format doesn't support ghosts, just add normally.
85
parent_texts['r1'] = f.add_lines('r1',
88
parent_texts=parent_texts)
89
f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
90
self.assertNotEqual(None, parent_texts['r0'])
91
self.assertNotEqual(None, parent_texts['r1'])
93
versions = f.versions()
94
self.assertTrue('r0' in versions)
95
self.assertTrue('r1' in versions)
96
self.assertTrue('r2' in versions)
97
self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
98
self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
99
self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
100
self.assertEqual(3, f.num_versions())
101
origins = f.annotate('r1')
102
self.assertEquals(origins[0][0], 'r0')
103
self.assertEquals(origins[1][0], 'r1')
104
origins = f.annotate('r2')
105
self.assertEquals(origins[0][0], 'r1')
106
self.assertEquals(origins[1][0], 'r2')
109
f = self.reopen_file()
112
def test_add_unicode_content(self):
113
# unicode content is not permitted in versioned files.
114
# versioned files version sequences of bytes only.
116
self.assertRaises(errors.BzrBadParameterUnicode,
117
vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
119
(errors.BzrBadParameterUnicode, NotImplementedError),
120
vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
122
def test_inline_newline_throws(self):
123
# \r characters are not permitted in lines being added
125
self.assertRaises(errors.BzrBadParameterContainsNewline,
126
vf.add_lines, 'a', [], ['a\n\n'])
128
(errors.BzrBadParameterContainsNewline, NotImplementedError),
129
vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
130
# but inline CR's are allowed
131
vf.add_lines('a', [], ['a\r\n'])
133
vf.add_lines_with_ghosts('b', [], ['a\r\n'])
134
except NotImplementedError:
137
def test_get_delta(self):
139
sha1s = self._setup_for_deltas(f)
140
expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False,
141
[(0, 0, 1, [('base', 'line\n')])])
142
self.assertEqual(expected_delta, f.get_delta('base'))
144
text_name = 'chain1-'
145
for depth in range(26):
146
new_version = text_name + '%s' % depth
147
expected_delta = (next_parent, sha1s[depth],
149
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
150
self.assertEqual(expected_delta, f.get_delta(new_version))
151
next_parent = new_version
153
text_name = 'chain2-'
154
for depth in range(26):
155
new_version = text_name + '%s' % depth
156
expected_delta = (next_parent, sha1s[depth], False,
157
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
158
self.assertEqual(expected_delta, f.get_delta(new_version))
159
next_parent = new_version
160
# smoke test for eol support
161
expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
162
self.assertEqual(['line'], f.get_lines('noeol'))
163
self.assertEqual(expected_delta, f.get_delta('noeol'))
165
def test_get_deltas(self):
167
sha1s = self._setup_for_deltas(f)
168
deltas = f.get_deltas(f.versions())
169
expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False,
170
[(0, 0, 1, [('base', 'line\n')])])
171
self.assertEqual(expected_delta, deltas['base'])
173
text_name = 'chain1-'
174
for depth in range(26):
175
new_version = text_name + '%s' % depth
176
expected_delta = (next_parent, sha1s[depth],
178
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
179
self.assertEqual(expected_delta, deltas[new_version])
180
next_parent = new_version
182
text_name = 'chain2-'
183
for depth in range(26):
184
new_version = text_name + '%s' % depth
185
expected_delta = (next_parent, sha1s[depth], False,
186
[(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
187
self.assertEqual(expected_delta, deltas[new_version])
188
next_parent = new_version
189
# smoke tests for eol support
190
expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
191
self.assertEqual(['line'], f.get_lines('noeol'))
192
self.assertEqual(expected_delta, deltas['noeol'])
193
# smoke tests for eol support - two noeol in a row same content
194
expected_deltas = (('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True,
195
[(0, 1, 2, [(u'noeolsecond', 'line\n'), (u'noeolsecond', 'line\n')])]),
196
('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True,
197
[(0, 0, 1, [('noeolsecond', 'line\n')]), (1, 1, 0, [])]))
198
self.assertEqual(['line\n', 'line'], f.get_lines('noeolsecond'))
199
self.assertTrue(deltas['noeolsecond'] in expected_deltas)
200
# two no-eol in a row, different content
201
expected_delta = ('noeolsecond', '8bb553a84e019ef1149db082d65f3133b195223b', True,
202
[(1, 2, 1, [(u'noeolnotshared', 'phone\n')])])
203
self.assertEqual(['line\n', 'phone'], f.get_lines('noeolnotshared'))
204
self.assertEqual(expected_delta, deltas['noeolnotshared'])
205
# eol folling a no-eol with content change
206
expected_delta = ('noeol', 'a61f6fb6cfc4596e8d88c34a308d1e724caf8977', False,
207
[(0, 1, 1, [(u'eol', 'phone\n')])])
208
self.assertEqual(['phone\n'], f.get_lines('eol'))
209
self.assertEqual(expected_delta, deltas['eol'])
210
# eol folling a no-eol with content change
211
expected_delta = ('noeol', '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False,
212
[(0, 1, 1, [(u'eolline', 'line\n')])])
213
self.assertEqual(['line\n'], f.get_lines('eolline'))
214
self.assertEqual(expected_delta, deltas['eolline'])
215
# eol with no parents
216
expected_delta = (None, '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
217
[(0, 0, 1, [(u'noeolbase', 'line\n')])])
218
self.assertEqual(['line'], f.get_lines('noeolbase'))
219
self.assertEqual(expected_delta, deltas['noeolbase'])
220
# eol with two parents, in inverse insertion order
221
expected_deltas = (('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
222
[(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]),
223
('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
224
[(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]))
225
self.assertEqual(['line'], f.get_lines('eolbeforefirstparent'))
226
#self.assertTrue(deltas['eolbeforefirstparent'] in expected_deltas)
228
def _setup_for_deltas(self, f):
229
self.assertRaises(errors.RevisionNotPresent, f.get_delta, 'base')
230
# add texts that should trip the knit maximum delta chain threshold
231
# as well as doing parallel chains of data in knits.
232
# this is done by two chains of 25 insertions
233
f.add_lines('base', [], ['line\n'])
234
f.add_lines('noeol', ['base'], ['line'])
235
# detailed eol tests:
236
# shared last line with parent no-eol
237
f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
238
# differing last line with parent, both no-eol
239
f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
240
# add eol following a noneol parent, change content
241
f.add_lines('eol', ['noeol'], ['phone\n'])
242
# add eol following a noneol parent, no change content
243
f.add_lines('eolline', ['noeol'], ['line\n'])
244
# noeol with no parents:
245
f.add_lines('noeolbase', [], ['line'])
246
# noeol preceeding its leftmost parent in the output:
247
# this is done by making it a merge of two parents with no common
248
# anestry: noeolbase and noeol with the
249
# later-inserted parent the leftmost.
250
f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
251
# two identical eol texts
252
f.add_lines('noeoldup', ['noeol'], ['line'])
254
text_name = 'chain1-'
256
sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
257
1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
258
2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
259
3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
260
4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
261
5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
262
6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
263
7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
264
8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
265
9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
266
10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
267
11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
268
12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
269
13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
270
14:'2c4b1736566b8ca6051e668de68650686a3922f2',
271
15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
272
16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
273
17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
274
18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
275
19:'1ebed371807ba5935958ad0884595126e8c4e823',
276
20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
277
21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
278
22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
279
23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
280
24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
281
25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
283
for depth in range(26):
284
new_version = text_name + '%s' % depth
285
text = text + ['line\n']
286
f.add_lines(new_version, [next_parent], text)
287
next_parent = new_version
289
text_name = 'chain2-'
291
for depth in range(26):
292
new_version = text_name + '%s' % depth
293
text = text + ['line\n']
294
f.add_lines(new_version, [next_parent], text)
295
next_parent = new_version
298
def test_add_delta(self):
299
# tests for the add-delta facility.
300
# at this point, optimising for speed, we assume no checks when deltas are inserted.
301
# this may need to be revisited.
302
source = self.get_file('source')
303
source.add_lines('base', [], ['line\n'])
305
text_name = 'chain1-'
307
for depth in range(26):
308
new_version = text_name + '%s' % depth
309
text = text + ['line\n']
310
source.add_lines(new_version, [next_parent], text)
311
next_parent = new_version
313
text_name = 'chain2-'
315
for depth in range(26):
316
new_version = text_name + '%s' % depth
317
text = text + ['line\n']
318
source.add_lines(new_version, [next_parent], text)
319
next_parent = new_version
320
source.add_lines('noeol', ['base'], ['line'])
322
target = self.get_file('target')
323
for version in source.versions():
324
parent, sha1, noeol, delta = source.get_delta(version)
325
target.add_delta(version,
326
source.get_parents(version),
331
self.assertRaises(RevisionAlreadyPresent,
332
target.add_delta, 'base', [], None, '', False, [])
333
for version in source.versions():
334
self.assertEqual(source.get_lines(version),
335
target.get_lines(version))
337
def test_ancestry(self):
339
self.assertEqual([], f.get_ancestry([]))
340
f.add_lines('r0', [], ['a\n', 'b\n'])
341
f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
342
f.add_lines('r2', ['r0'], ['b\n', 'c\n'])
343
f.add_lines('r3', ['r2'], ['b\n', 'c\n'])
344
f.add_lines('rM', ['r1', 'r2'], ['b\n', 'c\n'])
345
self.assertEqual([], f.get_ancestry([]))
346
versions = f.get_ancestry(['rM'])
347
# there are some possibilities:
351
# so we check indexes
352
r0 = versions.index('r0')
353
r1 = versions.index('r1')
354
r2 = versions.index('r2')
355
self.assertFalse('r3' in versions)
356
rM = versions.index('rM')
357
self.assertTrue(r0 < r1)
358
self.assertTrue(r0 < r2)
359
self.assertTrue(r1 < rM)
360
self.assertTrue(r2 < rM)
362
self.assertRaises(RevisionNotPresent,
363
f.get_ancestry, ['rM', 'rX'])
365
def test_mutate_after_finish(self):
367
f.transaction_finished()
368
self.assertRaises(errors.OutSideTransaction, f.add_delta, '', [], '', '', False, [])
369
self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
370
self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
371
self.assertRaises(errors.OutSideTransaction, f.fix_parents, '', [])
372
self.assertRaises(errors.OutSideTransaction, f.join, '')
373
self.assertRaises(errors.OutSideTransaction, f.clone_text, 'base', 'bar', ['foo'])
375
def test_clear_cache(self):
377
# on a new file it should not error
379
# and after adding content, doing a clear_cache and a get should work.
380
f.add_lines('0', [], ['a'])
382
self.assertEqual(['a'], f.get_lines('0'))
384
def test_clone_text(self):
386
f.add_lines('r0', [], ['a\n', 'b\n'])
387
f.clone_text('r1', 'r0', ['r0'])
389
self.assertEquals(f.get_lines('r1'), f.get_lines('r0'))
390
self.assertEquals(f.get_lines('r1'), ['a\n', 'b\n'])
391
self.assertEquals(f.get_parents('r1'), ['r0'])
393
self.assertRaises(RevisionNotPresent,
394
f.clone_text, 'r2', 'rX', [])
395
self.assertRaises(RevisionAlreadyPresent,
396
f.clone_text, 'r1', 'r0', [])
398
verify_file(self.reopen_file())
400
def test_create_empty(self):
402
f.add_lines('0', [], ['a\n'])
403
new_f = f.create_empty('t', MemoryTransport())
404
# smoke test, specific types should check it is honoured correctly for
405
# non type attributes
406
self.assertEqual([], new_f.versions())
407
self.assertTrue(isinstance(new_f, f.__class__))
409
def test_copy_to(self):
411
f.add_lines('0', [], ['a\n'])
412
t = MemoryTransport()
414
for suffix in f.__class__.get_suffixes():
415
self.assertTrue(t.has('foo' + suffix))
417
def test_get_suffixes(self):
420
self.assertEqual(f.__class__.get_suffixes(), f.__class__.get_suffixes())
421
# and should be a list
422
self.assertTrue(isinstance(f.__class__.get_suffixes(), list))
424
def build_graph(self, file, graph):
425
for node in topo_sort(graph.items()):
426
file.add_lines(node, graph[node], [])
428
def test_get_graph(self):
434
self.build_graph(f, graph)
435
self.assertEqual(graph, f.get_graph())
437
def test_get_graph_partial(self):
445
complex_graph.update(simple_a)
450
complex_graph.update(simple_b)
457
complex_graph.update(simple_gam)
459
simple_b_gam.update(simple_gam)
460
simple_b_gam.update(simple_b)
461
self.build_graph(f, complex_graph)
462
self.assertEqual(simple_a, f.get_graph(['a']))
463
self.assertEqual(simple_b, f.get_graph(['b']))
464
self.assertEqual(simple_gam, f.get_graph(['gam']))
465
self.assertEqual(simple_b_gam, f.get_graph(['b', 'gam']))
467
def test_get_parents(self):
469
f.add_lines('r0', [], ['a\n', 'b\n'])
470
f.add_lines('r1', [], ['a\n', 'b\n'])
471
f.add_lines('r2', [], ['a\n', 'b\n'])
472
f.add_lines('r3', [], ['a\n', 'b\n'])
473
f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
474
self.assertEquals(f.get_parents('m'), ['r0', 'r1', 'r2', 'r3'])
476
self.assertRaises(RevisionNotPresent,
479
def test_annotate(self):
481
f.add_lines('r0', [], ['a\n', 'b\n'])
482
f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
483
origins = f.annotate('r1')
484
self.assertEquals(origins[0][0], 'r1')
485
self.assertEquals(origins[1][0], 'r0')
487
self.assertRaises(RevisionNotPresent,
491
# tests that walk returns all the inclusions for the requested
492
# revisions as well as the revisions changes themselves.
493
f = self.get_file('1')
494
f.add_lines('r0', [], ['a\n', 'b\n'])
495
f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
496
f.add_lines('rX', ['r1'], ['d\n', 'b\n'])
497
f.add_lines('rY', ['r1'], ['c\n', 'e\n'])
500
for lineno, insert, dset, text in f.walk(['rX', 'rY']):
501
lines[text] = (insert, dset)
503
self.assertTrue(lines['a\n'], ('r0', set(['r1'])))
504
self.assertTrue(lines['b\n'], ('r0', set(['rY'])))
505
self.assertTrue(lines['c\n'], ('r1', set(['rX'])))
506
self.assertTrue(lines['d\n'], ('rX', set([])))
507
self.assertTrue(lines['e\n'], ('rY', set([])))
509
def test_detection(self):
510
# Test weaves detect corruption.
512
# Weaves contain a checksum of their texts.
513
# When a text is extracted, this checksum should be
516
w = self.get_file_corrupted_text()
518
self.assertEqual('hello\n', w.get_text('v1'))
519
self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
520
self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
521
self.assertRaises(errors.WeaveInvalidChecksum, w.check)
523
w = self.get_file_corrupted_checksum()
525
self.assertEqual('hello\n', w.get_text('v1'))
526
self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
527
self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
528
self.assertRaises(errors.WeaveInvalidChecksum, w.check)
530
def get_file_corrupted_text(self):
531
"""Return a versioned file with corrupt text but valid metadata."""
532
raise NotImplementedError(self.get_file_corrupted_text)
534
def reopen_file(self, name='foo'):
535
"""Open the versioned file from disk again."""
536
raise NotImplementedError(self.reopen_file)
538
def test_iter_lines_added_or_present_in_versions(self):
539
# test that we get at least an equalset of the lines added by
540
# versions in the weave
541
# the ordering here is to make a tree so that dumb searches have
542
# more changes to muck up.
544
# add a base to get included
545
vf.add_lines('base', [], ['base\n'])
546
# add a ancestor to be included on one side
547
vf.add_lines('lancestor', [], ['lancestor\n'])
548
# add a ancestor to be included on the other side
549
vf.add_lines('rancestor', ['base'], ['rancestor\n'])
550
# add a child of rancestor with no eofile-nl
551
vf.add_lines('child', ['rancestor'], ['base\n', 'child\n'])
552
# add a child of lancestor and base to join the two roots
553
vf.add_lines('otherchild',
554
['lancestor', 'base'],
555
['base\n', 'lancestor\n', 'otherchild\n'])
556
def iter_with_versions(versions):
557
# now we need to see what lines are returned, and how often.
564
# iterate over the lines
565
for line in vf.iter_lines_added_or_present_in_versions(versions):
568
lines = iter_with_versions(['child', 'otherchild'])
569
# we must see child and otherchild
570
self.assertTrue(lines['child\n'] > 0)
571
self.assertTrue(lines['otherchild\n'] > 0)
572
# we dont care if we got more than that.
575
lines = iter_with_versions(None)
576
# all lines must be seen at least once
577
self.assertTrue(lines['base\n'] > 0)
578
self.assertTrue(lines['lancestor\n'] > 0)
579
self.assertTrue(lines['rancestor\n'] > 0)
580
self.assertTrue(lines['child\n'] > 0)
581
self.assertTrue(lines['otherchild\n'] > 0)
583
def test_fix_parents(self):
584
# some versioned files allow incorrect parents to be corrected after
585
# insertion - this may not fix ancestry..
586
# if they do not supported, they just do not implement it.
587
# we test this as an interface test to ensure that those that *do*
588
# implementent it get it right.
590
vf.add_lines('notbase', [], [])
591
vf.add_lines('base', [], [])
593
vf.fix_parents('notbase', ['base'])
594
except NotImplementedError:
596
self.assertEqual(['base'], vf.get_parents('notbase'))
597
# open again, check it stuck.
599
self.assertEqual(['base'], vf.get_parents('notbase'))
601
def test_fix_parents_with_ghosts(self):
602
# when fixing parents, ghosts that are listed should not be ghosts
607
vf.add_lines_with_ghosts('notbase', ['base', 'stillghost'], [])
608
except NotImplementedError:
610
vf.add_lines('base', [], [])
611
vf.fix_parents('notbase', ['base', 'stillghost'])
612
self.assertEqual(['base'], vf.get_parents('notbase'))
613
# open again, check it stuck.
615
self.assertEqual(['base'], vf.get_parents('notbase'))
616
# and check the ghosts
617
self.assertEqual(['base', 'stillghost'],
618
vf.get_parents_with_ghosts('notbase'))
620
def test_add_lines_with_ghosts(self):
621
# some versioned file formats allow lines to be added with parent
622
# information that is > than that in the format. Formats that do
623
# not support this need to raise NotImplementedError on the
624
# add_lines_with_ghosts api.
626
# add a revision with ghost parents
628
vf.add_lines_with_ghosts(u'notbxbfse', [u'b\xbfse'], [])
629
except NotImplementedError:
630
# check the other ghost apis are also not implemented
631
self.assertRaises(NotImplementedError, vf.has_ghost, 'foo')
632
self.assertRaises(NotImplementedError, vf.get_ancestry_with_ghosts, ['foo'])
633
self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
634
self.assertRaises(NotImplementedError, vf.get_graph_with_ghosts)
636
# test key graph related apis: getncestry, _graph, get_parents
638
# - these are ghost unaware and must not be reflect ghosts
639
self.assertEqual([u'notbxbfse'], vf.get_ancestry(u'notbxbfse'))
640
self.assertEqual([], vf.get_parents(u'notbxbfse'))
641
self.assertEqual({u'notbxbfse':[]}, vf.get_graph())
642
self.assertFalse(vf.has_version(u'b\xbfse'))
643
# we have _with_ghost apis to give us ghost information.
644
self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
645
self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
646
self.assertEqual({u'notbxbfse':[u'b\xbfse']}, vf.get_graph_with_ghosts())
647
self.assertTrue(vf.has_ghost(u'b\xbfse'))
648
# if we add something that is a ghost of another, it should correct the
649
# results of the prior apis
650
vf.add_lines(u'b\xbfse', [], [])
651
self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry([u'notbxbfse']))
652
self.assertEqual([u'b\xbfse'], vf.get_parents(u'notbxbfse'))
653
self.assertEqual({u'b\xbfse':[],
654
u'notbxbfse':[u'b\xbfse'],
657
self.assertTrue(vf.has_version(u'b\xbfse'))
658
# we have _with_ghost apis to give us ghost information.
659
self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
660
self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
661
self.assertEqual({u'b\xbfse':[],
662
u'notbxbfse':[u'b\xbfse'],
664
vf.get_graph_with_ghosts())
665
self.assertFalse(vf.has_ghost(u'b\xbfse'))
667
def test_add_lines_with_ghosts_after_normal_revs(self):
668
# some versioned file formats allow lines to be added with parent
669
# information that is > than that in the format. Formats that do
670
# not support this need to raise NotImplementedError on the
671
# add_lines_with_ghosts api.
673
# probe for ghost support
676
except NotImplementedError:
678
vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
679
vf.add_lines_with_ghosts('references_ghost',
681
['line\n', 'line_b\n', 'line_c\n'])
682
origins = vf.annotate('references_ghost')
683
self.assertEquals(('base', 'line\n'), origins[0])
684
self.assertEquals(('base', 'line_b\n'), origins[1])
685
self.assertEquals(('references_ghost', 'line_c\n'), origins[2])
687
def test_readonly_mode(self):
688
transport = get_transport(self.get_url('.'))
689
factory = self.get_factory()
690
vf = factory('id', transport, 0777, create=True, access_mode='w')
691
vf = factory('id', transport, access_mode='r')
692
self.assertRaises(errors.ReadOnlyError, vf.add_delta, '', [], '', '', False, [])
693
self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
694
self.assertRaises(errors.ReadOnlyError,
695
vf.add_lines_with_ghosts,
699
self.assertRaises(errors.ReadOnlyError, vf.fix_parents, 'base', [])
700
self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
701
self.assertRaises(errors.ReadOnlyError, vf.clone_text, 'base', 'bar', ['foo'])
703
def test_get_sha1(self):
704
# check the sha1 data is available
707
vf.add_lines('a', [], ['a\n'])
708
# the same file, different metadata
709
vf.add_lines('b', ['a'], ['a\n'])
710
# a file differing only in last newline.
711
vf.add_lines('c', [], ['a'])
713
'3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('a'))
715
'3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('b'))
717
'86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', vf.get_sha1('c'))
720
class TestWeave(TestCaseWithTransport, VersionedFileTestMixIn):
722
def get_file(self, name='foo'):
723
return WeaveFile(name, get_transport(self.get_url('.')), create=True)
725
def get_file_corrupted_text(self):
726
w = WeaveFile('foo', get_transport(self.get_url('.')), create=True)
727
w.add_lines('v1', [], ['hello\n'])
728
w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
730
# We are going to invasively corrupt the text
731
# Make sure the internals of weave are the same
732
self.assertEqual([('{', 0)
740
self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
741
, '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
746
w._weave[4] = 'There\n'
749
def get_file_corrupted_checksum(self):
750
w = self.get_file_corrupted_text()
752
w._weave[4] = 'there\n'
753
self.assertEqual('hello\nthere\n', w.get_text('v2'))
755
#Invalid checksum, first digit changed
756
w._sha1s[1] = 'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
759
def reopen_file(self, name='foo', create=False):
760
return WeaveFile(name, get_transport(self.get_url('.')), create=create)
762
def test_no_implicit_create(self):
763
self.assertRaises(errors.NoSuchFile,
766
get_transport(self.get_url('.')))
768
def get_factory(self):
772
class TestKnit(TestCaseWithTransport, VersionedFileTestMixIn):
774
def get_file(self, name='foo'):
775
return KnitVersionedFile(name, get_transport(self.get_url('.')),
776
delta=True, create=True)
778
def get_factory(self):
779
return KnitVersionedFile
781
def get_file_corrupted_text(self):
782
knit = self.get_file()
783
knit.add_lines('v1', [], ['hello\n'])
784
knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
787
def reopen_file(self, name='foo', create=False):
788
return KnitVersionedFile(name, get_transport(self.get_url('.')),
792
def test_detection(self):
793
print "TODO for merging: create a corrupted knit."
794
knit = self.get_file()
797
def test_no_implicit_create(self):
798
self.assertRaises(errors.NoSuchFile,
801
get_transport(self.get_url('.')))
804
class InterString(versionedfile.InterVersionedFile):
805
"""An inter-versionedfile optimised code path for strings.
807
This is for use during testing where we use strings as versionedfiles
808
so that none of the default regsitered interversionedfile classes will
809
match - which lets us test the match logic.
813
def is_compatible(source, target):
814
"""InterString is compatible with strings-as-versionedfiles."""
815
return isinstance(source, str) and isinstance(target, str)
818
# TODO this and the InterRepository core logic should be consolidatable
819
# if we make the registry a separate class though we still need to
820
# test the behaviour in the active registry to catch failure-to-handle-
822
class TestInterVersionedFile(TestCaseWithTransport):
824
def test_get_default_inter_versionedfile(self):
825
# test that the InterVersionedFile.get(a, b) probes
826
# for a class where is_compatible(a, b) returns
827
# true and returns a default interversionedfile otherwise.
828
# This also tests that the default registered optimised interversionedfile
829
# classes do not barf inappropriately when a surprising versionedfile type
831
dummy_a = "VersionedFile 1."
832
dummy_b = "VersionedFile 2."
833
self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
835
def assertGetsDefaultInterVersionedFile(self, a, b):
836
"""Asserts that InterVersionedFile.get(a, b) -> the default."""
837
inter = versionedfile.InterVersionedFile.get(a, b)
838
self.assertEqual(versionedfile.InterVersionedFile,
840
self.assertEqual(a, inter.source)
841
self.assertEqual(b, inter.target)
843
def test_register_inter_versionedfile_class(self):
844
# test that a optimised code path provider - a
845
# InterVersionedFile subclass can be registered and unregistered
846
# and that it is correctly selected when given a versionedfile
847
# pair that it returns true on for the is_compatible static method
849
dummy_a = "VersionedFile 1."
850
dummy_b = "VersionedFile 2."
851
versionedfile.InterVersionedFile.register_optimiser(InterString)
853
# we should get the default for something InterString returns False
855
self.assertFalse(InterString.is_compatible(dummy_a, None))
856
self.assertGetsDefaultInterVersionedFile(dummy_a, None)
857
# and we should get an InterString for a pair it 'likes'
858
self.assertTrue(InterString.is_compatible(dummy_a, dummy_b))
859
inter = versionedfile.InterVersionedFile.get(dummy_a, dummy_b)
860
self.assertEqual(InterString, inter.__class__)
861
self.assertEqual(dummy_a, inter.source)
862
self.assertEqual(dummy_b, inter.target)
864
versionedfile.InterVersionedFile.unregister_optimiser(InterString)
865
# now we should get the default InterVersionedFile object again.
866
self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
869
class TestReadonlyHttpMixin(object):
871
def test_readonly_http_works(self):
872
# we should be able to read from http with a versioned file.
874
# try an empty file access
875
readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
876
self.assertEqual([], readonly_vf.versions())
878
vf.add_lines('1', [], ['a\n'])
879
vf.add_lines('2', ['1'], ['b\n', 'a\n'])
880
readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
881
self.assertEqual(['1', '2'], vf.versions())
882
for version in readonly_vf.versions():
883
readonly_vf.get_lines(version)
886
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
889
return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
891
def get_factory(self):
895
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
898
return KnitVersionedFile('foo', get_transport(self.get_url('.')),
899
delta=True, create=True)
901
def get_factory(self):
902
return KnitVersionedFile
905
class MergeCasesMixin(object):
907
def doMerge(self, base, a, b, mp):
908
from cStringIO import StringIO
909
from textwrap import dedent
915
w.add_lines('text0', [], map(addcrlf, base))
916
w.add_lines('text1', ['text0'], map(addcrlf, a))
917
w.add_lines('text2', ['text0'], map(addcrlf, b))
921
self.log('merge plan:')
922
p = list(w.plan_merge('text1', 'text2'))
923
for state, line in p:
925
self.log('%12s | %s' % (state, line[:-1]))
929
mt.writelines(w.weave_merge(p))
931
self.log(mt.getvalue())
933
mp = map(addcrlf, mp)
934
self.assertEqual(mt.readlines(), mp)
937
def testOneInsert(self):
943
def testSeparateInserts(self):
944
self.doMerge(['aaa', 'bbb', 'ccc'],
945
['aaa', 'xxx', 'bbb', 'ccc'],
946
['aaa', 'bbb', 'yyy', 'ccc'],
947
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
949
def testSameInsert(self):
950
self.doMerge(['aaa', 'bbb', 'ccc'],
951
['aaa', 'xxx', 'bbb', 'ccc'],
952
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
953
['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
954
overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
955
def testOverlappedInsert(self):
956
self.doMerge(['aaa', 'bbb'],
957
['aaa', 'xxx', 'yyy', 'bbb'],
958
['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
960
# really it ought to reduce this to
961
# ['aaa', 'xxx', 'yyy', 'bbb']
964
def testClashReplace(self):
965
self.doMerge(['aaa'],
968
['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz',
971
def testNonClashInsert1(self):
972
self.doMerge(['aaa'],
975
['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz',
978
def testNonClashInsert2(self):
979
self.doMerge(['aaa'],
985
def testDeleteAndModify(self):
986
"""Clashing delete and modification.
988
If one side modifies a region and the other deletes it then
989
there should be a conflict with one side blank.
992
#######################################
993
# skippd, not working yet
996
self.doMerge(['aaa', 'bbb', 'ccc'],
997
['aaa', 'ddd', 'ccc'],
999
['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
1001
def _test_merge_from_strings(self, base, a, b, expected):
1003
w.add_lines('text0', [], base.splitlines(True))
1004
w.add_lines('text1', ['text0'], a.splitlines(True))
1005
w.add_lines('text2', ['text0'], b.splitlines(True))
1006
self.log('merge plan:')
1007
p = list(w.plan_merge('text1', 'text2'))
1008
for state, line in p:
1010
self.log('%12s | %s' % (state, line[:-1]))
1011
self.log('merge result:')
1012
result_text = ''.join(w.weave_merge(p))
1013
self.log(result_text)
1014
self.assertEqualDiff(result_text, expected)
1016
def test_weave_merge_conflicts(self):
1017
# does weave merge properly handle plans that end with unchanged?
1018
result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
1019
self.assertEqual(result, 'hello\n')
1021
def test_deletion_extended(self):
1022
"""One side deletes, the other deletes more.
1039
self._test_merge_from_strings(base, a, b, result)
1041
def test_deletion_overlap(self):
1042
"""Delete overlapping regions with no other conflict.
1044
Arguably it'd be better to treat these as agreement, rather than
1045
conflict, but for now conflict is safer.
1073
self._test_merge_from_strings(base, a, b, result)
1075
def test_agreement_deletion(self):
1076
"""Agree to delete some lines, without conflicts."""
1098
self._test_merge_from_strings(base, a, b, result)
1100
def test_sync_on_deletion(self):
1101
"""Specific case of merge where we can synchronize incorrectly.
1103
A previous version of the weave merge concluded that the two versions
1104
agreed on deleting line 2, and this could be a synchronization point.
1105
Line 1 was then considered in isolation, and thought to be deleted on
1108
It's better to consider the whole thing as a disagreement region.
1119
a's replacement line 2
1132
a's replacement line 2
1139
self._test_merge_from_strings(base, a, b, result)
1142
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
1144
def get_file(self, name='foo'):
1145
return KnitVersionedFile(name, get_transport(self.get_url('.')),
1146
delta=True, create=True)
1148
def log_contents(self, w):
1152
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
1154
def get_file(self, name='foo'):
1155
return WeaveFile(name, get_transport(self.get_url('.')), create=True)
1157
def log_contents(self, w):
1158
self.log('weave is:')
1160
write_weave(w, tmpf)
1161
self.log(tmpf.getvalue())
1163
overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======',
1164
'xxx', '>>>>>>> ', 'bbb']