/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: Robert Collins
  • Date: 2008-04-08 03:39:43 UTC
  • mto: This revision was merged to the branch mainline in revision 3350.
  • Revision ID: robertc@robertcollins.net-20080408033943-ihbgs5wyqnh61bit
 * ``VersionedFile.get_sha1`` is deprecated, please use
   ``VersionedFile.get_sha1s``. (Robert Collins)

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
        self.assertRaises(errors.OutSideTransaction, f.clone_text, 'base', 'bar', ['foo'])
 
363
        
 
364
    def test_clear_cache(self):
 
365
        f = self.get_file()
 
366
        # on a new file it should not error
 
367
        f.clear_cache()
 
368
        # and after adding content, doing a clear_cache and a get should work.
 
369
        f.add_lines('0', [], ['a'])
 
370
        f.clear_cache()
 
371
        self.assertEqual(['a'], f.get_lines('0'))
 
372
 
 
373
    def test_clone_text(self):
 
374
        f = self.get_file()
 
375
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
376
        f.clone_text('r1', 'r0', ['r0'])
 
377
        def verify_file(f):
 
378
            self.assertEquals(f.get_lines('r1'), f.get_lines('r0'))
 
379
            self.assertEquals(f.get_lines('r1'), ['a\n', 'b\n'])
 
380
            self.assertEqual({'r1':('r0',)}, f.get_parent_map(['r1']))
 
381
            self.assertRaises(RevisionNotPresent,
 
382
                f.clone_text, 'r2', 'rX', [])
 
383
            self.assertRaises(RevisionAlreadyPresent,
 
384
                f.clone_text, 'r1', 'r0', [])
 
385
        verify_file(f)
 
386
        verify_file(self.reopen_file())
 
387
 
 
388
    def test_copy_to(self):
 
389
        f = self.get_file()
 
390
        f.add_lines('0', [], ['a\n'])
 
391
        t = MemoryTransport()
 
392
        f.copy_to('foo', t)
 
393
        for suffix in self.get_factory().get_suffixes():
 
394
            self.assertTrue(t.has('foo' + suffix))
 
395
 
 
396
    def test_get_suffixes(self):
 
397
        f = self.get_file()
 
398
        # and should be a list
 
399
        self.assertTrue(isinstance(self.get_factory().get_suffixes(), list))
 
400
 
 
401
    def build_graph(self, file, graph):
 
402
        for node in topo_sort(graph.items()):
 
403
            file.add_lines(node, graph[node], [])
 
404
 
 
405
    def test_get_graph(self):
 
406
        f = self.get_file()
 
407
        graph = {
 
408
            'v1': (),
 
409
            'v2': ('v1', ),
 
410
            'v3': ('v2', )}
 
411
        self.build_graph(f, graph)
 
412
        self.assertEqual(graph, self.applyDeprecated(one_four, f.get_graph))
 
413
    
 
414
    def test_get_graph_partial(self):
 
415
        f = self.get_file()
 
416
        complex_graph = {}
 
417
        simple_a = {
 
418
            'c': (),
 
419
            'b': ('c', ),
 
420
            'a': ('b', ),
 
421
            }
 
422
        complex_graph.update(simple_a)
 
423
        simple_b = {
 
424
            'c': (),
 
425
            'b': ('c', ),
 
426
            }
 
427
        complex_graph.update(simple_b)
 
428
        simple_gam = {
 
429
            'c': (),
 
430
            'oo': (),
 
431
            'bar': ('oo', 'c'),
 
432
            'gam': ('bar', ),
 
433
            }
 
434
        complex_graph.update(simple_gam)
 
435
        simple_b_gam = {}
 
436
        simple_b_gam.update(simple_gam)
 
437
        simple_b_gam.update(simple_b)
 
438
        self.build_graph(f, complex_graph)
 
439
        self.assertEqual(simple_a, self.applyDeprecated(one_four, f.get_graph,
 
440
            ['a']))
 
441
        self.assertEqual(simple_b, self.applyDeprecated(one_four, f.get_graph,
 
442
            ['b']))
 
443
        self.assertEqual(simple_gam, self.applyDeprecated(one_four,
 
444
            f.get_graph, ['gam']))
 
445
        self.assertEqual(simple_b_gam, self.applyDeprecated(one_four,
 
446
            f.get_graph, ['b', 'gam']))
 
447
 
 
448
    def test_get_parents(self):
 
449
        f = self.get_file()
 
450
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
451
        f.add_lines('r1', [], ['a\n', 'b\n'])
 
452
        f.add_lines('r2', [], ['a\n', 'b\n'])
 
453
        f.add_lines('r3', [], ['a\n', 'b\n'])
 
454
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
 
455
        self.assertEqual(['r0', 'r1', 'r2', 'r3'],
 
456
            self.applyDeprecated(one_four, f.get_parents, 'm'))
 
457
        self.assertRaises(RevisionNotPresent,
 
458
            self.applyDeprecated, one_four, f.get_parents, 'y')
 
459
 
 
460
    def test_get_parent_map(self):
 
461
        f = self.get_file()
 
462
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
463
        self.assertEqual(
 
464
            {'r0':()}, f.get_parent_map(['r0']))
 
465
        f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
 
466
        self.assertEqual(
 
467
            {'r1':('r0',)}, f.get_parent_map(['r1']))
 
468
        self.assertEqual(
 
469
            {'r0':(),
 
470
             'r1':('r0',)},
 
471
            f.get_parent_map(['r0', 'r1']))
 
472
        f.add_lines('r2', [], ['a\n', 'b\n'])
 
473
        f.add_lines('r3', [], ['a\n', 'b\n'])
 
474
        f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
 
475
        self.assertEqual(
 
476
            {'m':('r0', 'r1', 'r2', 'r3')}, f.get_parent_map(['m']))
 
477
        self.assertEqual({}, f.get_parent_map('y'))
 
478
        self.assertEqual(
 
479
            {'r0':(),
 
480
             'r1':('r0',)},
 
481
            f.get_parent_map(['r0', 'y', 'r1']))
 
482
 
 
483
    def test_annotate(self):
 
484
        f = self.get_file()
 
485
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
486
        f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
 
487
        origins = f.annotate('r1')
 
488
        self.assertEquals(origins[0][0], 'r1')
 
489
        self.assertEquals(origins[1][0], 'r0')
 
490
 
 
491
        self.assertRaises(RevisionNotPresent,
 
492
            f.annotate, 'foo')
 
493
 
 
494
    def test_detection(self):
 
495
        # Test weaves detect corruption.
 
496
        #
 
497
        # Weaves contain a checksum of their texts.
 
498
        # When a text is extracted, this checksum should be
 
499
        # verified.
 
500
 
 
501
        w = self.get_file_corrupted_text()
 
502
 
 
503
        self.assertEqual('hello\n', w.get_text('v1'))
 
504
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
505
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
506
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
507
 
 
508
        w = self.get_file_corrupted_checksum()
 
509
 
 
510
        self.assertEqual('hello\n', w.get_text('v1'))
 
511
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
 
512
        self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
 
513
        self.assertRaises(errors.WeaveInvalidChecksum, w.check)
 
514
 
 
515
    def get_file_corrupted_text(self):
 
516
        """Return a versioned file with corrupt text but valid metadata."""
 
517
        raise NotImplementedError(self.get_file_corrupted_text)
 
518
 
 
519
    def reopen_file(self, name='foo'):
 
520
        """Open the versioned file from disk again."""
 
521
        raise NotImplementedError(self.reopen_file)
 
522
 
 
523
    def test_iter_parents(self):
 
524
        """iter_parents returns the parents for many nodes."""
 
525
        f = self.get_file()
 
526
        # sample data:
 
527
        # no parents
 
528
        f.add_lines('r0', [], ['a\n', 'b\n'])
 
529
        # 1 parents
 
530
        f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
 
531
        # 2 parents
 
532
        f.add_lines('r2', ['r1', 'r0'], ['a\n', 'b\n'])
 
533
        # XXX TODO a ghost
 
534
        # cases: each sample data individually:
 
535
        self.assertEqual(set([('r0', ())]),
 
536
            set(self.applyDeprecated(one_four, f.iter_parents, ['r0'])))
 
537
        self.assertEqual(set([('r1', ('r0', ))]),
 
538
            set(self.applyDeprecated(one_four, f.iter_parents, ['r1'])))
 
539
        self.assertEqual(set([('r2', ('r1', 'r0'))]),
 
540
            set(self.applyDeprecated(one_four, f.iter_parents, ['r2'])))
 
541
        # no nodes returned for a missing node
 
542
        self.assertEqual(set(),
 
543
            set(self.applyDeprecated(one_four, f.iter_parents, ['missing'])))
 
544
        # 1 node returned with missing nodes skipped
 
545
        self.assertEqual(set([('r1', ('r0', ))]),
 
546
            set(self.applyDeprecated(one_four, f.iter_parents, ['ghost1', 'r1',
 
547
                'ghost'])))
 
548
        # 2 nodes returned
 
549
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
550
            set(self.applyDeprecated(one_four, f.iter_parents, ['r0', 'r1'])))
 
551
        # 2 nodes returned, missing skipped
 
552
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
 
553
            set(self.applyDeprecated(one_four, f.iter_parents,
 
554
                ['a', 'r0', 'b', 'r1', 'c'])))
 
555
 
 
556
    def test_iter_lines_added_or_present_in_versions(self):
 
557
        # test that we get at least an equalset of the lines added by
 
558
        # versions in the weave 
 
559
        # the ordering here is to make a tree so that dumb searches have
 
560
        # more changes to muck up.
 
561
 
 
562
        class InstrumentedProgress(progress.DummyProgress):
 
563
 
 
564
            def __init__(self):
 
565
 
 
566
                progress.DummyProgress.__init__(self)
 
567
                self.updates = []
 
568
 
 
569
            def update(self, msg=None, current=None, total=None):
 
570
                self.updates.append((msg, current, total))
 
571
 
 
572
        vf = self.get_file()
 
573
        # add a base to get included
 
574
        vf.add_lines('base', [], ['base\n'])
 
575
        # add a ancestor to be included on one side
 
576
        vf.add_lines('lancestor', [], ['lancestor\n'])
 
577
        # add a ancestor to be included on the other side
 
578
        vf.add_lines('rancestor', ['base'], ['rancestor\n'])
 
579
        # add a child of rancestor with no eofile-nl
 
580
        vf.add_lines('child', ['rancestor'], ['base\n', 'child\n'])
 
581
        # add a child of lancestor and base to join the two roots
 
582
        vf.add_lines('otherchild',
 
583
                     ['lancestor', 'base'],
 
584
                     ['base\n', 'lancestor\n', 'otherchild\n'])
 
585
        def iter_with_versions(versions, expected):
 
586
            # now we need to see what lines are returned, and how often.
 
587
            lines = {}
 
588
            progress = InstrumentedProgress()
 
589
            # iterate over the lines
 
590
            for line in vf.iter_lines_added_or_present_in_versions(versions,
 
591
                pb=progress):
 
592
                lines.setdefault(line, 0)
 
593
                lines[line] += 1
 
594
            if []!= progress.updates:
 
595
                self.assertEqual(expected, progress.updates)
 
596
            return lines
 
597
        lines = iter_with_versions(['child', 'otherchild'],
 
598
                                   [('Walking content.', 0, 2),
 
599
                                    ('Walking content.', 1, 2),
 
600
                                    ('Walking content.', 2, 2)])
 
601
        # we must see child and otherchild
 
602
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
603
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
 
604
        # we dont care if we got more than that.
 
605
        
 
606
        # test all lines
 
607
        lines = iter_with_versions(None, [('Walking content.', 0, 5),
 
608
                                          ('Walking content.', 1, 5),
 
609
                                          ('Walking content.', 2, 5),
 
610
                                          ('Walking content.', 3, 5),
 
611
                                          ('Walking content.', 4, 5),
 
612
                                          ('Walking content.', 5, 5)])
 
613
        # all lines must be seen at least once
 
614
        self.assertTrue(lines[('base\n', 'base')] > 0)
 
615
        self.assertTrue(lines[('lancestor\n', 'lancestor')] > 0)
 
616
        self.assertTrue(lines[('rancestor\n', 'rancestor')] > 0)
 
617
        self.assertTrue(lines[('child\n', 'child')] > 0)
 
618
        self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
 
619
 
 
620
    def test_add_lines_with_ghosts(self):
 
621
        # some versioned file formats allow lines to be added with parent
 
622
        # information that is > than that in the format. Formats that do
 
623
        # not support this need to raise NotImplementedError on the
 
624
        # add_lines_with_ghosts api.
 
625
        vf = self.get_file()
 
626
        # add a revision with ghost parents
 
627
        # The preferred form is utf8, but we should translate when needed
 
628
        parent_id_unicode = u'b\xbfse'
 
629
        parent_id_utf8 = parent_id_unicode.encode('utf8')
 
630
        try:
 
631
            vf.add_lines_with_ghosts('notbxbfse', [parent_id_utf8], [])
 
632
        except NotImplementedError:
 
633
            # check the other ghost apis are also not implemented
 
634
            self.assertRaises(NotImplementedError, vf.get_ancestry_with_ghosts, ['foo'])
 
635
            self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
 
636
            return
 
637
        vf = self.reopen_file()
 
638
        # test key graph related apis: getncestry, _graph, get_parents
 
639
        # has_version
 
640
        # - these are ghost unaware and must not be reflect ghosts
 
641
        self.assertEqual(['notbxbfse'], vf.get_ancestry('notbxbfse'))
 
642
        self.assertEqual([],
 
643
            self.applyDeprecated(one_four, vf.get_parents, 'notbxbfse'))
 
644
        self.assertEqual({'notbxbfse':()}, self.applyDeprecated(one_four,
 
645
            vf.get_graph))
 
646
        self.assertFalse(vf.has_version(parent_id_utf8))
 
647
        # we have _with_ghost apis to give us ghost information.
 
648
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
 
649
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
650
        self.assertEqual({'notbxbfse':(parent_id_utf8,)},
 
651
            self.applyDeprecated(one_four, vf.get_graph_with_ghosts))
 
652
        self.assertTrue(self.applyDeprecated(one_four, vf.has_ghost,
 
653
            parent_id_utf8))
 
654
        # if we add something that is a ghost of another, it should correct the
 
655
        # results of the prior apis
 
656
        vf.add_lines(parent_id_utf8, [], [])
 
657
        self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry(['notbxbfse']))
 
658
        self.assertEqual({'notbxbfse':(parent_id_utf8,)},
 
659
            vf.get_parent_map(['notbxbfse']))
 
660
        self.assertEqual({parent_id_utf8:(),
 
661
                          'notbxbfse':(parent_id_utf8, ),
 
662
                          },
 
663
                         self.applyDeprecated(one_four, vf.get_graph))
 
664
        self.assertTrue(vf.has_version(parent_id_utf8))
 
665
        # we have _with_ghost apis to give us ghost information.
 
666
        self.assertEqual([parent_id_utf8, 'notbxbfse'],
 
667
            vf.get_ancestry_with_ghosts(['notbxbfse']))
 
668
        self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
 
669
        self.assertEqual({parent_id_utf8:(),
 
670
                          'notbxbfse':(parent_id_utf8,),
 
671
                          },
 
672
            self.applyDeprecated(one_four, vf.get_graph_with_ghosts))
 
673
        self.assertFalse(self.applyDeprecated(one_four, vf.has_ghost,
 
674
            parent_id_utf8))
 
675
 
 
676
    def test_add_lines_with_ghosts_after_normal_revs(self):
 
677
        # some versioned file formats allow lines to be added with parent
 
678
        # information that is > than that in the format. Formats that do
 
679
        # not support this need to raise NotImplementedError on the
 
680
        # add_lines_with_ghosts api.
 
681
        vf = self.get_file()
 
682
        # probe for ghost support
 
683
        try:
 
684
            vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
 
685
        except NotImplementedError:
 
686
            return
 
687
        vf.add_lines_with_ghosts('references_ghost',
 
688
                                 ['base', 'a_ghost'],
 
689
                                 ['line\n', 'line_b\n', 'line_c\n'])
 
690
        origins = vf.annotate('references_ghost')
 
691
        self.assertEquals(('base', 'line\n'), origins[0])
 
692
        self.assertEquals(('base', 'line_b\n'), origins[1])
 
693
        self.assertEquals(('references_ghost', 'line_c\n'), origins[2])
 
694
 
 
695
    def test_readonly_mode(self):
 
696
        transport = get_transport(self.get_url('.'))
 
697
        factory = self.get_factory()
 
698
        vf = factory('id', transport, 0777, create=True, access_mode='w')
 
699
        vf = factory('id', transport, access_mode='r')
 
700
        self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
 
701
        self.assertRaises(errors.ReadOnlyError,
 
702
                          vf.add_lines_with_ghosts,
 
703
                          'base',
 
704
                          [],
 
705
                          [])
 
706
        self.assertRaises(errors.ReadOnlyError, vf.join, 'base')
 
707
        self.assertRaises(errors.ReadOnlyError, vf.clone_text, 'base', 'bar', ['foo'])
 
708
    
 
709
    def test_get_sha1s(self):
 
710
        # check the sha1 data is available
 
711
        vf = self.get_file()
 
712
        # a simple file
 
713
        vf.add_lines('a', [], ['a\n'])
 
714
        # the same file, different metadata
 
715
        vf.add_lines('b', ['a'], ['a\n'])
 
716
        # a file differing only in last newline.
 
717
        vf.add_lines('c', [], ['a'])
 
718
        # Deprecasted single-version API.
 
719
        self.assertEqual(
 
720
            '3f786850e387550fdab836ed7e6dc881de23001b',
 
721
            self.applyDeprecated(one_four, vf.get_sha1, 'a'))
 
722
        self.assertEqual(
 
723
            '3f786850e387550fdab836ed7e6dc881de23001b',
 
724
            self.applyDeprecated(one_four, vf.get_sha1, 'b'))
 
725
        self.assertEqual(
 
726
            '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
727
            self.applyDeprecated(one_four, vf.get_sha1, 'c'))
 
728
        self.assertEqual(['3f786850e387550fdab836ed7e6dc881de23001b',
 
729
                          '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
730
                          '3f786850e387550fdab836ed7e6dc881de23001b'],
 
731
                          vf.get_sha1s(['a', 'c', 'b']))
 
732
        
 
733
 
 
734
class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
735
 
 
736
    def get_file(self, name='foo'):
 
737
        return WeaveFile(name, get_transport(self.get_url('.')), create=True,
 
738
            get_scope=self.get_transaction)
 
739
 
 
740
    def get_file_corrupted_text(self):
 
741
        w = WeaveFile('foo', get_transport(self.get_url('.')), create=True,
 
742
            get_scope=self.get_transaction)
 
743
        w.add_lines('v1', [], ['hello\n'])
 
744
        w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
745
        
 
746
        # We are going to invasively corrupt the text
 
747
        # Make sure the internals of weave are the same
 
748
        self.assertEqual([('{', 0)
 
749
                        , 'hello\n'
 
750
                        , ('}', None)
 
751
                        , ('{', 1)
 
752
                        , 'there\n'
 
753
                        , ('}', None)
 
754
                        ], w._weave)
 
755
        
 
756
        self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
 
757
                        , '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
758
                        ], w._sha1s)
 
759
        w.check()
 
760
        
 
761
        # Corrupted
 
762
        w._weave[4] = 'There\n'
 
763
        return w
 
764
 
 
765
    def get_file_corrupted_checksum(self):
 
766
        w = self.get_file_corrupted_text()
 
767
        # Corrected
 
768
        w._weave[4] = 'there\n'
 
769
        self.assertEqual('hello\nthere\n', w.get_text('v2'))
 
770
        
 
771
        #Invalid checksum, first digit changed
 
772
        w._sha1s[1] =  'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
 
773
        return w
 
774
 
 
775
    def reopen_file(self, name='foo', create=False):
 
776
        return WeaveFile(name, get_transport(self.get_url('.')), create=create,
 
777
            get_scope=self.get_transaction)
 
778
 
 
779
    def test_no_implicit_create(self):
 
780
        self.assertRaises(errors.NoSuchFile,
 
781
                          WeaveFile,
 
782
                          'foo',
 
783
                          get_transport(self.get_url('.')),
 
784
                          get_scope=self.get_transaction)
 
785
 
 
786
    def get_factory(self):
 
787
        return WeaveFile
 
788
 
 
789
 
 
790
class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
791
 
 
792
    def get_file(self, name='foo'):
 
793
        return self.get_factory()(name, get_transport(self.get_url('.')),
 
794
            delta=True, create=True, get_scope=self.get_transaction)
 
795
 
 
796
    def get_factory(self):
 
797
        return make_file_knit
 
798
 
 
799
    def get_file_corrupted_text(self):
 
800
        knit = self.get_file()
 
801
        knit.add_lines('v1', [], ['hello\n'])
 
802
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
803
        return knit
 
804
 
 
805
    def reopen_file(self, name='foo', create=False):
 
806
        return self.get_factory()(name, get_transport(self.get_url('.')),
 
807
            delta=True,
 
808
            create=create)
 
809
 
 
810
    def test_detection(self):
 
811
        knit = self.get_file()
 
812
        knit.check()
 
813
 
 
814
    def test_no_implicit_create(self):
 
815
        self.assertRaises(errors.NoSuchFile, self.get_factory(), 'foo',
 
816
            get_transport(self.get_url('.')))
 
817
 
 
818
 
 
819
class TestPlaintextKnit(TestKnit):
 
820
    """Test a knit with no cached annotations"""
 
821
 
 
822
    def get_factory(self):
 
823
        return make_file_knit
 
824
 
 
825
 
 
826
class TestPlanMergeVersionedFile(TestCaseWithMemoryTransport):
 
827
 
 
828
    def setUp(self):
 
829
        TestCaseWithMemoryTransport.setUp(self)
 
830
        self.vf1 = make_file_knit('root', self.get_transport(), create=True)
 
831
        self.vf2 = make_file_knit('root', self.get_transport(), create=True)
 
832
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root',
 
833
            [self.vf1, self.vf2])
 
834
 
 
835
    def test_add_lines(self):
 
836
        self.plan_merge_vf.add_lines('a:', [], [])
 
837
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a', [],
 
838
                          [])
 
839
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', None,
 
840
                          [])
 
841
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', [],
 
842
                          None)
 
843
 
 
844
    def test_ancestry(self):
 
845
        self.vf1.add_lines('A', [], [])
 
846
        self.vf1.add_lines('B', ['A'], [])
 
847
        self.plan_merge_vf.add_lines('C:', ['B'], [])
 
848
        self.plan_merge_vf.add_lines('D:', ['C:'], [])
 
849
        self.assertEqual(set(['A', 'B', 'C:', 'D:']),
 
850
            self.plan_merge_vf.get_ancestry('D:', topo_sorted=False))
 
851
 
 
852
    def setup_abcde(self):
 
853
        self.vf1.add_lines('A', [], ['a'])
 
854
        self.vf1.add_lines('B', ['A'], ['b'])
 
855
        self.vf2.add_lines('C', [], ['c'])
 
856
        self.vf2.add_lines('D', ['C'], ['d'])
 
857
        self.plan_merge_vf.add_lines('E:', ['B', 'D'], ['e'])
 
858
 
 
859
    def test_ancestry_uses_all_versionedfiles(self):
 
860
        self.setup_abcde()
 
861
        self.assertEqual(set(['A', 'B', 'C', 'D', 'E:']),
 
862
            self.plan_merge_vf.get_ancestry('E:', topo_sorted=False))
 
863
 
 
864
    def test_ancestry_raises_revision_not_present(self):
 
865
        error = self.assertRaises(errors.RevisionNotPresent,
 
866
                                  self.plan_merge_vf.get_ancestry, 'E:', False)
 
867
        self.assertContainsRe(str(error), '{E:} not present in "root"')
 
868
 
 
869
    def test_get_parents(self):
 
870
        self.setup_abcde()
 
871
        self.assertEqual({'B':('A',)}, self.plan_merge_vf.get_parent_map(['B']))
 
872
        self.assertEqual({'D':('C',)}, self.plan_merge_vf.get_parent_map(['D']))
 
873
        self.assertEqual({'E:':('B', 'D')},
 
874
            self.plan_merge_vf.get_parent_map(['E:']))
 
875
        self.assertEqual({}, self.plan_merge_vf.get_parent_map(['F']))
 
876
        self.assertEqual({
 
877
                'B':('A',),
 
878
                'D':('C',),
 
879
                'E:':('B', 'D'),
 
880
                }, self.plan_merge_vf.get_parent_map(['B', 'D', 'E:', 'F']))
 
881
 
 
882
    def test_get_lines(self):
 
883
        self.setup_abcde()
 
884
        self.assertEqual(['a'], self.plan_merge_vf.get_lines('A'))
 
885
        self.assertEqual(['c'], self.plan_merge_vf.get_lines('C'))
 
886
        self.assertEqual(['e'], self.plan_merge_vf.get_lines('E:'))
 
887
        error = self.assertRaises(errors.RevisionNotPresent,
 
888
                                  self.plan_merge_vf.get_lines, 'F')
 
889
        self.assertContainsRe(str(error), '{F} not present in "root"')
 
890
 
 
891
 
 
892
class InterString(versionedfile.InterVersionedFile):
 
893
    """An inter-versionedfile optimised code path for strings.
 
894
 
 
895
    This is for use during testing where we use strings as versionedfiles
 
896
    so that none of the default regsitered interversionedfile classes will
 
897
    match - which lets us test the match logic.
 
898
    """
 
899
 
 
900
    @staticmethod
 
901
    def is_compatible(source, target):
 
902
        """InterString is compatible with strings-as-versionedfiles."""
 
903
        return isinstance(source, str) and isinstance(target, str)
 
904
 
 
905
 
 
906
# TODO this and the InterRepository core logic should be consolidatable
 
907
# if we make the registry a separate class though we still need to 
 
908
# test the behaviour in the active registry to catch failure-to-handle-
 
909
# stange-objects
 
910
class TestInterVersionedFile(TestCaseWithMemoryTransport):
 
911
 
 
912
    def test_get_default_inter_versionedfile(self):
 
913
        # test that the InterVersionedFile.get(a, b) probes
 
914
        # for a class where is_compatible(a, b) returns
 
915
        # true and returns a default interversionedfile otherwise.
 
916
        # This also tests that the default registered optimised interversionedfile
 
917
        # classes do not barf inappropriately when a surprising versionedfile type
 
918
        # is handed to them.
 
919
        dummy_a = "VersionedFile 1."
 
920
        dummy_b = "VersionedFile 2."
 
921
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
922
 
 
923
    def assertGetsDefaultInterVersionedFile(self, a, b):
 
924
        """Asserts that InterVersionedFile.get(a, b) -> the default."""
 
925
        inter = versionedfile.InterVersionedFile.get(a, b)
 
926
        self.assertEqual(versionedfile.InterVersionedFile,
 
927
                         inter.__class__)
 
928
        self.assertEqual(a, inter.source)
 
929
        self.assertEqual(b, inter.target)
 
930
 
 
931
    def test_register_inter_versionedfile_class(self):
 
932
        # test that a optimised code path provider - a
 
933
        # InterVersionedFile subclass can be registered and unregistered
 
934
        # and that it is correctly selected when given a versionedfile
 
935
        # pair that it returns true on for the is_compatible static method
 
936
        # check
 
937
        dummy_a = "VersionedFile 1."
 
938
        dummy_b = "VersionedFile 2."
 
939
        versionedfile.InterVersionedFile.register_optimiser(InterString)
 
940
        try:
 
941
            # we should get the default for something InterString returns False
 
942
            # to
 
943
            self.assertFalse(InterString.is_compatible(dummy_a, None))
 
944
            self.assertGetsDefaultInterVersionedFile(dummy_a, None)
 
945
            # and we should get an InterString for a pair it 'likes'
 
946
            self.assertTrue(InterString.is_compatible(dummy_a, dummy_b))
 
947
            inter = versionedfile.InterVersionedFile.get(dummy_a, dummy_b)
 
948
            self.assertEqual(InterString, inter.__class__)
 
949
            self.assertEqual(dummy_a, inter.source)
 
950
            self.assertEqual(dummy_b, inter.target)
 
951
        finally:
 
952
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
 
953
        # now we should get the default InterVersionedFile object again.
 
954
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
955
 
 
956
 
 
957
class TestReadonlyHttpMixin(object):
 
958
 
 
959
    def get_transaction(self):
 
960
        return 1
 
961
 
 
962
    def test_readonly_http_works(self):
 
963
        # we should be able to read from http with a versioned file.
 
964
        vf = self.get_file()
 
965
        # try an empty file access
 
966
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
967
        self.assertEqual([], readonly_vf.versions())
 
968
        # now with feeling.
 
969
        vf.add_lines('1', [], ['a\n'])
 
970
        vf.add_lines('2', ['1'], ['b\n', 'a\n'])
 
971
        readonly_vf = self.get_factory()('foo', get_transport(self.get_readonly_url('.')))
 
972
        self.assertEqual(['1', '2'], vf.versions())
 
973
        for version in readonly_vf.versions():
 
974
            readonly_vf.get_lines(version)
 
975
 
 
976
 
 
977
class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
978
 
 
979
    def get_file(self):
 
980
        return WeaveFile('foo', get_transport(self.get_url('.')), create=True,
 
981
            get_scope=self.get_transaction)
 
982
 
 
983
    def get_factory(self):
 
984
        return WeaveFile
 
985
 
 
986
 
 
987
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
988
 
 
989
    def get_file(self):
 
990
        return make_file_knit('foo', get_transport(self.get_url('.')),
 
991
            delta=True, create=True, get_scope=self.get_transaction)
 
992
 
 
993
    def get_factory(self):
 
994
        return make_file_knit
 
995
 
 
996
 
 
997
class MergeCasesMixin(object):
 
998
 
 
999
    def doMerge(self, base, a, b, mp):
 
1000
        from cStringIO import StringIO
 
1001
        from textwrap import dedent
 
1002
 
 
1003
        def addcrlf(x):
 
1004
            return x + '\n'
 
1005
        
 
1006
        w = self.get_file()
 
1007
        w.add_lines('text0', [], map(addcrlf, base))
 
1008
        w.add_lines('text1', ['text0'], map(addcrlf, a))
 
1009
        w.add_lines('text2', ['text0'], map(addcrlf, b))
 
1010
 
 
1011
        self.log_contents(w)
 
1012
 
 
1013
        self.log('merge plan:')
 
1014
        p = list(w.plan_merge('text1', 'text2'))
 
1015
        for state, line in p:
 
1016
            if line:
 
1017
                self.log('%12s | %s' % (state, line[:-1]))
 
1018
 
 
1019
        self.log('merge:')
 
1020
        mt = StringIO()
 
1021
        mt.writelines(w.weave_merge(p))
 
1022
        mt.seek(0)
 
1023
        self.log(mt.getvalue())
 
1024
 
 
1025
        mp = map(addcrlf, mp)
 
1026
        self.assertEqual(mt.readlines(), mp)
 
1027
        
 
1028
        
 
1029
    def testOneInsert(self):
 
1030
        self.doMerge([],
 
1031
                     ['aa'],
 
1032
                     [],
 
1033
                     ['aa'])
 
1034
 
 
1035
    def testSeparateInserts(self):
 
1036
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1037
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1038
                     ['aaa', 'bbb', 'yyy', 'ccc'],
 
1039
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1040
 
 
1041
    def testSameInsert(self):
 
1042
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1043
                     ['aaa', 'xxx', 'bbb', 'ccc'],
 
1044
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
 
1045
                     ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
 
1046
    overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
 
1047
    def testOverlappedInsert(self):
 
1048
        self.doMerge(['aaa', 'bbb'],
 
1049
                     ['aaa', 'xxx', 'yyy', 'bbb'],
 
1050
                     ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
 
1051
 
 
1052
        # really it ought to reduce this to 
 
1053
        # ['aaa', 'xxx', 'yyy', 'bbb']
 
1054
 
 
1055
 
 
1056
    def testClashReplace(self):
 
1057
        self.doMerge(['aaa'],
 
1058
                     ['xxx'],
 
1059
                     ['yyy', 'zzz'],
 
1060
                     ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz', 
 
1061
                      '>>>>>>> '])
 
1062
 
 
1063
    def testNonClashInsert1(self):
 
1064
        self.doMerge(['aaa'],
 
1065
                     ['xxx', 'aaa'],
 
1066
                     ['yyy', 'zzz'],
 
1067
                     ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz', 
 
1068
                      '>>>>>>> '])
 
1069
 
 
1070
    def testNonClashInsert2(self):
 
1071
        self.doMerge(['aaa'],
 
1072
                     ['aaa'],
 
1073
                     ['yyy', 'zzz'],
 
1074
                     ['yyy', 'zzz'])
 
1075
 
 
1076
 
 
1077
    def testDeleteAndModify(self):
 
1078
        """Clashing delete and modification.
 
1079
 
 
1080
        If one side modifies a region and the other deletes it then
 
1081
        there should be a conflict with one side blank.
 
1082
        """
 
1083
 
 
1084
        #######################################
 
1085
        # skippd, not working yet
 
1086
        return
 
1087
        
 
1088
        self.doMerge(['aaa', 'bbb', 'ccc'],
 
1089
                     ['aaa', 'ddd', 'ccc'],
 
1090
                     ['aaa', 'ccc'],
 
1091
                     ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
 
1092
 
 
1093
    def _test_merge_from_strings(self, base, a, b, expected):
 
1094
        w = self.get_file()
 
1095
        w.add_lines('text0', [], base.splitlines(True))
 
1096
        w.add_lines('text1', ['text0'], a.splitlines(True))
 
1097
        w.add_lines('text2', ['text0'], b.splitlines(True))
 
1098
        self.log('merge plan:')
 
1099
        p = list(w.plan_merge('text1', 'text2'))
 
1100
        for state, line in p:
 
1101
            if line:
 
1102
                self.log('%12s | %s' % (state, line[:-1]))
 
1103
        self.log('merge result:')
 
1104
        result_text = ''.join(w.weave_merge(p))
 
1105
        self.log(result_text)
 
1106
        self.assertEqualDiff(result_text, expected)
 
1107
 
 
1108
    def test_weave_merge_conflicts(self):
 
1109
        # does weave merge properly handle plans that end with unchanged?
 
1110
        result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
 
1111
        self.assertEqual(result, 'hello\n')
 
1112
 
 
1113
    def test_deletion_extended(self):
 
1114
        """One side deletes, the other deletes more.
 
1115
        """
 
1116
        base = """\
 
1117
            line 1
 
1118
            line 2
 
1119
            line 3
 
1120
            """
 
1121
        a = """\
 
1122
            line 1
 
1123
            line 2
 
1124
            """
 
1125
        b = """\
 
1126
            line 1
 
1127
            """
 
1128
        result = """\
 
1129
            line 1
 
1130
            """
 
1131
        self._test_merge_from_strings(base, a, b, result)
 
1132
 
 
1133
    def test_deletion_overlap(self):
 
1134
        """Delete overlapping regions with no other conflict.
 
1135
 
 
1136
        Arguably it'd be better to treat these as agreement, rather than 
 
1137
        conflict, but for now conflict is safer.
 
1138
        """
 
1139
        base = """\
 
1140
            start context
 
1141
            int a() {}
 
1142
            int b() {}
 
1143
            int c() {}
 
1144
            end context
 
1145
            """
 
1146
        a = """\
 
1147
            start context
 
1148
            int a() {}
 
1149
            end context
 
1150
            """
 
1151
        b = """\
 
1152
            start context
 
1153
            int c() {}
 
1154
            end context
 
1155
            """
 
1156
        result = """\
 
1157
            start context
 
1158
<<<<<<< 
 
1159
            int a() {}
 
1160
=======
 
1161
            int c() {}
 
1162
>>>>>>> 
 
1163
            end context
 
1164
            """
 
1165
        self._test_merge_from_strings(base, a, b, result)
 
1166
 
 
1167
    def test_agreement_deletion(self):
 
1168
        """Agree to delete some lines, without conflicts."""
 
1169
        base = """\
 
1170
            start context
 
1171
            base line 1
 
1172
            base line 2
 
1173
            end context
 
1174
            """
 
1175
        a = """\
 
1176
            start context
 
1177
            base line 1
 
1178
            end context
 
1179
            """
 
1180
        b = """\
 
1181
            start context
 
1182
            base line 1
 
1183
            end context
 
1184
            """
 
1185
        result = """\
 
1186
            start context
 
1187
            base line 1
 
1188
            end context
 
1189
            """
 
1190
        self._test_merge_from_strings(base, a, b, result)
 
1191
 
 
1192
    def test_sync_on_deletion(self):
 
1193
        """Specific case of merge where we can synchronize incorrectly.
 
1194
        
 
1195
        A previous version of the weave merge concluded that the two versions
 
1196
        agreed on deleting line 2, and this could be a synchronization point.
 
1197
        Line 1 was then considered in isolation, and thought to be deleted on 
 
1198
        both sides.
 
1199
 
 
1200
        It's better to consider the whole thing as a disagreement region.
 
1201
        """
 
1202
        base = """\
 
1203
            start context
 
1204
            base line 1
 
1205
            base line 2
 
1206
            end context
 
1207
            """
 
1208
        a = """\
 
1209
            start context
 
1210
            base line 1
 
1211
            a's replacement line 2
 
1212
            end context
 
1213
            """
 
1214
        b = """\
 
1215
            start context
 
1216
            b replaces
 
1217
            both lines
 
1218
            end context
 
1219
            """
 
1220
        result = """\
 
1221
            start context
 
1222
<<<<<<< 
 
1223
            base line 1
 
1224
            a's replacement line 2
 
1225
=======
 
1226
            b replaces
 
1227
            both lines
 
1228
>>>>>>> 
 
1229
            end context
 
1230
            """
 
1231
        self._test_merge_from_strings(base, a, b, result)
 
1232
 
 
1233
 
 
1234
class TestKnitMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1235
 
 
1236
    def get_file(self, name='foo'):
 
1237
        return make_file_knit(name, get_transport(self.get_url('.')),
 
1238
                                 delta=True, create=True)
 
1239
 
 
1240
    def log_contents(self, w):
 
1241
        pass
 
1242
 
 
1243
 
 
1244
class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1245
 
 
1246
    def get_file(self, name='foo'):
 
1247
        return WeaveFile(name, get_transport(self.get_url('.')), create=True)
 
1248
 
 
1249
    def log_contents(self, w):
 
1250
        self.log('weave is:')
 
1251
        tmpf = StringIO()
 
1252
        write_weave(w, tmpf)
 
1253
        self.log(tmpf.getvalue())
 
1254
 
 
1255
    overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
 
1256
                                'xxx', '>>>>>>> ', 'bbb']