/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: 2007-09-04 03:53:07 UTC
  • mto: (2592.3.126 repository)
  • mto: This revision was merged to the branch mainline in revision 2795.
  • Revision ID: robertc@robertcollins.net-20070904035307-y5jacs7zl15v2nvo
Add reasonably comprehensive tests for path last modified and per file graph behaviour.

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