/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: Keir Mierle
  • Date: 2007-09-04 19:03:51 UTC
  • mto: This revision was merged to the branch mainline in revision 2824.
  • Revision ID: keir@cs.utoronto.ca-20070904190351-ug32zv9dcfqtluvi
Change ordering of clients listing.

Show diffs side-by-side

added added

removed removed

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