~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_versionedfile.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-06-05 04:05:05 UTC
  • mfrom: (3473.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20080605040505-i9kqxg2fps2qjdi0
Add the 'alias' command (Tim Penhey)

Show diffs side-by-side

added added

removed removed

Lines of Context:
37
37
                           )
38
38
from bzrlib import knit as _mod_knit
39
39
from bzrlib.knit import (
40
 
    cleanup_pack_knit,
41
 
    make_file_factory,
42
 
    make_pack_factory,
 
40
    make_file_knit,
43
41
    KnitAnnotateFactory,
44
42
    KnitPlainFactory,
45
43
    )
46
44
from bzrlib.symbol_versioning import one_four, one_five
47
 
from bzrlib.tests import (
48
 
    TestCase,
49
 
    TestCaseWithMemoryTransport,
50
 
    TestScenarioApplier,
51
 
    TestSkipped,
52
 
    condition_isinstance,
53
 
    split_suite_by_condition,
54
 
    iter_suite_tests,
55
 
    )
 
45
from bzrlib.tests import TestCaseWithMemoryTransport, TestSkipped
56
46
from bzrlib.tests.http_utils import TestCaseWithWebserver
57
47
from bzrlib.trace import mutter
58
48
from bzrlib.transport import get_transport
60
50
from bzrlib.tsort import topo_sort
61
51
from bzrlib.tuned_gzip import GzipFile
62
52
import bzrlib.versionedfile as versionedfile
63
 
from bzrlib.versionedfile import (
64
 
    ConstantMapper,
65
 
    HashEscapedPrefixMapper,
66
 
    PrefixMapper,
67
 
    VirtualVersionedFiles,
68
 
    make_versioned_files_factory,
69
 
    )
70
53
from bzrlib.weave import WeaveFile
71
54
from bzrlib.weavefile import read_weave, write_weave
72
55
 
73
56
 
74
 
def load_tests(standard_tests, module, loader):
75
 
    """Parameterize VersionedFiles tests for different implementations."""
76
 
    to_adapt, result = split_suite_by_condition(
77
 
        standard_tests, condition_isinstance(TestVersionedFiles))
78
 
    len_one_adapter = TestScenarioApplier()
79
 
    len_two_adapter = TestScenarioApplier()
80
 
    # We want to be sure of behaviour for:
81
 
    # weaves prefix layout (weave texts)
82
 
    # individually named weaves (weave inventories)
83
 
    # annotated knits - prefix|hash|hash-escape layout, we test the third only
84
 
    #                   as it is the most complex mapper.
85
 
    # individually named knits
86
 
    # individual no-graph knits in packs (signatures)
87
 
    # individual graph knits in packs (inventories)
88
 
    # individual graph nocompression knits in packs (revisions)
89
 
    # plain text knits in packs (texts)
90
 
    len_one_adapter.scenarios = [
91
 
        ('weave-named', {
92
 
            'cleanup':None,
93
 
            'factory':make_versioned_files_factory(WeaveFile,
94
 
                ConstantMapper('inventory')),
95
 
            'graph':True,
96
 
            'key_length':1,
97
 
            }),
98
 
        ('named-knit', {
99
 
            'cleanup':None,
100
 
            'factory':make_file_factory(False, ConstantMapper('revisions')),
101
 
            'graph':True,
102
 
            'key_length':1,
103
 
            }),
104
 
        ('named-nograph-knit-pack', {
105
 
            'cleanup':cleanup_pack_knit,
106
 
            'factory':make_pack_factory(False, False, 1),
107
 
            'graph':False,
108
 
            'key_length':1,
109
 
            }),
110
 
        ('named-graph-knit-pack', {
111
 
            'cleanup':cleanup_pack_knit,
112
 
            'factory':make_pack_factory(True, True, 1),
113
 
            'graph':True,
114
 
            'key_length':1,
115
 
            }),
116
 
        ('named-graph-nodelta-knit-pack', {
117
 
            'cleanup':cleanup_pack_knit,
118
 
            'factory':make_pack_factory(True, False, 1),
119
 
            'graph':True,
120
 
            'key_length':1,
121
 
            }),
122
 
        ]
123
 
    len_two_adapter.scenarios = [
124
 
        ('weave-prefix', {
125
 
            'cleanup':None,
126
 
            'factory':make_versioned_files_factory(WeaveFile,
127
 
                PrefixMapper()),
128
 
            'graph':True,
129
 
            'key_length':2,
130
 
            }),
131
 
        ('annotated-knit-escape', {
132
 
            'cleanup':None,
133
 
            'factory':make_file_factory(True, HashEscapedPrefixMapper()),
134
 
            'graph':True,
135
 
            'key_length':2,
136
 
            }),
137
 
        ('plain-knit-pack', {
138
 
            'cleanup':cleanup_pack_knit,
139
 
            'factory':make_pack_factory(True, True, 2),
140
 
            'graph':True,
141
 
            'key_length':2,
142
 
            }),
143
 
        ]
144
 
    for test in iter_suite_tests(to_adapt):
145
 
        result.addTests(len_one_adapter.adapt(test))
146
 
        result.addTests(len_two_adapter.adapt(test))
147
 
    return result
148
 
 
149
 
 
150
57
def get_diamond_vf(f, trailing_eol=True, left_only=False):
151
58
    """Get a diamond graph to exercise deltas and merges.
152
59
    
175
82
    return f, parents
176
83
 
177
84
 
178
 
def get_diamond_files(files, key_length, trailing_eol=True, left_only=False,
179
 
    nograph=False):
180
 
    """Get a diamond graph to exercise deltas and merges.
181
 
 
182
 
    This creates a 5-node graph in files. If files supports 2-length keys two
183
 
    graphs are made to exercise the support for multiple ids.
184
 
    
185
 
    :param trailing_eol: If True end the last line with \n.
186
 
    :param key_length: The length of keys in files. Currently supports length 1
187
 
        and 2 keys.
188
 
    :param left_only: If True do not add the right and merged nodes.
189
 
    :param nograph: If True, do not provide parents to the add_lines calls;
190
 
        this is useful for tests that need inserted data but have graphless
191
 
        stores.
192
 
    :return: The results of the add_lines calls.
193
 
    """
194
 
    if key_length == 1:
195
 
        prefixes = [()]
196
 
    else:
197
 
        prefixes = [('FileA',), ('FileB',)]
198
 
    # insert a diamond graph to exercise deltas and merges.
199
 
    if trailing_eol:
200
 
        last_char = '\n'
201
 
    else:
202
 
        last_char = ''
203
 
    result = []
204
 
    def get_parents(suffix_list):
205
 
        if nograph:
206
 
            return ()
207
 
        else:
208
 
            result = [prefix + suffix for suffix in suffix_list]
209
 
            return result
210
 
    # we loop over each key because that spreads the inserts across prefixes,
211
 
    # which is how commit operates.
212
 
    for prefix in prefixes:
213
 
        result.append(files.add_lines(prefix + ('origin',), (),
214
 
            ['origin' + last_char]))
215
 
    for prefix in prefixes:
216
 
        result.append(files.add_lines(prefix + ('base',),
217
 
            get_parents([('origin',)]), ['base' + last_char]))
218
 
    for prefix in prefixes:
219
 
        result.append(files.add_lines(prefix + ('left',),
220
 
            get_parents([('base',)]),
221
 
            ['base\n', 'left' + last_char]))
222
 
    if not left_only:
223
 
        for prefix in prefixes:
224
 
            result.append(files.add_lines(prefix + ('right',),
225
 
                get_parents([('base',)]),
226
 
                ['base\n', 'right' + last_char]))
227
 
        for prefix in prefixes:
228
 
            result.append(files.add_lines(prefix + ('merged',),
229
 
                get_parents([('left',), ('right',)]),
230
 
                ['base\n', 'left\n', 'right\n', 'merged' + last_char]))
231
 
    return result
232
 
 
233
 
 
234
85
class VersionedFileTestMixIn(object):
235
86
    """A mixin test class for testing VersionedFiles.
236
87
 
267
118
        f = self.reopen_file(create=True)
268
119
        verify_file(f)
269
120
 
 
121
    def test_get_record_stream_empty(self):
 
122
        """get_record_stream is a replacement for get_data_stream."""
 
123
        f = self.get_file()
 
124
        entries = f.get_record_stream([], 'unordered', False)
 
125
        self.assertEqual([], list(entries))
 
126
 
 
127
    def assertValidStorageKind(self, storage_kind):
 
128
        """Assert that storage_kind is a valid storage_kind."""
 
129
        self.assertSubset([storage_kind],
 
130
            ['mpdiff', 'knit-annotated-ft', 'knit-annotated-delta',
 
131
             'knit-ft', 'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
 
132
             'knit-annotated-delta-gz', 'knit-ft-gz', 'knit-delta-gz'])
 
133
 
 
134
    def capture_stream(self, f, entries, on_seen, parents):
 
135
        """Capture a stream for testing."""
 
136
        for factory in entries:
 
137
            on_seen(factory.key)
 
138
            self.assertValidStorageKind(factory.storage_kind)
 
139
            self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
140
            self.assertEqual(parents[factory.key[0]], factory.parents)
 
141
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
142
                str)
 
143
 
 
144
    def test_get_record_stream_interface(self):
 
145
        """Each item in a stream has to provide a regular interface."""
 
146
        f, parents = get_diamond_vf(self.get_file())
 
147
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
148
            'unordered', False)
 
149
        seen = set()
 
150
        self.capture_stream(f, entries, seen.add, parents)
 
151
        self.assertEqual(set([('base',), ('left',), ('right',), ('merged',)]),
 
152
            seen)
 
153
 
 
154
    def test_get_record_stream_interface_ordered(self):
 
155
        """Each item in a stream has to provide a regular interface."""
 
156
        f, parents = get_diamond_vf(self.get_file())
 
157
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
158
            'topological', False)
 
159
        seen = []
 
160
        self.capture_stream(f, entries, seen.append, parents)
 
161
        self.assertSubset([tuple(seen)],
 
162
            (
 
163
             (('base',), ('left',), ('right',), ('merged',)),
 
164
             (('base',), ('right',), ('left',), ('merged',)),
 
165
            ))
 
166
 
 
167
    def test_get_record_stream_interface_ordered_with_delta_closure(self):
 
168
        """Each item in a stream has to provide a regular interface."""
 
169
        f, parents = get_diamond_vf(self.get_file())
 
170
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
171
            'topological', True)
 
172
        seen = []
 
173
        for factory in entries:
 
174
            seen.append(factory.key)
 
175
            self.assertValidStorageKind(factory.storage_kind)
 
176
            self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
177
            self.assertEqual(parents[factory.key[0]], factory.parents)
 
178
            self.assertEqual(f.get_text(factory.key[0]),
 
179
                factory.get_bytes_as('fulltext'))
 
180
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
181
                str)
 
182
        self.assertSubset([tuple(seen)],
 
183
            (
 
184
             (('base',), ('left',), ('right',), ('merged',)),
 
185
             (('base',), ('right',), ('left',), ('merged',)),
 
186
            ))
 
187
 
 
188
    def test_get_record_stream_unknown_storage_kind_raises(self):
 
189
        """Asking for a storage kind that the stream cannot supply raises."""
 
190
        f, parents = get_diamond_vf(self.get_file())
 
191
        entries = f.get_record_stream(['merged', 'left', 'right', 'base'],
 
192
            'unordered', False)
 
193
        # We track the contents because we should be able to try, fail a
 
194
        # particular kind and then ask for one that works and continue.
 
195
        seen = set()
 
196
        for factory in entries:
 
197
            seen.add(factory.key)
 
198
            self.assertValidStorageKind(factory.storage_kind)
 
199
            self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
200
            self.assertEqual(parents[factory.key[0]], factory.parents)
 
201
            # currently no stream emits mpdiff
 
202
            self.assertRaises(errors.UnavailableRepresentation,
 
203
                factory.get_bytes_as, 'mpdiff')
 
204
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
205
                str)
 
206
        self.assertEqual(set([('base',), ('left',), ('right',), ('merged',)]),
 
207
            seen)
 
208
 
 
209
    def test_get_record_stream_missing_records_are_absent(self):
 
210
        f, parents = get_diamond_vf(self.get_file())
 
211
        entries = f.get_record_stream(['merged', 'left', 'right', 'or', 'base'],
 
212
            'unordered', False)
 
213
        self.assertAbsentRecord(f, parents, entries)
 
214
        entries = f.get_record_stream(['merged', 'left', 'right', 'or', 'base'],
 
215
            'topological', False)
 
216
        self.assertAbsentRecord(f, parents, entries)
 
217
 
 
218
    def assertAbsentRecord(self, f, parents, entries):
 
219
        """Helper for test_get_record_stream_missing_records_are_absent."""
 
220
        seen = set()
 
221
        for factory in entries:
 
222
            seen.add(factory.key)
 
223
            if factory.key == ('or',):
 
224
                self.assertEqual('absent', factory.storage_kind)
 
225
                self.assertEqual(None, factory.sha1)
 
226
                self.assertEqual(None, factory.parents)
 
227
            else:
 
228
                self.assertValidStorageKind(factory.storage_kind)
 
229
                self.assertEqual(f.get_sha1s([factory.key[0]])[0], factory.sha1)
 
230
                self.assertEqual(parents[factory.key[0]], factory.parents)
 
231
                self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
232
                    str)
 
233
        self.assertEqual(
 
234
            set([('base',), ('left',), ('right',), ('merged',), ('or',)]),
 
235
            seen)
 
236
 
 
237
    def test_filter_absent_records(self):
 
238
        """Requested missing records can be filter trivially."""
 
239
        f, parents = get_diamond_vf(self.get_file())
 
240
        entries = f.get_record_stream(['merged', 'left', 'right', 'extra', 'base'],
 
241
            'unordered', False)
 
242
        seen = set()
 
243
        self.capture_stream(f, versionedfile.filter_absent(entries), seen.add,
 
244
            parents)
 
245
        self.assertEqual(set([('base',), ('left',), ('right',), ('merged',)]),
 
246
            seen)
 
247
 
 
248
    def test_insert_record_stream_empty(self):
 
249
        """Inserting an empty record stream should work."""
 
250
        f = self.get_file()
 
251
        stream = []
 
252
        f.insert_record_stream([])
 
253
 
 
254
    def assertIdenticalVersionedFile(self, left, right):
 
255
        """Assert that left and right have the same contents."""
 
256
        self.assertEqual(set(left.versions()), set(right.versions()))
 
257
        self.assertEqual(left.get_parent_map(left.versions()),
 
258
            right.get_parent_map(right.versions()))
 
259
        for v in left.versions():
 
260
            self.assertEqual(left.get_text(v), right.get_text(v))
 
261
 
 
262
    def test_insert_record_stream_fulltexts(self):
 
263
        """Any file should accept a stream of fulltexts."""
 
264
        f = self.get_file()
 
265
        weave_vf = WeaveFile('source', get_transport(self.get_url('.')),
 
266
            create=True, get_scope=self.get_transaction)
 
267
        source, _ = get_diamond_vf(weave_vf)
 
268
        stream = source.get_record_stream(source.versions(), 'topological',
 
269
            False)
 
270
        f.insert_record_stream(stream)
 
271
        self.assertIdenticalVersionedFile(f, source)
 
272
 
 
273
    def test_insert_record_stream_fulltexts_noeol(self):
 
274
        """Any file should accept a stream of fulltexts."""
 
275
        f = self.get_file()
 
276
        weave_vf = WeaveFile('source', get_transport(self.get_url('.')),
 
277
            create=True, get_scope=self.get_transaction)
 
278
        source, _ = get_diamond_vf(weave_vf, trailing_eol=False)
 
279
        stream = source.get_record_stream(source.versions(), 'topological',
 
280
            False)
 
281
        f.insert_record_stream(stream)
 
282
        self.assertIdenticalVersionedFile(f, source)
 
283
 
 
284
    def test_insert_record_stream_annotated_knits(self):
 
285
        """Any file should accept a stream from plain knits."""
 
286
        f = self.get_file()
 
287
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
288
            create=True)
 
289
        get_diamond_vf(source)
 
290
        stream = source.get_record_stream(source.versions(), 'topological',
 
291
            False)
 
292
        f.insert_record_stream(stream)
 
293
        self.assertIdenticalVersionedFile(f, source)
 
294
 
 
295
    def test_insert_record_stream_annotated_knits_noeol(self):
 
296
        """Any file should accept a stream from plain knits."""
 
297
        f = self.get_file()
 
298
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
299
            create=True)
 
300
        get_diamond_vf(source, trailing_eol=False)
 
301
        stream = source.get_record_stream(source.versions(), 'topological',
 
302
            False)
 
303
        f.insert_record_stream(stream)
 
304
        self.assertIdenticalVersionedFile(f, source)
 
305
 
 
306
    def test_insert_record_stream_plain_knits(self):
 
307
        """Any file should accept a stream from plain knits."""
 
308
        f = self.get_file()
 
309
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
310
            create=True, factory=KnitPlainFactory())
 
311
        get_diamond_vf(source)
 
312
        stream = source.get_record_stream(source.versions(), 'topological',
 
313
            False)
 
314
        f.insert_record_stream(stream)
 
315
        self.assertIdenticalVersionedFile(f, source)
 
316
 
 
317
    def test_insert_record_stream_plain_knits_noeol(self):
 
318
        """Any file should accept a stream from plain knits."""
 
319
        f = self.get_file()
 
320
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
321
            create=True, factory=KnitPlainFactory())
 
322
        get_diamond_vf(source, trailing_eol=False)
 
323
        stream = source.get_record_stream(source.versions(), 'topological',
 
324
            False)
 
325
        f.insert_record_stream(stream)
 
326
        self.assertIdenticalVersionedFile(f, source)
 
327
 
 
328
    def test_insert_record_stream_existing_keys(self):
 
329
        """Inserting keys already in a file should not error."""
 
330
        f = self.get_file()
 
331
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
332
            create=True, factory=KnitPlainFactory())
 
333
        get_diamond_vf(source)
 
334
        # insert some keys into f.
 
335
        get_diamond_vf(f, left_only=True)
 
336
        stream = source.get_record_stream(source.versions(), 'topological',
 
337
            False)
 
338
        f.insert_record_stream(stream)
 
339
        self.assertIdenticalVersionedFile(f, source)
 
340
 
 
341
    def test_insert_record_stream_missing_keys(self):
 
342
        """Inserting a stream with absent keys should raise an error."""
 
343
        f = self.get_file()
 
344
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
345
            create=True, factory=KnitPlainFactory())
 
346
        stream = source.get_record_stream(['missing'], 'topological',
 
347
            False)
 
348
        self.assertRaises(errors.RevisionNotPresent, f.insert_record_stream,
 
349
            stream)
 
350
 
 
351
    def test_insert_record_stream_out_of_order(self):
 
352
        """An out of order stream can either error or work."""
 
353
        f, parents = get_diamond_vf(self.get_file())
 
354
        origin_entries = f.get_record_stream(['origin'], 'unordered', False)
 
355
        end_entries = f.get_record_stream(['merged', 'left'],
 
356
            'topological', False)
 
357
        start_entries = f.get_record_stream(['right', 'base'],
 
358
            'topological', False)
 
359
        entries = chain(origin_entries, end_entries, start_entries)
 
360
        target = self.get_file('target')
 
361
        try:
 
362
            target.insert_record_stream(entries)
 
363
        except RevisionNotPresent:
 
364
            # Must not have corrupted the file.
 
365
            target.check()
 
366
        else:
 
367
            self.assertIdenticalVersionedFile(f, target)
 
368
 
 
369
    def test_insert_record_stream_delta_missing_basis_no_corruption(self):
 
370
        """Insertion where a needed basis is not included aborts safely."""
 
371
        # Annotated source - deltas can be used in any knit.
 
372
        source = make_file_knit('source', get_transport(self.get_url('.')),
 
373
            create=True)
 
374
        get_diamond_vf(source)
 
375
        entries = source.get_record_stream(['origin', 'merged'], 'unordered', False)
 
376
        f = self.get_file()
 
377
        self.assertRaises(RevisionNotPresent, f.insert_record_stream, entries)
 
378
        f.check()
 
379
        self.assertFalse(f.has_version('merged'))
 
380
 
270
381
    def test_adds_with_parent_texts(self):
271
382
        f = self.get_file()
272
383
        parent_texts = {}
419
530
        self.assertRaises(errors.ReservedId, vf.get_lines, 'b:')
420
531
        self.assertRaises(errors.ReservedId, vf.get_text, 'b:')
421
532
 
422
 
    def test_add_unchanged_last_line_noeol_snapshot(self):
423
 
        """Add a text with an unchanged last line with no eol should work."""
424
 
        # Test adding this in a number of chain lengths; because the interface
425
 
        # for VersionedFile does not allow forcing a specific chain length, we
426
 
        # just use a small base to get the first snapshot, then a much longer
427
 
        # first line for the next add (which will make the third add snapshot)
428
 
        # and so on. 20 has been chosen as an aribtrary figure - knits use 200
429
 
        # as a capped delta length, but ideally we would have some way of
430
 
        # tuning the test to the store (e.g. keep going until a snapshot
431
 
        # happens).
432
 
        for length in range(20):
433
 
            version_lines = {}
434
 
            vf = self.get_file('case-%d' % length)
435
 
            prefix = 'step-%d'
436
 
            parents = []
437
 
            for step in range(length):
438
 
                version = prefix % step
439
 
                lines = (['prelude \n'] * step) + ['line']
440
 
                vf.add_lines(version, parents, lines)
441
 
                version_lines[version] = lines
442
 
                parents = [version]
443
 
            vf.add_lines('no-eol', parents, ['line'])
444
 
            vf.get_texts(version_lines.keys())
445
 
            self.assertEqualDiff('line', vf.get_text('no-eol'))
446
 
 
447
 
    def test_get_texts_eol_variation(self):
448
 
        # similar to the failure in <http://bugs.launchpad.net/234748>
449
 
        vf = self.get_file()
450
 
        sample_text_nl = ["line\n"]
451
 
        sample_text_no_nl = ["line"]
452
 
        versions = []
453
 
        version_lines = {}
454
 
        parents = []
455
 
        for i in range(4):
456
 
            version = 'v%d' % i
457
 
            if i % 2:
458
 
                lines = sample_text_nl
459
 
            else:
460
 
                lines = sample_text_no_nl
461
 
            # left_matching blocks is an internal api; it operates on the
462
 
            # *internal* representation for a knit, which is with *all* lines
463
 
            # being normalised to end with \n - even the final line in a no_nl
464
 
            # file. Using it here ensures that a broken internal implementation
465
 
            # (which is what this test tests) will generate a correct line
466
 
            # delta (which is to say, an empty delta).
467
 
            vf.add_lines(version, parents, lines,
468
 
                left_matching_blocks=[(0, 0, 1)])
469
 
            parents = [version]
470
 
            versions.append(version)
471
 
            version_lines[version] = lines
472
 
        vf.check()
473
 
        vf.get_texts(versions)
474
 
        vf.get_texts(reversed(versions))
475
 
 
476
 
    def test_add_lines_with_matching_blocks_noeol_last_line(self):
477
 
        """Add a text with an unchanged last line with no eol should work."""
478
 
        from bzrlib import multiparent
479
 
        # Hand verified sha1 of the text we're adding.
480
 
        sha1 = '6a1d115ec7b60afb664dc14890b5af5ce3c827a4'
481
 
        # Create a mpdiff which adds a new line before the trailing line, and
482
 
        # reuse the last line unaltered (which can cause annotation reuse).
483
 
        # Test adding this in two situations:
484
 
        # On top of a new insertion
485
 
        vf = self.get_file('fulltext')
486
 
        vf.add_lines('noeol', [], ['line'])
487
 
        vf.add_lines('noeol2', ['noeol'], ['newline\n', 'line'],
488
 
            left_matching_blocks=[(0, 1, 1)])
489
 
        self.assertEqualDiff('newline\nline', vf.get_text('noeol2'))
490
 
        # On top of a delta
491
 
        vf = self.get_file('delta')
492
 
        vf.add_lines('base', [], ['line'])
493
 
        vf.add_lines('noeol', ['base'], ['prelude\n', 'line'])
494
 
        vf.add_lines('noeol2', ['noeol'], ['newline\n', 'line'],
495
 
            left_matching_blocks=[(1, 1, 1)])
496
 
        self.assertEqualDiff('newline\nline', vf.get_text('noeol2'))
497
 
 
498
533
    def test_make_mpdiffs(self):
499
534
        from bzrlib import multiparent
500
535
        vf = self.get_file('foo')
503
538
        for version in multiparent.topo_iter(vf):
504
539
            mpdiff = vf.make_mpdiffs([version])[0]
505
540
            new_vf.add_mpdiffs([(version, vf.get_parent_map([version])[version],
506
 
                                 vf.get_sha1s([version])[version], mpdiff)])
 
541
                                 vf.get_sha1s([version])[0], mpdiff)])
507
542
            self.assertEqualDiff(vf.get_text(version),
508
543
                                 new_vf.get_text(version))
509
544
 
510
 
    def test_make_mpdiffs_with_ghosts(self):
511
 
        vf = self.get_file('foo')
512
 
        try:
513
 
            vf.add_lines_with_ghosts('text', ['ghost'], ['line\n'])
514
 
        except NotImplementedError:
515
 
            # old Weave formats do not allow ghosts
516
 
            return
517
 
        self.assertRaises(errors.RevisionNotPresent, vf.make_mpdiffs, ['ghost'])
518
 
 
519
545
    def _setup_for_deltas(self, f):
520
546
        self.assertFalse(f.has_version('base'))
521
547
        # add texts that should trip the knit maximum delta chain threshold
623
649
        self._transaction = 'after'
624
650
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
625
651
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
 
652
        self.assertRaises(errors.OutSideTransaction, self.applyDeprecated,
 
653
            one_five, f.join, '')
626
654
        
627
655
    def test_copy_to(self):
628
656
        f = self.get_file()
832
860
                          'base',
833
861
                          [],
834
862
                          [])
 
863
        self.assertRaises(errors.ReadOnlyError, self.applyDeprecated, one_five,
 
864
            vf.join, 'base')
835
865
    
836
866
    def test_get_sha1s(self):
837
867
        # check the sha1 data is available
842
872
        vf.add_lines('b', ['a'], ['a\n'])
843
873
        # a file differing only in last newline.
844
874
        vf.add_lines('c', [], ['a'])
845
 
        self.assertEqual({
846
 
            'a': '3f786850e387550fdab836ed7e6dc881de23001b',
847
 
            'c': '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
848
 
            'b': '3f786850e387550fdab836ed7e6dc881de23001b',
849
 
            },
850
 
            vf.get_sha1s(['a', 'c', 'b']))
 
875
        self.assertEqual(['3f786850e387550fdab836ed7e6dc881de23001b',
 
876
                          '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
 
877
                          '3f786850e387550fdab836ed7e6dc881de23001b'],
 
878
                          vf.get_sha1s(['a', 'c', 'b']))
851
879
        
852
880
 
853
881
class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
906
934
        return WeaveFile
907
935
 
908
936
 
 
937
class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
938
 
 
939
    def get_file(self, name='foo', create=True):
 
940
        return make_file_knit(name, get_transport(self.get_url('.')),
 
941
            delta=True, create=True, get_scope=self.get_transaction)
 
942
 
 
943
    def get_factory(self):
 
944
        return make_file_knit
 
945
 
 
946
    def get_file_corrupted_text(self):
 
947
        knit = self.get_file()
 
948
        knit.add_lines('v1', [], ['hello\n'])
 
949
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
 
950
        return knit
 
951
 
 
952
    def reopen_file(self, name='foo', create=False):
 
953
        return self.get_file(name, create)
 
954
 
 
955
    def test_detection(self):
 
956
        knit = self.get_file()
 
957
        knit.check()
 
958
 
 
959
    def test_no_implicit_create(self):
 
960
        self.assertRaises(errors.NoSuchFile, self.get_factory(), 'foo',
 
961
            get_transport(self.get_url('.')))
 
962
 
 
963
 
 
964
class TestPlaintextKnit(TestKnit):
 
965
    """Test a knit with no cached annotations"""
 
966
 
 
967
    def get_file(self, name='foo', create=True):
 
968
        return make_file_knit(name, get_transport(self.get_url('.')),
 
969
            delta=True, create=create, get_scope=self.get_transaction,
 
970
            factory=_mod_knit.KnitPlainFactory())
 
971
 
 
972
 
909
973
class TestPlanMergeVersionedFile(TestCaseWithMemoryTransport):
910
974
 
911
975
    def setUp(self):
912
976
        TestCaseWithMemoryTransport.setUp(self)
913
 
        mapper = PrefixMapper()
914
 
        factory = make_file_factory(True, mapper)
915
 
        self.vf1 = factory(self.get_transport('root-1'))
916
 
        self.vf2 = factory(self.get_transport('root-2'))
917
 
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root')
918
 
        self.plan_merge_vf.fallback_versionedfiles.extend([self.vf1, self.vf2])
 
977
        self.vf1 = make_file_knit('root', self.get_transport(), create=True)
 
978
        self.vf2 = make_file_knit('root', self.get_transport(), create=True)
 
979
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root',
 
980
            [self.vf1, self.vf2])
919
981
 
920
982
    def test_add_lines(self):
921
 
        self.plan_merge_vf.add_lines(('root', 'a:'), [], [])
922
 
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
923
 
            ('root', 'a'), [], [])
924
 
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
925
 
            ('root', 'a:'), None, [])
926
 
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
927
 
            ('root', 'a:'), [], None)
 
983
        self.plan_merge_vf.add_lines('a:', [], [])
 
984
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a', [],
 
985
                          [])
 
986
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', None,
 
987
                          [])
 
988
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', [],
 
989
                          None)
 
990
 
 
991
    def test_ancestry(self):
 
992
        self.vf1.add_lines('A', [], [])
 
993
        self.vf1.add_lines('B', ['A'], [])
 
994
        self.plan_merge_vf.add_lines('C:', ['B'], [])
 
995
        self.plan_merge_vf.add_lines('D:', ['C:'], [])
 
996
        self.assertEqual(set(['A', 'B', 'C:', 'D:']),
 
997
            self.plan_merge_vf.get_ancestry('D:', topo_sorted=False))
928
998
 
929
999
    def setup_abcde(self):
930
 
        self.vf1.add_lines(('root', 'A'), [], ['a'])
931
 
        self.vf1.add_lines(('root', 'B'), [('root', 'A')], ['b'])
932
 
        self.vf2.add_lines(('root', 'C'), [], ['c'])
933
 
        self.vf2.add_lines(('root', 'D'), [('root', 'C')], ['d'])
934
 
        self.plan_merge_vf.add_lines(('root', 'E:'),
935
 
            [('root', 'B'), ('root', 'D')], ['e'])
 
1000
        self.vf1.add_lines('A', [], ['a'])
 
1001
        self.vf1.add_lines('B', ['A'], ['b'])
 
1002
        self.vf2.add_lines('C', [], ['c'])
 
1003
        self.vf2.add_lines('D', ['C'], ['d'])
 
1004
        self.plan_merge_vf.add_lines('E:', ['B', 'D'], ['e'])
 
1005
 
 
1006
    def test_ancestry_uses_all_versionedfiles(self):
 
1007
        self.setup_abcde()
 
1008
        self.assertEqual(set(['A', 'B', 'C', 'D', 'E:']),
 
1009
            self.plan_merge_vf.get_ancestry('E:', topo_sorted=False))
 
1010
 
 
1011
    def test_ancestry_raises_revision_not_present(self):
 
1012
        error = self.assertRaises(errors.RevisionNotPresent,
 
1013
                                  self.plan_merge_vf.get_ancestry, 'E:', False)
 
1014
        self.assertContainsRe(str(error), '{E:} not present in "root"')
936
1015
 
937
1016
    def test_get_parents(self):
938
1017
        self.setup_abcde()
939
 
        self.assertEqual({('root', 'B'):(('root', 'A'),)},
940
 
            self.plan_merge_vf.get_parent_map([('root', 'B')]))
941
 
        self.assertEqual({('root', 'D'):(('root', 'C'),)},
942
 
            self.plan_merge_vf.get_parent_map([('root', 'D')]))
943
 
        self.assertEqual({('root', 'E:'):(('root', 'B'),('root', 'D'))},
944
 
            self.plan_merge_vf.get_parent_map([('root', 'E:')]))
945
 
        self.assertEqual({},
946
 
            self.plan_merge_vf.get_parent_map([('root', 'F')]))
 
1018
        self.assertEqual({'B':('A',)}, self.plan_merge_vf.get_parent_map(['B']))
 
1019
        self.assertEqual({'D':('C',)}, self.plan_merge_vf.get_parent_map(['D']))
 
1020
        self.assertEqual({'E:':('B', 'D')},
 
1021
            self.plan_merge_vf.get_parent_map(['E:']))
 
1022
        self.assertEqual({}, self.plan_merge_vf.get_parent_map(['F']))
947
1023
        self.assertEqual({
948
 
                ('root', 'B'):(('root', 'A'),),
949
 
                ('root', 'D'):(('root', 'C'),),
950
 
                ('root', 'E:'):(('root', 'B'),('root', 'D')),
951
 
                },
952
 
            self.plan_merge_vf.get_parent_map(
953
 
                [('root', 'B'), ('root', 'D'), ('root', 'E:'), ('root', 'F')]))
 
1024
                'B':('A',),
 
1025
                'D':('C',),
 
1026
                'E:':('B', 'D'),
 
1027
                }, self.plan_merge_vf.get_parent_map(['B', 'D', 'E:', 'F']))
954
1028
 
955
 
    def test_get_record_stream(self):
 
1029
    def test_get_lines(self):
956
1030
        self.setup_abcde()
957
 
        def get_record(suffix):
958
 
            return self.plan_merge_vf.get_record_stream(
959
 
                [('root', suffix)], 'unordered', True).next()
960
 
        self.assertEqual('a', get_record('A').get_bytes_as('fulltext'))
961
 
        self.assertEqual('c', get_record('C').get_bytes_as('fulltext'))
962
 
        self.assertEqual('e', get_record('E:').get_bytes_as('fulltext'))
963
 
        self.assertEqual('absent', get_record('F').storage_kind)
 
1031
        self.assertEqual(['a'], self.plan_merge_vf.get_lines('A'))
 
1032
        self.assertEqual(['c'], self.plan_merge_vf.get_lines('C'))
 
1033
        self.assertEqual(['e'], self.plan_merge_vf.get_lines('E:'))
 
1034
        error = self.assertRaises(errors.RevisionNotPresent,
 
1035
                                  self.plan_merge_vf.get_lines, 'F')
 
1036
        self.assertContainsRe(str(error), '{F} not present in "root"')
 
1037
 
 
1038
 
 
1039
class InterString(versionedfile.InterVersionedFile):
 
1040
    """An inter-versionedfile optimised code path for strings.
 
1041
 
 
1042
    This is for use during testing where we use strings as versionedfiles
 
1043
    so that none of the default regsitered interversionedfile classes will
 
1044
    match - which lets us test the match logic.
 
1045
    """
 
1046
 
 
1047
    @staticmethod
 
1048
    def is_compatible(source, target):
 
1049
        """InterString is compatible with strings-as-versionedfiles."""
 
1050
        return isinstance(source, str) and isinstance(target, str)
 
1051
 
 
1052
 
 
1053
# TODO this and the InterRepository core logic should be consolidatable
 
1054
# if we make the registry a separate class though we still need to 
 
1055
# test the behaviour in the active registry to catch failure-to-handle-
 
1056
# stange-objects
 
1057
class TestInterVersionedFile(TestCaseWithMemoryTransport):
 
1058
 
 
1059
    def test_get_default_inter_versionedfile(self):
 
1060
        # test that the InterVersionedFile.get(a, b) probes
 
1061
        # for a class where is_compatible(a, b) returns
 
1062
        # true and returns a default interversionedfile otherwise.
 
1063
        # This also tests that the default registered optimised interversionedfile
 
1064
        # classes do not barf inappropriately when a surprising versionedfile type
 
1065
        # is handed to them.
 
1066
        dummy_a = "VersionedFile 1."
 
1067
        dummy_b = "VersionedFile 2."
 
1068
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
1069
 
 
1070
    def assertGetsDefaultInterVersionedFile(self, a, b):
 
1071
        """Asserts that InterVersionedFile.get(a, b) -> the default."""
 
1072
        inter = versionedfile.InterVersionedFile.get(a, b)
 
1073
        self.assertEqual(versionedfile.InterVersionedFile,
 
1074
                         inter.__class__)
 
1075
        self.assertEqual(a, inter.source)
 
1076
        self.assertEqual(b, inter.target)
 
1077
 
 
1078
    def test_register_inter_versionedfile_class(self):
 
1079
        # test that a optimised code path provider - a
 
1080
        # InterVersionedFile subclass can be registered and unregistered
 
1081
        # and that it is correctly selected when given a versionedfile
 
1082
        # pair that it returns true on for the is_compatible static method
 
1083
        # check
 
1084
        dummy_a = "VersionedFile 1."
 
1085
        dummy_b = "VersionedFile 2."
 
1086
        versionedfile.InterVersionedFile.register_optimiser(InterString)
 
1087
        try:
 
1088
            # we should get the default for something InterString returns False
 
1089
            # to
 
1090
            self.assertFalse(InterString.is_compatible(dummy_a, None))
 
1091
            self.assertGetsDefaultInterVersionedFile(dummy_a, None)
 
1092
            # and we should get an InterString for a pair it 'likes'
 
1093
            self.assertTrue(InterString.is_compatible(dummy_a, dummy_b))
 
1094
            inter = versionedfile.InterVersionedFile.get(dummy_a, dummy_b)
 
1095
            self.assertEqual(InterString, inter.__class__)
 
1096
            self.assertEqual(dummy_a, inter.source)
 
1097
            self.assertEqual(dummy_b, inter.target)
 
1098
        finally:
 
1099
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
 
1100
        # now we should get the default InterVersionedFile object again.
 
1101
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
964
1102
 
965
1103
 
966
1104
class TestReadonlyHttpMixin(object):
993
1131
        return WeaveFile
994
1132
 
995
1133
 
 
1134
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
 
1135
 
 
1136
    def get_file(self):
 
1137
        return make_file_knit('foo', get_transport(self.get_url('.')),
 
1138
            delta=True, create=True, get_scope=self.get_transaction)
 
1139
 
 
1140
    def get_factory(self):
 
1141
        return make_file_knit
 
1142
 
 
1143
 
996
1144
class MergeCasesMixin(object):
997
1145
 
998
1146
    def doMerge(self, base, a, b, mp):
1230
1378
        self._test_merge_from_strings(base, a, b, result)
1231
1379
 
1232
1380
 
 
1381
class TestKnitMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
1382
 
 
1383
    def get_file(self, name='foo'):
 
1384
        return make_file_knit(name, get_transport(self.get_url('.')),
 
1385
                                 delta=True, create=True)
 
1386
 
 
1387
    def log_contents(self, w):
 
1388
        pass
 
1389
 
 
1390
 
1233
1391
class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
1234
1392
 
1235
1393
    def get_file(self, name='foo'):
1270
1428
            self.assertIsInstance(adapter, klass)
1271
1429
 
1272
1430
    def get_knit(self, annotated=True):
1273
 
        mapper = ConstantMapper('knit')
1274
 
        transport = self.get_transport()
1275
 
        return make_file_factory(annotated, mapper)(transport)
 
1431
        if annotated:
 
1432
            factory = KnitAnnotateFactory()
 
1433
        else:
 
1434
            factory = KnitPlainFactory()
 
1435
        return make_file_knit('knit', self.get_transport('.'), delta=True,
 
1436
            create=True, factory=factory)
1276
1437
 
1277
1438
    def helpGetBytes(self, f, ft_adapter, delta_adapter):
1278
1439
        """Grab the interested adapted texts for tests."""
1279
1440
        # origin is a fulltext
1280
 
        entries = f.get_record_stream([('origin',)], 'unordered', False)
 
1441
        entries = f.get_record_stream(['origin'], 'unordered', False)
1281
1442
        base = entries.next()
1282
1443
        ft_data = ft_adapter.get_bytes(base, base.get_bytes_as(base.storage_kind))
1283
1444
        # merged is both a delta and multiple parents.
1284
 
        entries = f.get_record_stream([('merged',)], 'unordered', False)
 
1445
        entries = f.get_record_stream(['merged'], 'unordered', False)
1285
1446
        merged = entries.next()
1286
1447
        delta_data = delta_adapter.get_bytes(merged,
1287
1448
            merged.get_bytes_as(merged.storage_kind))
1290
1451
    def test_deannotation_noeol(self):
1291
1452
        """Test converting annotated knits to unannotated knits."""
1292
1453
        # we need a full text, and a delta
1293
 
        f = self.get_knit()
1294
 
        get_diamond_files(f, 1, trailing_eol=False)
 
1454
        f, parents = get_diamond_vf(self.get_knit(), trailing_eol=False)
1295
1455
        ft_data, delta_data = self.helpGetBytes(f,
1296
1456
            _mod_knit.FTAnnotatedToUnannotated(None),
1297
1457
            _mod_knit.DeltaAnnotatedToUnannotated(None))
1308
1468
    def test_deannotation(self):
1309
1469
        """Test converting annotated knits to unannotated knits."""
1310
1470
        # we need a full text, and a delta
1311
 
        f = self.get_knit()
1312
 
        get_diamond_files(f, 1)
 
1471
        f, parents = get_diamond_vf(self.get_knit())
1313
1472
        ft_data, delta_data = self.helpGetBytes(f,
1314
1473
            _mod_knit.FTAnnotatedToUnannotated(None),
1315
1474
            _mod_knit.DeltaAnnotatedToUnannotated(None))
1326
1485
    def test_annotated_to_fulltext_no_eol(self):
1327
1486
        """Test adapting annotated knits to full texts (for -> weaves)."""
1328
1487
        # we need a full text, and a delta
1329
 
        f = self.get_knit()
1330
 
        get_diamond_files(f, 1, trailing_eol=False)
 
1488
        f, parents = get_diamond_vf(self.get_knit(), trailing_eol=False)
1331
1489
        # Reconstructing a full text requires a backing versioned file, and it
1332
1490
        # must have the base lines requested from it.
1333
 
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
 
1491
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
1334
1492
        ft_data, delta_data = self.helpGetBytes(f,
1335
1493
            _mod_knit.FTAnnotatedToFullText(None),
1336
1494
            _mod_knit.DeltaAnnotatedToFullText(logged_vf))
1337
1495
        self.assertEqual('origin', ft_data)
1338
1496
        self.assertEqual('base\nleft\nright\nmerged', delta_data)
1339
 
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
1340
 
            True)], logged_vf.calls)
 
1497
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
1341
1498
 
1342
1499
    def test_annotated_to_fulltext(self):
1343
1500
        """Test adapting annotated knits to full texts (for -> weaves)."""
1344
1501
        # we need a full text, and a delta
1345
 
        f = self.get_knit()
1346
 
        get_diamond_files(f, 1)
 
1502
        f, parents = get_diamond_vf(self.get_knit())
1347
1503
        # Reconstructing a full text requires a backing versioned file, and it
1348
1504
        # must have the base lines requested from it.
1349
 
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
 
1505
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
1350
1506
        ft_data, delta_data = self.helpGetBytes(f,
1351
1507
            _mod_knit.FTAnnotatedToFullText(None),
1352
1508
            _mod_knit.DeltaAnnotatedToFullText(logged_vf))
1353
1509
        self.assertEqual('origin\n', ft_data)
1354
1510
        self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
1355
 
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
1356
 
            True)], logged_vf.calls)
 
1511
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
1357
1512
 
1358
1513
    def test_unannotated_to_fulltext(self):
1359
1514
        """Test adapting unannotated knits to full texts.
1361
1516
        This is used for -> weaves, and for -> annotated knits.
1362
1517
        """
1363
1518
        # we need a full text, and a delta
1364
 
        f = self.get_knit(annotated=False)
1365
 
        get_diamond_files(f, 1)
 
1519
        f, parents = get_diamond_vf(self.get_knit(annotated=False))
1366
1520
        # Reconstructing a full text requires a backing versioned file, and it
1367
1521
        # must have the base lines requested from it.
1368
 
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
 
1522
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
1369
1523
        ft_data, delta_data = self.helpGetBytes(f,
1370
1524
            _mod_knit.FTPlainToFullText(None),
1371
1525
            _mod_knit.DeltaPlainToFullText(logged_vf))
1372
1526
        self.assertEqual('origin\n', ft_data)
1373
1527
        self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
1374
 
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
1375
 
            True)], logged_vf.calls)
 
1528
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
1376
1529
 
1377
1530
    def test_unannotated_to_fulltext_no_eol(self):
1378
1531
        """Test adapting unannotated knits to full texts.
1380
1533
        This is used for -> weaves, and for -> annotated knits.
1381
1534
        """
1382
1535
        # we need a full text, and a delta
1383
 
        f = self.get_knit(annotated=False)
1384
 
        get_diamond_files(f, 1, trailing_eol=False)
 
1536
        f, parents = get_diamond_vf(self.get_knit(annotated=False),
 
1537
            trailing_eol=False)
1385
1538
        # Reconstructing a full text requires a backing versioned file, and it
1386
1539
        # must have the base lines requested from it.
1387
 
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
 
1540
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
1388
1541
        ft_data, delta_data = self.helpGetBytes(f,
1389
1542
            _mod_knit.FTPlainToFullText(None),
1390
1543
            _mod_knit.DeltaPlainToFullText(logged_vf))
1391
1544
        self.assertEqual('origin', ft_data)
1392
1545
        self.assertEqual('base\nleft\nright\nmerged', delta_data)
1393
 
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
1394
 
            True)], logged_vf.calls)
1395
 
 
1396
 
 
1397
 
class TestKeyMapper(TestCaseWithMemoryTransport):
1398
 
    """Tests for various key mapping logic."""
1399
 
 
1400
 
    def test_identity_mapper(self):
1401
 
        mapper = versionedfile.ConstantMapper("inventory")
1402
 
        self.assertEqual("inventory", mapper.map(('foo@ar',)))
1403
 
        self.assertEqual("inventory", mapper.map(('quux',)))
1404
 
 
1405
 
    def test_prefix_mapper(self):
1406
 
        #format5: plain
1407
 
        mapper = versionedfile.PrefixMapper()
1408
 
        self.assertEqual("file-id", mapper.map(("file-id", "revision-id")))
1409
 
        self.assertEqual("new-id", mapper.map(("new-id", "revision-id")))
1410
 
        self.assertEqual(('file-id',), mapper.unmap("file-id"))
1411
 
        self.assertEqual(('new-id',), mapper.unmap("new-id"))
1412
 
 
1413
 
    def test_hash_prefix_mapper(self):
1414
 
        #format6: hash + plain
1415
 
        mapper = versionedfile.HashPrefixMapper()
1416
 
        self.assertEqual("9b/file-id", mapper.map(("file-id", "revision-id")))
1417
 
        self.assertEqual("45/new-id", mapper.map(("new-id", "revision-id")))
1418
 
        self.assertEqual(('file-id',), mapper.unmap("9b/file-id"))
1419
 
        self.assertEqual(('new-id',), mapper.unmap("45/new-id"))
1420
 
 
1421
 
    def test_hash_escaped_mapper(self):
1422
 
        #knit1: hash + escaped
1423
 
        mapper = versionedfile.HashEscapedPrefixMapper()
1424
 
        self.assertEqual("88/%2520", mapper.map((" ", "revision-id")))
1425
 
        self.assertEqual("ed/fil%2545-%2549d", mapper.map(("filE-Id",
1426
 
            "revision-id")))
1427
 
        self.assertEqual("88/ne%2557-%2549d", mapper.map(("neW-Id",
1428
 
            "revision-id")))
1429
 
        self.assertEqual(('filE-Id',), mapper.unmap("ed/fil%2545-%2549d"))
1430
 
        self.assertEqual(('neW-Id',), mapper.unmap("88/ne%2557-%2549d"))
1431
 
 
1432
 
 
1433
 
class TestVersionedFiles(TestCaseWithMemoryTransport):
1434
 
    """Tests for the multiple-file variant of VersionedFile."""
1435
 
 
1436
 
    def get_versionedfiles(self, relpath='files'):
1437
 
        transport = self.get_transport(relpath)
1438
 
        if relpath != '.':
1439
 
            transport.mkdir('.')
1440
 
        files = self.factory(transport)
1441
 
        if self.cleanup is not None:
1442
 
            self.addCleanup(lambda:self.cleanup(files))
1443
 
        return files
1444
 
 
1445
 
    def test_annotate(self):
1446
 
        files = self.get_versionedfiles()
1447
 
        self.get_diamond_files(files)
1448
 
        if self.key_length == 1:
1449
 
            prefix = ()
1450
 
        else:
1451
 
            prefix = ('FileA',)
1452
 
        # introduced full text
1453
 
        origins = files.annotate(prefix + ('origin',))
1454
 
        self.assertEqual([
1455
 
            (prefix + ('origin',), 'origin\n')],
1456
 
            origins)
1457
 
        # a delta
1458
 
        origins = files.annotate(prefix + ('base',))
1459
 
        self.assertEqual([
1460
 
            (prefix + ('base',), 'base\n')],
1461
 
            origins)
1462
 
        # a merge
1463
 
        origins = files.annotate(prefix + ('merged',))
1464
 
        if self.graph:
1465
 
            self.assertEqual([
1466
 
                (prefix + ('base',), 'base\n'),
1467
 
                (prefix + ('left',), 'left\n'),
1468
 
                (prefix + ('right',), 'right\n'),
1469
 
                (prefix + ('merged',), 'merged\n')
1470
 
                ],
1471
 
                origins)
1472
 
        else:
1473
 
            # Without a graph everything is new.
1474
 
            self.assertEqual([
1475
 
                (prefix + ('merged',), 'base\n'),
1476
 
                (prefix + ('merged',), 'left\n'),
1477
 
                (prefix + ('merged',), 'right\n'),
1478
 
                (prefix + ('merged',), 'merged\n')
1479
 
                ],
1480
 
                origins)
1481
 
        self.assertRaises(RevisionNotPresent,
1482
 
            files.annotate, prefix + ('missing-key',))
1483
 
 
1484
 
    def test_construct(self):
1485
 
        """Each parameterised test can be constructed on a transport."""
1486
 
        files = self.get_versionedfiles()
1487
 
 
1488
 
    def get_diamond_files(self, files, trailing_eol=True, left_only=False):
1489
 
        return get_diamond_files(files, self.key_length,
1490
 
            trailing_eol=trailing_eol, nograph=not self.graph,
1491
 
            left_only=left_only)
1492
 
 
1493
 
    def test_add_lines_return(self):
1494
 
        files = self.get_versionedfiles()
1495
 
        # save code by using the stock data insertion helper.
1496
 
        adds = self.get_diamond_files(files)
1497
 
        results = []
1498
 
        # We can only validate the first 2 elements returned from add_lines.
1499
 
        for add in adds:
1500
 
            self.assertEqual(3, len(add))
1501
 
            results.append(add[:2])
1502
 
        if self.key_length == 1:
1503
 
            self.assertEqual([
1504
 
                ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
1505
 
                ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
1506
 
                ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
1507
 
                ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
1508
 
                ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
1509
 
                results)
1510
 
        elif self.key_length == 2:
1511
 
            self.assertEqual([
1512
 
                ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
1513
 
                ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
1514
 
                ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
1515
 
                ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
1516
 
                ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
1517
 
                ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
1518
 
                ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
1519
 
                ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
1520
 
                ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23),
1521
 
                ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
1522
 
                results)
1523
 
 
1524
 
    def test_empty_lines(self):
1525
 
        """Empty files can be stored."""
1526
 
        f = self.get_versionedfiles()
1527
 
        key_a = self.get_simple_key('a')
1528
 
        f.add_lines(key_a, [], [])
1529
 
        self.assertEqual('',
1530
 
            f.get_record_stream([key_a], 'unordered', True
1531
 
                ).next().get_bytes_as('fulltext'))
1532
 
        key_b = self.get_simple_key('b')
1533
 
        f.add_lines(key_b, self.get_parents([key_a]), [])
1534
 
        self.assertEqual('',
1535
 
            f.get_record_stream([key_b], 'unordered', True
1536
 
                ).next().get_bytes_as('fulltext'))
1537
 
 
1538
 
    def test_newline_only(self):
1539
 
        f = self.get_versionedfiles()
1540
 
        key_a = self.get_simple_key('a')
1541
 
        f.add_lines(key_a, [], ['\n'])
1542
 
        self.assertEqual('\n',
1543
 
            f.get_record_stream([key_a], 'unordered', True
1544
 
                ).next().get_bytes_as('fulltext'))
1545
 
        key_b = self.get_simple_key('b')
1546
 
        f.add_lines(key_b, self.get_parents([key_a]), ['\n'])
1547
 
        self.assertEqual('\n',
1548
 
            f.get_record_stream([key_b], 'unordered', True
1549
 
                ).next().get_bytes_as('fulltext'))
1550
 
 
1551
 
    def test_get_record_stream_empty(self):
1552
 
        """An empty stream can be requested without error."""
1553
 
        f = self.get_versionedfiles()
1554
 
        entries = f.get_record_stream([], 'unordered', False)
1555
 
        self.assertEqual([], list(entries))
1556
 
 
1557
 
    def assertValidStorageKind(self, storage_kind):
1558
 
        """Assert that storage_kind is a valid storage_kind."""
1559
 
        self.assertSubset([storage_kind],
1560
 
            ['mpdiff', 'knit-annotated-ft', 'knit-annotated-delta',
1561
 
             'knit-ft', 'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
1562
 
             'knit-annotated-delta-gz', 'knit-ft-gz', 'knit-delta-gz'])
1563
 
 
1564
 
    def capture_stream(self, f, entries, on_seen, parents):
1565
 
        """Capture a stream for testing."""
1566
 
        for factory in entries:
1567
 
            on_seen(factory.key)
1568
 
            self.assertValidStorageKind(factory.storage_kind)
1569
 
            self.assertEqual(f.get_sha1s([factory.key])[factory.key],
1570
 
                factory.sha1)
1571
 
            self.assertEqual(parents[factory.key], factory.parents)
1572
 
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
1573
 
                str)
1574
 
 
1575
 
    def test_get_record_stream_interface(self):
1576
 
        """each item in a stream has to provide a regular interface."""
1577
 
        files = self.get_versionedfiles()
1578
 
        self.get_diamond_files(files)
1579
 
        keys, _ = self.get_keys_and_sort_order()
1580
 
        parent_map = files.get_parent_map(keys)
1581
 
        entries = files.get_record_stream(keys, 'unordered', False)
1582
 
        seen = set()
1583
 
        self.capture_stream(files, entries, seen.add, parent_map)
1584
 
        self.assertEqual(set(keys), seen)
1585
 
 
1586
 
    def get_simple_key(self, suffix):
1587
 
        """Return a key for the object under test."""
1588
 
        if self.key_length == 1:
1589
 
            return (suffix,)
1590
 
        else:
1591
 
            return ('FileA',) + (suffix,)
1592
 
 
1593
 
    def get_keys_and_sort_order(self):
1594
 
        """Get diamond test keys list, and their sort ordering."""
1595
 
        if self.key_length == 1:
1596
 
            keys = [('merged',), ('left',), ('right',), ('base',)]
1597
 
            sort_order = {('merged',):2, ('left',):1, ('right',):1, ('base',):0}
1598
 
        else:
1599
 
            keys = [
1600
 
                ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
1601
 
                ('FileA', 'base'),
1602
 
                ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
1603
 
                ('FileB', 'base'),
1604
 
                ]
1605
 
            sort_order = {
1606
 
                ('FileA', 'merged'):2, ('FileA', 'left'):1, ('FileA', 'right'):1,
1607
 
                ('FileA', 'base'):0,
1608
 
                ('FileB', 'merged'):2, ('FileB', 'left'):1, ('FileB', 'right'):1,
1609
 
                ('FileB', 'base'):0,
1610
 
                }
1611
 
        return keys, sort_order
1612
 
 
1613
 
    def test_get_record_stream_interface_ordered(self):
1614
 
        """each item in a stream has to provide a regular interface."""
1615
 
        files = self.get_versionedfiles()
1616
 
        self.get_diamond_files(files)
1617
 
        keys, sort_order = self.get_keys_and_sort_order()
1618
 
        parent_map = files.get_parent_map(keys)
1619
 
        entries = files.get_record_stream(keys, 'topological', False)
1620
 
        seen = []
1621
 
        self.capture_stream(files, entries, seen.append, parent_map)
1622
 
        self.assertStreamOrder(sort_order, seen, keys)
1623
 
 
1624
 
    def test_get_record_stream_interface_ordered_with_delta_closure(self):
1625
 
        """each item must be accessible as a fulltext."""
1626
 
        files = self.get_versionedfiles()
1627
 
        self.get_diamond_files(files)
1628
 
        keys, sort_order = self.get_keys_and_sort_order()
1629
 
        parent_map = files.get_parent_map(keys)
1630
 
        entries = files.get_record_stream(keys, 'topological', True)
1631
 
        seen = []
1632
 
        for factory in entries:
1633
 
            seen.append(factory.key)
1634
 
            self.assertValidStorageKind(factory.storage_kind)
1635
 
            self.assertSubset([factory.sha1],
1636
 
                [None, files.get_sha1s([factory.key])[factory.key]])
1637
 
            self.assertEqual(parent_map[factory.key], factory.parents)
1638
 
            # self.assertEqual(files.get_text(factory.key),
1639
 
            self.assertIsInstance(factory.get_bytes_as('fulltext'), str)
1640
 
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
1641
 
                str)
1642
 
        self.assertStreamOrder(sort_order, seen, keys)
1643
 
 
1644
 
    def assertStreamOrder(self, sort_order, seen, keys):
1645
 
        self.assertEqual(len(set(seen)), len(keys))
1646
 
        if self.key_length == 1:
1647
 
            lows = {():0}
1648
 
        else:
1649
 
            lows = {('FileA',):0, ('FileB',):0}
1650
 
        if not self.graph:
1651
 
            self.assertEqual(set(keys), set(seen))
1652
 
        else:
1653
 
            for key in seen:
1654
 
                sort_pos = sort_order[key]
1655
 
                self.assertTrue(sort_pos >= lows[key[:-1]],
1656
 
                    "Out of order in sorted stream: %r, %r" % (key, seen))
1657
 
                lows[key[:-1]] = sort_pos
1658
 
 
1659
 
    def test_get_record_stream_unknown_storage_kind_raises(self):
1660
 
        """Asking for a storage kind that the stream cannot supply raises."""
1661
 
        files = self.get_versionedfiles()
1662
 
        self.get_diamond_files(files)
1663
 
        if self.key_length == 1:
1664
 
            keys = [('merged',), ('left',), ('right',), ('base',)]
1665
 
        else:
1666
 
            keys = [
1667
 
                ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
1668
 
                ('FileA', 'base'),
1669
 
                ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
1670
 
                ('FileB', 'base'),
1671
 
                ]
1672
 
        parent_map = files.get_parent_map(keys)
1673
 
        entries = files.get_record_stream(keys, 'unordered', False)
1674
 
        # We track the contents because we should be able to try, fail a
1675
 
        # particular kind and then ask for one that works and continue.
1676
 
        seen = set()
1677
 
        for factory in entries:
1678
 
            seen.add(factory.key)
1679
 
            self.assertValidStorageKind(factory.storage_kind)
1680
 
            self.assertEqual(files.get_sha1s([factory.key])[factory.key],
1681
 
                factory.sha1)
1682
 
            self.assertEqual(parent_map[factory.key], factory.parents)
1683
 
            # currently no stream emits mpdiff
1684
 
            self.assertRaises(errors.UnavailableRepresentation,
1685
 
                factory.get_bytes_as, 'mpdiff')
1686
 
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
1687
 
                str)
1688
 
        self.assertEqual(set(keys), seen)
1689
 
 
1690
 
    def test_get_record_stream_missing_records_are_absent(self):
1691
 
        files = self.get_versionedfiles()
1692
 
        self.get_diamond_files(files)
1693
 
        if self.key_length == 1:
1694
 
            keys = [('merged',), ('left',), ('right',), ('absent',), ('base',)]
1695
 
        else:
1696
 
            keys = [
1697
 
                ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
1698
 
                ('FileA', 'absent'), ('FileA', 'base'),
1699
 
                ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
1700
 
                ('FileB', 'absent'), ('FileB', 'base'),
1701
 
                ('absent', 'absent'),
1702
 
                ]
1703
 
        parent_map = files.get_parent_map(keys)
1704
 
        entries = files.get_record_stream(keys, 'unordered', False)
1705
 
        self.assertAbsentRecord(files, keys, parent_map, entries)
1706
 
        entries = files.get_record_stream(keys, 'topological', False)
1707
 
        self.assertAbsentRecord(files, keys, parent_map, entries)
1708
 
 
1709
 
    def assertAbsentRecord(self, files, keys, parents, entries):
1710
 
        """Helper for test_get_record_stream_missing_records_are_absent."""
1711
 
        seen = set()
1712
 
        for factory in entries:
1713
 
            seen.add(factory.key)
1714
 
            if factory.key[-1] == 'absent':
1715
 
                self.assertEqual('absent', factory.storage_kind)
1716
 
                self.assertEqual(None, factory.sha1)
1717
 
                self.assertEqual(None, factory.parents)
1718
 
            else:
1719
 
                self.assertValidStorageKind(factory.storage_kind)
1720
 
                self.assertEqual(files.get_sha1s([factory.key])[factory.key],
1721
 
                    factory.sha1)
1722
 
                self.assertEqual(parents[factory.key], factory.parents)
1723
 
                self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
1724
 
                    str)
1725
 
        self.assertEqual(set(keys), seen)
1726
 
 
1727
 
    def test_filter_absent_records(self):
1728
 
        """Requested missing records can be filter trivially."""
1729
 
        files = self.get_versionedfiles()
1730
 
        self.get_diamond_files(files)
1731
 
        keys, _ = self.get_keys_and_sort_order()
1732
 
        parent_map = files.get_parent_map(keys)
1733
 
        # Add an absent record in the middle of the present keys. (We don't ask
1734
 
        # for just absent keys to ensure that content before and after the
1735
 
        # absent keys is still delivered).
1736
 
        present_keys = list(keys)
1737
 
        if self.key_length == 1:
1738
 
            keys.insert(2, ('extra',))
1739
 
        else:
1740
 
            keys.insert(2, ('extra', 'extra'))
1741
 
        entries = files.get_record_stream(keys, 'unordered', False)
1742
 
        seen = set()
1743
 
        self.capture_stream(files, versionedfile.filter_absent(entries), seen.add,
1744
 
            parent_map)
1745
 
        self.assertEqual(set(present_keys), seen)
1746
 
 
1747
 
    def get_mapper(self):
1748
 
        """Get a mapper suitable for the key length of the test interface."""
1749
 
        if self.key_length == 1:
1750
 
            return ConstantMapper('source')
1751
 
        else:
1752
 
            return HashEscapedPrefixMapper()
1753
 
 
1754
 
    def get_parents(self, parents):
1755
 
        """Get parents, taking self.graph into consideration."""
1756
 
        if self.graph:
1757
 
            return parents
1758
 
        else:
1759
 
            return None
1760
 
 
1761
 
    def test_get_parent_map(self):
1762
 
        files = self.get_versionedfiles()
1763
 
        if self.key_length == 1:
1764
 
            parent_details = [
1765
 
                (('r0',), self.get_parents(())),
1766
 
                (('r1',), self.get_parents((('r0',),))),
1767
 
                (('r2',), self.get_parents(())),
1768
 
                (('r3',), self.get_parents(())),
1769
 
                (('m',), self.get_parents((('r0',),('r1',),('r2',),('r3',)))),
1770
 
                ]
1771
 
        else:
1772
 
            parent_details = [
1773
 
                (('FileA', 'r0'), self.get_parents(())),
1774
 
                (('FileA', 'r1'), self.get_parents((('FileA', 'r0'),))),
1775
 
                (('FileA', 'r2'), self.get_parents(())),
1776
 
                (('FileA', 'r3'), self.get_parents(())),
1777
 
                (('FileA', 'm'), self.get_parents((('FileA', 'r0'),
1778
 
                    ('FileA', 'r1'), ('FileA', 'r2'), ('FileA', 'r3')))),
1779
 
                ]
1780
 
        for key, parents in parent_details:
1781
 
            files.add_lines(key, parents, [])
1782
 
            # immediately after adding it should be queryable.
1783
 
            self.assertEqual({key:parents}, files.get_parent_map([key]))
1784
 
        # We can ask for an empty set
1785
 
        self.assertEqual({}, files.get_parent_map([]))
1786
 
        # We can ask for many keys
1787
 
        all_parents = dict(parent_details)
1788
 
        self.assertEqual(all_parents, files.get_parent_map(all_parents.keys()))
1789
 
        # Absent keys are just not included in the result.
1790
 
        keys = all_parents.keys()
1791
 
        if self.key_length == 1:
1792
 
            keys.insert(1, ('missing',))
1793
 
        else:
1794
 
            keys.insert(1, ('missing', 'missing'))
1795
 
        # Absent keys are just ignored
1796
 
        self.assertEqual(all_parents, files.get_parent_map(keys))
1797
 
 
1798
 
    def test_get_sha1s(self):
1799
 
        files = self.get_versionedfiles()
1800
 
        self.get_diamond_files(files)
1801
 
        if self.key_length == 1:
1802
 
            keys = [('base',), ('origin',), ('left',), ('merged',), ('right',)]
1803
 
        else:
1804
 
            # ask for shas from different prefixes.
1805
 
            keys = [
1806
 
                ('FileA', 'base'), ('FileB', 'origin'), ('FileA', 'left'),
1807
 
                ('FileA', 'merged'), ('FileB', 'right'),
1808
 
                ]
1809
 
        self.assertEqual({
1810
 
            keys[0]: '51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44',
1811
 
            keys[1]: '00e364d235126be43292ab09cb4686cf703ddc17',
1812
 
            keys[2]: 'a8478686da38e370e32e42e8a0c220e33ee9132f',
1813
 
            keys[3]: 'ed8bce375198ea62444dc71952b22cfc2b09226d',
1814
 
            keys[4]: '9ef09dfa9d86780bdec9219a22560c6ece8e0ef1',
1815
 
            },
1816
 
            files.get_sha1s(keys))
1817
 
        
1818
 
    def test_insert_record_stream_empty(self):
1819
 
        """Inserting an empty record stream should work."""
1820
 
        files = self.get_versionedfiles()
1821
 
        files.insert_record_stream([])
1822
 
 
1823
 
    def assertIdenticalVersionedFile(self, expected, actual):
1824
 
        """Assert that left and right have the same contents."""
1825
 
        self.assertEqual(set(actual.keys()), set(expected.keys()))
1826
 
        actual_parents = actual.get_parent_map(actual.keys())
1827
 
        if self.graph:
1828
 
            self.assertEqual(actual_parents, expected.get_parent_map(expected.keys()))
1829
 
        else:
1830
 
            for key, parents in actual_parents.items():
1831
 
                self.assertEqual(None, parents)
1832
 
        for key in actual.keys():
1833
 
            actual_text = actual.get_record_stream(
1834
 
                [key], 'unordered', True).next().get_bytes_as('fulltext')
1835
 
            expected_text = expected.get_record_stream(
1836
 
                [key], 'unordered', True).next().get_bytes_as('fulltext')
1837
 
            self.assertEqual(actual_text, expected_text)
1838
 
 
1839
 
    def test_insert_record_stream_fulltexts(self):
1840
 
        """Any file should accept a stream of fulltexts."""
1841
 
        files = self.get_versionedfiles()
1842
 
        mapper = self.get_mapper()
1843
 
        source_transport = self.get_transport('source')
1844
 
        source_transport.mkdir('.')
1845
 
        # weaves always output fulltexts.
1846
 
        source = make_versioned_files_factory(WeaveFile, mapper)(
1847
 
            source_transport)
1848
 
        self.get_diamond_files(source, trailing_eol=False)
1849
 
        stream = source.get_record_stream(source.keys(), 'topological',
1850
 
            False)
1851
 
        files.insert_record_stream(stream)
1852
 
        self.assertIdenticalVersionedFile(source, files)
1853
 
 
1854
 
    def test_insert_record_stream_fulltexts_noeol(self):
1855
 
        """Any file should accept a stream of fulltexts."""
1856
 
        files = self.get_versionedfiles()
1857
 
        mapper = self.get_mapper()
1858
 
        source_transport = self.get_transport('source')
1859
 
        source_transport.mkdir('.')
1860
 
        # weaves always output fulltexts.
1861
 
        source = make_versioned_files_factory(WeaveFile, mapper)(
1862
 
            source_transport)
1863
 
        self.get_diamond_files(source, trailing_eol=False)
1864
 
        stream = source.get_record_stream(source.keys(), 'topological',
1865
 
            False)
1866
 
        files.insert_record_stream(stream)
1867
 
        self.assertIdenticalVersionedFile(source, files)
1868
 
 
1869
 
    def test_insert_record_stream_annotated_knits(self):
1870
 
        """Any file should accept a stream from plain knits."""
1871
 
        files = self.get_versionedfiles()
1872
 
        mapper = self.get_mapper()
1873
 
        source_transport = self.get_transport('source')
1874
 
        source_transport.mkdir('.')
1875
 
        source = make_file_factory(True, mapper)(source_transport)
1876
 
        self.get_diamond_files(source)
1877
 
        stream = source.get_record_stream(source.keys(), 'topological',
1878
 
            False)
1879
 
        files.insert_record_stream(stream)
1880
 
        self.assertIdenticalVersionedFile(source, files)
1881
 
 
1882
 
    def test_insert_record_stream_annotated_knits_noeol(self):
1883
 
        """Any file should accept a stream from plain knits."""
1884
 
        files = self.get_versionedfiles()
1885
 
        mapper = self.get_mapper()
1886
 
        source_transport = self.get_transport('source')
1887
 
        source_transport.mkdir('.')
1888
 
        source = make_file_factory(True, mapper)(source_transport)
1889
 
        self.get_diamond_files(source, trailing_eol=False)
1890
 
        stream = source.get_record_stream(source.keys(), 'topological',
1891
 
            False)
1892
 
        files.insert_record_stream(stream)
1893
 
        self.assertIdenticalVersionedFile(source, files)
1894
 
 
1895
 
    def test_insert_record_stream_plain_knits(self):
1896
 
        """Any file should accept a stream from plain knits."""
1897
 
        files = self.get_versionedfiles()
1898
 
        mapper = self.get_mapper()
1899
 
        source_transport = self.get_transport('source')
1900
 
        source_transport.mkdir('.')
1901
 
        source = make_file_factory(False, mapper)(source_transport)
1902
 
        self.get_diamond_files(source)
1903
 
        stream = source.get_record_stream(source.keys(), 'topological',
1904
 
            False)
1905
 
        files.insert_record_stream(stream)
1906
 
        self.assertIdenticalVersionedFile(source, files)
1907
 
 
1908
 
    def test_insert_record_stream_plain_knits_noeol(self):
1909
 
        """Any file should accept a stream from plain knits."""
1910
 
        files = self.get_versionedfiles()
1911
 
        mapper = self.get_mapper()
1912
 
        source_transport = self.get_transport('source')
1913
 
        source_transport.mkdir('.')
1914
 
        source = make_file_factory(False, mapper)(source_transport)
1915
 
        self.get_diamond_files(source, trailing_eol=False)
1916
 
        stream = source.get_record_stream(source.keys(), 'topological',
1917
 
            False)
1918
 
        files.insert_record_stream(stream)
1919
 
        self.assertIdenticalVersionedFile(source, files)
1920
 
 
1921
 
    def test_insert_record_stream_existing_keys(self):
1922
 
        """Inserting keys already in a file should not error."""
1923
 
        files = self.get_versionedfiles()
1924
 
        source = self.get_versionedfiles('source')
1925
 
        self.get_diamond_files(source)
1926
 
        # insert some keys into f.
1927
 
        self.get_diamond_files(files, left_only=True)
1928
 
        stream = source.get_record_stream(source.keys(), 'topological',
1929
 
            False)
1930
 
        files.insert_record_stream(stream)
1931
 
        self.assertIdenticalVersionedFile(source, files)
1932
 
 
1933
 
    def test_insert_record_stream_missing_keys(self):
1934
 
        """Inserting a stream with absent keys should raise an error."""
1935
 
        files = self.get_versionedfiles()
1936
 
        source = self.get_versionedfiles('source')
1937
 
        stream = source.get_record_stream([('missing',) * self.key_length],
1938
 
            'topological', False)
1939
 
        self.assertRaises(errors.RevisionNotPresent, files.insert_record_stream,
1940
 
            stream)
1941
 
 
1942
 
    def test_insert_record_stream_out_of_order(self):
1943
 
        """An out of order stream can either error or work."""
1944
 
        files = self.get_versionedfiles()
1945
 
        source = self.get_versionedfiles('source')
1946
 
        self.get_diamond_files(source)
1947
 
        if self.key_length == 1:
1948
 
            origin_keys = [('origin',)]
1949
 
            end_keys = [('merged',), ('left',)]
1950
 
            start_keys = [('right',), ('base',)]
1951
 
        else:
1952
 
            origin_keys = [('FileA', 'origin'), ('FileB', 'origin')]
1953
 
            end_keys = [('FileA', 'merged',), ('FileA', 'left',),
1954
 
                ('FileB', 'merged',), ('FileB', 'left',)]
1955
 
            start_keys = [('FileA', 'right',), ('FileA', 'base',),
1956
 
                ('FileB', 'right',), ('FileB', 'base',)]
1957
 
        origin_entries = source.get_record_stream(origin_keys, 'unordered', False)
1958
 
        end_entries = source.get_record_stream(end_keys, 'topological', False)
1959
 
        start_entries = source.get_record_stream(start_keys, 'topological', False)
1960
 
        entries = chain(origin_entries, end_entries, start_entries)
1961
 
        try:
1962
 
            files.insert_record_stream(entries)
1963
 
        except RevisionNotPresent:
1964
 
            # Must not have corrupted the file.
1965
 
            files.check()
1966
 
        else:
1967
 
            self.assertIdenticalVersionedFile(source, files)
1968
 
 
1969
 
    def test_insert_record_stream_delta_missing_basis_no_corruption(self):
1970
 
        """Insertion where a needed basis is not included aborts safely."""
1971
 
        # We use a knit always here to be sure we are getting a binary delta.
1972
 
        mapper = self.get_mapper()
1973
 
        source_transport = self.get_transport('source')
1974
 
        source_transport.mkdir('.')
1975
 
        source = make_file_factory(False, mapper)(source_transport)
1976
 
        self.get_diamond_files(source)
1977
 
        entries = source.get_record_stream(['origin', 'merged'], 'unordered', False)
1978
 
        files = self.get_versionedfiles()
1979
 
        self.assertRaises(RevisionNotPresent, files.insert_record_stream,
1980
 
            entries)
1981
 
        files.check()
1982
 
        self.assertEqual({}, files.get_parent_map([]))
1983
 
 
1984
 
    def test_iter_lines_added_or_present_in_keys(self):
1985
 
        # test that we get at least an equalset of the lines added by
1986
 
        # versions in the store.
1987
 
        # the ordering here is to make a tree so that dumb searches have
1988
 
        # more changes to muck up.
1989
 
 
1990
 
        class InstrumentedProgress(progress.DummyProgress):
1991
 
 
1992
 
            def __init__(self):
1993
 
 
1994
 
                progress.DummyProgress.__init__(self)
1995
 
                self.updates = []
1996
 
 
1997
 
            def update(self, msg=None, current=None, total=None):
1998
 
                self.updates.append((msg, current, total))
1999
 
 
2000
 
        files = self.get_versionedfiles()
2001
 
        # add a base to get included
2002
 
        files.add_lines(self.get_simple_key('base'), (), ['base\n'])
2003
 
        # add a ancestor to be included on one side
2004
 
        files.add_lines(self.get_simple_key('lancestor'), (), ['lancestor\n'])
2005
 
        # add a ancestor to be included on the other side
2006
 
        files.add_lines(self.get_simple_key('rancestor'),
2007
 
            self.get_parents([self.get_simple_key('base')]), ['rancestor\n'])
2008
 
        # add a child of rancestor with no eofile-nl
2009
 
        files.add_lines(self.get_simple_key('child'),
2010
 
            self.get_parents([self.get_simple_key('rancestor')]),
2011
 
            ['base\n', 'child\n'])
2012
 
        # add a child of lancestor and base to join the two roots
2013
 
        files.add_lines(self.get_simple_key('otherchild'),
2014
 
            self.get_parents([self.get_simple_key('lancestor'),
2015
 
                self.get_simple_key('base')]),
2016
 
            ['base\n', 'lancestor\n', 'otherchild\n'])
2017
 
        def iter_with_keys(keys, expected):
2018
 
            # now we need to see what lines are returned, and how often.
2019
 
            lines = {}
2020
 
            progress = InstrumentedProgress()
2021
 
            # iterate over the lines
2022
 
            for line in files.iter_lines_added_or_present_in_keys(keys,
2023
 
                pb=progress):
2024
 
                lines.setdefault(line, 0)
2025
 
                lines[line] += 1
2026
 
            if []!= progress.updates:
2027
 
                self.assertEqual(expected, progress.updates)
2028
 
            return lines
2029
 
        lines = iter_with_keys(
2030
 
            [self.get_simple_key('child'), self.get_simple_key('otherchild')],
2031
 
            [('Walking content.', 0, 2),
2032
 
             ('Walking content.', 1, 2),
2033
 
             ('Walking content.', 2, 2)])
2034
 
        # we must see child and otherchild
2035
 
        self.assertTrue(lines[('child\n', self.get_simple_key('child'))] > 0)
2036
 
        self.assertTrue(
2037
 
            lines[('otherchild\n', self.get_simple_key('otherchild'))] > 0)
2038
 
        # we dont care if we got more than that.
2039
 
        
2040
 
        # test all lines
2041
 
        lines = iter_with_keys(files.keys(),
2042
 
            [('Walking content.', 0, 5),
2043
 
             ('Walking content.', 1, 5),
2044
 
             ('Walking content.', 2, 5),
2045
 
             ('Walking content.', 3, 5),
2046
 
             ('Walking content.', 4, 5),
2047
 
             ('Walking content.', 5, 5)])
2048
 
        # all lines must be seen at least once
2049
 
        self.assertTrue(lines[('base\n', self.get_simple_key('base'))] > 0)
2050
 
        self.assertTrue(
2051
 
            lines[('lancestor\n', self.get_simple_key('lancestor'))] > 0)
2052
 
        self.assertTrue(
2053
 
            lines[('rancestor\n', self.get_simple_key('rancestor'))] > 0)
2054
 
        self.assertTrue(lines[('child\n', self.get_simple_key('child'))] > 0)
2055
 
        self.assertTrue(
2056
 
            lines[('otherchild\n', self.get_simple_key('otherchild'))] > 0)
2057
 
 
2058
 
    def test_make_mpdiffs(self):
2059
 
        from bzrlib import multiparent
2060
 
        files = self.get_versionedfiles('source')
2061
 
        # add texts that should trip the knit maximum delta chain threshold
2062
 
        # as well as doing parallel chains of data in knits.
2063
 
        # this is done by two chains of 25 insertions
2064
 
        files.add_lines(self.get_simple_key('base'), [], ['line\n'])
2065
 
        files.add_lines(self.get_simple_key('noeol'),
2066
 
            self.get_parents([self.get_simple_key('base')]), ['line'])
2067
 
        # detailed eol tests:
2068
 
        # shared last line with parent no-eol
2069
 
        files.add_lines(self.get_simple_key('noeolsecond'),
2070
 
            self.get_parents([self.get_simple_key('noeol')]),
2071
 
                ['line\n', 'line'])
2072
 
        # differing last line with parent, both no-eol
2073
 
        files.add_lines(self.get_simple_key('noeolnotshared'),
2074
 
            self.get_parents([self.get_simple_key('noeolsecond')]),
2075
 
                ['line\n', 'phone'])
2076
 
        # add eol following a noneol parent, change content
2077
 
        files.add_lines(self.get_simple_key('eol'),
2078
 
            self.get_parents([self.get_simple_key('noeol')]), ['phone\n'])
2079
 
        # add eol following a noneol parent, no change content
2080
 
        files.add_lines(self.get_simple_key('eolline'),
2081
 
            self.get_parents([self.get_simple_key('noeol')]), ['line\n'])
2082
 
        # noeol with no parents:
2083
 
        files.add_lines(self.get_simple_key('noeolbase'), [], ['line'])
2084
 
        # noeol preceeding its leftmost parent in the output:
2085
 
        # this is done by making it a merge of two parents with no common
2086
 
        # anestry: noeolbase and noeol with the 
2087
 
        # later-inserted parent the leftmost.
2088
 
        files.add_lines(self.get_simple_key('eolbeforefirstparent'),
2089
 
            self.get_parents([self.get_simple_key('noeolbase'),
2090
 
                self.get_simple_key('noeol')]),
2091
 
            ['line'])
2092
 
        # two identical eol texts
2093
 
        files.add_lines(self.get_simple_key('noeoldup'),
2094
 
            self.get_parents([self.get_simple_key('noeol')]), ['line'])
2095
 
        next_parent = self.get_simple_key('base')
2096
 
        text_name = 'chain1-'
2097
 
        text = ['line\n']
2098
 
        sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
2099
 
                 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
2100
 
                 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
2101
 
                 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
2102
 
                 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
2103
 
                 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
2104
 
                 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
2105
 
                 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
2106
 
                 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
2107
 
                 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
2108
 
                 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
2109
 
                 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
2110
 
                 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
2111
 
                 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
2112
 
                 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
2113
 
                 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
2114
 
                 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
2115
 
                 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
2116
 
                 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
2117
 
                 19:'1ebed371807ba5935958ad0884595126e8c4e823',
2118
 
                 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
2119
 
                 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
2120
 
                 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
2121
 
                 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
2122
 
                 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
2123
 
                 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
2124
 
                 }
2125
 
        for depth in range(26):
2126
 
            new_version = self.get_simple_key(text_name + '%s' % depth)
2127
 
            text = text + ['line\n']
2128
 
            files.add_lines(new_version, self.get_parents([next_parent]), text)
2129
 
            next_parent = new_version
2130
 
        next_parent = self.get_simple_key('base')
2131
 
        text_name = 'chain2-'
2132
 
        text = ['line\n']
2133
 
        for depth in range(26):
2134
 
            new_version = self.get_simple_key(text_name + '%s' % depth)
2135
 
            text = text + ['line\n']
2136
 
            files.add_lines(new_version, self.get_parents([next_parent]), text)
2137
 
            next_parent = new_version
2138
 
        target = self.get_versionedfiles('target')
2139
 
        for key in multiparent.topo_iter_keys(files, files.keys()):
2140
 
            mpdiff = files.make_mpdiffs([key])[0]
2141
 
            parents = files.get_parent_map([key])[key] or []
2142
 
            target.add_mpdiffs(
2143
 
                [(key, parents, files.get_sha1s([key])[key], mpdiff)])
2144
 
            self.assertEqualDiff(
2145
 
                files.get_record_stream([key], 'unordered',
2146
 
                    True).next().get_bytes_as('fulltext'),
2147
 
                target.get_record_stream([key], 'unordered',
2148
 
                    True).next().get_bytes_as('fulltext')
2149
 
                )
2150
 
 
2151
 
    def test_keys(self):
2152
 
        # While use is discouraged, versions() is still needed by aspects of
2153
 
        # bzr.
2154
 
        files = self.get_versionedfiles()
2155
 
        self.assertEqual(set(), set(files.keys()))
2156
 
        if self.key_length == 1:
2157
 
            key = ('foo',)
2158
 
        else:
2159
 
            key = ('foo', 'bar',)
2160
 
        files.add_lines(key, (), [])
2161
 
        self.assertEqual(set([key]), set(files.keys()))
2162
 
 
2163
 
 
2164
 
class VirtualVersionedFilesTests(TestCase):
2165
 
    """Basic tests for the VirtualVersionedFiles implementations."""
2166
 
 
2167
 
    def _get_parent_map(self, keys):
2168
 
        ret = {}
2169
 
        for k in keys:
2170
 
            if k in self._parent_map:
2171
 
                ret[k] = self._parent_map[k]
2172
 
        return ret
2173
 
 
2174
 
    def setUp(self):
2175
 
        TestCase.setUp(self)
2176
 
        self._lines = {}
2177
 
        self._parent_map = {}
2178
 
        self.texts = VirtualVersionedFiles(self._get_parent_map, 
2179
 
                                           self._lines.get)
2180
 
 
2181
 
    def test_add_lines(self):
2182
 
        self.assertRaises(NotImplementedError, 
2183
 
                self.texts.add_lines, "foo", [], [])
2184
 
 
2185
 
    def test_add_mpdiffs(self):
2186
 
        self.assertRaises(NotImplementedError, 
2187
 
                self.texts.add_mpdiffs, [])
2188
 
 
2189
 
    def test_check(self):
2190
 
        self.assertTrue(self.texts.check())
2191
 
 
2192
 
    def test_insert_record_stream(self):
2193
 
        self.assertRaises(NotImplementedError, self.texts.insert_record_stream,
2194
 
                          [])
2195
 
 
2196
 
    def test_get_sha1s_nonexistent(self):
2197
 
        self.assertEquals({}, self.texts.get_sha1s([("NONEXISTENT",)]))
2198
 
 
2199
 
    def test_get_sha1s(self):
2200
 
        self._lines["key"] = ["dataline1", "dataline2"]
2201
 
        self.assertEquals({("key",): osutils.sha_strings(self._lines["key"])},
2202
 
                           self.texts.get_sha1s([("key",)]))
2203
 
 
2204
 
    def test_get_parent_map(self):
2205
 
        self._parent_map = {"G": ("A", "B")}
2206
 
        self.assertEquals({("G",): (("A",),("B",))}, 
2207
 
                          self.texts.get_parent_map([("G",), ("L",)]))
2208
 
 
2209
 
    def test_get_record_stream(self):
2210
 
        self._lines["A"] = ["FOO", "BAR"]
2211
 
        it = self.texts.get_record_stream([("A",)], "unordered", True)
2212
 
        record = it.next()
2213
 
        self.assertEquals("fulltext", record.storage_kind)
2214
 
        self.assertEquals("FOOBAR", record.get_bytes_as("fulltext"))
2215
 
 
2216
 
    def test_get_record_stream_absent(self):
2217
 
        it = self.texts.get_record_stream([("A",)], "unordered", True)
2218
 
        record = it.next()
2219
 
        self.assertEquals("absent", record.storage_kind)
 
1546
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
2220
1547