~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

(mbp) merge bzr.dev to 0.8, prepare for release

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
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
 
41
 
 
42
 
 
43
class VersionedFileTestMixIn(object):
 
44
    """A mixin test class for testing VersionedFiles.
 
45
 
 
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.
 
49
    """
 
50
 
 
51
    def test_add(self):
 
52
        f = self.get_file()
 
53
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
54
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
 
55
        def verify_file(f):
 
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())
 
64
    
 
65
            self.assertRaises(RevisionNotPresent,
 
66
                f.add_lines, 'r2', ['foo'], [])
 
67
            self.assertRaises(RevisionAlreadyPresent,
 
68
                f.add_lines, 'r1', [], [])
 
69
        verify_file(f)
 
70
        # this checks that reopen with create=True does not break anything.
 
71
        f = self.reopen_file(create=True)
 
72
        verify_file(f)
 
73
 
 
74
    def test_adds_with_parent_texts(self):
 
75
        f = self.get_file()
 
76
        parent_texts = {}
 
77
        parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
 
78
        try:
 
79
            parent_texts['r1'] = f.add_lines_with_ghosts('r1',
 
80
                                                         ['r0', 'ghost'], 
 
81
                                                         ['b\n', 'c\n'],
 
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',
 
86
                                             ['r0'], 
 
87
                                             ['b\n', 'c\n'],
 
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'])
 
92
        def verify_file(f):
 
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')
 
107
 
 
108
        verify_file(f)
 
109
        f = self.reopen_file()
 
110
        verify_file(f)
 
111
 
 
112
    def test_add_unicode_content(self):
 
113
        # unicode content is not permitted in versioned files. 
 
114
        # versioned files version sequences of bytes only.
 
115
        vf = self.get_file()
 
116
        self.assertRaises(errors.BzrBadParameterUnicode,
 
117
            vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
 
118
        self.assertRaises(
 
119
            (errors.BzrBadParameterUnicode, NotImplementedError),
 
120
            vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
 
121
 
 
122
    def test_inline_newline_throws(self):
 
123
        # \r characters are not permitted in lines being added
 
124
        vf = self.get_file()
 
125
        self.assertRaises(errors.BzrBadParameterContainsNewline, 
 
126
            vf.add_lines, 'a', [], ['a\n\n'])
 
127
        self.assertRaises(
 
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'])
 
132
        try:
 
133
            vf.add_lines_with_ghosts('b', [], ['a\r\n'])
 
134
        except NotImplementedError:
 
135
            pass
 
136
 
 
137
    def test_get_delta(self):
 
138
        f = self.get_file()
 
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'))
 
143
        next_parent = '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], 
 
148
                              False,
 
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
 
152
        next_parent = 'base'
 
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'))
 
164
 
 
165
    def test_get_deltas(self):
 
166
        f = self.get_file()
 
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'])
 
172
        next_parent = '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], 
 
177
                              False,
 
178
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
 
179
            self.assertEqual(expected_delta, deltas[new_version])
 
180
            next_parent = new_version
 
181
        next_parent = 'base'
 
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)
 
227
 
 
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'])
 
253
        next_parent = 'base'
 
254
        text_name = 'chain1-'
 
255
        text = ['line\n']
 
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',
 
282
                 }
 
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
 
288
        next_parent = 'base'
 
289
        text_name = 'chain2-'
 
290
        text = ['line\n']
 
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
 
296
        return sha1s
 
297
 
 
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'])
 
304
        next_parent = 'base'
 
305
        text_name = 'chain1-'
 
306
        text = ['line\n']
 
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
 
312
        next_parent = 'base'
 
313
        text_name = 'chain2-'
 
314
        text = ['line\n']
 
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'])
 
321
        
 
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),
 
327
                             parent,
 
328
                             sha1,
 
329
                             noeol,
 
330
                             delta)
 
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))
 
336
 
 
337
    def test_ancestry(self):
 
338
        f = self.get_file()
 
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:
 
348
        # r0 r1 r2 rM r3
 
349
        # r0 r1 r2 r3 rM
 
350
        # etc
 
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)
 
361
 
 
362
        self.assertRaises(RevisionNotPresent,
 
363
            f.get_ancestry, ['rM', 'rX'])
 
364
 
 
365
    def test_mutate_after_finish(self):
 
366
        f = self.get_file()
 
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'])
 
374
        
 
375
    def test_clear_cache(self):
 
376
        f = self.get_file()
 
377
        # on a new file it should not error
 
378
        f.clear_cache()
 
379
        # and after adding content, doing a clear_cache and a get should work.
 
380
        f.add_lines('0', [], ['a'])
 
381
        f.clear_cache()
 
382
        self.assertEqual(['a'], f.get_lines('0'))
 
383
 
 
384
    def test_clone_text(self):
 
385
        f = self.get_file()
 
386
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
387
        f.clone_text('r1', 'r0', ['r0'])
 
388
        def verify_file(f):
 
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'])
 
392
    
 
393
            self.assertRaises(RevisionNotPresent,
 
394
                f.clone_text, 'r2', 'rX', [])
 
395
            self.assertRaises(RevisionAlreadyPresent,
 
396
                f.clone_text, 'r1', 'r0', [])
 
397
        verify_file(f)
 
398
        verify_file(self.reopen_file())
 
399
 
 
400
    def test_create_empty(self):
 
401
        f = self.get_file()
 
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__))
 
408
 
 
409
    def test_copy_to(self):
 
410
        f = self.get_file()
 
411
        f.add_lines('0', [], ['a\n'])
 
412
        t = MemoryTransport()
 
413
        f.copy_to('foo', t)
 
414
        for suffix in f.__class__.get_suffixes():
 
415
            self.assertTrue(t.has('foo' + suffix))
 
416
 
 
417
    def test_get_suffixes(self):
 
418
        f = self.get_file()
 
419
        # should be the same
 
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))
 
423
 
 
424
    def build_graph(self, file, graph):
 
425
        for node in topo_sort(graph.items()):
 
426
            file.add_lines(node, graph[node], [])
 
427
 
 
428
    def test_get_graph(self):
 
429
        f = self.get_file()
 
430
        graph = {
 
431
            'v1': [],
 
432
            'v2': ['v1'],
 
433
            'v3': ['v2']}
 
434
        self.build_graph(f, graph)
 
435
        self.assertEqual(graph, f.get_graph())
 
436
    
 
437
    def test_get_graph_partial(self):
 
438
        f = self.get_file()
 
439
        complex_graph = {}
 
440
        simple_a = {
 
441
            'c': [],
 
442
            'b': ['c'],
 
443
            'a': ['b'],
 
444
            }
 
445
        complex_graph.update(simple_a)
 
446
        simple_b = {
 
447
            'c': [],
 
448
            'b': ['c'],
 
449
            }
 
450
        complex_graph.update(simple_b)
 
451
        simple_gam = {
 
452
            'c': [],
 
453
            'oo': [],
 
454
            'bar': ['oo', 'c'],
 
455
            'gam': ['bar'],
 
456
            }
 
457
        complex_graph.update(simple_gam)
 
458
        simple_b_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']))
 
466
 
 
467
    def test_get_parents(self):
 
468
        f = self.get_file()
 
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'])
 
475
 
 
476
        self.assertRaises(RevisionNotPresent,
 
477
            f.get_parents, 'y')
 
478
 
 
479
    def test_annotate(self):
 
480
        f = self.get_file()
 
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')
 
486
 
 
487
        self.assertRaises(RevisionNotPresent,
 
488
            f.annotate, 'foo')
 
489
 
 
490
    def test_walk(self):
 
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'])
 
498
 
 
499
        lines = {}
 
500
        for lineno, insert, dset, text in f.walk(['rX', 'rY']):
 
501
            lines[text] = (insert, dset)
 
502
 
 
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([])))
 
508
 
 
509
    def test_detection(self):
 
510
        # Test weaves detect corruption.
 
511
        #
 
512
        # Weaves contain a checksum of their texts.
 
513
        # When a text is extracted, this checksum should be
 
514
        # verified.
 
515
 
 
516
        w = self.get_file_corrupted_text()
 
517
 
 
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)
 
522
 
 
523
        w = self.get_file_corrupted_checksum()
 
524
 
 
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)
 
529
 
 
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)
 
533
 
 
534
    def reopen_file(self, name='foo'):
 
535
        """Open the versioned file from disk again."""
 
536
        raise NotImplementedError(self.reopen_file)
 
537
 
 
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.
 
543
        vf = self.get_file()
 
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.
 
558
            lines = {'base\n':0,
 
559
                     'lancestor\n':0,
 
560
                     'rancestor\n':0,
 
561
                     'child\n':0,
 
562
                     'otherchild\n':0,
 
563
                     }
 
564
            # iterate over the lines
 
565
            for line in vf.iter_lines_added_or_present_in_versions(versions):
 
566
                lines[line] += 1
 
567
            return lines
 
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.
 
573
        
 
574
        # test all lines
 
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)
 
582
 
 
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.
 
589
        vf = self.get_file()
 
590
        vf.add_lines('notbase', [], [])
 
591
        vf.add_lines('base', [], [])
 
592
        try:
 
593
            vf.fix_parents('notbase', ['base'])
 
594
        except NotImplementedError:
 
595
            return
 
596
        self.assertEqual(['base'], vf.get_parents('notbase'))
 
597
        # open again, check it stuck.
 
598
        vf = self.get_file()
 
599
        self.assertEqual(['base'], vf.get_parents('notbase'))
 
600
 
 
601
    def test_fix_parents_with_ghosts(self):
 
602
        # when fixing parents, ghosts that are listed should not be ghosts
 
603
        # anymore.
 
604
        vf = self.get_file()
 
605
 
 
606
        try:
 
607
            vf.add_lines_with_ghosts('notbase', ['base', 'stillghost'], [])
 
608
        except NotImplementedError:
 
609
            return
 
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.
 
614
        vf = self.get_file()
 
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'))
 
619
 
 
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.
 
625
        vf = self.get_file()
 
626
        # add a revision with ghost parents
 
627
        try:
 
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)
 
635
            return
 
636
        # test key graph related apis: getncestry, _graph, get_parents
 
637
        # has_version
 
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'],
 
655
                          },
 
656
                         vf.get_graph())
 
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'],
 
663
                          },
 
664
                         vf.get_graph_with_ghosts())
 
665
        self.assertFalse(vf.has_ghost(u'b\xbfse'))
 
666
 
 
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.
 
672
        vf = self.get_file()
 
673
        # probe for ghost support
 
674
        try:
 
675
            vf.has_ghost('hoo')
 
676
        except NotImplementedError:
 
677
            return
 
678
        vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
 
679
        vf.add_lines_with_ghosts('references_ghost',
 
680
                                 ['base', 'a_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])
 
686
 
 
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,
 
696
                          'base',
 
697
                          [],
 
698
                          [])
 
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'])
 
702
    
 
703
    def test_get_sha1(self):
 
704
        # check the sha1 data is available
 
705
        vf = self.get_file()
 
706
        # a simple file
 
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'])
 
712
        self.assertEqual(
 
713
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('a'))
 
714
        self.assertEqual(
 
715
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('b'))
 
716
        self.assertEqual(
 
717
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', vf.get_sha1('c'))
 
718
        
 
719
 
 
720
class TestWeave(TestCaseWithTransport, VersionedFileTestMixIn):
 
721
 
 
722
    def get_file(self, name='foo'):
 
723
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
724
 
 
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'])
 
729
        
 
730
        # We are going to invasively corrupt the text
 
731
        # Make sure the internals of weave are the same
 
732
        self.assertEqual([('{', 0)
 
733
                        , 'hello\n'
 
734
                        , ('}', None)
 
735
                        , ('{', 1)
 
736
                        , 'there\n'
 
737
                        , ('}', None)
 
738
                        ], w._weave)
 
739
        
 
740
        self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
 
741
                        , '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
742
                        ], w._sha1s)
 
743
        w.check()
 
744
        
 
745
        # Corrupted
 
746
        w._weave[4] = 'There\n'
 
747
        return w
 
748
 
 
749
    def get_file_corrupted_checksum(self):
 
750
        w = self.get_file_corrupted_text()
 
751
        # Corrected
 
752
        w._weave[4] = 'there\n'
 
753
        self.assertEqual('hello\nthere\n', w.get_text('v2'))
 
754
        
 
755
        #Invalid checksum, first digit changed
 
756
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
757
        return w
 
758
 
 
759
    def reopen_file(self, name='foo', create=False):
 
760
        return WeaveFile(name, get_transport(self.get_url('.')), create=create)
 
761
 
 
762
    def test_no_implicit_create(self):
 
763
        self.assertRaises(errors.NoSuchFile,
 
764
                          WeaveFile,
 
765
                          'foo',
 
766
                          get_transport(self.get_url('.')))
 
767
 
 
768
    def get_factory(self):
 
769
        return WeaveFile
 
770
 
 
771
 
 
772
class TestKnit(TestCaseWithTransport, VersionedFileTestMixIn):
 
773
 
 
774
    def get_file(self, name='foo'):
 
775
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
776
                                 delta=True, create=True)
 
777
 
 
778
    def get_factory(self):
 
779
        return KnitVersionedFile
 
780
 
 
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'])
 
785
        return knit
 
786
 
 
787
    def reopen_file(self, name='foo', create=False):
 
788
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
789
            delta=True,
 
790
            create=create)
 
791
 
 
792
    def test_detection(self):
 
793
        print "TODO for merging: create a corrupted knit."
 
794
        knit = self.get_file()
 
795
        knit.check()
 
796
 
 
797
    def test_no_implicit_create(self):
 
798
        self.assertRaises(errors.NoSuchFile,
 
799
                          KnitVersionedFile,
 
800
                          'foo',
 
801
                          get_transport(self.get_url('.')))
 
802
 
 
803
 
 
804
class InterString(versionedfile.InterVersionedFile):
 
805
    """An inter-versionedfile optimised code path for strings.
 
806
 
 
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.
 
810
    """
 
811
 
 
812
    @staticmethod
 
813
    def is_compatible(source, target):
 
814
        """InterString is compatible with strings-as-versionedfiles."""
 
815
        return isinstance(source, str) and isinstance(target, str)
 
816
 
 
817
 
 
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-
 
821
# stange-objects
 
822
class TestInterVersionedFile(TestCaseWithTransport):
 
823
 
 
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
 
830
        # is handed to them.
 
831
        dummy_a = "VersionedFile 1."
 
832
        dummy_b = "VersionedFile 2."
 
833
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
834
 
 
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,
 
839
                         inter.__class__)
 
840
        self.assertEqual(a, inter.source)
 
841
        self.assertEqual(b, inter.target)
 
842
 
 
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
 
848
        # check
 
849
        dummy_a = "VersionedFile 1."
 
850
        dummy_b = "VersionedFile 2."
 
851
        versionedfile.InterVersionedFile.register_optimiser(InterString)
 
852
        try:
 
853
            # we should get the default for something InterString returns False
 
854
            # to
 
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)
 
863
        finally:
 
864
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
 
865
        # now we should get the default InterVersionedFile object again.
 
866
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
867
 
 
868
 
 
869
class TestReadonlyHttpMixin(object):
 
870
 
 
871
    def test_readonly_http_works(self):
 
872
        # we should be able to read from http with a versioned file.
 
873
        vf = self.get_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())
 
877
        # now with feeling.
 
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)
 
884
 
 
885
 
 
886
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
887
 
 
888
    def get_file(self):
 
889
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
 
890
 
 
891
    def get_factory(self):
 
892
        return WeaveFile
 
893
 
 
894
 
 
895
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
896
 
 
897
    def get_file(self):
 
898
        return KnitVersionedFile('foo', get_transport(self.get_url('.')),
 
899
                                 delta=True, create=True)
 
900
 
 
901
    def get_factory(self):
 
902
        return KnitVersionedFile
 
903
 
 
904
 
 
905
class MergeCasesMixin(object):
 
906
 
 
907
    def doMerge(self, base, a, b, mp):
 
908
        from cStringIO import StringIO
 
909
        from textwrap import dedent
 
910
 
 
911
        def addcrlf(x):
 
912
            return x + '\n'
 
913
        
 
914
        w = self.get_file()
 
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))
 
918
 
 
919
        self.log_contents(w)
 
920
 
 
921
        self.log('merge plan:')
 
922
        p = list(w.plan_merge('text1', 'text2'))
 
923
        for state, line in p:
 
924
            if line:
 
925
                self.log('%12s | %s' % (state, line[:-1]))
 
926
 
 
927
        self.log('merge:')
 
928
        mt = StringIO()
 
929
        mt.writelines(w.weave_merge(p))
 
930
        mt.seek(0)
 
931
        self.log(mt.getvalue())
 
932
 
 
933
        mp = map(addcrlf, mp)
 
934
        self.assertEqual(mt.readlines(), mp)
 
935
        
 
936
        
 
937
    def testOneInsert(self):
 
938
        self.doMerge([],
 
939
                     ['aa'],
 
940
                     [],
 
941
                     ['aa'])
 
942
 
 
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'])
 
948
 
 
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)
 
959
 
 
960
        # really it ought to reduce this to 
 
961
        # ['aaa', 'xxx', 'yyy', 'bbb']
 
962
 
 
963
 
 
964
    def testClashReplace(self):
 
965
        self.doMerge(['aaa'],
 
966
                     ['xxx'],
 
967
                     ['yyy', 'zzz'],
 
968
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
 
969
                      '>>>>>>> '])
 
970
 
 
971
    def testNonClashInsert1(self):
 
972
        self.doMerge(['aaa'],
 
973
                     ['xxx', 'aaa'],
 
974
                     ['yyy', 'zzz'],
 
975
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
 
976
                      '>>>>>>> '])
 
977
 
 
978
    def testNonClashInsert2(self):
 
979
        self.doMerge(['aaa'],
 
980
                     ['aaa'],
 
981
                     ['yyy', 'zzz'],
 
982
                     ['yyy', 'zzz'])
 
983
 
 
984
 
 
985
    def testDeleteAndModify(self):
 
986
        """Clashing delete and modification.
 
987
 
 
988
        If one side modifies a region and the other deletes it then
 
989
        there should be a conflict with one side blank.
 
990
        """
 
991
 
 
992
        #######################################
 
993
        # skippd, not working yet
 
994
        return
 
995
        
 
996
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
997
                     ['aaa', 'ddd', 'ccc'],
 
998
                     ['aaa', 'ccc'],
 
999
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
 
1000
 
 
1001
    def _test_merge_from_strings(self, base, a, b, expected):
 
1002
        w = self.get_file()
 
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:
 
1009
            if line:
 
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)
 
1015
 
 
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')
 
1020
 
 
1021
    def test_deletion_extended(self):
 
1022
        """One side deletes, the other deletes more.
 
1023
        """
 
1024
        base = """\
 
1025
            line 1
 
1026
            line 2
 
1027
            line 3
 
1028
            """
 
1029
        a = """\
 
1030
            line 1
 
1031
            line 2
 
1032
            """
 
1033
        b = """\
 
1034
            line 1
 
1035
            """
 
1036
        result = """\
 
1037
            line 1
 
1038
            """
 
1039
        self._test_merge_from_strings(base, a, b, result)
 
1040
 
 
1041
    def test_deletion_overlap(self):
 
1042
        """Delete overlapping regions with no other conflict.
 
1043
 
 
1044
        Arguably it'd be better to treat these as agreement, rather than 
 
1045
        conflict, but for now conflict is safer.
 
1046
        """
 
1047
        base = """\
 
1048
            start context
 
1049
            int a() {}
 
1050
            int b() {}
 
1051
            int c() {}
 
1052
            end context
 
1053
            """
 
1054
        a = """\
 
1055
            start context
 
1056
            int a() {}
 
1057
            end context
 
1058
            """
 
1059
        b = """\
 
1060
            start context
 
1061
            int c() {}
 
1062
            end context
 
1063
            """
 
1064
        result = """\
 
1065
            start context
 
1066
<<<<<<< 
 
1067
            int a() {}
 
1068
=======
 
1069
            int c() {}
 
1070
>>>>>>> 
 
1071
            end context
 
1072
            """
 
1073
        self._test_merge_from_strings(base, a, b, result)
 
1074
 
 
1075
    def test_agreement_deletion(self):
 
1076
        """Agree to delete some lines, without conflicts."""
 
1077
        base = """\
 
1078
            start context
 
1079
            base line 1
 
1080
            base line 2
 
1081
            end context
 
1082
            """
 
1083
        a = """\
 
1084
            start context
 
1085
            base line 1
 
1086
            end context
 
1087
            """
 
1088
        b = """\
 
1089
            start context
 
1090
            base line 1
 
1091
            end context
 
1092
            """
 
1093
        result = """\
 
1094
            start context
 
1095
            base line 1
 
1096
            end context
 
1097
            """
 
1098
        self._test_merge_from_strings(base, a, b, result)
 
1099
 
 
1100
    def test_sync_on_deletion(self):
 
1101
        """Specific case of merge where we can synchronize incorrectly.
 
1102
        
 
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 
 
1106
        both sides.
 
1107
 
 
1108
        It's better to consider the whole thing as a disagreement region.
 
1109
        """
 
1110
        base = """\
 
1111
            start context
 
1112
            base line 1
 
1113
            base line 2
 
1114
            end context
 
1115
            """
 
1116
        a = """\
 
1117
            start context
 
1118
            base line 1
 
1119
            a's replacement line 2
 
1120
            end context
 
1121
            """
 
1122
        b = """\
 
1123
            start context
 
1124
            b replaces
 
1125
            both lines
 
1126
            end context
 
1127
            """
 
1128
        result = """\
 
1129
            start context
 
1130
<<<<<<< 
 
1131
            base line 1
 
1132
            a's replacement line 2
 
1133
=======
 
1134
            b replaces
 
1135
            both lines
 
1136
>>>>>>> 
 
1137
            end context
 
1138
            """
 
1139
        self._test_merge_from_strings(base, a, b, result)
 
1140
 
 
1141
 
 
1142
class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
 
1143
 
 
1144
    def get_file(self, name='foo'):
 
1145
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
1146
                                 delta=True, create=True)
 
1147
 
 
1148
    def log_contents(self, w):
 
1149
        pass
 
1150
 
 
1151
 
 
1152
class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
 
1153
 
 
1154
    def get_file(self, name='foo'):
 
1155
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
1156
 
 
1157
    def log_contents(self, w):
 
1158
        self.log('weave is:')
 
1159
        tmpf = StringIO()
 
1160
        write_weave(w, tmpf)
 
1161
        self.log(tmpf.getvalue())
 
1162
 
 
1163
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
 
1164
                                'xxx', '>>>>>>> ', 'bbb']