~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-25 10:36:36 UTC
  • mfrom: (3350.6.12 versionedfiles)
  • Revision ID: pqm@pqm.ubuntu.com-20080625103636-6kxh4e1gmyn82f50
(mbp for robertc) VersionedFiles refactoring

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
 
    make_file_knit,
 
40
    cleanup_pack_knit,
 
41
    make_file_factory,
 
42
    make_pack_factory,
41
43
    KnitAnnotateFactory,
42
44
    KnitPlainFactory,
43
45
    )
44
46
from bzrlib.symbol_versioning import one_four, one_five
45
 
from bzrlib.tests import TestCaseWithMemoryTransport, TestSkipped
 
47
from bzrlib.tests import (
 
48
    TestCaseWithMemoryTransport,
 
49
    TestScenarioApplier,
 
50
    TestSkipped,
 
51
    condition_isinstance,
 
52
    split_suite_by_condition,
 
53
    iter_suite_tests,
 
54
    )
46
55
from bzrlib.tests.http_utils import TestCaseWithWebserver
47
56
from bzrlib.trace import mutter
48
57
from bzrlib.transport import get_transport
50
59
from bzrlib.tsort import topo_sort
51
60
from bzrlib.tuned_gzip import GzipFile
52
61
import bzrlib.versionedfile as versionedfile
 
62
from bzrlib.versionedfile import (
 
63
    ConstantMapper,
 
64
    HashEscapedPrefixMapper,
 
65
    PrefixMapper,
 
66
    make_versioned_files_factory,
 
67
    )
53
68
from bzrlib.weave import WeaveFile
54
69
from bzrlib.weavefile import read_weave, write_weave
55
70
 
56
71
 
 
72
def load_tests(standard_tests, module, loader):
 
73
    """Parameterize VersionedFiles tests for different implementations."""
 
74
    to_adapt, result = split_suite_by_condition(
 
75
        standard_tests, condition_isinstance(TestVersionedFiles))
 
76
    len_one_adapter = TestScenarioApplier()
 
77
    len_two_adapter = TestScenarioApplier()
 
78
    # We want to be sure of behaviour for:
 
79
    # weaves prefix layout (weave texts)
 
80
    # individually named weaves (weave inventories)
 
81
    # annotated knits - prefix|hash|hash-escape layout, we test the third only
 
82
    #                   as it is the most complex mapper.
 
83
    # individually named knits
 
84
    # individual no-graph knits in packs (signatures)
 
85
    # individual graph knits in packs (inventories)
 
86
    # individual graph nocompression knits in packs (revisions)
 
87
    # plain text knits in packs (texts)
 
88
    len_one_adapter.scenarios = [
 
89
        ('weave-named', {
 
90
            'cleanup':None,
 
91
            'factory':make_versioned_files_factory(WeaveFile,
 
92
                ConstantMapper('inventory')),
 
93
            'graph':True,
 
94
            'key_length':1,
 
95
            }),
 
96
        ('named-knit', {
 
97
            'cleanup':None,
 
98
            'factory':make_file_factory(False, ConstantMapper('revisions')),
 
99
            'graph':True,
 
100
            'key_length':1,
 
101
            }),
 
102
        ('named-nograph-knit-pack', {
 
103
            'cleanup':cleanup_pack_knit,
 
104
            'factory':make_pack_factory(False, False, 1),
 
105
            'graph':False,
 
106
            'key_length':1,
 
107
            }),
 
108
        ('named-graph-knit-pack', {
 
109
            'cleanup':cleanup_pack_knit,
 
110
            'factory':make_pack_factory(True, True, 1),
 
111
            'graph':True,
 
112
            'key_length':1,
 
113
            }),
 
114
        ('named-graph-nodelta-knit-pack', {
 
115
            'cleanup':cleanup_pack_knit,
 
116
            'factory':make_pack_factory(True, False, 1),
 
117
            'graph':True,
 
118
            'key_length':1,
 
119
            }),
 
120
        ]
 
121
    len_two_adapter.scenarios = [
 
122
        ('weave-prefix', {
 
123
            'cleanup':None,
 
124
            'factory':make_versioned_files_factory(WeaveFile,
 
125
                PrefixMapper()),
 
126
            'graph':True,
 
127
            'key_length':2,
 
128
            }),
 
129
        ('annotated-knit-escape', {
 
130
            'cleanup':None,
 
131
            'factory':make_file_factory(True, HashEscapedPrefixMapper()),
 
132
            'graph':True,
 
133
            'key_length':2,
 
134
            }),
 
135
        ('plain-knit-pack', {
 
136
            'cleanup':cleanup_pack_knit,
 
137
            'factory':make_pack_factory(True, True, 2),
 
138
            'graph':True,
 
139
            'key_length':2,
 
140
            }),
 
141
        ]
 
142
    for test in iter_suite_tests(to_adapt):
 
143
        result.addTests(len_one_adapter.adapt(test))
 
144
        result.addTests(len_two_adapter.adapt(test))
 
145
    return result
 
146
 
 
147
 
57
148
def get_diamond_vf(f, trailing_eol=True, left_only=False):
58
149
    """Get a diamond graph to exercise deltas and merges.
59
150
    
82
173
    return f, parents
83
174
 
84
175
 
 
176
def get_diamond_files(files, key_length, trailing_eol=True, left_only=False,
 
177
    nograph=False):
 
178
    """Get a diamond graph to exercise deltas and merges.
 
179
 
 
180
    This creates a 5-node graph in files. If files supports 2-length keys two
 
181
    graphs are made to exercise the support for multiple ids.
 
182
    
 
183
    :param trailing_eol: If True end the last line with \n.
 
184
    :param key_length: The length of keys in files. Currently supports length 1
 
185
        and 2 keys.
 
186
    :param left_only: If True do not add the right and merged nodes.
 
187
    :param nograph: If True, do not provide parents to the add_lines calls;
 
188
        this is useful for tests that need inserted data but have graphless
 
189
        stores.
 
190
    :return: The results of the add_lines calls.
 
191
    """
 
192
    if key_length == 1:
 
193
        prefixes = [()]
 
194
    else:
 
195
        prefixes = [('FileA',), ('FileB',)]
 
196
    # insert a diamond graph to exercise deltas and merges.
 
197
    if trailing_eol:
 
198
        last_char = '\n'
 
199
    else:
 
200
        last_char = ''
 
201
    result = []
 
202
    def get_parents(suffix_list):
 
203
        if nograph:
 
204
            return ()
 
205
        else:
 
206
            result = [prefix + suffix for suffix in suffix_list]
 
207
            return result
 
208
    # we loop over each key because that spreads the inserts across prefixes,
 
209
    # which is how commit operates.
 
210
    for prefix in prefixes:
 
211
        result.append(files.add_lines(prefix + ('origin',), (),
 
212
            ['origin' + last_char]))
 
213
    for prefix in prefixes:
 
214
        result.append(files.add_lines(prefix + ('base',),
 
215
            get_parents([('origin',)]), ['base' + last_char]))
 
216
    for prefix in prefixes:
 
217
        result.append(files.add_lines(prefix + ('left',),
 
218
            get_parents([('base',)]),
 
219
            ['base\n', 'left' + last_char]))
 
220
    if not left_only:
 
221
        for prefix in prefixes:
 
222
            result.append(files.add_lines(prefix + ('right',),
 
223
                get_parents([('base',)]),
 
224
                ['base\n', 'right' + last_char]))
 
225
        for prefix in prefixes:
 
226
            result.append(files.add_lines(prefix + ('merged',),
 
227
                get_parents([('left',), ('right',)]),
 
228
                ['base\n', 'left\n', 'right\n', 'merged' + last_char]))
 
229
    return result
 
230
 
 
231
 
85
232
class VersionedFileTestMixIn(object):
86
233
    """A mixin test class for testing VersionedFiles.
87
234
 
118
265
        f = self.reopen_file(create=True)
119
266
        verify_file(f)
120
267
 
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
 
 
381
268
    def test_adds_with_parent_texts(self):
382
269
        f = self.get_file()
383
270
        parent_texts = {}
734
621
        self._transaction = 'after'
735
622
        self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
736
623
        self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
737
 
        self.assertRaises(errors.OutSideTransaction, self.applyDeprecated,
738
 
            one_five, f.join, '')
739
624
        
740
625
    def test_copy_to(self):
741
626
        f = self.get_file()
945
830
                          'base',
946
831
                          [],
947
832
                          [])
948
 
        self.assertRaises(errors.ReadOnlyError, self.applyDeprecated, one_five,
949
 
            vf.join, 'base')
950
833
    
951
834
    def test_get_sha1s(self):
952
835
        # check the sha1 data is available
1019
902
        return WeaveFile
1020
903
 
1021
904
 
1022
 
class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
1023
 
 
1024
 
    def get_file(self, name='foo', create=True):
1025
 
        return make_file_knit(name, get_transport(self.get_url('.')),
1026
 
            delta=True, create=True, get_scope=self.get_transaction)
1027
 
 
1028
 
    def get_factory(self):
1029
 
        return make_file_knit
1030
 
 
1031
 
    def get_file_corrupted_text(self):
1032
 
        knit = self.get_file()
1033
 
        knit.add_lines('v1', [], ['hello\n'])
1034
 
        knit.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
1035
 
        return knit
1036
 
 
1037
 
    def reopen_file(self, name='foo', create=False):
1038
 
        return self.get_file(name, create)
1039
 
 
1040
 
    def test_detection(self):
1041
 
        knit = self.get_file()
1042
 
        knit.check()
1043
 
 
1044
 
    def test_no_implicit_create(self):
1045
 
        self.assertRaises(errors.NoSuchFile, self.get_factory(), 'foo',
1046
 
            get_transport(self.get_url('.')))
1047
 
 
1048
 
 
1049
 
class TestPlaintextKnit(TestKnit):
1050
 
    """Test a knit with no cached annotations"""
1051
 
 
1052
 
    def get_file(self, name='foo', create=True):
1053
 
        return make_file_knit(name, get_transport(self.get_url('.')),
1054
 
            delta=True, create=create, get_scope=self.get_transaction,
1055
 
            factory=_mod_knit.KnitPlainFactory())
1056
 
 
1057
 
 
1058
905
class TestPlanMergeVersionedFile(TestCaseWithMemoryTransport):
1059
906
 
1060
907
    def setUp(self):
1061
908
        TestCaseWithMemoryTransport.setUp(self)
1062
 
        self.vf1 = make_file_knit('root', self.get_transport(), create=True)
1063
 
        self.vf2 = make_file_knit('root', self.get_transport(), create=True)
1064
 
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root',
1065
 
            [self.vf1, self.vf2])
 
909
        mapper = PrefixMapper()
 
910
        factory = make_file_factory(True, mapper)
 
911
        self.vf1 = factory(self.get_transport('root-1'))
 
912
        self.vf2 = factory(self.get_transport('root-2'))
 
913
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root')
 
914
        self.plan_merge_vf.fallback_versionedfiles.extend([self.vf1, self.vf2])
1066
915
 
1067
916
    def test_add_lines(self):
1068
 
        self.plan_merge_vf.add_lines('a:', [], [])
1069
 
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a', [],
1070
 
                          [])
1071
 
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', None,
1072
 
                          [])
1073
 
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines, 'a:', [],
1074
 
                          None)
1075
 
 
1076
 
    def test_ancestry(self):
1077
 
        self.vf1.add_lines('A', [], [])
1078
 
        self.vf1.add_lines('B', ['A'], [])
1079
 
        self.plan_merge_vf.add_lines('C:', ['B'], [])
1080
 
        self.plan_merge_vf.add_lines('D:', ['C:'], [])
1081
 
        self.assertEqual(set(['A', 'B', 'C:', 'D:']),
1082
 
            self.plan_merge_vf.get_ancestry('D:', topo_sorted=False))
 
917
        self.plan_merge_vf.add_lines(('root', 'a:'), [], [])
 
918
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
 
919
            ('root', 'a'), [], [])
 
920
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
 
921
            ('root', 'a:'), None, [])
 
922
        self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
 
923
            ('root', 'a:'), [], None)
1083
924
 
1084
925
    def setup_abcde(self):
1085
 
        self.vf1.add_lines('A', [], ['a'])
1086
 
        self.vf1.add_lines('B', ['A'], ['b'])
1087
 
        self.vf2.add_lines('C', [], ['c'])
1088
 
        self.vf2.add_lines('D', ['C'], ['d'])
1089
 
        self.plan_merge_vf.add_lines('E:', ['B', 'D'], ['e'])
1090
 
 
1091
 
    def test_ancestry_uses_all_versionedfiles(self):
1092
 
        self.setup_abcde()
1093
 
        self.assertEqual(set(['A', 'B', 'C', 'D', 'E:']),
1094
 
            self.plan_merge_vf.get_ancestry('E:', topo_sorted=False))
1095
 
 
1096
 
    def test_ancestry_raises_revision_not_present(self):
1097
 
        error = self.assertRaises(errors.RevisionNotPresent,
1098
 
                                  self.plan_merge_vf.get_ancestry, 'E:', False)
1099
 
        self.assertContainsRe(str(error), '{E:} not present in "root"')
 
926
        self.vf1.add_lines(('root', 'A'), [], ['a'])
 
927
        self.vf1.add_lines(('root', 'B'), [('root', 'A')], ['b'])
 
928
        self.vf2.add_lines(('root', 'C'), [], ['c'])
 
929
        self.vf2.add_lines(('root', 'D'), [('root', 'C')], ['d'])
 
930
        self.plan_merge_vf.add_lines(('root', 'E:'),
 
931
            [('root', 'B'), ('root', 'D')], ['e'])
1100
932
 
1101
933
    def test_get_parents(self):
1102
934
        self.setup_abcde()
1103
 
        self.assertEqual({'B':('A',)}, self.plan_merge_vf.get_parent_map(['B']))
1104
 
        self.assertEqual({'D':('C',)}, self.plan_merge_vf.get_parent_map(['D']))
1105
 
        self.assertEqual({'E:':('B', 'D')},
1106
 
            self.plan_merge_vf.get_parent_map(['E:']))
1107
 
        self.assertEqual({}, self.plan_merge_vf.get_parent_map(['F']))
 
935
        self.assertEqual({('root', 'B'):(('root', 'A'),)},
 
936
            self.plan_merge_vf.get_parent_map([('root', 'B')]))
 
937
        self.assertEqual({('root', 'D'):(('root', 'C'),)},
 
938
            self.plan_merge_vf.get_parent_map([('root', 'D')]))
 
939
        self.assertEqual({('root', 'E:'):(('root', 'B'),('root', 'D'))},
 
940
            self.plan_merge_vf.get_parent_map([('root', 'E:')]))
 
941
        self.assertEqual({},
 
942
            self.plan_merge_vf.get_parent_map([('root', 'F')]))
1108
943
        self.assertEqual({
1109
 
                'B':('A',),
1110
 
                'D':('C',),
1111
 
                'E:':('B', 'D'),
1112
 
                }, self.plan_merge_vf.get_parent_map(['B', 'D', 'E:', 'F']))
 
944
                ('root', 'B'):(('root', 'A'),),
 
945
                ('root', 'D'):(('root', 'C'),),
 
946
                ('root', 'E:'):(('root', 'B'),('root', 'D')),
 
947
                },
 
948
            self.plan_merge_vf.get_parent_map(
 
949
                [('root', 'B'), ('root', 'D'), ('root', 'E:'), ('root', 'F')]))
1113
950
 
1114
 
    def test_get_lines(self):
 
951
    def test_get_record_stream(self):
1115
952
        self.setup_abcde()
1116
 
        self.assertEqual(['a'], self.plan_merge_vf.get_lines('A'))
1117
 
        self.assertEqual(['c'], self.plan_merge_vf.get_lines('C'))
1118
 
        self.assertEqual(['e'], self.plan_merge_vf.get_lines('E:'))
1119
 
        error = self.assertRaises(errors.RevisionNotPresent,
1120
 
                                  self.plan_merge_vf.get_lines, 'F')
1121
 
        self.assertContainsRe(str(error), '{F} not present in "root"')
1122
 
 
1123
 
 
1124
 
class InterString(versionedfile.InterVersionedFile):
1125
 
    """An inter-versionedfile optimised code path for strings.
1126
 
 
1127
 
    This is for use during testing where we use strings as versionedfiles
1128
 
    so that none of the default regsitered interversionedfile classes will
1129
 
    match - which lets us test the match logic.
1130
 
    """
1131
 
 
1132
 
    @staticmethod
1133
 
    def is_compatible(source, target):
1134
 
        """InterString is compatible with strings-as-versionedfiles."""
1135
 
        return isinstance(source, str) and isinstance(target, str)
1136
 
 
1137
 
 
1138
 
# TODO this and the InterRepository core logic should be consolidatable
1139
 
# if we make the registry a separate class though we still need to 
1140
 
# test the behaviour in the active registry to catch failure-to-handle-
1141
 
# stange-objects
1142
 
class TestInterVersionedFile(TestCaseWithMemoryTransport):
1143
 
 
1144
 
    def test_get_default_inter_versionedfile(self):
1145
 
        # test that the InterVersionedFile.get(a, b) probes
1146
 
        # for a class where is_compatible(a, b) returns
1147
 
        # true and returns a default interversionedfile otherwise.
1148
 
        # This also tests that the default registered optimised interversionedfile
1149
 
        # classes do not barf inappropriately when a surprising versionedfile type
1150
 
        # is handed to them.
1151
 
        dummy_a = "VersionedFile 1."
1152
 
        dummy_b = "VersionedFile 2."
1153
 
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
1154
 
 
1155
 
    def assertGetsDefaultInterVersionedFile(self, a, b):
1156
 
        """Asserts that InterVersionedFile.get(a, b) -> the default."""
1157
 
        inter = versionedfile.InterVersionedFile.get(a, b)
1158
 
        self.assertEqual(versionedfile.InterVersionedFile,
1159
 
                         inter.__class__)
1160
 
        self.assertEqual(a, inter.source)
1161
 
        self.assertEqual(b, inter.target)
1162
 
 
1163
 
    def test_register_inter_versionedfile_class(self):
1164
 
        # test that a optimised code path provider - a
1165
 
        # InterVersionedFile subclass can be registered and unregistered
1166
 
        # and that it is correctly selected when given a versionedfile
1167
 
        # pair that it returns true on for the is_compatible static method
1168
 
        # check
1169
 
        dummy_a = "VersionedFile 1."
1170
 
        dummy_b = "VersionedFile 2."
1171
 
        versionedfile.InterVersionedFile.register_optimiser(InterString)
1172
 
        try:
1173
 
            # we should get the default for something InterString returns False
1174
 
            # to
1175
 
            self.assertFalse(InterString.is_compatible(dummy_a, None))
1176
 
            self.assertGetsDefaultInterVersionedFile(dummy_a, None)
1177
 
            # and we should get an InterString for a pair it 'likes'
1178
 
            self.assertTrue(InterString.is_compatible(dummy_a, dummy_b))
1179
 
            inter = versionedfile.InterVersionedFile.get(dummy_a, dummy_b)
1180
 
            self.assertEqual(InterString, inter.__class__)
1181
 
            self.assertEqual(dummy_a, inter.source)
1182
 
            self.assertEqual(dummy_b, inter.target)
1183
 
        finally:
1184
 
            versionedfile.InterVersionedFile.unregister_optimiser(InterString)
1185
 
        # now we should get the default InterVersionedFile object again.
1186
 
        self.assertGetsDefaultInterVersionedFile(dummy_a, dummy_b)
 
953
        def get_record(suffix):
 
954
            return self.plan_merge_vf.get_record_stream(
 
955
                [('root', suffix)], 'unordered', True).next()
 
956
        self.assertEqual('a', get_record('A').get_bytes_as('fulltext'))
 
957
        self.assertEqual('c', get_record('C').get_bytes_as('fulltext'))
 
958
        self.assertEqual('e', get_record('E:').get_bytes_as('fulltext'))
 
959
        self.assertEqual('absent', get_record('F').storage_kind)
1187
960
 
1188
961
 
1189
962
class TestReadonlyHttpMixin(object):
1216
989
        return WeaveFile
1217
990
 
1218
991
 
1219
 
class TestKnitHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
1220
 
 
1221
 
    def get_file(self):
1222
 
        return make_file_knit('foo', get_transport(self.get_url('.')),
1223
 
            delta=True, create=True, get_scope=self.get_transaction)
1224
 
 
1225
 
    def get_factory(self):
1226
 
        return make_file_knit
1227
 
 
1228
 
 
1229
992
class MergeCasesMixin(object):
1230
993
 
1231
994
    def doMerge(self, base, a, b, mp):
1463
1226
        self._test_merge_from_strings(base, a, b, result)
1464
1227
 
1465
1228
 
1466
 
class TestKnitMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
1467
 
 
1468
 
    def get_file(self, name='foo'):
1469
 
        return make_file_knit(name, get_transport(self.get_url('.')),
1470
 
                                 delta=True, create=True)
1471
 
 
1472
 
    def log_contents(self, w):
1473
 
        pass
1474
 
 
1475
 
 
1476
1229
class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
1477
1230
 
1478
1231
    def get_file(self, name='foo'):
1513
1266
            self.assertIsInstance(adapter, klass)
1514
1267
 
1515
1268
    def get_knit(self, annotated=True):
1516
 
        if annotated:
1517
 
            factory = KnitAnnotateFactory()
1518
 
        else:
1519
 
            factory = KnitPlainFactory()
1520
 
        return make_file_knit('knit', self.get_transport('.'), delta=True,
1521
 
            create=True, factory=factory)
 
1269
        mapper = ConstantMapper('knit')
 
1270
        transport = self.get_transport()
 
1271
        return make_file_factory(annotated, mapper)(transport)
1522
1272
 
1523
1273
    def helpGetBytes(self, f, ft_adapter, delta_adapter):
1524
1274
        """Grab the interested adapted texts for tests."""
1525
1275
        # origin is a fulltext
1526
 
        entries = f.get_record_stream(['origin'], 'unordered', False)
 
1276
        entries = f.get_record_stream([('origin',)], 'unordered', False)
1527
1277
        base = entries.next()
1528
1278
        ft_data = ft_adapter.get_bytes(base, base.get_bytes_as(base.storage_kind))
1529
1279
        # merged is both a delta and multiple parents.
1530
 
        entries = f.get_record_stream(['merged'], 'unordered', False)
 
1280
        entries = f.get_record_stream([('merged',)], 'unordered', False)
1531
1281
        merged = entries.next()
1532
1282
        delta_data = delta_adapter.get_bytes(merged,
1533
1283
            merged.get_bytes_as(merged.storage_kind))
1536
1286
    def test_deannotation_noeol(self):
1537
1287
        """Test converting annotated knits to unannotated knits."""
1538
1288
        # we need a full text, and a delta
1539
 
        f, parents = get_diamond_vf(self.get_knit(), trailing_eol=False)
 
1289
        f = self.get_knit()
 
1290
        get_diamond_files(f, 1, trailing_eol=False)
1540
1291
        ft_data, delta_data = self.helpGetBytes(f,
1541
1292
            _mod_knit.FTAnnotatedToUnannotated(None),
1542
1293
            _mod_knit.DeltaAnnotatedToUnannotated(None))
1553
1304
    def test_deannotation(self):
1554
1305
        """Test converting annotated knits to unannotated knits."""
1555
1306
        # we need a full text, and a delta
1556
 
        f, parents = get_diamond_vf(self.get_knit())
 
1307
        f = self.get_knit()
 
1308
        get_diamond_files(f, 1)
1557
1309
        ft_data, delta_data = self.helpGetBytes(f,
1558
1310
            _mod_knit.FTAnnotatedToUnannotated(None),
1559
1311
            _mod_knit.DeltaAnnotatedToUnannotated(None))
1570
1322
    def test_annotated_to_fulltext_no_eol(self):
1571
1323
        """Test adapting annotated knits to full texts (for -> weaves)."""
1572
1324
        # we need a full text, and a delta
1573
 
        f, parents = get_diamond_vf(self.get_knit(), trailing_eol=False)
 
1325
        f = self.get_knit()
 
1326
        get_diamond_files(f, 1, trailing_eol=False)
1574
1327
        # Reconstructing a full text requires a backing versioned file, and it
1575
1328
        # must have the base lines requested from it.
1576
 
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1329
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
1577
1330
        ft_data, delta_data = self.helpGetBytes(f,
1578
1331
            _mod_knit.FTAnnotatedToFullText(None),
1579
1332
            _mod_knit.DeltaAnnotatedToFullText(logged_vf))
1580
1333
        self.assertEqual('origin', ft_data)
1581
1334
        self.assertEqual('base\nleft\nright\nmerged', delta_data)
1582
 
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
 
1335
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
 
1336
            True)], logged_vf.calls)
1583
1337
 
1584
1338
    def test_annotated_to_fulltext(self):
1585
1339
        """Test adapting annotated knits to full texts (for -> weaves)."""
1586
1340
        # we need a full text, and a delta
1587
 
        f, parents = get_diamond_vf(self.get_knit())
 
1341
        f = self.get_knit()
 
1342
        get_diamond_files(f, 1)
1588
1343
        # Reconstructing a full text requires a backing versioned file, and it
1589
1344
        # must have the base lines requested from it.
1590
 
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1345
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
1591
1346
        ft_data, delta_data = self.helpGetBytes(f,
1592
1347
            _mod_knit.FTAnnotatedToFullText(None),
1593
1348
            _mod_knit.DeltaAnnotatedToFullText(logged_vf))
1594
1349
        self.assertEqual('origin\n', ft_data)
1595
1350
        self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
1596
 
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
 
1351
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
 
1352
            True)], logged_vf.calls)
1597
1353
 
1598
1354
    def test_unannotated_to_fulltext(self):
1599
1355
        """Test adapting unannotated knits to full texts.
1601
1357
        This is used for -> weaves, and for -> annotated knits.
1602
1358
        """
1603
1359
        # we need a full text, and a delta
1604
 
        f, parents = get_diamond_vf(self.get_knit(annotated=False))
 
1360
        f = self.get_knit(annotated=False)
 
1361
        get_diamond_files(f, 1)
1605
1362
        # Reconstructing a full text requires a backing versioned file, and it
1606
1363
        # must have the base lines requested from it.
1607
 
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1364
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
1608
1365
        ft_data, delta_data = self.helpGetBytes(f,
1609
1366
            _mod_knit.FTPlainToFullText(None),
1610
1367
            _mod_knit.DeltaPlainToFullText(logged_vf))
1611
1368
        self.assertEqual('origin\n', ft_data)
1612
1369
        self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
1613
 
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
 
1370
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
 
1371
            True)], logged_vf.calls)
1614
1372
 
1615
1373
    def test_unannotated_to_fulltext_no_eol(self):
1616
1374
        """Test adapting unannotated knits to full texts.
1618
1376
        This is used for -> weaves, and for -> annotated knits.
1619
1377
        """
1620
1378
        # we need a full text, and a delta
1621
 
        f, parents = get_diamond_vf(self.get_knit(annotated=False),
1622
 
            trailing_eol=False)
 
1379
        f = self.get_knit(annotated=False)
 
1380
        get_diamond_files(f, 1, trailing_eol=False)
1623
1381
        # Reconstructing a full text requires a backing versioned file, and it
1624
1382
        # must have the base lines requested from it.
1625
 
        logged_vf = versionedfile.RecordingVersionedFileDecorator(f)
 
1383
        logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
1626
1384
        ft_data, delta_data = self.helpGetBytes(f,
1627
1385
            _mod_knit.FTPlainToFullText(None),
1628
1386
            _mod_knit.DeltaPlainToFullText(logged_vf))
1629
1387
        self.assertEqual('origin', ft_data)
1630
1388
        self.assertEqual('base\nleft\nright\nmerged', delta_data)
1631
 
        self.assertEqual([('get_lines', 'left')], logged_vf.calls)
1632
 
 
 
1389
        self.assertEqual([('get_record_stream', [('left',)], 'unordered',
 
1390
            True)], logged_vf.calls)
 
1391
 
 
1392
 
 
1393
class TestKeyMapper(TestCaseWithMemoryTransport):
 
1394
    """Tests for various key mapping logic."""
 
1395
 
 
1396
    def test_identity_mapper(self):
 
1397
        mapper = versionedfile.ConstantMapper("inventory")
 
1398
        self.assertEqual("inventory", mapper.map(('foo@ar',)))
 
1399
        self.assertEqual("inventory", mapper.map(('quux',)))
 
1400
 
 
1401
    def test_prefix_mapper(self):
 
1402
        #format5: plain
 
1403
        mapper = versionedfile.PrefixMapper()
 
1404
        self.assertEqual("file-id", mapper.map(("file-id", "revision-id")))
 
1405
        self.assertEqual("new-id", mapper.map(("new-id", "revision-id")))
 
1406
        self.assertEqual(('file-id',), mapper.unmap("file-id"))
 
1407
        self.assertEqual(('new-id',), mapper.unmap("new-id"))
 
1408
 
 
1409
    def test_hash_prefix_mapper(self):
 
1410
        #format6: hash + plain
 
1411
        mapper = versionedfile.HashPrefixMapper()
 
1412
        self.assertEqual("9b/file-id", mapper.map(("file-id", "revision-id")))
 
1413
        self.assertEqual("45/new-id", mapper.map(("new-id", "revision-id")))
 
1414
        self.assertEqual(('file-id',), mapper.unmap("9b/file-id"))
 
1415
        self.assertEqual(('new-id',), mapper.unmap("45/new-id"))
 
1416
 
 
1417
    def test_hash_escaped_mapper(self):
 
1418
        #knit1: hash + escaped
 
1419
        mapper = versionedfile.HashEscapedPrefixMapper()
 
1420
        self.assertEqual("88/%2520", mapper.map((" ", "revision-id")))
 
1421
        self.assertEqual("ed/fil%2545-%2549d", mapper.map(("filE-Id",
 
1422
            "revision-id")))
 
1423
        self.assertEqual("88/ne%2557-%2549d", mapper.map(("neW-Id",
 
1424
            "revision-id")))
 
1425
        self.assertEqual(('filE-Id',), mapper.unmap("ed/fil%2545-%2549d"))
 
1426
        self.assertEqual(('neW-Id',), mapper.unmap("88/ne%2557-%2549d"))
 
1427
 
 
1428
 
 
1429
class TestVersionedFiles(TestCaseWithMemoryTransport):
 
1430
    """Tests for the multiple-file variant of VersionedFile."""
 
1431
 
 
1432
    def get_versionedfiles(self, relpath='files'):
 
1433
        transport = self.get_transport(relpath)
 
1434
        if relpath != '.':
 
1435
            transport.mkdir('.')
 
1436
        files = self.factory(transport)
 
1437
        if self.cleanup is not None:
 
1438
            self.addCleanup(lambda:self.cleanup(files))
 
1439
        return files
 
1440
 
 
1441
    def test_annotate(self):
 
1442
        files = self.get_versionedfiles()
 
1443
        self.get_diamond_files(files)
 
1444
        if self.key_length == 1:
 
1445
            prefix = ()
 
1446
        else:
 
1447
            prefix = ('FileA',)
 
1448
        # introduced full text
 
1449
        origins = files.annotate(prefix + ('origin',))
 
1450
        self.assertEqual([
 
1451
            (prefix + ('origin',), 'origin\n')],
 
1452
            origins)
 
1453
        # a delta
 
1454
        origins = files.annotate(prefix + ('base',))
 
1455
        self.assertEqual([
 
1456
            (prefix + ('base',), 'base\n')],
 
1457
            origins)
 
1458
        # a merge
 
1459
        origins = files.annotate(prefix + ('merged',))
 
1460
        if self.graph:
 
1461
            self.assertEqual([
 
1462
                (prefix + ('base',), 'base\n'),
 
1463
                (prefix + ('left',), 'left\n'),
 
1464
                (prefix + ('right',), 'right\n'),
 
1465
                (prefix + ('merged',), 'merged\n')
 
1466
                ],
 
1467
                origins)
 
1468
        else:
 
1469
            # Without a graph everything is new.
 
1470
            self.assertEqual([
 
1471
                (prefix + ('merged',), 'base\n'),
 
1472
                (prefix + ('merged',), 'left\n'),
 
1473
                (prefix + ('merged',), 'right\n'),
 
1474
                (prefix + ('merged',), 'merged\n')
 
1475
                ],
 
1476
                origins)
 
1477
        self.assertRaises(RevisionNotPresent,
 
1478
            files.annotate, prefix + ('missing-key',))
 
1479
 
 
1480
    def test_construct(self):
 
1481
        """Each parameterised test can be constructed on a transport."""
 
1482
        files = self.get_versionedfiles()
 
1483
 
 
1484
    def get_diamond_files(self, files, trailing_eol=True, left_only=False):
 
1485
        return get_diamond_files(files, self.key_length,
 
1486
            trailing_eol=trailing_eol, nograph=not self.graph,
 
1487
            left_only=left_only)
 
1488
 
 
1489
    def test_add_lines_return(self):
 
1490
        files = self.get_versionedfiles()
 
1491
        # save code by using the stock data insertion helper.
 
1492
        adds = self.get_diamond_files(files)
 
1493
        results = []
 
1494
        # We can only validate the first 2 elements returned from add_lines.
 
1495
        for add in adds:
 
1496
            self.assertEqual(3, len(add))
 
1497
            results.append(add[:2])
 
1498
        if self.key_length == 1:
 
1499
            self.assertEqual([
 
1500
                ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
 
1501
                ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
 
1502
                ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
 
1503
                ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
 
1504
                ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
 
1505
                results)
 
1506
        elif self.key_length == 2:
 
1507
            self.assertEqual([
 
1508
                ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
 
1509
                ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
 
1510
                ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
 
1511
                ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
 
1512
                ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
 
1513
                ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
 
1514
                ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
 
1515
                ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
 
1516
                ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23),
 
1517
                ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
 
1518
                results)
 
1519
 
 
1520
    def test_empty_lines(self):
 
1521
        """Empty files can be stored."""
 
1522
        f = self.get_versionedfiles()
 
1523
        key_a = self.get_simple_key('a')
 
1524
        f.add_lines(key_a, [], [])
 
1525
        self.assertEqual('',
 
1526
            f.get_record_stream([key_a], 'unordered', True
 
1527
                ).next().get_bytes_as('fulltext'))
 
1528
        key_b = self.get_simple_key('b')
 
1529
        f.add_lines(key_b, self.get_parents([key_a]), [])
 
1530
        self.assertEqual('',
 
1531
            f.get_record_stream([key_b], 'unordered', True
 
1532
                ).next().get_bytes_as('fulltext'))
 
1533
 
 
1534
    def test_newline_only(self):
 
1535
        f = self.get_versionedfiles()
 
1536
        key_a = self.get_simple_key('a')
 
1537
        f.add_lines(key_a, [], ['\n'])
 
1538
        self.assertEqual('\n',
 
1539
            f.get_record_stream([key_a], 'unordered', True
 
1540
                ).next().get_bytes_as('fulltext'))
 
1541
        key_b = self.get_simple_key('b')
 
1542
        f.add_lines(key_b, self.get_parents([key_a]), ['\n'])
 
1543
        self.assertEqual('\n',
 
1544
            f.get_record_stream([key_b], 'unordered', True
 
1545
                ).next().get_bytes_as('fulltext'))
 
1546
 
 
1547
    def test_get_record_stream_empty(self):
 
1548
        """An empty stream can be requested without error."""
 
1549
        f = self.get_versionedfiles()
 
1550
        entries = f.get_record_stream([], 'unordered', False)
 
1551
        self.assertEqual([], list(entries))
 
1552
 
 
1553
    def assertValidStorageKind(self, storage_kind):
 
1554
        """Assert that storage_kind is a valid storage_kind."""
 
1555
        self.assertSubset([storage_kind],
 
1556
            ['mpdiff', 'knit-annotated-ft', 'knit-annotated-delta',
 
1557
             'knit-ft', 'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
 
1558
             'knit-annotated-delta-gz', 'knit-ft-gz', 'knit-delta-gz'])
 
1559
 
 
1560
    def capture_stream(self, f, entries, on_seen, parents):
 
1561
        """Capture a stream for testing."""
 
1562
        for factory in entries:
 
1563
            on_seen(factory.key)
 
1564
            self.assertValidStorageKind(factory.storage_kind)
 
1565
            self.assertEqual(f.get_sha1s([factory.key])[0], factory.sha1)
 
1566
            self.assertEqual(parents[factory.key], factory.parents)
 
1567
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
1568
                str)
 
1569
 
 
1570
    def test_get_record_stream_interface(self):
 
1571
        """each item in a stream has to provide a regular interface."""
 
1572
        files = self.get_versionedfiles()
 
1573
        self.get_diamond_files(files)
 
1574
        keys, _ = self.get_keys_and_sort_order()
 
1575
        parent_map = files.get_parent_map(keys)
 
1576
        entries = files.get_record_stream(keys, 'unordered', False)
 
1577
        seen = set()
 
1578
        self.capture_stream(files, entries, seen.add, parent_map)
 
1579
        self.assertEqual(set(keys), seen)
 
1580
 
 
1581
    def get_simple_key(self, suffix):
 
1582
        """Return a key for the object under test."""
 
1583
        if self.key_length == 1:
 
1584
            return (suffix,)
 
1585
        else:
 
1586
            return ('FileA',) + (suffix,)
 
1587
 
 
1588
    def get_keys_and_sort_order(self):
 
1589
        """Get diamond test keys list, and their sort ordering."""
 
1590
        if self.key_length == 1:
 
1591
            keys = [('merged',), ('left',), ('right',), ('base',)]
 
1592
            sort_order = {('merged',):2, ('left',):1, ('right',):1, ('base',):0}
 
1593
        else:
 
1594
            keys = [
 
1595
                ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
 
1596
                ('FileA', 'base'),
 
1597
                ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
 
1598
                ('FileB', 'base'),
 
1599
                ]
 
1600
            sort_order = {
 
1601
                ('FileA', 'merged'):2, ('FileA', 'left'):1, ('FileA', 'right'):1,
 
1602
                ('FileA', 'base'):0,
 
1603
                ('FileB', 'merged'):2, ('FileB', 'left'):1, ('FileB', 'right'):1,
 
1604
                ('FileB', 'base'):0,
 
1605
                }
 
1606
        return keys, sort_order
 
1607
 
 
1608
    def test_get_record_stream_interface_ordered(self):
 
1609
        """each item in a stream has to provide a regular interface."""
 
1610
        files = self.get_versionedfiles()
 
1611
        self.get_diamond_files(files)
 
1612
        keys, sort_order = self.get_keys_and_sort_order()
 
1613
        parent_map = files.get_parent_map(keys)
 
1614
        entries = files.get_record_stream(keys, 'topological', False)
 
1615
        seen = []
 
1616
        self.capture_stream(files, entries, seen.append, parent_map)
 
1617
        self.assertStreamOrder(sort_order, seen, keys)
 
1618
 
 
1619
    def test_get_record_stream_interface_ordered_with_delta_closure(self):
 
1620
        """each item must be accessible as a fulltext."""
 
1621
        files = self.get_versionedfiles()
 
1622
        self.get_diamond_files(files)
 
1623
        keys, sort_order = self.get_keys_and_sort_order()
 
1624
        parent_map = files.get_parent_map(keys)
 
1625
        entries = files.get_record_stream(keys, 'topological', True)
 
1626
        seen = []
 
1627
        for factory in entries:
 
1628
            seen.append(factory.key)
 
1629
            self.assertValidStorageKind(factory.storage_kind)
 
1630
            self.assertSubset([factory.sha1], [None, files.get_sha1s([factory.key])[0]])
 
1631
            self.assertEqual(parent_map[factory.key], factory.parents)
 
1632
            # self.assertEqual(files.get_text(factory.key),
 
1633
            self.assertIsInstance(factory.get_bytes_as('fulltext'), str)
 
1634
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
1635
                str)
 
1636
        self.assertStreamOrder(sort_order, seen, keys)
 
1637
 
 
1638
    def assertStreamOrder(self, sort_order, seen, keys):
 
1639
        self.assertEqual(len(set(seen)), len(keys))
 
1640
        if self.key_length == 1:
 
1641
            lows = {():0}
 
1642
        else:
 
1643
            lows = {('FileA',):0, ('FileB',):0}
 
1644
        if not self.graph:
 
1645
            self.assertEqual(set(keys), set(seen))
 
1646
        else:
 
1647
            for key in seen:
 
1648
                sort_pos = sort_order[key]
 
1649
                self.assertTrue(sort_pos >= lows[key[:-1]],
 
1650
                    "Out of order in sorted stream: %r, %r" % (key, seen))
 
1651
                lows[key[:-1]] = sort_pos
 
1652
 
 
1653
    def test_get_record_stream_unknown_storage_kind_raises(self):
 
1654
        """Asking for a storage kind that the stream cannot supply raises."""
 
1655
        files = self.get_versionedfiles()
 
1656
        self.get_diamond_files(files)
 
1657
        if self.key_length == 1:
 
1658
            keys = [('merged',), ('left',), ('right',), ('base',)]
 
1659
        else:
 
1660
            keys = [
 
1661
                ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
 
1662
                ('FileA', 'base'),
 
1663
                ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
 
1664
                ('FileB', 'base'),
 
1665
                ]
 
1666
        parent_map = files.get_parent_map(keys)
 
1667
        entries = files.get_record_stream(keys, 'unordered', False)
 
1668
        # We track the contents because we should be able to try, fail a
 
1669
        # particular kind and then ask for one that works and continue.
 
1670
        seen = set()
 
1671
        for factory in entries:
 
1672
            seen.add(factory.key)
 
1673
            self.assertValidStorageKind(factory.storage_kind)
 
1674
            self.assertEqual(files.get_sha1s([factory.key])[0], factory.sha1)
 
1675
            self.assertEqual(parent_map[factory.key], factory.parents)
 
1676
            # currently no stream emits mpdiff
 
1677
            self.assertRaises(errors.UnavailableRepresentation,
 
1678
                factory.get_bytes_as, 'mpdiff')
 
1679
            self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
1680
                str)
 
1681
        self.assertEqual(set(keys), seen)
 
1682
 
 
1683
    def test_get_record_stream_missing_records_are_absent(self):
 
1684
        files = self.get_versionedfiles()
 
1685
        self.get_diamond_files(files)
 
1686
        if self.key_length == 1:
 
1687
            keys = [('merged',), ('left',), ('right',), ('absent',), ('base',)]
 
1688
        else:
 
1689
            keys = [
 
1690
                ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
 
1691
                ('FileA', 'absent'), ('FileA', 'base'),
 
1692
                ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
 
1693
                ('FileB', 'absent'), ('FileB', 'base'),
 
1694
                ('absent', 'absent'),
 
1695
                ]
 
1696
        parent_map = files.get_parent_map(keys)
 
1697
        entries = files.get_record_stream(keys, 'unordered', False)
 
1698
        self.assertAbsentRecord(files, keys, parent_map, entries)
 
1699
        entries = files.get_record_stream(keys, 'topological', False)
 
1700
        self.assertAbsentRecord(files, keys, parent_map, entries)
 
1701
 
 
1702
    def assertAbsentRecord(self, files, keys, parents, entries):
 
1703
        """Helper for test_get_record_stream_missing_records_are_absent."""
 
1704
        seen = set()
 
1705
        for factory in entries:
 
1706
            seen.add(factory.key)
 
1707
            if factory.key[-1] == 'absent':
 
1708
                self.assertEqual('absent', factory.storage_kind)
 
1709
                self.assertEqual(None, factory.sha1)
 
1710
                self.assertEqual(None, factory.parents)
 
1711
            else:
 
1712
                self.assertValidStorageKind(factory.storage_kind)
 
1713
                self.assertEqual(files.get_sha1s([factory.key])[0], factory.sha1)
 
1714
                self.assertEqual(parents[factory.key], factory.parents)
 
1715
                self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
 
1716
                    str)
 
1717
        self.assertEqual(set(keys), seen)
 
1718
 
 
1719
    def test_filter_absent_records(self):
 
1720
        """Requested missing records can be filter trivially."""
 
1721
        files = self.get_versionedfiles()
 
1722
        self.get_diamond_files(files)
 
1723
        keys, _ = self.get_keys_and_sort_order()
 
1724
        parent_map = files.get_parent_map(keys)
 
1725
        # Add an absent record in the middle of the present keys. (We don't ask
 
1726
        # for just absent keys to ensure that content before and after the
 
1727
        # absent keys is still delivered).
 
1728
        present_keys = list(keys)
 
1729
        if self.key_length == 1:
 
1730
            keys.insert(2, ('extra',))
 
1731
        else:
 
1732
            keys.insert(2, ('extra', 'extra'))
 
1733
        entries = files.get_record_stream(keys, 'unordered', False)
 
1734
        seen = set()
 
1735
        self.capture_stream(files, versionedfile.filter_absent(entries), seen.add,
 
1736
            parent_map)
 
1737
        self.assertEqual(set(present_keys), seen)
 
1738
 
 
1739
    def get_mapper(self):
 
1740
        """Get a mapper suitable for the key length of the test interface."""
 
1741
        if self.key_length == 1:
 
1742
            return ConstantMapper('source')
 
1743
        else:
 
1744
            return HashEscapedPrefixMapper()
 
1745
 
 
1746
    def get_parents(self, parents):
 
1747
        """Get parents, taking self.graph into consideration."""
 
1748
        if self.graph:
 
1749
            return parents
 
1750
        else:
 
1751
            return None
 
1752
 
 
1753
    def test_get_parent_map(self):
 
1754
        files = self.get_versionedfiles()
 
1755
        if self.key_length == 1:
 
1756
            parent_details = [
 
1757
                (('r0',), self.get_parents(())),
 
1758
                (('r1',), self.get_parents((('r0',),))),
 
1759
                (('r2',), self.get_parents(())),
 
1760
                (('r3',), self.get_parents(())),
 
1761
                (('m',), self.get_parents((('r0',),('r1',),('r2',),('r3',)))),
 
1762
                ]
 
1763
        else:
 
1764
            parent_details = [
 
1765
                (('FileA', 'r0'), self.get_parents(())),
 
1766
                (('FileA', 'r1'), self.get_parents((('FileA', 'r0'),))),
 
1767
                (('FileA', 'r2'), self.get_parents(())),
 
1768
                (('FileA', 'r3'), self.get_parents(())),
 
1769
                (('FileA', 'm'), self.get_parents((('FileA', 'r0'),
 
1770
                    ('FileA', 'r1'), ('FileA', 'r2'), ('FileA', 'r3')))),
 
1771
                ]
 
1772
        for key, parents in parent_details:
 
1773
            files.add_lines(key, parents, [])
 
1774
            # immediately after adding it should be queryable.
 
1775
            self.assertEqual({key:parents}, files.get_parent_map([key]))
 
1776
        # We can ask for an empty set
 
1777
        self.assertEqual({}, files.get_parent_map([]))
 
1778
        # We can ask for many keys
 
1779
        all_parents = dict(parent_details)
 
1780
        self.assertEqual(all_parents, files.get_parent_map(all_parents.keys()))
 
1781
        # Absent keys are just not included in the result.
 
1782
        keys = all_parents.keys()
 
1783
        if self.key_length == 1:
 
1784
            keys.insert(1, ('missing',))
 
1785
        else:
 
1786
            keys.insert(1, ('missing', 'missing'))
 
1787
        # Absent keys are just ignored
 
1788
        self.assertEqual(all_parents, files.get_parent_map(keys))
 
1789
 
 
1790
    def test_get_sha1s(self):
 
1791
        files = self.get_versionedfiles()
 
1792
        self.get_diamond_files(files)
 
1793
        if self.key_length == 1:
 
1794
            keys = [('base',), ('origin',), ('left',), ('merged',), ('right',)]
 
1795
        else:
 
1796
            # ask for shas from different prefixes.
 
1797
            keys = [
 
1798
                ('FileA', 'base'), ('FileB', 'origin'), ('FileA', 'left'),
 
1799
                ('FileA', 'merged'), ('FileB', 'right'),
 
1800
                ]
 
1801
        self.assertEqual([
 
1802
            '51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44',
 
1803
            '00e364d235126be43292ab09cb4686cf703ddc17',
 
1804
            'a8478686da38e370e32e42e8a0c220e33ee9132f',
 
1805
            'ed8bce375198ea62444dc71952b22cfc2b09226d',
 
1806
            '9ef09dfa9d86780bdec9219a22560c6ece8e0ef1',
 
1807
            ],
 
1808
            files.get_sha1s(keys))
 
1809
        
 
1810
    def test_insert_record_stream_empty(self):
 
1811
        """Inserting an empty record stream should work."""
 
1812
        files = self.get_versionedfiles()
 
1813
        files.insert_record_stream([])
 
1814
 
 
1815
    def assertIdenticalVersionedFile(self, expected, actual):
 
1816
        """Assert that left and right have the same contents."""
 
1817
        self.assertEqual(set(actual.keys()), set(expected.keys()))
 
1818
        actual_parents = actual.get_parent_map(actual.keys())
 
1819
        if self.graph:
 
1820
            self.assertEqual(actual_parents, expected.get_parent_map(expected.keys()))
 
1821
        else:
 
1822
            for key, parents in actual_parents.items():
 
1823
                self.assertEqual(None, parents)
 
1824
        for key in actual.keys():
 
1825
            actual_text = actual.get_record_stream(
 
1826
                [key], 'unordered', True).next().get_bytes_as('fulltext')
 
1827
            expected_text = expected.get_record_stream(
 
1828
                [key], 'unordered', True).next().get_bytes_as('fulltext')
 
1829
            self.assertEqual(actual_text, expected_text)
 
1830
 
 
1831
    def test_insert_record_stream_fulltexts(self):
 
1832
        """Any file should accept a stream of fulltexts."""
 
1833
        files = self.get_versionedfiles()
 
1834
        mapper = self.get_mapper()
 
1835
        source_transport = self.get_transport('source')
 
1836
        source_transport.mkdir('.')
 
1837
        # weaves always output fulltexts.
 
1838
        source = make_versioned_files_factory(WeaveFile, mapper)(
 
1839
            source_transport)
 
1840
        self.get_diamond_files(source, trailing_eol=False)
 
1841
        stream = source.get_record_stream(source.keys(), 'topological',
 
1842
            False)
 
1843
        files.insert_record_stream(stream)
 
1844
        self.assertIdenticalVersionedFile(source, files)
 
1845
 
 
1846
    def test_insert_record_stream_fulltexts_noeol(self):
 
1847
        """Any file should accept a stream of fulltexts."""
 
1848
        files = self.get_versionedfiles()
 
1849
        mapper = self.get_mapper()
 
1850
        source_transport = self.get_transport('source')
 
1851
        source_transport.mkdir('.')
 
1852
        # weaves always output fulltexts.
 
1853
        source = make_versioned_files_factory(WeaveFile, mapper)(
 
1854
            source_transport)
 
1855
        self.get_diamond_files(source, trailing_eol=False)
 
1856
        stream = source.get_record_stream(source.keys(), 'topological',
 
1857
            False)
 
1858
        files.insert_record_stream(stream)
 
1859
        self.assertIdenticalVersionedFile(source, files)
 
1860
 
 
1861
    def test_insert_record_stream_annotated_knits(self):
 
1862
        """Any file should accept a stream from plain knits."""
 
1863
        files = self.get_versionedfiles()
 
1864
        mapper = self.get_mapper()
 
1865
        source_transport = self.get_transport('source')
 
1866
        source_transport.mkdir('.')
 
1867
        source = make_file_factory(True, mapper)(source_transport)
 
1868
        self.get_diamond_files(source)
 
1869
        stream = source.get_record_stream(source.keys(), 'topological',
 
1870
            False)
 
1871
        files.insert_record_stream(stream)
 
1872
        self.assertIdenticalVersionedFile(source, files)
 
1873
 
 
1874
    def test_insert_record_stream_annotated_knits_noeol(self):
 
1875
        """Any file should accept a stream from plain knits."""
 
1876
        files = self.get_versionedfiles()
 
1877
        mapper = self.get_mapper()
 
1878
        source_transport = self.get_transport('source')
 
1879
        source_transport.mkdir('.')
 
1880
        source = make_file_factory(True, mapper)(source_transport)
 
1881
        self.get_diamond_files(source, trailing_eol=False)
 
1882
        stream = source.get_record_stream(source.keys(), 'topological',
 
1883
            False)
 
1884
        files.insert_record_stream(stream)
 
1885
        self.assertIdenticalVersionedFile(source, files)
 
1886
 
 
1887
    def test_insert_record_stream_plain_knits(self):
 
1888
        """Any file should accept a stream from plain knits."""
 
1889
        files = self.get_versionedfiles()
 
1890
        mapper = self.get_mapper()
 
1891
        source_transport = self.get_transport('source')
 
1892
        source_transport.mkdir('.')
 
1893
        source = make_file_factory(False, mapper)(source_transport)
 
1894
        self.get_diamond_files(source)
 
1895
        stream = source.get_record_stream(source.keys(), 'topological',
 
1896
            False)
 
1897
        files.insert_record_stream(stream)
 
1898
        self.assertIdenticalVersionedFile(source, files)
 
1899
 
 
1900
    def test_insert_record_stream_plain_knits_noeol(self):
 
1901
        """Any file should accept a stream from plain knits."""
 
1902
        files = self.get_versionedfiles()
 
1903
        mapper = self.get_mapper()
 
1904
        source_transport = self.get_transport('source')
 
1905
        source_transport.mkdir('.')
 
1906
        source = make_file_factory(False, mapper)(source_transport)
 
1907
        self.get_diamond_files(source, trailing_eol=False)
 
1908
        stream = source.get_record_stream(source.keys(), 'topological',
 
1909
            False)
 
1910
        files.insert_record_stream(stream)
 
1911
        self.assertIdenticalVersionedFile(source, files)
 
1912
 
 
1913
    def test_insert_record_stream_existing_keys(self):
 
1914
        """Inserting keys already in a file should not error."""
 
1915
        files = self.get_versionedfiles()
 
1916
        source = self.get_versionedfiles('source')
 
1917
        self.get_diamond_files(source)
 
1918
        # insert some keys into f.
 
1919
        self.get_diamond_files(files, left_only=True)
 
1920
        stream = source.get_record_stream(source.keys(), 'topological',
 
1921
            False)
 
1922
        files.insert_record_stream(stream)
 
1923
        self.assertIdenticalVersionedFile(source, files)
 
1924
 
 
1925
    def test_insert_record_stream_missing_keys(self):
 
1926
        """Inserting a stream with absent keys should raise an error."""
 
1927
        files = self.get_versionedfiles()
 
1928
        source = self.get_versionedfiles('source')
 
1929
        stream = source.get_record_stream([('missing',) * self.key_length],
 
1930
            'topological', False)
 
1931
        self.assertRaises(errors.RevisionNotPresent, files.insert_record_stream,
 
1932
            stream)
 
1933
 
 
1934
    def test_insert_record_stream_out_of_order(self):
 
1935
        """An out of order stream can either error or work."""
 
1936
        files = self.get_versionedfiles()
 
1937
        source = self.get_versionedfiles('source')
 
1938
        self.get_diamond_files(source)
 
1939
        if self.key_length == 1:
 
1940
            origin_keys = [('origin',)]
 
1941
            end_keys = [('merged',), ('left',)]
 
1942
            start_keys = [('right',), ('base',)]
 
1943
        else:
 
1944
            origin_keys = [('FileA', 'origin'), ('FileB', 'origin')]
 
1945
            end_keys = [('FileA', 'merged',), ('FileA', 'left',),
 
1946
                ('FileB', 'merged',), ('FileB', 'left',)]
 
1947
            start_keys = [('FileA', 'right',), ('FileA', 'base',),
 
1948
                ('FileB', 'right',), ('FileB', 'base',)]
 
1949
        origin_entries = source.get_record_stream(origin_keys, 'unordered', False)
 
1950
        end_entries = source.get_record_stream(end_keys, 'topological', False)
 
1951
        start_entries = source.get_record_stream(start_keys, 'topological', False)
 
1952
        entries = chain(origin_entries, end_entries, start_entries)
 
1953
        try:
 
1954
            files.insert_record_stream(entries)
 
1955
        except RevisionNotPresent:
 
1956
            # Must not have corrupted the file.
 
1957
            files.check()
 
1958
        else:
 
1959
            self.assertIdenticalVersionedFile(source, files)
 
1960
 
 
1961
    def test_insert_record_stream_delta_missing_basis_no_corruption(self):
 
1962
        """Insertion where a needed basis is not included aborts safely."""
 
1963
        # We use a knit always here to be sure we are getting a binary delta.
 
1964
        mapper = self.get_mapper()
 
1965
        source_transport = self.get_transport('source')
 
1966
        source_transport.mkdir('.')
 
1967
        source = make_file_factory(False, mapper)(source_transport)
 
1968
        self.get_diamond_files(source)
 
1969
        entries = source.get_record_stream(['origin', 'merged'], 'unordered', False)
 
1970
        files = self.get_versionedfiles()
 
1971
        self.assertRaises(RevisionNotPresent, files.insert_record_stream,
 
1972
            entries)
 
1973
        files.check()
 
1974
        self.assertEqual({}, files.get_parent_map([]))
 
1975
 
 
1976
    def test_iter_lines_added_or_present_in_keys(self):
 
1977
        # test that we get at least an equalset of the lines added by
 
1978
        # versions in the store.
 
1979
        # the ordering here is to make a tree so that dumb searches have
 
1980
        # more changes to muck up.
 
1981
 
 
1982
        class InstrumentedProgress(progress.DummyProgress):
 
1983
 
 
1984
            def __init__(self):
 
1985
 
 
1986
                progress.DummyProgress.__init__(self)
 
1987
                self.updates = []
 
1988
 
 
1989
            def update(self, msg=None, current=None, total=None):
 
1990
                self.updates.append((msg, current, total))
 
1991
 
 
1992
        files = self.get_versionedfiles()
 
1993
        # add a base to get included
 
1994
        files.add_lines(self.get_simple_key('base'), (), ['base\n'])
 
1995
        # add a ancestor to be included on one side
 
1996
        files.add_lines(self.get_simple_key('lancestor'), (), ['lancestor\n'])
 
1997
        # add a ancestor to be included on the other side
 
1998
        files.add_lines(self.get_simple_key('rancestor'),
 
1999
            self.get_parents([self.get_simple_key('base')]), ['rancestor\n'])
 
2000
        # add a child of rancestor with no eofile-nl
 
2001
        files.add_lines(self.get_simple_key('child'),
 
2002
            self.get_parents([self.get_simple_key('rancestor')]),
 
2003
            ['base\n', 'child\n'])
 
2004
        # add a child of lancestor and base to join the two roots
 
2005
        files.add_lines(self.get_simple_key('otherchild'),
 
2006
            self.get_parents([self.get_simple_key('lancestor'),
 
2007
                self.get_simple_key('base')]),
 
2008
            ['base\n', 'lancestor\n', 'otherchild\n'])
 
2009
        def iter_with_keys(keys, expected):
 
2010
            # now we need to see what lines are returned, and how often.
 
2011
            lines = {}
 
2012
            progress = InstrumentedProgress()
 
2013
            # iterate over the lines
 
2014
            for line in files.iter_lines_added_or_present_in_keys(keys,
 
2015
                pb=progress):
 
2016
                lines.setdefault(line, 0)
 
2017
                lines[line] += 1
 
2018
            if []!= progress.updates:
 
2019
                self.assertEqual(expected, progress.updates)
 
2020
            return lines
 
2021
        lines = iter_with_keys(
 
2022
            [self.get_simple_key('child'), self.get_simple_key('otherchild')],
 
2023
            [('Walking content.', 0, 2),
 
2024
             ('Walking content.', 1, 2),
 
2025
             ('Walking content.', 2, 2)])
 
2026
        # we must see child and otherchild
 
2027
        self.assertTrue(lines[('child\n', self.get_simple_key('child'))] > 0)
 
2028
        self.assertTrue(
 
2029
            lines[('otherchild\n', self.get_simple_key('otherchild'))] > 0)
 
2030
        # we dont care if we got more than that.
 
2031
        
 
2032
        # test all lines
 
2033
        lines = iter_with_keys(files.keys(),
 
2034
            [('Walking content.', 0, 5),
 
2035
             ('Walking content.', 1, 5),
 
2036
             ('Walking content.', 2, 5),
 
2037
             ('Walking content.', 3, 5),
 
2038
             ('Walking content.', 4, 5),
 
2039
             ('Walking content.', 5, 5)])
 
2040
        # all lines must be seen at least once
 
2041
        self.assertTrue(lines[('base\n', self.get_simple_key('base'))] > 0)
 
2042
        self.assertTrue(
 
2043
            lines[('lancestor\n', self.get_simple_key('lancestor'))] > 0)
 
2044
        self.assertTrue(
 
2045
            lines[('rancestor\n', self.get_simple_key('rancestor'))] > 0)
 
2046
        self.assertTrue(lines[('child\n', self.get_simple_key('child'))] > 0)
 
2047
        self.assertTrue(
 
2048
            lines[('otherchild\n', self.get_simple_key('otherchild'))] > 0)
 
2049
 
 
2050
    def test_make_mpdiffs(self):
 
2051
        from bzrlib import multiparent
 
2052
        files = self.get_versionedfiles('source')
 
2053
        # add texts that should trip the knit maximum delta chain threshold
 
2054
        # as well as doing parallel chains of data in knits.
 
2055
        # this is done by two chains of 25 insertions
 
2056
        files.add_lines(self.get_simple_key('base'), [], ['line\n'])
 
2057
        files.add_lines(self.get_simple_key('noeol'),
 
2058
            self.get_parents([self.get_simple_key('base')]), ['line'])
 
2059
        # detailed eol tests:
 
2060
        # shared last line with parent no-eol
 
2061
        files.add_lines(self.get_simple_key('noeolsecond'),
 
2062
            self.get_parents([self.get_simple_key('noeol')]),
 
2063
                ['line\n', 'line'])
 
2064
        # differing last line with parent, both no-eol
 
2065
        files.add_lines(self.get_simple_key('noeolnotshared'),
 
2066
            self.get_parents([self.get_simple_key('noeolsecond')]),
 
2067
                ['line\n', 'phone'])
 
2068
        # add eol following a noneol parent, change content
 
2069
        files.add_lines(self.get_simple_key('eol'),
 
2070
            self.get_parents([self.get_simple_key('noeol')]), ['phone\n'])
 
2071
        # add eol following a noneol parent, no change content
 
2072
        files.add_lines(self.get_simple_key('eolline'),
 
2073
            self.get_parents([self.get_simple_key('noeol')]), ['line\n'])
 
2074
        # noeol with no parents:
 
2075
        files.add_lines(self.get_simple_key('noeolbase'), [], ['line'])
 
2076
        # noeol preceeding its leftmost parent in the output:
 
2077
        # this is done by making it a merge of two parents with no common
 
2078
        # anestry: noeolbase and noeol with the 
 
2079
        # later-inserted parent the leftmost.
 
2080
        files.add_lines(self.get_simple_key('eolbeforefirstparent'),
 
2081
            self.get_parents([self.get_simple_key('noeolbase'),
 
2082
                self.get_simple_key('noeol')]),
 
2083
            ['line'])
 
2084
        # two identical eol texts
 
2085
        files.add_lines(self.get_simple_key('noeoldup'),
 
2086
            self.get_parents([self.get_simple_key('noeol')]), ['line'])
 
2087
        next_parent = self.get_simple_key('base')
 
2088
        text_name = 'chain1-'
 
2089
        text = ['line\n']
 
2090
        sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
 
2091
                 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
 
2092
                 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
 
2093
                 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
 
2094
                 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
 
2095
                 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
 
2096
                 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
 
2097
                 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
 
2098
                 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
 
2099
                 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
 
2100
                 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
 
2101
                 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
 
2102
                 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
 
2103
                 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
 
2104
                 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
 
2105
                 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
 
2106
                 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
 
2107
                 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
 
2108
                 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
 
2109
                 19:'1ebed371807ba5935958ad0884595126e8c4e823',
 
2110
                 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
 
2111
                 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
 
2112
                 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
 
2113
                 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
 
2114
                 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
 
2115
                 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
 
2116
                 }
 
2117
        for depth in range(26):
 
2118
            new_version = self.get_simple_key(text_name + '%s' % depth)
 
2119
            text = text + ['line\n']
 
2120
            files.add_lines(new_version, self.get_parents([next_parent]), text)
 
2121
            next_parent = new_version
 
2122
        next_parent = self.get_simple_key('base')
 
2123
        text_name = 'chain2-'
 
2124
        text = ['line\n']
 
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
        target = self.get_versionedfiles('target')
 
2131
        for key in multiparent.topo_iter_keys(files, files.keys()):
 
2132
            mpdiff = files.make_mpdiffs([key])[0]
 
2133
            parents = files.get_parent_map([key])[key] or []
 
2134
            target.add_mpdiffs(
 
2135
                [(key, parents, files.get_sha1s([key])[0], mpdiff)])
 
2136
            self.assertEqualDiff(
 
2137
                files.get_record_stream([key], 'unordered',
 
2138
                    True).next().get_bytes_as('fulltext'),
 
2139
                target.get_record_stream([key], 'unordered',
 
2140
                    True).next().get_bytes_as('fulltext')
 
2141
                )
 
2142
 
 
2143
    def test_keys(self):
 
2144
        # While use is discouraged, versions() is still needed by aspects of
 
2145
        # bzr.
 
2146
        files = self.get_versionedfiles()
 
2147
        self.assertEqual(set(), set(files.keys()))
 
2148
        if self.key_length == 1:
 
2149
            key = ('foo',)
 
2150
        else:
 
2151
            key = ('foo', 'bar',)
 
2152
        files.add_lines(key, (), [])
 
2153
        self.assertEqual(set([key]), set(files.keys()))