/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: Martin Pool
  • Date: 2008-01-04 23:53:04 UTC
  • mto: (3170.2.1 1.1)
  • mto: This revision was merged to the branch mainline in revision 3185.
  • Revision ID: mbp@sourcefrog.net-20080104235304-ms9b1y63r710qv2i
PrepareĀ 1.1rc1

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Canonical Ltd
 
1
# Copyright (C) 2005 Canonical Ltd
2
2
#
3
3
# Authors:
4
4
#   Johan Rydberg <jrydberg@gnu.org>
7
7
# it under the terms of the GNU General Public License as published by
8
8
# the Free Software Foundation; either version 2 of the License, or
9
9
# (at your option) any later version.
10
 
 
 
10
#
11
11
# This program is distributed in the hope that it will be useful,
12
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
14
# GNU General Public License for more details.
15
 
 
 
15
#
16
16
# You should have received a copy of the GNU General Public License
17
17
# along with this program; if not, write to the Free Software
18
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
 
20
20
 
 
21
# TODO: might be nice to create a versionedfile with some type of corruption
 
22
# considered typical and check that it can be detected/corrected.
 
23
 
 
24
from StringIO import StringIO
 
25
 
21
26
import bzrlib
22
 
import bzrlib.errors as errors
 
27
from bzrlib import (
 
28
    errors,
 
29
    osutils,
 
30
    progress,
 
31
    )
23
32
from bzrlib.errors import (
24
33
                           RevisionNotPresent, 
25
34
                           RevisionAlreadyPresent,
26
35
                           WeaveParentMismatch
27
36
                           )
28
 
from bzrlib.knit import KnitVersionedFile, \
29
 
     KnitAnnotateFactory
30
 
from bzrlib.tests import TestCaseWithTransport
 
37
from bzrlib.knit import (
 
38
    KnitVersionedFile,
 
39
    KnitAnnotateFactory,
 
40
    KnitPlainFactory,
 
41
    )
 
42
from bzrlib.tests import TestCaseWithMemoryTransport, TestSkipped
 
43
from bzrlib.tests.http_utils import TestCaseWithWebserver
31
44
from bzrlib.trace import mutter
32
45
from bzrlib.transport import get_transport
33
46
from bzrlib.transport.memory import MemoryTransport
 
47
from bzrlib.tsort import topo_sort
34
48
import bzrlib.versionedfile as versionedfile
35
49
from bzrlib.weave import WeaveFile
36
 
from bzrlib.weavefile import read_weave
 
50
from bzrlib.weavefile import read_weave, write_weave
37
51
 
38
52
 
39
53
class VersionedFileTestMixIn(object):
63
77
            self.assertRaises(RevisionAlreadyPresent,
64
78
                f.add_lines, 'r1', [], [])
65
79
        verify_file(f)
66
 
        f = self.reopen_file()
 
80
        # this checks that reopen with create=True does not break anything.
 
81
        f = self.reopen_file(create=True)
67
82
        verify_file(f)
68
83
 
69
84
    def test_adds_with_parent_texts(self):
70
85
        f = self.get_file()
71
86
        parent_texts = {}
72
 
        parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
 
87
        _, _, parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
73
88
        try:
74
 
            parent_texts['r1'] = f.add_lines_with_ghosts('r1',
75
 
                                                         ['r0', 'ghost'], 
76
 
                                                         ['b\n', 'c\n'],
77
 
                                                         parent_texts=parent_texts)
 
89
            _, _, parent_texts['r1'] = f.add_lines_with_ghosts('r1',
 
90
                ['r0', 'ghost'], ['b\n', 'c\n'], parent_texts=parent_texts)
78
91
        except NotImplementedError:
79
92
            # if the format doesn't support ghosts, just add normally.
80
 
            parent_texts['r1'] = f.add_lines('r1',
81
 
                                             ['r0'], 
82
 
                                             ['b\n', 'c\n'],
83
 
                                             parent_texts=parent_texts)
 
93
            _, _, parent_texts['r1'] = f.add_lines('r1',
 
94
                ['r0'], ['b\n', 'c\n'], parent_texts=parent_texts)
84
95
        f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
85
96
        self.assertNotEqual(None, parent_texts['r0'])
86
97
        self.assertNotEqual(None, parent_texts['r1'])
104
115
        f = self.reopen_file()
105
116
        verify_file(f)
106
117
 
107
 
    def test_get_delta(self):
108
 
        f = self.get_file()
109
 
        sha1s = self._setup_for_deltas(f)
110
 
        expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
111
 
                          [(0, 0, 1, [('base', 'line\n')])])
112
 
        self.assertEqual(expected_delta, f.get_delta('base'))
113
 
        next_parent = 'base'
114
 
        text_name = 'chain1-'
115
 
        for depth in range(26):
116
 
            new_version = text_name + '%s' % depth
117
 
            expected_delta = (next_parent, sha1s[depth], 
118
 
                              False,
119
 
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
120
 
            self.assertEqual(expected_delta, f.get_delta(new_version))
121
 
            next_parent = new_version
122
 
        next_parent = 'base'
123
 
        text_name = 'chain2-'
124
 
        for depth in range(26):
125
 
            new_version = text_name + '%s' % depth
126
 
            expected_delta = (next_parent, sha1s[depth], False,
127
 
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
128
 
            self.assertEqual(expected_delta, f.get_delta(new_version))
129
 
            next_parent = new_version
130
 
        # smoke test for eol support
131
 
        expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
132
 
        self.assertEqual(['line'], f.get_lines('noeol'))
133
 
        self.assertEqual(expected_delta, f.get_delta('noeol'))
134
 
 
135
 
    def test_get_deltas(self):
136
 
        f = self.get_file()
137
 
        sha1s = self._setup_for_deltas(f)
138
 
        deltas = f.get_deltas(f.versions())
139
 
        expected_delta = (None, '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
140
 
                          [(0, 0, 1, [('base', 'line\n')])])
141
 
        self.assertEqual(expected_delta, deltas['base'])
142
 
        next_parent = 'base'
143
 
        text_name = 'chain1-'
144
 
        for depth in range(26):
145
 
            new_version = text_name + '%s' % depth
146
 
            expected_delta = (next_parent, sha1s[depth], 
147
 
                              False,
148
 
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
149
 
            self.assertEqual(expected_delta, deltas[new_version])
150
 
            next_parent = new_version
151
 
        next_parent = 'base'
152
 
        text_name = 'chain2-'
153
 
        for depth in range(26):
154
 
            new_version = text_name + '%s' % depth
155
 
            expected_delta = (next_parent, sha1s[depth], False,
156
 
                              [(depth + 1, depth + 1, 1, [(new_version, 'line\n')])])
157
 
            self.assertEqual(expected_delta, deltas[new_version])
158
 
            next_parent = new_version
159
 
        # smoke tests for eol support
160
 
        expected_delta = ('base', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, [])
161
 
        self.assertEqual(['line'], f.get_lines('noeol'))
162
 
        self.assertEqual(expected_delta, deltas['noeol'])
163
 
        # smoke tests for eol support - two noeol in a row same content
164
 
        expected_deltas = (('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
165
 
                          [(0, 1, 2, [(u'noeolsecond', 'line\n'), (u'noeolsecond', 'line\n')])]),
166
 
                          ('noeol', '3ad7ee82dbd8f29ecba073f96e43e414b3f70a4d', True, 
167
 
                           [(0, 0, 1, [('noeolsecond', 'line\n')]), (1, 1, 0, [])]))
168
 
        self.assertEqual(['line\n', 'line'], f.get_lines('noeolsecond'))
169
 
        self.assertTrue(deltas['noeolsecond'] in expected_deltas)
170
 
        # two no-eol in a row, different content
171
 
        expected_delta = ('noeolsecond', '8bb553a84e019ef1149db082d65f3133b195223b', True, 
172
 
                          [(1, 2, 1, [(u'noeolnotshared', 'phone\n')])])
173
 
        self.assertEqual(['line\n', 'phone'], f.get_lines('noeolnotshared'))
174
 
        self.assertEqual(expected_delta, deltas['noeolnotshared'])
175
 
        # eol folling a no-eol with content change
176
 
        expected_delta = ('noeol', 'a61f6fb6cfc4596e8d88c34a308d1e724caf8977', False, 
177
 
                          [(0, 1, 1, [(u'eol', 'phone\n')])])
178
 
        self.assertEqual(['phone\n'], f.get_lines('eol'))
179
 
        self.assertEqual(expected_delta, deltas['eol'])
180
 
        # eol folling a no-eol with content change
181
 
        expected_delta = ('noeol', '6bfa09d82ce3e898ad4641ae13dd4fdb9cf0d76b', False, 
182
 
                          [(0, 1, 1, [(u'eolline', 'line\n')])])
183
 
        self.assertEqual(['line\n'], f.get_lines('eolline'))
184
 
        self.assertEqual(expected_delta, deltas['eolline'])
185
 
        # eol with no parents
186
 
        expected_delta = (None, '264f39cab871e4cfd65b3a002f7255888bb5ed97', True, 
187
 
                          [(0, 0, 1, [(u'noeolbase', 'line\n')])])
188
 
        self.assertEqual(['line'], f.get_lines('noeolbase'))
189
 
        self.assertEqual(expected_delta, deltas['noeolbase'])
190
 
        # eol with two parents, in inverse insertion order
191
 
        expected_deltas = (('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
192
 
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]),
193
 
                           ('noeolbase', '264f39cab871e4cfd65b3a002f7255888bb5ed97', True,
194
 
                            [(0, 1, 1, [(u'eolbeforefirstparent', 'line\n')])]))
195
 
        self.assertEqual(['line'], f.get_lines('eolbeforefirstparent'))
196
 
        #self.assertTrue(deltas['eolbeforefirstparent'] in expected_deltas)
 
118
    def test_add_unicode_content(self):
 
119
        # unicode content is not permitted in versioned files. 
 
120
        # versioned files version sequences of bytes only.
 
121
        vf = self.get_file()
 
122
        self.assertRaises(errors.BzrBadParameterUnicode,
 
123
            vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
 
124
        self.assertRaises(
 
125
            (errors.BzrBadParameterUnicode, NotImplementedError),
 
126
            vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
 
127
 
 
128
    def test_add_follows_left_matching_blocks(self):
 
129
        """If we change left_matching_blocks, delta changes
 
130
 
 
131
        Note: There are multiple correct deltas in this case, because
 
132
        we start with 1 "a" and we get 3.
 
133
        """
 
134
        vf = self.get_file()
 
135
        if isinstance(vf, WeaveFile):
 
136
            raise TestSkipped("WeaveFile ignores left_matching_blocks")
 
137
        vf.add_lines('1', [], ['a\n'])
 
138
        vf.add_lines('2', ['1'], ['a\n', 'a\n', 'a\n'],
 
139
                     left_matching_blocks=[(0, 0, 1), (1, 3, 0)])
 
140
        self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('2'))
 
141
        vf.add_lines('3', ['1'], ['a\n', 'a\n', 'a\n'],
 
142
                     left_matching_blocks=[(0, 2, 1), (1, 3, 0)])
 
143
        self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('3'))
 
144
 
 
145
    def test_inline_newline_throws(self):
 
146
        # \r characters are not permitted in lines being added
 
147
        vf = self.get_file()
 
148
        self.assertRaises(errors.BzrBadParameterContainsNewline, 
 
149
            vf.add_lines, 'a', [], ['a\n\n'])
 
150
        self.assertRaises(
 
151
            (errors.BzrBadParameterContainsNewline, NotImplementedError),
 
152
            vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
 
153
        # but inline CR's are allowed
 
154
        vf.add_lines('a', [], ['a\r\n'])
 
155
        try:
 
156
            vf.add_lines_with_ghosts('b', [], ['a\r\n'])
 
157
        except NotImplementedError:
 
158
            pass
 
159
 
 
160
    def test_add_reserved(self):
 
161
        vf = self.get_file()
 
162
        self.assertRaises(errors.ReservedId,
 
163
            vf.add_lines, 'a:', [], ['a\n', 'b\n', 'c\n'])
 
164
 
 
165
    def test_add_lines_nostoresha(self):
 
166
        """When nostore_sha is supplied using old content raises."""
 
167
        vf = self.get_file()
 
168
        empty_text = ('a', [])
 
169
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
170
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
171
        shas = []
 
172
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
173
            sha, _, _ = vf.add_lines(version, [], lines)
 
174
            shas.append(sha)
 
175
        # we now have a copy of all the lines in the vf.
 
176
        for sha, (version, lines) in zip(
 
177
            shas, (empty_text, sample_text_nl, sample_text_no_nl)):
 
178
            self.assertRaises(errors.ExistingContent,
 
179
                vf.add_lines, version + "2", [], lines,
 
180
                nostore_sha=sha)
 
181
            # and no new version should have been added.
 
182
            self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
 
183
                version + "2")
 
184
 
 
185
    def test_add_lines_with_ghosts_nostoresha(self):
 
186
        """When nostore_sha is supplied using old content raises."""
 
187
        vf = self.get_file()
 
188
        empty_text = ('a', [])
 
189
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
190
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
191
        shas = []
 
192
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
193
            sha, _, _ = vf.add_lines(version, [], lines)
 
194
            shas.append(sha)
 
195
        # we now have a copy of all the lines in the vf.
 
196
        # is the test applicable to this vf implementation?
 
197
        try:
 
198
            vf.add_lines_with_ghosts('d', [], [])
 
199
        except NotImplementedError:
 
200
            raise TestSkipped("add_lines_with_ghosts is optional")
 
201
        for sha, (version, lines) in zip(
 
202
            shas, (empty_text, sample_text_nl, sample_text_no_nl)):
 
203
            self.assertRaises(errors.ExistingContent,
 
204
                vf.add_lines_with_ghosts, version + "2", [], lines,
 
205
                nostore_sha=sha)
 
206
            # and no new version should have been added.
 
207
            self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
 
208
                version + "2")
 
209
 
 
210
    def test_add_lines_return_value(self):
 
211
        # add_lines should return the sha1 and the text size.
 
212
        vf = self.get_file()
 
213
        empty_text = ('a', [])
 
214
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
215
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
216
        # check results for the three cases:
 
217
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
218
            # the first two elements are the same for all versioned files:
 
219
            # - the digest and the size of the text. For some versioned files
 
220
            #   additional data is returned in additional tuple elements.
 
221
            result = vf.add_lines(version, [], lines)
 
222
            self.assertEqual(3, len(result))
 
223
            self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
 
224
                result[0:2])
 
225
        # parents should not affect the result:
 
226
        lines = sample_text_nl[1]
 
227
        self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
 
228
            vf.add_lines('d', ['b', 'c'], lines)[0:2])
 
229
 
 
230
    def test_get_reserved(self):
 
231
        vf = self.get_file()
 
232
        self.assertRaises(errors.ReservedId, vf.get_texts, ['b:'])
 
233
        self.assertRaises(errors.ReservedId, vf.get_lines, 'b:')
 
234
        self.assertRaises(errors.ReservedId, vf.get_text, 'b:')
 
235
 
 
236
    def test_make_mpdiffs(self):
 
237
        from bzrlib import multiparent
 
238
        vf = self.get_file('foo')
 
239
        sha1s = self._setup_for_deltas(vf)
 
240
        new_vf = self.get_file('bar')
 
241
        for version in multiparent.topo_iter(vf):
 
242
            mpdiff = vf.make_mpdiffs([version])[0]
 
243
            new_vf.add_mpdiffs([(version, vf.get_parents(version),
 
244
                                 vf.get_sha1(version), mpdiff)])
 
245
            self.assertEqualDiff(vf.get_text(version),
 
246
                                 new_vf.get_text(version))
197
247
 
198
248
    def _setup_for_deltas(self, f):
199
 
        self.assertRaises(errors.RevisionNotPresent, f.get_delta, 'base')
 
249
        self.assertFalse(f.has_version('base'))
200
250
        # add texts that should trip the knit maximum delta chain threshold
201
251
        # as well as doing parallel chains of data in knits.
202
252
        # this is done by two chains of 25 insertions
265
315
            next_parent = new_version
266
316
        return sha1s
267
317
 
268
 
    def test_add_delta(self):
269
 
        # tests for the add-delta facility.
270
 
        # at this point, optimising for speed, we assume no checks when deltas are inserted.
271
 
        # this may need to be revisited.
272
 
        source = self.get_file('source')
273
 
        source.add_lines('base', [], ['line\n'])
274
 
        next_parent = 'base'
275
 
        text_name = 'chain1-'
276
 
        text = ['line\n']
277
 
        for depth in range(26):
278
 
            new_version = text_name + '%s' % depth
279
 
            text = text + ['line\n']
280
 
            source.add_lines(new_version, [next_parent], text)
281
 
            next_parent = new_version
282
 
        next_parent = 'base'
283
 
        text_name = 'chain2-'
284
 
        text = ['line\n']
285
 
        for depth in range(26):
286
 
            new_version = text_name + '%s' % depth
287
 
            text = text + ['line\n']
288
 
            source.add_lines(new_version, [next_parent], text)
289
 
            next_parent = new_version
290
 
        source.add_lines('noeol', ['base'], ['line'])
291
 
        
292
 
        target = self.get_file('target')
293
 
        for version in source.versions():
294
 
            parent, sha1, noeol, delta = source.get_delta(version)
295
 
            target.add_delta(version,
296
 
                             source.get_parents(version),
297
 
                             parent,
298
 
                             sha1,
299
 
                             noeol,
300
 
                             delta)
301
 
        self.assertRaises(RevisionAlreadyPresent,
302
 
                          target.add_delta, 'base', [], None, '', False, [])
303
 
        for version in source.versions():
304
 
            self.assertEqual(source.get_lines(version),
305
 
                             target.get_lines(version))
306
 
 
307
318
    def test_ancestry(self):
308
319
        f = self.get_file()
309
320
        self.assertEqual([], f.get_ancestry([]))
332
343
        self.assertRaises(RevisionNotPresent,
333
344
            f.get_ancestry, ['rM', 'rX'])
334
345
 
 
346
        self.assertEqual(set(f.get_ancestry('rM')),
 
347
            set(f.get_ancestry('rM', topo_sorted=False)))
 
348
 
335
349
    def test_mutate_after_finish(self):
336
350
        f = self.get_file()
337
351
        f.transaction_finished()
338
 
        self.assertRaises(errors.OutSideTransaction, f.add_delta, '', [], '', '', False, [])
339
352
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
340
353
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
341
 
        self.assertRaises(errors.OutSideTransaction, f.fix_parents, '', [])
342
354
        self.assertRaises(errors.OutSideTransaction, f.join, '')
343
355
        self.assertRaises(errors.OutSideTransaction, f.clone_text, 'base', 'bar', ['foo'])
344
356
        
391
403
        # and should be a list
392
404
        self.assertTrue(isinstance(f.__class__.get_suffixes(), list))
393
405
 
 
406
    def build_graph(self, file, graph):
 
407
        for node in topo_sort(graph.items()):
 
408
            file.add_lines(node, graph[node], [])
 
409
 
394
410
    def test_get_graph(self):
395
411
        f = self.get_file()
396
 
        f.add_lines('v1', [], ['hello\n'])
397
 
        f.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
398
 
        f.add_lines('v3', ['v2'], ['hello\n', 'cruel\n', 'world\n'])
399
 
        self.assertEqual({'v1': [],
400
 
                          'v2': ['v1'],
401
 
                          'v3': ['v2']},
402
 
                         f.get_graph())
 
412
        graph = {
 
413
            'v1': (),
 
414
            'v2': ('v1', ),
 
415
            'v3': ('v2', )}
 
416
        self.build_graph(f, graph)
 
417
        self.assertEqual(graph, f.get_graph())
 
418
    
 
419
    def test_get_graph_partial(self):
 
420
        f = self.get_file()
 
421
        complex_graph = {}
 
422
        simple_a = {
 
423
            'c': (),
 
424
            'b': ('c', ),
 
425
            'a': ('b', ),
 
426
            }
 
427
        complex_graph.update(simple_a)
 
428
        simple_b = {
 
429
            'c': (),
 
430
            'b': ('c', ),
 
431
            }
 
432
        complex_graph.update(simple_b)
 
433
        simple_gam = {
 
434
            'c': (),
 
435
            'oo': (),
 
436
            'bar': ('oo', 'c'),
 
437
            'gam': ('bar', ),
 
438
            }
 
439
        complex_graph.update(simple_gam)
 
440
        simple_b_gam = {}
 
441
        simple_b_gam.update(simple_gam)
 
442
        simple_b_gam.update(simple_b)
 
443
        self.build_graph(f, complex_graph)
 
444
        self.assertEqual(simple_a, f.get_graph(['a']))
 
445
        self.assertEqual(simple_b, f.get_graph(['b']))
 
446
        self.assertEqual(simple_gam, f.get_graph(['gam']))
 
447
        self.assertEqual(simple_b_gam, f.get_graph(['b', 'gam']))
403
448
 
404
449
    def test_get_parents(self):
405
450
        f = self.get_file()
424
469
        self.assertRaises(RevisionNotPresent,
425
470
            f.annotate, 'foo')
426
471
 
427
 
    def test_walk(self):
428
 
        # tests that walk returns all the inclusions for the requested
429
 
        # revisions as well as the revisions changes themselves.
430
 
        f = self.get_file('1')
431
 
        f.add_lines('r0', [], ['a\n', 'b\n'])
432
 
        f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
433
 
        f.add_lines('rX', ['r1'], ['d\n', 'b\n'])
434
 
        f.add_lines('rY', ['r1'], ['c\n', 'e\n'])
435
 
 
436
 
        lines = {}
437
 
        for lineno, insert, dset, text in f.walk(['rX', 'rY']):
438
 
            lines[text] = (insert, dset)
439
 
 
440
 
        self.assertTrue(lines['a\n'], ('r0', set(['r1'])))
441
 
        self.assertTrue(lines['b\n'], ('r0', set(['rY'])))
442
 
        self.assertTrue(lines['c\n'], ('r1', set(['rX'])))
443
 
        self.assertTrue(lines['d\n'], ('rX', set([])))
444
 
        self.assertTrue(lines['e\n'], ('rY', set([])))
445
 
 
446
472
    def test_detection(self):
447
473
        # Test weaves detect corruption.
448
474
        #
472
498
        """Open the versioned file from disk again."""
473
499
        raise NotImplementedError(self.reopen_file)
474
500
 
 
501
    def test_iter_parents(self):
 
502
        """iter_parents returns the parents for many nodes."""
 
503
        f = self.get_file()
 
504
        # sample data:
 
505
        # no parents
 
506
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
507
        # 1 parents
 
508
        f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
 
509
        # 2 parents
 
510
        f.add_lines('r2', ['r1', 'r0'], ['a\n', 'b\n'])
 
511
        # XXX TODO a ghost
 
512
        # cases: each sample data individually:
 
513
        self.assertEqual(set([('r0', ())]),
 
514
            set(f.iter_parents(['r0'])))
 
515
        self.assertEqual(set([('r1', ('r0', ))]),
 
516
            set(f.iter_parents(['r1'])))
 
517
        self.assertEqual(set([('r2', ('r1', 'r0'))]),
 
518
            set(f.iter_parents(['r2'])))
 
519
        # no nodes returned for a missing node
 
520
        self.assertEqual(set(),
 
521
            set(f.iter_parents(['missing'])))
 
522
        # 1 node returned with missing nodes skipped
 
523
        self.assertEqual(set([('r1', ('r0', ))]),
 
524
            set(f.iter_parents(['ghost1', 'r1', 'ghost'])))
 
525
        # 2 nodes returned
 
526
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
527
            set(f.iter_parents(['r0', 'r1'])))
 
528
        # 2 nodes returned, missing skipped
 
529
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
530
            set(f.iter_parents(['a', 'r0', 'b', 'r1', 'c'])))
 
531
 
475
532
    def test_iter_lines_added_or_present_in_versions(self):
476
533
        # test that we get at least an equalset of the lines added by
477
534
        # versions in the weave 
478
535
        # the ordering here is to make a tree so that dumb searches have
479
536
        # more changes to muck up.
 
537
 
 
538
        class InstrumentedProgress(progress.DummyProgress):
 
539
 
 
540
            def __init__(self):
 
541
 
 
542
                progress.DummyProgress.__init__(self)
 
543
                self.updates = []
 
544
 
 
545
            def update(self, msg=None, current=None, total=None):
 
546
                self.updates.append((msg, current, total))
 
547
 
480
548
        vf = self.get_file()
481
549
        # add a base to get included
482
550
        vf.add_lines('base', [], ['base\n'])
490
558
        vf.add_lines('otherchild',
491
559
                     ['lancestor', 'base'],
492
560
                     ['base\n', 'lancestor\n', 'otherchild\n'])
493
 
        def iter_with_versions(versions):
 
561
        def iter_with_versions(versions, expected):
494
562
            # now we need to see what lines are returned, and how often.
495
 
            lines = {'base\n':0,
496
 
                     'lancestor\n':0,
497
 
                     'rancestor\n':0,
498
 
                     'child\n':0,
499
 
                     'otherchild\n':0,
500
 
                     }
 
563
            lines = {}
 
564
            progress = InstrumentedProgress()
501
565
            # iterate over the lines
502
 
            for line in vf.iter_lines_added_or_present_in_versions(versions):
 
566
            for line in vf.iter_lines_added_or_present_in_versions(versions,
 
567
                pb=progress):
 
568
                lines.setdefault(line, 0)
503
569
                lines[line] += 1
 
570
            if []!= progress.updates:
 
571
                self.assertEqual(expected, progress.updates)
504
572
            return lines
505
 
        lines = iter_with_versions(['child', 'otherchild'])
 
573
        lines = iter_with_versions(['child', 'otherchild'],
 
574
                                   [('Walking content.', 0, 2),
 
575
                                    ('Walking content.', 1, 2),
 
576
                                    ('Walking content.', 2, 2)])
506
577
        # we must see child and otherchild
507
 
        self.assertTrue(lines['child\n'] > 0)
508
 
        self.assertTrue(lines['otherchild\n'] > 0)
 
578
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
579
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
509
580
        # we dont care if we got more than that.
510
581
        
511
582
        # test all lines
512
 
        lines = iter_with_versions(None)
 
583
        lines = iter_with_versions(None, [('Walking content.', 0, 5),
 
584
                                          ('Walking content.', 1, 5),
 
585
                                          ('Walking content.', 2, 5),
 
586
                                          ('Walking content.', 3, 5),
 
587
                                          ('Walking content.', 4, 5),
 
588
                                          ('Walking content.', 5, 5)])
513
589
        # all lines must be seen at least once
514
 
        self.assertTrue(lines['base\n'] > 0)
515
 
        self.assertTrue(lines['lancestor\n'] > 0)
516
 
        self.assertTrue(lines['rancestor\n'] > 0)
517
 
        self.assertTrue(lines['child\n'] > 0)
518
 
        self.assertTrue(lines['otherchild\n'] > 0)
519
 
 
520
 
    def test_fix_parents(self):
521
 
        # some versioned files allow incorrect parents to be corrected after
522
 
        # insertion - this may not fix ancestry..
523
 
        # if they do not supported, they just do not implement it.
524
 
        # we test this as an interface test to ensure that those that *do*
525
 
        # implementent it get it right.
526
 
        vf = self.get_file()
527
 
        vf.add_lines('notbase', [], [])
528
 
        vf.add_lines('base', [], [])
529
 
        try:
530
 
            vf.fix_parents('notbase', ['base'])
531
 
        except NotImplementedError:
532
 
            return
533
 
        self.assertEqual(['base'], vf.get_parents('notbase'))
534
 
        # open again, check it stuck.
535
 
        vf = self.get_file()
536
 
        self.assertEqual(['base'], vf.get_parents('notbase'))
537
 
 
538
 
    def test_fix_parents_with_ghosts(self):
539
 
        # when fixing parents, ghosts that are listed should not be ghosts
540
 
        # anymore.
541
 
        vf = self.get_file()
542
 
 
543
 
        try:
544
 
            vf.add_lines_with_ghosts('notbase', ['base', 'stillghost'], [])
545
 
        except NotImplementedError:
546
 
            return
547
 
        vf.add_lines('base', [], [])
548
 
        vf.fix_parents('notbase', ['base', 'stillghost'])
549
 
        self.assertEqual(['base'], vf.get_parents('notbase'))
550
 
        # open again, check it stuck.
551
 
        vf = self.get_file()
552
 
        self.assertEqual(['base'], vf.get_parents('notbase'))
553
 
        # and check the ghosts
554
 
        self.assertEqual(['base', 'stillghost'],
555
 
                         vf.get_parents_with_ghosts('notbase'))
 
590
        self.assertTrue(lines[('base\n', 'base')] > 0)
 
591
        self.assertTrue(lines[('lancestor\n', 'lancestor')] > 0)
 
592
        self.assertTrue(lines[('rancestor\n', 'rancestor')] > 0)
 
593
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
594
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
556
595
 
557
596
    def test_add_lines_with_ghosts(self):
558
597
        # some versioned file formats allow lines to be added with parent
561
600
        # add_lines_with_ghosts api.
562
601
        vf = self.get_file()
563
602
        # add a revision with ghost parents
 
603
        # The preferred form is utf8, but we should translate when needed
 
604
        parent_id_unicode = u'b\xbfse'
 
605
        parent_id_utf8 = parent_id_unicode.encode('utf8')
564
606
        try:
565
 
            vf.add_lines_with_ghosts(u'notbxbfse', [u'b\xbfse'], [])
 
607
            vf.add_lines_with_ghosts('notbxbfse', [parent_id_utf8], [])
566
608
        except NotImplementedError:
567
609
            # check the other ghost apis are also not implemented
568
610
            self.assertRaises(NotImplementedError, vf.has_ghost, 'foo')
570
612
            self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
571
613
            self.assertRaises(NotImplementedError, vf.get_graph_with_ghosts)
572
614
            return
 
615
        vf = self.reopen_file()
573
616
        # test key graph related apis: getncestry, _graph, get_parents
574
617
        # has_version
575
618
        # - these are ghost unaware and must not be reflect ghosts
576
 
        self.assertEqual([u'notbxbfse'], vf.get_ancestry(u'notbxbfse'))
577
 
        self.assertEqual([], vf.get_parents(u'notbxbfse'))
578
 
        self.assertEqual({u'notbxbfse':[]}, vf.get_graph())
579
 
        self.assertFalse(vf.has_version(u'b\xbfse'))
 
619
        self.assertEqual(['notbxbfse'], vf.get_ancestry('notbxbfse'))
 
620
        self.assertEqual([], vf.get_parents('notbxbfse'))
 
621
        self.assertEqual({'notbxbfse':()}, vf.get_graph())
 
622
        self.assertFalse(vf.has_version(parent_id_utf8))
580
623
        # we have _with_ghost apis to give us ghost information.
581
 
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
582
 
        self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
583
 
        self.assertEqual({u'notbxbfse':[u'b\xbfse']}, vf.get_graph_with_ghosts())
584
 
        self.assertTrue(vf.has_ghost(u'b\xbfse'))
 
624
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
 
625
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
626
        self.assertEqual({'notbxbfse':[parent_id_utf8]}, vf.get_graph_with_ghosts())
 
627
        self.assertTrue(vf.has_ghost(parent_id_utf8))
585
628
        # if we add something that is a ghost of another, it should correct the
586
629
        # results of the prior apis
587
 
        vf.add_lines(u'b\xbfse', [], [])
588
 
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry([u'notbxbfse']))
589
 
        self.assertEqual([u'b\xbfse'], vf.get_parents(u'notbxbfse'))
590
 
        self.assertEqual({u'b\xbfse':[],
591
 
                          u'notbxbfse':[u'b\xbfse'],
 
630
        vf.add_lines(parent_id_utf8, [], [])
 
631
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry(['notbxbfse']))
 
632
        self.assertEqual([parent_id_utf8], vf.get_parents('notbxbfse'))
 
633
        self.assertEqual({parent_id_utf8:(),
 
634
                          'notbxbfse':(parent_id_utf8, ),
592
635
                          },
593
636
                         vf.get_graph())
594
 
        self.assertTrue(vf.has_version(u'b\xbfse'))
 
637
        self.assertTrue(vf.has_version(parent_id_utf8))
595
638
        # we have _with_ghost apis to give us ghost information.
596
 
        self.assertEqual([u'b\xbfse', u'notbxbfse'], vf.get_ancestry_with_ghosts([u'notbxbfse']))
597
 
        self.assertEqual([u'b\xbfse'], vf.get_parents_with_ghosts(u'notbxbfse'))
598
 
        self.assertEqual({u'b\xbfse':[],
599
 
                          u'notbxbfse':[u'b\xbfse'],
 
639
        self.assertEqual([parent_id_utf8, 'notbxbfse'],
 
640
            vf.get_ancestry_with_ghosts(['notbxbfse']))
 
641
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
642
        self.assertEqual({parent_id_utf8:[],
 
643
                          'notbxbfse':[parent_id_utf8],
600
644
                          },
601
645
                         vf.get_graph_with_ghosts())
602
 
        self.assertFalse(vf.has_ghost(u'b\xbfse'))
 
646
        self.assertFalse(vf.has_ghost(parent_id_utf8))
603
647
 
604
648
    def test_add_lines_with_ghosts_after_normal_revs(self):
605
649
        # some versioned file formats allow lines to be added with parent
626
670
        factory = self.get_factory()
627
671
        vf = factory('id', transport, 0777, create=True, access_mode='w')
628
672
        vf = factory('id', transport, access_mode='r')
629
 
        self.assertRaises(errors.ReadOnlyError, vf.add_delta, '', [], '', '', False, [])
630
673
        self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
631
674
        self.assertRaises(errors.ReadOnlyError,
632
675
                          vf.add_lines_with_ghosts,
633
676
                          'base',
634
677
                          [],
635
678
                          [])
636
 
        self.assertRaises(errors.ReadOnlyError, vf.fix_parents, 'base', [])
637
679
        self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
638
680
        self.assertRaises(errors.ReadOnlyError, vf.clone_text, 'base', 'bar', ['foo'])
 
681
    
 
682
    def test_get_sha1(self):
 
683
        # check the sha1 data is available
 
684
        vf = self.get_file()
 
685
        # a simple file
 
686
        vf.add_lines('a', [], ['a\n'])
 
687
        # the same file, different metadata
 
688
        vf.add_lines('b', ['a'], ['a\n'])
 
689
        # a file differing only in last newline.
 
690
        vf.add_lines('c', [], ['a'])
 
691
        self.assertEqual(
 
692
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('a'))
 
693
        self.assertEqual(
 
694
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('b'))
 
695
        self.assertEqual(
 
696
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', vf.get_sha1('c'))
 
697
 
 
698
        self.assertEqual(['3f786850e387550fdab836ed7e6dc881de23001b',
 
699
                          '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
700
                          '3f786850e387550fdab836ed7e6dc881de23001b'],
 
701
                          vf.get_sha1s(['a', 'c', 'b']))
639
702
        
640
703
 
641
 
class TestWeave(TestCaseWithTransport, VersionedFileTestMixIn):
 
704
class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
642
705
 
643
706
    def get_file(self, name='foo'):
644
707
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
677
740
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
678
741
        return w
679
742
 
680
 
    def reopen_file(self, name='foo'):
681
 
        return WeaveFile(name, get_transport(self.get_url('.')))
 
743
    def reopen_file(self, name='foo', create=False):
 
744
        return WeaveFile(name, get_transport(self.get_url('.')), create=create)
682
745
 
683
746
    def test_no_implicit_create(self):
684
747
        self.assertRaises(errors.NoSuchFile,
690
753
        return WeaveFile
691
754
 
692
755
 
693
 
class TestKnit(TestCaseWithTransport, VersionedFileTestMixIn):
 
756
class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
694
757
 
695
758
    def get_file(self, name='foo'):
696
 
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
697
 
                                 delta=True, create=True)
 
759
        return self.get_factory()(name, get_transport(self.get_url('.')),
 
760
                                  delta=True, create=True)
698
761
 
699
762
    def get_factory(self):
700
763
        return KnitVersionedFile
705
768
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
706
769
        return knit
707
770
 
708
 
    def reopen_file(self, name='foo'):
709
 
        return KnitVersionedFile(name, get_transport(self.get_url('.')), delta=True)
 
771
    def reopen_file(self, name='foo', create=False):
 
772
        return self.get_factory()(name, get_transport(self.get_url('.')),
 
773
            delta=True,
 
774
            create=create)
710
775
 
711
776
    def test_detection(self):
712
 
        print "TODO for merging: create a corrupted knit."
713
777
        knit = self.get_file()
714
778
        knit.check()
715
779
 
720
784
                          get_transport(self.get_url('.')))
721
785
 
722
786
 
 
787
class TestPlaintextKnit(TestKnit):
 
788
    """Test a knit with no cached annotations"""
 
789
 
 
790
    def _factory(self, name, transport, file_mode=None, access_mode=None,
 
791
                 delta=True, create=False):
 
792
        return KnitVersionedFile(name, transport, file_mode, access_mode,
 
793
                                 KnitPlainFactory(), delta=delta,
 
794
                                 create=create)
 
795
 
 
796
    def get_factory(self):
 
797
        return self._factory
 
798
 
 
799
 
 
800
class TestPlanMergeVersionedFile(TestCaseWithMemoryTransport):
 
801
 
 
802
    def setUp(self):
 
803
        TestCaseWithMemoryTransport.setUp(self)
 
804
        self.vf1 = KnitVersionedFile('root', self.get_transport(), create=True)
 
805
        self.vf2 = KnitVersionedFile('root', self.get_transport(), create=True)
 
806
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root',
 
807
            [self.vf1, self.vf2])
 
808
 
 
809
    def test_add_lines(self):
 
810
        self.plan_merge_vf.add_lines('a:', [], [])
 
811
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a', [],
 
812
                          [])
 
813
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', None,
 
814
                          [])
 
815
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', [],
 
816
                          None)
 
817
 
 
818
    def test_ancestry(self):
 
819
        self.vf1.add_lines('A', [], [])
 
820
        self.vf1.add_lines('B', ['A'], [])
 
821
        self.plan_merge_vf.add_lines('C:', ['B'], [])
 
822
        self.plan_merge_vf.add_lines('D:', ['C:'], [])
 
823
        self.assertEqual(set(['A', 'B', 'C:', 'D:']),
 
824
            self.plan_merge_vf.get_ancestry('D:', topo_sorted=False))
 
825
 
 
826
    def setup_abcde(self):
 
827
        self.vf1.add_lines('A', [], ['a'])
 
828
        self.vf1.add_lines('B', ['A'], ['b'])
 
829
        self.vf2.add_lines('C', [], ['c'])
 
830
        self.vf2.add_lines('D', ['C'], ['d'])
 
831
        self.plan_merge_vf.add_lines('E:', ['B', 'D'], ['e'])
 
832
 
 
833
    def test_ancestry_uses_all_versionedfiles(self):
 
834
        self.setup_abcde()
 
835
        self.assertEqual(set(['A', 'B', 'C', 'D', 'E:']),
 
836
            self.plan_merge_vf.get_ancestry('E:', topo_sorted=False))
 
837
 
 
838
    def test_ancestry_raises_revision_not_present(self):
 
839
        error = self.assertRaises(errors.RevisionNotPresent,
 
840
                                  self.plan_merge_vf.get_ancestry, 'E:', False)
 
841
        self.assertContainsRe(str(error), '{E:} not present in "root"')
 
842
 
 
843
    def test_get_parents(self):
 
844
        self.setup_abcde()
 
845
        self.assertEqual(['A'], self.plan_merge_vf.get_parents('B'))
 
846
        self.assertEqual(['C'], self.plan_merge_vf.get_parents('D'))
 
847
        self.assertEqual(['B', 'D'], self.plan_merge_vf.get_parents('E:'))
 
848
        error = self.assertRaises(errors.RevisionNotPresent,
 
849
                                  self.plan_merge_vf.get_parents, 'F')
 
850
        self.assertContainsRe(str(error), '{F} not present in "root"')
 
851
 
 
852
    def test_get_lines(self):
 
853
        self.setup_abcde()
 
854
        self.assertEqual(['a'], self.plan_merge_vf.get_lines('A'))
 
855
        self.assertEqual(['c'], self.plan_merge_vf.get_lines('C'))
 
856
        self.assertEqual(['e'], self.plan_merge_vf.get_lines('E:'))
 
857
        error = self.assertRaises(errors.RevisionNotPresent,
 
858
                                  self.plan_merge_vf.get_lines, 'F')
 
859
        self.assertContainsRe(str(error), '{F} not present in "root"')
 
860
 
 
861
 
723
862
class InterString(versionedfile.InterVersionedFile):
724
863
    """An inter-versionedfile optimised code path for strings.
725
864
 
738
877
# if we make the registry a separate class though we still need to 
739
878
# test the behaviour in the active registry to catch failure-to-handle-
740
879
# stange-objects
741
 
class TestInterVersionedFile(TestCaseWithTransport):
 
880
class TestInterVersionedFile(TestCaseWithMemoryTransport):
742
881
 
743
882
    def test_get_default_inter_versionedfile(self):
744
883
        # test that the InterVersionedFile.get(a, b) probes
783
922
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
784
923
        # now we should get the default InterVersionedFile object again.
785
924
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
925
 
 
926
 
 
927
class TestReadonlyHttpMixin(object):
 
928
 
 
929
    def test_readonly_http_works(self):
 
930
        # we should be able to read from http with a versioned file.
 
931
        vf = self.get_file()
 
932
        # try an empty file access
 
933
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
934
        self.assertEqual([], readonly_vf.versions())
 
935
        # now with feeling.
 
936
        vf.add_lines('1', [], ['a\n'])
 
937
        vf.add_lines('2', ['1'], ['b\n', 'a\n'])
 
938
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
939
        self.assertEqual(['1', '2'], vf.versions())
 
940
        for version in readonly_vf.versions():
 
941
            readonly_vf.get_lines(version)
 
942
 
 
943
 
 
944
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
945
 
 
946
    def get_file(self):
 
947
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True)
 
948
 
 
949
    def get_factory(self):
 
950
        return WeaveFile
 
951
 
 
952
 
 
953
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
954
 
 
955
    def get_file(self):
 
956
        return KnitVersionedFile('foo', get_transport(self.get_url('.')),
 
957
                                 delta=True, create=True)
 
958
 
 
959
    def get_factory(self):
 
960
        return KnitVersionedFile
 
961
 
 
962
 
 
963
class MergeCasesMixin(object):
 
964
 
 
965
    def doMerge(self, base, a, b, mp):
 
966
        from cStringIO import StringIO
 
967
        from textwrap import dedent
 
968
 
 
969
        def addcrlf(x):
 
970
            return x + '\n'
 
971
        
 
972
        w = self.get_file()
 
973
        w.add_lines('text0', [], map(addcrlf, base))
 
974
        w.add_lines('text1', ['text0'], map(addcrlf, a))
 
975
        w.add_lines('text2', ['text0'], map(addcrlf, b))
 
976
 
 
977
        self.log_contents(w)
 
978
 
 
979
        self.log('merge plan:')
 
980
        p = list(w.plan_merge('text1', 'text2'))
 
981
        for state, line in p:
 
982
            if line:
 
983
                self.log('%12s | %s' % (state, line[:-1]))
 
984
 
 
985
        self.log('merge:')
 
986
        mt = StringIO()
 
987
        mt.writelines(w.weave_merge(p))
 
988
        mt.seek(0)
 
989
        self.log(mt.getvalue())
 
990
 
 
991
        mp = map(addcrlf, mp)
 
992
        self.assertEqual(mt.readlines(), mp)
 
993
        
 
994
        
 
995
    def testOneInsert(self):
 
996
        self.doMerge([],
 
997
                     ['aa'],
 
998
                     [],
 
999
                     ['aa'])
 
1000
 
 
1001
    def testSeparateInserts(self):
 
1002
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1003
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1004
                     ['aaa', 'bbb', 'yyy', 'ccc'],
 
1005
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1006
 
 
1007
    def testSameInsert(self):
 
1008
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1009
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1010
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
 
1011
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1012
    overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
 
1013
    def testOverlappedInsert(self):
 
1014
        self.doMerge(['aaa', 'bbb'],
 
1015
                     ['aaa', 'xxx', 'yyy', 'bbb'],
 
1016
                     ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
 
1017
 
 
1018
        # really it ought to reduce this to 
 
1019
        # ['aaa', 'xxx', 'yyy', 'bbb']
 
1020
 
 
1021
 
 
1022
    def testClashReplace(self):
 
1023
        self.doMerge(['aaa'],
 
1024
                     ['xxx'],
 
1025
                     ['yyy', 'zzz'],
 
1026
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
 
1027
                      '>>>>>>> '])
 
1028
 
 
1029
    def testNonClashInsert1(self):
 
1030
        self.doMerge(['aaa'],
 
1031
                     ['xxx', 'aaa'],
 
1032
                     ['yyy', 'zzz'],
 
1033
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
 
1034
                      '>>>>>>> '])
 
1035
 
 
1036
    def testNonClashInsert2(self):
 
1037
        self.doMerge(['aaa'],
 
1038
                     ['aaa'],
 
1039
                     ['yyy', 'zzz'],
 
1040
                     ['yyy', 'zzz'])
 
1041
 
 
1042
 
 
1043
    def testDeleteAndModify(self):
 
1044
        """Clashing delete and modification.
 
1045
 
 
1046
        If one side modifies a region and the other deletes it then
 
1047
        there should be a conflict with one side blank.
 
1048
        """
 
1049
 
 
1050
        #######################################
 
1051
        # skippd, not working yet
 
1052
        return
 
1053
        
 
1054
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1055
                     ['aaa', 'ddd', 'ccc'],
 
1056
                     ['aaa', 'ccc'],
 
1057
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
 
1058
 
 
1059
    def _test_merge_from_strings(self, base, a, b, expected):
 
1060
        w = self.get_file()
 
1061
        w.add_lines('text0', [], base.splitlines(True))
 
1062
        w.add_lines('text1', ['text0'], a.splitlines(True))
 
1063
        w.add_lines('text2', ['text0'], b.splitlines(True))
 
1064
        self.log('merge plan:')
 
1065
        p = list(w.plan_merge('text1', 'text2'))
 
1066
        for state, line in p:
 
1067
            if line:
 
1068
                self.log('%12s | %s' % (state, line[:-1]))
 
1069
        self.log('merge result:')
 
1070
        result_text = ''.join(w.weave_merge(p))
 
1071
        self.log(result_text)
 
1072
        self.assertEqualDiff(result_text, expected)
 
1073
 
 
1074
    def test_weave_merge_conflicts(self):
 
1075
        # does weave merge properly handle plans that end with unchanged?
 
1076
        result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
 
1077
        self.assertEqual(result, 'hello\n')
 
1078
 
 
1079
    def test_deletion_extended(self):
 
1080
        """One side deletes, the other deletes more.
 
1081
        """
 
1082
        base = """\
 
1083
            line 1
 
1084
            line 2
 
1085
            line 3
 
1086
            """
 
1087
        a = """\
 
1088
            line 1
 
1089
            line 2
 
1090
            """
 
1091
        b = """\
 
1092
            line 1
 
1093
            """
 
1094
        result = """\
 
1095
            line 1
 
1096
            """
 
1097
        self._test_merge_from_strings(base, a, b, result)
 
1098
 
 
1099
    def test_deletion_overlap(self):
 
1100
        """Delete overlapping regions with no other conflict.
 
1101
 
 
1102
        Arguably it'd be better to treat these as agreement, rather than 
 
1103
        conflict, but for now conflict is safer.
 
1104
        """
 
1105
        base = """\
 
1106
            start context
 
1107
            int a() {}
 
1108
            int b() {}
 
1109
            int c() {}
 
1110
            end context
 
1111
            """
 
1112
        a = """\
 
1113
            start context
 
1114
            int a() {}
 
1115
            end context
 
1116
            """
 
1117
        b = """\
 
1118
            start context
 
1119
            int c() {}
 
1120
            end context
 
1121
            """
 
1122
        result = """\
 
1123
            start context
 
1124
<<<<<<< 
 
1125
            int a() {}
 
1126
=======
 
1127
            int c() {}
 
1128
>>>>>>> 
 
1129
            end context
 
1130
            """
 
1131
        self._test_merge_from_strings(base, a, b, result)
 
1132
 
 
1133
    def test_agreement_deletion(self):
 
1134
        """Agree to delete some lines, without conflicts."""
 
1135
        base = """\
 
1136
            start context
 
1137
            base line 1
 
1138
            base line 2
 
1139
            end context
 
1140
            """
 
1141
        a = """\
 
1142
            start context
 
1143
            base line 1
 
1144
            end context
 
1145
            """
 
1146
        b = """\
 
1147
            start context
 
1148
            base line 1
 
1149
            end context
 
1150
            """
 
1151
        result = """\
 
1152
            start context
 
1153
            base line 1
 
1154
            end context
 
1155
            """
 
1156
        self._test_merge_from_strings(base, a, b, result)
 
1157
 
 
1158
    def test_sync_on_deletion(self):
 
1159
        """Specific case of merge where we can synchronize incorrectly.
 
1160
        
 
1161
        A previous version of the weave merge concluded that the two versions
 
1162
        agreed on deleting line 2, and this could be a synchronization point.
 
1163
        Line 1 was then considered in isolation, and thought to be deleted on 
 
1164
        both sides.
 
1165
 
 
1166
        It's better to consider the whole thing as a disagreement region.
 
1167
        """
 
1168
        base = """\
 
1169
            start context
 
1170
            base line 1
 
1171
            base line 2
 
1172
            end context
 
1173
            """
 
1174
        a = """\
 
1175
            start context
 
1176
            base line 1
 
1177
            a's replacement line 2
 
1178
            end context
 
1179
            """
 
1180
        b = """\
 
1181
            start context
 
1182
            b replaces
 
1183
            both lines
 
1184
            end context
 
1185
            """
 
1186
        result = """\
 
1187
            start context
 
1188
<<<<<<< 
 
1189
            base line 1
 
1190
            a's replacement line 2
 
1191
=======
 
1192
            b replaces
 
1193
            both lines
 
1194
>>>>>>> 
 
1195
            end context
 
1196
            """
 
1197
        self._test_merge_from_strings(base, a, b, result)
 
1198
 
 
1199
 
 
1200
class TestKnitMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1201
 
 
1202
    def get_file(self, name='foo'):
 
1203
        return KnitVersionedFile(name, get_transport(self.get_url('.')),
 
1204
                                 delta=True, create=True)
 
1205
 
 
1206
    def log_contents(self, w):
 
1207
        pass
 
1208
 
 
1209
 
 
1210
class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1211
 
 
1212
    def get_file(self, name='foo'):
 
1213
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
1214
 
 
1215
    def log_contents(self, w):
 
1216
        self.log('weave is:')
 
1217
        tmpf = StringIO()
 
1218
        write_weave(w, tmpf)
 
1219
        self.log(tmpf.getvalue())
 
1220
 
 
1221
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
 
1222
                                'xxx', '>>>>>>> ', 'bbb']
 
1223
 
 
1224
 
 
1225
class TestFormatSignatures(TestCaseWithMemoryTransport):
 
1226
 
 
1227
    def get_knit_file(self, name, annotated):
 
1228
        if annotated:
 
1229
            factory = KnitAnnotateFactory()
 
1230
        else:
 
1231
            factory = KnitPlainFactory()
 
1232
        return KnitVersionedFile(
 
1233
            name, get_transport(self.get_url('.')), create=True,
 
1234
            factory=factory)
 
1235
 
 
1236
    def test_knit_format_signatures(self):
 
1237
        """Different formats of knit have different signature strings."""
 
1238
        knit = self.get_knit_file('a', True)
 
1239
        self.assertEqual('knit-annotated', knit.get_format_signature())
 
1240
        knit = self.get_knit_file('p', False)
 
1241
        self.assertEqual('knit-plain', knit.get_format_signature())
 
1242