~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: Tim Penhey
  • Date: 2008-04-25 11:23:00 UTC
  • mto: (3473.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3474.
  • Revision ID: tim@penhey.net-20080425112300-sf5soa5dg2d37kvc
Added tests.

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
    make_file_knit,
 
39
    KnitAnnotateFactory,
 
40
    KnitPlainFactory,
 
41
    )
 
42
from bzrlib.symbol_versioning import one_four
 
43
from bzrlib.tests import TestCaseWithMemoryTransport, TestSkipped
 
44
from bzrlib.tests.http_utils import TestCaseWithWebserver
 
45
from bzrlib.trace import mutter
 
46
from bzrlib.transport import get_transport
 
47
from bzrlib.transport.memory import MemoryTransport
 
48
from bzrlib.tsort import topo_sort
 
49
import bzrlib.versionedfile as versionedfile
 
50
from bzrlib.weave import WeaveFile
 
51
from bzrlib.weavefile import read_weave, write_weave
 
52
 
 
53
 
 
54
class VersionedFileTestMixIn(object):
 
55
    """A mixin test class for testing VersionedFiles.
 
56
 
 
57
    This is not an adaptor-style test at this point because
 
58
    theres no dynamic substitution of versioned file implementations,
 
59
    they are strictly controlled by their owning repositories.
 
60
    """
 
61
 
 
62
    def get_transaction(self):
 
63
        if not hasattr(self, '_transaction'):
 
64
            self._transaction = None
 
65
        return self._transaction
 
66
 
 
67
    def test_add(self):
 
68
        f = self.get_file()
 
69
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
70
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
 
71
        def verify_file(f):
 
72
            versions = f.versions()
 
73
            self.assertTrue('r0' in versions)
 
74
            self.assertTrue('r1' in versions)
 
75
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
 
76
            self.assertEquals(f.get_text('r0'), 'a\nb\n')
 
77
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
 
78
            self.assertEqual(2, len(f))
 
79
            self.assertEqual(2, f.num_versions())
 
80
    
 
81
            self.assertRaises(RevisionNotPresent,
 
82
                f.add_lines, 'r2', ['foo'], [])
 
83
            self.assertRaises(RevisionAlreadyPresent,
 
84
                f.add_lines, 'r1', [], [])
 
85
        verify_file(f)
 
86
        # this checks that reopen with create=True does not break anything.
 
87
        f = self.reopen_file(create=True)
 
88
        verify_file(f)
 
89
 
 
90
    def test_adds_with_parent_texts(self):
 
91
        f = self.get_file()
 
92
        parent_texts = {}
 
93
        _, _, parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
 
94
        try:
 
95
            _, _, parent_texts['r1'] = f.add_lines_with_ghosts('r1',
 
96
                ['r0', 'ghost'], ['b\n', 'c\n'], parent_texts=parent_texts)
 
97
        except NotImplementedError:
 
98
            # if the format doesn't support ghosts, just add normally.
 
99
            _, _, parent_texts['r1'] = f.add_lines('r1',
 
100
                ['r0'], ['b\n', 'c\n'], parent_texts=parent_texts)
 
101
        f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
 
102
        self.assertNotEqual(None, parent_texts['r0'])
 
103
        self.assertNotEqual(None, parent_texts['r1'])
 
104
        def verify_file(f):
 
105
            versions = f.versions()
 
106
            self.assertTrue('r0' in versions)
 
107
            self.assertTrue('r1' in versions)
 
108
            self.assertTrue('r2' in versions)
 
109
            self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
 
110
            self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
 
111
            self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
 
112
            self.assertEqual(3, f.num_versions())
 
113
            origins = f.annotate('r1')
 
114
            self.assertEquals(origins[0][0], 'r0')
 
115
            self.assertEquals(origins[1][0], 'r1')
 
116
            origins = f.annotate('r2')
 
117
            self.assertEquals(origins[0][0], 'r1')
 
118
            self.assertEquals(origins[1][0], 'r2')
 
119
 
 
120
        verify_file(f)
 
121
        f = self.reopen_file()
 
122
        verify_file(f)
 
123
 
 
124
    def test_add_unicode_content(self):
 
125
        # unicode content is not permitted in versioned files. 
 
126
        # versioned files version sequences of bytes only.
 
127
        vf = self.get_file()
 
128
        self.assertRaises(errors.BzrBadParameterUnicode,
 
129
            vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
 
130
        self.assertRaises(
 
131
            (errors.BzrBadParameterUnicode, NotImplementedError),
 
132
            vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
 
133
 
 
134
    def test_add_follows_left_matching_blocks(self):
 
135
        """If we change left_matching_blocks, delta changes
 
136
 
 
137
        Note: There are multiple correct deltas in this case, because
 
138
        we start with 1 "a" and we get 3.
 
139
        """
 
140
        vf = self.get_file()
 
141
        if isinstance(vf, WeaveFile):
 
142
            raise TestSkipped("WeaveFile ignores left_matching_blocks")
 
143
        vf.add_lines('1', [], ['a\n'])
 
144
        vf.add_lines('2', ['1'], ['a\n', 'a\n', 'a\n'],
 
145
                     left_matching_blocks=[(0, 0, 1), (1, 3, 0)])
 
146
        self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('2'))
 
147
        vf.add_lines('3', ['1'], ['a\n', 'a\n', 'a\n'],
 
148
                     left_matching_blocks=[(0, 2, 1), (1, 3, 0)])
 
149
        self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('3'))
 
150
 
 
151
    def test_inline_newline_throws(self):
 
152
        # \r characters are not permitted in lines being added
 
153
        vf = self.get_file()
 
154
        self.assertRaises(errors.BzrBadParameterContainsNewline, 
 
155
            vf.add_lines, 'a', [], ['a\n\n'])
 
156
        self.assertRaises(
 
157
            (errors.BzrBadParameterContainsNewline, NotImplementedError),
 
158
            vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
 
159
        # but inline CR's are allowed
 
160
        vf.add_lines('a', [], ['a\r\n'])
 
161
        try:
 
162
            vf.add_lines_with_ghosts('b', [], ['a\r\n'])
 
163
        except NotImplementedError:
 
164
            pass
 
165
 
 
166
    def test_add_reserved(self):
 
167
        vf = self.get_file()
 
168
        self.assertRaises(errors.ReservedId,
 
169
            vf.add_lines, 'a:', [], ['a\n', 'b\n', 'c\n'])
 
170
 
 
171
    def test_add_lines_nostoresha(self):
 
172
        """When nostore_sha is supplied using old content raises."""
 
173
        vf = self.get_file()
 
174
        empty_text = ('a', [])
 
175
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
176
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
177
        shas = []
 
178
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
179
            sha, _, _ = vf.add_lines(version, [], lines)
 
180
            shas.append(sha)
 
181
        # we now have a copy of all the lines in the vf.
 
182
        for sha, (version, lines) in zip(
 
183
            shas, (empty_text, sample_text_nl, sample_text_no_nl)):
 
184
            self.assertRaises(errors.ExistingContent,
 
185
                vf.add_lines, version + "2", [], lines,
 
186
                nostore_sha=sha)
 
187
            # and no new version should have been added.
 
188
            self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
 
189
                version + "2")
 
190
 
 
191
    def test_add_lines_with_ghosts_nostoresha(self):
 
192
        """When nostore_sha is supplied using old content raises."""
 
193
        vf = self.get_file()
 
194
        empty_text = ('a', [])
 
195
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
196
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
197
        shas = []
 
198
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
199
            sha, _, _ = vf.add_lines(version, [], lines)
 
200
            shas.append(sha)
 
201
        # we now have a copy of all the lines in the vf.
 
202
        # is the test applicable to this vf implementation?
 
203
        try:
 
204
            vf.add_lines_with_ghosts('d', [], [])
 
205
        except NotImplementedError:
 
206
            raise TestSkipped("add_lines_with_ghosts is optional")
 
207
        for sha, (version, lines) in zip(
 
208
            shas, (empty_text, sample_text_nl, sample_text_no_nl)):
 
209
            self.assertRaises(errors.ExistingContent,
 
210
                vf.add_lines_with_ghosts, version + "2", [], lines,
 
211
                nostore_sha=sha)
 
212
            # and no new version should have been added.
 
213
            self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
 
214
                version + "2")
 
215
 
 
216
    def test_add_lines_return_value(self):
 
217
        # add_lines should return the sha1 and the text size.
 
218
        vf = self.get_file()
 
219
        empty_text = ('a', [])
 
220
        sample_text_nl = ('b', ["foo\n", "bar\n"])
 
221
        sample_text_no_nl = ('c', ["foo\n", "bar"])
 
222
        # check results for the three cases:
 
223
        for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
 
224
            # the first two elements are the same for all versioned files:
 
225
            # - the digest and the size of the text. For some versioned files
 
226
            #   additional data is returned in additional tuple elements.
 
227
            result = vf.add_lines(version, [], lines)
 
228
            self.assertEqual(3, len(result))
 
229
            self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
 
230
                result[0:2])
 
231
        # parents should not affect the result:
 
232
        lines = sample_text_nl[1]
 
233
        self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
 
234
            vf.add_lines('d', ['b', 'c'], lines)[0:2])
 
235
 
 
236
    def test_get_reserved(self):
 
237
        vf = self.get_file()
 
238
        self.assertRaises(errors.ReservedId, vf.get_texts, ['b:'])
 
239
        self.assertRaises(errors.ReservedId, vf.get_lines, 'b:')
 
240
        self.assertRaises(errors.ReservedId, vf.get_text, 'b:')
 
241
 
 
242
    def test_make_mpdiffs(self):
 
243
        from bzrlib import multiparent
 
244
        vf = self.get_file('foo')
 
245
        sha1s = self._setup_for_deltas(vf)
 
246
        new_vf = self.get_file('bar')
 
247
        for version in multiparent.topo_iter(vf):
 
248
            mpdiff = vf.make_mpdiffs([version])[0]
 
249
            new_vf.add_mpdiffs([(version, vf.get_parent_map([version])[version],
 
250
                                 vf.get_sha1s([version])[0], mpdiff)])
 
251
            self.assertEqualDiff(vf.get_text(version),
 
252
                                 new_vf.get_text(version))
 
253
 
 
254
    def _setup_for_deltas(self, f):
 
255
        self.assertFalse(f.has_version('base'))
 
256
        # add texts that should trip the knit maximum delta chain threshold
 
257
        # as well as doing parallel chains of data in knits.
 
258
        # this is done by two chains of 25 insertions
 
259
        f.add_lines('base', [], ['line\n'])
 
260
        f.add_lines('noeol', ['base'], ['line'])
 
261
        # detailed eol tests:
 
262
        # shared last line with parent no-eol
 
263
        f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
 
264
        # differing last line with parent, both no-eol
 
265
        f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
 
266
        # add eol following a noneol parent, change content
 
267
        f.add_lines('eol', ['noeol'], ['phone\n'])
 
268
        # add eol following a noneol parent, no change content
 
269
        f.add_lines('eolline', ['noeol'], ['line\n'])
 
270
        # noeol with no parents:
 
271
        f.add_lines('noeolbase', [], ['line'])
 
272
        # noeol preceeding its leftmost parent in the output:
 
273
        # this is done by making it a merge of two parents with no common
 
274
        # anestry: noeolbase and noeol with the 
 
275
        # later-inserted parent the leftmost.
 
276
        f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
 
277
        # two identical eol texts
 
278
        f.add_lines('noeoldup', ['noeol'], ['line'])
 
279
        next_parent = 'base'
 
280
        text_name = 'chain1-'
 
281
        text = ['line\n']
 
282
        sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
 
283
                 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
 
284
                 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
 
285
                 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
 
286
                 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
 
287
                 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
 
288
                 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
 
289
                 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
 
290
                 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
 
291
                 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
 
292
                 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
 
293
                 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
 
294
                 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
 
295
                 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
 
296
                 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
 
297
                 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
 
298
                 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
 
299
                 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
 
300
                 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
 
301
                 19:'1ebed371807ba5935958ad0884595126e8c4e823',
 
302
                 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
 
303
                 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
 
304
                 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
 
305
                 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
 
306
                 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
 
307
                 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
 
308
                 }
 
309
        for depth in range(26):
 
310
            new_version = text_name + '%s' % depth
 
311
            text = text + ['line\n']
 
312
            f.add_lines(new_version, [next_parent], text)
 
313
            next_parent = new_version
 
314
        next_parent = 'base'
 
315
        text_name = 'chain2-'
 
316
        text = ['line\n']
 
317
        for depth in range(26):
 
318
            new_version = text_name + '%s' % depth
 
319
            text = text + ['line\n']
 
320
            f.add_lines(new_version, [next_parent], text)
 
321
            next_parent = new_version
 
322
        return sha1s
 
323
 
 
324
    def test_ancestry(self):
 
325
        f = self.get_file()
 
326
        self.assertEqual([], f.get_ancestry([]))
 
327
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
328
        f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
 
329
        f.add_lines('r2', ['r0'], ['b\n', 'c\n'])
 
330
        f.add_lines('r3', ['r2'], ['b\n', 'c\n'])
 
331
        f.add_lines('rM', ['r1', 'r2'], ['b\n', 'c\n'])
 
332
        self.assertEqual([], f.get_ancestry([]))
 
333
        versions = f.get_ancestry(['rM'])
 
334
        # there are some possibilities:
 
335
        # r0 r1 r2 rM r3
 
336
        # r0 r1 r2 r3 rM
 
337
        # etc
 
338
        # so we check indexes
 
339
        r0 = versions.index('r0')
 
340
        r1 = versions.index('r1')
 
341
        r2 = versions.index('r2')
 
342
        self.assertFalse('r3' in versions)
 
343
        rM = versions.index('rM')
 
344
        self.assertTrue(r0 < r1)
 
345
        self.assertTrue(r0 < r2)
 
346
        self.assertTrue(r1 < rM)
 
347
        self.assertTrue(r2 < rM)
 
348
 
 
349
        self.assertRaises(RevisionNotPresent,
 
350
            f.get_ancestry, ['rM', 'rX'])
 
351
 
 
352
        self.assertEqual(set(f.get_ancestry('rM')),
 
353
            set(f.get_ancestry('rM', topo_sorted=False)))
 
354
 
 
355
    def test_mutate_after_finish(self):
 
356
        self._transaction = 'before'
 
357
        f = self.get_file()
 
358
        self._transaction = 'after'
 
359
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
 
360
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
 
361
        self.assertRaises(errors.OutSideTransaction, f.join, '')
 
362
        
 
363
    def test_clone_text(self):
 
364
        f = self.get_file()
 
365
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
366
        self.applyDeprecated(one_four, f.clone_text, 'r1', 'r0', ['r0'])
 
367
        def verify_file(f):
 
368
            self.assertEquals(f.get_lines('r1'), f.get_lines('r0'))
 
369
            self.assertEquals(f.get_lines('r1'), ['a\n', 'b\n'])
 
370
            self.assertEqual({'r1':('r0',)}, f.get_parent_map(['r1']))
 
371
            self.assertRaises(RevisionNotPresent,
 
372
                self.applyDeprecated, one_four, f.clone_text, 'r2', 'rX', [])
 
373
            self.assertRaises(RevisionAlreadyPresent,
 
374
                self.applyDeprecated, one_four, f.clone_text, 'r1', 'r0', [])
 
375
        verify_file(f)
 
376
        verify_file(self.reopen_file())
 
377
 
 
378
    def test_copy_to(self):
 
379
        f = self.get_file()
 
380
        f.add_lines('0', [], ['a\n'])
 
381
        t = MemoryTransport()
 
382
        f.copy_to('foo', t)
 
383
        for suffix in self.get_factory().get_suffixes():
 
384
            self.assertTrue(t.has('foo' + suffix))
 
385
 
 
386
    def test_get_suffixes(self):
 
387
        f = self.get_file()
 
388
        # and should be a list
 
389
        self.assertTrue(isinstance(self.get_factory().get_suffixes(), list))
 
390
 
 
391
    def build_graph(self, file, graph):
 
392
        for node in topo_sort(graph.items()):
 
393
            file.add_lines(node, graph[node], [])
 
394
 
 
395
    def test_get_graph(self):
 
396
        f = self.get_file()
 
397
        graph = {
 
398
            'v1': (),
 
399
            'v2': ('v1', ),
 
400
            'v3': ('v2', )}
 
401
        self.build_graph(f, graph)
 
402
        self.assertEqual(graph, self.applyDeprecated(one_four, f.get_graph))
 
403
    
 
404
    def test_get_graph_partial(self):
 
405
        f = self.get_file()
 
406
        complex_graph = {}
 
407
        simple_a = {
 
408
            'c': (),
 
409
            'b': ('c', ),
 
410
            'a': ('b', ),
 
411
            }
 
412
        complex_graph.update(simple_a)
 
413
        simple_b = {
 
414
            'c': (),
 
415
            'b': ('c', ),
 
416
            }
 
417
        complex_graph.update(simple_b)
 
418
        simple_gam = {
 
419
            'c': (),
 
420
            'oo': (),
 
421
            'bar': ('oo', 'c'),
 
422
            'gam': ('bar', ),
 
423
            }
 
424
        complex_graph.update(simple_gam)
 
425
        simple_b_gam = {}
 
426
        simple_b_gam.update(simple_gam)
 
427
        simple_b_gam.update(simple_b)
 
428
        self.build_graph(f, complex_graph)
 
429
        self.assertEqual(simple_a, self.applyDeprecated(one_four, f.get_graph,
 
430
            ['a']))
 
431
        self.assertEqual(simple_b, self.applyDeprecated(one_four, f.get_graph,
 
432
            ['b']))
 
433
        self.assertEqual(simple_gam, self.applyDeprecated(one_four,
 
434
            f.get_graph, ['gam']))
 
435
        self.assertEqual(simple_b_gam, self.applyDeprecated(one_four,
 
436
            f.get_graph, ['b', 'gam']))
 
437
 
 
438
    def test_get_parents(self):
 
439
        f = self.get_file()
 
440
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
441
        f.add_lines('r1', [], ['a\n', 'b\n'])
 
442
        f.add_lines('r2', [], ['a\n', 'b\n'])
 
443
        f.add_lines('r3', [], ['a\n', 'b\n'])
 
444
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
 
445
        self.assertEqual(['r0', 'r1', 'r2', 'r3'],
 
446
            self.applyDeprecated(one_four, f.get_parents, 'm'))
 
447
        self.assertRaises(RevisionNotPresent,
 
448
            self.applyDeprecated, one_four, f.get_parents, 'y')
 
449
 
 
450
    def test_get_parent_map(self):
 
451
        f = self.get_file()
 
452
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
453
        self.assertEqual(
 
454
            {'r0':()}, f.get_parent_map(['r0']))
 
455
        f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
 
456
        self.assertEqual(
 
457
            {'r1':('r0',)}, f.get_parent_map(['r1']))
 
458
        self.assertEqual(
 
459
            {'r0':(),
 
460
             'r1':('r0',)},
 
461
            f.get_parent_map(['r0', 'r1']))
 
462
        f.add_lines('r2', [], ['a\n', 'b\n'])
 
463
        f.add_lines('r3', [], ['a\n', 'b\n'])
 
464
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
 
465
        self.assertEqual(
 
466
            {'m':('r0', 'r1', 'r2', 'r3')}, f.get_parent_map(['m']))
 
467
        self.assertEqual({}, f.get_parent_map('y'))
 
468
        self.assertEqual(
 
469
            {'r0':(),
 
470
             'r1':('r0',)},
 
471
            f.get_parent_map(['r0', 'y', 'r1']))
 
472
 
 
473
    def test_annotate(self):
 
474
        f = self.get_file()
 
475
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
476
        f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
 
477
        origins = f.annotate('r1')
 
478
        self.assertEquals(origins[0][0], 'r1')
 
479
        self.assertEquals(origins[1][0], 'r0')
 
480
 
 
481
        self.assertRaises(RevisionNotPresent,
 
482
            f.annotate, 'foo')
 
483
 
 
484
    def test_detection(self):
 
485
        # Test weaves detect corruption.
 
486
        #
 
487
        # Weaves contain a checksum of their texts.
 
488
        # When a text is extracted, this checksum should be
 
489
        # verified.
 
490
 
 
491
        w = self.get_file_corrupted_text()
 
492
 
 
493
        self.assertEqual('hello\n', w.get_text('v1'))
 
494
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
495
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
496
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
497
 
 
498
        w = self.get_file_corrupted_checksum()
 
499
 
 
500
        self.assertEqual('hello\n', w.get_text('v1'))
 
501
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
502
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
503
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
504
 
 
505
    def get_file_corrupted_text(self):
 
506
        """Return a versioned file with corrupt text but valid metadata."""
 
507
        raise NotImplementedError(self.get_file_corrupted_text)
 
508
 
 
509
    def reopen_file(self, name='foo'):
 
510
        """Open the versioned file from disk again."""
 
511
        raise NotImplementedError(self.reopen_file)
 
512
 
 
513
    def test_iter_parents(self):
 
514
        """iter_parents returns the parents for many nodes."""
 
515
        f = self.get_file()
 
516
        # sample data:
 
517
        # no parents
 
518
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
519
        # 1 parents
 
520
        f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
 
521
        # 2 parents
 
522
        f.add_lines('r2', ['r1', 'r0'], ['a\n', 'b\n'])
 
523
        # XXX TODO a ghost
 
524
        # cases: each sample data individually:
 
525
        self.assertEqual(set([('r0', ())]),
 
526
            set(self.applyDeprecated(one_four, f.iter_parents, ['r0'])))
 
527
        self.assertEqual(set([('r1', ('r0', ))]),
 
528
            set(self.applyDeprecated(one_four, f.iter_parents, ['r1'])))
 
529
        self.assertEqual(set([('r2', ('r1', 'r0'))]),
 
530
            set(self.applyDeprecated(one_four, f.iter_parents, ['r2'])))
 
531
        # no nodes returned for a missing node
 
532
        self.assertEqual(set(),
 
533
            set(self.applyDeprecated(one_four, f.iter_parents, ['missing'])))
 
534
        # 1 node returned with missing nodes skipped
 
535
        self.assertEqual(set([('r1', ('r0', ))]),
 
536
            set(self.applyDeprecated(one_four, f.iter_parents, ['ghost1', 'r1',
 
537
                'ghost'])))
 
538
        # 2 nodes returned
 
539
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
540
            set(self.applyDeprecated(one_four, f.iter_parents, ['r0', 'r1'])))
 
541
        # 2 nodes returned, missing skipped
 
542
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
543
            set(self.applyDeprecated(one_four, f.iter_parents,
 
544
                ['a', 'r0', 'b', 'r1', 'c'])))
 
545
 
 
546
    def test_iter_lines_added_or_present_in_versions(self):
 
547
        # test that we get at least an equalset of the lines added by
 
548
        # versions in the weave 
 
549
        # the ordering here is to make a tree so that dumb searches have
 
550
        # more changes to muck up.
 
551
 
 
552
        class InstrumentedProgress(progress.DummyProgress):
 
553
 
 
554
            def __init__(self):
 
555
 
 
556
                progress.DummyProgress.__init__(self)
 
557
                self.updates = []
 
558
 
 
559
            def update(self, msg=None, current=None, total=None):
 
560
                self.updates.append((msg, current, total))
 
561
 
 
562
        vf = self.get_file()
 
563
        # add a base to get included
 
564
        vf.add_lines('base', [], ['base\n'])
 
565
        # add a ancestor to be included on one side
 
566
        vf.add_lines('lancestor', [], ['lancestor\n'])
 
567
        # add a ancestor to be included on the other side
 
568
        vf.add_lines('rancestor', ['base'], ['rancestor\n'])
 
569
        # add a child of rancestor with no eofile-nl
 
570
        vf.add_lines('child', ['rancestor'], ['base\n', 'child\n'])
 
571
        # add a child of lancestor and base to join the two roots
 
572
        vf.add_lines('otherchild',
 
573
                     ['lancestor', 'base'],
 
574
                     ['base\n', 'lancestor\n', 'otherchild\n'])
 
575
        def iter_with_versions(versions, expected):
 
576
            # now we need to see what lines are returned, and how often.
 
577
            lines = {}
 
578
            progress = InstrumentedProgress()
 
579
            # iterate over the lines
 
580
            for line in vf.iter_lines_added_or_present_in_versions(versions,
 
581
                pb=progress):
 
582
                lines.setdefault(line, 0)
 
583
                lines[line] += 1
 
584
            if []!= progress.updates:
 
585
                self.assertEqual(expected, progress.updates)
 
586
            return lines
 
587
        lines = iter_with_versions(['child', 'otherchild'],
 
588
                                   [('Walking content.', 0, 2),
 
589
                                    ('Walking content.', 1, 2),
 
590
                                    ('Walking content.', 2, 2)])
 
591
        # we must see child and otherchild
 
592
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
593
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
 
594
        # we dont care if we got more than that.
 
595
        
 
596
        # test all lines
 
597
        lines = iter_with_versions(None, [('Walking content.', 0, 5),
 
598
                                          ('Walking content.', 1, 5),
 
599
                                          ('Walking content.', 2, 5),
 
600
                                          ('Walking content.', 3, 5),
 
601
                                          ('Walking content.', 4, 5),
 
602
                                          ('Walking content.', 5, 5)])
 
603
        # all lines must be seen at least once
 
604
        self.assertTrue(lines[('base\n', 'base')] > 0)
 
605
        self.assertTrue(lines[('lancestor\n', 'lancestor')] > 0)
 
606
        self.assertTrue(lines[('rancestor\n', 'rancestor')] > 0)
 
607
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
608
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
 
609
 
 
610
    def test_add_lines_with_ghosts(self):
 
611
        # some versioned file formats allow lines to be added with parent
 
612
        # information that is > than that in the format. Formats that do
 
613
        # not support this need to raise NotImplementedError on the
 
614
        # add_lines_with_ghosts api.
 
615
        vf = self.get_file()
 
616
        # add a revision with ghost parents
 
617
        # The preferred form is utf8, but we should translate when needed
 
618
        parent_id_unicode = u'b\xbfse'
 
619
        parent_id_utf8 = parent_id_unicode.encode('utf8')
 
620
        try:
 
621
            vf.add_lines_with_ghosts('notbxbfse', [parent_id_utf8], [])
 
622
        except NotImplementedError:
 
623
            # check the other ghost apis are also not implemented
 
624
            self.assertRaises(NotImplementedError, vf.get_ancestry_with_ghosts, ['foo'])
 
625
            self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
 
626
            return
 
627
        vf = self.reopen_file()
 
628
        # test key graph related apis: getncestry, _graph, get_parents
 
629
        # has_version
 
630
        # - these are ghost unaware and must not be reflect ghosts
 
631
        self.assertEqual(['notbxbfse'], vf.get_ancestry('notbxbfse'))
 
632
        self.assertEqual([],
 
633
            self.applyDeprecated(one_four, vf.get_parents, 'notbxbfse'))
 
634
        self.assertEqual({'notbxbfse':()}, self.applyDeprecated(one_four,
 
635
            vf.get_graph))
 
636
        self.assertFalse(vf.has_version(parent_id_utf8))
 
637
        # we have _with_ghost apis to give us ghost information.
 
638
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
 
639
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
640
        self.assertEqual({'notbxbfse':(parent_id_utf8,)},
 
641
            self.applyDeprecated(one_four, vf.get_graph_with_ghosts))
 
642
        self.assertTrue(self.applyDeprecated(one_four, vf.has_ghost,
 
643
            parent_id_utf8))
 
644
        # if we add something that is a ghost of another, it should correct the
 
645
        # results of the prior apis
 
646
        vf.add_lines(parent_id_utf8, [], [])
 
647
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry(['notbxbfse']))
 
648
        self.assertEqual({'notbxbfse':(parent_id_utf8,)},
 
649
            vf.get_parent_map(['notbxbfse']))
 
650
        self.assertEqual({parent_id_utf8:(),
 
651
                          'notbxbfse':(parent_id_utf8, ),
 
652
                          },
 
653
                         self.applyDeprecated(one_four, vf.get_graph))
 
654
        self.assertTrue(vf.has_version(parent_id_utf8))
 
655
        # we have _with_ghost apis to give us ghost information.
 
656
        self.assertEqual([parent_id_utf8, 'notbxbfse'],
 
657
            vf.get_ancestry_with_ghosts(['notbxbfse']))
 
658
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
659
        self.assertEqual({parent_id_utf8:(),
 
660
                          'notbxbfse':(parent_id_utf8,),
 
661
                          },
 
662
            self.applyDeprecated(one_four, vf.get_graph_with_ghosts))
 
663
        self.assertFalse(self.applyDeprecated(one_four, vf.has_ghost,
 
664
            parent_id_utf8))
 
665
 
 
666
    def test_add_lines_with_ghosts_after_normal_revs(self):
 
667
        # some versioned file formats allow lines to be added with parent
 
668
        # information that is > than that in the format. Formats that do
 
669
        # not support this need to raise NotImplementedError on the
 
670
        # add_lines_with_ghosts api.
 
671
        vf = self.get_file()
 
672
        # probe for ghost support
 
673
        try:
 
674
            vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
 
675
        except NotImplementedError:
 
676
            return
 
677
        vf.add_lines_with_ghosts('references_ghost',
 
678
                                 ['base', 'a_ghost'],
 
679
                                 ['line\n', 'line_b\n', 'line_c\n'])
 
680
        origins = vf.annotate('references_ghost')
 
681
        self.assertEquals(('base', 'line\n'), origins[0])
 
682
        self.assertEquals(('base', 'line_b\n'), origins[1])
 
683
        self.assertEquals(('references_ghost', 'line_c\n'), origins[2])
 
684
 
 
685
    def test_readonly_mode(self):
 
686
        transport = get_transport(self.get_url('.'))
 
687
        factory = self.get_factory()
 
688
        vf = factory('id', transport, 0777, create=True, access_mode='w')
 
689
        vf = factory('id', transport, access_mode='r')
 
690
        self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
 
691
        self.assertRaises(errors.ReadOnlyError,
 
692
                          vf.add_lines_with_ghosts,
 
693
                          'base',
 
694
                          [],
 
695
                          [])
 
696
        self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
 
697
    
 
698
    def test_get_sha1s(self):
 
699
        # check the sha1 data is available
 
700
        vf = self.get_file()
 
701
        # a simple file
 
702
        vf.add_lines('a', [], ['a\n'])
 
703
        # the same file, different metadata
 
704
        vf.add_lines('b', ['a'], ['a\n'])
 
705
        # a file differing only in last newline.
 
706
        vf.add_lines('c', [], ['a'])
 
707
        # Deprecasted single-version API.
 
708
        self.assertEqual(
 
709
            '3f786850e387550fdab836ed7e6dc881de23001b',
 
710
            self.applyDeprecated(one_four, vf.get_sha1, 'a'))
 
711
        self.assertEqual(
 
712
            '3f786850e387550fdab836ed7e6dc881de23001b',
 
713
            self.applyDeprecated(one_four, vf.get_sha1, 'b'))
 
714
        self.assertEqual(
 
715
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
716
            self.applyDeprecated(one_four, vf.get_sha1, 'c'))
 
717
        self.assertEqual(['3f786850e387550fdab836ed7e6dc881de23001b',
 
718
                          '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
719
                          '3f786850e387550fdab836ed7e6dc881de23001b'],
 
720
                          vf.get_sha1s(['a', 'c', 'b']))
 
721
        
 
722
 
 
723
class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
724
 
 
725
    def get_file(self, name='foo'):
 
726
        return WeaveFile(name, get_transport(self.get_url('.')), create=True,
 
727
            get_scope=self.get_transaction)
 
728
 
 
729
    def get_file_corrupted_text(self):
 
730
        w = WeaveFile('foo', get_transport(self.get_url('.')), create=True,
 
731
            get_scope=self.get_transaction)
 
732
        w.add_lines('v1', [], ['hello\n'])
 
733
        w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
734
        
 
735
        # We are going to invasively corrupt the text
 
736
        # Make sure the internals of weave are the same
 
737
        self.assertEqual([('{', 0)
 
738
                        , 'hello\n'
 
739
                        , ('}', None)
 
740
                        , ('{', 1)
 
741
                        , 'there\n'
 
742
                        , ('}', None)
 
743
                        ], w._weave)
 
744
        
 
745
        self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
 
746
                        , '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
747
                        ], w._sha1s)
 
748
        w.check()
 
749
        
 
750
        # Corrupted
 
751
        w._weave[4] = 'There\n'
 
752
        return w
 
753
 
 
754
    def get_file_corrupted_checksum(self):
 
755
        w = self.get_file_corrupted_text()
 
756
        # Corrected
 
757
        w._weave[4] = 'there\n'
 
758
        self.assertEqual('hello\nthere\n', w.get_text('v2'))
 
759
        
 
760
        #Invalid checksum, first digit changed
 
761
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
762
        return w
 
763
 
 
764
    def reopen_file(self, name='foo', create=False):
 
765
        return WeaveFile(name, get_transport(self.get_url('.')), create=create,
 
766
            get_scope=self.get_transaction)
 
767
 
 
768
    def test_no_implicit_create(self):
 
769
        self.assertRaises(errors.NoSuchFile,
 
770
                          WeaveFile,
 
771
                          'foo',
 
772
                          get_transport(self.get_url('.')),
 
773
                          get_scope=self.get_transaction)
 
774
 
 
775
    def get_factory(self):
 
776
        return WeaveFile
 
777
 
 
778
 
 
779
class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
780
 
 
781
    def get_file(self, name='foo'):
 
782
        return self.get_factory()(name, get_transport(self.get_url('.')),
 
783
            delta=True, create=True, get_scope=self.get_transaction)
 
784
 
 
785
    def get_factory(self):
 
786
        return make_file_knit
 
787
 
 
788
    def get_file_corrupted_text(self):
 
789
        knit = self.get_file()
 
790
        knit.add_lines('v1', [], ['hello\n'])
 
791
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
792
        return knit
 
793
 
 
794
    def reopen_file(self, name='foo', create=False):
 
795
        return self.get_factory()(name, get_transport(self.get_url('.')),
 
796
            delta=True,
 
797
            create=create)
 
798
 
 
799
    def test_detection(self):
 
800
        knit = self.get_file()
 
801
        knit.check()
 
802
 
 
803
    def test_no_implicit_create(self):
 
804
        self.assertRaises(errors.NoSuchFile, self.get_factory(), 'foo',
 
805
            get_transport(self.get_url('.')))
 
806
 
 
807
 
 
808
class TestPlaintextKnit(TestKnit):
 
809
    """Test a knit with no cached annotations"""
 
810
 
 
811
    def get_factory(self):
 
812
        return make_file_knit
 
813
 
 
814
 
 
815
class TestPlanMergeVersionedFile(TestCaseWithMemoryTransport):
 
816
 
 
817
    def setUp(self):
 
818
        TestCaseWithMemoryTransport.setUp(self)
 
819
        self.vf1 = make_file_knit('root', self.get_transport(), create=True)
 
820
        self.vf2 = make_file_knit('root', self.get_transport(), create=True)
 
821
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root',
 
822
            [self.vf1, self.vf2])
 
823
 
 
824
    def test_add_lines(self):
 
825
        self.plan_merge_vf.add_lines('a:', [], [])
 
826
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a', [],
 
827
                          [])
 
828
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', None,
 
829
                          [])
 
830
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', [],
 
831
                          None)
 
832
 
 
833
    def test_ancestry(self):
 
834
        self.vf1.add_lines('A', [], [])
 
835
        self.vf1.add_lines('B', ['A'], [])
 
836
        self.plan_merge_vf.add_lines('C:', ['B'], [])
 
837
        self.plan_merge_vf.add_lines('D:', ['C:'], [])
 
838
        self.assertEqual(set(['A', 'B', 'C:', 'D:']),
 
839
            self.plan_merge_vf.get_ancestry('D:', topo_sorted=False))
 
840
 
 
841
    def setup_abcde(self):
 
842
        self.vf1.add_lines('A', [], ['a'])
 
843
        self.vf1.add_lines('B', ['A'], ['b'])
 
844
        self.vf2.add_lines('C', [], ['c'])
 
845
        self.vf2.add_lines('D', ['C'], ['d'])
 
846
        self.plan_merge_vf.add_lines('E:', ['B', 'D'], ['e'])
 
847
 
 
848
    def test_ancestry_uses_all_versionedfiles(self):
 
849
        self.setup_abcde()
 
850
        self.assertEqual(set(['A', 'B', 'C', 'D', 'E:']),
 
851
            self.plan_merge_vf.get_ancestry('E:', topo_sorted=False))
 
852
 
 
853
    def test_ancestry_raises_revision_not_present(self):
 
854
        error = self.assertRaises(errors.RevisionNotPresent,
 
855
                                  self.plan_merge_vf.get_ancestry, 'E:', False)
 
856
        self.assertContainsRe(str(error), '{E:} not present in "root"')
 
857
 
 
858
    def test_get_parents(self):
 
859
        self.setup_abcde()
 
860
        self.assertEqual({'B':('A',)}, self.plan_merge_vf.get_parent_map(['B']))
 
861
        self.assertEqual({'D':('C',)}, self.plan_merge_vf.get_parent_map(['D']))
 
862
        self.assertEqual({'E:':('B', 'D')},
 
863
            self.plan_merge_vf.get_parent_map(['E:']))
 
864
        self.assertEqual({}, self.plan_merge_vf.get_parent_map(['F']))
 
865
        self.assertEqual({
 
866
                'B':('A',),
 
867
                'D':('C',),
 
868
                'E:':('B', 'D'),
 
869
                }, self.plan_merge_vf.get_parent_map(['B', 'D', 'E:', 'F']))
 
870
 
 
871
    def test_get_lines(self):
 
872
        self.setup_abcde()
 
873
        self.assertEqual(['a'], self.plan_merge_vf.get_lines('A'))
 
874
        self.assertEqual(['c'], self.plan_merge_vf.get_lines('C'))
 
875
        self.assertEqual(['e'], self.plan_merge_vf.get_lines('E:'))
 
876
        error = self.assertRaises(errors.RevisionNotPresent,
 
877
                                  self.plan_merge_vf.get_lines, 'F')
 
878
        self.assertContainsRe(str(error), '{F} not present in "root"')
 
879
 
 
880
 
 
881
class InterString(versionedfile.InterVersionedFile):
 
882
    """An inter-versionedfile optimised code path for strings.
 
883
 
 
884
    This is for use during testing where we use strings as versionedfiles
 
885
    so that none of the default regsitered interversionedfile classes will
 
886
    match - which lets us test the match logic.
 
887
    """
 
888
 
 
889
    @staticmethod
 
890
    def is_compatible(source, target):
 
891
        """InterString is compatible with strings-as-versionedfiles."""
 
892
        return isinstance(source, str) and isinstance(target, str)
 
893
 
 
894
 
 
895
# TODO this and the InterRepository core logic should be consolidatable
 
896
# if we make the registry a separate class though we still need to 
 
897
# test the behaviour in the active registry to catch failure-to-handle-
 
898
# stange-objects
 
899
class TestInterVersionedFile(TestCaseWithMemoryTransport):
 
900
 
 
901
    def test_get_default_inter_versionedfile(self):
 
902
        # test that the InterVersionedFile.get(a, b) probes
 
903
        # for a class where is_compatible(a, b) returns
 
904
        # true and returns a default interversionedfile otherwise.
 
905
        # This also tests that the default registered optimised interversionedfile
 
906
        # classes do not barf inappropriately when a surprising versionedfile type
 
907
        # is handed to them.
 
908
        dummy_a = "VersionedFile 1."
 
909
        dummy_b = "VersionedFile 2."
 
910
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
911
 
 
912
    def assertGetsDefaultInterVersionedFile(self, a, b):
 
913
        """Asserts that InterVersionedFile.get(a, b) -> the default."""
 
914
        inter = versionedfile.InterVersionedFile.get(a, b)
 
915
        self.assertEqual(versionedfile.InterVersionedFile,
 
916
                         inter.__class__)
 
917
        self.assertEqual(a, inter.source)
 
918
        self.assertEqual(b, inter.target)
 
919
 
 
920
    def test_register_inter_versionedfile_class(self):
 
921
        # test that a optimised code path provider - a
 
922
        # InterVersionedFile subclass can be registered and unregistered
 
923
        # and that it is correctly selected when given a versionedfile
 
924
        # pair that it returns true on for the is_compatible static method
 
925
        # check
 
926
        dummy_a = "VersionedFile 1."
 
927
        dummy_b = "VersionedFile 2."
 
928
        versionedfile.InterVersionedFile.register_optimiser(InterString)
 
929
        try:
 
930
            # we should get the default for something InterString returns False
 
931
            # to
 
932
            self.assertFalse(InterString.is_compatible(dummy_a, None))
 
933
            self.assertGetsDefaultInterVersionedFile(dummy_a, None)
 
934
            # and we should get an InterString for a pair it 'likes'
 
935
            self.assertTrue(InterString.is_compatible(dummy_a, dummy_b))
 
936
            inter = versionedfile.InterVersionedFile.get(dummy_a, dummy_b)
 
937
            self.assertEqual(InterString, inter.__class__)
 
938
            self.assertEqual(dummy_a, inter.source)
 
939
            self.assertEqual(dummy_b, inter.target)
 
940
        finally:
 
941
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
 
942
        # now we should get the default InterVersionedFile object again.
 
943
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
944
 
 
945
 
 
946
class TestReadonlyHttpMixin(object):
 
947
 
 
948
    def get_transaction(self):
 
949
        return 1
 
950
 
 
951
    def test_readonly_http_works(self):
 
952
        # we should be able to read from http with a versioned file.
 
953
        vf = self.get_file()
 
954
        # try an empty file access
 
955
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
956
        self.assertEqual([], readonly_vf.versions())
 
957
        # now with feeling.
 
958
        vf.add_lines('1', [], ['a\n'])
 
959
        vf.add_lines('2', ['1'], ['b\n', 'a\n'])
 
960
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
961
        self.assertEqual(['1', '2'], vf.versions())
 
962
        for version in readonly_vf.versions():
 
963
            readonly_vf.get_lines(version)
 
964
 
 
965
 
 
966
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
967
 
 
968
    def get_file(self):
 
969
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True,
 
970
            get_scope=self.get_transaction)
 
971
 
 
972
    def get_factory(self):
 
973
        return WeaveFile
 
974
 
 
975
 
 
976
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
977
 
 
978
    def get_file(self):
 
979
        return make_file_knit('foo', get_transport(self.get_url('.')),
 
980
            delta=True, create=True, get_scope=self.get_transaction)
 
981
 
 
982
    def get_factory(self):
 
983
        return make_file_knit
 
984
 
 
985
 
 
986
class MergeCasesMixin(object):
 
987
 
 
988
    def doMerge(self, base, a, b, mp):
 
989
        from cStringIO import StringIO
 
990
        from textwrap import dedent
 
991
 
 
992
        def addcrlf(x):
 
993
            return x + '\n'
 
994
        
 
995
        w = self.get_file()
 
996
        w.add_lines('text0', [], map(addcrlf, base))
 
997
        w.add_lines('text1', ['text0'], map(addcrlf, a))
 
998
        w.add_lines('text2', ['text0'], map(addcrlf, b))
 
999
 
 
1000
        self.log_contents(w)
 
1001
 
 
1002
        self.log('merge plan:')
 
1003
        p = list(w.plan_merge('text1', 'text2'))
 
1004
        for state, line in p:
 
1005
            if line:
 
1006
                self.log('%12s | %s' % (state, line[:-1]))
 
1007
 
 
1008
        self.log('merge:')
 
1009
        mt = StringIO()
 
1010
        mt.writelines(w.weave_merge(p))
 
1011
        mt.seek(0)
 
1012
        self.log(mt.getvalue())
 
1013
 
 
1014
        mp = map(addcrlf, mp)
 
1015
        self.assertEqual(mt.readlines(), mp)
 
1016
        
 
1017
        
 
1018
    def testOneInsert(self):
 
1019
        self.doMerge([],
 
1020
                     ['aa'],
 
1021
                     [],
 
1022
                     ['aa'])
 
1023
 
 
1024
    def testSeparateInserts(self):
 
1025
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1026
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1027
                     ['aaa', 'bbb', 'yyy', 'ccc'],
 
1028
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1029
 
 
1030
    def testSameInsert(self):
 
1031
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1032
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1033
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
 
1034
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1035
    overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
 
1036
    def testOverlappedInsert(self):
 
1037
        self.doMerge(['aaa', 'bbb'],
 
1038
                     ['aaa', 'xxx', 'yyy', 'bbb'],
 
1039
                     ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
 
1040
 
 
1041
        # really it ought to reduce this to 
 
1042
        # ['aaa', 'xxx', 'yyy', 'bbb']
 
1043
 
 
1044
 
 
1045
    def testClashReplace(self):
 
1046
        self.doMerge(['aaa'],
 
1047
                     ['xxx'],
 
1048
                     ['yyy', 'zzz'],
 
1049
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
 
1050
                      '>>>>>>> '])
 
1051
 
 
1052
    def testNonClashInsert1(self):
 
1053
        self.doMerge(['aaa'],
 
1054
                     ['xxx', 'aaa'],
 
1055
                     ['yyy', 'zzz'],
 
1056
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
 
1057
                      '>>>>>>> '])
 
1058
 
 
1059
    def testNonClashInsert2(self):
 
1060
        self.doMerge(['aaa'],
 
1061
                     ['aaa'],
 
1062
                     ['yyy', 'zzz'],
 
1063
                     ['yyy', 'zzz'])
 
1064
 
 
1065
 
 
1066
    def testDeleteAndModify(self):
 
1067
        """Clashing delete and modification.
 
1068
 
 
1069
        If one side modifies a region and the other deletes it then
 
1070
        there should be a conflict with one side blank.
 
1071
        """
 
1072
 
 
1073
        #######################################
 
1074
        # skippd, not working yet
 
1075
        return
 
1076
        
 
1077
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1078
                     ['aaa', 'ddd', 'ccc'],
 
1079
                     ['aaa', 'ccc'],
 
1080
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
 
1081
 
 
1082
    def _test_merge_from_strings(self, base, a, b, expected):
 
1083
        w = self.get_file()
 
1084
        w.add_lines('text0', [], base.splitlines(True))
 
1085
        w.add_lines('text1', ['text0'], a.splitlines(True))
 
1086
        w.add_lines('text2', ['text0'], b.splitlines(True))
 
1087
        self.log('merge plan:')
 
1088
        p = list(w.plan_merge('text1', 'text2'))
 
1089
        for state, line in p:
 
1090
            if line:
 
1091
                self.log('%12s | %s' % (state, line[:-1]))
 
1092
        self.log('merge result:')
 
1093
        result_text = ''.join(w.weave_merge(p))
 
1094
        self.log(result_text)
 
1095
        self.assertEqualDiff(result_text, expected)
 
1096
 
 
1097
    def test_weave_merge_conflicts(self):
 
1098
        # does weave merge properly handle plans that end with unchanged?
 
1099
        result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
 
1100
        self.assertEqual(result, 'hello\n')
 
1101
 
 
1102
    def test_deletion_extended(self):
 
1103
        """One side deletes, the other deletes more.
 
1104
        """
 
1105
        base = """\
 
1106
            line 1
 
1107
            line 2
 
1108
            line 3
 
1109
            """
 
1110
        a = """\
 
1111
            line 1
 
1112
            line 2
 
1113
            """
 
1114
        b = """\
 
1115
            line 1
 
1116
            """
 
1117
        result = """\
 
1118
            line 1
 
1119
            """
 
1120
        self._test_merge_from_strings(base, a, b, result)
 
1121
 
 
1122
    def test_deletion_overlap(self):
 
1123
        """Delete overlapping regions with no other conflict.
 
1124
 
 
1125
        Arguably it'd be better to treat these as agreement, rather than 
 
1126
        conflict, but for now conflict is safer.
 
1127
        """
 
1128
        base = """\
 
1129
            start context
 
1130
            int a() {}
 
1131
            int b() {}
 
1132
            int c() {}
 
1133
            end context
 
1134
            """
 
1135
        a = """\
 
1136
            start context
 
1137
            int a() {}
 
1138
            end context
 
1139
            """
 
1140
        b = """\
 
1141
            start context
 
1142
            int c() {}
 
1143
            end context
 
1144
            """
 
1145
        result = """\
 
1146
            start context
 
1147
<<<<<<< 
 
1148
            int a() {}
 
1149
=======
 
1150
            int c() {}
 
1151
>>>>>>> 
 
1152
            end context
 
1153
            """
 
1154
        self._test_merge_from_strings(base, a, b, result)
 
1155
 
 
1156
    def test_agreement_deletion(self):
 
1157
        """Agree to delete some lines, without conflicts."""
 
1158
        base = """\
 
1159
            start context
 
1160
            base line 1
 
1161
            base line 2
 
1162
            end context
 
1163
            """
 
1164
        a = """\
 
1165
            start context
 
1166
            base line 1
 
1167
            end context
 
1168
            """
 
1169
        b = """\
 
1170
            start context
 
1171
            base line 1
 
1172
            end context
 
1173
            """
 
1174
        result = """\
 
1175
            start context
 
1176
            base line 1
 
1177
            end context
 
1178
            """
 
1179
        self._test_merge_from_strings(base, a, b, result)
 
1180
 
 
1181
    def test_sync_on_deletion(self):
 
1182
        """Specific case of merge where we can synchronize incorrectly.
 
1183
        
 
1184
        A previous version of the weave merge concluded that the two versions
 
1185
        agreed on deleting line 2, and this could be a synchronization point.
 
1186
        Line 1 was then considered in isolation, and thought to be deleted on 
 
1187
        both sides.
 
1188
 
 
1189
        It's better to consider the whole thing as a disagreement region.
 
1190
        """
 
1191
        base = """\
 
1192
            start context
 
1193
            base line 1
 
1194
            base line 2
 
1195
            end context
 
1196
            """
 
1197
        a = """\
 
1198
            start context
 
1199
            base line 1
 
1200
            a's replacement line 2
 
1201
            end context
 
1202
            """
 
1203
        b = """\
 
1204
            start context
 
1205
            b replaces
 
1206
            both lines
 
1207
            end context
 
1208
            """
 
1209
        result = """\
 
1210
            start context
 
1211
<<<<<<< 
 
1212
            base line 1
 
1213
            a's replacement line 2
 
1214
=======
 
1215
            b replaces
 
1216
            both lines
 
1217
>>>>>>> 
 
1218
            end context
 
1219
            """
 
1220
        self._test_merge_from_strings(base, a, b, result)
 
1221
 
 
1222
 
 
1223
class TestKnitMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1224
 
 
1225
    def get_file(self, name='foo'):
 
1226
        return make_file_knit(name, get_transport(self.get_url('.')),
 
1227
                                 delta=True, create=True)
 
1228
 
 
1229
    def log_contents(self, w):
 
1230
        pass
 
1231
 
 
1232
 
 
1233
class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1234
 
 
1235
    def get_file(self, name='foo'):
 
1236
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
1237
 
 
1238
    def log_contents(self, w):
 
1239
        self.log('weave is:')
 
1240
        tmpf = StringIO()
 
1241
        write_weave(w, tmpf)
 
1242
        self.log(tmpf.getvalue())
 
1243
 
 
1244
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
 
1245
                                'xxx', '>>>>>>> ', 'bbb']