1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
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
17
"""Tests for Knit data structure"""
19
from cStringIO import StringIO
32
from bzrlib.errors import (
33
RevisionAlreadyPresent,
38
from bzrlib.index import *
39
from bzrlib.knit import (
51
from bzrlib.tests import (
55
TestCaseWithMemoryTransport,
56
TestCaseWithTransport,
59
from bzrlib.transport import get_transport
60
from bzrlib.transport.memory import MemoryTransport
61
from bzrlib.tuned_gzip import GzipFile
62
from bzrlib.versionedfile import (
65
RecordingVersionedFilesDecorator,
69
class _CompiledKnitFeature(Feature):
73
import bzrlib._knit_load_data_c
78
def feature_name(self):
79
return 'bzrlib._knit_load_data_c'
81
CompiledKnitFeature = _CompiledKnitFeature()
84
class KnitContentTestsMixin(object):
86
def test_constructor(self):
87
content = self._make_content([])
90
content = self._make_content([])
91
self.assertEqual(content.text(), [])
93
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
94
self.assertEqual(content.text(), ["text1", "text2"])
97
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
99
self.assertIsInstance(copy, content.__class__)
100
self.assertEqual(copy.annotate(), content.annotate())
102
def assertDerivedBlocksEqual(self, source, target, noeol=False):
103
"""Assert that the derived matching blocks match real output"""
104
source_lines = source.splitlines(True)
105
target_lines = target.splitlines(True)
107
if noeol and not line.endswith('\n'):
111
source_content = self._make_content([(None, nl(l)) for l in source_lines])
112
target_content = self._make_content([(None, nl(l)) for l in target_lines])
113
line_delta = source_content.line_delta(target_content)
114
delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
115
source_lines, target_lines))
116
matcher = KnitSequenceMatcher(None, source_lines, target_lines)
117
matcher_blocks = list(list(matcher.get_matching_blocks()))
118
self.assertEqual(matcher_blocks, delta_blocks)
120
def test_get_line_delta_blocks(self):
121
self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
122
self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
123
self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
124
self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
125
self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
126
self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
127
self.assertDerivedBlocksEqual(TEXT_1A, '')
128
self.assertDerivedBlocksEqual('', TEXT_1A)
129
self.assertDerivedBlocksEqual('', '')
130
self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
132
def test_get_line_delta_blocks_noeol(self):
133
"""Handle historical knit deltas safely
135
Some existing knit deltas don't consider the last line to differ
136
when the only difference whether it has a final newline.
138
New knit deltas appear to always consider the last line to differ
141
self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
142
self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
143
self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
144
self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
156
Banana cup cake recipe
166
Banana cup cake recipe
168
- bananas (do not use plantains!!!)
175
Banana cup cake recipe
192
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
194
def _make_content(self, lines):
195
annotated_content = AnnotatedKnitContent(lines)
196
return PlainKnitContent(annotated_content.text(), 'bogus')
198
def test_annotate(self):
199
content = self._make_content([])
200
self.assertEqual(content.annotate(), [])
202
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
203
self.assertEqual(content.annotate(),
204
[("bogus", "text1"), ("bogus", "text2")])
206
def test_line_delta(self):
207
content1 = self._make_content([("", "a"), ("", "b")])
208
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
209
self.assertEqual(content1.line_delta(content2),
210
[(1, 2, 2, ["a", "c"])])
212
def test_line_delta_iter(self):
213
content1 = self._make_content([("", "a"), ("", "b")])
214
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
215
it = content1.line_delta_iter(content2)
216
self.assertEqual(it.next(), (1, 2, 2, ["a", "c"]))
217
self.assertRaises(StopIteration, it.next)
220
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
222
def _make_content(self, lines):
223
return AnnotatedKnitContent(lines)
225
def test_annotate(self):
226
content = self._make_content([])
227
self.assertEqual(content.annotate(), [])
229
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
230
self.assertEqual(content.annotate(),
231
[("origin1", "text1"), ("origin2", "text2")])
233
def test_line_delta(self):
234
content1 = self._make_content([("", "a"), ("", "b")])
235
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
236
self.assertEqual(content1.line_delta(content2),
237
[(1, 2, 2, [("", "a"), ("", "c")])])
239
def test_line_delta_iter(self):
240
content1 = self._make_content([("", "a"), ("", "b")])
241
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
242
it = content1.line_delta_iter(content2)
243
self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
244
self.assertRaises(StopIteration, it.next)
247
class MockTransport(object):
249
def __init__(self, file_lines=None):
250
self.file_lines = file_lines
252
# We have no base directory for the MockTransport
255
def get(self, filename):
256
if self.file_lines is None:
257
raise NoSuchFile(filename)
259
return StringIO("\n".join(self.file_lines))
261
def readv(self, relpath, offsets):
262
fp = self.get(relpath)
263
for offset, size in offsets:
265
yield offset, fp.read(size)
267
def __getattr__(self, name):
268
def queue_call(*args, **kwargs):
269
self.calls.append((name, args, kwargs))
273
class KnitRecordAccessTestsMixin(object):
274
"""Tests for getting and putting knit records."""
276
def test_add_raw_records(self):
277
"""Add_raw_records adds records retrievable later."""
278
access = self.get_access()
279
memos = access.add_raw_records([('key', 10)], '1234567890')
280
self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
282
def test_add_several_raw_records(self):
283
"""add_raw_records with many records and read some back."""
284
access = self.get_access()
285
memos = access.add_raw_records([('key', 10), ('key2', 2), ('key3', 5)],
287
self.assertEqual(['1234567890', '12', '34567'],
288
list(access.get_raw_records(memos)))
289
self.assertEqual(['1234567890'],
290
list(access.get_raw_records(memos[0:1])))
291
self.assertEqual(['12'],
292
list(access.get_raw_records(memos[1:2])))
293
self.assertEqual(['34567'],
294
list(access.get_raw_records(memos[2:3])))
295
self.assertEqual(['1234567890', '34567'],
296
list(access.get_raw_records(memos[0:1] + memos[2:3])))
299
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
300
"""Tests for the .kndx implementation."""
302
def get_access(self):
303
"""Get a .knit style access instance."""
304
mapper = ConstantMapper("foo")
305
access = _KnitKeyAccess(self.get_transport(), mapper)
309
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
310
"""Tests for the pack based access."""
312
def get_access(self):
313
return self._get_access()[0]
315
def _get_access(self, packname='packfile', index='FOO'):
316
transport = self.get_transport()
317
def write_data(bytes):
318
transport.append_bytes(packname, bytes)
319
writer = pack.ContainerWriter(write_data)
321
access = _DirectPackAccess({})
322
access.set_writer(writer, index, (transport, packname))
323
return access, writer
325
def test_read_from_several_packs(self):
326
access, writer = self._get_access()
328
memos.extend(access.add_raw_records([('key', 10)], '1234567890'))
330
access, writer = self._get_access('pack2', 'FOOBAR')
331
memos.extend(access.add_raw_records([('key', 5)], '12345'))
333
access, writer = self._get_access('pack3', 'BAZ')
334
memos.extend(access.add_raw_records([('key', 5)], 'alpha'))
336
transport = self.get_transport()
337
access = _DirectPackAccess({"FOO":(transport, 'packfile'),
338
"FOOBAR":(transport, 'pack2'),
339
"BAZ":(transport, 'pack3')})
340
self.assertEqual(['1234567890', '12345', 'alpha'],
341
list(access.get_raw_records(memos)))
342
self.assertEqual(['1234567890'],
343
list(access.get_raw_records(memos[0:1])))
344
self.assertEqual(['12345'],
345
list(access.get_raw_records(memos[1:2])))
346
self.assertEqual(['alpha'],
347
list(access.get_raw_records(memos[2:3])))
348
self.assertEqual(['1234567890', 'alpha'],
349
list(access.get_raw_records(memos[0:1] + memos[2:3])))
351
def test_set_writer(self):
352
"""The writer should be settable post construction."""
353
access = _DirectPackAccess({})
354
transport = self.get_transport()
355
packname = 'packfile'
357
def write_data(bytes):
358
transport.append_bytes(packname, bytes)
359
writer = pack.ContainerWriter(write_data)
361
access.set_writer(writer, index, (transport, packname))
362
memos = access.add_raw_records([('key', 10)], '1234567890')
364
self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
367
class LowLevelKnitDataTests(TestCase):
369
def create_gz_content(self, text):
371
gz_file = gzip.GzipFile(mode='wb', fileobj=sio)
374
return sio.getvalue()
376
def test_valid_knit_data(self):
377
sha1sum = osutils.sha('foo\nbar\n').hexdigest()
378
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
383
transport = MockTransport([gz_txt])
384
access = _KnitKeyAccess(transport, ConstantMapper('filename'))
385
knit = KnitVersionedFiles(None, access)
386
records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
388
contents = list(knit._read_records_iter(records))
389
self.assertEqual([(('rev-id-1',), ['foo\n', 'bar\n'],
390
'4e48e2c9a3d2ca8a708cb0cc545700544efb5021')], contents)
392
raw_contents = list(knit._read_records_iter_raw(records))
393
self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
395
def test_not_enough_lines(self):
396
sha1sum = osutils.sha('foo\n').hexdigest()
397
# record says 2 lines data says 1
398
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
402
transport = MockTransport([gz_txt])
403
access = _KnitKeyAccess(transport, ConstantMapper('filename'))
404
knit = KnitVersionedFiles(None, access)
405
records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
406
self.assertRaises(errors.KnitCorrupt, list,
407
knit._read_records_iter(records))
409
# read_records_iter_raw won't detect that sort of mismatch/corruption
410
raw_contents = list(knit._read_records_iter_raw(records))
411
self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
413
def test_too_many_lines(self):
414
sha1sum = osutils.sha('foo\nbar\n').hexdigest()
415
# record says 1 lines data says 2
416
gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
421
transport = MockTransport([gz_txt])
422
access = _KnitKeyAccess(transport, ConstantMapper('filename'))
423
knit = KnitVersionedFiles(None, access)
424
records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
425
self.assertRaises(errors.KnitCorrupt, list,
426
knit._read_records_iter(records))
428
# read_records_iter_raw won't detect that sort of mismatch/corruption
429
raw_contents = list(knit._read_records_iter_raw(records))
430
self.assertEqual([(('rev-id-1',), gz_txt, sha1sum)], raw_contents)
432
def test_mismatched_version_id(self):
433
sha1sum = osutils.sha('foo\nbar\n').hexdigest()
434
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
439
transport = MockTransport([gz_txt])
440
access = _KnitKeyAccess(transport, ConstantMapper('filename'))
441
knit = KnitVersionedFiles(None, access)
442
# We are asking for rev-id-2, but the data is rev-id-1
443
records = [(('rev-id-2',), (('rev-id-2',), 0, len(gz_txt)))]
444
self.assertRaises(errors.KnitCorrupt, list,
445
knit._read_records_iter(records))
447
# read_records_iter_raw detects mismatches in the header
448
self.assertRaises(errors.KnitCorrupt, list,
449
knit._read_records_iter_raw(records))
451
def test_uncompressed_data(self):
452
sha1sum = osutils.sha('foo\nbar\n').hexdigest()
453
txt = ('version rev-id-1 2 %s\n'
458
transport = MockTransport([txt])
459
access = _KnitKeyAccess(transport, ConstantMapper('filename'))
460
knit = KnitVersionedFiles(None, access)
461
records = [(('rev-id-1',), (('rev-id-1',), 0, len(txt)))]
463
# We don't have valid gzip data ==> corrupt
464
self.assertRaises(errors.KnitCorrupt, list,
465
knit._read_records_iter(records))
467
# read_records_iter_raw will notice the bad data
468
self.assertRaises(errors.KnitCorrupt, list,
469
knit._read_records_iter_raw(records))
471
def test_corrupted_data(self):
472
sha1sum = osutils.sha('foo\nbar\n').hexdigest()
473
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
478
# Change 2 bytes in the middle to \xff
479
gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
480
transport = MockTransport([gz_txt])
481
access = _KnitKeyAccess(transport, ConstantMapper('filename'))
482
knit = KnitVersionedFiles(None, access)
483
records = [(('rev-id-1',), (('rev-id-1',), 0, len(gz_txt)))]
484
self.assertRaises(errors.KnitCorrupt, list,
485
knit._read_records_iter(records))
486
# read_records_iter_raw will barf on bad gz data
487
self.assertRaises(errors.KnitCorrupt, list,
488
knit._read_records_iter_raw(records))
491
class LowLevelKnitIndexTests(TestCase):
493
def get_knit_index(self, transport, name, mode):
494
mapper = ConstantMapper(name)
495
orig = knit._load_data
497
knit._load_data = orig
498
self.addCleanup(reset)
499
from bzrlib._knit_load_data_py import _load_data_py
500
knit._load_data = _load_data_py
501
allow_writes = lambda: 'w' in mode
502
return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
504
def test_create_file(self):
505
transport = MockTransport()
506
index = self.get_knit_index(transport, "filename", "w")
508
call = transport.calls.pop(0)
509
# call[1][1] is a StringIO - we can't test it by simple equality.
510
self.assertEqual('put_file_non_atomic', call[0])
511
self.assertEqual('filename.kndx', call[1][0])
512
# With no history, _KndxIndex writes a new index:
513
self.assertEqual(_KndxIndex.HEADER,
514
call[1][1].getvalue())
515
self.assertEqual({'create_parent_dir': True}, call[2])
517
def test_read_utf8_version_id(self):
518
unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
519
utf8_revision_id = unicode_revision_id.encode('utf-8')
520
transport = MockTransport([
522
'%s option 0 1 :' % (utf8_revision_id,)
524
index = self.get_knit_index(transport, "filename", "r")
525
# _KndxIndex is a private class, and deals in utf8 revision_ids, not
526
# Unicode revision_ids.
527
self.assertEqual({(utf8_revision_id,):()},
528
index.get_parent_map(index.keys()))
529
self.assertFalse((unicode_revision_id,) in index.keys())
531
def test_read_utf8_parents(self):
532
unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
533
utf8_revision_id = unicode_revision_id.encode('utf-8')
534
transport = MockTransport([
536
"version option 0 1 .%s :" % (utf8_revision_id,)
538
index = self.get_knit_index(transport, "filename", "r")
539
self.assertEqual({("version",):((utf8_revision_id,),)},
540
index.get_parent_map(index.keys()))
542
def test_read_ignore_corrupted_lines(self):
543
transport = MockTransport([
546
"corrupted options 0 1 .b .c ",
547
"version options 0 1 :"
549
index = self.get_knit_index(transport, "filename", "r")
550
self.assertEqual(1, len(index.keys()))
551
self.assertEqual(set([("version",)]), index.keys())
553
def test_read_corrupted_header(self):
554
transport = MockTransport(['not a bzr knit index header\n'])
555
index = self.get_knit_index(transport, "filename", "r")
556
self.assertRaises(KnitHeaderError, index.keys)
558
def test_read_duplicate_entries(self):
559
transport = MockTransport([
561
"parent options 0 1 :",
562
"version options1 0 1 0 :",
563
"version options2 1 2 .other :",
564
"version options3 3 4 0 .other :"
566
index = self.get_knit_index(transport, "filename", "r")
567
self.assertEqual(2, len(index.keys()))
568
# check that the index used is the first one written. (Specific
569
# to KnitIndex style indices.
570
self.assertEqual("1", index._dictionary_compress([("version",)]))
571
self.assertEqual((("version",), 3, 4), index.get_position(("version",)))
572
self.assertEqual(["options3"], index.get_options(("version",)))
573
self.assertEqual({("version",):(("parent",), ("other",))},
574
index.get_parent_map([("version",)]))
576
def test_read_compressed_parents(self):
577
transport = MockTransport([
581
"c option 0 1 1 0 :",
583
index = self.get_knit_index(transport, "filename", "r")
584
self.assertEqual({("b",):(("a",),), ("c",):(("b",), ("a",))},
585
index.get_parent_map([("b",), ("c",)]))
587
def test_write_utf8_version_id(self):
588
unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
589
utf8_revision_id = unicode_revision_id.encode('utf-8')
590
transport = MockTransport([
593
index = self.get_knit_index(transport, "filename", "r")
595
((utf8_revision_id,), ["option"], ((utf8_revision_id,), 0, 1), [])])
596
call = transport.calls.pop(0)
597
# call[1][1] is a StringIO - we can't test it by simple equality.
598
self.assertEqual('put_file_non_atomic', call[0])
599
self.assertEqual('filename.kndx', call[1][0])
600
# With no history, _KndxIndex writes a new index:
601
self.assertEqual(_KndxIndex.HEADER +
602
"\n%s option 0 1 :" % (utf8_revision_id,),
603
call[1][1].getvalue())
604
self.assertEqual({'create_parent_dir': True}, call[2])
606
def test_write_utf8_parents(self):
607
unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
608
utf8_revision_id = unicode_revision_id.encode('utf-8')
609
transport = MockTransport([
612
index = self.get_knit_index(transport, "filename", "r")
614
(("version",), ["option"], (("version",), 0, 1), [(utf8_revision_id,)])])
615
call = transport.calls.pop(0)
616
# call[1][1] is a StringIO - we can't test it by simple equality.
617
self.assertEqual('put_file_non_atomic', call[0])
618
self.assertEqual('filename.kndx', call[1][0])
619
# With no history, _KndxIndex writes a new index:
620
self.assertEqual(_KndxIndex.HEADER +
621
"\nversion option 0 1 .%s :" % (utf8_revision_id,),
622
call[1][1].getvalue())
623
self.assertEqual({'create_parent_dir': True}, call[2])
626
transport = MockTransport([
629
index = self.get_knit_index(transport, "filename", "r")
631
self.assertEqual(set(), index.keys())
633
index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
634
self.assertEqual(set([("a",)]), index.keys())
636
index.add_records([(("a",), ["option"], (("a",), 0, 1), [])])
637
self.assertEqual(set([("a",)]), index.keys())
639
index.add_records([(("b",), ["option"], (("b",), 0, 1), [])])
640
self.assertEqual(set([("a",), ("b",)]), index.keys())
642
def add_a_b(self, index, random_id=None):
644
if random_id is not None:
645
kwargs["random_id"] = random_id
647
(("a",), ["option"], (("a",), 0, 1), [("b",)]),
648
(("a",), ["opt"], (("a",), 1, 2), [("c",)]),
649
(("b",), ["option"], (("b",), 2, 3), [("a",)])
652
def assertIndexIsAB(self, index):
657
index.get_parent_map(index.keys()))
658
self.assertEqual((("a",), 1, 2), index.get_position(("a",)))
659
self.assertEqual((("b",), 2, 3), index.get_position(("b",)))
660
self.assertEqual(["opt"], index.get_options(("a",)))
662
def test_add_versions(self):
663
transport = MockTransport([
666
index = self.get_knit_index(transport, "filename", "r")
669
call = transport.calls.pop(0)
670
# call[1][1] is a StringIO - we can't test it by simple equality.
671
self.assertEqual('put_file_non_atomic', call[0])
672
self.assertEqual('filename.kndx', call[1][0])
673
# With no history, _KndxIndex writes a new index:
676
"\na option 0 1 .b :"
678
"\nb option 2 3 0 :",
679
call[1][1].getvalue())
680
self.assertEqual({'create_parent_dir': True}, call[2])
681
self.assertIndexIsAB(index)
683
def test_add_versions_random_id_is_accepted(self):
684
transport = MockTransport([
687
index = self.get_knit_index(transport, "filename", "r")
688
self.add_a_b(index, random_id=True)
690
def test_delay_create_and_add_versions(self):
691
transport = MockTransport()
693
index = self.get_knit_index(transport, "filename", "w")
695
self.assertEqual([], transport.calls)
698
#[ {"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
700
# Two calls: one during which we load the existing index (and when its
701
# missing create it), then a second where we write the contents out.
702
self.assertEqual(2, len(transport.calls))
703
call = transport.calls.pop(0)
704
self.assertEqual('put_file_non_atomic', call[0])
705
self.assertEqual('filename.kndx', call[1][0])
706
# With no history, _KndxIndex writes a new index:
707
self.assertEqual(_KndxIndex.HEADER, call[1][1].getvalue())
708
self.assertEqual({'create_parent_dir': True}, call[2])
709
call = transport.calls.pop(0)
710
# call[1][1] is a StringIO - we can't test it by simple equality.
711
self.assertEqual('put_file_non_atomic', call[0])
712
self.assertEqual('filename.kndx', call[1][0])
713
# With no history, _KndxIndex writes a new index:
716
"\na option 0 1 .b :"
718
"\nb option 2 3 0 :",
719
call[1][1].getvalue())
720
self.assertEqual({'create_parent_dir': True}, call[2])
722
def test_get_position(self):
723
transport = MockTransport([
728
index = self.get_knit_index(transport, "filename", "r")
730
self.assertEqual((("a",), 0, 1), index.get_position(("a",)))
731
self.assertEqual((("b",), 1, 2), index.get_position(("b",)))
733
def test_get_method(self):
734
transport = MockTransport([
736
"a fulltext,unknown 0 1 :",
737
"b unknown,line-delta 1 2 :",
740
index = self.get_knit_index(transport, "filename", "r")
742
self.assertEqual("fulltext", index.get_method("a"))
743
self.assertEqual("line-delta", index.get_method("b"))
744
self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
746
def test_get_options(self):
747
transport = MockTransport([
752
index = self.get_knit_index(transport, "filename", "r")
754
self.assertEqual(["opt1"], index.get_options("a"))
755
self.assertEqual(["opt2", "opt3"], index.get_options("b"))
757
def test_get_parent_map(self):
758
transport = MockTransport([
761
"b option 1 2 0 .c :",
762
"c option 1 2 1 0 .e :"
764
index = self.get_knit_index(transport, "filename", "r")
768
("b",):(("a",), ("c",)),
769
("c",):(("b",), ("a",), ("e",)),
770
}, index.get_parent_map(index.keys()))
772
def test_impossible_parent(self):
773
"""Test we get KnitCorrupt if the parent couldn't possibly exist."""
774
transport = MockTransport([
777
"b option 0 1 4 :" # We don't have a 4th record
779
index = self.get_knit_index(transport, 'filename', 'r')
781
self.assertRaises(errors.KnitCorrupt, index.keys)
783
if (str(e) == ('exceptions must be strings, classes, or instances,'
784
' not exceptions.IndexError')
785
and sys.version_info[0:2] >= (2,5)):
786
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
787
' raising new style exceptions with python'
792
def test_corrupted_parent(self):
793
transport = MockTransport([
797
"c option 0 1 1v :", # Can't have a parent of '1v'
799
index = self.get_knit_index(transport, 'filename', 'r')
801
self.assertRaises(errors.KnitCorrupt, index.keys)
803
if (str(e) == ('exceptions must be strings, classes, or instances,'
804
' not exceptions.ValueError')
805
and sys.version_info[0:2] >= (2,5)):
806
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
807
' raising new style exceptions with python'
812
def test_corrupted_parent_in_list(self):
813
transport = MockTransport([
817
"c option 0 1 1 v :", # Can't have a parent of 'v'
819
index = self.get_knit_index(transport, 'filename', 'r')
821
self.assertRaises(errors.KnitCorrupt, index.keys)
823
if (str(e) == ('exceptions must be strings, classes, or instances,'
824
' not exceptions.ValueError')
825
and sys.version_info[0:2] >= (2,5)):
826
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
827
' raising new style exceptions with python'
832
def test_invalid_position(self):
833
transport = MockTransport([
837
index = self.get_knit_index(transport, 'filename', 'r')
839
self.assertRaises(errors.KnitCorrupt, index.keys)
841
if (str(e) == ('exceptions must be strings, classes, or instances,'
842
' not exceptions.ValueError')
843
and sys.version_info[0:2] >= (2,5)):
844
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
845
' raising new style exceptions with python'
850
def test_invalid_size(self):
851
transport = MockTransport([
855
index = self.get_knit_index(transport, 'filename', 'r')
857
self.assertRaises(errors.KnitCorrupt, index.keys)
859
if (str(e) == ('exceptions must be strings, classes, or instances,'
860
' not exceptions.ValueError')
861
and sys.version_info[0:2] >= (2,5)):
862
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
863
' raising new style exceptions with python'
868
def test_short_line(self):
869
transport = MockTransport([
872
"b option 10 10 0", # This line isn't terminated, ignored
874
index = self.get_knit_index(transport, "filename", "r")
875
self.assertEqual(set([('a',)]), index.keys())
877
def test_skip_incomplete_record(self):
878
# A line with bogus data should just be skipped
879
transport = MockTransport([
882
"b option 10 10 0", # This line isn't terminated, ignored
883
"c option 20 10 0 :", # Properly terminated, and starts with '\n'
885
index = self.get_knit_index(transport, "filename", "r")
886
self.assertEqual(set([('a',), ('c',)]), index.keys())
888
def test_trailing_characters(self):
889
# A line with bogus data should just be skipped
890
transport = MockTransport([
893
"b option 10 10 0 :a", # This line has extra trailing characters
894
"c option 20 10 0 :", # Properly terminated, and starts with '\n'
896
index = self.get_knit_index(transport, "filename", "r")
897
self.assertEqual(set([('a',), ('c',)]), index.keys())
900
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
902
_test_needs_features = [CompiledKnitFeature]
904
def get_knit_index(self, transport, name, mode):
905
mapper = ConstantMapper(name)
906
orig = knit._load_data
908
knit._load_data = orig
909
self.addCleanup(reset)
910
from bzrlib._knit_load_data_c import _load_data_c
911
knit._load_data = _load_data_c
912
allow_writes = lambda: mode == 'w'
913
return _KndxIndex(transport, mapper, lambda:None, allow_writes, lambda:True)
916
class KnitTests(TestCaseWithTransport):
917
"""Class containing knit test helper routines."""
919
def make_test_knit(self, annotate=False, name='test'):
920
mapper = ConstantMapper(name)
921
return make_file_factory(annotate, mapper)(self.get_transport())
924
class TestBadShaError(KnitTests):
925
"""Tests for handling of sha errors."""
927
def test_exception_has_text(self):
928
# having the failed text included in the error allows for recovery.
929
source = self.make_test_knit()
930
target = self.make_test_knit(name="target")
931
if not source._max_delta_chain:
932
raise TestNotApplicable(
933
"cannot get delta-caused sha failures without deltas.")
937
source.add_lines(basis, (), ['foo\n'])
938
source.add_lines(broken, (basis,), ['foo\n', 'bar\n'])
939
# Seed target with a bad basis text
940
target.add_lines(basis, (), ['gam\n'])
941
target.insert_record_stream(
942
source.get_record_stream([broken], 'unordered', False))
943
err = self.assertRaises(errors.KnitCorrupt,
944
target.get_record_stream([broken], 'unordered', True).next)
945
self.assertEqual(['gam\n', 'bar\n'], err.content)
946
# Test for formatting with live data
947
self.assertStartsWith(str(err), "Knit ")
950
class TestKnitIndex(KnitTests):
952
def test_add_versions_dictionary_compresses(self):
953
"""Adding versions to the index should update the lookup dict"""
954
knit = self.make_test_knit()
956
idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
957
self.check_file_contents('test.kndx',
958
'# bzr knit index 8\n'
963
(('a-2',), ['fulltext'], (('a-2',), 0, 0), [('a-1',)]),
964
(('a-3',), ['fulltext'], (('a-3',), 0, 0), [('a-2',)]),
966
self.check_file_contents('test.kndx',
967
'# bzr knit index 8\n'
969
'a-1 fulltext 0 0 :\n'
970
'a-2 fulltext 0 0 0 :\n'
971
'a-3 fulltext 0 0 1 :'
973
self.assertEqual(set([('a-3',), ('a-1',), ('a-2',)]), idx.keys())
975
('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False)),
976
('a-2',): ((('a-2',), 0, 0), None, (('a-1',),), ('fulltext', False)),
977
('a-3',): ((('a-3',), 0, 0), None, (('a-2',),), ('fulltext', False)),
978
}, idx.get_build_details(idx.keys()))
979
self.assertEqual({('a-1',):(),
980
('a-2',):(('a-1',),),
981
('a-3',):(('a-2',),),},
982
idx.get_parent_map(idx.keys()))
984
def test_add_versions_fails_clean(self):
985
"""If add_versions fails in the middle, it restores a pristine state.
987
Any modifications that are made to the index are reset if all versions
990
# This cheats a little bit by passing in a generator which will
991
# raise an exception before the processing finishes
992
# Other possibilities would be to have an version with the wrong number
993
# of entries, or to make the backing transport unable to write any
996
knit = self.make_test_knit()
998
idx.add_records([(('a-1',), ['fulltext'], (('a-1',), 0, 0), [])])
1000
class StopEarly(Exception):
1003
def generate_failure():
1004
"""Add some entries and then raise an exception"""
1005
yield (('a-2',), ['fulltext'], (None, 0, 0), ('a-1',))
1006
yield (('a-3',), ['fulltext'], (None, 0, 0), ('a-2',))
1009
# Assert the pre-condition
1011
self.assertEqual(set([('a-1',)]), set(idx.keys()))
1013
{('a-1',): ((('a-1',), 0, 0), None, (), ('fulltext', False))},
1014
idx.get_build_details([('a-1',)]))
1015
self.assertEqual({('a-1',):()}, idx.get_parent_map(idx.keys()))
1018
self.assertRaises(StopEarly, idx.add_records, generate_failure())
1019
# And it shouldn't be modified
1022
def test_knit_index_ignores_empty_files(self):
1023
# There was a race condition in older bzr, where a ^C at the right time
1024
# could leave an empty .kndx file, which bzr would later claim was a
1025
# corrupted file since the header was not present. In reality, the file
1026
# just wasn't created, so it should be ignored.
1027
t = get_transport('.')
1028
t.put_bytes('test.kndx', '')
1030
knit = self.make_test_knit()
1032
def test_knit_index_checks_header(self):
1033
t = get_transport('.')
1034
t.put_bytes('test.kndx', '# not really a knit header\n\n')
1035
k = self.make_test_knit()
1036
self.assertRaises(KnitHeaderError, k.keys)
1039
class TestGraphIndexKnit(KnitTests):
1040
"""Tests for knits using a GraphIndex rather than a KnitIndex."""
1042
def make_g_index(self, name, ref_lists=0, nodes=[]):
1043
builder = GraphIndexBuilder(ref_lists)
1044
for node, references, value in nodes:
1045
builder.add_node(node, references, value)
1046
stream = builder.finish()
1047
trans = self.get_transport()
1048
size = trans.put_file(name, stream)
1049
return GraphIndex(trans, name, size)
1051
def two_graph_index(self, deltas=False, catch_adds=False):
1052
"""Build a two-graph index.
1054
:param deltas: If true, use underlying indices with two node-ref
1055
lists and 'parent' set to a delta-compressed against tail.
1057
# build a complex graph across several indices.
1059
# delta compression inn the index
1060
index1 = self.make_g_index('1', 2, [
1061
(('tip', ), 'N0 100', ([('parent', )], [], )),
1062
(('tail', ), '', ([], []))])
1063
index2 = self.make_g_index('2', 2, [
1064
(('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
1065
(('separate', ), '', ([], []))])
1067
# just blob location and graph in the index.
1068
index1 = self.make_g_index('1', 1, [
1069
(('tip', ), 'N0 100', ([('parent', )], )),
1070
(('tail', ), '', ([], ))])
1071
index2 = self.make_g_index('2', 1, [
1072
(('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
1073
(('separate', ), '', ([], ))])
1074
combined_index = CombinedGraphIndex([index1, index2])
1076
self.combined_index = combined_index
1077
self.caught_entries = []
1078
add_callback = self.catch_add
1081
return _KnitGraphIndex(combined_index, lambda:True, deltas=deltas,
1082
add_callback=add_callback)
1084
def test_keys(self):
1085
index = self.two_graph_index()
1086
self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
1089
def test_get_position(self):
1090
index = self.two_graph_index()
1091
self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position(('tip',)))
1092
self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position(('parent',)))
1094
def test_get_method_deltas(self):
1095
index = self.two_graph_index(deltas=True)
1096
self.assertEqual('fulltext', index.get_method(('tip',)))
1097
self.assertEqual('line-delta', index.get_method(('parent',)))
1099
def test_get_method_no_deltas(self):
1100
# check that the parent-history lookup is ignored with deltas=False.
1101
index = self.two_graph_index(deltas=False)
1102
self.assertEqual('fulltext', index.get_method(('tip',)))
1103
self.assertEqual('fulltext', index.get_method(('parent',)))
1105
def test_get_options_deltas(self):
1106
index = self.two_graph_index(deltas=True)
1107
self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1108
self.assertEqual(['line-delta'], index.get_options(('parent',)))
1110
def test_get_options_no_deltas(self):
1111
# check that the parent-history lookup is ignored with deltas=False.
1112
index = self.two_graph_index(deltas=False)
1113
self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1114
self.assertEqual(['fulltext'], index.get_options(('parent',)))
1116
def test_get_parent_map(self):
1117
index = self.two_graph_index()
1118
self.assertEqual({('parent',):(('tail',), ('ghost',))},
1119
index.get_parent_map([('parent',), ('ghost',)]))
1121
def catch_add(self, entries):
1122
self.caught_entries.append(entries)
1124
def test_add_no_callback_errors(self):
1125
index = self.two_graph_index()
1126
self.assertRaises(errors.ReadOnlyError, index.add_records,
1127
[(('new',), 'fulltext,no-eol', (None, 50, 60), ['separate'])])
1129
def test_add_version_smoke(self):
1130
index = self.two_graph_index(catch_adds=True)
1131
index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60),
1133
self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
1134
self.caught_entries)
1136
def test_add_version_delta_not_delta_index(self):
1137
index = self.two_graph_index(catch_adds=True)
1138
self.assertRaises(errors.KnitCorrupt, index.add_records,
1139
[(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1140
self.assertEqual([], self.caught_entries)
1142
def test_add_version_same_dup(self):
1143
index = self.two_graph_index(catch_adds=True)
1144
# options can be spelt two different ways
1145
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1146
index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
1147
# position/length are ignored (because each pack could have fulltext or
1148
# delta, and be at a different position.
1149
index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
1151
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
1153
# but neither should have added data:
1154
self.assertEqual([[], [], [], []], self.caught_entries)
1156
def test_add_version_different_dup(self):
1157
index = self.two_graph_index(deltas=True, catch_adds=True)
1159
self.assertRaises(errors.KnitCorrupt, index.add_records,
1160
[(('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1161
self.assertRaises(errors.KnitCorrupt, index.add_records,
1162
[(('tip',), 'line-delta,no-eol', (None, 0, 100), [('parent',)])])
1163
self.assertRaises(errors.KnitCorrupt, index.add_records,
1164
[(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
1166
self.assertRaises(errors.KnitCorrupt, index.add_records,
1167
[(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1168
self.assertEqual([], self.caught_entries)
1170
def test_add_versions_nodeltas(self):
1171
index = self.two_graph_index(catch_adds=True)
1173
(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
1174
(('new2',), 'fulltext', (None, 0, 6), [('new',)]),
1176
self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
1177
(('new2', ), ' 0 6', ((('new',),),))],
1178
sorted(self.caught_entries[0]))
1179
self.assertEqual(1, len(self.caught_entries))
1181
def test_add_versions_deltas(self):
1182
index = self.two_graph_index(deltas=True, catch_adds=True)
1184
(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)]),
1185
(('new2',), 'line-delta', (None, 0, 6), [('new',)]),
1187
self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
1188
(('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
1189
sorted(self.caught_entries[0]))
1190
self.assertEqual(1, len(self.caught_entries))
1192
def test_add_versions_delta_not_delta_index(self):
1193
index = self.two_graph_index(catch_adds=True)
1194
self.assertRaises(errors.KnitCorrupt, index.add_records,
1195
[(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1196
self.assertEqual([], self.caught_entries)
1198
def test_add_versions_random_id_accepted(self):
1199
index = self.two_graph_index(catch_adds=True)
1200
index.add_records([], random_id=True)
1202
def test_add_versions_same_dup(self):
1203
index = self.two_graph_index(catch_adds=True)
1204
# options can be spelt two different ways
1205
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100),
1207
index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100),
1209
# position/length are ignored (because each pack could have fulltext or
1210
# delta, and be at a different position.
1211
index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100),
1213
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000),
1215
# but neither should have added data.
1216
self.assertEqual([[], [], [], []], self.caught_entries)
1218
def test_add_versions_different_dup(self):
1219
index = self.two_graph_index(deltas=True, catch_adds=True)
1221
self.assertRaises(errors.KnitCorrupt, index.add_records,
1222
[(('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1223
self.assertRaises(errors.KnitCorrupt, index.add_records,
1224
[(('tip',), 'line-delta,no-eol', (None, 0, 100), [('parent',)])])
1225
self.assertRaises(errors.KnitCorrupt, index.add_records,
1226
[(('tip',), 'fulltext', (None, 0, 100), [('parent',)])])
1228
self.assertRaises(errors.KnitCorrupt, index.add_records,
1229
[(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1230
# change options in the second record
1231
self.assertRaises(errors.KnitCorrupt, index.add_records,
1232
[(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)]),
1233
(('tip',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1234
self.assertEqual([], self.caught_entries)
1237
class TestNoParentsGraphIndexKnit(KnitTests):
1238
"""Tests for knits using _KnitGraphIndex with no parents."""
1240
def make_g_index(self, name, ref_lists=0, nodes=[]):
1241
builder = GraphIndexBuilder(ref_lists)
1242
for node, references in nodes:
1243
builder.add_node(node, references)
1244
stream = builder.finish()
1245
trans = self.get_transport()
1246
size = trans.put_file(name, stream)
1247
return GraphIndex(trans, name, size)
1249
def test_parents_deltas_incompatible(self):
1250
index = CombinedGraphIndex([])
1251
self.assertRaises(errors.KnitError, _KnitGraphIndex, lambda:True,
1252
index, deltas=True, parents=False)
1254
def two_graph_index(self, catch_adds=False):
1255
"""Build a two-graph index.
1257
:param deltas: If true, use underlying indices with two node-ref
1258
lists and 'parent' set to a delta-compressed against tail.
1260
# put several versions in the index.
1261
index1 = self.make_g_index('1', 0, [
1262
(('tip', ), 'N0 100'),
1264
index2 = self.make_g_index('2', 0, [
1265
(('parent', ), ' 100 78'),
1266
(('separate', ), '')])
1267
combined_index = CombinedGraphIndex([index1, index2])
1269
self.combined_index = combined_index
1270
self.caught_entries = []
1271
add_callback = self.catch_add
1274
return _KnitGraphIndex(combined_index, lambda:True, parents=False,
1275
add_callback=add_callback)
1277
def test_keys(self):
1278
index = self.two_graph_index()
1279
self.assertEqual(set([('tail',), ('tip',), ('parent',), ('separate',)]),
1282
def test_get_position(self):
1283
index = self.two_graph_index()
1284
self.assertEqual((index._graph_index._indices[0], 0, 100),
1285
index.get_position(('tip',)))
1286
self.assertEqual((index._graph_index._indices[1], 100, 78),
1287
index.get_position(('parent',)))
1289
def test_get_method(self):
1290
index = self.two_graph_index()
1291
self.assertEqual('fulltext', index.get_method(('tip',)))
1292
self.assertEqual(['fulltext'], index.get_options(('parent',)))
1294
def test_get_options(self):
1295
index = self.two_graph_index()
1296
self.assertEqual(['fulltext', 'no-eol'], index.get_options(('tip',)))
1297
self.assertEqual(['fulltext'], index.get_options(('parent',)))
1299
def test_get_parent_map(self):
1300
index = self.two_graph_index()
1301
self.assertEqual({('parent',):None},
1302
index.get_parent_map([('parent',), ('ghost',)]))
1304
def catch_add(self, entries):
1305
self.caught_entries.append(entries)
1307
def test_add_no_callback_errors(self):
1308
index = self.two_graph_index()
1309
self.assertRaises(errors.ReadOnlyError, index.add_records,
1310
[(('new',), 'fulltext,no-eol', (None, 50, 60), [('separate',)])])
1312
def test_add_version_smoke(self):
1313
index = self.two_graph_index(catch_adds=True)
1314
index.add_records([(('new',), 'fulltext,no-eol', (None, 50, 60), [])])
1315
self.assertEqual([[(('new', ), 'N50 60')]],
1316
self.caught_entries)
1318
def test_add_version_delta_not_delta_index(self):
1319
index = self.two_graph_index(catch_adds=True)
1320
self.assertRaises(errors.KnitCorrupt, index.add_records,
1321
[(('new',), 'no-eol,line-delta', (None, 0, 100), [])])
1322
self.assertEqual([], self.caught_entries)
1324
def test_add_version_same_dup(self):
1325
index = self.two_graph_index(catch_adds=True)
1326
# options can be spelt two different ways
1327
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1328
index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
1329
# position/length are ignored (because each pack could have fulltext or
1330
# delta, and be at a different position.
1331
index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
1332
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
1333
# but neither should have added data.
1334
self.assertEqual([[], [], [], []], self.caught_entries)
1336
def test_add_version_different_dup(self):
1337
index = self.two_graph_index(catch_adds=True)
1339
self.assertRaises(errors.KnitCorrupt, index.add_records,
1340
[(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1341
self.assertRaises(errors.KnitCorrupt, index.add_records,
1342
[(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
1343
self.assertRaises(errors.KnitCorrupt, index.add_records,
1344
[(('tip',), 'fulltext', (None, 0, 100), [])])
1346
self.assertRaises(errors.KnitCorrupt, index.add_records,
1347
[(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1348
self.assertEqual([], self.caught_entries)
1350
def test_add_versions(self):
1351
index = self.two_graph_index(catch_adds=True)
1353
(('new',), 'fulltext,no-eol', (None, 50, 60), []),
1354
(('new2',), 'fulltext', (None, 0, 6), []),
1356
self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
1357
sorted(self.caught_entries[0]))
1358
self.assertEqual(1, len(self.caught_entries))
1360
def test_add_versions_delta_not_delta_index(self):
1361
index = self.two_graph_index(catch_adds=True)
1362
self.assertRaises(errors.KnitCorrupt, index.add_records,
1363
[(('new',), 'no-eol,line-delta', (None, 0, 100), [('parent',)])])
1364
self.assertEqual([], self.caught_entries)
1366
def test_add_versions_parents_not_parents_index(self):
1367
index = self.two_graph_index(catch_adds=True)
1368
self.assertRaises(errors.KnitCorrupt, index.add_records,
1369
[(('new',), 'no-eol,fulltext', (None, 0, 100), [('parent',)])])
1370
self.assertEqual([], self.caught_entries)
1372
def test_add_versions_random_id_accepted(self):
1373
index = self.two_graph_index(catch_adds=True)
1374
index.add_records([], random_id=True)
1376
def test_add_versions_same_dup(self):
1377
index = self.two_graph_index(catch_adds=True)
1378
# options can be spelt two different ways
1379
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 100), [])])
1380
index.add_records([(('tip',), 'no-eol,fulltext', (None, 0, 100), [])])
1381
# position/length are ignored (because each pack could have fulltext or
1382
# delta, and be at a different position.
1383
index.add_records([(('tip',), 'fulltext,no-eol', (None, 50, 100), [])])
1384
index.add_records([(('tip',), 'fulltext,no-eol', (None, 0, 1000), [])])
1385
# but neither should have added data.
1386
self.assertEqual([[], [], [], []], self.caught_entries)
1388
def test_add_versions_different_dup(self):
1389
index = self.two_graph_index(catch_adds=True)
1391
self.assertRaises(errors.KnitCorrupt, index.add_records,
1392
[(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1393
self.assertRaises(errors.KnitCorrupt, index.add_records,
1394
[(('tip',), 'line-delta,no-eol', (None, 0, 100), [])])
1395
self.assertRaises(errors.KnitCorrupt, index.add_records,
1396
[(('tip',), 'fulltext', (None, 0, 100), [])])
1398
self.assertRaises(errors.KnitCorrupt, index.add_records,
1399
[(('tip',), 'fulltext,no-eol', (None, 0, 100), [('parent',)])])
1400
# change options in the second record
1401
self.assertRaises(errors.KnitCorrupt, index.add_records,
1402
[(('tip',), 'fulltext,no-eol', (None, 0, 100), []),
1403
(('tip',), 'no-eol,line-delta', (None, 0, 100), [])])
1404
self.assertEqual([], self.caught_entries)
1407
class TestStacking(KnitTests):
1409
def get_basis_and_test_knit(self):
1410
basis = self.make_test_knit(name='basis')
1411
basis = RecordingVersionedFilesDecorator(basis)
1412
test = self.make_test_knit(name='test')
1413
test.add_fallback_versioned_files(basis)
1416
def test_add_fallback_versioned_files(self):
1417
basis = self.make_test_knit(name='basis')
1418
test = self.make_test_knit(name='test')
1419
# It must not error; other tests test that the fallback is referred to
1420
# when accessing data.
1421
test.add_fallback_versioned_files(basis)
1423
def test_add_lines(self):
1424
# lines added to the test are not added to the basis
1425
basis, test = self.get_basis_and_test_knit()
1427
key_basis = ('bar',)
1428
key_cross_border = ('quux',)
1429
key_delta = ('zaphod',)
1430
test.add_lines(key, (), ['foo\n'])
1431
self.assertEqual({}, basis.get_parent_map([key]))
1432
# lines added to the test that reference across the stack do a
1434
basis.add_lines(key_basis, (), ['foo\n'])
1436
test.add_lines(key_cross_border, (key_basis,), ['foo\n'])
1437
self.assertEqual('fulltext', test._index.get_method(key_cross_border))
1438
self.assertEqual([("get_parent_map", set([key_basis]))], basis.calls)
1439
# Subsequent adds do delta.
1441
test.add_lines(key_delta, (key_cross_border,), ['foo\n'])
1442
self.assertEqual('line-delta', test._index.get_method(key_delta))
1443
self.assertEqual([], basis.calls)
1445
def test_annotate(self):
1446
# annotations from the test knit are answered without asking the basis
1447
basis, test = self.get_basis_and_test_knit()
1449
key_basis = ('bar',)
1450
key_missing = ('missing',)
1451
test.add_lines(key, (), ['foo\n'])
1452
details = test.annotate(key)
1453
self.assertEqual([(key, 'foo\n')], details)
1454
self.assertEqual([], basis.calls)
1455
# But texts that are not in the test knit are looked for in the basis
1457
basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1459
details = test.annotate(key_basis)
1460
self.assertEqual([(key_basis, 'foo\n'), (key_basis, 'bar\n')], details)
1461
# Not optimised to date:
1462
# self.assertEqual([("annotate", key_basis)], basis.calls)
1463
self.assertEqual([('get_parent_map', set([key_basis])),
1464
('get_parent_map', set([key_basis])),
1465
('get_parent_map', set([key_basis])),
1466
('get_record_stream', [key_basis], 'unordered', True)],
1469
def test_check(self):
1470
# At the moment checking a stacked knit does implicitly check the
1472
basis, test = self.get_basis_and_test_knit()
1475
def test_get_parent_map(self):
1476
# parents in the test knit are answered without asking the basis
1477
basis, test = self.get_basis_and_test_knit()
1479
key_basis = ('bar',)
1480
key_missing = ('missing',)
1481
test.add_lines(key, (), [])
1482
parent_map = test.get_parent_map([key])
1483
self.assertEqual({key: ()}, parent_map)
1484
self.assertEqual([], basis.calls)
1485
# But parents that are not in the test knit are looked for in the basis
1486
basis.add_lines(key_basis, (), [])
1488
parent_map = test.get_parent_map([key, key_basis, key_missing])
1489
self.assertEqual({key: (),
1490
key_basis: ()}, parent_map)
1491
self.assertEqual([("get_parent_map", set([key_basis, key_missing]))],
1494
def test_get_record_stream_unordered_fulltexts(self):
1495
# records from the test knit are answered without asking the basis:
1496
basis, test = self.get_basis_and_test_knit()
1498
key_basis = ('bar',)
1499
key_missing = ('missing',)
1500
test.add_lines(key, (), ['foo\n'])
1501
records = list(test.get_record_stream([key], 'unordered', True))
1502
self.assertEqual(1, len(records))
1503
self.assertEqual([], basis.calls)
1504
# Missing (from test knit) objects are retrieved from the basis:
1505
basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1507
records = list(test.get_record_stream([key_basis, key_missing],
1509
self.assertEqual(2, len(records))
1510
calls = list(basis.calls)
1511
for record in records:
1512
self.assertSubset([record.key], (key_basis, key_missing))
1513
if record.key == key_missing:
1514
self.assertIsInstance(record, AbsentContentFactory)
1516
reference = list(basis.get_record_stream([key_basis],
1517
'unordered', True))[0]
1518
self.assertEqual(reference.key, record.key)
1519
self.assertEqual(reference.sha1, record.sha1)
1520
self.assertEqual(reference.storage_kind, record.storage_kind)
1521
self.assertEqual(reference.get_bytes_as(reference.storage_kind),
1522
record.get_bytes_as(record.storage_kind))
1523
self.assertEqual(reference.get_bytes_as('fulltext'),
1524
record.get_bytes_as('fulltext'))
1525
# It's not strictly minimal, but it seems reasonable for now for it to
1526
# ask which fallbacks have which parents.
1528
("get_parent_map", set([key_basis, key_missing])),
1529
("get_record_stream", [key_basis], 'unordered', True)],
1532
def test_get_record_stream_ordered_fulltexts(self):
1533
# ordering is preserved down into the fallback store.
1534
basis, test = self.get_basis_and_test_knit()
1536
key_basis = ('bar',)
1537
key_basis_2 = ('quux',)
1538
key_missing = ('missing',)
1539
test.add_lines(key, (key_basis,), ['foo\n'])
1540
# Missing (from test knit) objects are retrieved from the basis:
1541
basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
1542
basis.add_lines(key_basis_2, (), ['quux\n'])
1544
# ask for in non-topological order
1545
records = list(test.get_record_stream(
1546
[key, key_basis, key_missing, key_basis_2], 'topological', True))
1547
self.assertEqual(4, len(records))
1549
for record in records:
1550
self.assertSubset([record.key],
1551
(key_basis, key_missing, key_basis_2, key))
1552
if record.key == key_missing:
1553
self.assertIsInstance(record, AbsentContentFactory)
1555
results.append((record.key, record.sha1, record.storage_kind,
1556
record.get_bytes_as('fulltext')))
1557
calls = list(basis.calls)
1558
order = [record[0] for record in results]
1559
self.assertEqual([key_basis_2, key_basis, key], order)
1560
for result in results:
1561
if result[0] == key:
1565
record = source.get_record_stream([result[0]], 'unordered',
1567
self.assertEqual(record.key, result[0])
1568
self.assertEqual(record.sha1, result[1])
1569
self.assertEqual(record.storage_kind, result[2])
1570
self.assertEqual(record.get_bytes_as('fulltext'), result[3])
1571
# It's not strictly minimal, but it seems reasonable for now for it to
1572
# ask which fallbacks have which parents.
1574
("get_parent_map", set([key_basis, key_basis_2, key_missing])),
1575
# unordered is asked for by the underlying worker as it still
1576
# buffers everything while answering - which is a problem!
1577
("get_record_stream", [key_basis_2, key_basis], 'unordered', True)],
1580
def test_get_record_stream_unordered_deltas(self):
1581
# records from the test knit are answered without asking the basis:
1582
basis, test = self.get_basis_and_test_knit()
1584
key_basis = ('bar',)
1585
key_missing = ('missing',)
1586
test.add_lines(key, (), ['foo\n'])
1587
records = list(test.get_record_stream([key], 'unordered', False))
1588
self.assertEqual(1, len(records))
1589
self.assertEqual([], basis.calls)
1590
# Missing (from test knit) objects are retrieved from the basis:
1591
basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1593
records = list(test.get_record_stream([key_basis, key_missing],
1594
'unordered', False))
1595
self.assertEqual(2, len(records))
1596
calls = list(basis.calls)
1597
for record in records:
1598
self.assertSubset([record.key], (key_basis, key_missing))
1599
if record.key == key_missing:
1600
self.assertIsInstance(record, AbsentContentFactory)
1602
reference = list(basis.get_record_stream([key_basis],
1603
'unordered', False))[0]
1604
self.assertEqual(reference.key, record.key)
1605
self.assertEqual(reference.sha1, record.sha1)
1606
self.assertEqual(reference.storage_kind, record.storage_kind)
1607
self.assertEqual(reference.get_bytes_as(reference.storage_kind),
1608
record.get_bytes_as(record.storage_kind))
1609
# It's not strictly minimal, but it seems reasonable for now for it to
1610
# ask which fallbacks have which parents.
1612
("get_parent_map", set([key_basis, key_missing])),
1613
("get_record_stream", [key_basis], 'unordered', False)],
1616
def test_get_record_stream_ordered_deltas(self):
1617
# ordering is preserved down into the fallback store.
1618
basis, test = self.get_basis_and_test_knit()
1620
key_basis = ('bar',)
1621
key_basis_2 = ('quux',)
1622
key_missing = ('missing',)
1623
test.add_lines(key, (key_basis,), ['foo\n'])
1624
# Missing (from test knit) objects are retrieved from the basis:
1625
basis.add_lines(key_basis, (key_basis_2,), ['foo\n', 'bar\n'])
1626
basis.add_lines(key_basis_2, (), ['quux\n'])
1628
# ask for in non-topological order
1629
records = list(test.get_record_stream(
1630
[key, key_basis, key_missing, key_basis_2], 'topological', False))
1631
self.assertEqual(4, len(records))
1633
for record in records:
1634
self.assertSubset([record.key],
1635
(key_basis, key_missing, key_basis_2, key))
1636
if record.key == key_missing:
1637
self.assertIsInstance(record, AbsentContentFactory)
1639
results.append((record.key, record.sha1, record.storage_kind,
1640
record.get_bytes_as(record.storage_kind)))
1641
calls = list(basis.calls)
1642
order = [record[0] for record in results]
1643
self.assertEqual([key_basis_2, key_basis, key], order)
1644
for result in results:
1645
if result[0] == key:
1649
record = source.get_record_stream([result[0]], 'unordered',
1651
self.assertEqual(record.key, result[0])
1652
self.assertEqual(record.sha1, result[1])
1653
self.assertEqual(record.storage_kind, result[2])
1654
self.assertEqual(record.get_bytes_as(record.storage_kind), result[3])
1655
# It's not strictly minimal, but it seems reasonable for now for it to
1656
# ask which fallbacks have which parents.
1658
("get_parent_map", set([key_basis, key_basis_2, key_missing])),
1659
("get_record_stream", [key_basis_2, key_basis], 'topological', False)],
1662
def test_get_sha1s(self):
1663
# sha1's in the test knit are answered without asking the basis
1664
basis, test = self.get_basis_and_test_knit()
1666
key_basis = ('bar',)
1667
key_missing = ('missing',)
1668
test.add_lines(key, (), ['foo\n'])
1669
key_sha1sum = osutils.sha('foo\n').hexdigest()
1670
sha1s = test.get_sha1s([key])
1671
self.assertEqual({key: key_sha1sum}, sha1s)
1672
self.assertEqual([], basis.calls)
1673
# But texts that are not in the test knit are looked for in the basis
1674
# directly (rather than via text reconstruction) so that remote servers
1675
# etc don't have to answer with full content.
1676
basis.add_lines(key_basis, (), ['foo\n', 'bar\n'])
1677
basis_sha1sum = osutils.sha('foo\nbar\n').hexdigest()
1679
sha1s = test.get_sha1s([key, key_missing, key_basis])
1680
self.assertEqual({key: key_sha1sum,
1681
key_basis: basis_sha1sum}, sha1s)
1682
self.assertEqual([("get_sha1s", set([key_basis, key_missing]))],
1685
def test_insert_record_stream(self):
1686
# records are inserted as normal; insert_record_stream builds on
1687
# add_lines, so a smoke test should be all that's needed:
1689
key_basis = ('bar',)
1690
key_delta = ('zaphod',)
1691
basis, test = self.get_basis_and_test_knit()
1692
source = self.make_test_knit(name='source')
1693
basis.add_lines(key_basis, (), ['foo\n'])
1695
source.add_lines(key_basis, (), ['foo\n'])
1696
source.add_lines(key_delta, (key_basis,), ['bar\n'])
1697
stream = source.get_record_stream([key_delta], 'unordered', False)
1698
test.insert_record_stream(stream)
1699
self.assertEqual([("get_parent_map", set([key_basis]))],
1701
self.assertEqual({key_delta:(key_basis,)},
1702
test.get_parent_map([key_delta]))
1703
self.assertEqual('bar\n', test.get_record_stream([key_delta],
1704
'unordered', True).next().get_bytes_as('fulltext'))
1706
def test_iter_lines_added_or_present_in_keys(self):
1707
# Lines from the basis are returned, and lines for a given key are only
1711
# all sources are asked for keys:
1712
basis, test = self.get_basis_and_test_knit()
1713
basis.add_lines(key1, (), ["foo"])
1715
lines = list(test.iter_lines_added_or_present_in_keys([key1]))
1716
self.assertEqual([("foo\n", key1)], lines)
1717
self.assertEqual([("iter_lines_added_or_present_in_keys", set([key1]))],
1719
# keys in both are not duplicated:
1720
test.add_lines(key2, (), ["bar\n"])
1721
basis.add_lines(key2, (), ["bar\n"])
1723
lines = list(test.iter_lines_added_or_present_in_keys([key2]))
1724
self.assertEqual([("bar\n", key2)], lines)
1725
self.assertEqual([], basis.calls)
1727
def test_keys(self):
1730
# all sources are asked for keys:
1731
basis, test = self.get_basis_and_test_knit()
1733
self.assertEqual(set(), set(keys))
1734
self.assertEqual([("keys",)], basis.calls)
1735
# keys from a basis are returned:
1736
basis.add_lines(key1, (), [])
1739
self.assertEqual(set([key1]), set(keys))
1740
self.assertEqual([("keys",)], basis.calls)
1741
# keys in both are not duplicated:
1742
test.add_lines(key2, (), [])
1743
basis.add_lines(key2, (), [])
1746
self.assertEqual(2, len(keys))
1747
self.assertEqual(set([key1, key2]), set(keys))
1748
self.assertEqual([("keys",)], basis.calls)
1750
def test_add_mpdiffs(self):
1751
# records are inserted as normal; add_mpdiff builds on
1752
# add_lines, so a smoke test should be all that's needed:
1754
key_basis = ('bar',)
1755
key_delta = ('zaphod',)
1756
basis, test = self.get_basis_and_test_knit()
1757
source = self.make_test_knit(name='source')
1758
basis.add_lines(key_basis, (), ['foo\n'])
1760
source.add_lines(key_basis, (), ['foo\n'])
1761
source.add_lines(key_delta, (key_basis,), ['bar\n'])
1762
diffs = source.make_mpdiffs([key_delta])
1763
test.add_mpdiffs([(key_delta, (key_basis,),
1764
source.get_sha1s([key_delta])[key_delta], diffs[0])])
1765
self.assertEqual([("get_parent_map", set([key_basis])),
1766
('get_record_stream', [key_basis], 'unordered', True),
1767
('get_parent_map', set([key_basis]))],
1769
self.assertEqual({key_delta:(key_basis,)},
1770
test.get_parent_map([key_delta]))
1771
self.assertEqual('bar\n', test.get_record_stream([key_delta],
1772
'unordered', True).next().get_bytes_as('fulltext'))
1774
def test_make_mpdiffs(self):
1775
# Generating an mpdiff across a stacking boundary should detect parent
1779
key_right = ('zaphod',)
1780
basis, test = self.get_basis_and_test_knit()
1781
basis.add_lines(key_left, (), ['bar\n'])
1782
basis.add_lines(key_right, (), ['zaphod\n'])
1784
test.add_lines(key, (key_left, key_right),
1785
['bar\n', 'foo\n', 'zaphod\n'])
1786
diffs = test.make_mpdiffs([key])
1788
multiparent.MultiParent([multiparent.ParentText(0, 0, 0, 1),
1789
multiparent.NewText(['foo\n']),
1790
multiparent.ParentText(1, 0, 2, 1)])],
1792
self.assertEqual(4, len(basis.calls))
1794
("get_parent_map", set([key_left, key_right])),
1795
("get_parent_map", set([key_left, key_right])),
1796
("get_parent_map", set([key_left, key_right])),
1799
last_call = basis.calls[3]
1800
self.assertEqual('get_record_stream', last_call[0])
1801
self.assertEqual(set([key_left, key_right]), set(last_call[1]))
1802
self.assertEqual('unordered', last_call[2])
1803
self.assertEqual(True, last_call[3])