~bzr-pqm/bzr/bzr.dev

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