/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: Aaron Bentley
  • Date: 2006-04-19 02:50:56 UTC
  • mto: This revision was merged to the branch mainline in revision 1672.
  • Revision ID: aaron.bentley@utoronto.ca-20060419025056-3847eb042f075760
Knit plan_merge uses slices instead of xenumerate

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 by Canonical Ltd
 
2
#
 
3
# Authors:
 
4
#   Johan Rydberg <jrydberg@gnu.org>
 
5
#
 
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.
 
10
 
 
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.
 
15
 
 
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
 
19
 
 
20
 
 
21
from StringIO import StringIO
 
22
 
 
23
import bzrlib
 
24
import bzrlib.errors as errors
 
25
from bzrlib.errors import (
 
26
                           RevisionNotPresent, 
 
27
                           RevisionAlreadyPresent,
 
28
                           WeaveParentMismatch
 
29
                           )
 
30
from bzrlib.knit import KnitVersionedFile, \
 
31
     KnitAnnotateFactory
 
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
import bzrlib.versionedfile as versionedfile
 
38
from bzrlib.weave import WeaveFile
 
39
from bzrlib.weavefile import read_weave, write_weave
 
40
 
 
41
 
 
42
class VersionedFileTestMixIn(object):
 
43
    """A mixin test class for testing VersionedFiles.
 
44
 
 
45
    This is not an adaptor-style test at this point because
 
46
    theres no dynamic substitution of versioned file implementations,
 
47
    they are strictly controlled by their owning repositories.
 
48
    """
 
49
 
 
50
    def test_add(self):
 
51
        f = self.get_file()
 
52
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
53
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
 
54
        def verify_file(f):
 
55
            versions = f.versions()
 
56
            self.assertTrue('r0' in versions)
 
57
            self.assertTrue('r1' in versions)
 
58
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
 
59
            self.assertEquals(f.get_text('r0'), 'a\nb\n')
 
60
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
 
61
            self.assertEqual(2, len(f))
 
62
            self.assertEqual(2, f.num_versions())
 
63
    
 
64
            self.assertRaises(RevisionNotPresent,
 
65
                f.add_lines, 'r2', ['foo'], [])
 
66
            self.assertRaises(RevisionAlreadyPresent,
 
67
                f.add_lines, 'r1', [], [])
 
68
        verify_file(f)
 
69
        f = self.reopen_file()
 
70
        verify_file(f)
 
71
 
 
72
    def test_adds_with_parent_texts(self):
 
73
        f = self.get_file()
 
74
        parent_texts = {}
 
75
        parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
 
76
        try:
 
77
            parent_texts['r1'] = f.add_lines_with_ghosts('r1',
 
78
                                                         ['r0', 'ghost'], 
 
79
                                                         ['b\n', 'c\n'],
 
80
                                                         parent_texts=parent_texts)
 
81
        except NotImplementedError:
 
82
            # if the format doesn't support ghosts, just add normally.
 
83
            parent_texts['r1'] = f.add_lines('r1',
 
84
                                             ['r0'], 
 
85
                                             ['b\n', 'c\n'],
 
86
                                             parent_texts=parent_texts)
 
87
        f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
 
88
        self.assertNotEqual(None, parent_texts['r0'])
 
89
        self.assertNotEqual(None, parent_texts['r1'])
 
90
        def verify_file(f):
 
91
            versions = f.versions()
 
92
            self.assertTrue('r0' in versions)
 
93
            self.assertTrue('r1' in versions)
 
94
            self.assertTrue('r2' in versions)
 
95
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
 
96
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
 
97
            self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
 
98
            self.assertEqual(3, f.num_versions())
 
99
            origins = f.annotate('r1')
 
100
            self.assertEquals(origins[0][0], 'r0')
 
101
            self.assertEquals(origins[1][0], 'r1')
 
102
            origins = f.annotate('r2')
 
103
            self.assertEquals(origins[0][0], 'r1')
 
104
            self.assertEquals(origins[1][0], 'r2')
 
105
 
 
106
        verify_file(f)
 
107
        f = self.reopen_file()
 
108
        verify_file(f)
 
109
 
 
110
    def test_get_delta(self):
 
111
        f = self.get_file()
 
112
        sha1s = self._setup_for_deltas(f)
 
113
        expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
 
114
                          [(0, 0, 1, [('base', 'line\n')])])
 
115
        self.assertEqual(expected_delta, f.get_delta('base'))
 
116
        next_parent = 'base'
 
117
        text_name = 'chain1-'
 
118
        for depth in range(26):
 
119
            new_version = text_name + '%s' % depth
 
120
            expected_delta = (next_parent, sha1s[depth], 
 
121
                              False,
 
122
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
123
            self.assertEqual(expected_delta, f.get_delta(new_version))
 
124
            next_parent = new_version
 
125
        next_parent = 'base'
 
126
        text_name = 'chain2-'
 
127
        for depth in range(26):
 
128
            new_version = text_name + '%s' % depth
 
129
            expected_delta = (next_parent, sha1s[depth], False,
 
130
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
131
            self.assertEqual(expected_delta, f.get_delta(new_version))
 
132
            next_parent = new_version
 
133
        # smoke test for eol support
 
134
        expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
 
135
        self.assertEqual(['line'], f.get_lines('noeol'))
 
136
        self.assertEqual(expected_delta, f.get_delta('noeol'))
 
137
 
 
138
    def test_get_deltas(self):
 
139
        f = self.get_file()
 
140
        sha1s = self._setup_for_deltas(f)
 
141
        deltas = f.get_deltas(f.versions())
 
142
        expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
 
143
                          [(0, 0, 1, [('base', 'line\n')])])
 
144
        self.assertEqual(expected_delta, deltas['base'])
 
145
        next_parent = 'base'
 
146
        text_name = 'chain1-'
 
147
        for depth in range(26):
 
148
            new_version = text_name + '%s' % depth
 
149
            expected_delta = (next_parent, sha1s[depth], 
 
150
                              False,
 
151
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
152
            self.assertEqual(expected_delta, deltas[new_version])
 
153
            next_parent = new_version
 
154
        next_parent = 'base'
 
155
        text_name = 'chain2-'
 
156
        for depth in range(26):
 
157
            new_version = text_name + '%s' % depth
 
158
            expected_delta = (next_parent, sha1s[depth], False,
 
159
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
160
            self.assertEqual(expected_delta, deltas[new_version])
 
161
            next_parent = new_version
 
162
        # smoke tests for eol support
 
163
        expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
 
164
        self.assertEqual(['line'], f.get_lines('noeol'))
 
165
        self.assertEqual(expected_delta, deltas['noeol'])
 
166
        # smoke tests for eol support - two noeol in a row same content
 
167
        expected_deltas = (('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
 
168
                          [(0, 1, 2, [(u'noeolsecond', 'line\n'), (u'noeolsecond', 'line\n')])]),
 
169
                          ('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
 
170
                           [(0, 0, 1, [('noeolsecond', 'line\n')]), (1, 1, 0, [])]))
 
171
        self.assertEqual(['line\n', 'line'], f.get_lines('noeolsecond'))
 
172
        self.assertTrue(deltas['noeolsecond'] in expected_deltas)
 
173
        # two no-eol in a row, different content
 
174
        expected_delta = ('noeolsecond', '8bb553a84e019ef1149db082d65f3133b195223b', True, 
 
175
                          [(1, 2, 1, [(u'noeolnotshared', 'phone\n')])])
 
176
        self.assertEqual(['line\n', 'phone'], f.get_lines('noeolnotshared'))
 
177
        self.assertEqual(expected_delta, deltas['noeolnotshared'])
 
178
        # eol folling a no-eol with content change
 
179
        expected_delta = ('noeol', 'a61f6fb6cfc4596e8d88c34a308d1e724caf8977', False, 
 
180
                          [(0, 1, 1, [(u'eol', 'phone\n')])])
 
181
        self.assertEqual(['phone\n'], f.get_lines('eol'))
 
182
        self.assertEqual(expected_delta, deltas['eol'])
 
183
        # eol folling a no-eol with content change
 
184
        expected_delta = ('noeol', '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
 
185
                          [(0, 1, 1, [(u'eolline', 'line\n')])])
 
186
        self.assertEqual(['line\n'], f.get_lines('eolline'))
 
187
        self.assertEqual(expected_delta, deltas['eolline'])
 
188
        # eol with no parents
 
189
        expected_delta = (None, '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, 
 
190
                          [(0, 0, 1, [(u'noeolbase', 'line\n')])])
 
191
        self.assertEqual(['line'], f.get_lines('noeolbase'))
 
192
        self.assertEqual(expected_delta, deltas['noeolbase'])
 
193
        # eol with two parents, in inverse insertion order
 
194
        expected_deltas = (('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
 
195
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]),
 
196
                           ('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
 
197
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]))
 
198
        self.assertEqual(['line'], f.get_lines('eolbeforefirstparent'))
 
199
        #self.assertTrue(deltas['eolbeforefirstparent'] in expected_deltas)
 
200
 
 
201
    def _setup_for_deltas(self, f):
 
202
        self.assertRaises(errors.RevisionNotPresent, f.get_delta, 'base')
 
203
        # add texts that should trip the knit maximum delta chain threshold
 
204
        # as well as doing parallel chains of data in knits.
 
205
        # this is done by two chains of 25 insertions
 
206
        f.add_lines('base', [], ['line\n'])
 
207
        f.add_lines('noeol', ['base'], ['line'])
 
208
        # detailed eol tests:
 
209
        # shared last line with parent no-eol
 
210
        f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
 
211
        # differing last line with parent, both no-eol
 
212
        f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
 
213
        # add eol following a noneol parent, change content
 
214
        f.add_lines('eol', ['noeol'], ['phone\n'])
 
215
        # add eol following a noneol parent, no change content
 
216
        f.add_lines('eolline', ['noeol'], ['line\n'])
 
217
        # noeol with no parents:
 
218
        f.add_lines('noeolbase', [], ['line'])
 
219
        # noeol preceeding its leftmost parent in the output:
 
220
        # this is done by making it a merge of two parents with no common
 
221
        # anestry: noeolbase and noeol with the 
 
222
        # later-inserted parent the leftmost.
 
223
        f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
 
224
        # two identical eol texts
 
225
        f.add_lines('noeoldup', ['noeol'], ['line'])
 
226
        next_parent = 'base'
 
227
        text_name = 'chain1-'
 
228
        text = ['line\n']
 
229
        sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
 
230
                 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
 
231
                 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
 
232
                 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
 
233
                 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
 
234
                 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
 
235
                 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
 
236
                 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
 
237
                 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
 
238
                 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
 
239
                 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
 
240
                 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
 
241
                 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
 
242
                 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
 
243
                 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
 
244
                 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
 
245
                 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
 
246
                 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
 
247
                 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
 
248
                 19:'1ebed371807ba5935958ad0884595126e8c4e823',
 
249
                 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
 
250
                 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
 
251
                 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
 
252
                 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
 
253
                 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
 
254
                 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
 
255
                 }
 
256
        for depth in range(26):
 
257
            new_version = text_name + '%s' % depth
 
258
            text = text + ['line\n']
 
259
            f.add_lines(new_version, [next_parent], text)
 
260
            next_parent = new_version
 
261
        next_parent = 'base'
 
262
        text_name = 'chain2-'
 
263
        text = ['line\n']
 
264
        for depth in range(26):
 
265
            new_version = text_name + '%s' % depth
 
266
            text = text + ['line\n']
 
267
            f.add_lines(new_version, [next_parent], text)
 
268
            next_parent = new_version
 
269
        return sha1s
 
270
 
 
271
    def test_add_delta(self):
 
272
        # tests for the add-delta facility.
 
273
        # at this point, optimising for speed, we assume no checks when deltas are inserted.
 
274
        # this may need to be revisited.
 
275
        source = self.get_file('source')
 
276
        source.add_lines('base', [], ['line\n'])
 
277
        next_parent = 'base'
 
278
        text_name = 'chain1-'
 
279
        text = ['line\n']
 
280
        for depth in range(26):
 
281
            new_version = text_name + '%s' % depth
 
282
            text = text + ['line\n']
 
283
            source.add_lines(new_version, [next_parent], text)
 
284
            next_parent = new_version
 
285
        next_parent = 'base'
 
286
        text_name = 'chain2-'
 
287
        text = ['line\n']
 
288
        for depth in range(26):
 
289
            new_version = text_name + '%s' % depth
 
290
            text = text + ['line\n']
 
291
            source.add_lines(new_version, [next_parent], text)
 
292
            next_parent = new_version
 
293
        source.add_lines('noeol', ['base'], ['line'])
 
294
        
 
295
        target = self.get_file('target')
 
296
        for version in source.versions():
 
297
            parent, sha1, noeol, delta = source.get_delta(version)
 
298
            target.add_delta(version,
 
299
                             source.get_parents(version),
 
300
                             parent,
 
301
                             sha1,
 
302
                             noeol,
 
303
                             delta)
 
304
        self.assertRaises(RevisionAlreadyPresent,
 
305
                          target.add_delta, 'base', [], None, '', False, [])
 
306
        for version in source.versions():
 
307
            self.assertEqual(source.get_lines(version),
 
308
                             target.get_lines(version))
 
309
 
 
310
    def test_ancestry(self):
 
311
        f = self.get_file()
 
312
        self.assertEqual([], f.get_ancestry([]))
 
313
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
314
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
 
315
        f.add_lines('r2', ['r0'], ['b\n', 'c\n'])
 
316
        f.add_lines('r3', ['r2'], ['b\n', 'c\n'])
 
317
        f.add_lines('rM', ['r1', 'r2'], ['b\n', 'c\n'])
 
318
        self.assertEqual([], f.get_ancestry([]))
 
319
        versions = f.get_ancestry(['rM'])
 
320
        # there are some possibilities:
 
321
        # r0 r1 r2 rM r3
 
322
        # r0 r1 r2 r3 rM
 
323
        # etc
 
324
        # so we check indexes
 
325
        r0 = versions.index('r0')
 
326
        r1 = versions.index('r1')
 
327
        r2 = versions.index('r2')
 
328
        self.assertFalse('r3' in versions)
 
329
        rM = versions.index('rM')
 
330
        self.assertTrue(r0 < r1)
 
331
        self.assertTrue(r0 < r2)
 
332
        self.assertTrue(r1 < rM)
 
333
        self.assertTrue(r2 < rM)
 
334
 
 
335
        self.assertRaises(RevisionNotPresent,
 
336
            f.get_ancestry, ['rM', 'rX'])
 
337
 
 
338
    def test_mutate_after_finish(self):
 
339
        f = self.get_file()
 
340
        f.transaction_finished()
 
341
        self.assertRaises(errors.OutSideTransaction, f.add_delta, '', [], '', '', False, [])
 
342
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
 
343
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
 
344
        self.assertRaises(errors.OutSideTransaction, f.fix_parents, '', [])
 
345
        self.assertRaises(errors.OutSideTransaction, f.join, '')
 
346
        self.assertRaises(errors.OutSideTransaction, f.clone_text, 'base', 'bar', ['foo'])
 
347
        
 
348
    def test_clear_cache(self):
 
349
        f = self.get_file()
 
350
        # on a new file it should not error
 
351
        f.clear_cache()
 
352
        # and after adding content, doing a clear_cache and a get should work.
 
353
        f.add_lines('0', [], ['a'])
 
354
        f.clear_cache()
 
355
        self.assertEqual(['a'], f.get_lines('0'))
 
356
 
 
357
    def test_clone_text(self):
 
358
        f = self.get_file()
 
359
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
360
        f.clone_text('r1', 'r0', ['r0'])
 
361
        def verify_file(f):
 
362
            self.assertEquals(f.get_lines('r1'), f.get_lines('r0'))
 
363
            self.assertEquals(f.get_lines('r1'), ['a\n', 'b\n'])
 
364
            self.assertEquals(f.get_parents('r1'), ['r0'])
 
365
    
 
366
            self.assertRaises(RevisionNotPresent,
 
367
                f.clone_text, 'r2', 'rX', [])
 
368
            self.assertRaises(RevisionAlreadyPresent,
 
369
                f.clone_text, 'r1', 'r0', [])
 
370
        verify_file(f)
 
371
        verify_file(self.reopen_file())
 
372
 
 
373
    def test_create_empty(self):
 
374
        f = self.get_file()
 
375
        f.add_lines('0', [], ['a\n'])
 
376
        new_f = f.create_empty('t', MemoryTransport())
 
377
        # smoke test, specific types should check it is honoured correctly for
 
378
        # non type attributes
 
379
        self.assertEqual([], new_f.versions())
 
380
        self.assertTrue(isinstance(new_f, f.__class__))
 
381
 
 
382
    def test_copy_to(self):
 
383
        f = self.get_file()
 
384
        f.add_lines('0', [], ['a\n'])
 
385
        t = MemoryTransport()
 
386
        f.copy_to('foo', t)
 
387
        for suffix in f.__class__.get_suffixes():
 
388
            self.assertTrue(t.has('foo' + suffix))
 
389
 
 
390
    def test_get_suffixes(self):
 
391
        f = self.get_file()
 
392
        # should be the same
 
393
        self.assertEqual(f.__class__.get_suffixes(), f.__class__.get_suffixes())
 
394
        # and should be a list
 
395
        self.assertTrue(isinstance(f.__class__.get_suffixes(), list))
 
396
 
 
397
    def test_get_graph(self):
 
398
        f = self.get_file()
 
399
        f.add_lines('v1', [], ['hello\n'])
 
400
        f.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
 
401
        f.add_lines('v3', ['v2'], ['hello\n', 'cruel\n', 'world\n'])
 
402
        self.assertEqual({'v1': [],
 
403
                          'v2': ['v1'],
 
404
                          'v3': ['v2']},
 
405
                         f.get_graph())
 
406
 
 
407
    def test_get_parents(self):
 
408
        f = self.get_file()
 
409
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
410
        f.add_lines('r1', [], ['a\n', 'b\n'])
 
411
        f.add_lines('r2', [], ['a\n', 'b\n'])
 
412
        f.add_lines('r3', [], ['a\n', 'b\n'])
 
413
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
 
414
        self.assertEquals(f.get_parents('m'), ['r0', 'r1', 'r2', 'r3'])
 
415
 
 
416
        self.assertRaises(RevisionNotPresent,
 
417
            f.get_parents, 'y')
 
418
 
 
419
    def test_annotate(self):
 
420
        f = self.get_file()
 
421
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
422
        f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
 
423
        origins = f.annotate('r1')
 
424
        self.assertEquals(origins[0][0], 'r1')
 
425
        self.assertEquals(origins[1][0], 'r0')
 
426
 
 
427
        self.assertRaises(RevisionNotPresent,
 
428
            f.annotate, 'foo')
 
429
 
 
430
    def test_walk(self):
 
431
        # tests that walk returns all the inclusions for the requested
 
432
        # revisions as well as the revisions changes themselves.
 
433
        f = self.get_file('1')
 
434
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
435
        f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
 
436
        f.add_lines('rX', ['r1'], ['d\n', 'b\n'])
 
437
        f.add_lines('rY', ['r1'], ['c\n', 'e\n'])
 
438
 
 
439
        lines = {}
 
440
        for lineno, insert, dset, text in f.walk(['rX', 'rY']):
 
441
            lines[text] = (insert, dset)
 
442
 
 
443
        self.assertTrue(lines['a\n'], ('r0', set(['r1'])))
 
444
        self.assertTrue(lines['b\n'], ('r0', set(['rY'])))
 
445
        self.assertTrue(lines['c\n'], ('r1', set(['rX'])))
 
446
        self.assertTrue(lines['d\n'], ('rX', set([])))
 
447
        self.assertTrue(lines['e\n'], ('rY', set([])))
 
448
 
 
449
    def test_detection(self):
 
450
        # Test weaves detect corruption.
 
451
        #
 
452
        # Weaves contain a checksum of their texts.
 
453
        # When a text is extracted, this checksum should be
 
454
        # verified.
 
455
 
 
456
        w = self.get_file_corrupted_text()
 
457
 
 
458
        self.assertEqual('hello\n', w.get_text('v1'))
 
459
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
460
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
461
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
462
 
 
463
        w = self.get_file_corrupted_checksum()
 
464
 
 
465
        self.assertEqual('hello\n', w.get_text('v1'))
 
466
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
467
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
468
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
469
 
 
470
    def get_file_corrupted_text(self):
 
471
        """Return a versioned file with corrupt text but valid metadata."""
 
472
        raise NotImplementedError(self.get_file_corrupted_text)
 
473
 
 
474
    def reopen_file(self, name='foo'):
 
475
        """Open the versioned file from disk again."""
 
476
        raise NotImplementedError(self.reopen_file)
 
477
 
 
478
    def test_iter_lines_added_or_present_in_versions(self):
 
479
        # test that we get at least an equalset of the lines added by
 
480
        # versions in the weave 
 
481
        # the ordering here is to make a tree so that dumb searches have
 
482
        # more changes to muck up.
 
483
        vf = self.get_file()
 
484
        # add a base to get included
 
485
        vf.add_lines('base', [], ['base\n'])
 
486
        # add a ancestor to be included on one side
 
487
        vf.add_lines('lancestor', [], ['lancestor\n'])
 
488
        # add a ancestor to be included on the other side
 
489
        vf.add_lines('rancestor', ['base'], ['rancestor\n'])
 
490
        # add a child of rancestor with no eofile-nl
 
491
        vf.add_lines('child', ['rancestor'], ['base\n', 'child\n'])
 
492
        # add a child of lancestor and base to join the two roots
 
493
        vf.add_lines('otherchild',
 
494
                     ['lancestor', 'base'],
 
495
                     ['base\n', 'lancestor\n', 'otherchild\n'])
 
496
        def iter_with_versions(versions):
 
497
            # now we need to see what lines are returned, and how often.
 
498
            lines = {'base\n':0,
 
499
                     'lancestor\n':0,
 
500
                     'rancestor\n':0,
 
501
                     'child\n':0,
 
502
                     'otherchild\n':0,
 
503
                     }
 
504
            # iterate over the lines
 
505
            for line in vf.iter_lines_added_or_present_in_versions(versions):
 
506
                lines[line] += 1
 
507
            return lines
 
508
        lines = iter_with_versions(['child', 'otherchild'])
 
509
        # we must see child and otherchild
 
510
        self.assertTrue(lines['child\n'] > 0)
 
511
        self.assertTrue(lines['otherchild\n'] > 0)
 
512
        # we dont care if we got more than that.
 
513
        
 
514
        # test all lines
 
515
        lines = iter_with_versions(None)
 
516
        # all lines must be seen at least once
 
517
        self.assertTrue(lines['base\n'] > 0)
 
518
        self.assertTrue(lines['lancestor\n'] > 0)
 
519
        self.assertTrue(lines['rancestor\n'] > 0)
 
520
        self.assertTrue(lines['child\n'] > 0)
 
521
        self.assertTrue(lines['otherchild\n'] > 0)
 
522
 
 
523
    def test_fix_parents(self):
 
524
        # some versioned files allow incorrect parents to be corrected after
 
525
        # insertion - this may not fix ancestry..
 
526
        # if they do not supported, they just do not implement it.
 
527
        # we test this as an interface test to ensure that those that *do*
 
528
        # implementent it get it right.
 
529
        vf = self.get_file()
 
530
        vf.add_lines('notbase', [], [])
 
531
        vf.add_lines('base', [], [])
 
532
        try:
 
533
            vf.fix_parents('notbase', ['base'])
 
534
        except NotImplementedError:
 
535
            return
 
536
        self.assertEqual(['base'], vf.get_parents('notbase'))
 
537
        # open again, check it stuck.
 
538
        vf = self.get_file()
 
539
        self.assertEqual(['base'], vf.get_parents('notbase'))
 
540
 
 
541
    def test_fix_parents_with_ghosts(self):
 
542
        # when fixing parents, ghosts that are listed should not be ghosts
 
543
        # anymore.
 
544
        vf = self.get_file()
 
545
 
 
546
        try:
 
547
            vf.add_lines_with_ghosts('notbase', ['base', 'stillghost'], [])
 
548
        except NotImplementedError:
 
549
            return
 
550
        vf.add_lines('base', [], [])
 
551
        vf.fix_parents('notbase', ['base', 'stillghost'])
 
552
        self.assertEqual(['base'], vf.get_parents('notbase'))
 
553
        # open again, check it stuck.
 
554
        vf = self.get_file()
 
555
        self.assertEqual(['base'], vf.get_parents('notbase'))
 
556
        # and check the ghosts
 
557
        self.assertEqual(['base', 'stillghost'],
 
558
                         vf.get_parents_with_ghosts('notbase'))
 
559
 
 
560
    def test_add_lines_with_ghosts(self):
 
561
        # some versioned file formats allow lines to be added with parent
 
562
        # information that is > than that in the format. Formats that do
 
563
        # not support this need to raise NotImplementedError on the
 
564
        # add_lines_with_ghosts api.
 
565
        vf = self.get_file()
 
566
        # add a revision with ghost parents
 
567
        try:
 
568
            vf.add_lines_with_ghosts(u'notbxbfse', [u'b\xbfse'], [])
 
569
        except NotImplementedError:
 
570
            # check the other ghost apis are also not implemented
 
571
            self.assertRaises(NotImplementedError, vf.has_ghost, 'foo')
 
572
            self.assertRaises(NotImplementedError, vf.get_ancestry_with_ghosts, ['foo'])
 
573
            self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
 
574
            self.assertRaises(NotImplementedError, vf.get_graph_with_ghosts)
 
575
            return
 
576
        # test key graph related apis: getncestry, _graph, get_parents
 
577
        # has_version
 
578
        # - these are ghost unaware and must not be reflect ghosts
 
579
        self.assertEqual([u'notbxbfse'], vf.get_ancestry(u'notbxbfse'))
 
580
        self.assertEqual([], vf.get_parents(u'notbxbfse'))
 
581
        self.assertEqual({u'notbxbfse':[]}, vf.get_graph())
 
582
        self.assertFalse(vf.has_version(u'b\xbfse'))
 
583
        # we have _with_ghost apis to give us ghost information.
 
584
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
 
585
        self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
 
586
        self.assertEqual({u'notbxbfse':[u'b\xbfse']}, vf.get_graph_with_ghosts())
 
587
        self.assertTrue(vf.has_ghost(u'b\xbfse'))
 
588
        # if we add something that is a ghost of another, it should correct the
 
589
        # results of the prior apis
 
590
        vf.add_lines(u'b\xbfse', [], [])
 
591
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry([u'notbxbfse']))
 
592
        self.assertEqual([u'b\xbfse'], vf.get_parents(u'notbxbfse'))
 
593
        self.assertEqual({u'b\xbfse':[],
 
594
                          u'notbxbfse':[u'b\xbfse'],
 
595
                          },
 
596
                         vf.get_graph())
 
597
        self.assertTrue(vf.has_version(u'b\xbfse'))
 
598
        # we have _with_ghost apis to give us ghost information.
 
599
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
 
600
        self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
 
601
        self.assertEqual({u'b\xbfse':[],
 
602
                          u'notbxbfse':[u'b\xbfse'],
 
603
                          },
 
604
                         vf.get_graph_with_ghosts())
 
605
        self.assertFalse(vf.has_ghost(u'b\xbfse'))
 
606
 
 
607
    def test_add_lines_with_ghosts_after_normal_revs(self):
 
608
        # some versioned file formats allow lines to be added with parent
 
609
        # information that is > than that in the format. Formats that do
 
610
        # not support this need to raise NotImplementedError on the
 
611
        # add_lines_with_ghosts api.
 
612
        vf = self.get_file()
 
613
        # probe for ghost support
 
614
        try:
 
615
            vf.has_ghost('hoo')
 
616
        except NotImplementedError:
 
617
            return
 
618
        vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
 
619
        vf.add_lines_with_ghosts('references_ghost',
 
620
                                 ['base', 'a_ghost'],
 
621
                                 ['line\n', 'line_b\n', 'line_c\n'])
 
622
        origins = vf.annotate('references_ghost')
 
623
        self.assertEquals(('base', 'line\n'), origins[0])
 
624
        self.assertEquals(('base', 'line_b\n'), origins[1])
 
625
        self.assertEquals(('references_ghost', 'line_c\n'), origins[2])
 
626
 
 
627
    def test_readonly_mode(self):
 
628
        transport = get_transport(self.get_url('.'))
 
629
        factory = self.get_factory()
 
630
        vf = factory('id', transport, 0777, create=True, access_mode='w')
 
631
        vf = factory('id', transport, access_mode='r')
 
632
        self.assertRaises(errors.ReadOnlyError, vf.add_delta, '', [], '', '', False, [])
 
633
        self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
 
634
        self.assertRaises(errors.ReadOnlyError,
 
635
                          vf.add_lines_with_ghosts,
 
636
                          'base',
 
637
                          [],
 
638
                          [])
 
639
        self.assertRaises(errors.ReadOnlyError, vf.fix_parents, 'base', [])
 
640
        self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
 
641
        self.assertRaises(errors.ReadOnlyError, vf.clone_text, 'base', 'bar', ['foo'])
 
642
        
 
643
 
 
644
class TestWeave(TestCaseWithTransport, VersionedFileTestMixIn):
 
645
 
 
646
    def get_file(self, name='foo'):
 
647
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
648
 
 
649
    def get_file_corrupted_text(self):
 
650
        w = WeaveFile('foo', get_transport(self.get_url('.')), create=True)
 
651
        w.add_lines('v1', [], ['hello\n'])
 
652
        w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
653
        
 
654
        # We are going to invasively corrupt the text
 
655
        # Make sure the internals of weave are the same
 
656
        self.assertEqual([('{', 0)
 
657
                        , 'hello\n'
 
658
                        , ('}', None)
 
659
                        , ('{', 1)
 
660
                        , 'there\n'
 
661
                        , ('}', None)
 
662
                        ], w._weave)
 
663
        
 
664
        self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
 
665
                        , '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
666
                        ], w._sha1s)
 
667
        w.check()
 
668
        
 
669
        # Corrupted
 
670
        w._weave[4] = 'There\n'
 
671
        return w
 
672
 
 
673
    def get_file_corrupted_checksum(self):
 
674
        w = self.get_file_corrupted_text()
 
675
        # Corrected
 
676
        w._weave[4] = 'there\n'
 
677
        self.assertEqual('hello\nthere\n', w.get_text('v2'))
 
678
        
 
679
        #Invalid checksum, first digit changed
 
680
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
681
        return w
 
682
 
 
683
    def reopen_file(self, name='foo'):
 
684
        return WeaveFile(name, get_transport(self.get_url('.')))
 
685
 
 
686
    def test_no_implicit_create(self):
 
687
        self.assertRaises(errors.NoSuchFile,
 
688
                          WeaveFile,
 
689
                          'foo',
 
690
                          get_transport(self.get_url('.')))
 
691
 
 
692
    def get_factory(self):
 
693
        return WeaveFile
 
694
 
 
695
 
 
696
class TestKnit(TestCaseWithTransport, VersionedFileTestMixIn):
 
697
 
 
698
    def get_file(self, name='foo'):
 
699
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
700
                                 delta=True, create=True)
 
701
 
 
702
    def get_factory(self):
 
703
        return KnitVersionedFile
 
704
 
 
705
    def get_file_corrupted_text(self):
 
706
        knit = self.get_file()
 
707
        knit.add_lines('v1', [], ['hello\n'])
 
708
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
709
        return knit
 
710
 
 
711
    def reopen_file(self, name='foo'):
 
712
        return KnitVersionedFile(name, get_transport(self.get_url('.')), delta=True)
 
713
 
 
714
    def test_detection(self):
 
715
        print "TODO for merging: create a corrupted knit."
 
716
        knit = self.get_file()
 
717
        knit.check()
 
718
 
 
719
    def test_no_implicit_create(self):
 
720
        self.assertRaises(errors.NoSuchFile,
 
721
                          KnitVersionedFile,
 
722
                          'foo',
 
723
                          get_transport(self.get_url('.')))
 
724
 
 
725
 
 
726
class InterString(versionedfile.InterVersionedFile):
 
727
    """An inter-versionedfile optimised code path for strings.
 
728
 
 
729
    This is for use during testing where we use strings as versionedfiles
 
730
    so that none of the default regsitered interversionedfile classes will
 
731
    match - which lets us test the match logic.
 
732
    """
 
733
 
 
734
    @staticmethod
 
735
    def is_compatible(source, target):
 
736
        """InterString is compatible with strings-as-versionedfiles."""
 
737
        return isinstance(source, str) and isinstance(target, str)
 
738
 
 
739
 
 
740
# TODO this and the InterRepository core logic should be consolidatable
 
741
# if we make the registry a separate class though we still need to 
 
742
# test the behaviour in the active registry to catch failure-to-handle-
 
743
# stange-objects
 
744
class TestInterVersionedFile(TestCaseWithTransport):
 
745
 
 
746
    def test_get_default_inter_versionedfile(self):
 
747
        # test that the InterVersionedFile.get(a, b) probes
 
748
        # for a class where is_compatible(a, b) returns
 
749
        # true and returns a default interversionedfile otherwise.
 
750
        # This also tests that the default registered optimised interversionedfile
 
751
        # classes do not barf inappropriately when a surprising versionedfile type
 
752
        # is handed to them.
 
753
        dummy_a = "VersionedFile 1."
 
754
        dummy_b = "VersionedFile 2."
 
755
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
756
 
 
757
    def assertGetsDefaultInterVersionedFile(self, a, b):
 
758
        """Asserts that InterVersionedFile.get(a, b) -> the default."""
 
759
        inter = versionedfile.InterVersionedFile.get(a, b)
 
760
        self.assertEqual(versionedfile.InterVersionedFile,
 
761
                         inter.__class__)
 
762
        self.assertEqual(a, inter.source)
 
763
        self.assertEqual(b, inter.target)
 
764
 
 
765
    def test_register_inter_versionedfile_class(self):
 
766
        # test that a optimised code path provider - a
 
767
        # InterVersionedFile subclass can be registered and unregistered
 
768
        # and that it is correctly selected when given a versionedfile
 
769
        # pair that it returns true on for the is_compatible static method
 
770
        # check
 
771
        dummy_a = "VersionedFile 1."
 
772
        dummy_b = "VersionedFile 2."
 
773
        versionedfile.InterVersionedFile.register_optimiser(InterString)
 
774
        try:
 
775
            # we should get the default for something InterString returns False
 
776
            # to
 
777
            self.assertFalse(InterString.is_compatible(dummy_a, None))
 
778
            self.assertGetsDefaultInterVersionedFile(dummy_a, None)
 
779
            # and we should get an InterString for a pair it 'likes'
 
780
            self.assertTrue(InterString.is_compatible(dummy_a, dummy_b))
 
781
            inter = versionedfile.InterVersionedFile.get(dummy_a, dummy_b)
 
782
            self.assertEqual(InterString, inter.__class__)
 
783
            self.assertEqual(dummy_a, inter.source)
 
784
            self.assertEqual(dummy_b, inter.target)
 
785
        finally:
 
786
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
 
787
        # now we should get the default InterVersionedFile object again.
 
788
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
789
 
 
790
 
 
791
class TestReadonlyHttpMixin(object):
 
792
 
 
793
    def test_readonly_http_works(self):
 
794
        # we should be able to read from http with a versioned file.
 
795
        vf = self.get_file()
 
796
        vf.add_lines('1', [], ['a\n'])
 
797
        vf.add_lines('2', ['1'], ['b\n', 'a\n'])
 
798
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
799
        for version in readonly_vf.versions():
 
800
            readonly_vf.get_lines(version)
 
801
 
 
802
 
 
803
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
804
 
 
805
    def get_file(self):
 
806
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
 
807
 
 
808
    def get_factory(self):
 
809
        return WeaveFile
 
810
 
 
811
 
 
812
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
813
 
 
814
    def get_file(self):
 
815
        return KnitVersionedFile('foo', get_transport(self.get_url('.')),
 
816
                                 delta=True, create=True)
 
817
 
 
818
    def get_factory(self):
 
819
        return KnitVersionedFile
 
820
 
 
821
 
 
822
class MergeCasesMixin(object):
 
823
 
 
824
    def doMerge(self, base, a, b, mp):
 
825
        from cStringIO import StringIO
 
826
        from textwrap import dedent
 
827
 
 
828
        def addcrlf(x):
 
829
            return x + '\n'
 
830
        
 
831
        w = self.get_file()
 
832
        w.add_lines('text0', [], map(addcrlf, base))
 
833
        w.add_lines('text1', ['text0'], map(addcrlf, a))
 
834
        w.add_lines('text2', ['text0'], map(addcrlf, b))
 
835
 
 
836
        self.log_contents(w)
 
837
 
 
838
        self.log('merge plan:')
 
839
        p = list(w.plan_merge('text1', 'text2'))
 
840
        for state, line in p:
 
841
            if line:
 
842
                self.log('%12s | %s' % (state, line[:-1]))
 
843
 
 
844
        self.log('merge:')
 
845
        mt = StringIO()
 
846
        mt.writelines(w.weave_merge(p))
 
847
        mt.seek(0)
 
848
        self.log(mt.getvalue())
 
849
 
 
850
        mp = map(addcrlf, mp)
 
851
        self.assertEqual(mt.readlines(), mp)
 
852
        
 
853
        
 
854
    def testOneInsert(self):
 
855
        self.doMerge([],
 
856
                     ['aa'],
 
857
                     [],
 
858
                     ['aa'])
 
859
 
 
860
    def testSeparateInserts(self):
 
861
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
862
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
863
                     ['aaa', 'bbb', 'yyy', 'ccc'],
 
864
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
865
 
 
866
    def testSameInsert(self):
 
867
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
868
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
869
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
 
870
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
871
    overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
 
872
    def testOverlappedInsert(self):
 
873
        self.doMerge(['aaa', 'bbb'],
 
874
                     ['aaa', 'xxx', 'yyy', 'bbb'],
 
875
                     ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
 
876
 
 
877
        # really it ought to reduce this to 
 
878
        # ['aaa', 'xxx', 'yyy', 'bbb']
 
879
 
 
880
 
 
881
    def testClashReplace(self):
 
882
        self.doMerge(['aaa'],
 
883
                     ['xxx'],
 
884
                     ['yyy', 'zzz'],
 
885
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
 
886
                      '>>>>>>> '])
 
887
 
 
888
    def testNonClashInsert1(self):
 
889
        self.doMerge(['aaa'],
 
890
                     ['xxx', 'aaa'],
 
891
                     ['yyy', 'zzz'],
 
892
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
 
893
                      '>>>>>>> '])
 
894
 
 
895
    def testNonClashInsert2(self):
 
896
        self.doMerge(['aaa'],
 
897
                     ['aaa'],
 
898
                     ['yyy', 'zzz'],
 
899
                     ['yyy', 'zzz'])
 
900
 
 
901
 
 
902
    def testDeleteAndModify(self):
 
903
        """Clashing delete and modification.
 
904
 
 
905
        If one side modifies a region and the other deletes it then
 
906
        there should be a conflict with one side blank.
 
907
        """
 
908
 
 
909
        #######################################
 
910
        # skippd, not working yet
 
911
        return
 
912
        
 
913
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
914
                     ['aaa', 'ddd', 'ccc'],
 
915
                     ['aaa', 'ccc'],
 
916
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
 
917
 
 
918
    def _test_merge_from_strings(self, base, a, b, expected):
 
919
        w = self.get_file()
 
920
        w.add_lines('text0', [], base.splitlines(True))
 
921
        w.add_lines('text1', ['text0'], a.splitlines(True))
 
922
        w.add_lines('text2', ['text0'], b.splitlines(True))
 
923
        self.log('merge plan:')
 
924
        p = list(w.plan_merge('text1', 'text2'))
 
925
        for state, line in p:
 
926
            if line:
 
927
                self.log('%12s | %s' % (state, line[:-1]))
 
928
        self.log('merge result:')
 
929
        result_text = ''.join(w.weave_merge(p))
 
930
        self.log(result_text)
 
931
        self.assertEqualDiff(result_text, expected)
 
932
 
 
933
    def test_weave_merge_conflicts(self):
 
934
        # does weave merge properly handle plans that end with unchanged?
 
935
        result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
 
936
        self.assertEqual(result, 'hello\n')
 
937
 
 
938
    def test_deletion_extended(self):
 
939
        """One side deletes, the other deletes more.
 
940
        """
 
941
        base = """\
 
942
            line 1
 
943
            line 2
 
944
            line 3
 
945
            """
 
946
        a = """\
 
947
            line 1
 
948
            line 2
 
949
            """
 
950
        b = """\
 
951
            line 1
 
952
            """
 
953
        result = """\
 
954
            line 1
 
955
            """
 
956
        self._test_merge_from_strings(base, a, b, result)
 
957
 
 
958
    def test_deletion_overlap(self):
 
959
        """Delete overlapping regions with no other conflict.
 
960
 
 
961
        Arguably it'd be better to treat these as agreement, rather than 
 
962
        conflict, but for now conflict is safer.
 
963
        """
 
964
        base = """\
 
965
            start context
 
966
            int a() {}
 
967
            int b() {}
 
968
            int c() {}
 
969
            end context
 
970
            """
 
971
        a = """\
 
972
            start context
 
973
            int a() {}
 
974
            end context
 
975
            """
 
976
        b = """\
 
977
            start context
 
978
            int c() {}
 
979
            end context
 
980
            """
 
981
        result = """\
 
982
            start context
 
983
<<<<<<< 
 
984
            int a() {}
 
985
=======
 
986
            int c() {}
 
987
>>>>>>> 
 
988
            end context
 
989
            """
 
990
        self._test_merge_from_strings(base, a, b, result)
 
991
 
 
992
    def test_agreement_deletion(self):
 
993
        """Agree to delete some lines, without conflicts."""
 
994
        base = """\
 
995
            start context
 
996
            base line 1
 
997
            base line 2
 
998
            end context
 
999
            """
 
1000
        a = """\
 
1001
            start context
 
1002
            base line 1
 
1003
            end context
 
1004
            """
 
1005
        b = """\
 
1006
            start context
 
1007
            base line 1
 
1008
            end context
 
1009
            """
 
1010
        result = """\
 
1011
            start context
 
1012
            base line 1
 
1013
            end context
 
1014
            """
 
1015
        self._test_merge_from_strings(base, a, b, result)
 
1016
 
 
1017
    def test_sync_on_deletion(self):
 
1018
        """Specific case of merge where we can synchronize incorrectly.
 
1019
        
 
1020
        A previous version of the weave merge concluded that the two versions
 
1021
        agreed on deleting line 2, and this could be a synchronization point.
 
1022
        Line 1 was then considered in isolation, and thought to be deleted on 
 
1023
        both sides.
 
1024
 
 
1025
        It's better to consider the whole thing as a disagreement region.
 
1026
        """
 
1027
        base = """\
 
1028
            start context
 
1029
            base line 1
 
1030
            base line 2
 
1031
            end context
 
1032
            """
 
1033
        a = """\
 
1034
            start context
 
1035
            base line 1
 
1036
            a's replacement line 2
 
1037
            end context
 
1038
            """
 
1039
        b = """\
 
1040
            start context
 
1041
            b replaces
 
1042
            both lines
 
1043
            end context
 
1044
            """
 
1045
        result = """\
 
1046
            start context
 
1047
<<<<<<< 
 
1048
            base line 1
 
1049
            a's replacement line 2
 
1050
=======
 
1051
            b replaces
 
1052
            both lines
 
1053
>>>>>>> 
 
1054
            end context
 
1055
            """
 
1056
        self._test_merge_from_strings(base, a, b, result)
 
1057
 
 
1058
 
 
1059
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
 
1060
 
 
1061
    def get_file(self, name='foo'):
 
1062
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
1063
                                 delta=True, create=True)
 
1064
 
 
1065
    def log_contents(self, w):
 
1066
        pass
 
1067
 
 
1068
 
 
1069
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
 
1070
 
 
1071
    def get_file(self, name='foo'):
 
1072
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
1073
 
 
1074
    def log_contents(self, w):
 
1075
        self.log('weave is:')
 
1076
        tmpf = StringIO()
 
1077
        write_weave(w, tmpf)
 
1078
        self.log(tmpf.getvalue())
 
1079
 
 
1080
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
 
1081
                                'xxx', '>>>>>>> ', 'bbb']