~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_knit.py

  • Committer: Ian Clatworthy
  • Date: 2009-09-09 11:43:10 UTC
  • mto: (4634.37.2 prepare-2.0)
  • mto: This revision was merged to the branch mainline in revision 4689.
  • Revision ID: ian.clatworthy@canonical.com-20090909114310-glw7tv76i5gnx9pt
put rules back in Makefile supporting plain-style docs

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""Tests for Knit data structure"""
18
18
 
19
19
from cStringIO import StringIO
20
20
import difflib
21
21
import gzip
22
 
import sha
23
22
import sys
24
23
 
25
24
from bzrlib import (
26
25
    errors,
27
26
    generate_ids,
28
27
    knit,
 
28
    multiparent,
 
29
    osutils,
29
30
    pack,
30
31
    )
31
32
from bzrlib.errors import (
36
37
    )
37
38
from bzrlib.index import *
38
39
from bzrlib.knit import (
 
40
    AnnotatedKnitContent,
39
41
    KnitContent,
40
 
    KnitGraphIndex,
41
 
    KnitVersionedFile,
42
 
    KnitPlainFactory,
43
 
    KnitAnnotateFactory,
44
 
    _KnitAccess,
45
 
    _KnitData,
46
 
    _KnitIndex,
47
 
    _PackAccess,
48
 
    WeaveToKnit,
49
42
    KnitSequenceMatcher,
 
43
    KnitVersionedFiles,
 
44
    PlainKnitContent,
 
45
    _VFContentMapGenerator,
 
46
    _DirectPackAccess,
 
47
    _KndxIndex,
 
48
    _KnitGraphIndex,
 
49
    _KnitKeyAccess,
 
50
    make_file_factory,
50
51
    )
51
 
from bzrlib.osutils import split_lines
 
52
from bzrlib.repofmt import pack_repo
52
53
from bzrlib.tests import (
53
54
    Feature,
 
55
    KnownFailure,
54
56
    TestCase,
55
57
    TestCaseWithMemoryTransport,
56
58
    TestCaseWithTransport,
 
59
    TestNotApplicable,
57
60
    )
58
 
from bzrlib.transport import TransportLogger, get_transport
 
61
from bzrlib.transport import get_transport
59
62
from bzrlib.transport.memory import MemoryTransport
60
 
from bzrlib.util import bencode
61
 
from bzrlib.weave import Weave
 
63
from bzrlib.tuned_gzip import GzipFile
 
64
from bzrlib.versionedfile import (
 
65
    AbsentContentFactory,
 
66
    ConstantMapper,
 
67
    network_bytes_to_kind_and_offset,
 
68
    RecordingVersionedFilesDecorator,
 
69
    )
62
70
 
63
71
 
64
72
class _CompiledKnitFeature(Feature):
65
73
 
66
74
    def _probe(self):
67
75
        try:
68
 
            import bzrlib._knit_load_data_c
 
76
            import bzrlib._knit_load_data_pyx
69
77
        except ImportError:
70
78
            return False
71
79
        return True
72
80
 
73
81
    def feature_name(self):
74
 
        return 'bzrlib._knit_load_data_c'
 
82
        return 'bzrlib._knit_load_data_pyx'
75
83
 
76
84
CompiledKnitFeature = _CompiledKnitFeature()
77
85
 
78
86
 
79
 
class KnitContentTests(TestCase):
 
87
class KnitContentTestsMixin(object):
80
88
 
81
89
    def test_constructor(self):
82
 
        content = KnitContent([])
 
90
        content = self._make_content([])
83
91
 
84
92
    def test_text(self):
85
 
        content = KnitContent([])
 
93
        content = self._make_content([])
86
94
        self.assertEqual(content.text(), [])
87
95
 
88
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
 
96
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
89
97
        self.assertEqual(content.text(), ["text1", "text2"])
90
98
 
91
 
    def test_annotate(self):
92
 
        content = KnitContent([])
93
 
        self.assertEqual(content.annotate(), [])
94
 
 
95
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
96
 
        self.assertEqual(content.annotate(),
97
 
            [("origin1", "text1"), ("origin2", "text2")])
98
 
 
99
 
    def test_annotate_iter(self):
100
 
        content = KnitContent([])
101
 
        it = content.annotate_iter()
102
 
        self.assertRaises(StopIteration, it.next)
103
 
 
104
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
105
 
        it = content.annotate_iter()
106
 
        self.assertEqual(it.next(), ("origin1", "text1"))
107
 
        self.assertEqual(it.next(), ("origin2", "text2"))
108
 
        self.assertRaises(StopIteration, it.next)
109
 
 
110
99
    def test_copy(self):
111
 
        content = KnitContent([("origin1", "text1"), ("origin2", "text2")])
 
100
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
112
101
        copy = content.copy()
113
 
        self.assertIsInstance(copy, KnitContent)
114
 
        self.assertEqual(copy.annotate(),
 
102
        self.assertIsInstance(copy, content.__class__)
 
103
        self.assertEqual(copy.annotate(), content.annotate())
 
104
 
 
105
    def assertDerivedBlocksEqual(self, source, target, noeol=False):
 
106
        """Assert that the derived matching blocks match real output"""
 
107
        source_lines = source.splitlines(True)
 
108
        target_lines = target.splitlines(True)
 
109
        def nl(line):
 
110
            if noeol and not line.endswith('\n'):
 
111
                return line + '\n'
 
112
            else:
 
113
                return line
 
114
        source_content = self._make_content([(None, nl(l)) for l in source_lines])
 
115
        target_content = self._make_content([(None, nl(l)) for l in target_lines])
 
116
        line_delta = source_content.line_delta(target_content)
 
117
        delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
 
118
            source_lines, target_lines))
 
119
        matcher = KnitSequenceMatcher(None, source_lines, target_lines)
 
120
        matcher_blocks = list(list(matcher.get_matching_blocks()))
 
121
        self.assertEqual(matcher_blocks, delta_blocks)
 
122
 
 
123
    def test_get_line_delta_blocks(self):
 
124
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
 
125
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
 
126
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
 
127
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
 
128
        self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
 
129
        self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
 
130
        self.assertDerivedBlocksEqual(TEXT_1A, '')
 
131
        self.assertDerivedBlocksEqual('', TEXT_1A)
 
132
        self.assertDerivedBlocksEqual('', '')
 
133
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
 
134
 
 
135
    def test_get_line_delta_blocks_noeol(self):
 
136
        """Handle historical knit deltas safely
 
137
 
 
138
        Some existing knit deltas don't consider the last line to differ
 
139
        when the only difference whether it has a final newline.
 
140
 
 
141
        New knit deltas appear to always consider the last line to differ
 
142
        in this case.
 
143
        """
 
144
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
 
145
        self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
 
146
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
 
147
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
 
148
 
 
149
 
 
150
TEXT_1 = """\
 
151
Banana cup cakes:
 
152
 
 
153
- bananas
 
154
- eggs
 
155
- broken tea cups
 
156
"""
 
157
 
 
158
TEXT_1A = """\
 
159
Banana cup cake recipe
 
160
(serves 6)
 
161
 
 
162
- bananas
 
163
- eggs
 
164
- broken tea cups
 
165
- self-raising flour
 
166
"""
 
167
 
 
168
TEXT_1B = """\
 
169
Banana cup cake recipe
 
170
 
 
171
- bananas (do not use plantains!!!)
 
172
- broken tea cups
 
173
- flour
 
174
"""
 
175
 
 
176
delta_1_1a = """\
 
177
0,1,2
 
178
Banana cup cake recipe
 
179
(serves 6)
 
180
5,5,1
 
181
- self-raising flour
 
182
"""
 
183
 
 
184
TEXT_2 = """\
 
185
Boeuf bourguignon
 
186
 
 
187
- beef
 
188
- red wine
 
189
- small onions
 
190
- carrot
 
191
- mushrooms
 
192
"""
 
193
 
 
194
 
 
195
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
 
196
 
 
197
    def _make_content(self, lines):
 
198
        annotated_content = AnnotatedKnitContent(lines)
 
199
        return PlainKnitContent(annotated_content.text(), 'bogus')
 
200
 
 
201
    def test_annotate(self):
 
202
        content = self._make_content([])
 
203
        self.assertEqual(content.annotate(), [])
 
204
 
 
205
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
206
        self.assertEqual(content.annotate(),
 
207
            [("bogus", "text1"), ("bogus", "text2")])
 
208
 
 
209
    def test_line_delta(self):
 
210
        content1 = self._make_content([("", "a"), ("", "b")])
 
211
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
212
        self.assertEqual(content1.line_delta(content2),
 
213
            [(1, 2, 2, ["a", "c"])])
 
214
 
 
215
    def test_line_delta_iter(self):
 
216
        content1 = self._make_content([("", "a"), ("", "b")])
 
217
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
 
218
        it = content1.line_delta_iter(content2)
 
219
        self.assertEqual(it.next(), (1, 2, 2, ["a", "c"]))
 
220
        self.assertRaises(StopIteration, it.next)
 
221
 
 
222
 
 
223
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
 
224
 
 
225
    def _make_content(self, lines):
 
226
        return AnnotatedKnitContent(lines)
 
227
 
 
228
    def test_annotate(self):
 
229
        content = self._make_content([])
 
230
        self.assertEqual(content.annotate(), [])
 
231
 
 
232
        content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
 
233
        self.assertEqual(content.annotate(),
115
234
            [("origin1", "text1"), ("origin2", "text2")])
116
235
 
117
236
    def test_line_delta(self):
118
 
        content1 = KnitContent([("", "a"), ("", "b")])
119
 
        content2 = KnitContent([("", "a"), ("", "a"), ("", "c")])
 
237
        content1 = self._make_content([("", "a"), ("", "b")])
 
238
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
120
239
        self.assertEqual(content1.line_delta(content2),
121
240
            [(1, 2, 2, [("", "a"), ("", "c")])])
122
241
 
123
242
    def test_line_delta_iter(self):
124
 
        content1 = KnitContent([("", "a"), ("", "b")])
125
 
        content2 = KnitContent([("", "a"), ("", "a"), ("", "c")])
 
243
        content1 = self._make_content([("", "a"), ("", "b")])
 
244
        content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
126
245
        it = content1.line_delta_iter(content2)
127
246
        self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
128
247
        self.assertRaises(StopIteration, it.next)
154
273
        return queue_call
155
274
 
156
275
 
 
276
class MockReadvFailingTransport(MockTransport):
 
277
    """Fail in the middle of a readv() result.
 
278
 
 
279
    This Transport will successfully yield the first two requested hunks, but
 
280
    raise NoSuchFile for the rest.
 
281
    """
 
282
 
 
283
    def readv(self, relpath, offsets):
 
284
        count = 0
 
285
        for result in MockTransport.readv(self, relpath, offsets):
 
286
            count += 1
 
287
            # we use 2 because the first offset is the pack header, the second
 
288
            # is the first actual content requset
 
289
            if count > 2:
 
290
                raise errors.NoSuchFile(relpath)
 
291
            yield result
 
292
 
 
293
 
157
294
class KnitRecordAccessTestsMixin(object):
158
295
    """Tests for getting and putting knit records."""
159
296
 
160
 
    def assertAccessExists(self, access):
161
 
        """Ensure the data area for access has been initialised/exists."""
162
 
        raise NotImplementedError(self.assertAccessExists)
163
 
 
164
297
    def test_add_raw_records(self):
165
298
        """Add_raw_records adds records retrievable later."""
166
299
        access = self.get_access()
167
 
        memos = access.add_raw_records([10], '1234567890')
 
300
        memos = access.add_raw_records([('key', 10)], '1234567890')
168
301
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
169
 
 
 
302
 
170
303
    def test_add_several_raw_records(self):
171
304
        """add_raw_records with many records and read some back."""
172
305
        access = self.get_access()
173
 
        memos = access.add_raw_records([10, 2, 5], '12345678901234567')
 
306
        memos = access.add_raw_records([('key', 10), ('key2', 2), ('key3', 5)],
 
307
            '12345678901234567')
174
308
        self.assertEqual(['1234567890', '12', '34567'],
175
309
            list(access.get_raw_records(memos)))
176
310
        self.assertEqual(['1234567890'],
182
316
        self.assertEqual(['1234567890', '34567'],
183
317
            list(access.get_raw_records(memos[0:1] + memos[2:3])))
184
318
 
185
 
    def test_create(self):
186
 
        """create() should make a file on disk."""
187
 
        access = self.get_access()
188
 
        access.create()
189
 
        self.assertAccessExists(access)
190
 
 
191
 
    def test_open_file(self):
192
 
        """open_file never errors."""
193
 
        access = self.get_access()
194
 
        access.open_file()
195
 
 
196
319
 
197
320
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
198
321
    """Tests for the .kndx implementation."""
199
322
 
200
 
    def assertAccessExists(self, access):
201
 
        self.assertNotEqual(None, access.open_file())
202
 
 
203
323
    def get_access(self):
204
324
        """Get a .knit style access instance."""
205
 
        access = _KnitAccess(self.get_transport(), "foo.knit", None, None,
206
 
            False, False)
 
325
        mapper = ConstantMapper("foo")
 
326
        access = _KnitKeyAccess(self.get_transport(), mapper)
207
327
        return access
208
 
    
 
328
 
 
329
 
 
330
class _TestException(Exception):
 
331
    """Just an exception for local tests to use."""
 
332
 
209
333
 
210
334
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
211
335
    """Tests for the pack based access."""
212
336
 
213
 
    def assertAccessExists(self, access):
214
 
        # as pack based access has no backing unless an index maps data, this
215
 
        # is a no-op.
216
 
        pass
217
 
 
218
337
    def get_access(self):
219
338
        return self._get_access()[0]
220
339
 
224
343
            transport.append_bytes(packname, bytes)
225
344
        writer = pack.ContainerWriter(write_data)
226
345
        writer.begin()
227
 
        indices = {index:(transport, packname)}
228
 
        access = _PackAccess(indices, writer=(writer, index))
 
346
        access = _DirectPackAccess({})
 
347
        access.set_writer(writer, index, (transport, packname))
229
348
        return access, writer
230
349
 
 
350
    def make_pack_file(self):
 
351
        """Create a pack file with 2 records."""
 
352
        access, writer = self._get_access(packname='packname', index='foo')
 
353
        memos = []
 
354
        memos.extend(access.add_raw_records([('key1', 10)], '1234567890'))
 
355
        memos.extend(access.add_raw_records([('key2', 5)], '12345'))
 
356
        writer.end()
 
357
        return memos
 
358
 
 
359
    def make_vf_for_retrying(self):
 
360
        """Create 3 packs and a reload function.
 
361
 
 
362
        Originally, 2 pack files will have the data, but one will be missing.
 
363
        And then the third will be used in place of the first two if reload()
 
364
        is called.
 
365
 
 
366
        :return: (versioned_file, reload_counter)
 
367
            versioned_file  a KnitVersionedFiles using the packs for access
 
368
        """
 
369
        builder = self.make_branch_builder('.', format="1.9")
 
370
        builder.start_series()
 
371
        builder.build_snapshot('rev-1', None, [
 
372
            ('add', ('', 'root-id', 'directory', None)),
 
373
            ('add', ('file', 'file-id', 'file', 'content\nrev 1\n')),
 
374
            ])
 
375
        builder.build_snapshot('rev-2', ['rev-1'], [
 
376
            ('modify', ('file-id', 'content\nrev 2\n')),
 
377
            ])
 
378
        builder.build_snapshot('rev-3', ['rev-2'], [
 
379
            ('modify', ('file-id', 'content\nrev 3\n')),
 
380
            ])
 
381
        builder.finish_series()
 
382
        b = builder.get_branch()
 
383
        b.lock_write()
 
384
        self.addCleanup(b.unlock)
 
385
        # Pack these three revisions into another pack file, but don't remove
 
386
        # the originals
 
387
        repo = b.repository
 
388
        collection = repo._pack_collection
 
389
        collection.ensure_loaded()
 
390
        orig_packs = collection.packs
 
391
        packer = pack_repo.Packer(collection, orig_packs, '.testpack')
 
392
        new_pack = packer.pack()
 
393
        # forget about the new pack
 
394
        collection.reset()
 
395
        repo.refresh_data()
 
396
        vf = repo.revisions
 
397
        # Set up a reload() function that switches to using the new pack file
 
398
        new_index = new_pack.revision_index
 
399
        access_tuple = new_pack.access_tuple()
 
400
        reload_counter = [0, 0, 0]
 
401
        def reload():
 
402
            reload_counter[0] += 1
 
403
            if reload_counter[1] > 0:
 
404
                # We already reloaded, nothing more to do
 
405
                reload_counter[2] += 1
 
406
                return False
 
407
            reload_counter[1] += 1
 
408
            vf._index._graph_index._indices[:] = [new_index]
 
409
            vf._access._indices.clear()
 
410
            vf._access._indices[new_index] = access_tuple
 
411
            return True
 
412
        # Delete one of the pack files so the data will need to be reloaded. We
 
413
        # will delete the file with 'rev-2' in it
 
414
        trans, name = orig_packs[1].access_tuple()
 
415
        trans.delete(name)
 
416
        # We don't have the index trigger reloading because we want to test
 
417
        # that we reload when the .pack disappears
 
418
        vf._access._reload_func = reload
 
419
        return vf, reload_counter
 
420
 
 
421
    def make_reload_func(self, return_val=True):
 
422
        reload_called = [0]
 
423
        def reload():
 
424
            reload_called[0] += 1
 
425
            return return_val
 
426
        return reload_called, reload
 
427
 
 
428
    def make_retry_exception(self):
 
429
        # We raise a real exception so that sys.exc_info() is properly
 
430
        # populated
 
431
        try:
 
432
            raise _TestException('foobar')
 
433
        except _TestException, e:
 
434
            retry_exc = errors.RetryWithNewPacks(None, reload_occurred=False,
 
435
                                                 exc_info=sys.exc_info())
 
436
        return retry_exc
 
437
 
231
438
    def test_read_from_several_packs(self):
232
439
        access, writer = self._get_access()
233
440
        memos = []
234
 
        memos.extend(access.add_raw_records([10], '1234567890'))
 
441
        memos.extend(access.add_raw_records([('key', 10)], '1234567890'))
235
442
        writer.end()
236
443
        access, writer = self._get_access('pack2', 'FOOBAR')
237
 
        memos.extend(access.add_raw_records([5], '12345'))
 
444
        memos.extend(access.add_raw_records([('key', 5)], '12345'))
238
445
        writer.end()
239
446
        access, writer = self._get_access('pack3', 'BAZ')
240
 
        memos.extend(access.add_raw_records([5], 'alpha'))
 
447
        memos.extend(access.add_raw_records([('key', 5)], 'alpha'))
241
448
        writer.end()
242
449
        transport = self.get_transport()
243
 
        access = _PackAccess({"FOO":(transport, 'packfile'),
 
450
        access = _DirectPackAccess({"FOO":(transport, 'packfile'),
244
451
            "FOOBAR":(transport, 'pack2'),
245
452
            "BAZ":(transport, 'pack3')})
246
453
        self.assertEqual(['1234567890', '12345', 'alpha'],
256
463
 
257
464
    def test_set_writer(self):
258
465
        """The writer should be settable post construction."""
259
 
        access = _PackAccess({})
 
466
        access = _DirectPackAccess({})
260
467
        transport = self.get_transport()
261
468
        packname = 'packfile'
262
469
        index = 'foo'
265
472
        writer = pack.ContainerWriter(write_data)
266
473
        writer.begin()
267
474
        access.set_writer(writer, index, (transport, packname))
268
 
        memos = access.add_raw_records([10], '1234567890')
 
475
        memos = access.add_raw_records([('key', 10)], '1234567890')
269
476
        writer.end()
270
477
        self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
271
478
 
 
479
    def test_missing_index_raises_retry(self):
 
480
        memos = self.make_pack_file()
 
481
        transport = self.get_transport()
 
482
        reload_called, reload_func = self.make_reload_func()
 
483
        # Note that the index key has changed from 'foo' to 'bar'
 
484
        access = _DirectPackAccess({'bar':(transport, 'packname')},
 
485
                                   reload_func=reload_func)
 
486
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
487
                                  access.get_raw_records, memos)
 
488
        # Because a key was passed in which does not match our index list, we
 
489
        # assume that the listing was already reloaded
 
490
        self.assertTrue(e.reload_occurred)
 
491
        self.assertIsInstance(e.exc_info, tuple)
 
492
        self.assertIs(e.exc_info[0], KeyError)
 
493
        self.assertIsInstance(e.exc_info[1], KeyError)
 
494
 
 
495
    def test_missing_index_raises_key_error_with_no_reload(self):
 
496
        memos = self.make_pack_file()
 
497
        transport = self.get_transport()
 
498
        # Note that the index key has changed from 'foo' to 'bar'
 
499
        access = _DirectPackAccess({'bar':(transport, 'packname')})
 
500
        e = self.assertListRaises(KeyError, access.get_raw_records, memos)
 
501
 
 
502
    def test_missing_file_raises_retry(self):
 
503
        memos = self.make_pack_file()
 
504
        transport = self.get_transport()
 
505
        reload_called, reload_func = self.make_reload_func()
 
506
        # Note that the 'filename' has been changed to 'different-packname'
 
507
        access = _DirectPackAccess({'foo':(transport, 'different-packname')},
 
508
                                   reload_func=reload_func)
 
509
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
510
                                  access.get_raw_records, memos)
 
511
        # The file has gone missing, so we assume we need to reload
 
512
        self.assertFalse(e.reload_occurred)
 
513
        self.assertIsInstance(e.exc_info, tuple)
 
514
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
515
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
516
        self.assertEqual('different-packname', e.exc_info[1].path)
 
517
 
 
518
    def test_missing_file_raises_no_such_file_with_no_reload(self):
 
519
        memos = self.make_pack_file()
 
520
        transport = self.get_transport()
 
521
        # Note that the 'filename' has been changed to 'different-packname'
 
522
        access = _DirectPackAccess({'foo':(transport, 'different-packname')})
 
523
        e = self.assertListRaises(errors.NoSuchFile,
 
524
                                  access.get_raw_records, memos)
 
525
 
 
526
    def test_failing_readv_raises_retry(self):
 
527
        memos = self.make_pack_file()
 
528
        transport = self.get_transport()
 
529
        failing_transport = MockReadvFailingTransport(
 
530
                                [transport.get_bytes('packname')])
 
531
        reload_called, reload_func = self.make_reload_func()
 
532
        access = _DirectPackAccess({'foo':(failing_transport, 'packname')},
 
533
                                   reload_func=reload_func)
 
534
        # Asking for a single record will not trigger the Mock failure
 
535
        self.assertEqual(['1234567890'],
 
536
            list(access.get_raw_records(memos[:1])))
 
537
        self.assertEqual(['12345'],
 
538
            list(access.get_raw_records(memos[1:2])))
 
539
        # A multiple offset readv() will fail mid-way through
 
540
        e = self.assertListRaises(errors.RetryWithNewPacks,
 
541
                                  access.get_raw_records, memos)
 
542
        # The file has gone missing, so we assume we need to reload
 
543
        self.assertFalse(e.reload_occurred)
 
544
        self.assertIsInstance(e.exc_info, tuple)
 
545
        self.assertIs(e.exc_info[0], errors.NoSuchFile)
 
546
        self.assertIsInstance(e.exc_info[1], errors.NoSuchFile)
 
547
        self.assertEqual('packname', e.exc_info[1].path)
 
548
 
 
549
    def test_failing_readv_raises_no_such_file_with_no_reload(self):
 
550
        memos = self.make_pack_file()
 
551
        transport = self.get_transport()
 
552
        failing_transport = MockReadvFailingTransport(
 
553
                                [transport.get_bytes('packname')])
 
554
        reload_called, reload_func = self.make_reload_func()
 
555
        access = _DirectPackAccess({'foo':(failing_transport, 'packname')})
 
556
        # Asking for a single record will not trigger the Mock failure
 
557
        self.assertEqual(['1234567890'],
 
558
            list(access.get_raw_records(memos[:1])))
 
559
        self.assertEqual(['12345'],
 
560
            list(access.get_raw_records(memos[1:2])))
 
561
        # A multiple offset readv() will fail mid-way through
 
562
        e = self.assertListRaises(errors.NoSuchFile,
 
563
                                  access.get_raw_records, memos)
 
564
 
 
565
    def test_reload_or_raise_no_reload(self):
 
566
        access = _DirectPackAccess({}, reload_func=None)
 
567
        retry_exc = self.make_retry_exception()
 
568
        # Without a reload_func, we will just re-raise the original exception
 
569
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
570
 
 
571
    def test_reload_or_raise_reload_changed(self):
 
572
        reload_called, reload_func = self.make_reload_func(return_val=True)
 
573
        access = _DirectPackAccess({}, reload_func=reload_func)
 
574
        retry_exc = self.make_retry_exception()
 
575
        access.reload_or_raise(retry_exc)
 
576
        self.assertEqual([1], reload_called)
 
577
        retry_exc.reload_occurred=True
 
578
        access.reload_or_raise(retry_exc)
 
579
        self.assertEqual([2], reload_called)
 
580
 
 
581
    def test_reload_or_raise_reload_no_change(self):
 
582
        reload_called, reload_func = self.make_reload_func(return_val=False)
 
583
        access = _DirectPackAccess({}, reload_func=reload_func)
 
584
        retry_exc = self.make_retry_exception()
 
585
        # If reload_occurred is False, then we consider it an error to have
 
586
        # reload_func() return False (no changes).
 
587
        self.assertRaises(_TestException, access.reload_or_raise, retry_exc)
 
588
        self.assertEqual([1], reload_called)
 
589
        retry_exc.reload_occurred=True
 
590
        # If reload_occurred is True, then we assume nothing changed because
 
591
        # it had changed earlier, but didn't change again
 
592
        access.reload_or_raise(retry_exc)
 
593
        self.assertEqual([2], reload_called)
 
594
 
 
595
    def test_annotate_retries(self):
 
596
        vf, reload_counter = self.make_vf_for_retrying()
 
597
        # It is a little bit bogus to annotate the Revision VF, but it works,
 
598
        # as we have ancestry stored there
 
599
        key = ('rev-3',)
 
600
        reload_lines = vf.annotate(key)
 
601
        self.assertEqual([1, 1, 0], reload_counter)
 
602
        plain_lines = vf.annotate(key)
 
603
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
604
        if reload_lines != plain_lines:
 
605
            self.fail('Annotation was not identical with reloading.')
 
606
        # Now delete the packs-in-use, which should trigger another reload, but
 
607
        # this time we just raise an exception because we can't recover
 
608
        for trans, name in vf._access._indices.itervalues():
 
609
            trans.delete(name)
 
610
        self.assertRaises(errors.NoSuchFile, vf.annotate, key)
 
611
        self.assertEqual([2, 1, 1], reload_counter)
 
612
 
 
613
    def test__get_record_map_retries(self):
 
614
        vf, reload_counter = self.make_vf_for_retrying()
 
615
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
616
        records = vf._get_record_map(keys)
 
617
        self.assertEqual(keys, sorted(records.keys()))
 
618
        self.assertEqual([1, 1, 0], reload_counter)
 
619
        # Now delete the packs-in-use, which should trigger another reload, but
 
620
        # this time we just raise an exception because we can't recover
 
621
        for trans, name in vf._access._indices.itervalues():
 
622
            trans.delete(name)
 
623
        self.assertRaises(errors.NoSuchFile, vf._get_record_map, keys)
 
624
        self.assertEqual([2, 1, 1], reload_counter)
 
625
 
 
626
    def test_get_record_stream_retries(self):
 
627
        vf, reload_counter = self.make_vf_for_retrying()
 
628
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
629
        record_stream = vf.get_record_stream(keys, 'topological', False)
 
630
        record = record_stream.next()
 
631
        self.assertEqual(('rev-1',), record.key)
 
632
        self.assertEqual([0, 0, 0], reload_counter)
 
633
        record = record_stream.next()
 
634
        self.assertEqual(('rev-2',), record.key)
 
635
        self.assertEqual([1, 1, 0], reload_counter)
 
636
        record = record_stream.next()
 
637
        self.assertEqual(('rev-3',), record.key)
 
638
        self.assertEqual([1, 1, 0], reload_counter)
 
639
        # Now delete all pack files, and see that we raise the right error
 
640
        for trans, name in vf._access._indices.itervalues():
 
641
            trans.delete(name)
 
642
        self.assertListRaises(errors.NoSuchFile,
 
643
            vf.get_record_stream, keys, 'topological', False)
 
644
 
 
645
    def test_iter_lines_added_or_present_in_keys_retries(self):
 
646
        vf, reload_counter = self.make_vf_for_retrying()
 
647
        keys = [('rev-1',), ('rev-2',), ('rev-3',)]
 
648
        # Unfortunately, iter_lines_added_or_present_in_keys iterates the
 
649
        # result in random order (determined by the iteration order from a
 
650
        # set()), so we don't have any solid way to trigger whether data is
 
651
        # read before or after. However we tried to delete the middle node to
 
652
        # exercise the code well.
 
653
        # What we care about is that all lines are always yielded, but not
 
654
        # duplicated
 
655
        count = 0
 
656
        reload_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
657
        self.assertEqual([1, 1, 0], reload_counter)
 
658
        # Now do it again, to make sure the result is equivalent
 
659
        plain_lines = sorted(vf.iter_lines_added_or_present_in_keys(keys))
 
660
        self.assertEqual([1, 1, 0], reload_counter) # No extra reloading
 
661
        self.assertEqual(plain_lines, reload_lines)
 
662
        self.assertEqual(21, len(plain_lines))
 
663
        # Now delete all pack files, and see that we raise the right error
 
664
        for trans, name in vf._access._indices.itervalues():
 
665
            trans.delete(name)
 
666
        self.assertListRaises(errors.NoSuchFile,
 
667
            vf.iter_lines_added_or_present_in_keys, keys)
 
668
        self.assertEqual([2, 1, 1], reload_counter)
 
669
 
 
670
    def test_get_record_stream_yields_disk_sorted_order(self):
 
671
        # if we get 'unordered' pick a semi-optimal order for reading. The
 
672
        # order should be grouped by pack file, and then by position in file
 
673
        repo = self.make_repository('test', format='pack-0.92')
 
674
        repo.lock_write()
 
675
        self.addCleanup(repo.unlock)
 
676
        repo.start_write_group()
 
677
        vf = repo.texts
 
678
        vf.add_lines(('f-id', 'rev-5'), [('f-id', 'rev-4')], ['lines\n'])
 
679
        vf.add_lines(('f-id', 'rev-1'), [], ['lines\n'])
 
680
        vf.add_lines(('f-id', 'rev-2'), [('f-id', 'rev-1')], ['lines\n'])
 
681
        repo.commit_write_group()
 
682
        # We inserted them as rev-5, rev-1, rev-2, we should get them back in
 
683
        # the same order
 
684
        stream = vf.get_record_stream([('f-id', 'rev-1'), ('f-id', 'rev-5'),
 
685
                                       ('f-id', 'rev-2')], 'unordered', False)
 
686
        keys = [r.key for r in stream]
 
687
        self.assertEqual([('f-id', 'rev-5'), ('f-id', 'rev-1'),
 
688
                          ('f-id', 'rev-2')], keys)
 
689
        repo.start_write_group()
 
690
        vf.add_lines(('f-id', 'rev-4'), [('f-id', 'rev-3')], ['lines\n'])
 
691
        vf.add_lines(('f-id', 'rev-3'), [('f-id', 'rev-2')], ['lines\n'])
 
692
        vf.add_lines(('f-id', 'rev-6'), [('f-id', 'rev-5')], ['lines\n'])
 
693
        repo.commit_write_group()
 
694
        # Request in random order, to make sure the output order isn't based on
 
695
        # the request
 
696
        request_keys = set(('f-id', 'rev-%d' % i) for i in range(1, 7))
 
697
        stream = vf.get_record_stream(request_keys, 'unordered', False)
 
698
        keys = [r.key for r in stream]
 
699
        # We want to get the keys back in disk order, but it doesn't matter
 
700
        # which pack we read from first. So this can come back in 2 orders
 
701
        alt1 = [('f-id', 'rev-%d' % i) for i in [4, 3, 6, 5, 1, 2]]
 
702
        alt2 = [('f-id', 'rev-%d' % i) for i in [5, 1, 2, 4, 3, 6]]
 
703
        if keys != alt1 and keys != alt2:
 
704
            self.fail('Returned key order did not match either expected order.'
 
705
                      ' expected %s or %s, not %s'
 
706
                      % (alt1, alt2, keys))
 
707
 
272
708
 
273
709
class LowLevelKnitDataTests(TestCase):
274
710
 
279
715
        gz_file.close()
280
716
        return sio.getvalue()
281
717
 
 
718
    def make_multiple_records(self):
 
719
        """Create the content for multiple records."""
 
720
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
721
        total_txt = []
 
722
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
 
723
                                        'foo\n'
 
724
                                        'bar\n'
 
725
                                        'end rev-id-1\n'
 
726
                                        % (sha1sum,))
 
727
        record_1 = (0, len(gz_txt), sha1sum)
 
728
        total_txt.append(gz_txt)
 
729
        sha1sum = osutils.sha('baz\n').hexdigest()
 
730
        gz_txt = self.create_gz_content('version rev-id-2 1 %s\n'
 
731
                                        'baz\n'
 
732
                                        'end rev-id-2\n'
 
733
                                        % (sha1sum,))
 
734
        record_2 = (record_1[1], len(gz_txt), sha1sum)
 
735
        total_txt.append(gz_txt)
 
736
        return total_txt, record_1, record_2
 
737
 
282
738
    def test_valid_knit_data(self):
283
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
739
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
284
740
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
285
741
                                        'foo\n'
286
742
                                        'bar\n'
287
743
                                        'end rev-id-1\n'
288
744
                                        % (sha1sum,))
289
745
        transport = MockTransport([gz_txt])
290
 
        access = _KnitAccess(transport, 'filename', None, None, False, False)
291
 
        data = _KnitData(access=access)
292
 
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
293
 
 
294
 
        contents = data.read_records(records)
295
 
        self.assertEqual({'rev-id-1':(['foo\n', 'bar\n'], sha1sum)}, contents)
296
 
 
297
 
        raw_contents = list(data.read_records_iter_raw(records))
298
 
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
746
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
747
        knit = KnitVersionedFiles(None, access)
 
748
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
749
 
 
750
        contents = list(knit._read_records_iter(records))
 
751
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'],
 
752
            '4e48e2c9a3d2ca8a708cb0cc545700544efb5021')], contents)
 
753
 
 
754
        raw_contents = list(knit._read_records_iter_raw(records))
 
755
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
 
756
 
 
757
    def test_multiple_records_valid(self):
 
758
        total_txt, record_1, record_2 = self.make_multiple_records()
 
759
        transport = MockTransport([''.join(total_txt)])
 
760
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
761
        knit = KnitVersionedFiles(None, access)
 
762
        records = [(('rev-id-1',), (('rev-id-1',), record_1[0], record_1[1])),
 
763
                   (('rev-id-2',), (('rev-id-2',), record_2[0], record_2[1]))]
 
764
 
 
765
        contents = list(knit._read_records_iter(records))
 
766
        self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'], record_1[2]),
 
767
                          (('rev-id-2',), ['baz\n'], record_2[2])],
 
768
                         contents)
 
769
 
 
770
        raw_contents = list(knit._read_records_iter_raw(records))
 
771
        self.assertEqual([(('rev-id-1',), total_txt[0], record_1[2]),
 
772
                          (('rev-id-2',), total_txt[1], record_2[2])],
 
773
                         raw_contents)
299
774
 
300
775
    def test_not_enough_lines(self):
301
 
        sha1sum = sha.new('foo\n').hexdigest()
 
776
        sha1sum = osutils.sha('foo\n').hexdigest()
302
777
        # record says 2 lines data says 1
303
778
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
304
779
                                        'foo\n'
305
780
                                        'end rev-id-1\n'
306
781
                                        % (sha1sum,))
307
782
        transport = MockTransport([gz_txt])
308
 
        access = _KnitAccess(transport, 'filename', None, None, False, False)
309
 
        data = _KnitData(access=access)
310
 
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
311
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
783
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
784
        knit = KnitVersionedFiles(None, access)
 
785
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
786
        self.assertRaises(errors.KnitCorrupt, list,
 
787
            knit._read_records_iter(records))
312
788
 
313
789
        # read_records_iter_raw won't detect that sort of mismatch/corruption
314
 
        raw_contents = list(data.read_records_iter_raw(records))
315
 
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
790
        raw_contents = list(knit._read_records_iter_raw(records))
 
791
        self.assertEqual([(('rev-id-1',),  gz_txt, sha1sum)], raw_contents)
316
792
 
317
793
    def test_too_many_lines(self):
318
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
794
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
319
795
        # record says 1 lines data says 2
320
796
        gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
321
797
                                        'foo\n'
323
799
                                        'end rev-id-1\n'
324
800
                                        % (sha1sum,))
325
801
        transport = MockTransport([gz_txt])
326
 
        access = _KnitAccess(transport, 'filename', None, None, False, False)
327
 
        data = _KnitData(access=access)
328
 
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
329
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
802
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
803
        knit = KnitVersionedFiles(None, access)
 
804
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
805
        self.assertRaises(errors.KnitCorrupt, list,
 
806
            knit._read_records_iter(records))
330
807
 
331
808
        # read_records_iter_raw won't detect that sort of mismatch/corruption
332
 
        raw_contents = list(data.read_records_iter_raw(records))
333
 
        self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
 
809
        raw_contents = list(knit._read_records_iter_raw(records))
 
810
        self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
334
811
 
335
812
    def test_mismatched_version_id(self):
336
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
813
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
337
814
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
338
815
                                        'foo\n'
339
816
                                        'bar\n'
340
817
                                        'end rev-id-1\n'
341
818
                                        % (sha1sum,))
342
819
        transport = MockTransport([gz_txt])
343
 
        access = _KnitAccess(transport, 'filename', None, None, False, False)
344
 
        data = _KnitData(access=access)
 
820
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
821
        knit = KnitVersionedFiles(None, access)
345
822
        # We are asking for rev-id-2, but the data is rev-id-1
346
 
        records = [('rev-id-2', (None, 0, len(gz_txt)))]
347
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
823
        records = [(('rev-id-2',), (('rev-id-2',), 0, len(gz_txt)))]
 
824
        self.assertRaises(errors.KnitCorrupt, list,
 
825
            knit._read_records_iter(records))
348
826
 
349
 
        # read_records_iter_raw will notice if we request the wrong version.
 
827
        # read_records_iter_raw detects mismatches in the header
350
828
        self.assertRaises(errors.KnitCorrupt, list,
351
 
                          data.read_records_iter_raw(records))
 
829
            knit._read_records_iter_raw(records))
352
830
 
353
831
    def test_uncompressed_data(self):
354
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
832
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
355
833
        txt = ('version rev-id-1 2 %s\n'
356
834
               'foo\n'
357
835
               'bar\n'
358
836
               'end rev-id-1\n'
359
837
               % (sha1sum,))
360
838
        transport = MockTransport([txt])
361
 
        access = _KnitAccess(transport, 'filename', None, None, False, False)
362
 
        data = _KnitData(access=access)
363
 
        records = [('rev-id-1', (None, 0, len(txt)))]
 
839
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
840
        knit = KnitVersionedFiles(None, access)
 
841
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(txt)))]
364
842
 
365
843
        # We don't have valid gzip data ==> corrupt
366
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
 
844
        self.assertRaises(errors.KnitCorrupt, list,
 
845
            knit._read_records_iter(records))
367
846
 
368
847
        # read_records_iter_raw will notice the bad data
369
848
        self.assertRaises(errors.KnitCorrupt, list,
370
 
                          data.read_records_iter_raw(records))
 
849
            knit._read_records_iter_raw(records))
371
850
 
372
851
    def test_corrupted_data(self):
373
 
        sha1sum = sha.new('foo\nbar\n').hexdigest()
 
852
        sha1sum = osutils.sha('foo\nbar\n').hexdigest()
374
853
        gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
375
854
                                        'foo\n'
376
855
                                        'bar\n'
379
858
        # Change 2 bytes in the middle to \xff
380
859
        gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
381
860
        transport = MockTransport([gz_txt])
382
 
        access = _KnitAccess(transport, 'filename', None, None, False, False)
383
 
        data = _KnitData(access=access)
384
 
        records = [('rev-id-1', (None, 0, len(gz_txt)))]
385
 
 
386
 
        self.assertRaises(errors.KnitCorrupt, data.read_records, records)
387
 
 
388
 
        # read_records_iter_raw will notice if we request the wrong version.
389
 
        self.assertRaises(errors.KnitCorrupt, list,
390
 
                          data.read_records_iter_raw(records))
 
861
        access = _KnitKeyAccess(transport, ConstantMapper('filename'))
 
862
        knit = KnitVersionedFiles(None, access)
 
863
        records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
 
864
        self.assertRaises(errors.KnitCorrupt, list,
 
865
            knit._read_records_iter(records))
 
866
        # read_records_iter_raw will barf on bad gz data
 
867
        self.assertRaises(errors.KnitCorrupt, list,
 
868
            knit._read_records_iter_raw(records))
391
869
 
392
870
 
393
871
class LowLevelKnitIndexTests(TestCase):
394
872
 
395
 
    def get_knit_index(self, *args, **kwargs):
 
873
    def get_knit_index(self, transport, name, mode):
 
874
        mapper = ConstantMapper(name)
396
875
        orig = knit._load_data
397
876
        def reset():
398
877
            knit._load_data = orig
399
878
        self.addCleanup(reset)
400
879
        from bzrlib._knit_load_data_py import _load_data_py
401
880
        knit._load_data = _load_data_py
402
 
        return _KnitIndex(*args, **kwargs)
403
 
 
404
 
    def test_no_such_file(self):
405
 
        transport = MockTransport()
406
 
 
407
 
        self.assertRaises(NoSuchFile, self.get_knit_index,
408
 
                          transport, "filename", "r")
409
 
        self.assertRaises(NoSuchFile, self.get_knit_index,
410
 
                          transport, "filename", "w", create=False)
 
881
        allow_writes = lambda: 'w' in mode
 
882
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
411
883
 
412
884
    def test_create_file(self):
413
885
        transport = MockTransport()
414
 
 
415
 
        index = self.get_knit_index(transport, "filename", "w",
416
 
            file_mode="wb", create=True)
417
 
        self.assertEqual(
418
 
                ("put_bytes_non_atomic",
419
 
                    ("filename", index.HEADER), {"mode": "wb"}),
420
 
                transport.calls.pop(0))
421
 
 
422
 
    def test_delay_create_file(self):
423
 
        transport = MockTransport()
424
 
 
425
 
        index = self.get_knit_index(transport, "filename", "w",
426
 
            create=True, file_mode="wb", create_parent_dir=True,
427
 
            delay_create=True, dir_mode=0777)
428
 
        self.assertEqual([], transport.calls)
429
 
 
430
 
        index.add_versions([])
431
 
        name, (filename, f), kwargs = transport.calls.pop(0)
432
 
        self.assertEqual("put_file_non_atomic", name)
433
 
        self.assertEqual(
434
 
            {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
435
 
            kwargs)
436
 
        self.assertEqual("filename", filename)
437
 
        self.assertEqual(index.HEADER, f.read())
438
 
 
439
 
        index.add_versions([])
440
 
        self.assertEqual(("append_bytes", ("filename", ""), {}),
441
 
            transport.calls.pop(0))
 
886
        index = self.get_knit_index(transport, "filename", "w")
 
887
        index.keys()
 
888
        call = transport.calls.pop(0)
 
889
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
890
        self.assertEqual('put_file_non_atomic', call[0])
 
891
        self.assertEqual('filename.kndx', call[1][0])
 
892
        # With no history, _KndxIndex writes a new index:
 
893
        self.assertEqual(_KndxIndex.HEADER,
 
894
            call[1][1].getvalue())
 
895
        self.assertEqual({'create_parent_dir': True}, call[2])
442
896
 
443
897
    def test_read_utf8_version_id(self):
444
898
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
445
899
        utf8_revision_id = unicode_revision_id.encode('utf-8')
446
900
        transport = MockTransport([
447
 
            _KnitIndex.HEADER,
 
901
            _KndxIndex.HEADER,
448
902
            '%s option 0 1 :' % (utf8_revision_id,)
449
903
            ])
450
904
        index = self.get_knit_index(transport, "filename", "r")
451
 
        # _KnitIndex is a private class, and deals in utf8 revision_ids, not
 
905
        # _KndxIndex is a private class, and deals in utf8 revision_ids, not
452
906
        # Unicode revision_ids.
453
 
        self.assertTrue(index.has_version(utf8_revision_id))
454
 
        self.assertFalse(index.has_version(unicode_revision_id))
 
907
        self.assertEqual({(utf8_revision_id,):()},
 
908
            index.get_parent_map(index.keys()))
 
909
        self.assertFalse((unicode_revision_id,) in index.keys())
455
910
 
456
911
    def test_read_utf8_parents(self):
457
912
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
458
913
        utf8_revision_id = unicode_revision_id.encode('utf-8')
459
914
        transport = MockTransport([
460
 
            _KnitIndex.HEADER,
 
915
            _KndxIndex.HEADER,
461
916
            "version option 0 1 .%s :" % (utf8_revision_id,)
462
917
            ])
463
918
        index = self.get_knit_index(transport, "filename", "r")
464
 
        self.assertEqual([utf8_revision_id],
465
 
            index.get_parents_with_ghosts("version"))
 
919
        self.assertEqual({("version",):((utf8_revision_id,),)},
 
920
            index.get_parent_map(index.keys()))
466
921
 
467
922
    def test_read_ignore_corrupted_lines(self):
468
923
        transport = MockTransport([
469
 
            _KnitIndex.HEADER,
 
924
            _KndxIndex.HEADER,
470
925
            "corrupted",
471
926
            "corrupted options 0 1 .b .c ",
472
927
            "version options 0 1 :"
473
928
            ])
474
929
        index = self.get_knit_index(transport, "filename", "r")
475
 
        self.assertEqual(1, index.num_versions())
476
 
        self.assertTrue(index.has_version("version"))
 
930
        self.assertEqual(1, len(index.keys()))
 
931
        self.assertEqual(set([("version",)]), index.keys())
477
932
 
478
933
    def test_read_corrupted_header(self):
479
934
        transport = MockTransport(['not a bzr knit index header\n'])
480
 
        self.assertRaises(KnitHeaderError,
481
 
            self.get_knit_index, transport, "filename", "r")
 
935
        index = self.get_knit_index(transport, "filename", "r")
 
936
        self.assertRaises(KnitHeaderError, index.keys)
482
937
 
483
938
    def test_read_duplicate_entries(self):
484
939
        transport = MockTransport([
485
 
            _KnitIndex.HEADER,
 
940
            _KndxIndex.HEADER,
486
941
            "parent options 0 1 :",
487
942
            "version options1 0 1 0 :",
488
943
            "version options2 1 2 .other :",
489
944
            "version options3 3 4 0 .other :"
490
945
            ])
491
946
        index = self.get_knit_index(transport, "filename", "r")
492
 
        self.assertEqual(2, index.num_versions())
 
947
        self.assertEqual(2, len(index.keys()))
493
948
        # check that the index used is the first one written. (Specific
494
949
        # to KnitIndex style indices.
495
 
        self.assertEqual("1", index._version_list_to_index(["version"]))
496
 
        self.assertEqual((None, 3, 4), index.get_position("version"))
497
 
        self.assertEqual(["options3"], index.get_options("version"))
498
 
        self.assertEqual(["parent", "other"],
499
 
            index.get_parents_with_ghosts("version"))
 
950
        self.assertEqual("1", index._dictionary_compress([("version",)]))
 
951
        self.assertEqual((("version",), 3, 4), index.get_position(("version",)))
 
952
        self.assertEqual(["options3"], index.get_options(("version",)))
 
953
        self.assertEqual({("version",):(("parent",), ("other",))},
 
954
            index.get_parent_map([("version",)]))
500
955
 
501
956
    def test_read_compressed_parents(self):
502
957
        transport = MockTransport([
503
 
            _KnitIndex.HEADER,
 
958
            _KndxIndex.HEADER,
504
959
            "a option 0 1 :",
505
960
            "b option 0 1 0 :",
506
961
            "c option 0 1 1 0 :",
507
962
            ])
508
963
        index = self.get_knit_index(transport, "filename", "r")
509
 
        self.assertEqual(["a"], index.get_parents("b"))
510
 
        self.assertEqual(["b", "a"], index.get_parents("c"))
 
964
        self.assertEqual({("b",):(("a",),), ("c",):(("b",), ("a",))},
 
965
            index.get_parent_map([("b",), ("c",)]))
511
966
 
512
967
    def test_write_utf8_version_id(self):
513
968
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
514
969
        utf8_revision_id = unicode_revision_id.encode('utf-8')
515
970
        transport = MockTransport([
516
 
            _KnitIndex.HEADER
 
971
            _KndxIndex.HEADER
517
972
            ])
518
973
        index = self.get_knit_index(transport, "filename", "r")
519
 
        index.add_version(utf8_revision_id, ["option"], (None, 0, 1), [])
520
 
        self.assertEqual(("append_bytes", ("filename",
521
 
            "\n%s option 0 1  :" % (utf8_revision_id,)),
522
 
            {}),
523
 
            transport.calls.pop(0))
 
974
        index.add_records([
 
975
            ((utf8_revision_id,), ["option"], ((utf8_revision_id,), 0, 1), [])])
 
976
        call = transport.calls.pop(0)
 
977
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
978
        self.assertEqual('put_file_non_atomic', call[0])
 
979
        self.assertEqual('filename.kndx', call[1][0])
 
980
        # With no history, _KndxIndex writes a new index:
 
981
        self.assertEqual(_KndxIndex.HEADER +
 
982
            "\n%s option 0 1  :" % (utf8_revision_id,),
 
983
            call[1][1].getvalue())
 
984
        self.assertEqual({'create_parent_dir': True}, call[2])
524
985
 
525
986
    def test_write_utf8_parents(self):
526
987
        unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
527
988
        utf8_revision_id = unicode_revision_id.encode('utf-8')
528
989
        transport = MockTransport([
529
 
            _KnitIndex.HEADER
530
 
            ])
531
 
        index = self.get_knit_index(transport, "filename", "r")
532
 
        index.add_version("version", ["option"], (None, 0, 1), [utf8_revision_id])
533
 
        self.assertEqual(("append_bytes", ("filename",
534
 
            "\nversion option 0 1 .%s :" % (utf8_revision_id,)),
535
 
            {}),
536
 
            transport.calls.pop(0))
537
 
 
538
 
    def test_get_graph(self):
539
 
        transport = MockTransport()
540
 
        index = self.get_knit_index(transport, "filename", "w", create=True)
541
 
        self.assertEqual([], index.get_graph())
542
 
 
543
 
        index.add_version("a", ["option"], (None, 0, 1), ["b"])
544
 
        self.assertEqual([("a", ["b"])], index.get_graph())
545
 
 
546
 
        index.add_version("c", ["option"], (None, 0, 1), ["d"])
547
 
        self.assertEqual([("a", ["b"]), ("c", ["d"])],
548
 
            sorted(index.get_graph()))
549
 
 
550
 
    def test_get_ancestry(self):
551
 
        transport = MockTransport([
552
 
            _KnitIndex.HEADER,
553
 
            "a option 0 1 :",
554
 
            "b option 0 1 0 .e :",
555
 
            "c option 0 1 1 0 :",
556
 
            "d option 0 1 2 .f :"
557
 
            ])
558
 
        index = self.get_knit_index(transport, "filename", "r")
559
 
 
560
 
        self.assertEqual([], index.get_ancestry([]))
561
 
        self.assertEqual(["a"], index.get_ancestry(["a"]))
562
 
        self.assertEqual(["a", "b"], index.get_ancestry(["b"]))
563
 
        self.assertEqual(["a", "b", "c"], index.get_ancestry(["c"]))
564
 
        self.assertEqual(["a", "b", "c", "d"], index.get_ancestry(["d"]))
565
 
        self.assertEqual(["a", "b"], index.get_ancestry(["a", "b"]))
566
 
        self.assertEqual(["a", "b", "c"], index.get_ancestry(["a", "c"]))
567
 
 
568
 
        self.assertRaises(RevisionNotPresent, index.get_ancestry, ["e"])
569
 
 
570
 
    def test_get_ancestry_with_ghosts(self):
571
 
        transport = MockTransport([
572
 
            _KnitIndex.HEADER,
573
 
            "a option 0 1 :",
574
 
            "b option 0 1 0 .e :",
575
 
            "c option 0 1 0 .f .g :",
576
 
            "d option 0 1 2 .h .j .k :"
577
 
            ])
578
 
        index = self.get_knit_index(transport, "filename", "r")
579
 
 
580
 
        self.assertEqual([], index.get_ancestry_with_ghosts([]))
581
 
        self.assertEqual(["a"], index.get_ancestry_with_ghosts(["a"]))
582
 
        self.assertEqual(["a", "e", "b"],
583
 
            index.get_ancestry_with_ghosts(["b"]))
584
 
        self.assertEqual(["a", "g", "f", "c"],
585
 
            index.get_ancestry_with_ghosts(["c"]))
586
 
        self.assertEqual(["a", "g", "f", "c", "k", "j", "h", "d"],
587
 
            index.get_ancestry_with_ghosts(["d"]))
588
 
        self.assertEqual(["a", "e", "b"],
589
 
            index.get_ancestry_with_ghosts(["a", "b"]))
590
 
        self.assertEqual(["a", "g", "f", "c"],
591
 
            index.get_ancestry_with_ghosts(["a", "c"]))
 
990
            _KndxIndex.HEADER
 
991
            ])
 
992
        index = self.get_knit_index(transport, "filename", "r")
 
993
        index.add_records([
 
994
            (("version",), ["option"], (("version",), 0, 1), [(utf8_revision_id,)])])
 
995
        call = transport.calls.pop(0)
 
996
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
997
        self.assertEqual('put_file_non_atomic', call[0])
 
998
        self.assertEqual('filename.kndx', call[1][0])
 
999
        # With no history, _KndxIndex writes a new index:
 
1000
        self.assertEqual(_KndxIndex.HEADER +
 
1001
            "\nversion option 0 1 .%s :" % (utf8_revision_id,),
 
1002
            call[1][1].getvalue())
 
1003
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1004
 
 
1005
    def test_keys(self):
 
1006
        transport = MockTransport([
 
1007
            _KndxIndex.HEADER
 
1008
            ])
 
1009
        index = self.get_knit_index(transport, "filename", "r")
 
1010
 
 
1011
        self.assertEqual(set(), index.keys())
 
1012
 
 
1013
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
 
1014
        self.assertEqual(set([("a",)]), index.keys())
 
1015
 
 
1016
        index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
 
1017
        self.assertEqual(set([("a",)]), index.keys())
 
1018
 
 
1019
        index.add_records([(("b",), ["option"], (("b",), 0, 1), [])])
 
1020
        self.assertEqual(set([("a",), ("b",)]), index.keys())
 
1021
 
 
1022
    def add_a_b(self, index, random_id=None):
 
1023
        kwargs = {}
 
1024
        if random_id is not None:
 
1025
            kwargs["random_id"] = random_id
 
1026
        index.add_records([
 
1027
            (("a",), ["option"], (("a",), 0, 1), [("b",)]),
 
1028
            (("a",), ["opt"], (("a",), 1, 2), [("c",)]),
 
1029
            (("b",), ["option"], (("b",), 2, 3), [("a",)])
 
1030
            ], **kwargs)
 
1031
 
 
1032
    def assertIndexIsAB(self, index):
 
1033
        self.assertEqual({
 
1034
            ('a',): (('c',),),
 
1035
            ('b',): (('a',),),
 
1036
            },
 
1037
            index.get_parent_map(index.keys()))
 
1038
        self.assertEqual((("a",), 1, 2), index.get_position(("a",)))
 
1039
        self.assertEqual((("b",), 2, 3), index.get_position(("b",)))
 
1040
        self.assertEqual(["opt"], index.get_options(("a",)))
 
1041
 
 
1042
    def test_add_versions(self):
 
1043
        transport = MockTransport([
 
1044
            _KndxIndex.HEADER
 
1045
            ])
 
1046
        index = self.get_knit_index(transport, "filename", "r")
 
1047
 
 
1048
        self.add_a_b(index)
 
1049
        call = transport.calls.pop(0)
 
1050
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
1051
        self.assertEqual('put_file_non_atomic', call[0])
 
1052
        self.assertEqual('filename.kndx', call[1][0])
 
1053
        # With no history, _KndxIndex writes a new index:
592
1054
        self.assertEqual(
593
 
            ["a", "g", "f", "c", "e", "b", "k", "j", "h", "d"],
594
 
            index.get_ancestry_with_ghosts(["b", "d"]))
595
 
 
596
 
        self.assertRaises(RevisionNotPresent,
597
 
            index.get_ancestry_with_ghosts, ["e"])
598
 
 
599
 
    def test_iter_parents(self):
600
 
        transport = MockTransport()
601
 
        index = self.get_knit_index(transport, "filename", "w", create=True)
602
 
        # no parents
603
 
        index.add_version('r0', ['option'], (None, 0, 1), [])
604
 
        # 1 parent
605
 
        index.add_version('r1', ['option'], (None, 0, 1), ['r0'])
606
 
        # 2 parents
607
 
        index.add_version('r2', ['option'], (None, 0, 1), ['r1', 'r0'])
608
 
        # XXX TODO a ghost
609
 
        # cases: each sample data individually:
610
 
        self.assertEqual(set([('r0', ())]),
611
 
            set(index.iter_parents(['r0'])))
612
 
        self.assertEqual(set([('r1', ('r0', ))]),
613
 
            set(index.iter_parents(['r1'])))
614
 
        self.assertEqual(set([('r2', ('r1', 'r0'))]),
615
 
            set(index.iter_parents(['r2'])))
616
 
        # no nodes returned for a missing node
617
 
        self.assertEqual(set(),
618
 
            set(index.iter_parents(['missing'])))
619
 
        # 1 node returned with missing nodes skipped
620
 
        self.assertEqual(set([('r1', ('r0', ))]),
621
 
            set(index.iter_parents(['ghost1', 'r1', 'ghost'])))
622
 
        # 2 nodes returned
623
 
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
624
 
            set(index.iter_parents(['r0', 'r1'])))
625
 
        # 2 nodes returned, missing skipped
626
 
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
627
 
            set(index.iter_parents(['a', 'r0', 'b', 'r1', 'c'])))
628
 
 
629
 
    def test_num_versions(self):
630
 
        transport = MockTransport([
631
 
            _KnitIndex.HEADER
632
 
            ])
633
 
        index = self.get_knit_index(transport, "filename", "r")
634
 
 
635
 
        self.assertEqual(0, index.num_versions())
636
 
        self.assertEqual(0, len(index))
637
 
 
638
 
        index.add_version("a", ["option"], (None, 0, 1), [])
639
 
        self.assertEqual(1, index.num_versions())
640
 
        self.assertEqual(1, len(index))
641
 
 
642
 
        index.add_version("a", ["option2"], (None, 1, 2), [])
643
 
        self.assertEqual(1, index.num_versions())
644
 
        self.assertEqual(1, len(index))
645
 
 
646
 
        index.add_version("b", ["option"], (None, 0, 1), [])
647
 
        self.assertEqual(2, index.num_versions())
648
 
        self.assertEqual(2, len(index))
649
 
 
650
 
    def test_get_versions(self):
651
 
        transport = MockTransport([
652
 
            _KnitIndex.HEADER
653
 
            ])
654
 
        index = self.get_knit_index(transport, "filename", "r")
655
 
 
656
 
        self.assertEqual([], index.get_versions())
657
 
 
658
 
        index.add_version("a", ["option"], (None, 0, 1), [])
659
 
        self.assertEqual(["a"], index.get_versions())
660
 
 
661
 
        index.add_version("a", ["option"], (None, 0, 1), [])
662
 
        self.assertEqual(["a"], index.get_versions())
663
 
 
664
 
        index.add_version("b", ["option"], (None, 0, 1), [])
665
 
        self.assertEqual(["a", "b"], index.get_versions())
666
 
 
667
 
    def test_add_version(self):
668
 
        transport = MockTransport([
669
 
            _KnitIndex.HEADER
670
 
            ])
671
 
        index = self.get_knit_index(transport, "filename", "r")
672
 
 
673
 
        index.add_version("a", ["option"], (None, 0, 1), ["b"])
674
 
        self.assertEqual(("append_bytes",
675
 
            ("filename", "\na option 0 1 .b :"),
676
 
            {}), transport.calls.pop(0))
677
 
        self.assertTrue(index.has_version("a"))
678
 
        self.assertEqual(1, index.num_versions())
679
 
        self.assertEqual((None, 0, 1), index.get_position("a"))
680
 
        self.assertEqual(["option"], index.get_options("a"))
681
 
        self.assertEqual(["b"], index.get_parents_with_ghosts("a"))
682
 
 
683
 
        index.add_version("a", ["opt"], (None, 1, 2), ["c"])
684
 
        self.assertEqual(("append_bytes",
685
 
            ("filename", "\na opt 1 2 .c :"),
686
 
            {}), transport.calls.pop(0))
687
 
        self.assertTrue(index.has_version("a"))
688
 
        self.assertEqual(1, index.num_versions())
689
 
        self.assertEqual((None, 1, 2), index.get_position("a"))
690
 
        self.assertEqual(["opt"], index.get_options("a"))
691
 
        self.assertEqual(["c"], index.get_parents_with_ghosts("a"))
692
 
 
693
 
        index.add_version("b", ["option"], (None, 2, 3), ["a"])
694
 
        self.assertEqual(("append_bytes",
695
 
            ("filename", "\nb option 2 3 0 :"),
696
 
            {}), transport.calls.pop(0))
697
 
        self.assertTrue(index.has_version("b"))
698
 
        self.assertEqual(2, index.num_versions())
699
 
        self.assertEqual((None, 2, 3), index.get_position("b"))
700
 
        self.assertEqual(["option"], index.get_options("b"))
701
 
        self.assertEqual(["a"], index.get_parents_with_ghosts("b"))
702
 
 
703
 
    def test_add_versions(self):
704
 
        transport = MockTransport([
705
 
            _KnitIndex.HEADER
706
 
            ])
707
 
        index = self.get_knit_index(transport, "filename", "r")
708
 
 
709
 
        index.add_versions([
710
 
            ("a", ["option"], (None, 0, 1), ["b"]),
711
 
            ("a", ["opt"], (None, 1, 2), ["c"]),
712
 
            ("b", ["option"], (None, 2, 3), ["a"])
713
 
            ])
714
 
        self.assertEqual(("append_bytes", ("filename",
 
1055
            _KndxIndex.HEADER +
715
1056
            "\na option 0 1 .b :"
716
1057
            "\na opt 1 2 .c :"
717
 
            "\nb option 2 3 0 :"
718
 
            ), {}), transport.calls.pop(0))
719
 
        self.assertTrue(index.has_version("a"))
720
 
        self.assertTrue(index.has_version("b"))
721
 
        self.assertEqual(2, index.num_versions())
722
 
        self.assertEqual((None, 1, 2), index.get_position("a"))
723
 
        self.assertEqual((None, 2, 3), index.get_position("b"))
724
 
        self.assertEqual(["opt"], index.get_options("a"))
725
 
        self.assertEqual(["option"], index.get_options("b"))
726
 
        self.assertEqual(["c"], index.get_parents_with_ghosts("a"))
727
 
        self.assertEqual(["a"], index.get_parents_with_ghosts("b"))
 
1058
            "\nb option 2 3 0 :",
 
1059
            call[1][1].getvalue())
 
1060
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1061
        self.assertIndexIsAB(index)
 
1062
 
 
1063
    def test_add_versions_random_id_is_accepted(self):
 
1064
        transport = MockTransport([
 
1065
            _KndxIndex.HEADER
 
1066
            ])
 
1067
        index = self.get_knit_index(transport, "filename", "r")
 
1068
        self.add_a_b(index, random_id=True)
728
1069
 
729
1070
    def test_delay_create_and_add_versions(self):
730
1071
        transport = MockTransport()
731
1072
 
732
 
        index = self.get_knit_index(transport, "filename", "w",
733
 
            create=True, file_mode="wb", create_parent_dir=True,
734
 
            delay_create=True, dir_mode=0777)
 
1073
        index = self.get_knit_index(transport, "filename", "w")
 
1074
        # dir_mode=0777)
735
1075
        self.assertEqual([], transport.calls)
736
 
 
737
 
        index.add_versions([
738
 
            ("a", ["option"], (None, 0, 1), ["b"]),
739
 
            ("a", ["opt"], (None, 1, 2), ["c"]),
740
 
            ("b", ["option"], (None, 2, 3), ["a"])
741
 
            ])
742
 
        name, (filename, f), kwargs = transport.calls.pop(0)
743
 
        self.assertEqual("put_file_non_atomic", name)
744
 
        self.assertEqual(
745
 
            {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
746
 
            kwargs)
747
 
        self.assertEqual("filename", filename)
748
 
        self.assertEqual(
749
 
            index.HEADER +
 
1076
        self.add_a_b(index)
 
1077
        #self.assertEqual(
 
1078
        #[    {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
 
1079
        #    kwargs)
 
1080
        # Two calls: one during which we load the existing index (and when its
 
1081
        # missing create it), then a second where we write the contents out.
 
1082
        self.assertEqual(2, len(transport.calls))
 
1083
        call = transport.calls.pop(0)
 
1084
        self.assertEqual('put_file_non_atomic', call[0])
 
1085
        self.assertEqual('filename.kndx', call[1][0])
 
1086
        # With no history, _KndxIndex writes a new index:
 
1087
        self.assertEqual(_KndxIndex.HEADER, call[1][1].getvalue())
 
1088
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1089
        call = transport.calls.pop(0)
 
1090
        # call[1][1] is a StringIO - we can't test it by simple equality.
 
1091
        self.assertEqual('put_file_non_atomic', call[0])
 
1092
        self.assertEqual('filename.kndx', call[1][0])
 
1093
        # With no history, _KndxIndex writes a new index:
 
1094
        self.assertEqual(
 
1095
            _KndxIndex.HEADER +
750
1096
            "\na option 0 1 .b :"
751
1097
            "\na opt 1 2 .c :"
752
1098
            "\nb option 2 3 0 :",
753
 
            f.read())
754
 
 
755
 
    def test_has_version(self):
756
 
        transport = MockTransport([
757
 
            _KnitIndex.HEADER,
758
 
            "a option 0 1 :"
759
 
            ])
760
 
        index = self.get_knit_index(transport, "filename", "r")
761
 
 
762
 
        self.assertTrue(index.has_version("a"))
763
 
        self.assertFalse(index.has_version("b"))
 
1099
            call[1][1].getvalue())
 
1100
        self.assertEqual({'create_parent_dir': True}, call[2])
 
1101
 
 
1102
    def assertTotalBuildSize(self, size, keys, positions):
 
1103
        self.assertEqual(size,
 
1104
                         knit._get_total_build_size(None, keys, positions))
 
1105
 
 
1106
    def test__get_total_build_size(self):
 
1107
        positions = {
 
1108
            ('a',): (('fulltext', False), (('a',), 0, 100), None),
 
1109
            ('b',): (('line-delta', False), (('b',), 100, 21), ('a',)),
 
1110
            ('c',): (('line-delta', False), (('c',), 121, 35), ('b',)),
 
1111
            ('d',): (('line-delta', False), (('d',), 156, 12), ('b',)),
 
1112
            }
 
1113
        self.assertTotalBuildSize(100, [('a',)], positions)
 
1114
        self.assertTotalBuildSize(121, [('b',)], positions)
 
1115
        # c needs both a & b
 
1116
        self.assertTotalBuildSize(156, [('c',)], positions)
 
1117
        # we shouldn't count 'b' twice
 
1118
        self.assertTotalBuildSize(156, [('b',), ('c',)], positions)
 
1119
        self.assertTotalBuildSize(133, [('d',)], positions)
 
1120
        self.assertTotalBuildSize(168, [('c',), ('d',)], positions)
764
1121
 
765
1122
    def test_get_position(self):
766
1123
        transport = MockTransport([
767
 
            _KnitIndex.HEADER,
 
1124
            _KndxIndex.HEADER,
768
1125
            "a option 0 1 :",
769
1126
            "b option 1 2 :"
770
1127
            ])
771
1128
        index = self.get_knit_index(transport, "filename", "r")
772
1129
 
773
 
        self.assertEqual((None, 0, 1), index.get_position("a"))
774
 
        self.assertEqual((None, 1, 2), index.get_position("b"))
 
1130
        self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
 
1131
        self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
775
1132
 
776
1133
    def test_get_method(self):
777
1134
        transport = MockTransport([
778
 
            _KnitIndex.HEADER,
 
1135
            _KndxIndex.HEADER,
779
1136
            "a fulltext,unknown 0 1 :",
780
1137
            "b unknown,line-delta 1 2 :",
781
1138
            "c bad 3 4 :"
788
1145
 
789
1146
    def test_get_options(self):
790
1147
        transport = MockTransport([
791
 
            _KnitIndex.HEADER,
 
1148
            _KndxIndex.HEADER,
792
1149
            "a opt1 0 1 :",
793
1150
            "b opt2,opt3 1 2 :"
794
1151
            ])
797
1154
        self.assertEqual(["opt1"], index.get_options("a"))
798
1155
        self.assertEqual(["opt2", "opt3"], index.get_options("b"))
799
1156
 
800
 
    def test_get_parents(self):
801
 
        transport = MockTransport([
802
 
            _KnitIndex.HEADER,
803
 
            "a option 0 1 :",
804
 
            "b option 1 2 0 .c :",
805
 
            "c option 1 2 1 0 .e :"
806
 
            ])
807
 
        index = self.get_knit_index(transport, "filename", "r")
808
 
 
809
 
        self.assertEqual([], index.get_parents("a"))
810
 
        self.assertEqual(["a", "c"], index.get_parents("b"))
811
 
        self.assertEqual(["b", "a"], index.get_parents("c"))
812
 
 
813
 
    def test_get_parents_with_ghosts(self):
814
 
        transport = MockTransport([
815
 
            _KnitIndex.HEADER,
816
 
            "a option 0 1 :",
817
 
            "b option 1 2 0 .c :",
818
 
            "c option 1 2 1 0 .e :"
819
 
            ])
820
 
        index = self.get_knit_index(transport, "filename", "r")
821
 
 
822
 
        self.assertEqual([], index.get_parents_with_ghosts("a"))
823
 
        self.assertEqual(["a", "c"], index.get_parents_with_ghosts("b"))
824
 
        self.assertEqual(["b", "a", "e"],
825
 
            index.get_parents_with_ghosts("c"))
826
 
 
827
 
    def test_check_versions_present(self):
828
 
        transport = MockTransport([
829
 
            _KnitIndex.HEADER,
830
 
            "a option 0 1 :",
831
 
            "b option 0 1 :"
832
 
            ])
833
 
        index = self.get_knit_index(transport, "filename", "r")
834
 
 
835
 
        check = index.check_versions_present
836
 
 
837
 
        check([])
838
 
        check(["a"])
839
 
        check(["b"])
840
 
        check(["a", "b"])
841
 
        self.assertRaises(RevisionNotPresent, check, ["c"])
842
 
        self.assertRaises(RevisionNotPresent, check, ["a", "b", "c"])
 
1157
    def test_get_parent_map(self):
 
1158
        transport = MockTransport([
 
1159
            _KndxIndex.HEADER,
 
1160
            "a option 0 1 :",
 
1161
            "b option 1 2 0 .c :",
 
1162
            "c option 1 2 1 0 .e :"
 
1163
            ])
 
1164
        index = self.get_knit_index(transport, "filename", "r")
 
1165
 
 
1166
        self.assertEqual({
 
1167
            ("a",):(),
 
1168
            ("b",):(("a",), ("c",)),
 
1169
            ("c",):(("b",), ("a",), ("e",)),
 
1170
            }, index.get_parent_map(index.keys()))
843
1171
 
844
1172
    def test_impossible_parent(self):
845
1173
        """Test we get KnitCorrupt if the parent couldn't possibly exist."""
846
1174
        transport = MockTransport([
847
 
            _KnitIndex.HEADER,
 
1175
            _KndxIndex.HEADER,
848
1176
            "a option 0 1 :",
849
1177
            "b option 0 1 4 :"  # We don't have a 4th record
850
1178
            ])
 
1179
        index = self.get_knit_index(transport, 'filename', 'r')
851
1180
        try:
852
 
            self.assertRaises(errors.KnitCorrupt,
853
 
                              self.get_knit_index, transport, 'filename', 'r')
 
1181
            self.assertRaises(errors.KnitCorrupt, index.keys)
854
1182
        except TypeError, e:
855
1183
            if (str(e) == ('exceptions must be strings, classes, or instances,'
856
1184
                           ' not exceptions.IndexError')
863
1191
 
864
1192
    def test_corrupted_parent(self):
865
1193
        transport = MockTransport([
866
 
            _KnitIndex.HEADER,
 
1194
            _KndxIndex.HEADER,
867
1195
            "a option 0 1 :",
868
1196
            "b option 0 1 :",
869
1197
            "c option 0 1 1v :", # Can't have a parent of '1v'
870
1198
            ])
 
1199
        index = self.get_knit_index(transport, 'filename', 'r')
871
1200
        try:
872
 
            self.assertRaises(errors.KnitCorrupt,
873
 
                              self.get_knit_index, transport, 'filename', 'r')
 
1201
            self.assertRaises(errors.KnitCorrupt, index.keys)
874
1202
        except TypeError, e:
875
1203
            if (str(e) == ('exceptions must be strings, classes, or instances,'
876
1204
                           ' not exceptions.ValueError')
883
1211
 
884
1212
    def test_corrupted_parent_in_list(self):
885
1213
        transport = MockTransport([
886
 
            _KnitIndex.HEADER,
 
1214
            _KndxIndex.HEADER,
887
1215
            "a option 0 1 :",
888
1216
            "b option 0 1 :",
889
1217
            "c option 0 1 1 v :", # Can't have a parent of 'v'
890
1218
            ])
 
1219
        index = self.get_knit_index(transport, 'filename', 'r')
891
1220
        try:
892
 
            self.assertRaises(errors.KnitCorrupt,
893
 
                              self.get_knit_index, transport, 'filename', 'r')
 
1221
            self.assertRaises(errors.KnitCorrupt, index.keys)
894
1222
        except TypeError, e:
895
1223
            if (str(e) == ('exceptions must be strings, classes, or instances,'
896
1224
                           ' not exceptions.ValueError')
903
1231
 
904
1232
    def test_invalid_position(self):
905
1233
        transport = MockTransport([
906
 
            _KnitIndex.HEADER,
 
1234
            _KndxIndex.HEADER,
907
1235
            "a option 1v 1 :",
908
1236
            ])
 
1237
        index = self.get_knit_index(transport, 'filename', 'r')
909
1238
        try:
910
 
            self.assertRaises(errors.KnitCorrupt,
911
 
                              self.get_knit_index, transport, 'filename', 'r')
 
1239
            self.assertRaises(errors.KnitCorrupt, index.keys)
912
1240
        except TypeError, e:
913
1241
            if (str(e) == ('exceptions must be strings, classes, or instances,'
914
1242
                           ' not exceptions.ValueError')
921
1249
 
922
1250
    def test_invalid_size(self):
923
1251
        transport = MockTransport([
924
 
            _KnitIndex.HEADER,
 
1252
            _KndxIndex.HEADER,
925
1253
            "a option 1 1v :",
926
1254
            ])
 
1255
        index = self.get_knit_index(transport, 'filename', 'r')
927
1256
        try:
928
 
            self.assertRaises(errors.KnitCorrupt,
929
 
                              self.get_knit_index, transport, 'filename', 'r')
 
1257
            self.assertRaises(errors.KnitCorrupt, index.keys)
930
1258
        except TypeError, e:
931
1259
            if (str(e) == ('exceptions must be strings, classes, or instances,'
932
1260
                           ' not exceptions.ValueError')
937
1265
            else:
938
1266
                raise
939
1267
 
 
1268
    def test_scan_unvalidated_index_not_implemented(self):
 
1269
        transport = MockTransport()
 
1270
        index = self.get_knit_index(transport, 'filename', 'r')
 
1271
        self.assertRaises(
 
1272
            NotImplementedError, index.scan_unvalidated_index,
 
1273
            'dummy graph_index')
 
1274
        self.assertRaises(
 
1275
            NotImplementedError, index.get_missing_compression_parents)
 
1276
 
940
1277
    def test_short_line(self):
941
1278
        transport = MockTransport([
942
 
            _KnitIndex.HEADER,
 
1279
            _KndxIndex.HEADER,
943
1280
            "a option 0 10  :",
944
1281
            "b option 10 10 0", # This line isn't terminated, ignored
945
1282
            ])
946
1283
        index = self.get_knit_index(transport, "filename", "r")
947
 
        self.assertEqual(['a'], index.get_versions())
 
1284
        self.assertEqual(set([('a',)]), index.keys())
948
1285
 
949
1286
    def test_skip_incomplete_record(self):
950
1287
        # A line with bogus data should just be skipped
951
1288
        transport = MockTransport([
952
 
            _KnitIndex.HEADER,
 
1289
            _KndxIndex.HEADER,
953
1290
            "a option 0 10  :",
954
1291
            "b option 10 10 0", # This line isn't terminated, ignored
955
1292
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
956
1293
            ])
957
1294
        index = self.get_knit_index(transport, "filename", "r")
958
 
        self.assertEqual(['a', 'c'], index.get_versions())
 
1295
        self.assertEqual(set([('a',), ('c',)]), index.keys())
959
1296
 
960
1297
    def test_trailing_characters(self):
961
1298
        # A line with bogus data should just be skipped
962
1299
        transport = MockTransport([
963
 
            _KnitIndex.HEADER,
 
1300
            _KndxIndex.HEADER,
964
1301
            "a option 0 10  :",
965
1302
            "b option 10 10 0 :a", # This line has extra trailing characters
966
1303
            "c option 20 10 0 :", # Properly terminated, and starts with '\n'
967
1304
            ])
968
1305
        index = self.get_knit_index(transport, "filename", "r")
969
 
        self.assertEqual(['a', 'c'], index.get_versions())
 
1306
        self.assertEqual(set([('a',), ('c',)]), index.keys())
970
1307
 
971
1308
 
972
1309
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
973
1310
 
974
1311
    _test_needs_features = [CompiledKnitFeature]
975
1312
 
976
 
    def get_knit_index(self, *args, **kwargs):
 
1313
    def get_knit_index(self, transport, name, mode):
 
1314
        mapper = ConstantMapper(name)
977
1315
        orig = knit._load_data
978
1316
        def reset():
979
1317
            knit._load_data = orig
980
1318
        self.addCleanup(reset)
981
 
        from bzrlib._knit_load_data_c import _load_data_c
 
1319
        from bzrlib._knit_load_data_pyx import _load_data_c
982
1320
        knit._load_data = _load_data_c
983
 
        return _KnitIndex(*args, **kwargs)
984
 
 
 
1321
        allow_writes = lambda: mode == 'w'
 
1322
        return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
 
1323
 
 
1324
 
 
1325
class Test_KnitAnnotator(TestCaseWithMemoryTransport):
 
1326
 
 
1327
    def make_annotator(self):
 
1328
        factory = knit.make_pack_factory(True, True, 1)
 
1329
        vf = factory(self.get_transport())
 
1330
        return knit._KnitAnnotator(vf)
 
1331
 
 
1332
    def test__expand_fulltext(self):
 
1333
        ann = self.make_annotator()
 
1334
        rev_key = ('rev-id',)
 
1335
        ann._num_compression_children[rev_key] = 1
 
1336
        res = ann._expand_record(rev_key, (('parent-id',),), None,
 
1337
                           ['line1\n', 'line2\n'], ('fulltext', True))
 
1338
        # The content object and text lines should be cached appropriately
 
1339
        self.assertEqual(['line1\n', 'line2'], res)
 
1340
        content_obj = ann._content_objects[rev_key]
 
1341
        self.assertEqual(['line1\n', 'line2\n'], content_obj._lines)
 
1342
        self.assertEqual(res, content_obj.text())
 
1343
        self.assertEqual(res, ann._text_cache[rev_key])
 
1344
 
 
1345
    def test__expand_delta_comp_parent_not_available(self):
 
1346
        # Parent isn't available yet, so we return nothing, but queue up this
 
1347
        # node for later processing
 
1348
        ann = self.make_annotator()
 
1349
        rev_key = ('rev-id',)
 
1350
        parent_key = ('parent-id',)
 
1351
        record = ['0,1,1\n', 'new-line\n']
 
1352
        details = ('line-delta', False)
 
1353
        res = ann._expand_record(rev_key, (parent_key,), parent_key,
 
1354
                                 record, details)
 
1355
        self.assertEqual(None, res)
 
1356
        self.assertTrue(parent_key in ann._pending_deltas)
 
1357
        pending = ann._pending_deltas[parent_key]
 
1358
        self.assertEqual(1, len(pending))
 
1359
        self.assertEqual((rev_key, (parent_key,), record, details), pending[0])
 
1360
 
 
1361
    def test__expand_record_tracks_num_children(self):
 
1362
        ann = self.make_annotator()
 
1363
        rev_key = ('rev-id',)
 
1364
        rev2_key = ('rev2-id',)
 
1365
        parent_key = ('parent-id',)
 
1366
        record = ['0,1,1\n', 'new-line\n']
 
1367
        details = ('line-delta', False)
 
1368
        ann._num_compression_children[parent_key] = 2
 
1369
        ann._expand_record(parent_key, (), None, ['line1\n', 'line2\n'],
 
1370
                           ('fulltext', False))
 
1371
        res = ann._expand_record(rev_key, (parent_key,), parent_key,
 
1372
                                 record, details)
 
1373
        self.assertEqual({parent_key: 1}, ann._num_compression_children)
 
1374
        # Expanding the second child should remove the content object, and the
 
1375
        # num_compression_children entry
 
1376
        res = ann._expand_record(rev2_key, (parent_key,), parent_key,
 
1377
                                 record, details)
 
1378
        self.assertFalse(parent_key in ann._content_objects)
 
1379
        self.assertEqual({}, ann._num_compression_children)
 
1380
        # We should not cache the content_objects for rev2 and rev, because
 
1381
        # they do not have compression children of their own.
 
1382
        self.assertEqual({}, ann._content_objects)
 
1383
 
 
1384
    def test__expand_delta_records_blocks(self):
 
1385
        ann = self.make_annotator()
 
1386
        rev_key = ('rev-id',)
 
1387
        parent_key = ('parent-id',)
 
1388
        record = ['0,1,1\n', 'new-line\n']
 
1389
        details = ('line-delta', True)
 
1390
        ann._num_compression_children[parent_key] = 2
 
1391
        ann._expand_record(parent_key, (), None,
 
1392
                           ['line1\n', 'line2\n', 'line3\n'],
 
1393
                           ('fulltext', False))
 
1394
        ann._expand_record(rev_key, (parent_key,), parent_key, record, details)
 
1395
        self.assertEqual({(rev_key, parent_key): [(1, 1, 1), (3, 3, 0)]},
 
1396
                         ann._matching_blocks)
 
1397
        rev2_key = ('rev2-id',)
 
1398
        record = ['0,1,1\n', 'new-line\n']
 
1399
        details = ('line-delta', False)
 
1400
        ann._expand_record(rev2_key, (parent_key,), parent_key, record, details)
 
1401
        self.assertEqual([(1, 1, 2), (3, 3, 0)],
 
1402
                         ann._matching_blocks[(rev2_key, parent_key)])
 
1403
 
 
1404
    def test__get_parent_ann_uses_matching_blocks(self):
 
1405
        ann = self.make_annotator()
 
1406
        rev_key = ('rev-id',)
 
1407
        parent_key = ('parent-id',)
 
1408
        parent_ann = [(parent_key,)]*3
 
1409
        block_key = (rev_key, parent_key)
 
1410
        ann._annotations_cache[parent_key] = parent_ann
 
1411
        ann._matching_blocks[block_key] = [(0, 1, 1), (3, 3, 0)]
 
1412
        # We should not try to access any parent_lines content, because we know
 
1413
        # we already have the matching blocks
 
1414
        par_ann, blocks = ann._get_parent_annotations_and_matches(rev_key,
 
1415
                                        ['1\n', '2\n', '3\n'], parent_key)
 
1416
        self.assertEqual(parent_ann, par_ann)
 
1417
        self.assertEqual([(0, 1, 1), (3, 3, 0)], blocks)
 
1418
        self.assertEqual({}, ann._matching_blocks)
 
1419
 
 
1420
    def test__process_pending(self):
 
1421
        ann = self.make_annotator()
 
1422
        rev_key = ('rev-id',)
 
1423
        p1_key = ('p1-id',)
 
1424
        p2_key = ('p2-id',)
 
1425
        record = ['0,1,1\n', 'new-line\n']
 
1426
        details = ('line-delta', False)
 
1427
        p1_record = ['line1\n', 'line2\n']
 
1428
        ann._num_compression_children[p1_key] = 1
 
1429
        res = ann._expand_record(rev_key, (p1_key,p2_key), p1_key,
 
1430
                                 record, details)
 
1431
        self.assertEqual(None, res)
 
1432
        # self.assertTrue(p1_key in ann._pending_deltas)
 
1433
        self.assertEqual({}, ann._pending_annotation)
 
1434
        # Now insert p1, and we should be able to expand the delta
 
1435
        res = ann._expand_record(p1_key, (), None, p1_record,
 
1436
                                 ('fulltext', False))
 
1437
        self.assertEqual(p1_record, res)
 
1438
        ann._annotations_cache[p1_key] = [(p1_key,)]*2
 
1439
        res = ann._process_pending(p1_key)
 
1440
        self.assertEqual([], res)
 
1441
        self.assertFalse(p1_key in ann._pending_deltas)
 
1442
        self.assertTrue(p2_key in ann._pending_annotation)
 
1443
        self.assertEqual({p2_key: [(rev_key, (p1_key, p2_key))]},
 
1444
                         ann._pending_annotation)
 
1445
        # Now fill in parent 2, and pending annotation should be satisfied
 
1446
        res = ann._expand_record(p2_key, (), None, [], ('fulltext', False))
 
1447
        ann._annotations_cache[p2_key] = []
 
1448
        res = ann._process_pending(p2_key)
 
1449
        self.assertEqual([rev_key], res)
 
1450
        self.assertEqual({}, ann._pending_annotation)
 
1451
        self.assertEqual({}, ann._pending_deltas)
 
1452
 
 
1453
    def test_record_delta_removes_basis(self):
 
1454
        ann = self.make_annotator()
 
1455
        ann._expand_record(('parent-id',), (), None,
 
1456
                           ['line1\n', 'line2\n'], ('fulltext', False))
 
1457
        ann._num_compression_children['parent-id'] = 2
 
1458
 
 
1459
    def test_annotate_special_text(self):
 
1460
        ann = self.make_annotator()
 
1461
        vf = ann._vf
 
1462
        rev1_key = ('rev-1',)
 
1463
        rev2_key = ('rev-2',)
 
1464
        rev3_key = ('rev-3',)
 
1465
        spec_key = ('special:',)
 
1466
        vf.add_lines(rev1_key, [], ['initial content\n'])
 
1467
        vf.add_lines(rev2_key, [rev1_key], ['initial content\n',
 
1468
                                            'common content\n',
 
1469
                                            'content in 2\n'])
 
1470
        vf.add_lines(rev3_key, [rev1_key], ['initial content\n',
 
1471
                                            'common content\n',
 
1472
                                            'content in 3\n'])
 
1473
        spec_text = ('initial content\n'
 
1474
                     'common content\n'
 
1475
                     'content in 2\n'
 
1476
                     'content in 3\n')
 
1477
        ann.add_special_text(spec_key, [rev2_key, rev3_key], spec_text)
 
1478
        anns, lines = ann.annotate(spec_key)
 
1479
        self.assertEqual([(rev1_key,),
 
1480
                          (rev2_key, rev3_key),
 
1481
                          (rev2_key,),
 
1482
                          (rev3_key,),
 
1483
                         ], anns)
 
1484
        self.assertEqualDiff(spec_text, ''.join(lines))
985
1485
 
986
1486
 
987
1487
class KnitTests(TestCaseWithTransport):
988
1488
    """Class containing knit test helper routines."""
989
1489
 
990
 
    def make_test_knit(self, annotate=False, delay_create=False, index=None,
991
 
                       name='test'):
992
 
        if not annotate:
993
 
            factory = KnitPlainFactory()
994
 
        else:
995
 
            factory = None
996
 
        return KnitVersionedFile(name, get_transport('.'), access_mode='w',
997
 
                                 factory=factory, create=True,
998
 
                                 delay_create=delay_create, index=index)
999
 
 
1000
 
    def assertRecordContentEqual(self, knit, version_id, candidate_content):
1001
 
        """Assert that some raw record content matches the raw record content
1002
 
        for a particular version_id in the given knit.
1003
 
        """
1004
 
        index_memo = knit._index.get_position(version_id)
1005
 
        record = (version_id, index_memo)
1006
 
        [(_, expected_content)] = list(knit._data.read_records_iter_raw([record]))
1007
 
        self.assertEqual(expected_content, candidate_content)
1008
 
 
1009
 
 
1010
 
class BasicKnitTests(KnitTests):
1011
 
 
1012
 
    def add_stock_one_and_one_a(self, k):
1013
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
1014
 
        k.add_lines('text-1a', ['text-1'], split_lines(TEXT_1A))
1015
 
 
1016
 
    def test_knit_constructor(self):
1017
 
        """Construct empty k"""
1018
 
        self.make_test_knit()
1019
 
 
1020
 
    def test_make_explicit_index(self):
1021
 
        """We can supply an index to use."""
1022
 
        knit = KnitVersionedFile('test', get_transport('.'),
1023
 
            index='strangelove')
1024
 
        self.assertEqual(knit._index, 'strangelove')
1025
 
 
1026
 
    def test_knit_add(self):
1027
 
        """Store one text in knit and retrieve"""
1028
 
        k = self.make_test_knit()
1029
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
1030
 
        self.assertTrue(k.has_version('text-1'))
1031
 
        self.assertEqualDiff(''.join(k.get_lines('text-1')), TEXT_1)
1032
 
 
1033
 
    def test_knit_reload(self):
1034
 
        # test that the content in a reloaded knit is correct
1035
 
        k = self.make_test_knit()
1036
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
1037
 
        del k
1038
 
        k2 = KnitVersionedFile('test', get_transport('.'), access_mode='r', factory=KnitPlainFactory(), create=True)
1039
 
        self.assertTrue(k2.has_version('text-1'))
1040
 
        self.assertEqualDiff(''.join(k2.get_lines('text-1')), TEXT_1)
1041
 
 
1042
 
    def test_knit_several(self):
1043
 
        """Store several texts in a knit"""
1044
 
        k = self.make_test_knit()
1045
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
1046
 
        k.add_lines('text-2', [], split_lines(TEXT_2))
1047
 
        self.assertEqualDiff(''.join(k.get_lines('text-1')), TEXT_1)
1048
 
        self.assertEqualDiff(''.join(k.get_lines('text-2')), TEXT_2)
1049
 
        
1050
 
    def test_repeated_add(self):
1051
 
        """Knit traps attempt to replace existing version"""
1052
 
        k = self.make_test_knit()
1053
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
1054
 
        self.assertRaises(RevisionAlreadyPresent, 
1055
 
                k.add_lines,
1056
 
                'text-1', [], split_lines(TEXT_1))
1057
 
 
1058
 
    def test_empty(self):
1059
 
        k = self.make_test_knit(True)
1060
 
        k.add_lines('text-1', [], [])
1061
 
        self.assertEquals(k.get_lines('text-1'), [])
1062
 
 
1063
 
    def test_incomplete(self):
1064
 
        """Test if texts without a ending line-end can be inserted and
1065
 
        extracted."""
1066
 
        k = KnitVersionedFile('test', get_transport('.'), delta=False, create=True)
1067
 
        k.add_lines('text-1', [], ['a\n',    'b'  ])
1068
 
        k.add_lines('text-2', ['text-1'], ['a\rb\n', 'b\n'])
1069
 
        # reopening ensures maximum room for confusion
1070
 
        k = KnitVersionedFile('test', get_transport('.'), delta=False, create=True)
1071
 
        self.assertEquals(k.get_lines('text-1'), ['a\n',    'b'  ])
1072
 
        self.assertEquals(k.get_lines('text-2'), ['a\rb\n', 'b\n'])
1073
 
 
1074
 
    def test_delta(self):
1075
 
        """Expression of knit delta as lines"""
1076
 
        k = self.make_test_knit()
1077
 
        KnitContent
1078
 
        td = list(line_delta(TEXT_1.splitlines(True),
1079
 
                             TEXT_1A.splitlines(True)))
1080
 
        self.assertEqualDiff(''.join(td), delta_1_1a)
1081
 
        out = apply_line_delta(TEXT_1.splitlines(True), td)
1082
 
        self.assertEqualDiff(''.join(out), TEXT_1A)
1083
 
 
1084
 
    def assertDerivedBlocksEqual(self, source, target, noeol=False):
1085
 
        """Assert that the derived matching blocks match real output"""
1086
 
        source_lines = source.splitlines(True)
1087
 
        target_lines = target.splitlines(True)
1088
 
        def nl(line):
1089
 
            if noeol and not line.endswith('\n'):
1090
 
                return line + '\n'
1091
 
            else:
1092
 
                return line
1093
 
        source_content = KnitContent([(None, nl(l)) for l in source_lines])
1094
 
        target_content = KnitContent([(None, nl(l)) for l in target_lines])
1095
 
        line_delta = source_content.line_delta(target_content)
1096
 
        delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
1097
 
            source_lines, target_lines))
1098
 
        matcher = KnitSequenceMatcher(None, source_lines, target_lines)
1099
 
        matcher_blocks = list(list(matcher.get_matching_blocks()))
1100
 
        self.assertEqual(matcher_blocks, delta_blocks)
1101
 
 
1102
 
    def test_get_line_delta_blocks(self):
1103
 
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
1104
 
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
1105
 
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
1106
 
        self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
1107
 
        self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
1108
 
        self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
1109
 
        self.assertDerivedBlocksEqual(TEXT_1A, '')
1110
 
        self.assertDerivedBlocksEqual('', TEXT_1A)
1111
 
        self.assertDerivedBlocksEqual('', '')
1112
 
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
1113
 
 
1114
 
    def test_get_line_delta_blocks_noeol(self):
1115
 
        """Handle historical knit deltas safely
1116
 
 
1117
 
        Some existing knit deltas don't consider the last line to differ
1118
 
        when the only difference whether it has a final newline.
1119
 
 
1120
 
        New knit deltas appear to always consider the last line to differ
1121
 
        in this case.
1122
 
        """
1123
 
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
1124
 
        self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
1125
 
        self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
1126
 
        self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
1127
 
 
1128
 
    def test_add_with_parents(self):
1129
 
        """Store in knit with parents"""
1130
 
        k = self.make_test_knit()
1131
 
        self.add_stock_one_and_one_a(k)
1132
 
        self.assertEquals(k.get_parents('text-1'), [])
1133
 
        self.assertEquals(k.get_parents('text-1a'), ['text-1'])
1134
 
 
1135
 
    def test_ancestry(self):
1136
 
        """Store in knit with parents"""
1137
 
        k = self.make_test_knit()
1138
 
        self.add_stock_one_and_one_a(k)
1139
 
        self.assertEquals(set(k.get_ancestry(['text-1a'])), set(['text-1a', 'text-1']))
1140
 
 
1141
 
    def test_add_delta(self):
1142
 
        """Store in knit with parents"""
1143
 
        k = KnitVersionedFile('test', get_transport('.'), factory=KnitPlainFactory(),
1144
 
            delta=True, create=True)
1145
 
        self.add_stock_one_and_one_a(k)
1146
 
        k.clear_cache()
1147
 
        self.assertEqualDiff(''.join(k.get_lines('text-1a')), TEXT_1A)
1148
 
 
1149
 
    def test_add_delta_knit_graph_index(self):
1150
 
        """Does adding work with a KnitGraphIndex."""
1151
 
        index = InMemoryGraphIndex(2)
1152
 
        knit_index = KnitGraphIndex(index, add_callback=index.add_nodes,
1153
 
            deltas=True)
1154
 
        k = KnitVersionedFile('test', get_transport('.'),
1155
 
            delta=True, create=True, index=knit_index)
1156
 
        self.add_stock_one_and_one_a(k)
1157
 
        k.clear_cache()
1158
 
        self.assertEqualDiff(''.join(k.get_lines('text-1a')), TEXT_1A)
1159
 
        # check the index had the right data added.
1160
 
        self.assertEqual(set([
1161
 
            (index, ('text-1', ), ' 0 127', ((), ())),
1162
 
            (index, ('text-1a', ), ' 127 140', ((('text-1', ),), (('text-1', ),))),
1163
 
            ]), set(index.iter_all_entries()))
1164
 
        # we should not have a .kndx file
1165
 
        self.assertFalse(get_transport('.').has('test.kndx'))
1166
 
 
1167
 
    def test_annotate(self):
1168
 
        """Annotations"""
1169
 
        k = KnitVersionedFile('knit', get_transport('.'), factory=KnitAnnotateFactory(),
1170
 
            delta=True, create=True)
1171
 
        self.insert_and_test_small_annotate(k)
1172
 
 
1173
 
    def insert_and_test_small_annotate(self, k):
1174
 
        """test annotation with k works correctly."""
1175
 
        k.add_lines('text-1', [], ['a\n', 'b\n'])
1176
 
        k.add_lines('text-2', ['text-1'], ['a\n', 'c\n'])
1177
 
 
1178
 
        origins = k.annotate('text-2')
1179
 
        self.assertEquals(origins[0], ('text-1', 'a\n'))
1180
 
        self.assertEquals(origins[1], ('text-2', 'c\n'))
1181
 
 
1182
 
    def test_annotate_fulltext(self):
1183
 
        """Annotations"""
1184
 
        k = KnitVersionedFile('knit', get_transport('.'), factory=KnitAnnotateFactory(),
1185
 
            delta=False, create=True)
1186
 
        self.insert_and_test_small_annotate(k)
1187
 
 
1188
 
    def test_annotate_merge_1(self):
1189
 
        k = self.make_test_knit(True)
1190
 
        k.add_lines('text-a1', [], ['a\n', 'b\n'])
1191
 
        k.add_lines('text-a2', [], ['d\n', 'c\n'])
1192
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['d\n', 'b\n'])
1193
 
        origins = k.annotate('text-am')
1194
 
        self.assertEquals(origins[0], ('text-a2', 'd\n'))
1195
 
        self.assertEquals(origins[1], ('text-a1', 'b\n'))
1196
 
 
1197
 
    def test_annotate_merge_2(self):
1198
 
        k = self.make_test_knit(True)
1199
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
1200
 
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
1201
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['a\n', 'y\n', 'c\n'])
1202
 
        origins = k.annotate('text-am')
1203
 
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
1204
 
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
1205
 
        self.assertEquals(origins[2], ('text-a1', 'c\n'))
1206
 
 
1207
 
    def test_annotate_merge_9(self):
1208
 
        k = self.make_test_knit(True)
1209
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
1210
 
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
1211
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['k\n', 'y\n', 'c\n'])
1212
 
        origins = k.annotate('text-am')
1213
 
        self.assertEquals(origins[0], ('text-am', 'k\n'))
1214
 
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
1215
 
        self.assertEquals(origins[2], ('text-a1', 'c\n'))
1216
 
 
1217
 
    def test_annotate_merge_3(self):
1218
 
        k = self.make_test_knit(True)
1219
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
1220
 
        k.add_lines('text-a2', [] ,['x\n', 'y\n', 'z\n'])
1221
 
        k.add_lines('text-am', ['text-a1', 'text-a2'], ['k\n', 'y\n', 'z\n'])
1222
 
        origins = k.annotate('text-am')
1223
 
        self.assertEquals(origins[0], ('text-am', 'k\n'))
1224
 
        self.assertEquals(origins[1], ('text-a2', 'y\n'))
1225
 
        self.assertEquals(origins[2], ('text-a2', 'z\n'))
1226
 
 
1227
 
    def test_annotate_merge_4(self):
1228
 
        k = self.make_test_knit(True)
1229
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
1230
 
        k.add_lines('text-a2', [], ['x\n', 'y\n', 'z\n'])
1231
 
        k.add_lines('text-a3', ['text-a1'], ['a\n', 'b\n', 'p\n'])
1232
 
        k.add_lines('text-am', ['text-a2', 'text-a3'], ['a\n', 'b\n', 'z\n'])
1233
 
        origins = k.annotate('text-am')
1234
 
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
1235
 
        self.assertEquals(origins[1], ('text-a1', 'b\n'))
1236
 
        self.assertEquals(origins[2], ('text-a2', 'z\n'))
1237
 
 
1238
 
    def test_annotate_merge_5(self):
1239
 
        k = self.make_test_knit(True)
1240
 
        k.add_lines('text-a1', [], ['a\n', 'b\n', 'c\n'])
1241
 
        k.add_lines('text-a2', [], ['d\n', 'e\n', 'f\n'])
1242
 
        k.add_lines('text-a3', [], ['x\n', 'y\n', 'z\n'])
1243
 
        k.add_lines('text-am',
1244
 
                    ['text-a1', 'text-a2', 'text-a3'],
1245
 
                    ['a\n', 'e\n', 'z\n'])
1246
 
        origins = k.annotate('text-am')
1247
 
        self.assertEquals(origins[0], ('text-a1', 'a\n'))
1248
 
        self.assertEquals(origins[1], ('text-a2', 'e\n'))
1249
 
        self.assertEquals(origins[2], ('text-a3', 'z\n'))
1250
 
 
1251
 
    def test_annotate_file_cherry_pick(self):
1252
 
        k = self.make_test_knit(True)
1253
 
        k.add_lines('text-1', [], ['a\n', 'b\n', 'c\n'])
1254
 
        k.add_lines('text-2', ['text-1'], ['d\n', 'e\n', 'f\n'])
1255
 
        k.add_lines('text-3', ['text-2', 'text-1'], ['a\n', 'b\n', 'c\n'])
1256
 
        origins = k.annotate('text-3')
1257
 
        self.assertEquals(origins[0], ('text-1', 'a\n'))
1258
 
        self.assertEquals(origins[1], ('text-1', 'b\n'))
1259
 
        self.assertEquals(origins[2], ('text-1', 'c\n'))
1260
 
 
1261
 
    def test_knit_join(self):
1262
 
        """Store in knit with parents"""
1263
 
        k1 = KnitVersionedFile('test1', get_transport('.'), factory=KnitPlainFactory(), create=True)
1264
 
        k1.add_lines('text-a', [], split_lines(TEXT_1))
1265
 
        k1.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
1266
 
 
1267
 
        k1.add_lines('text-c', [], split_lines(TEXT_1))
1268
 
        k1.add_lines('text-d', ['text-c'], split_lines(TEXT_1))
1269
 
 
1270
 
        k1.add_lines('text-m', ['text-b', 'text-d'], split_lines(TEXT_1))
1271
 
 
1272
 
        k2 = KnitVersionedFile('test2', get_transport('.'), factory=KnitPlainFactory(), create=True)
1273
 
        count = k2.join(k1, version_ids=['text-m'])
1274
 
        self.assertEquals(count, 5)
1275
 
        self.assertTrue(k2.has_version('text-a'))
1276
 
        self.assertTrue(k2.has_version('text-c'))
1277
 
 
1278
 
    def test_reannotate(self):
1279
 
        k1 = KnitVersionedFile('knit1', get_transport('.'),
1280
 
                               factory=KnitAnnotateFactory(), create=True)
1281
 
        # 0
1282
 
        k1.add_lines('text-a', [], ['a\n', 'b\n'])
1283
 
        # 1
1284
 
        k1.add_lines('text-b', ['text-a'], ['a\n', 'c\n'])
1285
 
 
1286
 
        k2 = KnitVersionedFile('test2', get_transport('.'),
1287
 
                               factory=KnitAnnotateFactory(), create=True)
1288
 
        k2.join(k1, version_ids=['text-b'])
1289
 
 
1290
 
        # 2
1291
 
        k1.add_lines('text-X', ['text-b'], ['a\n', 'b\n'])
1292
 
        # 2
1293
 
        k2.add_lines('text-c', ['text-b'], ['z\n', 'c\n'])
1294
 
        # 3
1295
 
        k2.add_lines('text-Y', ['text-b'], ['b\n', 'c\n'])
1296
 
 
1297
 
        # test-c will have index 3
1298
 
        k1.join(k2, version_ids=['text-c'])
1299
 
 
1300
 
        lines = k1.get_lines('text-c')
1301
 
        self.assertEquals(lines, ['z\n', 'c\n'])
1302
 
 
1303
 
        origins = k1.annotate('text-c')
1304
 
        self.assertEquals(origins[0], ('text-c', 'z\n'))
1305
 
        self.assertEquals(origins[1], ('text-b', 'c\n'))
1306
 
 
1307
 
    def test_get_line_delta_texts(self):
1308
 
        """Make sure we can call get_texts on text with reused line deltas"""
1309
 
        k1 = KnitVersionedFile('test1', get_transport('.'), 
1310
 
                               factory=KnitPlainFactory(), create=True)
1311
 
        for t in range(3):
1312
 
            if t == 0:
1313
 
                parents = []
1314
 
            else:
1315
 
                parents = ['%d' % (t-1)]
1316
 
            k1.add_lines('%d' % t, parents, ['hello\n'] * t)
1317
 
        k1.get_texts(('%d' % t) for t in range(3))
1318
 
        
1319
 
    def test_iter_lines_reads_in_order(self):
1320
 
        t = MemoryTransport()
1321
 
        instrumented_t = TransportLogger(t)
1322
 
        k1 = KnitVersionedFile('id', instrumented_t, create=True, delta=True)
1323
 
        self.assertEqual([('id.kndx',)], instrumented_t._calls)
1324
 
        # add texts with no required ordering
1325
 
        k1.add_lines('base', [], ['text\n'])
1326
 
        k1.add_lines('base2', [], ['text2\n'])
1327
 
        k1.clear_cache()
1328
 
        instrumented_t._calls = []
1329
 
        # request a last-first iteration
1330
 
        results = list(k1.iter_lines_added_or_present_in_versions(['base2', 'base']))
1331
 
        self.assertEqual([('id.knit', [(0, 87), (87, 89)])], instrumented_t._calls)
1332
 
        self.assertEqual(['text\n', 'text2\n'], results)
1333
 
 
1334
 
    def test_create_empty_annotated(self):
1335
 
        k1 = self.make_test_knit(True)
1336
 
        # 0
1337
 
        k1.add_lines('text-a', [], ['a\n', 'b\n'])
1338
 
        k2 = k1.create_empty('t', MemoryTransport())
1339
 
        self.assertTrue(isinstance(k2.factory, KnitAnnotateFactory))
1340
 
        self.assertEqual(k1.delta, k2.delta)
1341
 
        # the generic test checks for empty content and file class
1342
 
 
1343
 
    def test_knit_format(self):
1344
 
        # this tests that a new knit index file has the expected content
1345
 
        # and that is writes the data we expect as records are added.
1346
 
        knit = self.make_test_knit(True)
1347
 
        # Now knit files are not created until we first add data to them
1348
 
        self.assertFileEqual("# bzr knit index 8\n", 'test.kndx')
1349
 
        knit.add_lines_with_ghosts('revid', ['a_ghost'], ['a\n'])
1350
 
        self.assertFileEqual(
1351
 
            "# bzr knit index 8\n"
1352
 
            "\n"
1353
 
            "revid fulltext 0 84 .a_ghost :",
1354
 
            'test.kndx')
1355
 
        knit.add_lines_with_ghosts('revid2', ['revid'], ['a\n'])
1356
 
        self.assertFileEqual(
1357
 
            "# bzr knit index 8\n"
1358
 
            "\nrevid fulltext 0 84 .a_ghost :"
1359
 
            "\nrevid2 line-delta 84 82 0 :",
1360
 
            'test.kndx')
1361
 
        # we should be able to load this file again
1362
 
        knit = KnitVersionedFile('test', get_transport('.'), access_mode='r')
1363
 
        self.assertEqual(['revid', 'revid2'], knit.versions())
1364
 
        # write a short write to the file and ensure that its ignored
1365
 
        indexfile = file('test.kndx', 'ab')
1366
 
        indexfile.write('\nrevid3 line-delta 166 82 1 2 3 4 5 .phwoar:demo ')
1367
 
        indexfile.close()
1368
 
        # we should be able to load this file again
1369
 
        knit = KnitVersionedFile('test', get_transport('.'), access_mode='w')
1370
 
        self.assertEqual(['revid', 'revid2'], knit.versions())
1371
 
        # and add a revision with the same id the failed write had
1372
 
        knit.add_lines('revid3', ['revid2'], ['a\n'])
1373
 
        # and when reading it revid3 should now appear.
1374
 
        knit = KnitVersionedFile('test', get_transport('.'), access_mode='r')
1375
 
        self.assertEqual(['revid', 'revid2', 'revid3'], knit.versions())
1376
 
        self.assertEqual(['revid2'], knit.get_parents('revid3'))
1377
 
 
1378
 
    def test_delay_create(self):
1379
 
        """Test that passing delay_create=True creates files late"""
1380
 
        knit = self.make_test_knit(annotate=True, delay_create=True)
1381
 
        self.failIfExists('test.knit')
1382
 
        self.failIfExists('test.kndx')
1383
 
        knit.add_lines_with_ghosts('revid', ['a_ghost'], ['a\n'])
1384
 
        self.failUnlessExists('test.knit')
1385
 
        self.assertFileEqual(
1386
 
            "# bzr knit index 8\n"
1387
 
            "\n"
1388
 
            "revid fulltext 0 84 .a_ghost :",
1389
 
            'test.kndx')
1390
 
 
1391
 
    def test_create_parent_dir(self):
1392
 
        """create_parent_dir can create knits in nonexistant dirs"""
1393
 
        # Has no effect if we don't set 'delay_create'
1394
 
        trans = get_transport('.')
1395
 
        self.assertRaises(NoSuchFile, KnitVersionedFile, 'dir/test',
1396
 
                          trans, access_mode='w', factory=None,
1397
 
                          create=True, create_parent_dir=True)
1398
 
        # Nothing should have changed yet
1399
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1400
 
                                 factory=None, create=True,
1401
 
                                 create_parent_dir=True,
1402
 
                                 delay_create=True)
1403
 
        self.failIfExists('dir/test.knit')
1404
 
        self.failIfExists('dir/test.kndx')
1405
 
        self.failIfExists('dir')
1406
 
        knit.add_lines('revid', [], ['a\n'])
1407
 
        self.failUnlessExists('dir')
1408
 
        self.failUnlessExists('dir/test.knit')
1409
 
        self.assertFileEqual(
1410
 
            "# bzr knit index 8\n"
1411
 
            "\n"
1412
 
            "revid fulltext 0 84  :",
1413
 
            'dir/test.kndx')
1414
 
 
1415
 
    def test_create_mode_700(self):
1416
 
        trans = get_transport('.')
1417
 
        if not trans._can_roundtrip_unix_modebits():
1418
 
            # Can't roundtrip, so no need to run this test
1419
 
            return
1420
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1421
 
                                 factory=None, create=True,
1422
 
                                 create_parent_dir=True,
1423
 
                                 delay_create=True,
1424
 
                                 file_mode=0600,
1425
 
                                 dir_mode=0700)
1426
 
        knit.add_lines('revid', [], ['a\n'])
1427
 
        self.assertTransportMode(trans, 'dir', 0700)
1428
 
        self.assertTransportMode(trans, 'dir/test.knit', 0600)
1429
 
        self.assertTransportMode(trans, 'dir/test.kndx', 0600)
1430
 
 
1431
 
    def test_create_mode_770(self):
1432
 
        trans = get_transport('.')
1433
 
        if not trans._can_roundtrip_unix_modebits():
1434
 
            # Can't roundtrip, so no need to run this test
1435
 
            return
1436
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1437
 
                                 factory=None, create=True,
1438
 
                                 create_parent_dir=True,
1439
 
                                 delay_create=True,
1440
 
                                 file_mode=0660,
1441
 
                                 dir_mode=0770)
1442
 
        knit.add_lines('revid', [], ['a\n'])
1443
 
        self.assertTransportMode(trans, 'dir', 0770)
1444
 
        self.assertTransportMode(trans, 'dir/test.knit', 0660)
1445
 
        self.assertTransportMode(trans, 'dir/test.kndx', 0660)
1446
 
 
1447
 
    def test_create_mode_777(self):
1448
 
        trans = get_transport('.')
1449
 
        if not trans._can_roundtrip_unix_modebits():
1450
 
            # Can't roundtrip, so no need to run this test
1451
 
            return
1452
 
        knit = KnitVersionedFile('dir/test', trans, access_mode='w',
1453
 
                                 factory=None, create=True,
1454
 
                                 create_parent_dir=True,
1455
 
                                 delay_create=True,
1456
 
                                 file_mode=0666,
1457
 
                                 dir_mode=0777)
1458
 
        knit.add_lines('revid', [], ['a\n'])
1459
 
        self.assertTransportMode(trans, 'dir', 0777)
1460
 
        self.assertTransportMode(trans, 'dir/test.knit', 0666)
1461
 
        self.assertTransportMode(trans, 'dir/test.kndx', 0666)
1462
 
 
1463
 
    def test_plan_merge(self):
1464
 
        my_knit = self.make_test_knit(annotate=True)
1465
 
        my_knit.add_lines('text1', [], split_lines(TEXT_1))
1466
 
        my_knit.add_lines('text1a', ['text1'], split_lines(TEXT_1A))
1467
 
        my_knit.add_lines('text1b', ['text1'], split_lines(TEXT_1B))
1468
 
        plan = list(my_knit.plan_merge('text1a', 'text1b'))
1469
 
        for plan_line, expected_line in zip(plan, AB_MERGE):
1470
 
            self.assertEqual(plan_line, expected_line)
1471
 
 
1472
 
    def test_get_stream_empty(self):
1473
 
        """Get a data stream for an empty knit file."""
1474
 
        k1 = self.make_test_knit()
1475
 
        format, data_list, reader_callable = k1.get_data_stream([])
1476
 
        self.assertEqual('knit-plain', format)
1477
 
        self.assertEqual([], data_list)
1478
 
        content = reader_callable(None)
1479
 
        self.assertEqual('', content)
1480
 
        self.assertIsInstance(content, str)
1481
 
 
1482
 
    def test_get_stream_one_version(self):
1483
 
        """Get a data stream for a single record out of a knit containing just
1484
 
        one record.
1485
 
        """
1486
 
        k1 = self.make_test_knit()
1487
 
        test_data = [
1488
 
            ('text-a', [], TEXT_1),
1489
 
            ]
1490
 
        expected_data_list = [
1491
 
            # version, options, length, parents
1492
 
            ('text-a', ['fulltext'], 122, []),
1493
 
           ]
1494
 
        for version_id, parents, lines in test_data:
1495
 
            k1.add_lines(version_id, parents, split_lines(lines))
1496
 
 
1497
 
        format, data_list, reader_callable = k1.get_data_stream(['text-a'])
1498
 
        self.assertEqual('knit-plain', format)
1499
 
        self.assertEqual(expected_data_list, data_list)
1500
 
        # There's only one record in the knit, so the content should be the
1501
 
        # entire knit data file's contents.
1502
 
        self.assertEqual(k1.transport.get_bytes(k1._data._access._filename),
1503
 
                         reader_callable(None))
1504
 
        
1505
 
    def test_get_stream_get_one_version_of_many(self):
1506
 
        """Get a data stream for just one version out of a knit containing many
1507
 
        versions.
1508
 
        """
1509
 
        k1 = self.make_test_knit()
1510
 
        # Insert the same data as test_knit_join, as they seem to cover a range
1511
 
        # of cases (no parents, one parent, multiple parents).
1512
 
        test_data = [
1513
 
            ('text-a', [], TEXT_1),
1514
 
            ('text-b', ['text-a'], TEXT_1),
1515
 
            ('text-c', [], TEXT_1),
1516
 
            ('text-d', ['text-c'], TEXT_1),
1517
 
            ('text-m', ['text-b', 'text-d'], TEXT_1),
1518
 
            ]
1519
 
        expected_data_list = [
1520
 
            # version, options, length, parents
1521
 
            ('text-m', ['line-delta'], 84, ['text-b', 'text-d']),
1522
 
            ]
1523
 
        for version_id, parents, lines in test_data:
1524
 
            k1.add_lines(version_id, parents, split_lines(lines))
1525
 
 
1526
 
        format, data_list, reader_callable = k1.get_data_stream(['text-m'])
1527
 
        self.assertEqual('knit-plain', format)
1528
 
        self.assertEqual(expected_data_list, data_list)
1529
 
        self.assertRecordContentEqual(k1, 'text-m', reader_callable(None))
1530
 
        
1531
 
    def test_get_stream_ghost_parent(self):
1532
 
        """Get a data stream for a version with a ghost parent."""
1533
 
        k1 = self.make_test_knit()
1534
 
        # Test data
1535
 
        k1.add_lines('text-a', [], split_lines(TEXT_1))
1536
 
        k1.add_lines_with_ghosts('text-b', ['text-a', 'text-ghost'],
1537
 
                                 split_lines(TEXT_1))
1538
 
        # Expected data
1539
 
        expected_data_list = [
1540
 
            # version, options, length, parents
1541
 
            ('text-b', ['line-delta'], 84, ['text-a', 'text-ghost']),
1542
 
            ]
1543
 
        
1544
 
        format, data_list, reader_callable = k1.get_data_stream(['text-b'])
1545
 
        self.assertEqual('knit-plain', format)
1546
 
        self.assertEqual(expected_data_list, data_list)
1547
 
        self.assertRecordContentEqual(k1, 'text-b', reader_callable(None))
1548
 
    
1549
 
    def test_get_stream_get_multiple_records(self):
1550
 
        """Get a stream for multiple records of a knit."""
1551
 
        k1 = self.make_test_knit()
1552
 
        # Insert the same data as test_knit_join, as they seem to cover a range
1553
 
        # of cases (no parents, one parent, multiple parents).
1554
 
        test_data = [
1555
 
            ('text-a', [], TEXT_1),
1556
 
            ('text-b', ['text-a'], TEXT_1),
1557
 
            ('text-c', [], TEXT_1),
1558
 
            ('text-d', ['text-c'], TEXT_1),
1559
 
            ('text-m', ['text-b', 'text-d'], TEXT_1),
1560
 
            ]
1561
 
        expected_data_list = [
1562
 
            # version, options, length, parents
1563
 
            ('text-b', ['line-delta'], 84, ['text-a']),
1564
 
            ('text-d', ['line-delta'], 84, ['text-c']),
1565
 
            ]
1566
 
        for version_id, parents, lines in test_data:
1567
 
            k1.add_lines(version_id, parents, split_lines(lines))
1568
 
 
1569
 
        # Note that even though we request the revision IDs in a particular
1570
 
        # order, the data stream may return them in any order it likes.  In this
1571
 
        # case, they'll be in the order they were inserted into the knit.
1572
 
        format, data_list, reader_callable = k1.get_data_stream(
1573
 
            ['text-d', 'text-b'])
1574
 
        self.assertEqual('knit-plain', format)
1575
 
        self.assertEqual(expected_data_list, data_list)
1576
 
        self.assertRecordContentEqual(k1, 'text-b', reader_callable(84))
1577
 
        self.assertRecordContentEqual(k1, 'text-d', reader_callable(84))
1578
 
        self.assertEqual('', reader_callable(None),
1579
 
                         "There should be no more bytes left to read.")
1580
 
 
1581
 
    def test_get_stream_all(self):
1582
 
        """Get a data stream for all the records in a knit.
1583
 
 
1584
 
        This exercises fulltext records, line-delta records, records with
1585
 
        various numbers of parents, and reading multiple records out of the
1586
 
        callable.  These cases ought to all be exercised individually by the
1587
 
        other test_get_stream_* tests; this test is basically just paranoia.
1588
 
        """
1589
 
        k1 = self.make_test_knit()
1590
 
        # Insert the same data as test_knit_join, as they seem to cover a range
1591
 
        # of cases (no parents, one parent, multiple parents).
1592
 
        test_data = [
1593
 
            ('text-a', [], TEXT_1),
1594
 
            ('text-b', ['text-a'], TEXT_1),
1595
 
            ('text-c', [], TEXT_1),
1596
 
            ('text-d', ['text-c'], TEXT_1),
1597
 
            ('text-m', ['text-b', 'text-d'], TEXT_1),
1598
 
           ]
1599
 
        expected_data_list = [
1600
 
            # version, options, length, parents
1601
 
            ('text-a', ['fulltext'], 122, []),
1602
 
            ('text-b', ['line-delta'], 84, ['text-a']),
1603
 
            ('text-c', ['fulltext'], 121, []),
1604
 
            ('text-d', ['line-delta'], 84, ['text-c']),
1605
 
            ('text-m', ['line-delta'], 84, ['text-b', 'text-d']),
1606
 
            ]
1607
 
        for version_id, parents, lines in test_data:
1608
 
            k1.add_lines(version_id, parents, split_lines(lines))
1609
 
 
1610
 
        format, data_list, reader_callable = k1.get_data_stream(
1611
 
            ['text-a', 'text-b', 'text-c', 'text-d', 'text-m'])
1612
 
        self.assertEqual('knit-plain', format)
1613
 
        self.assertEqual(expected_data_list, data_list)
1614
 
        for version_id, options, length, parents in expected_data_list:
1615
 
            bytes = reader_callable(length)
1616
 
            self.assertRecordContentEqual(k1, version_id, bytes)
1617
 
 
1618
 
    def assertKnitFilesEqual(self, knit1, knit2):
1619
 
        """Assert that the contents of the index and data files of two knits are
1620
 
        equal.
1621
 
        """
1622
 
        self.assertEqual(
1623
 
            knit1.transport.get_bytes(knit1._data._access._filename),
1624
 
            knit2.transport.get_bytes(knit2._data._access._filename))
1625
 
        self.assertEqual(
1626
 
            knit1.transport.get_bytes(knit1._index._filename),
1627
 
            knit2.transport.get_bytes(knit2._index._filename))
1628
 
 
1629
 
    def test_insert_data_stream_empty(self):
1630
 
        """Inserting a data stream with no records should not put any data into
1631
 
        the knit.
1632
 
        """
1633
 
        k1 = self.make_test_knit()
1634
 
        k1.insert_data_stream(
1635
 
            (k1.get_format_signature(), [], lambda ignored: ''))
1636
 
        self.assertEqual('', k1.transport.get_bytes(k1._data._access._filename),
1637
 
                         "The .knit should be completely empty.")
1638
 
        self.assertEqual(k1._index.HEADER,
1639
 
                         k1.transport.get_bytes(k1._index._filename),
1640
 
                         "The .kndx should have nothing apart from the header.")
1641
 
 
1642
 
    def test_insert_data_stream_one_record(self):
1643
 
        """Inserting a data stream with one record from a knit with one record
1644
 
        results in byte-identical files.
1645
 
        """
1646
 
        source = self.make_test_knit(name='source')
1647
 
        source.add_lines('text-a', [], split_lines(TEXT_1))
1648
 
        data_stream = source.get_data_stream(['text-a'])
1649
 
        
1650
 
        target = self.make_test_knit(name='target')
1651
 
        target.insert_data_stream(data_stream)
1652
 
        
1653
 
        self.assertKnitFilesEqual(source, target)
1654
 
 
1655
 
    def test_insert_data_stream_records_already_present(self):
1656
 
        """Insert a data stream where some records are alreday present in the
1657
 
        target, and some not.  Only the new records are inserted.
1658
 
        """
1659
 
        source = self.make_test_knit(name='source')
1660
 
        target = self.make_test_knit(name='target')
1661
 
        # Insert 'text-a' into both source and target
1662
 
        source.add_lines('text-a', [], split_lines(TEXT_1))
1663
 
        target.insert_data_stream(source.get_data_stream(['text-a']))
1664
 
        # Insert 'text-b' into just the source.
1665
 
        source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
1666
 
        # Get a data stream of both text-a and text-b, and insert it.
1667
 
        data_stream = source.get_data_stream(['text-a', 'text-b'])
1668
 
        target.insert_data_stream(data_stream)
1669
 
        # The source and target will now be identical.  This means the text-a
1670
 
        # record was not added a second time.
1671
 
        self.assertKnitFilesEqual(source, target)
1672
 
 
1673
 
    def test_insert_data_stream_multiple_records(self):
1674
 
        """Inserting a data stream of all records from a knit with multiple
1675
 
        records results in byte-identical files.
1676
 
        """
1677
 
        source = self.make_test_knit(name='source')
1678
 
        source.add_lines('text-a', [], split_lines(TEXT_1))
1679
 
        source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
1680
 
        source.add_lines('text-c', [], split_lines(TEXT_1))
1681
 
        data_stream = source.get_data_stream(['text-a', 'text-b', 'text-c'])
1682
 
        
1683
 
        target = self.make_test_knit(name='target')
1684
 
        target.insert_data_stream(data_stream)
1685
 
        
1686
 
        self.assertKnitFilesEqual(source, target)
1687
 
 
1688
 
    def test_insert_data_stream_ghost_parent(self):
1689
 
        """Insert a data stream with a record that has a ghost parent."""
1690
 
        # Make a knit with a record, text-a, that has a ghost parent.
1691
 
        source = self.make_test_knit(name='source')
1692
 
        source.add_lines_with_ghosts('text-a', ['text-ghost'],
1693
 
                                     split_lines(TEXT_1))
1694
 
        data_stream = source.get_data_stream(['text-a'])
1695
 
 
1696
 
        target = self.make_test_knit(name='target')
1697
 
        target.insert_data_stream(data_stream)
1698
 
 
1699
 
        self.assertKnitFilesEqual(source, target)
1700
 
 
1701
 
        # The target knit object is in a consistent state, i.e. the record we
1702
 
        # just added is immediately visible.
1703
 
        self.assertTrue(target.has_version('text-a'))
1704
 
        self.assertTrue(target.has_ghost('text-ghost'))
1705
 
        self.assertEqual(split_lines(TEXT_1), target.get_lines('text-a'))
1706
 
 
1707
 
    def test_insert_data_stream_inconsistent_version_lines(self):
1708
 
        """Inserting a data stream which has different content for a version_id
1709
 
        than already exists in the knit will raise KnitCorrupt.
1710
 
        """
1711
 
        source = self.make_test_knit(name='source')
1712
 
        target = self.make_test_knit(name='target')
1713
 
        # Insert a different 'text-a' into both source and target
1714
 
        source.add_lines('text-a', [], split_lines(TEXT_1))
1715
 
        target.add_lines('text-a', [], split_lines(TEXT_2))
1716
 
        # Insert a data stream with conflicting content into the target
1717
 
        data_stream = source.get_data_stream(['text-a'])
1718
 
        self.assertRaises(
1719
 
            errors.KnitCorrupt, target.insert_data_stream, data_stream)
1720
 
 
1721
 
    def test_insert_data_stream_inconsistent_version_parents(self):
1722
 
        """Inserting a data stream which has different parents for a version_id
1723
 
        than already exists in the knit will raise KnitCorrupt.
1724
 
        """
1725
 
        source = self.make_test_knit(name='source')
1726
 
        target = self.make_test_knit(name='target')
1727
 
        # Insert a different 'text-a' into both source and target.  They differ
1728
 
        # only by the parents list, the content is the same.
1729
 
        source.add_lines_with_ghosts('text-a', [], split_lines(TEXT_1))
1730
 
        target.add_lines_with_ghosts('text-a', ['a-ghost'], split_lines(TEXT_1))
1731
 
        # Insert a data stream with conflicting content into the target
1732
 
        data_stream = source.get_data_stream(['text-a'])
1733
 
        self.assertRaises(
1734
 
            errors.KnitCorrupt, target.insert_data_stream, data_stream)
1735
 
 
1736
 
    def test_insert_data_stream_incompatible_format(self):
1737
 
        """A data stream in a different format to the target knit cannot be
1738
 
        inserted.
1739
 
 
1740
 
        It will raise KnitDataStreamIncompatible.
1741
 
        """
1742
 
        data_stream = ('fake-format-signature', [], lambda _: '')
1743
 
        target = self.make_test_knit(name='target')
1744
 
        self.assertRaises(
1745
 
            errors.KnitDataStreamIncompatible,
1746
 
            target.insert_data_stream, data_stream)
1747
 
 
1748
 
    #  * test that a stream of "already present version, then new version"
1749
 
    #    inserts correctly.
1750
 
 
1751
 
TEXT_1 = """\
1752
 
Banana cup cakes:
1753
 
 
1754
 
- bananas
1755
 
- eggs
1756
 
- broken tea cups
1757
 
"""
1758
 
 
1759
 
TEXT_1A = """\
1760
 
Banana cup cake recipe
1761
 
(serves 6)
1762
 
 
1763
 
- bananas
1764
 
- eggs
1765
 
- broken tea cups
1766
 
- self-raising flour
1767
 
"""
1768
 
 
1769
 
TEXT_1B = """\
1770
 
Banana cup cake recipe
1771
 
 
1772
 
- bananas (do not use plantains!!!)
1773
 
- broken tea cups
1774
 
- flour
1775
 
"""
1776
 
 
1777
 
delta_1_1a = """\
1778
 
0,1,2
1779
 
Banana cup cake recipe
1780
 
(serves 6)
1781
 
5,5,1
1782
 
- self-raising flour
1783
 
"""
1784
 
 
1785
 
TEXT_2 = """\
1786
 
Boeuf bourguignon
1787
 
 
1788
 
- beef
1789
 
- red wine
1790
 
- small onions
1791
 
- carrot
1792
 
- mushrooms
1793
 
"""
1794
 
 
1795
 
AB_MERGE_TEXT="""unchanged|Banana cup cake recipe
1796
 
new-a|(serves 6)
1797
 
unchanged|
1798
 
killed-b|- bananas
1799
 
killed-b|- eggs
1800
 
new-b|- bananas (do not use plantains!!!)
1801
 
unchanged|- broken tea cups
1802
 
new-a|- self-raising flour
1803
 
new-b|- flour
1804
 
"""
1805
 
AB_MERGE=[tuple(l.split('|')) for l in AB_MERGE_TEXT.splitlines(True)]
1806
 
 
1807
 
 
1808
 
def line_delta(from_lines, to_lines):
1809
 
    """Generate line-based delta from one text to another"""
1810
 
    s = difflib.SequenceMatcher(None, from_lines, to_lines)
1811
 
    for op in s.get_opcodes():
1812
 
        if op[0] == 'equal':
1813
 
            continue
1814
 
        yield '%d,%d,%d\n' % (op[1], op[2], op[4]-op[3])
1815
 
        for i in range(op[3], op[4]):
1816
 
            yield to_lines[i]
1817
 
 
1818
 
 
1819
 
def apply_line_delta(basis_lines, delta_lines):
1820
 
    """Apply a line-based perfect diff
1821
 
    
1822
 
    basis_lines -- text to apply the patch to
1823
 
    delta_lines -- diff instructions and content
1824
 
    """
1825
 
    out = basis_lines[:]
1826
 
    i = 0
1827
 
    offset = 0
1828
 
    while i < len(delta_lines):
1829
 
        l = delta_lines[i]
1830
 
        a, b, c = map(long, l.split(','))
1831
 
        i = i + 1
1832
 
        out[offset+a:offset+b] = delta_lines[i:i+c]
1833
 
        i = i + c
1834
 
        offset = offset + (b - a) + c
1835
 
    return out
1836
 
 
1837
 
 
1838
 
class TestWeaveToKnit(KnitTests):
1839
 
 
1840
 
    def test_weave_to_knit_matches(self):
1841
 
        # check that the WeaveToKnit is_compatible function
1842
 
        # registers True for a Weave to a Knit.
1843
 
        w = Weave()
1844
 
        k = self.make_test_knit()
1845
 
        self.failUnless(WeaveToKnit.is_compatible(w, k))
1846
 
        self.failIf(WeaveToKnit.is_compatible(k, w))
1847
 
        self.failIf(WeaveToKnit.is_compatible(w, w))
1848
 
        self.failIf(WeaveToKnit.is_compatible(k, k))
1849
 
 
1850
 
 
1851
 
class TestKnitCaching(KnitTests):
1852
 
    
1853
 
    def create_knit(self, cache_add=False):
1854
 
        k = self.make_test_knit(True)
1855
 
        if cache_add:
1856
 
            k.enable_cache()
1857
 
 
1858
 
        k.add_lines('text-1', [], split_lines(TEXT_1))
1859
 
        k.add_lines('text-2', [], split_lines(TEXT_2))
1860
 
        return k
1861
 
 
1862
 
    def test_no_caching(self):
1863
 
        k = self.create_knit()
1864
 
        # Nothing should be cached without setting 'enable_cache'
1865
 
        self.assertEqual({}, k._data._cache)
1866
 
 
1867
 
    def test_cache_add_and_clear(self):
1868
 
        k = self.create_knit(True)
1869
 
 
1870
 
        self.assertEqual(['text-1', 'text-2'], sorted(k._data._cache.keys()))
1871
 
 
1872
 
        k.clear_cache()
1873
 
        self.assertEqual({}, k._data._cache)
1874
 
 
1875
 
    def test_cache_data_read_raw(self):
1876
 
        k = self.create_knit()
1877
 
 
1878
 
        # Now cache and read
1879
 
        k.enable_cache()
1880
 
 
1881
 
        def read_one_raw(version):
1882
 
            pos_map = k._get_components_positions([version])
1883
 
            method, index_memo, next = pos_map[version]
1884
 
            lst = list(k._data.read_records_iter_raw([(version, index_memo)]))
1885
 
            self.assertEqual(1, len(lst))
1886
 
            return lst[0]
1887
 
 
1888
 
        val = read_one_raw('text-1')
1889
 
        self.assertEqual({'text-1':val[1]}, k._data._cache)
1890
 
 
1891
 
        k.clear_cache()
1892
 
        # After clear, new reads are not cached
1893
 
        self.assertEqual({}, k._data._cache)
1894
 
 
1895
 
        val2 = read_one_raw('text-1')
1896
 
        self.assertEqual(val, val2)
1897
 
        self.assertEqual({}, k._data._cache)
1898
 
 
1899
 
    def test_cache_data_read(self):
1900
 
        k = self.create_knit()
1901
 
 
1902
 
        def read_one(version):
1903
 
            pos_map = k._get_components_positions([version])
1904
 
            method, index_memo, next = pos_map[version]
1905
 
            lst = list(k._data.read_records_iter([(version, index_memo)]))
1906
 
            self.assertEqual(1, len(lst))
1907
 
            return lst[0]
1908
 
 
1909
 
        # Now cache and read
1910
 
        k.enable_cache()
1911
 
 
1912
 
        val = read_one('text-2')
1913
 
        self.assertEqual(['text-2'], k._data._cache.keys())
1914
 
        self.assertEqual('text-2', val[0])
1915
 
        content, digest = k._data._parse_record('text-2',
1916
 
                                                k._data._cache['text-2'])
1917
 
        self.assertEqual(content, val[1])
1918
 
        self.assertEqual(digest, val[2])
1919
 
 
1920
 
        k.clear_cache()
1921
 
        self.assertEqual({}, k._data._cache)
1922
 
 
1923
 
        val2 = read_one('text-2')
1924
 
        self.assertEqual(val, val2)
1925
 
        self.assertEqual({}, k._data._cache)
1926
 
 
1927
 
    def test_cache_read(self):
1928
 
        k = self.create_knit()
1929
 
        k.enable_cache()
1930
 
 
1931
 
        text = k.get_text('text-1')
1932
 
        self.assertEqual(TEXT_1, text)
1933
 
        self.assertEqual(['text-1'], k._data._cache.keys())
1934
 
 
1935
 
        k.clear_cache()
1936
 
        self.assertEqual({}, k._data._cache)
1937
 
 
1938
 
        text = k.get_text('text-1')
1939
 
        self.assertEqual(TEXT_1, text)
1940
 
        self.assertEqual({}, k._data._cache)
 
1490
    def make_test_knit(self, annotate=False, name='test'):
 
1491
        mapper = ConstantMapper(name)
 
1492
        return make_file_factory(annotate, mapper)(self.get_transport())
 
1493
 
 
1494
 
 
1495
class TestBadShaError(KnitTests):
 
1496
    """Tests for handling of sha errors."""
 
1497
 
 
1498
    def test_sha_exception_has_text(self):
 
1499
        # having the failed text included in the error allows for recovery.
 
1500
        source = self.make_test_knit()
 
1501
        target = self.make_test_knit(name="target")
 
1502
        if not source._max_delta_chain:
 
1503
            raise TestNotApplicable(
 
1504
                "cannot get delta-caused sha failures without deltas.")
 
1505
        # create a basis
 
1506
        basis = ('basis',)
 
1507
        broken = ('broken',)
 
1508
        source.add_lines(basis, (), ['foo\n'])
 
1509
        source.add_lines(broken, (basis,), ['foo\n', 'bar\n'])
 
1510
        # Seed target with a bad basis text
 
1511
        target.add_lines(basis, (), ['gam\n'])
 
1512
        target.insert_record_stream(
 
1513
            source.get_record_stream([broken], 'unordered', False))
 
1514
        err = self.assertRaises(errors.KnitCorrupt,
 
1515
            target.get_record_stream([broken], 'unordered', True
 
1516
            ).next().get_bytes_as, 'chunked')
 
1517
        self.assertEqual(['gam\n', 'bar\n'], err.content)
 
1518
        # Test for formatting with live data
 
1519
        self.assertStartsWith(str(err), "Knit ")
1941
1520
 
1942
1521
 
1943
1522
class TestKnitIndex(KnitTests):
1946
1525
        """Adding versions to the index should update the lookup dict"""
1947
1526
        knit = self.make_test_knit()
1948
1527
        idx = knit._index
1949
 
        idx.add_version('a-1', ['fulltext'], (None, 0, 0), [])
 
1528
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
1950
1529
        self.check_file_contents('test.kndx',
1951
1530
            '# bzr knit index 8\n'
1952
1531
            '\n'
1953
1532
            'a-1 fulltext 0 0  :'
1954
1533
            )
1955
 
        idx.add_versions([('a-2', ['fulltext'], (None, 0, 0), ['a-1']),
1956
 
                          ('a-3', ['fulltext'], (None, 0, 0), ['a-2']),
1957
 
                         ])
 
1534
        idx.add_records([
 
1535
            (('a-2',), ['fulltext'], (('a-2',), 0, 0), [('a-1',)]),
 
1536
            (('a-3',), ['fulltext'], (('a-3',), 0, 0), [('a-2',)]),
 
1537
            ])
1958
1538
        self.check_file_contents('test.kndx',
1959
1539
            '# bzr knit index 8\n'
1960
1540
            '\n'
1962
1542
            'a-2 fulltext 0 0 0 :\n'
1963
1543
            'a-3 fulltext 0 0 1 :'
1964
1544
            )
1965
 
        self.assertEqual(['a-1', 'a-2', 'a-3'], idx._history)
1966
 
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, [], 0),
1967
 
                          'a-2':('a-2', ['fulltext'], 0, 0, ['a-1'], 1),
1968
 
                          'a-3':('a-3', ['fulltext'], 0, 0, ['a-2'], 2),
1969
 
                         }, idx._cache)
 
1545
        self.assertEqual(set([('a-3',), ('a-1',), ('a-2',)]), idx.keys())
 
1546
        self.assertEqual({
 
1547
            ('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False)),
 
1548
            ('a-2',): ((('a-2',), 0, 0), None, (('a-1',),), ('fulltext', False)),
 
1549
            ('a-3',): ((('a-3',), 0, 0), None, (('a-2',),), ('fulltext', False)),
 
1550
            }, idx.get_build_details(idx.keys()))
 
1551
        self.assertEqual({('a-1',):(),
 
1552
            ('a-2',):(('a-1',),),
 
1553
            ('a-3',):(('a-2',),),},
 
1554
            idx.get_parent_map(idx.keys()))
1970
1555
 
1971
1556
    def test_add_versions_fails_clean(self):
1972
1557
        """If add_versions fails in the middle, it restores a pristine state.
1982
1567
 
1983
1568
        knit = self.make_test_knit()
1984
1569
        idx = knit._index
1985
 
        idx.add_version('a-1', ['fulltext'], (None, 0, 0), [])
 
1570
        idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
1986
1571
 
1987
1572
        class StopEarly(Exception):
1988
1573
            pass
1989
1574
 
1990
1575
        def generate_failure():
1991
1576
            """Add some entries and then raise an exception"""
1992
 
            yield ('a-2', ['fulltext'], (None, 0, 0), ['a-1'])
1993
 
            yield ('a-3', ['fulltext'], (None, 0, 0), ['a-2'])
 
1577
            yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
 
1578
            yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
1994
1579
            raise StopEarly()
1995
1580
 
1996
1581
        # Assert the pre-condition
1997
 
        self.assertEqual(['a-1'], idx._history)
1998
 
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, [], 0)}, idx._cache)
1999
 
 
2000
 
        self.assertRaises(StopEarly, idx.add_versions, generate_failure())
2001
 
 
 
1582
        def assertA1Only():
 
1583
            self.assertEqual(set([('a-1',)]), set(idx.keys()))
 
1584
            self.assertEqual(
 
1585
                {('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False))},
 
1586
                idx.get_build_details([('a-1',)]))
 
1587
            self.assertEqual({('a-1',):()}, idx.get_parent_map(idx.keys()))
 
1588
 
 
1589
        assertA1Only()
 
1590
        self.assertRaises(StopEarly, idx.add_records, generate_failure())
2002
1591
        # And it shouldn't be modified
2003
 
        self.assertEqual(['a-1'], idx._history)
2004
 
        self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, [], 0)}, idx._cache)
 
1592
        assertA1Only()
2005
1593
 
2006
1594
    def test_knit_index_ignores_empty_files(self):
2007
1595
        # There was a race condition in older bzr, where a ^C at the right time
2016
1604
    def test_knit_index_checks_header(self):
2017
1605
        t = get_transport('.')
2018
1606
        t.put_bytes('test.kndx', '# not really a knit header\n\n')
2019
 
 
2020
 
        self.assertRaises(KnitHeaderError, self.make_test_knit)
 
1607
        k = self.make_test_knit()
 
1608
        self.assertRaises(KnitHeaderError, k.keys)
2021
1609
 
2022
1610
 
2023
1611
class TestGraphIndexKnit(KnitTests):
2029
1617
            builder.add_node(node, references, value)
2030
1618
        stream = builder.finish()
2031
1619
        trans = self.get_transport()
2032
 
        trans.put_file(name, stream)
2033
 
        return GraphIndex(trans, name)
 
1620
        size = trans.put_file(name, stream)
 
1621
        return GraphIndex(trans, name, size)
2034
1622
 
2035
1623
    def two_graph_index(self, deltas=False, catch_adds=False):
2036
1624
        """Build a two-graph index.
2062
1650
            add_callback = self.catch_add
2063
1651
        else:
2064
1652
            add_callback = None
2065
 
        return KnitGraphIndex(combined_index, deltas=deltas,
 
1653
        return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
2066
1654
            add_callback=add_callback)
2067
1655
 
2068
 
    def test_get_graph(self):
2069
 
        index = self.two_graph_index()
2070
 
        self.assertEqual(set([
2071
 
            ('tip', ('parent', )),
2072
 
            ('tail', ()),
2073
 
            ('parent', ('tail', 'ghost')),
2074
 
            ('separate', ()),
2075
 
            ]), set(index.get_graph()))
2076
 
 
2077
 
    def test_get_ancestry(self):
2078
 
        # get_ancestry is defined as eliding ghosts, not erroring.
2079
 
        index = self.two_graph_index()
2080
 
        self.assertEqual([], index.get_ancestry([]))
2081
 
        self.assertEqual(['separate'], index.get_ancestry(['separate']))
2082
 
        self.assertEqual(['tail'], index.get_ancestry(['tail']))
2083
 
        self.assertEqual(['tail', 'parent'], index.get_ancestry(['parent']))
2084
 
        self.assertEqual(['tail', 'parent', 'tip'], index.get_ancestry(['tip']))
2085
 
        self.assertTrue(index.get_ancestry(['tip', 'separate']) in
2086
 
            (['tail', 'parent', 'tip', 'separate'],
2087
 
             ['separate', 'tail', 'parent', 'tip'],
2088
 
            ))
2089
 
        # and without topo_sort
2090
 
        self.assertEqual(set(['separate']),
2091
 
            set(index.get_ancestry(['separate'], topo_sorted=False)))
2092
 
        self.assertEqual(set(['tail']),
2093
 
            set(index.get_ancestry(['tail'], topo_sorted=False)))
2094
 
        self.assertEqual(set(['tail', 'parent']),
2095
 
            set(index.get_ancestry(['parent'], topo_sorted=False)))
2096
 
        self.assertEqual(set(['tail', 'parent', 'tip']),
2097
 
            set(index.get_ancestry(['tip'], topo_sorted=False)))
2098
 
        self.assertEqual(set(['separate', 'tail', 'parent', 'tip']),
2099
 
            set(index.get_ancestry(['tip', 'separate'])))
2100
 
        # asking for a ghost makes it go boom.
2101
 
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry, ['ghost'])
2102
 
 
2103
 
    def test_get_ancestry_with_ghosts(self):
2104
 
        index = self.two_graph_index()
2105
 
        self.assertEqual([], index.get_ancestry_with_ghosts([]))
2106
 
        self.assertEqual(['separate'], index.get_ancestry_with_ghosts(['separate']))
2107
 
        self.assertEqual(['tail'], index.get_ancestry_with_ghosts(['tail']))
2108
 
        self.assertTrue(index.get_ancestry_with_ghosts(['parent']) in
2109
 
            (['tail', 'ghost', 'parent'],
2110
 
             ['ghost', 'tail', 'parent'],
2111
 
            ))
2112
 
        self.assertTrue(index.get_ancestry_with_ghosts(['tip']) in
2113
 
            (['tail', 'ghost', 'parent', 'tip'],
2114
 
             ['ghost', 'tail', 'parent', 'tip'],
2115
 
            ))
2116
 
        self.assertTrue(index.get_ancestry_with_ghosts(['tip', 'separate']) in
2117
 
            (['tail', 'ghost', 'parent', 'tip', 'separate'],
2118
 
             ['ghost', 'tail', 'parent', 'tip', 'separate'],
2119
 
             ['separate', 'tail', 'ghost', 'parent', 'tip'],
2120
 
             ['separate', 'ghost', 'tail', 'parent', 'tip'],
2121
 
            ))
2122
 
        # asking for a ghost makes it go boom.
2123
 
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry_with_ghosts, ['ghost'])
2124
 
 
2125
 
    def test_num_versions(self):
2126
 
        index = self.two_graph_index()
2127
 
        self.assertEqual(4, index.num_versions())
2128
 
 
2129
 
    def test_get_versions(self):
2130
 
        index = self.two_graph_index()
2131
 
        self.assertEqual(set(['tail', 'tip', 'parent', 'separate']),
2132
 
            set(index.get_versions()))
2133
 
 
2134
 
    def test_has_version(self):
2135
 
        index = self.two_graph_index()
2136
 
        self.assertTrue(index.has_version('tail'))
2137
 
        self.assertFalse(index.has_version('ghost'))
 
1656
    def test_keys(self):
 
1657
        index = self.two_graph_index()
 
1658
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1659
            set(index.keys()))
2138
1660
 
2139
1661
    def test_get_position(self):
2140
1662
        index = self.two_graph_index()
2141
 
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position('tip'))
2142
 
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position('parent'))
 
1663
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position(('tip',)))
 
1664
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position(('parent',)))
2143
1665
 
2144
1666
    def test_get_method_deltas(self):
2145
1667
        index = self.two_graph_index(deltas=True)
2146
 
        self.assertEqual('fulltext', index.get_method('tip'))
2147
 
        self.assertEqual('line-delta', index.get_method('parent'))
 
1668
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1669
        self.assertEqual('line-delta', index.get_method(('parent',)))
2148
1670
 
2149
1671
    def test_get_method_no_deltas(self):
2150
1672
        # check that the parent-history lookup is ignored with deltas=False.
2151
1673
        index = self.two_graph_index(deltas=False)
2152
 
        self.assertEqual('fulltext', index.get_method('tip'))
2153
 
        self.assertEqual('fulltext', index.get_method('parent'))
 
1674
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1675
        self.assertEqual('fulltext', index.get_method(('parent',)))
2154
1676
 
2155
1677
    def test_get_options_deltas(self):
2156
1678
        index = self.two_graph_index(deltas=True)
2157
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
2158
 
        self.assertEqual(['line-delta'], index.get_options('parent'))
 
1679
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1680
        self.assertEqual(['line-delta'], index.get_options(('parent',)))
2159
1681
 
2160
1682
    def test_get_options_no_deltas(self):
2161
1683
        # check that the parent-history lookup is ignored with deltas=False.
2162
1684
        index = self.two_graph_index(deltas=False)
2163
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
2164
 
        self.assertEqual(['fulltext'], index.get_options('parent'))
2165
 
 
2166
 
    def test_get_parents(self):
2167
 
        # get_parents ignores ghosts
2168
 
        index = self.two_graph_index()
2169
 
        self.assertEqual(('tail', ), index.get_parents('parent'))
2170
 
        # and errors on ghosts.
2171
 
        self.assertRaises(errors.RevisionNotPresent,
2172
 
            index.get_parents, 'ghost')
2173
 
 
2174
 
    def test_get_parents_with_ghosts(self):
2175
 
        index = self.two_graph_index()
2176
 
        self.assertEqual(('tail', 'ghost'), index.get_parents_with_ghosts('parent'))
2177
 
        # and errors on ghosts.
2178
 
        self.assertRaises(errors.RevisionNotPresent,
2179
 
            index.get_parents_with_ghosts, 'ghost')
2180
 
 
2181
 
    def test_check_versions_present(self):
2182
 
        # ghosts should not be considered present
2183
 
        index = self.two_graph_index()
2184
 
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
2185
 
            ['ghost'])
2186
 
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
2187
 
            ['tail', 'ghost'])
2188
 
        index.check_versions_present(['tail', 'separate'])
 
1685
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1686
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1687
 
 
1688
    def test_get_parent_map(self):
 
1689
        index = self.two_graph_index()
 
1690
        self.assertEqual({('parent',):(('tail',), ('ghost',))},
 
1691
            index.get_parent_map([('parent',), ('ghost',)]))
2189
1692
 
2190
1693
    def catch_add(self, entries):
2191
1694
        self.caught_entries.append(entries)
2192
1695
 
2193
1696
    def test_add_no_callback_errors(self):
2194
1697
        index = self.two_graph_index()
2195
 
        self.assertRaises(errors.ReadOnlyError, index.add_version,
2196
 
            'new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
 
1698
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1699
            [(('new',), 'fulltext,no-eol', (None, 50, 60), ['separate'])])
2197
1700
 
2198
1701
    def test_add_version_smoke(self):
2199
1702
        index = self.two_graph_index(catch_adds=True)
2200
 
        index.add_version('new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
 
1703
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60),
 
1704
            [('separate',)])])
2201
1705
        self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
2202
1706
            self.caught_entries)
2203
1707
 
2204
1708
    def test_add_version_delta_not_delta_index(self):
2205
1709
        index = self.two_graph_index(catch_adds=True)
2206
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2207
 
            'new', 'no-eol,line-delta', (None, 0, 100), ['parent'])
 
1710
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1711
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
2208
1712
        self.assertEqual([], self.caught_entries)
2209
1713
 
2210
1714
    def test_add_version_same_dup(self):
2211
1715
        index = self.two_graph_index(catch_adds=True)
2212
1716
        # options can be spelt two different ways
2213
 
        index.add_version('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])
2214
 
        index.add_version('tip', 'no-eol,fulltext', (None, 0, 100), ['parent'])
2215
 
        # but neither should have added data.
2216
 
        self.assertEqual([[], []], self.caught_entries)
2217
 
        
 
1717
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
 
1718
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
 
1719
        # position/length are ignored (because each pack could have fulltext or
 
1720
        # delta, and be at a different position.
 
1721
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1722
            [('parent',)])])
 
1723
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1724
            [('parent',)])])
 
1725
        # but neither should have added data:
 
1726
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1727
 
2218
1728
    def test_add_version_different_dup(self):
2219
1729
        index = self.two_graph_index(deltas=True, catch_adds=True)
2220
1730
        # change options
2221
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2222
 
            'tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])
2223
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2224
 
            'tip', 'line-delta,no-eol', (None, 0, 100), ['parent'])
2225
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2226
 
            'tip', 'fulltext', (None, 0, 100), ['parent'])
2227
 
        # position/length
2228
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2229
 
            'tip', 'fulltext,no-eol', (None, 50, 100), ['parent'])
2230
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2231
 
            'tip', 'fulltext,no-eol', (None, 0, 1000), ['parent'])
 
1731
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1732
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1733
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1734
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
2232
1735
        # parents
2233
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2234
 
            'tip', 'fulltext,no-eol', (None, 0, 100), [])
 
1736
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1737
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
2235
1738
        self.assertEqual([], self.caught_entries)
2236
 
        
 
1739
 
2237
1740
    def test_add_versions_nodeltas(self):
2238
1741
        index = self.two_graph_index(catch_adds=True)
2239
 
        index.add_versions([
2240
 
                ('new', 'fulltext,no-eol', (None, 50, 60), ['separate']),
2241
 
                ('new2', 'fulltext', (None, 0, 6), ['new']),
 
1742
        index.add_records([
 
1743
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1744
                (('new2',), 'fulltext', (None, 0, 6), [('new',)]),
2242
1745
                ])
2243
1746
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
2244
1747
            (('new2', ), ' 0 6', ((('new',),),))],
2247
1750
 
2248
1751
    def test_add_versions_deltas(self):
2249
1752
        index = self.two_graph_index(deltas=True, catch_adds=True)
2250
 
        index.add_versions([
2251
 
                ('new', 'fulltext,no-eol', (None, 50, 60), ['separate']),
2252
 
                ('new2', 'line-delta', (None, 0, 6), ['new']),
 
1753
        index.add_records([
 
1754
                (('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
 
1755
                (('new2',), 'line-delta', (None, 0, 6), [('new',)]),
2253
1756
                ])
2254
1757
        self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
2255
1758
            (('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
2258
1761
 
2259
1762
    def test_add_versions_delta_not_delta_index(self):
2260
1763
        index = self.two_graph_index(catch_adds=True)
2261
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2262
 
            [('new', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
 
1764
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1765
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
2263
1766
        self.assertEqual([], self.caught_entries)
2264
1767
 
 
1768
    def test_add_versions_random_id_accepted(self):
 
1769
        index = self.two_graph_index(catch_adds=True)
 
1770
        index.add_records([], random_id=True)
 
1771
 
2265
1772
    def test_add_versions_same_dup(self):
2266
1773
        index = self.two_graph_index(catch_adds=True)
2267
1774
        # options can be spelt two different ways
2268
 
        index.add_versions([('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])])
2269
 
        index.add_versions([('tip', 'no-eol,fulltext', (None, 0, 100), ['parent'])])
 
1775
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100),
 
1776
            [('parent',)])])
 
1777
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100),
 
1778
            [('parent',)])])
 
1779
        # position/length are ignored (because each pack could have fulltext or
 
1780
        # delta, and be at a different position.
 
1781
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
 
1782
            [('parent',)])])
 
1783
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
 
1784
            [('parent',)])])
2270
1785
        # but neither should have added data.
2271
 
        self.assertEqual([[], []], self.caught_entries)
2272
 
        
 
1786
        self.assertEqual([[], [], [], []], self.caught_entries)
 
1787
 
2273
1788
    def test_add_versions_different_dup(self):
2274
1789
        index = self.two_graph_index(deltas=True, catch_adds=True)
2275
1790
        # change options
2276
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2277
 
            [('tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
2278
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2279
 
            [('tip', 'line-delta,no-eol', (None, 0, 100), ['parent'])])
2280
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2281
 
            [('tip', 'fulltext', (None, 0, 100), ['parent'])])
2282
 
        # position/length
2283
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2284
 
            [('tip', 'fulltext,no-eol', (None, 50, 100), ['parent'])])
2285
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2286
 
            [('tip', 'fulltext,no-eol', (None, 0, 1000), ['parent'])])
 
1791
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1792
            [(('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
 
1793
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1794
            [(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
2287
1795
        # parents
2288
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2289
 
            [('tip', 'fulltext,no-eol', (None, 0, 100), [])])
 
1796
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1797
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
2290
1798
        # change options in the second record
2291
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2292
 
            [('tip', 'fulltext,no-eol', (None, 0, 100), ['parent']),
2293
 
             ('tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
 
1799
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
1800
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)]),
 
1801
             (('tip',), 'line-delta', (None, 0, 100), [('parent',)])])
2294
1802
        self.assertEqual([], self.caught_entries)
2295
1803
 
2296
 
    def test_iter_parents(self):
2297
 
        index1 = self.make_g_index('1', 1, [
2298
 
        # no parents
2299
 
            (('r0', ), 'N0 100', ([], )),
2300
 
        # 1 parent
2301
 
            (('r1', ), '', ([('r0', )], ))])
2302
 
        index2 = self.make_g_index('2', 1, [
2303
 
        # 2 parents
2304
 
            (('r2', ), 'N0 100', ([('r1', ), ('r0', )], )),
2305
 
            ])
2306
 
        combined_index = CombinedGraphIndex([index1, index2])
2307
 
        index = KnitGraphIndex(combined_index)
2308
 
        # XXX TODO a ghost
2309
 
        # cases: each sample data individually:
2310
 
        self.assertEqual(set([('r0', ())]),
2311
 
            set(index.iter_parents(['r0'])))
2312
 
        self.assertEqual(set([('r1', ('r0', ))]),
2313
 
            set(index.iter_parents(['r1'])))
2314
 
        self.assertEqual(set([('r2', ('r1', 'r0'))]),
2315
 
            set(index.iter_parents(['r2'])))
2316
 
        # no nodes returned for a missing node
2317
 
        self.assertEqual(set(),
2318
 
            set(index.iter_parents(['missing'])))
2319
 
        # 1 node returned with missing nodes skipped
2320
 
        self.assertEqual(set([('r1', ('r0', ))]),
2321
 
            set(index.iter_parents(['ghost1', 'r1', 'ghost'])))
2322
 
        # 2 nodes returned
2323
 
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
2324
 
            set(index.iter_parents(['r0', 'r1'])))
2325
 
        # 2 nodes returned, missing skipped
2326
 
        self.assertEqual(set([('r0', ()), ('r1', ('r0', ))]),
2327
 
            set(index.iter_parents(['a', 'r0', 'b', 'r1', 'c'])))
 
1804
    def make_g_index_missing_compression_parent(self):
 
1805
        graph_index = self.make_g_index('missing_comp', 2,
 
1806
            [(('tip', ), ' 100 78',
 
1807
              ([('missing-parent', ), ('ghost', )], [('missing-parent', )]))])
 
1808
        return graph_index
 
1809
 
 
1810
    def make_g_index_missing_parent(self):
 
1811
        graph_index = self.make_g_index('missing_parent', 2,
 
1812
            [(('parent', ), ' 100 78', ([], [])),
 
1813
             (('tip', ), ' 100 78',
 
1814
              ([('parent', ), ('missing-parent', )], [('parent', )])),
 
1815
              ])
 
1816
        return graph_index
 
1817
 
 
1818
    def make_g_index_no_external_refs(self):
 
1819
        graph_index = self.make_g_index('no_external_refs', 2,
 
1820
            [(('rev', ), ' 100 78',
 
1821
              ([('parent', ), ('ghost', )], []))])
 
1822
        return graph_index
 
1823
 
 
1824
    def test_add_good_unvalidated_index(self):
 
1825
        unvalidated = self.make_g_index_no_external_refs()
 
1826
        combined = CombinedGraphIndex([unvalidated])
 
1827
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1828
        index.scan_unvalidated_index(unvalidated)
 
1829
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1830
 
 
1831
    def test_add_missing_compression_parent_unvalidated_index(self):
 
1832
        unvalidated = self.make_g_index_missing_compression_parent()
 
1833
        combined = CombinedGraphIndex([unvalidated])
 
1834
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1835
        index.scan_unvalidated_index(unvalidated)
 
1836
        # This also checks that its only the compression parent that is
 
1837
        # examined, otherwise 'ghost' would also be reported as a missing
 
1838
        # parent.
 
1839
        self.assertEqual(
 
1840
            frozenset([('missing-parent',)]),
 
1841
            index.get_missing_compression_parents())
 
1842
 
 
1843
    def test_add_missing_noncompression_parent_unvalidated_index(self):
 
1844
        unvalidated = self.make_g_index_missing_parent()
 
1845
        combined = CombinedGraphIndex([unvalidated])
 
1846
        index = _KnitGraphIndex(combined, lambda: True, deltas=True,
 
1847
            track_external_parent_refs=True)
 
1848
        index.scan_unvalidated_index(unvalidated)
 
1849
        self.assertEqual(
 
1850
            frozenset([('missing-parent',)]), index.get_missing_parents())
 
1851
 
 
1852
    def test_track_external_parent_refs(self):
 
1853
        g_index = self.make_g_index('empty', 2, [])
 
1854
        combined = CombinedGraphIndex([g_index])
 
1855
        index = _KnitGraphIndex(combined, lambda: True, deltas=True,
 
1856
            add_callback=self.catch_add, track_external_parent_refs=True)
 
1857
        self.caught_entries = []
 
1858
        index.add_records([
 
1859
            (('new-key',), 'fulltext,no-eol', (None, 50, 60),
 
1860
             [('parent-1',), ('parent-2',)])])
 
1861
        self.assertEqual(
 
1862
            frozenset([('parent-1',), ('parent-2',)]),
 
1863
            index.get_missing_parents())
 
1864
 
 
1865
    def test_add_unvalidated_index_with_present_external_references(self):
 
1866
        index = self.two_graph_index(deltas=True)
 
1867
        # Ugly hack to get at one of the underlying GraphIndex objects that
 
1868
        # two_graph_index built.
 
1869
        unvalidated = index._graph_index._indices[1]
 
1870
        # 'parent' is an external ref of _indices[1] (unvalidated), but is
 
1871
        # present in _indices[0].
 
1872
        index.scan_unvalidated_index(unvalidated)
 
1873
        self.assertEqual(frozenset(), index.get_missing_compression_parents())
 
1874
 
 
1875
    def make_new_missing_parent_g_index(self, name):
 
1876
        missing_parent = name + '-missing-parent'
 
1877
        graph_index = self.make_g_index(name, 2,
 
1878
            [((name + 'tip', ), ' 100 78',
 
1879
              ([(missing_parent, ), ('ghost', )], [(missing_parent, )]))])
 
1880
        return graph_index
 
1881
 
 
1882
    def test_add_mulitiple_unvalidated_indices_with_missing_parents(self):
 
1883
        g_index_1 = self.make_new_missing_parent_g_index('one')
 
1884
        g_index_2 = self.make_new_missing_parent_g_index('two')
 
1885
        combined = CombinedGraphIndex([g_index_1, g_index_2])
 
1886
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1887
        index.scan_unvalidated_index(g_index_1)
 
1888
        index.scan_unvalidated_index(g_index_2)
 
1889
        self.assertEqual(
 
1890
            frozenset([('one-missing-parent',), ('two-missing-parent',)]),
 
1891
            index.get_missing_compression_parents())
 
1892
 
 
1893
    def test_add_mulitiple_unvalidated_indices_with_mutual_dependencies(self):
 
1894
        graph_index_a = self.make_g_index('one', 2,
 
1895
            [(('parent-one', ), ' 100 78', ([('non-compression-parent',)], [])),
 
1896
             (('child-of-two', ), ' 100 78',
 
1897
              ([('parent-two',)], [('parent-two',)]))])
 
1898
        graph_index_b = self.make_g_index('two', 2,
 
1899
            [(('parent-two', ), ' 100 78', ([('non-compression-parent',)], [])),
 
1900
             (('child-of-one', ), ' 100 78',
 
1901
              ([('parent-one',)], [('parent-one',)]))])
 
1902
        combined = CombinedGraphIndex([graph_index_a, graph_index_b])
 
1903
        index = _KnitGraphIndex(combined, lambda: True, deltas=True)
 
1904
        index.scan_unvalidated_index(graph_index_a)
 
1905
        index.scan_unvalidated_index(graph_index_b)
 
1906
        self.assertEqual(
 
1907
            frozenset([]), index.get_missing_compression_parents())
2328
1908
 
2329
1909
 
2330
1910
class TestNoParentsGraphIndexKnit(KnitTests):
2331
 
    """Tests for knits using KnitGraphIndex with no parents."""
 
1911
    """Tests for knits using _KnitGraphIndex with no parents."""
2332
1912
 
2333
1913
    def make_g_index(self, name, ref_lists=0, nodes=[]):
2334
1914
        builder = GraphIndexBuilder(ref_lists)
2336
1916
            builder.add_node(node, references)
2337
1917
        stream = builder.finish()
2338
1918
        trans = self.get_transport()
2339
 
        trans.put_file(name, stream)
2340
 
        return GraphIndex(trans, name)
 
1919
        size = trans.put_file(name, stream)
 
1920
        return GraphIndex(trans, name, size)
 
1921
 
 
1922
    def test_add_good_unvalidated_index(self):
 
1923
        unvalidated = self.make_g_index('unvalidated')
 
1924
        combined = CombinedGraphIndex([unvalidated])
 
1925
        index = _KnitGraphIndex(combined, lambda: True, parents=False)
 
1926
        index.scan_unvalidated_index(unvalidated)
 
1927
        self.assertEqual(frozenset(),
 
1928
            index.get_missing_compression_parents())
2341
1929
 
2342
1930
    def test_parents_deltas_incompatible(self):
2343
1931
        index = CombinedGraphIndex([])
2344
 
        self.assertRaises(errors.KnitError, KnitGraphIndex, index,
2345
 
            deltas=True, parents=False)
 
1932
        self.assertRaises(errors.KnitError, _KnitGraphIndex, lambda:True,
 
1933
            index, deltas=True, parents=False)
2346
1934
 
2347
1935
    def two_graph_index(self, catch_adds=False):
2348
1936
        """Build a two-graph index.
2364
1952
            add_callback = self.catch_add
2365
1953
        else:
2366
1954
            add_callback = None
2367
 
        return KnitGraphIndex(combined_index, parents=False,
 
1955
        return _KnitGraphIndex(combined_index, lambda:True, parents=False,
2368
1956
            add_callback=add_callback)
2369
1957
 
2370
 
    def test_get_graph(self):
2371
 
        index = self.two_graph_index()
2372
 
        self.assertEqual(set([
2373
 
            ('tip', ()),
2374
 
            ('tail', ()),
2375
 
            ('parent', ()),
2376
 
            ('separate', ()),
2377
 
            ]), set(index.get_graph()))
2378
 
 
2379
 
    def test_get_ancestry(self):
2380
 
        # with no parents, ancestry is always just the key.
2381
 
        index = self.two_graph_index()
2382
 
        self.assertEqual([], index.get_ancestry([]))
2383
 
        self.assertEqual(['separate'], index.get_ancestry(['separate']))
2384
 
        self.assertEqual(['tail'], index.get_ancestry(['tail']))
2385
 
        self.assertEqual(['parent'], index.get_ancestry(['parent']))
2386
 
        self.assertEqual(['tip'], index.get_ancestry(['tip']))
2387
 
        self.assertTrue(index.get_ancestry(['tip', 'separate']) in
2388
 
            (['tip', 'separate'],
2389
 
             ['separate', 'tip'],
2390
 
            ))
2391
 
        # asking for a ghost makes it go boom.
2392
 
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry, ['ghost'])
2393
 
 
2394
 
    def test_get_ancestry_with_ghosts(self):
2395
 
        index = self.two_graph_index()
2396
 
        self.assertEqual([], index.get_ancestry_with_ghosts([]))
2397
 
        self.assertEqual(['separate'], index.get_ancestry_with_ghosts(['separate']))
2398
 
        self.assertEqual(['tail'], index.get_ancestry_with_ghosts(['tail']))
2399
 
        self.assertEqual(['parent'], index.get_ancestry_with_ghosts(['parent']))
2400
 
        self.assertEqual(['tip'], index.get_ancestry_with_ghosts(['tip']))
2401
 
        self.assertTrue(index.get_ancestry_with_ghosts(['tip', 'separate']) in
2402
 
            (['tip', 'separate'],
2403
 
             ['separate', 'tip'],
2404
 
            ))
2405
 
        # asking for a ghost makes it go boom.
2406
 
        self.assertRaises(errors.RevisionNotPresent, index.get_ancestry_with_ghosts, ['ghost'])
2407
 
 
2408
 
    def test_num_versions(self):
2409
 
        index = self.two_graph_index()
2410
 
        self.assertEqual(4, index.num_versions())
2411
 
 
2412
 
    def test_get_versions(self):
2413
 
        index = self.two_graph_index()
2414
 
        self.assertEqual(set(['tail', 'tip', 'parent', 'separate']),
2415
 
            set(index.get_versions()))
2416
 
 
2417
 
    def test_has_version(self):
2418
 
        index = self.two_graph_index()
2419
 
        self.assertTrue(index.has_version('tail'))
2420
 
        self.assertFalse(index.has_version('ghost'))
 
1958
    def test_keys(self):
 
1959
        index = self.two_graph_index()
 
1960
        self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
 
1961
            set(index.keys()))
2421
1962
 
2422
1963
    def test_get_position(self):
2423
1964
        index = self.two_graph_index()
2424
 
        self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position('tip'))
2425
 
        self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position('parent'))
 
1965
        self.assertEqual((index._graph_index._indices[0], 0, 100),
 
1966
            index.get_position(('tip',)))
 
1967
        self.assertEqual((index._graph_index._indices[1], 100, 78),
 
1968
            index.get_position(('parent',)))
2426
1969
 
2427
1970
    def test_get_method(self):
2428
1971
        index = self.two_graph_index()
2429
 
        self.assertEqual('fulltext', index.get_method('tip'))
2430
 
        self.assertEqual(['fulltext'], index.get_options('parent'))
 
1972
        self.assertEqual('fulltext', index.get_method(('tip',)))
 
1973
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
2431
1974
 
2432
1975
    def test_get_options(self):
2433
1976
        index = self.two_graph_index()
2434
 
        self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
2435
 
        self.assertEqual(['fulltext'], index.get_options('parent'))
2436
 
 
2437
 
    def test_get_parents(self):
2438
 
        index = self.two_graph_index()
2439
 
        self.assertEqual((), index.get_parents('parent'))
2440
 
        # and errors on ghosts.
2441
 
        self.assertRaises(errors.RevisionNotPresent,
2442
 
            index.get_parents, 'ghost')
2443
 
 
2444
 
    def test_get_parents_with_ghosts(self):
2445
 
        index = self.two_graph_index()
2446
 
        self.assertEqual((), index.get_parents_with_ghosts('parent'))
2447
 
        # and errors on ghosts.
2448
 
        self.assertRaises(errors.RevisionNotPresent,
2449
 
            index.get_parents_with_ghosts, 'ghost')
2450
 
 
2451
 
    def test_check_versions_present(self):
2452
 
        index = self.two_graph_index()
2453
 
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
2454
 
            ['missing'])
2455
 
        self.assertRaises(RevisionNotPresent, index.check_versions_present,
2456
 
            ['tail', 'missing'])
2457
 
        index.check_versions_present(['tail', 'separate'])
 
1977
        self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
 
1978
        self.assertEqual(['fulltext'], index.get_options(('parent',)))
 
1979
 
 
1980
    def test_get_parent_map(self):
 
1981
        index = self.two_graph_index()
 
1982
        self.assertEqual({('parent',):None},
 
1983
            index.get_parent_map([('parent',), ('ghost',)]))
2458
1984
 
2459
1985
    def catch_add(self, entries):
2460
1986
        self.caught_entries.append(entries)
2461
1987
 
2462
1988
    def test_add_no_callback_errors(self):
2463
1989
        index = self.two_graph_index()
2464
 
        self.assertRaises(errors.ReadOnlyError, index.add_version,
2465
 
            'new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
 
1990
        self.assertRaises(errors.ReadOnlyError, index.add_records,
 
1991
            [(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)])])
2466
1992
 
2467
1993
    def test_add_version_smoke(self):
2468
1994
        index = self.two_graph_index(catch_adds=True)
2469
 
        index.add_version('new', 'fulltext,no-eol', (None, 50, 60), [])
 
1995
        index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60), [])])
2470
1996
        self.assertEqual([[(('new', ), 'N50 60')]],
2471
1997
            self.caught_entries)
2472
1998
 
2473
1999
    def test_add_version_delta_not_delta_index(self):
2474
2000
        index = self.two_graph_index(catch_adds=True)
2475
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2476
 
            'new', 'no-eol,line-delta', (None, 0, 100), [])
 
2001
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2002
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [])])
2477
2003
        self.assertEqual([], self.caught_entries)
2478
2004
 
2479
2005
    def test_add_version_same_dup(self):
2480
2006
        index = self.two_graph_index(catch_adds=True)
2481
2007
        # options can be spelt two different ways
2482
 
        index.add_version('tip', 'fulltext,no-eol', (None, 0, 100), [])
2483
 
        index.add_version('tip', 'no-eol,fulltext', (None, 0, 100), [])
 
2008
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
2009
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
2010
        # position/length are ignored (because each pack could have fulltext or
 
2011
        # delta, and be at a different position.
 
2012
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
2013
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
2484
2014
        # but neither should have added data.
2485
 
        self.assertEqual([[], []], self.caught_entries)
2486
 
        
 
2015
        self.assertEqual([[], [], [], []], self.caught_entries)
 
2016
 
2487
2017
    def test_add_version_different_dup(self):
2488
2018
        index = self.two_graph_index(catch_adds=True)
2489
2019
        # change options
2490
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2491
 
            'tip', 'no-eol,line-delta', (None, 0, 100), [])
2492
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2493
 
            'tip', 'line-delta,no-eol', (None, 0, 100), [])
2494
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2495
 
            'tip', 'fulltext', (None, 0, 100), [])
2496
 
        # position/length
2497
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2498
 
            'tip', 'fulltext,no-eol', (None, 50, 100), [])
2499
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2500
 
            'tip', 'fulltext,no-eol', (None, 0, 1000), [])
 
2020
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2021
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
2022
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2023
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
2024
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2025
            [(('tip',), 'fulltext', (None, 0, 100), [])])
2501
2026
        # parents
2502
 
        self.assertRaises(errors.KnitCorrupt, index.add_version,
2503
 
            'tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])
 
2027
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2028
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
2504
2029
        self.assertEqual([], self.caught_entries)
2505
 
        
 
2030
 
2506
2031
    def test_add_versions(self):
2507
2032
        index = self.two_graph_index(catch_adds=True)
2508
 
        index.add_versions([
2509
 
                ('new', 'fulltext,no-eol', (None, 50, 60), []),
2510
 
                ('new2', 'fulltext', (None, 0, 6), []),
 
2033
        index.add_records([
 
2034
                (('new',), 'fulltext,no-eol', (None, 50, 60), []),
 
2035
                (('new2',), 'fulltext', (None, 0, 6), []),
2511
2036
                ])
2512
2037
        self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
2513
2038
            sorted(self.caught_entries[0]))
2515
2040
 
2516
2041
    def test_add_versions_delta_not_delta_index(self):
2517
2042
        index = self.two_graph_index(catch_adds=True)
2518
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2519
 
            [('new', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
 
2043
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2044
            [(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
2520
2045
        self.assertEqual([], self.caught_entries)
2521
2046
 
2522
2047
    def test_add_versions_parents_not_parents_index(self):
2523
2048
        index = self.two_graph_index(catch_adds=True)
2524
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2525
 
            [('new', 'no-eol,fulltext', (None, 0, 100), ['parent'])])
 
2049
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2050
            [(('new',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
2526
2051
        self.assertEqual([], self.caught_entries)
2527
2052
 
 
2053
    def test_add_versions_random_id_accepted(self):
 
2054
        index = self.two_graph_index(catch_adds=True)
 
2055
        index.add_records([], random_id=True)
 
2056
 
2528
2057
    def test_add_versions_same_dup(self):
2529
2058
        index = self.two_graph_index(catch_adds=True)
2530
2059
        # options can be spelt two different ways
2531
 
        index.add_versions([('tip', 'fulltext,no-eol', (None, 0, 100), [])])
2532
 
        index.add_versions([('tip', 'no-eol,fulltext', (None, 0, 100), [])])
 
2060
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
 
2061
        index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
 
2062
        # position/length are ignored (because each pack could have fulltext or
 
2063
        # delta, and be at a different position.
 
2064
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
 
2065
        index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
2533
2066
        # but neither should have added data.
2534
 
        self.assertEqual([[], []], self.caught_entries)
2535
 
        
 
2067
        self.assertEqual([[], [], [], []], self.caught_entries)
 
2068
 
2536
2069
    def test_add_versions_different_dup(self):
2537
2070
        index = self.two_graph_index(catch_adds=True)
2538
2071
        # change options
2539
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2540
 
            [('tip', 'no-eol,line-delta', (None, 0, 100), [])])
2541
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2542
 
            [('tip', 'line-delta,no-eol', (None, 0, 100), [])])
2543
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2544
 
            [('tip', 'fulltext', (None, 0, 100), [])])
2545
 
        # position/length
2546
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2547
 
            [('tip', 'fulltext,no-eol', (None, 50, 100), [])])
2548
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2549
 
            [('tip', 'fulltext,no-eol', (None, 0, 1000), [])])
 
2072
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2073
            [(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
 
2074
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2075
            [(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
 
2076
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2077
            [(('tip',), 'fulltext', (None, 0, 100), [])])
2550
2078
        # parents
2551
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2552
 
            [('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])])
 
2079
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2080
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
2553
2081
        # change options in the second record
2554
 
        self.assertRaises(errors.KnitCorrupt, index.add_versions,
2555
 
            [('tip', 'fulltext,no-eol', (None, 0, 100), []),
2556
 
             ('tip', 'no-eol,line-delta', (None, 0, 100), [])])
 
2082
        self.assertRaises(errors.KnitCorrupt, index.add_records,
 
2083
            [(('tip',), 'fulltext,no-eol', (None, 0, 100), []),
 
2084
             (('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
2557
2085
        self.assertEqual([], self.caught_entries)
2558
2086
 
2559
 
    def test_iter_parents(self):
2560
 
        index = self.two_graph_index()
2561
 
        self.assertEqual(set([
2562
 
            ('tip', ()), ('tail', ()), ('parent', ()), ('separate', ())
2563
 
            ]),
2564
 
            set(index.iter_parents(['tip', 'tail', 'ghost', 'parent', 'separate'])))
2565
 
        self.assertEqual(set([('tip', ())]),
2566
 
            set(index.iter_parents(['tip'])))
2567
 
        self.assertEqual(set(),
2568
 
            set(index.iter_parents([])))
 
2087
 
 
2088
class TestKnitVersionedFiles(KnitTests):
 
2089
 
 
2090
    def assertGroupKeysForIo(self, exp_groups, keys, non_local_keys,
 
2091
                             positions, _min_buffer_size=None):
 
2092
        kvf = self.make_test_knit()
 
2093
        if _min_buffer_size is None:
 
2094
            _min_buffer_size = knit._STREAM_MIN_BUFFER_SIZE
 
2095
        self.assertEqual(exp_groups, kvf._group_keys_for_io(keys,
 
2096
                                        non_local_keys, positions,
 
2097
                                        _min_buffer_size=_min_buffer_size))
 
2098
 
 
2099
    def assertSplitByPrefix(self, expected_map, expected_prefix_order,
 
2100
                            keys):
 
2101
        split, prefix_order = KnitVersionedFiles._split_by_prefix(keys)
 
2102
        self.assertEqual(expected_map, split)
 
2103
        self.assertEqual(expected_prefix_order, prefix_order)
 
2104
 
 
2105
    def test__group_keys_for_io(self):
 
2106
        ft_detail = ('fulltext', False)
 
2107
        ld_detail = ('line-delta', False)
 
2108
        f_a = ('f', 'a')
 
2109
        f_b = ('f', 'b')
 
2110
        f_c = ('f', 'c')
 
2111
        g_a = ('g', 'a')
 
2112
        g_b = ('g', 'b')
 
2113
        g_c = ('g', 'c')
 
2114
        positions = {
 
2115
            f_a: (ft_detail, (f_a, 0, 100), None),
 
2116
            f_b: (ld_detail, (f_b, 100, 21), f_a),
 
2117
            f_c: (ld_detail, (f_c, 180, 15), f_b),
 
2118
            g_a: (ft_detail, (g_a, 121, 35), None),
 
2119
            g_b: (ld_detail, (g_b, 156, 12), g_a),
 
2120
            g_c: (ld_detail, (g_c, 195, 13), g_a),
 
2121
            }
 
2122
        self.assertGroupKeysForIo([([f_a], set())],
 
2123
                                  [f_a], [], positions)
 
2124
        self.assertGroupKeysForIo([([f_a], set([f_a]))],
 
2125
                                  [f_a], [f_a], positions)
 
2126
        self.assertGroupKeysForIo([([f_a, f_b], set([]))],
 
2127
                                  [f_a, f_b], [], positions)
 
2128
        self.assertGroupKeysForIo([([f_a, f_b], set([f_b]))],
 
2129
                                  [f_a, f_b], [f_b], positions)
 
2130
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
2131
                                  [f_a, g_a, f_b, g_b], [], positions)
 
2132
        self.assertGroupKeysForIo([([f_a, f_b, g_a, g_b], set())],
 
2133
                                  [f_a, g_a, f_b, g_b], [], positions,
 
2134
                                  _min_buffer_size=150)
 
2135
        self.assertGroupKeysForIo([([f_a, f_b], set()), ([g_a, g_b], set())],
 
2136
                                  [f_a, g_a, f_b, g_b], [], positions,
 
2137
                                  _min_buffer_size=100)
 
2138
        self.assertGroupKeysForIo([([f_c], set()), ([g_b], set())],
 
2139
                                  [f_c, g_b], [], positions,
 
2140
                                  _min_buffer_size=125)
 
2141
        self.assertGroupKeysForIo([([g_b, f_c], set())],
 
2142
                                  [g_b, f_c], [], positions,
 
2143
                                  _min_buffer_size=125)
 
2144
 
 
2145
    def test__split_by_prefix(self):
 
2146
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2147
                                  'g': [('g', 'b'), ('g', 'a')],
 
2148
                                 }, ['f', 'g'],
 
2149
                                 [('f', 'a'), ('g', 'b'),
 
2150
                                  ('g', 'a'), ('f', 'b')])
 
2151
 
 
2152
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2153
                                  'g': [('g', 'b'), ('g', 'a')],
 
2154
                                 }, ['f', 'g'],
 
2155
                                 [('f', 'a'), ('f', 'b'),
 
2156
                                  ('g', 'b'), ('g', 'a')])
 
2157
 
 
2158
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2159
                                  'g': [('g', 'b'), ('g', 'a')],
 
2160
                                 }, ['f', 'g'],
 
2161
                                 [('f', 'a'), ('f', 'b'),
 
2162
                                  ('g', 'b'), ('g', 'a')])
 
2163
 
 
2164
        self.assertSplitByPrefix({'f': [('f', 'a'), ('f', 'b')],
 
2165
                                  'g': [('g', 'b'), ('g', 'a')],
 
2166
                                  '': [('a',), ('b',)]
 
2167
                                 }, ['f', 'g', ''],
 
2168
                                 [('f', 'a'), ('g', 'b'),
 
2169
                                  ('a',), ('b',),
 
2170
                                  ('g', 'a'), ('f', 'b')])
 
2171
 
 
2172
 
 
2173
class TestStacking(KnitTests):
 
2174
 
 
2175
    def get_basis_and_test_knit(self):
 
2176
        basis = self.make_test_knit(name='basis')
 
2177
        basis = RecordingVersionedFilesDecorator(basis)
 
2178
        test = self.make_test_knit(name='test')
 
2179
        test.add_fallback_versioned_files(basis)
 
2180
        return basis, test
 
2181
 
 
2182
    def test_add_fallback_versioned_files(self):
 
2183
        basis = self.make_test_knit(name='basis')
 
2184
        test = self.make_test_knit(name='test')
 
2185
        # It must not error; other tests test that the fallback is referred to
 
2186
        # when accessing data.
 
2187
        test.add_fallback_versioned_files(basis)
 
2188
 
 
2189
    def test_add_lines(self):
 
2190
        # lines added to the test are not added to the basis
 
2191
        basis, test = self.get_basis_and_test_knit()
 
2192
        key = ('foo',)
 
2193
        key_basis = ('bar',)
 
2194
        key_cross_border = ('quux',)
 
2195
        key_delta = ('zaphod',)
 
2196
        test.add_lines(key, (), ['foo\n'])
 
2197
        self.assertEqual({}, basis.get_parent_map([key]))
 
2198
        # lines added to the test that reference across the stack do a
 
2199
        # fulltext.
 
2200
        basis.add_lines(key_basis, (), ['foo\n'])
 
2201
        basis.calls = []
 
2202
        test.add_lines(key_cross_border, (key_basis,), ['foo\n'])
 
2203
        self.assertEqual('fulltext', test._index.get_method(key_cross_border))
 
2204
        # we don't even need to look at the basis to see that this should be
 
2205
        # stored as a fulltext
 
2206
        self.assertEqual([], basis.calls)
 
2207
        # Subsequent adds do delta.
 
2208
        basis.calls = []
 
2209
        test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
 
2210
        self.assertEqual('line-delta', test._index.get_method(key_delta))
 
2211
        self.assertEqual([], basis.calls)
 
2212
 
 
2213
    def test_annotate(self):
 
2214
        # annotations from the test knit are answered without asking the basis
 
2215
        basis, test = self.get_basis_and_test_knit()
 
2216
        key = ('foo',)
 
2217
        key_basis = ('bar',)
 
2218
        key_missing = ('missing',)
 
2219
        test.add_lines(key, (), ['foo\n'])
 
2220
        details = test.annotate(key)
 
2221
        self.assertEqual([(key, 'foo\n')], details)
 
2222
        self.assertEqual([], basis.calls)
 
2223
        # But texts that are not in the test knit are looked for in the basis
 
2224
        # directly.
 
2225
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2226
        basis.calls = []
 
2227
        details = test.annotate(key_basis)
 
2228
        self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
 
2229
        # Not optimised to date:
 
2230
        # self.assertEqual([("annotate", key_basis)], basis.calls)
 
2231
        self.assertEqual([('get_parent_map', set([key_basis])),
 
2232
            ('get_parent_map', set([key_basis])),
 
2233
            ('get_record_stream', [key_basis], 'topological', True)],
 
2234
            basis.calls)
 
2235
 
 
2236
    def test_check(self):
 
2237
        # At the moment checking a stacked knit does implicitly check the
 
2238
        # fallback files.
 
2239
        basis, test = self.get_basis_and_test_knit()
 
2240
        test.check()
 
2241
 
 
2242
    def test_get_parent_map(self):
 
2243
        # parents in the test knit are answered without asking the basis
 
2244
        basis, test = self.get_basis_and_test_knit()
 
2245
        key = ('foo',)
 
2246
        key_basis = ('bar',)
 
2247
        key_missing = ('missing',)
 
2248
        test.add_lines(key, (), [])
 
2249
        parent_map = test.get_parent_map([key])
 
2250
        self.assertEqual({key: ()}, parent_map)
 
2251
        self.assertEqual([], basis.calls)
 
2252
        # But parents that are not in the test knit are looked for in the basis
 
2253
        basis.add_lines(key_basis, (), [])
 
2254
        basis.calls = []
 
2255
        parent_map = test.get_parent_map([key, key_basis, key_missing])
 
2256
        self.assertEqual({key: (),
 
2257
            key_basis: ()}, parent_map)
 
2258
        self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
 
2259
            basis.calls)
 
2260
 
 
2261
    def test_get_record_stream_unordered_fulltexts(self):
 
2262
        # records from the test knit are answered without asking the basis:
 
2263
        basis, test = self.get_basis_and_test_knit()
 
2264
        key = ('foo',)
 
2265
        key_basis = ('bar',)
 
2266
        key_missing = ('missing',)
 
2267
        test.add_lines(key, (), ['foo\n'])
 
2268
        records = list(test.get_record_stream([key], 'unordered', True))
 
2269
        self.assertEqual(1, len(records))
 
2270
        self.assertEqual([], basis.calls)
 
2271
        # Missing (from test knit) objects are retrieved from the basis:
 
2272
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2273
        basis.calls = []
 
2274
        records = list(test.get_record_stream([key_basis, key_missing],
 
2275
            'unordered', True))
 
2276
        self.assertEqual(2, len(records))
 
2277
        calls = list(basis.calls)
 
2278
        for record in records:
 
2279
            self.assertSubset([record.key], (key_basis, key_missing))
 
2280
            if record.key == key_missing:
 
2281
                self.assertIsInstance(record, AbsentContentFactory)
 
2282
            else:
 
2283
                reference = list(basis.get_record_stream([key_basis],
 
2284
                    'unordered', True))[0]
 
2285
                self.assertEqual(reference.key, record.key)
 
2286
                self.assertEqual(reference.sha1, record.sha1)
 
2287
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2288
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2289
                    record.get_bytes_as(record.storage_kind))
 
2290
                self.assertEqual(reference.get_bytes_as('fulltext'),
 
2291
                    record.get_bytes_as('fulltext'))
 
2292
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2293
        # ask which fallbacks have which parents.
 
2294
        self.assertEqual([
 
2295
            ("get_parent_map", set([key_basis, key_missing])),
 
2296
            ("get_record_stream", [key_basis], 'unordered', True)],
 
2297
            calls)
 
2298
 
 
2299
    def test_get_record_stream_ordered_fulltexts(self):
 
2300
        # ordering is preserved down into the fallback store.
 
2301
        basis, test = self.get_basis_and_test_knit()
 
2302
        key = ('foo',)
 
2303
        key_basis = ('bar',)
 
2304
        key_basis_2 = ('quux',)
 
2305
        key_missing = ('missing',)
 
2306
        test.add_lines(key, (key_basis,), ['foo\n'])
 
2307
        # Missing (from test knit) objects are retrieved from the basis:
 
2308
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
2309
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
2310
        basis.calls = []
 
2311
        # ask for in non-topological order
 
2312
        records = list(test.get_record_stream(
 
2313
            [key, key_basis, key_missing, key_basis_2], 'topological', True))
 
2314
        self.assertEqual(4, len(records))
 
2315
        results = []
 
2316
        for record in records:
 
2317
            self.assertSubset([record.key],
 
2318
                (key_basis, key_missing, key_basis_2, key))
 
2319
            if record.key == key_missing:
 
2320
                self.assertIsInstance(record, AbsentContentFactory)
 
2321
            else:
 
2322
                results.append((record.key, record.sha1, record.storage_kind,
 
2323
                    record.get_bytes_as('fulltext')))
 
2324
        calls = list(basis.calls)
 
2325
        order = [record[0] for record in results]
 
2326
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2327
        for result in results:
 
2328
            if result[0] == key:
 
2329
                source = test
 
2330
            else:
 
2331
                source = basis
 
2332
            record = source.get_record_stream([result[0]], 'unordered',
 
2333
                True).next()
 
2334
            self.assertEqual(record.key, result[0])
 
2335
            self.assertEqual(record.sha1, result[1])
 
2336
            # We used to check that the storage kind matched, but actually it
 
2337
            # depends on whether it was sourced from the basis, or in a single
 
2338
            # group, because asking for full texts returns proxy objects to a
 
2339
            # _ContentMapGenerator object; so checking the kind is unneeded.
 
2340
            self.assertEqual(record.get_bytes_as('fulltext'), result[3])
 
2341
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2342
        # ask which fallbacks have which parents.
 
2343
        self.assertEqual([
 
2344
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
2345
            # topological is requested from the fallback, because that is what
 
2346
            # was requested at the top level.
 
2347
            ("get_record_stream", [key_basis_2, key_basis], 'topological', True)],
 
2348
            calls)
 
2349
 
 
2350
    def test_get_record_stream_unordered_deltas(self):
 
2351
        # records from the test knit are answered without asking the basis:
 
2352
        basis, test = self.get_basis_and_test_knit()
 
2353
        key = ('foo',)
 
2354
        key_basis = ('bar',)
 
2355
        key_missing = ('missing',)
 
2356
        test.add_lines(key, (), ['foo\n'])
 
2357
        records = list(test.get_record_stream([key], 'unordered', False))
 
2358
        self.assertEqual(1, len(records))
 
2359
        self.assertEqual([], basis.calls)
 
2360
        # Missing (from test knit) objects are retrieved from the basis:
 
2361
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2362
        basis.calls = []
 
2363
        records = list(test.get_record_stream([key_basis, key_missing],
 
2364
            'unordered', False))
 
2365
        self.assertEqual(2, len(records))
 
2366
        calls = list(basis.calls)
 
2367
        for record in records:
 
2368
            self.assertSubset([record.key], (key_basis, key_missing))
 
2369
            if record.key == key_missing:
 
2370
                self.assertIsInstance(record, AbsentContentFactory)
 
2371
            else:
 
2372
                reference = list(basis.get_record_stream([key_basis],
 
2373
                    'unordered', False))[0]
 
2374
                self.assertEqual(reference.key, record.key)
 
2375
                self.assertEqual(reference.sha1, record.sha1)
 
2376
                self.assertEqual(reference.storage_kind, record.storage_kind)
 
2377
                self.assertEqual(reference.get_bytes_as(reference.storage_kind),
 
2378
                    record.get_bytes_as(record.storage_kind))
 
2379
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2380
        # ask which fallbacks have which parents.
 
2381
        self.assertEqual([
 
2382
            ("get_parent_map", set([key_basis, key_missing])),
 
2383
            ("get_record_stream", [key_basis], 'unordered', False)],
 
2384
            calls)
 
2385
 
 
2386
    def test_get_record_stream_ordered_deltas(self):
 
2387
        # ordering is preserved down into the fallback store.
 
2388
        basis, test = self.get_basis_and_test_knit()
 
2389
        key = ('foo',)
 
2390
        key_basis = ('bar',)
 
2391
        key_basis_2 = ('quux',)
 
2392
        key_missing = ('missing',)
 
2393
        test.add_lines(key, (key_basis,), ['foo\n'])
 
2394
        # Missing (from test knit) objects are retrieved from the basis:
 
2395
        basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
 
2396
        basis.add_lines(key_basis_2, (), ['quux\n'])
 
2397
        basis.calls = []
 
2398
        # ask for in non-topological order
 
2399
        records = list(test.get_record_stream(
 
2400
            [key, key_basis, key_missing, key_basis_2], 'topological', False))
 
2401
        self.assertEqual(4, len(records))
 
2402
        results = []
 
2403
        for record in records:
 
2404
            self.assertSubset([record.key],
 
2405
                (key_basis, key_missing, key_basis_2, key))
 
2406
            if record.key == key_missing:
 
2407
                self.assertIsInstance(record, AbsentContentFactory)
 
2408
            else:
 
2409
                results.append((record.key, record.sha1, record.storage_kind,
 
2410
                    record.get_bytes_as(record.storage_kind)))
 
2411
        calls = list(basis.calls)
 
2412
        order = [record[0] for record in results]
 
2413
        self.assertEqual([key_basis_2, key_basis, key], order)
 
2414
        for result in results:
 
2415
            if result[0] == key:
 
2416
                source = test
 
2417
            else:
 
2418
                source = basis
 
2419
            record = source.get_record_stream([result[0]], 'unordered',
 
2420
                False).next()
 
2421
            self.assertEqual(record.key, result[0])
 
2422
            self.assertEqual(record.sha1, result[1])
 
2423
            self.assertEqual(record.storage_kind, result[2])
 
2424
            self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
 
2425
        # It's not strictly minimal, but it seems reasonable for now for it to
 
2426
        # ask which fallbacks have which parents.
 
2427
        self.assertEqual([
 
2428
            ("get_parent_map", set([key_basis, key_basis_2, key_missing])),
 
2429
            ("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
 
2430
            calls)
 
2431
 
 
2432
    def test_get_sha1s(self):
 
2433
        # sha1's in the test knit are answered without asking the basis
 
2434
        basis, test = self.get_basis_and_test_knit()
 
2435
        key = ('foo',)
 
2436
        key_basis = ('bar',)
 
2437
        key_missing = ('missing',)
 
2438
        test.add_lines(key, (), ['foo\n'])
 
2439
        key_sha1sum = osutils.sha('foo\n').hexdigest()
 
2440
        sha1s = test.get_sha1s([key])
 
2441
        self.assertEqual({key: key_sha1sum}, sha1s)
 
2442
        self.assertEqual([], basis.calls)
 
2443
        # But texts that are not in the test knit are looked for in the basis
 
2444
        # directly (rather than via text reconstruction) so that remote servers
 
2445
        # etc don't have to answer with full content.
 
2446
        basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
 
2447
        basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
 
2448
        basis.calls = []
 
2449
        sha1s = test.get_sha1s([key, key_missing, key_basis])
 
2450
        self.assertEqual({key: key_sha1sum,
 
2451
            key_basis: basis_sha1sum}, sha1s)
 
2452
        self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
 
2453
            basis.calls)
 
2454
 
 
2455
    def test_insert_record_stream(self):
 
2456
        # records are inserted as normal; insert_record_stream builds on
 
2457
        # add_lines, so a smoke test should be all that's needed:
 
2458
        key = ('foo',)
 
2459
        key_basis = ('bar',)
 
2460
        key_delta = ('zaphod',)
 
2461
        basis, test = self.get_basis_and_test_knit()
 
2462
        source = self.make_test_knit(name='source')
 
2463
        basis.add_lines(key_basis, (), ['foo\n'])
 
2464
        basis.calls = []
 
2465
        source.add_lines(key_basis, (), ['foo\n'])
 
2466
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2467
        stream = source.get_record_stream([key_delta], 'unordered', False)
 
2468
        test.insert_record_stream(stream)
 
2469
        # XXX: this does somewhat too many calls in making sure of whether it
 
2470
        # has to recreate the full text.
 
2471
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2472
             ('get_parent_map', set([key_basis])),
 
2473
             ('get_record_stream', [key_basis], 'unordered', True)],
 
2474
            basis.calls)
 
2475
        self.assertEqual({key_delta:(key_basis,)},
 
2476
            test.get_parent_map([key_delta]))
 
2477
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2478
            'unordered', True).next().get_bytes_as('fulltext'))
 
2479
 
 
2480
    def test_iter_lines_added_or_present_in_keys(self):
 
2481
        # Lines from the basis are returned, and lines for a given key are only
 
2482
        # returned once.
 
2483
        key1 = ('foo1',)
 
2484
        key2 = ('foo2',)
 
2485
        # all sources are asked for keys:
 
2486
        basis, test = self.get_basis_and_test_knit()
 
2487
        basis.add_lines(key1, (), ["foo"])
 
2488
        basis.calls = []
 
2489
        lines = list(test.iter_lines_added_or_present_in_keys([key1]))
 
2490
        self.assertEqual([("foo\n", key1)], lines)
 
2491
        self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
 
2492
            basis.calls)
 
2493
        # keys in both are not duplicated:
 
2494
        test.add_lines(key2, (), ["bar\n"])
 
2495
        basis.add_lines(key2, (), ["bar\n"])
 
2496
        basis.calls = []
 
2497
        lines = list(test.iter_lines_added_or_present_in_keys([key2]))
 
2498
        self.assertEqual([("bar\n", key2)], lines)
 
2499
        self.assertEqual([], basis.calls)
 
2500
 
 
2501
    def test_keys(self):
 
2502
        key1 = ('foo1',)
 
2503
        key2 = ('foo2',)
 
2504
        # all sources are asked for keys:
 
2505
        basis, test = self.get_basis_and_test_knit()
 
2506
        keys = test.keys()
 
2507
        self.assertEqual(set(), set(keys))
 
2508
        self.assertEqual([("keys",)], basis.calls)
 
2509
        # keys from a basis are returned:
 
2510
        basis.add_lines(key1, (), [])
 
2511
        basis.calls = []
 
2512
        keys = test.keys()
 
2513
        self.assertEqual(set([key1]), set(keys))
 
2514
        self.assertEqual([("keys",)], basis.calls)
 
2515
        # keys in both are not duplicated:
 
2516
        test.add_lines(key2, (), [])
 
2517
        basis.add_lines(key2, (), [])
 
2518
        basis.calls = []
 
2519
        keys = test.keys()
 
2520
        self.assertEqual(2, len(keys))
 
2521
        self.assertEqual(set([key1, key2]), set(keys))
 
2522
        self.assertEqual([("keys",)], basis.calls)
 
2523
 
 
2524
    def test_add_mpdiffs(self):
 
2525
        # records are inserted as normal; add_mpdiff builds on
 
2526
        # add_lines, so a smoke test should be all that's needed:
 
2527
        key = ('foo',)
 
2528
        key_basis = ('bar',)
 
2529
        key_delta = ('zaphod',)
 
2530
        basis, test = self.get_basis_and_test_knit()
 
2531
        source = self.make_test_knit(name='source')
 
2532
        basis.add_lines(key_basis, (), ['foo\n'])
 
2533
        basis.calls = []
 
2534
        source.add_lines(key_basis, (), ['foo\n'])
 
2535
        source.add_lines(key_delta, (key_basis,), ['bar\n'])
 
2536
        diffs = source.make_mpdiffs([key_delta])
 
2537
        test.add_mpdiffs([(key_delta, (key_basis,),
 
2538
            source.get_sha1s([key_delta])[key_delta], diffs[0])])
 
2539
        self.assertEqual([("get_parent_map", set([key_basis])),
 
2540
            ('get_record_stream', [key_basis], 'unordered', True),],
 
2541
            basis.calls)
 
2542
        self.assertEqual({key_delta:(key_basis,)},
 
2543
            test.get_parent_map([key_delta]))
 
2544
        self.assertEqual('bar\n', test.get_record_stream([key_delta],
 
2545
            'unordered', True).next().get_bytes_as('fulltext'))
 
2546
 
 
2547
    def test_make_mpdiffs(self):
 
2548
        # Generating an mpdiff across a stacking boundary should detect parent
 
2549
        # texts regions.
 
2550
        key = ('foo',)
 
2551
        key_left = ('bar',)
 
2552
        key_right = ('zaphod',)
 
2553
        basis, test = self.get_basis_and_test_knit()
 
2554
        basis.add_lines(key_left, (), ['bar\n'])
 
2555
        basis.add_lines(key_right, (), ['zaphod\n'])
 
2556
        basis.calls = []
 
2557
        test.add_lines(key, (key_left, key_right),
 
2558
            ['bar\n', 'foo\n', 'zaphod\n'])
 
2559
        diffs = test.make_mpdiffs([key])
 
2560
        self.assertEqual([
 
2561
            multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
 
2562
                multiparent.NewText(['foo\n']),
 
2563
                multiparent.ParentText(1, 0, 2, 1)])],
 
2564
            diffs)
 
2565
        self.assertEqual(3, len(basis.calls))
 
2566
        self.assertEqual([
 
2567
            ("get_parent_map", set([key_left, key_right])),
 
2568
            ("get_parent_map", set([key_left, key_right])),
 
2569
            ],
 
2570
            basis.calls[:-1])
 
2571
        last_call = basis.calls[-1]
 
2572
        self.assertEqual('get_record_stream', last_call[0])
 
2573
        self.assertEqual(set([key_left, key_right]), set(last_call[1]))
 
2574
        self.assertEqual('topological', last_call[2])
 
2575
        self.assertEqual(True, last_call[3])
 
2576
 
 
2577
 
 
2578
class TestNetworkBehaviour(KnitTests):
 
2579
    """Tests for getting data out of/into knits over the network."""
 
2580
 
 
2581
    def test_include_delta_closure_generates_a_knit_delta_closure(self):
 
2582
        vf = self.make_test_knit(name='test')
 
2583
        # put in three texts, giving ft, delta, delta
 
2584
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2585
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2586
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2587
        # But heuristics could interfere, so check what happened:
 
2588
        self.assertEqual(['knit-ft-gz', 'knit-delta-gz', 'knit-delta-gz'],
 
2589
            [record.storage_kind for record in
 
2590
             vf.get_record_stream([('base',), ('d1',), ('d2',)],
 
2591
                'topological', False)])
 
2592
        # generate a stream of just the deltas include_delta_closure=True,
 
2593
        # serialise to the network, and check that we get a delta closure on the wire.
 
2594
        stream = vf.get_record_stream([('d1',), ('d2',)], 'topological', True)
 
2595
        netb = [record.get_bytes_as(record.storage_kind) for record in stream]
 
2596
        # The first bytes should be a memo from _ContentMapGenerator, and the
 
2597
        # second bytes should be empty (because its a API proxy not something
 
2598
        # for wire serialisation.
 
2599
        self.assertEqual('', netb[1])
 
2600
        bytes = netb[0]
 
2601
        kind, line_end = network_bytes_to_kind_and_offset(bytes)
 
2602
        self.assertEqual('knit-delta-closure', kind)
 
2603
 
 
2604
 
 
2605
class TestContentMapGenerator(KnitTests):
 
2606
    """Tests for ContentMapGenerator"""
 
2607
 
 
2608
    def test_get_record_stream_gives_records(self):
 
2609
        vf = self.make_test_knit(name='test')
 
2610
        # put in three texts, giving ft, delta, delta
 
2611
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2612
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2613
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2614
        keys = [('d1',), ('d2',)]
 
2615
        generator = _VFContentMapGenerator(vf, keys,
 
2616
            global_map=vf.get_parent_map(keys))
 
2617
        for record in generator.get_record_stream():
 
2618
            if record.key == ('d1',):
 
2619
                self.assertEqual('d1\n', record.get_bytes_as('fulltext'))
 
2620
            else:
 
2621
                self.assertEqual('d2\n', record.get_bytes_as('fulltext'))
 
2622
 
 
2623
    def test_get_record_stream_kinds_are_raw(self):
 
2624
        vf = self.make_test_knit(name='test')
 
2625
        # put in three texts, giving ft, delta, delta
 
2626
        vf.add_lines(('base',), (), ['base\n', 'content\n'])
 
2627
        vf.add_lines(('d1',), (('base',),), ['d1\n'])
 
2628
        vf.add_lines(('d2',), (('d1',),), ['d2\n'])
 
2629
        keys = [('base',), ('d1',), ('d2',)]
 
2630
        generator = _VFContentMapGenerator(vf, keys,
 
2631
            global_map=vf.get_parent_map(keys))
 
2632
        kinds = {('base',): 'knit-delta-closure',
 
2633
            ('d1',): 'knit-delta-closure-ref',
 
2634
            ('d2',): 'knit-delta-closure-ref',
 
2635
            }
 
2636
        for record in generator.get_record_stream():
 
2637
            self.assertEqual(kinds[record.key], record.storage_kind)