~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: John Arbash Meinel
  • Date: 2007-11-13 20:37:09 UTC
  • mto: This revision was merged to the branch mainline in revision 3001.
  • Revision ID: john@arbash-meinel.com-20071113203709-kysdte0emqv84pnj
Fix bug #162486, by having RemoteBranch properly initialize self._revision_id_to_revno_map.

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