~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: Martin Pool
  • Date: 2005-05-09 04:38:31 UTC
  • Revision ID: mbp@sourcefrog.net-20050509043831-d45f7832b7d4d5b0
- better message when refusing to add symlinks (from mpe)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 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
 
# 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
 
 
26
 
import bzrlib
27
 
from bzrlib import (
28
 
    errors,
29
 
    osutils,
30
 
    progress,
31
 
    )
32
 
from bzrlib.errors import (
33
 
                           RevisionNotPresent, 
34
 
                           RevisionAlreadyPresent,
35
 
                           WeaveParentMismatch
36
 
                           )
37
 
from bzrlib.knit import (
38
 
    KnitVersionedFile,
39
 
    KnitAnnotateFactory,
40
 
    KnitPlainFactory,
41
 
    )
42
 
from bzrlib.tests import TestCaseWithMemoryTransport, TestSkipped
43
 
from bzrlib.tests.HTTPTestUtil import TestCaseWithWebserver
44
 
from bzrlib.trace import mutter
45
 
from bzrlib.transport import get_transport
46
 
from bzrlib.transport.memory import MemoryTransport
47
 
from bzrlib.tsort import topo_sort
48
 
import bzrlib.versionedfile as versionedfile
49
 
from bzrlib.weave import WeaveFile
50
 
from bzrlib.weavefile import read_weave, write_weave
51
 
 
52
 
 
53
 
class VersionedFileTestMixIn(object):
54
 
    """A mixin test class for testing VersionedFiles.
55
 
 
56
 
    This is not an adaptor-style test at this point because
57
 
    theres no dynamic substitution of versioned file implementations,
58
 
    they are strictly controlled by their owning repositories.
59
 
    """
60
 
 
61
 
    def test_add(self):
62
 
        f = self.get_file()
63
 
        f.add_lines('r0', [], ['a\n', 'b\n'])
64
 
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
65
 
        def verify_file(f):
66
 
            versions = f.versions()
67
 
            self.assertTrue('r0' in versions)
68
 
            self.assertTrue('r1' in versions)
69
 
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
70
 
            self.assertEquals(f.get_text('r0'), 'a\nb\n')
71
 
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
72
 
            self.assertEqual(2, len(f))
73
 
            self.assertEqual(2, f.num_versions())
74
 
    
75
 
            self.assertRaises(RevisionNotPresent,
76
 
                f.add_lines, 'r2', ['foo'], [])
77
 
            self.assertRaises(RevisionAlreadyPresent,
78
 
                f.add_lines, 'r1', [], [])
79
 
        verify_file(f)
80
 
        # this checks that reopen with create=True does not break anything.
81
 
        f = self.reopen_file(create=True)
82
 
        verify_file(f)
83
 
 
84
 
    def test_adds_with_parent_texts(self):
85
 
        f = self.get_file()
86
 
        parent_texts = {}
87
 
        _, _, parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
88
 
        try:
89
 
            _, _, parent_texts['r1'] = f.add_lines_with_ghosts('r1',
90
 
                ['r0', 'ghost'], ['b\n', 'c\n'], parent_texts=parent_texts)
91
 
        except NotImplementedError:
92
 
            # if the format doesn't support ghosts, just add normally.
93
 
            _, _, parent_texts['r1'] = f.add_lines('r1',
94
 
                ['r0'], ['b\n', 'c\n'], parent_texts=parent_texts)
95
 
        f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
96
 
        self.assertNotEqual(None, parent_texts['r0'])
97
 
        self.assertNotEqual(None, parent_texts['r1'])
98
 
        def verify_file(f):
99
 
            versions = f.versions()
100
 
            self.assertTrue('r0' in versions)
101
 
            self.assertTrue('r1' in versions)
102
 
            self.assertTrue('r2' in versions)
103
 
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
104
 
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
105
 
            self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
106
 
            self.assertEqual(3, f.num_versions())
107
 
            origins = f.annotate('r1')
108
 
            self.assertEquals(origins[0][0], 'r0')
109
 
            self.assertEquals(origins[1][0], 'r1')
110
 
            origins = f.annotate('r2')
111
 
            self.assertEquals(origins[0][0], 'r1')
112
 
            self.assertEquals(origins[1][0], 'r2')
113
 
 
114
 
        verify_file(f)
115
 
        f = self.reopen_file()
116
 
        verify_file(f)
117
 
 
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))
247
 
 
248
 
    def _setup_for_deltas(self, f):
249
 
        self.assertFalse(f.has_version('base'))
250
 
        # add texts that should trip the knit maximum delta chain threshold
251
 
        # as well as doing parallel chains of data in knits.
252
 
        # this is done by two chains of 25 insertions
253
 
        f.add_lines('base', [], ['line\n'])
254
 
        f.add_lines('noeol', ['base'], ['line'])
255
 
        # detailed eol tests:
256
 
        # shared last line with parent no-eol
257
 
        f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
258
 
        # differing last line with parent, both no-eol
259
 
        f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
260
 
        # add eol following a noneol parent, change content
261
 
        f.add_lines('eol', ['noeol'], ['phone\n'])
262
 
        # add eol following a noneol parent, no change content
263
 
        f.add_lines('eolline', ['noeol'], ['line\n'])
264
 
        # noeol with no parents:
265
 
        f.add_lines('noeolbase', [], ['line'])
266
 
        # noeol preceeding its leftmost parent in the output:
267
 
        # this is done by making it a merge of two parents with no common
268
 
        # anestry: noeolbase and noeol with the 
269
 
        # later-inserted parent the leftmost.
270
 
        f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
271
 
        # two identical eol texts
272
 
        f.add_lines('noeoldup', ['noeol'], ['line'])
273
 
        next_parent = 'base'
274
 
        text_name = 'chain1-'
275
 
        text = ['line\n']
276
 
        sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
277
 
                 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
278
 
                 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
279
 
                 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
280
 
                 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
281
 
                 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
282
 
                 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
283
 
                 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
284
 
                 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
285
 
                 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
286
 
                 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
287
 
                 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
288
 
                 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
289
 
                 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
290
 
                 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
291
 
                 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
292
 
                 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
293
 
                 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
294
 
                 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
295
 
                 19:'1ebed371807ba5935958ad0884595126e8c4e823',
296
 
                 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
297
 
                 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
298
 
                 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
299
 
                 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
300
 
                 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
301
 
                 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
302
 
                 }
303
 
        for depth in range(26):
304
 
            new_version = text_name + '%s' % depth
305
 
            text = text + ['line\n']
306
 
            f.add_lines(new_version, [next_parent], text)
307
 
            next_parent = new_version
308
 
        next_parent = 'base'
309
 
        text_name = 'chain2-'
310
 
        text = ['line\n']
311
 
        for depth in range(26):
312
 
            new_version = text_name + '%s' % depth
313
 
            text = text + ['line\n']
314
 
            f.add_lines(new_version, [next_parent], text)
315
 
            next_parent = new_version
316
 
        return sha1s
317
 
 
318
 
    def test_ancestry(self):
319
 
        f = self.get_file()
320
 
        self.assertEqual([], f.get_ancestry([]))
321
 
        f.add_lines('r0', [], ['a\n', 'b\n'])
322
 
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
323
 
        f.add_lines('r2', ['r0'], ['b\n', 'c\n'])
324
 
        f.add_lines('r3', ['r2'], ['b\n', 'c\n'])
325
 
        f.add_lines('rM', ['r1', 'r2'], ['b\n', 'c\n'])
326
 
        self.assertEqual([], f.get_ancestry([]))
327
 
        versions = f.get_ancestry(['rM'])
328
 
        # there are some possibilities:
329
 
        # r0 r1 r2 rM r3
330
 
        # r0 r1 r2 r3 rM
331
 
        # etc
332
 
        # so we check indexes
333
 
        r0 = versions.index('r0')
334
 
        r1 = versions.index('r1')
335
 
        r2 = versions.index('r2')
336
 
        self.assertFalse('r3' in versions)
337
 
        rM = versions.index('rM')
338
 
        self.assertTrue(r0 < r1)
339
 
        self.assertTrue(r0 < r2)
340
 
        self.assertTrue(r1 < rM)
341
 
        self.assertTrue(r2 < rM)
342
 
 
343
 
        self.assertRaises(RevisionNotPresent,
344
 
            f.get_ancestry, ['rM', 'rX'])
345
 
 
346
 
        self.assertEqual(set(f.get_ancestry('rM')),
347
 
            set(f.get_ancestry('rM', topo_sorted=False)))
348
 
 
349
 
    def test_mutate_after_finish(self):
350
 
        f = self.get_file()
351
 
        f.transaction_finished()
352
 
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
353
 
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
354
 
        self.assertRaises(errors.OutSideTransaction, f.join, '')
355
 
        self.assertRaises(errors.OutSideTransaction, f.clone_text, 'base', 'bar', ['foo'])
356
 
        
357
 
    def test_clear_cache(self):
358
 
        f = self.get_file()
359
 
        # on a new file it should not error
360
 
        f.clear_cache()
361
 
        # and after adding content, doing a clear_cache and a get should work.
362
 
        f.add_lines('0', [], ['a'])
363
 
        f.clear_cache()
364
 
        self.assertEqual(['a'], f.get_lines('0'))
365
 
 
366
 
    def test_clone_text(self):
367
 
        f = self.get_file()
368
 
        f.add_lines('r0', [], ['a\n', 'b\n'])
369
 
        f.clone_text('r1', 'r0', ['r0'])
370
 
        def verify_file(f):
371
 
            self.assertEquals(f.get_lines('r1'), f.get_lines('r0'))
372
 
            self.assertEquals(f.get_lines('r1'), ['a\n', 'b\n'])
373
 
            self.assertEquals(f.get_parents('r1'), ['r0'])
374
 
    
375
 
            self.assertRaises(RevisionNotPresent,
376
 
                f.clone_text, 'r2', 'rX', [])
377
 
            self.assertRaises(RevisionAlreadyPresent,
378
 
                f.clone_text, 'r1', 'r0', [])
379
 
        verify_file(f)
380
 
        verify_file(self.reopen_file())
381
 
 
382
 
    def test_create_empty(self):
383
 
        f = self.get_file()
384
 
        f.add_lines('0', [], ['a\n'])
385
 
        new_f = f.create_empty('t', MemoryTransport())
386
 
        # smoke test, specific types should check it is honoured correctly for
387
 
        # non type attributes
388
 
        self.assertEqual([], new_f.versions())
389
 
        self.assertTrue(isinstance(new_f, f.__class__))
390
 
 
391
 
    def test_copy_to(self):
392
 
        f = self.get_file()
393
 
        f.add_lines('0', [], ['a\n'])
394
 
        t = MemoryTransport()
395
 
        f.copy_to('foo', t)
396
 
        for suffix in f.__class__.get_suffixes():
397
 
            self.assertTrue(t.has('foo' + suffix))
398
 
 
399
 
    def test_get_suffixes(self):
400
 
        f = self.get_file()
401
 
        # should be the same
402
 
        self.assertEqual(f.__class__.get_suffixes(), f.__class__.get_suffixes())
403
 
        # and should be a list
404
 
        self.assertTrue(isinstance(f.__class__.get_suffixes(), list))
405
 
 
406
 
    def build_graph(self, file, graph):
407
 
        for node in topo_sort(graph.items()):
408
 
            file.add_lines(node, graph[node], [])
409
 
 
410
 
    def test_get_graph(self):
411
 
        f = self.get_file()
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']))
448
 
 
449
 
    def test_get_parents(self):
450
 
        f = self.get_file()
451
 
        f.add_lines('r0', [], ['a\n', 'b\n'])
452
 
        f.add_lines('r1', [], ['a\n', 'b\n'])
453
 
        f.add_lines('r2', [], ['a\n', 'b\n'])
454
 
        f.add_lines('r3', [], ['a\n', 'b\n'])
455
 
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
456
 
        self.assertEquals(f.get_parents('m'), ['r0', 'r1', 'r2', 'r3'])
457
 
 
458
 
        self.assertRaises(RevisionNotPresent,
459
 
            f.get_parents, 'y')
460
 
 
461
 
    def test_annotate(self):
462
 
        f = self.get_file()
463
 
        f.add_lines('r0', [], ['a\n', 'b\n'])
464
 
        f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
465
 
        origins = f.annotate('r1')
466
 
        self.assertEquals(origins[0][0], 'r1')
467
 
        self.assertEquals(origins[1][0], 'r0')
468
 
 
469
 
        self.assertRaises(RevisionNotPresent,
470
 
            f.annotate, 'foo')
471
 
 
472
 
    def test_detection(self):
473
 
        # Test weaves detect corruption.
474
 
        #
475
 
        # Weaves contain a checksum of their texts.
476
 
        # When a text is extracted, this checksum should be
477
 
        # verified.
478
 
 
479
 
        w = self.get_file_corrupted_text()
480
 
 
481
 
        self.assertEqual('hello\n', w.get_text('v1'))
482
 
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
483
 
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
484
 
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
485
 
 
486
 
        w = self.get_file_corrupted_checksum()
487
 
 
488
 
        self.assertEqual('hello\n', w.get_text('v1'))
489
 
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
490
 
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
491
 
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
492
 
 
493
 
    def get_file_corrupted_text(self):
494
 
        """Return a versioned file with corrupt text but valid metadata."""
495
 
        raise NotImplementedError(self.get_file_corrupted_text)
496
 
 
497
 
    def reopen_file(self, name='foo'):
498
 
        """Open the versioned file from disk again."""
499
 
        raise NotImplementedError(self.reopen_file)
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
 
 
532
 
    def test_iter_lines_added_or_present_in_versions(self):
533
 
        # test that we get at least an equalset of the lines added by
534
 
        # versions in the weave 
535
 
        # the ordering here is to make a tree so that dumb searches have
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
 
 
548
 
        vf = self.get_file()
549
 
        # add a base to get included
550
 
        vf.add_lines('base', [], ['base\n'])
551
 
        # add a ancestor to be included on one side
552
 
        vf.add_lines('lancestor', [], ['lancestor\n'])
553
 
        # add a ancestor to be included on the other side
554
 
        vf.add_lines('rancestor', ['base'], ['rancestor\n'])
555
 
        # add a child of rancestor with no eofile-nl
556
 
        vf.add_lines('child', ['rancestor'], ['base\n', 'child\n'])
557
 
        # add a child of lancestor and base to join the two roots
558
 
        vf.add_lines('otherchild',
559
 
                     ['lancestor', 'base'],
560
 
                     ['base\n', 'lancestor\n', 'otherchild\n'])
561
 
        def iter_with_versions(versions, expected):
562
 
            # now we need to see what lines are returned, and how often.
563
 
            lines = {'base\n':0,
564
 
                     'lancestor\n':0,
565
 
                     'rancestor\n':0,
566
 
                     'child\n':0,
567
 
                     'otherchild\n':0,
568
 
                     }
569
 
            progress = InstrumentedProgress()
570
 
            # iterate over the lines
571
 
            for line in vf.iter_lines_added_or_present_in_versions(versions, 
572
 
                pb=progress):
573
 
                lines[line] += 1
574
 
            if []!= progress.updates: 
575
 
                self.assertEqual(expected, progress.updates)
576
 
            return lines
577
 
        lines = iter_with_versions(['child', 'otherchild'],
578
 
                                   [('Walking content.', 0, 2),
579
 
                                    ('Walking content.', 1, 2),
580
 
                                    ('Walking content.', 2, 2)])
581
 
        # we must see child and otherchild
582
 
        self.assertTrue(lines['child\n'] > 0)
583
 
        self.assertTrue(lines['otherchild\n'] > 0)
584
 
        # we dont care if we got more than that.
585
 
        
586
 
        # test all lines
587
 
        lines = iter_with_versions(None, [('Walking content.', 0, 5),
588
 
                                          ('Walking content.', 1, 5),
589
 
                                          ('Walking content.', 2, 5),
590
 
                                          ('Walking content.', 3, 5),
591
 
                                          ('Walking content.', 4, 5),
592
 
                                          ('Walking content.', 5, 5)])
593
 
        # all lines must be seen at least once
594
 
        self.assertTrue(lines['base\n'] > 0)
595
 
        self.assertTrue(lines['lancestor\n'] > 0)
596
 
        self.assertTrue(lines['rancestor\n'] > 0)
597
 
        self.assertTrue(lines['child\n'] > 0)
598
 
        self.assertTrue(lines['otherchild\n'] > 0)
599
 
 
600
 
    def test_add_lines_with_ghosts(self):
601
 
        # some versioned file formats allow lines to be added with parent
602
 
        # information that is > than that in the format. Formats that do
603
 
        # not support this need to raise NotImplementedError on the
604
 
        # add_lines_with_ghosts api.
605
 
        vf = self.get_file()
606
 
        # add a revision with ghost parents
607
 
        # The preferred form is utf8, but we should translate when needed
608
 
        parent_id_unicode = u'b\xbfse'
609
 
        parent_id_utf8 = parent_id_unicode.encode('utf8')
610
 
        try:
611
 
            vf.add_lines_with_ghosts('notbxbfse', [parent_id_utf8], [])
612
 
        except NotImplementedError:
613
 
            # check the other ghost apis are also not implemented
614
 
            self.assertRaises(NotImplementedError, vf.has_ghost, 'foo')
615
 
            self.assertRaises(NotImplementedError, vf.get_ancestry_with_ghosts, ['foo'])
616
 
            self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
617
 
            self.assertRaises(NotImplementedError, vf.get_graph_with_ghosts)
618
 
            return
619
 
        vf = self.reopen_file()
620
 
        # test key graph related apis: getncestry, _graph, get_parents
621
 
        # has_version
622
 
        # - these are ghost unaware and must not be reflect ghosts
623
 
        self.assertEqual(['notbxbfse'], vf.get_ancestry('notbxbfse'))
624
 
        self.assertEqual([], vf.get_parents('notbxbfse'))
625
 
        self.assertEqual({'notbxbfse':()}, vf.get_graph())
626
 
        self.assertFalse(vf.has_version(parent_id_utf8))
627
 
        # we have _with_ghost apis to give us ghost information.
628
 
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
629
 
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
630
 
        self.assertEqual({'notbxbfse':[parent_id_utf8]}, vf.get_graph_with_ghosts())
631
 
        self.assertTrue(vf.has_ghost(parent_id_utf8))
632
 
        # if we add something that is a ghost of another, it should correct the
633
 
        # results of the prior apis
634
 
        vf.add_lines(parent_id_utf8, [], [])
635
 
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry(['notbxbfse']))
636
 
        self.assertEqual([parent_id_utf8], vf.get_parents('notbxbfse'))
637
 
        self.assertEqual({parent_id_utf8:(),
638
 
                          'notbxbfse':(parent_id_utf8, ),
639
 
                          },
640
 
                         vf.get_graph())
641
 
        self.assertTrue(vf.has_version(parent_id_utf8))
642
 
        # we have _with_ghost apis to give us ghost information.
643
 
        self.assertEqual([parent_id_utf8, 'notbxbfse'],
644
 
            vf.get_ancestry_with_ghosts(['notbxbfse']))
645
 
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
646
 
        self.assertEqual({parent_id_utf8:[],
647
 
                          'notbxbfse':[parent_id_utf8],
648
 
                          },
649
 
                         vf.get_graph_with_ghosts())
650
 
        self.assertFalse(vf.has_ghost(parent_id_utf8))
651
 
 
652
 
    def test_add_lines_with_ghosts_after_normal_revs(self):
653
 
        # some versioned file formats allow lines to be added with parent
654
 
        # information that is > than that in the format. Formats that do
655
 
        # not support this need to raise NotImplementedError on the
656
 
        # add_lines_with_ghosts api.
657
 
        vf = self.get_file()
658
 
        # probe for ghost support
659
 
        try:
660
 
            vf.has_ghost('hoo')
661
 
        except NotImplementedError:
662
 
            return
663
 
        vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
664
 
        vf.add_lines_with_ghosts('references_ghost',
665
 
                                 ['base', 'a_ghost'],
666
 
                                 ['line\n', 'line_b\n', 'line_c\n'])
667
 
        origins = vf.annotate('references_ghost')
668
 
        self.assertEquals(('base', 'line\n'), origins[0])
669
 
        self.assertEquals(('base', 'line_b\n'), origins[1])
670
 
        self.assertEquals(('references_ghost', 'line_c\n'), origins[2])
671
 
 
672
 
    def test_readonly_mode(self):
673
 
        transport = get_transport(self.get_url('.'))
674
 
        factory = self.get_factory()
675
 
        vf = factory('id', transport, 0777, create=True, access_mode='w')
676
 
        vf = factory('id', transport, access_mode='r')
677
 
        self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
678
 
        self.assertRaises(errors.ReadOnlyError,
679
 
                          vf.add_lines_with_ghosts,
680
 
                          'base',
681
 
                          [],
682
 
                          [])
683
 
        self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
684
 
        self.assertRaises(errors.ReadOnlyError, vf.clone_text, 'base', 'bar', ['foo'])
685
 
    
686
 
    def test_get_sha1(self):
687
 
        # check the sha1 data is available
688
 
        vf = self.get_file()
689
 
        # a simple file
690
 
        vf.add_lines('a', [], ['a\n'])
691
 
        # the same file, different metadata
692
 
        vf.add_lines('b', ['a'], ['a\n'])
693
 
        # a file differing only in last newline.
694
 
        vf.add_lines('c', [], ['a'])
695
 
        self.assertEqual(
696
 
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('a'))
697
 
        self.assertEqual(
698
 
            '3f786850e387550fdab836ed7e6dc881de23001b', vf.get_sha1('b'))
699
 
        self.assertEqual(
700
 
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', vf.get_sha1('c'))
701
 
 
702
 
        self.assertEqual(['3f786850e387550fdab836ed7e6dc881de23001b',
703
 
                          '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
704
 
                          '3f786850e387550fdab836ed7e6dc881de23001b'],
705
 
                          vf.get_sha1s(['a', 'c', 'b']))
706
 
        
707
 
 
708
 
class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
709
 
 
710
 
    def get_file(self, name='foo'):
711
 
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
712
 
 
713
 
    def get_file_corrupted_text(self):
714
 
        w = WeaveFile('foo', get_transport(self.get_url('.')), create=True)
715
 
        w.add_lines('v1', [], ['hello\n'])
716
 
        w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
717
 
        
718
 
        # We are going to invasively corrupt the text
719
 
        # Make sure the internals of weave are the same
720
 
        self.assertEqual([('{', 0)
721
 
                        , 'hello\n'
722
 
                        , ('}', None)
723
 
                        , ('{', 1)
724
 
                        , 'there\n'
725
 
                        , ('}', None)
726
 
                        ], w._weave)
727
 
        
728
 
        self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
729
 
                        , '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
730
 
                        ], w._sha1s)
731
 
        w.check()
732
 
        
733
 
        # Corrupted
734
 
        w._weave[4] = 'There\n'
735
 
        return w
736
 
 
737
 
    def get_file_corrupted_checksum(self):
738
 
        w = self.get_file_corrupted_text()
739
 
        # Corrected
740
 
        w._weave[4] = 'there\n'
741
 
        self.assertEqual('hello\nthere\n', w.get_text('v2'))
742
 
        
743
 
        #Invalid checksum, first digit changed
744
 
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
745
 
        return w
746
 
 
747
 
    def reopen_file(self, name='foo', create=False):
748
 
        return WeaveFile(name, get_transport(self.get_url('.')), create=create)
749
 
 
750
 
    def test_no_implicit_create(self):
751
 
        self.assertRaises(errors.NoSuchFile,
752
 
                          WeaveFile,
753
 
                          'foo',
754
 
                          get_transport(self.get_url('.')))
755
 
 
756
 
    def get_factory(self):
757
 
        return WeaveFile
758
 
 
759
 
 
760
 
class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
761
 
 
762
 
    def get_file(self, name='foo'):
763
 
        return self.get_factory()(name, get_transport(self.get_url('.')),
764
 
                                  delta=True, create=True)
765
 
 
766
 
    def get_factory(self):
767
 
        return KnitVersionedFile
768
 
 
769
 
    def get_file_corrupted_text(self):
770
 
        knit = self.get_file()
771
 
        knit.add_lines('v1', [], ['hello\n'])
772
 
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
773
 
        return knit
774
 
 
775
 
    def reopen_file(self, name='foo', create=False):
776
 
        return self.get_factory()(name, get_transport(self.get_url('.')),
777
 
            delta=True,
778
 
            create=create)
779
 
 
780
 
    def test_detection(self):
781
 
        knit = self.get_file()
782
 
        knit.check()
783
 
 
784
 
    def test_no_implicit_create(self):
785
 
        self.assertRaises(errors.NoSuchFile,
786
 
                          KnitVersionedFile,
787
 
                          'foo',
788
 
                          get_transport(self.get_url('.')))
789
 
 
790
 
 
791
 
class TestPlaintextKnit(TestKnit):
792
 
    """Test a knit with no cached annotations"""
793
 
 
794
 
    def _factory(self, name, transport, file_mode=None, access_mode=None,
795
 
                 delta=True, create=False):
796
 
        return KnitVersionedFile(name, transport, file_mode, access_mode,
797
 
                                 KnitPlainFactory(), delta=delta,
798
 
                                 create=create)
799
 
 
800
 
    def get_factory(self):
801
 
        return self._factory
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(TestCaseWithMemoryTransport):
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(TestCaseWithMemoryTransport, 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(TestCaseWithMemoryTransport, 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']
1165
 
 
1166
 
 
1167
 
class TestFormatSignatures(TestCaseWithMemoryTransport):
1168
 
 
1169
 
    def get_knit_file(self, name, annotated):
1170
 
        if annotated:
1171
 
            factory = KnitAnnotateFactory()
1172
 
        else:
1173
 
            factory = KnitPlainFactory()
1174
 
        return KnitVersionedFile(
1175
 
            name, get_transport(self.get_url('.')), create=True,
1176
 
            factory=factory)
1177
 
 
1178
 
    def test_knit_format_signatures(self):
1179
 
        """Different formats of knit have different signature strings."""
1180
 
        knit = self.get_knit_file('a', True)
1181
 
        self.assertEqual('knit-annotated', knit.get_format_signature())
1182
 
        knit = self.get_knit_file('p', False)
1183
 
        self.assertEqual('knit-plain', knit.get_format_signature())
1184