17
17
"""Tests for Knit data structure"""
19
from cStringIO import StringIO
23
from bzrlib.errors import KnitError, RevisionAlreadyPresent, NoSuchFile
31
from bzrlib.errors import (
32
RevisionAlreadyPresent,
37
from bzrlib.index import *
24
38
from bzrlib.knit import (
27
46
KnitAnnotateFactory,
29
58
from bzrlib.osutils import split_lines
30
from bzrlib.tests import TestCaseWithTransport
31
from bzrlib.transport import TransportLogger, get_transport
59
from bzrlib.symbol_versioning import one_four
60
from bzrlib.tests import (
63
TestCaseWithMemoryTransport,
64
TestCaseWithTransport,
66
from bzrlib.transport import get_transport
32
67
from bzrlib.transport.memory import MemoryTransport
68
from bzrlib.tuned_gzip import GzipFile
69
from bzrlib.util import bencode
33
70
from bzrlib.weave import Weave
73
class _CompiledKnitFeature(Feature):
77
import bzrlib._knit_load_data_c
82
def feature_name(self):
83
return 'bzrlib._knit_load_data_c'
85
CompiledKnitFeature = _CompiledKnitFeature()
88
class KnitContentTestsMixin(object):
90
def test_constructor(self):
91
content = self._make_content([])
94
content = self._make_content([])
95
self.assertEqual(content.text(), [])
97
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
98
self.assertEqual(content.text(), ["text1", "text2"])
101
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
102
copy = content.copy()
103
self.assertIsInstance(copy, content.__class__)
104
self.assertEqual(copy.annotate(), content.annotate())
106
def assertDerivedBlocksEqual(self, source, target, noeol=False):
107
"""Assert that the derived matching blocks match real output"""
108
source_lines = source.splitlines(True)
109
target_lines = target.splitlines(True)
111
if noeol and not line.endswith('\n'):
115
source_content = self._make_content([(None, nl(l)) for l in source_lines])
116
target_content = self._make_content([(None, nl(l)) for l in target_lines])
117
line_delta = source_content.line_delta(target_content)
118
delta_blocks = list(KnitContent.get_line_delta_blocks(line_delta,
119
source_lines, target_lines))
120
matcher = KnitSequenceMatcher(None, source_lines, target_lines)
121
matcher_blocks = list(list(matcher.get_matching_blocks()))
122
self.assertEqual(matcher_blocks, delta_blocks)
124
def test_get_line_delta_blocks(self):
125
self.assertDerivedBlocksEqual('a\nb\nc\n', 'q\nc\n')
126
self.assertDerivedBlocksEqual(TEXT_1, TEXT_1)
127
self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A)
128
self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B)
129
self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A)
130
self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B)
131
self.assertDerivedBlocksEqual(TEXT_1A, '')
132
self.assertDerivedBlocksEqual('', TEXT_1A)
133
self.assertDerivedBlocksEqual('', '')
134
self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd')
136
def test_get_line_delta_blocks_noeol(self):
137
"""Handle historical knit deltas safely
139
Some existing knit deltas don't consider the last line to differ
140
when the only difference whether it has a final newline.
142
New knit deltas appear to always consider the last line to differ
145
self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\nd\n', noeol=True)
146
self.assertDerivedBlocksEqual('a\nb\nc\nd\n', 'a\nb\nc', noeol=True)
147
self.assertDerivedBlocksEqual('a\nb\nc\n', 'a\nb\nc', noeol=True)
148
self.assertDerivedBlocksEqual('a\nb\nc', 'a\nb\nc\n', noeol=True)
151
class TestPlainKnitContent(TestCase, KnitContentTestsMixin):
153
def _make_content(self, lines):
154
annotated_content = AnnotatedKnitContent(lines)
155
return PlainKnitContent(annotated_content.text(), 'bogus')
157
def test_annotate(self):
158
content = self._make_content([])
159
self.assertEqual(content.annotate(), [])
161
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
162
self.assertEqual(content.annotate(),
163
[("bogus", "text1"), ("bogus", "text2")])
165
def test_line_delta(self):
166
content1 = self._make_content([("", "a"), ("", "b")])
167
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
168
self.assertEqual(content1.line_delta(content2),
169
[(1, 2, 2, ["a", "c"])])
171
def test_line_delta_iter(self):
172
content1 = self._make_content([("", "a"), ("", "b")])
173
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
174
it = content1.line_delta_iter(content2)
175
self.assertEqual(it.next(), (1, 2, 2, ["a", "c"]))
176
self.assertRaises(StopIteration, it.next)
179
class TestAnnotatedKnitContent(TestCase, KnitContentTestsMixin):
181
def _make_content(self, lines):
182
return AnnotatedKnitContent(lines)
184
def test_annotate(self):
185
content = self._make_content([])
186
self.assertEqual(content.annotate(), [])
188
content = self._make_content([("origin1", "text1"), ("origin2", "text2")])
189
self.assertEqual(content.annotate(),
190
[("origin1", "text1"), ("origin2", "text2")])
192
def test_line_delta(self):
193
content1 = self._make_content([("", "a"), ("", "b")])
194
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
195
self.assertEqual(content1.line_delta(content2),
196
[(1, 2, 2, [("", "a"), ("", "c")])])
198
def test_line_delta_iter(self):
199
content1 = self._make_content([("", "a"), ("", "b")])
200
content2 = self._make_content([("", "a"), ("", "a"), ("", "c")])
201
it = content1.line_delta_iter(content2)
202
self.assertEqual(it.next(), (1, 2, 2, [("", "a"), ("", "c")]))
203
self.assertRaises(StopIteration, it.next)
206
class MockTransport(object):
208
def __init__(self, file_lines=None):
209
self.file_lines = file_lines
211
# We have no base directory for the MockTransport
214
def get(self, filename):
215
if self.file_lines is None:
216
raise NoSuchFile(filename)
218
return StringIO("\n".join(self.file_lines))
220
def readv(self, relpath, offsets):
221
fp = self.get(relpath)
222
for offset, size in offsets:
224
yield offset, fp.read(size)
226
def __getattr__(self, name):
227
def queue_call(*args, **kwargs):
228
self.calls.append((name, args, kwargs))
232
class KnitRecordAccessTestsMixin(object):
233
"""Tests for getting and putting knit records."""
235
def assertAccessExists(self, access):
236
"""Ensure the data area for access has been initialised/exists."""
237
raise NotImplementedError(self.assertAccessExists)
239
def test_add_raw_records(self):
240
"""Add_raw_records adds records retrievable later."""
241
access = self.get_access()
242
memos = access.add_raw_records([10], '1234567890')
243
self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
245
def test_add_several_raw_records(self):
246
"""add_raw_records with many records and read some back."""
247
access = self.get_access()
248
memos = access.add_raw_records([10, 2, 5], '12345678901234567')
249
self.assertEqual(['1234567890', '12', '34567'],
250
list(access.get_raw_records(memos)))
251
self.assertEqual(['1234567890'],
252
list(access.get_raw_records(memos[0:1])))
253
self.assertEqual(['12'],
254
list(access.get_raw_records(memos[1:2])))
255
self.assertEqual(['34567'],
256
list(access.get_raw_records(memos[2:3])))
257
self.assertEqual(['1234567890', '34567'],
258
list(access.get_raw_records(memos[0:1] + memos[2:3])))
260
def test_create(self):
261
"""create() should make a file on disk."""
262
access = self.get_access()
264
self.assertAccessExists(access)
266
def test_open_file(self):
267
"""open_file never errors."""
268
access = self.get_access()
272
class TestKnitKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
273
"""Tests for the .kndx implementation."""
275
def assertAccessExists(self, access):
276
self.assertNotEqual(None, access.open_file())
278
def get_access(self):
279
"""Get a .knit style access instance."""
280
access = _KnitAccess(self.get_transport(), "foo.knit", None, None,
285
class TestPackKnitAccess(TestCaseWithMemoryTransport, KnitRecordAccessTestsMixin):
286
"""Tests for the pack based access."""
288
def assertAccessExists(self, access):
289
# as pack based access has no backing unless an index maps data, this
293
def get_access(self):
294
return self._get_access()[0]
296
def _get_access(self, packname='packfile', index='FOO'):
297
transport = self.get_transport()
298
def write_data(bytes):
299
transport.append_bytes(packname, bytes)
300
writer = pack.ContainerWriter(write_data)
302
indices = {index:(transport, packname)}
303
access = _PackAccess(indices, writer=(writer, index))
304
return access, writer
306
def test_read_from_several_packs(self):
307
access, writer = self._get_access()
309
memos.extend(access.add_raw_records([10], '1234567890'))
311
access, writer = self._get_access('pack2', 'FOOBAR')
312
memos.extend(access.add_raw_records([5], '12345'))
314
access, writer = self._get_access('pack3', 'BAZ')
315
memos.extend(access.add_raw_records([5], 'alpha'))
317
transport = self.get_transport()
318
access = _PackAccess({"FOO":(transport, 'packfile'),
319
"FOOBAR":(transport, 'pack2'),
320
"BAZ":(transport, 'pack3')})
321
self.assertEqual(['1234567890', '12345', 'alpha'],
322
list(access.get_raw_records(memos)))
323
self.assertEqual(['1234567890'],
324
list(access.get_raw_records(memos[0:1])))
325
self.assertEqual(['12345'],
326
list(access.get_raw_records(memos[1:2])))
327
self.assertEqual(['alpha'],
328
list(access.get_raw_records(memos[2:3])))
329
self.assertEqual(['1234567890', 'alpha'],
330
list(access.get_raw_records(memos[0:1] + memos[2:3])))
332
def test_set_writer(self):
333
"""The writer should be settable post construction."""
334
access = _PackAccess({})
335
transport = self.get_transport()
336
packname = 'packfile'
338
def write_data(bytes):
339
transport.append_bytes(packname, bytes)
340
writer = pack.ContainerWriter(write_data)
342
access.set_writer(writer, index, (transport, packname))
343
memos = access.add_raw_records([10], '1234567890')
345
self.assertEqual(['1234567890'], list(access.get_raw_records(memos)))
348
class LowLevelKnitDataTests(TestCase):
350
def create_gz_content(self, text):
352
gz_file = gzip.GzipFile(mode='wb', fileobj=sio)
355
return sio.getvalue()
357
def test_valid_knit_data(self):
358
sha1sum = sha.new('foo\nbar\n').hexdigest()
359
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
364
transport = MockTransport([gz_txt])
365
access = _KnitAccess(transport, 'filename', None, None, False, False)
366
data = _KnitData(access=access)
367
records = [('rev-id-1', (None, 0, len(gz_txt)))]
369
contents = data.read_records(records)
370
self.assertEqual({'rev-id-1':(['foo\n', 'bar\n'], sha1sum)}, contents)
372
raw_contents = list(data.read_records_iter_raw(records))
373
self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
375
def test_not_enough_lines(self):
376
sha1sum = sha.new('foo\n').hexdigest()
377
# record says 2 lines data says 1
378
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
382
transport = MockTransport([gz_txt])
383
access = _KnitAccess(transport, 'filename', None, None, False, False)
384
data = _KnitData(access=access)
385
records = [('rev-id-1', (None, 0, len(gz_txt)))]
386
self.assertRaises(errors.KnitCorrupt, data.read_records, records)
388
# read_records_iter_raw won't detect that sort of mismatch/corruption
389
raw_contents = list(data.read_records_iter_raw(records))
390
self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
392
def test_too_many_lines(self):
393
sha1sum = sha.new('foo\nbar\n').hexdigest()
394
# record says 1 lines data says 2
395
gz_txt = self.create_gz_content('version rev-id-1 1 %s\n'
400
transport = MockTransport([gz_txt])
401
access = _KnitAccess(transport, 'filename', None, None, False, False)
402
data = _KnitData(access=access)
403
records = [('rev-id-1', (None, 0, len(gz_txt)))]
404
self.assertRaises(errors.KnitCorrupt, data.read_records, records)
406
# read_records_iter_raw won't detect that sort of mismatch/corruption
407
raw_contents = list(data.read_records_iter_raw(records))
408
self.assertEqual([('rev-id-1', gz_txt)], raw_contents)
410
def test_mismatched_version_id(self):
411
sha1sum = sha.new('foo\nbar\n').hexdigest()
412
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
417
transport = MockTransport([gz_txt])
418
access = _KnitAccess(transport, 'filename', None, None, False, False)
419
data = _KnitData(access=access)
420
# We are asking for rev-id-2, but the data is rev-id-1
421
records = [('rev-id-2', (None, 0, len(gz_txt)))]
422
self.assertRaises(errors.KnitCorrupt, data.read_records, records)
424
# read_records_iter_raw will notice if we request the wrong version.
425
self.assertRaises(errors.KnitCorrupt, list,
426
data.read_records_iter_raw(records))
428
def test_uncompressed_data(self):
429
sha1sum = sha.new('foo\nbar\n').hexdigest()
430
txt = ('version rev-id-1 2 %s\n'
435
transport = MockTransport([txt])
436
access = _KnitAccess(transport, 'filename', None, None, False, False)
437
data = _KnitData(access=access)
438
records = [('rev-id-1', (None, 0, len(txt)))]
440
# We don't have valid gzip data ==> corrupt
441
self.assertRaises(errors.KnitCorrupt, data.read_records, records)
443
# read_records_iter_raw will notice the bad data
444
self.assertRaises(errors.KnitCorrupt, list,
445
data.read_records_iter_raw(records))
447
def test_corrupted_data(self):
448
sha1sum = sha.new('foo\nbar\n').hexdigest()
449
gz_txt = self.create_gz_content('version rev-id-1 2 %s\n'
454
# Change 2 bytes in the middle to \xff
455
gz_txt = gz_txt[:10] + '\xff\xff' + gz_txt[12:]
456
transport = MockTransport([gz_txt])
457
access = _KnitAccess(transport, 'filename', None, None, False, False)
458
data = _KnitData(access=access)
459
records = [('rev-id-1', (None, 0, len(gz_txt)))]
461
self.assertRaises(errors.KnitCorrupt, data.read_records, records)
463
# read_records_iter_raw will notice if we request the wrong version.
464
self.assertRaises(errors.KnitCorrupt, list,
465
data.read_records_iter_raw(records))
468
class LowLevelKnitIndexTests(TestCase):
470
def get_knit_index(self, *args, **kwargs):
471
orig = knit._load_data
473
knit._load_data = orig
474
self.addCleanup(reset)
475
from bzrlib._knit_load_data_py import _load_data_py
476
knit._load_data = _load_data_py
477
return _KnitIndex(get_scope=lambda:None, *args, **kwargs)
479
def test_no_such_file(self):
480
transport = MockTransport()
482
self.assertRaises(NoSuchFile, self.get_knit_index,
483
transport, "filename", "r")
484
self.assertRaises(NoSuchFile, self.get_knit_index,
485
transport, "filename", "w", create=False)
487
def test_create_file(self):
488
transport = MockTransport()
490
index = self.get_knit_index(transport, "filename", "w",
491
file_mode="wb", create=True)
493
("put_bytes_non_atomic",
494
("filename", index.HEADER), {"mode": "wb"}),
495
transport.calls.pop(0))
497
def test_delay_create_file(self):
498
transport = MockTransport()
500
index = self.get_knit_index(transport, "filename", "w",
501
create=True, file_mode="wb", create_parent_dir=True,
502
delay_create=True, dir_mode=0777)
503
self.assertEqual([], transport.calls)
505
index.add_versions([])
506
name, (filename, f), kwargs = transport.calls.pop(0)
507
self.assertEqual("put_file_non_atomic", name)
509
{"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
511
self.assertEqual("filename", filename)
512
self.assertEqual(index.HEADER, f.read())
514
index.add_versions([])
515
self.assertEqual(("append_bytes", ("filename", ""), {}),
516
transport.calls.pop(0))
518
def test_read_utf8_version_id(self):
519
unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
520
utf8_revision_id = unicode_revision_id.encode('utf-8')
521
transport = MockTransport([
523
'%s option 0 1 :' % (utf8_revision_id,)
525
index = self.get_knit_index(transport, "filename", "r")
526
# _KnitIndex is a private class, and deals in utf8 revision_ids, not
527
# Unicode revision_ids.
528
self.assertTrue(index.has_version(utf8_revision_id))
529
self.assertFalse(index.has_version(unicode_revision_id))
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((utf8_revision_id,),
540
index.get_parents_with_ghosts("version"))
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, index.num_versions())
551
self.assertTrue(index.has_version("version"))
553
def test_read_corrupted_header(self):
554
transport = MockTransport(['not a bzr knit index header\n'])
555
self.assertRaises(KnitHeaderError,
556
self.get_knit_index, transport, "filename", "r")
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, index.num_versions())
568
# check that the index used is the first one written. (Specific
569
# to KnitIndex style indices.
570
self.assertEqual("1", index._version_list_to_index(["version"]))
571
self.assertEqual((None, 3, 4), index.get_position("version"))
572
self.assertEqual(["options3"], index.get_options("version"))
573
self.assertEqual(("parent", "other"),
574
index.get_parents_with_ghosts("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")
594
index.add_version(utf8_revision_id, ["option"], (None, 0, 1), [])
595
self.assertEqual(("append_bytes", ("filename",
596
"\n%s option 0 1 :" % (utf8_revision_id,)),
598
transport.calls.pop(0))
600
def test_write_utf8_parents(self):
601
unicode_revision_id = u"version-\N{CYRILLIC CAPITAL LETTER A}"
602
utf8_revision_id = unicode_revision_id.encode('utf-8')
603
transport = MockTransport([
606
index = self.get_knit_index(transport, "filename", "r")
607
index.add_version("version", ["option"], (None, 0, 1), [utf8_revision_id])
608
self.assertEqual(("append_bytes", ("filename",
609
"\nversion option 0 1 .%s :" % (utf8_revision_id,)),
611
transport.calls.pop(0))
613
def test_get_ancestry(self):
614
transport = MockTransport([
617
"b option 0 1 0 .e :",
618
"c option 0 1 1 0 :",
619
"d option 0 1 2 .f :"
621
index = self.get_knit_index(transport, "filename", "r")
623
self.assertEqual([], index.get_ancestry([]))
624
self.assertEqual(["a"], index.get_ancestry(["a"]))
625
self.assertEqual(["a", "b"], index.get_ancestry(["b"]))
626
self.assertEqual(["a", "b", "c"], index.get_ancestry(["c"]))
627
self.assertEqual(["a", "b", "c", "d"], index.get_ancestry(["d"]))
628
self.assertEqual(["a", "b"], index.get_ancestry(["a", "b"]))
629
self.assertEqual(["a", "b", "c"], index.get_ancestry(["a", "c"]))
631
self.assertRaises(RevisionNotPresent, index.get_ancestry, ["e"])
633
def test_get_ancestry_with_ghosts(self):
634
transport = MockTransport([
637
"b option 0 1 0 .e :",
638
"c option 0 1 0 .f .g :",
639
"d option 0 1 2 .h .j .k :"
641
index = self.get_knit_index(transport, "filename", "r")
643
self.assertEqual([], index.get_ancestry_with_ghosts([]))
644
self.assertEqual(["a"], index.get_ancestry_with_ghosts(["a"]))
645
self.assertEqual(["a", "e", "b"],
646
index.get_ancestry_with_ghosts(["b"]))
647
self.assertEqual(["a", "g", "f", "c"],
648
index.get_ancestry_with_ghosts(["c"]))
649
self.assertEqual(["a", "g", "f", "c", "k", "j", "h", "d"],
650
index.get_ancestry_with_ghosts(["d"]))
651
self.assertEqual(["a", "e", "b"],
652
index.get_ancestry_with_ghosts(["a", "b"]))
653
self.assertEqual(["a", "g", "f", "c"],
654
index.get_ancestry_with_ghosts(["a", "c"]))
656
["a", "g", "f", "c", "e", "b", "k", "j", "h", "d"],
657
index.get_ancestry_with_ghosts(["b", "d"]))
659
self.assertRaises(RevisionNotPresent,
660
index.get_ancestry_with_ghosts, ["e"])
662
def test_num_versions(self):
663
transport = MockTransport([
666
index = self.get_knit_index(transport, "filename", "r")
668
self.assertEqual(0, index.num_versions())
669
self.assertEqual(0, len(index))
671
index.add_version("a", ["option"], (None, 0, 1), [])
672
self.assertEqual(1, index.num_versions())
673
self.assertEqual(1, len(index))
675
index.add_version("a", ["option2"], (None, 1, 2), [])
676
self.assertEqual(1, index.num_versions())
677
self.assertEqual(1, len(index))
679
index.add_version("b", ["option"], (None, 0, 1), [])
680
self.assertEqual(2, index.num_versions())
681
self.assertEqual(2, len(index))
683
def test_get_versions(self):
684
transport = MockTransport([
687
index = self.get_knit_index(transport, "filename", "r")
689
self.assertEqual([], index.get_versions())
691
index.add_version("a", ["option"], (None, 0, 1), [])
692
self.assertEqual(["a"], index.get_versions())
694
index.add_version("a", ["option"], (None, 0, 1), [])
695
self.assertEqual(["a"], index.get_versions())
697
index.add_version("b", ["option"], (None, 0, 1), [])
698
self.assertEqual(["a", "b"], index.get_versions())
700
def test_add_version(self):
701
transport = MockTransport([
704
index = self.get_knit_index(transport, "filename", "r")
706
index.add_version("a", ["option"], (None, 0, 1), ["b"])
707
self.assertEqual(("append_bytes",
708
("filename", "\na option 0 1 .b :"),
709
{}), transport.calls.pop(0))
710
self.assertTrue(index.has_version("a"))
711
self.assertEqual(1, index.num_versions())
712
self.assertEqual((None, 0, 1), index.get_position("a"))
713
self.assertEqual(["option"], index.get_options("a"))
714
self.assertEqual(("b",), index.get_parents_with_ghosts("a"))
716
index.add_version("a", ["opt"], (None, 1, 2), ["c"])
717
self.assertEqual(("append_bytes",
718
("filename", "\na opt 1 2 .c :"),
719
{}), transport.calls.pop(0))
720
self.assertTrue(index.has_version("a"))
721
self.assertEqual(1, index.num_versions())
722
self.assertEqual((None, 1, 2), index.get_position("a"))
723
self.assertEqual(["opt"], index.get_options("a"))
724
self.assertEqual(("c",), index.get_parents_with_ghosts("a"))
726
index.add_version("b", ["option"], (None, 2, 3), ["a"])
727
self.assertEqual(("append_bytes",
728
("filename", "\nb option 2 3 0 :"),
729
{}), transport.calls.pop(0))
730
self.assertTrue(index.has_version("b"))
731
self.assertEqual(2, index.num_versions())
732
self.assertEqual((None, 2, 3), index.get_position("b"))
733
self.assertEqual(["option"], index.get_options("b"))
734
self.assertEqual(("a",), index.get_parents_with_ghosts("b"))
736
def test_add_versions(self):
737
transport = MockTransport([
740
index = self.get_knit_index(transport, "filename", "r")
743
("a", ["option"], (None, 0, 1), ["b"]),
744
("a", ["opt"], (None, 1, 2), ["c"]),
745
("b", ["option"], (None, 2, 3), ["a"])
747
self.assertEqual(("append_bytes", ("filename",
748
"\na option 0 1 .b :"
751
), {}), transport.calls.pop(0))
752
self.assertTrue(index.has_version("a"))
753
self.assertTrue(index.has_version("b"))
754
self.assertEqual(2, index.num_versions())
755
self.assertEqual((None, 1, 2), index.get_position("a"))
756
self.assertEqual((None, 2, 3), index.get_position("b"))
757
self.assertEqual(["opt"], index.get_options("a"))
758
self.assertEqual(["option"], index.get_options("b"))
759
self.assertEqual(("c",), index.get_parents_with_ghosts("a"))
760
self.assertEqual(("a",), index.get_parents_with_ghosts("b"))
762
def test_add_versions_random_id_is_accepted(self):
763
transport = MockTransport([
766
index = self.get_knit_index(transport, "filename", "r")
769
("a", ["option"], (None, 0, 1), ["b"]),
770
("a", ["opt"], (None, 1, 2), ["c"]),
771
("b", ["option"], (None, 2, 3), ["a"])
774
def test_delay_create_and_add_versions(self):
775
transport = MockTransport()
777
index = self.get_knit_index(transport, "filename", "w",
778
create=True, file_mode="wb", create_parent_dir=True,
779
delay_create=True, dir_mode=0777)
780
self.assertEqual([], transport.calls)
783
("a", ["option"], (None, 0, 1), ["b"]),
784
("a", ["opt"], (None, 1, 2), ["c"]),
785
("b", ["option"], (None, 2, 3), ["a"])
787
name, (filename, f), kwargs = transport.calls.pop(0)
788
self.assertEqual("put_file_non_atomic", name)
790
{"dir_mode": 0777, "create_parent_dir": True, "mode": "wb"},
792
self.assertEqual("filename", filename)
795
"\na option 0 1 .b :"
797
"\nb option 2 3 0 :",
800
def test_has_version(self):
801
transport = MockTransport([
805
index = self.get_knit_index(transport, "filename", "r")
807
self.assertTrue(index.has_version("a"))
808
self.assertFalse(index.has_version("b"))
810
def test_get_position(self):
811
transport = MockTransport([
816
index = self.get_knit_index(transport, "filename", "r")
818
self.assertEqual((None, 0, 1), index.get_position("a"))
819
self.assertEqual((None, 1, 2), index.get_position("b"))
821
def test_get_method(self):
822
transport = MockTransport([
824
"a fulltext,unknown 0 1 :",
825
"b unknown,line-delta 1 2 :",
828
index = self.get_knit_index(transport, "filename", "r")
830
self.assertEqual("fulltext", index.get_method("a"))
831
self.assertEqual("line-delta", index.get_method("b"))
832
self.assertRaises(errors.KnitIndexUnknownMethod, index.get_method, "c")
834
def test_get_options(self):
835
transport = MockTransport([
840
index = self.get_knit_index(transport, "filename", "r")
842
self.assertEqual(["opt1"], index.get_options("a"))
843
self.assertEqual(["opt2", "opt3"], index.get_options("b"))
845
def test_get_parent_map(self):
846
transport = MockTransport([
849
"b option 1 2 0 .c :",
850
"c option 1 2 1 0 .e :"
852
index = self.get_knit_index(transport, "filename", "r")
858
}, index.get_parent_map(["a", "b", "c"]))
860
def test_get_parents_with_ghosts(self):
861
transport = MockTransport([
864
"b option 1 2 0 .c :",
865
"c option 1 2 1 0 .e :"
867
index = self.get_knit_index(transport, "filename", "r")
869
self.assertEqual((), index.get_parents_with_ghosts("a"))
870
self.assertEqual(("a", "c"), index.get_parents_with_ghosts("b"))
871
self.assertEqual(("b", "a", "e"),
872
index.get_parents_with_ghosts("c"))
874
def test_check_versions_present(self):
875
transport = MockTransport([
880
index = self.get_knit_index(transport, "filename", "r")
882
check = index.check_versions_present
888
self.assertRaises(RevisionNotPresent, check, ["c"])
889
self.assertRaises(RevisionNotPresent, check, ["a", "b", "c"])
891
def test_impossible_parent(self):
892
"""Test we get KnitCorrupt if the parent couldn't possibly exist."""
893
transport = MockTransport([
896
"b option 0 1 4 :" # We don't have a 4th record
899
self.assertRaises(errors.KnitCorrupt,
900
self.get_knit_index, transport, 'filename', 'r')
902
if (str(e) == ('exceptions must be strings, classes, or instances,'
903
' not exceptions.IndexError')
904
and sys.version_info[0:2] >= (2,5)):
905
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
906
' raising new style exceptions with python'
911
def test_corrupted_parent(self):
912
transport = MockTransport([
916
"c option 0 1 1v :", # Can't have a parent of '1v'
919
self.assertRaises(errors.KnitCorrupt,
920
self.get_knit_index, transport, 'filename', 'r')
922
if (str(e) == ('exceptions must be strings, classes, or instances,'
923
' not exceptions.ValueError')
924
and sys.version_info[0:2] >= (2,5)):
925
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
926
' raising new style exceptions with python'
931
def test_corrupted_parent_in_list(self):
932
transport = MockTransport([
936
"c option 0 1 1 v :", # Can't have a parent of 'v'
939
self.assertRaises(errors.KnitCorrupt,
940
self.get_knit_index, transport, 'filename', 'r')
942
if (str(e) == ('exceptions must be strings, classes, or instances,'
943
' not exceptions.ValueError')
944
and sys.version_info[0:2] >= (2,5)):
945
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
946
' raising new style exceptions with python'
951
def test_invalid_position(self):
952
transport = MockTransport([
957
self.assertRaises(errors.KnitCorrupt,
958
self.get_knit_index, transport, 'filename', 'r')
960
if (str(e) == ('exceptions must be strings, classes, or instances,'
961
' not exceptions.ValueError')
962
and sys.version_info[0:2] >= (2,5)):
963
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
964
' raising new style exceptions with python'
969
def test_invalid_size(self):
970
transport = MockTransport([
975
self.assertRaises(errors.KnitCorrupt,
976
self.get_knit_index, transport, 'filename', 'r')
978
if (str(e) == ('exceptions must be strings, classes, or instances,'
979
' not exceptions.ValueError')
980
and sys.version_info[0:2] >= (2,5)):
981
self.knownFailure('Pyrex <0.9.5 fails with TypeError when'
982
' raising new style exceptions with python'
987
def test_short_line(self):
988
transport = MockTransport([
991
"b option 10 10 0", # This line isn't terminated, ignored
993
index = self.get_knit_index(transport, "filename", "r")
994
self.assertEqual(['a'], index.get_versions())
996
def test_skip_incomplete_record(self):
997
# A line with bogus data should just be skipped
998
transport = MockTransport([
1001
"b option 10 10 0", # This line isn't terminated, ignored
1002
"c option 20 10 0 :", # Properly terminated, and starts with '\n'
1004
index = self.get_knit_index(transport, "filename", "r")
1005
self.assertEqual(['a', 'c'], index.get_versions())
1007
def test_trailing_characters(self):
1008
# A line with bogus data should just be skipped
1009
transport = MockTransport([
1012
"b option 10 10 0 :a", # This line has extra trailing characters
1013
"c option 20 10 0 :", # Properly terminated, and starts with '\n'
1015
index = self.get_knit_index(transport, "filename", "r")
1016
self.assertEqual(['a', 'c'], index.get_versions())
1019
class LowLevelKnitIndexTests_c(LowLevelKnitIndexTests):
1021
_test_needs_features = [CompiledKnitFeature]
1023
def get_knit_index(self, *args, **kwargs):
1024
orig = knit._load_data
1026
knit._load_data = orig
1027
self.addCleanup(reset)
1028
from bzrlib._knit_load_data_c import _load_data_c
1029
knit._load_data = _load_data_c
1030
return _KnitIndex(get_scope=lambda:None, *args, **kwargs)
36
1033
class KnitTests(TestCaseWithTransport):
37
1034
"""Class containing knit test helper routines."""
39
def make_test_knit(self, annotate=False, delay_create=False):
1036
def make_test_knit(self, annotate=False, delay_create=False, index=None,
1037
name='test', delta=True, access_mode='w'):
41
1039
factory = KnitPlainFactory()
44
return KnitVersionedFile('test', get_transport('.'), access_mode='w',
45
factory=factory, create=True,
46
delay_create=delay_create)
1043
index = _KnitIndex(get_transport('.'), name + INDEX_SUFFIX,
1044
access_mode, create=True, file_mode=None,
1045
create_parent_dir=False, delay_create=delay_create,
1046
dir_mode=None, get_scope=lambda:None)
1047
access = _KnitAccess(get_transport('.'), name + DATA_SUFFIX, None,
1048
None, delay_create, False)
1049
return KnitVersionedFile(name, get_transport('.'), factory=factory,
1050
create=True, delay_create=delay_create, index=index,
1051
access_method=access, delta=delta)
1053
def assertRecordContentEqual(self, knit, version_id, candidate_content):
1054
"""Assert that some raw record content matches the raw record content
1055
for a particular version_id in the given knit.
1057
index_memo = knit._index.get_position(version_id)
1058
record = (version_id, index_memo)
1059
[(_, expected_content)] = list(knit._data.read_records_iter_raw([record]))
1060
self.assertEqual(expected_content, candidate_content)
49
1063
class BasicKnitTests(KnitTests):
440
1506
self.assertEqual(plan_line, expected_line)
1509
class GetDataStreamTests(KnitTests):
1510
"""Tests for get_data_stream."""
1512
def test_get_stream_empty(self):
1513
"""Get a data stream for an empty knit file."""
1514
k1 = self.make_test_knit()
1515
format, data_list, reader_callable = k1.get_data_stream([])
1516
self.assertEqual('knit-plain', format)
1517
self.assertEqual([], data_list)
1518
content = reader_callable(None)
1519
self.assertEqual('', content)
1520
self.assertIsInstance(content, str)
1522
def test_get_stream_one_version(self):
1523
"""Get a data stream for a single record out of a knit containing just
1526
k1 = self.make_test_knit()
1528
('text-a', [], TEXT_1),
1530
expected_data_list = [
1531
# version, options, length, parents
1532
('text-a', ['fulltext'], 122, ()),
1534
for version_id, parents, lines in test_data:
1535
k1.add_lines(version_id, parents, split_lines(lines))
1537
format, data_list, reader_callable = k1.get_data_stream(['text-a'])
1538
self.assertEqual('knit-plain', format)
1539
self.assertEqual(expected_data_list, data_list)
1540
# There's only one record in the knit, so the content should be the
1541
# entire knit data file's contents.
1542
self.assertEqual(k1.transport.get_bytes(k1._data._access._filename),
1543
reader_callable(None))
1545
def test_get_stream_get_one_version_of_many(self):
1546
"""Get a data stream for just one version out of a knit containing many
1549
k1 = self.make_test_knit()
1550
# Insert the same data as test_knit_join, as they seem to cover a range
1551
# of cases (no parents, one parent, multiple parents).
1553
('text-a', [], TEXT_1),
1554
('text-b', ['text-a'], TEXT_1),
1555
('text-c', [], TEXT_1),
1556
('text-d', ['text-c'], TEXT_1),
1557
('text-m', ['text-b', 'text-d'], TEXT_1),
1559
expected_data_list = [
1560
# version, options, length, parents
1561
('text-m', ['line-delta'], 84, ('text-b', 'text-d')),
1563
for version_id, parents, lines in test_data:
1564
k1.add_lines(version_id, parents, split_lines(lines))
1566
format, data_list, reader_callable = k1.get_data_stream(['text-m'])
1567
self.assertEqual('knit-plain', format)
1568
self.assertEqual(expected_data_list, data_list)
1569
self.assertRecordContentEqual(k1, 'text-m', reader_callable(None))
1571
def test_get_data_stream_unordered_index(self):
1572
"""Get a data stream when the knit index reports versions out of order.
1574
https://bugs.launchpad.net/bzr/+bug/164637
1576
k1 = self.make_test_knit()
1578
('text-a', [], TEXT_1),
1579
('text-b', ['text-a'], TEXT_1),
1580
('text-c', [], TEXT_1),
1581
('text-d', ['text-c'], TEXT_1),
1582
('text-m', ['text-b', 'text-d'], TEXT_1),
1584
for version_id, parents, lines in test_data:
1585
k1.add_lines(version_id, parents, split_lines(lines))
1586
# monkey-patch versions method to return out of order, as if coming
1587
# from multiple independently indexed packs
1588
original_versions = k1.versions
1589
k1.versions = lambda: reversed(original_versions())
1590
expected_data_list = [
1591
('text-a', ['fulltext'], 122, ()),
1592
('text-b', ['line-delta'], 84, ('text-a',))]
1593
# now check the fulltext is first and the delta second
1594
format, data_list, _ = k1.get_data_stream(['text-a', 'text-b'])
1595
self.assertEqual('knit-plain', format)
1596
self.assertEqual(expected_data_list, data_list)
1597
# and that's true if we ask for them in the opposite order too
1598
format, data_list, _ = k1.get_data_stream(['text-b', 'text-a'])
1599
self.assertEqual(expected_data_list, data_list)
1600
# also try requesting more versions
1601
format, data_list, _ = k1.get_data_stream([
1602
'text-m', 'text-b', 'text-a'])
1604
('text-a', ['fulltext'], 122, ()),
1605
('text-b', ['line-delta'], 84, ('text-a',)),
1606
('text-m', ['line-delta'], 84, ('text-b', 'text-d')),
1609
def test_get_stream_ghost_parent(self):
1610
"""Get a data stream for a version with a ghost parent."""
1611
k1 = self.make_test_knit()
1613
k1.add_lines('text-a', [], split_lines(TEXT_1))
1614
k1.add_lines_with_ghosts('text-b', ['text-a', 'text-ghost'],
1615
split_lines(TEXT_1))
1617
expected_data_list = [
1618
# version, options, length, parents
1619
('text-b', ['line-delta'], 84, ('text-a', 'text-ghost')),
1622
format, data_list, reader_callable = k1.get_data_stream(['text-b'])
1623
self.assertEqual('knit-plain', format)
1624
self.assertEqual(expected_data_list, data_list)
1625
self.assertRecordContentEqual(k1, 'text-b', reader_callable(None))
1627
def test_get_stream_get_multiple_records(self):
1628
"""Get a stream for multiple records of a knit."""
1629
k1 = self.make_test_knit()
1630
# Insert the same data as test_knit_join, as they seem to cover a range
1631
# of cases (no parents, one parent, multiple parents).
1633
('text-a', [], TEXT_1),
1634
('text-b', ['text-a'], TEXT_1),
1635
('text-c', [], TEXT_1),
1636
('text-d', ['text-c'], TEXT_1),
1637
('text-m', ['text-b', 'text-d'], TEXT_1),
1639
for version_id, parents, lines in test_data:
1640
k1.add_lines(version_id, parents, split_lines(lines))
1642
# This test is actually a bit strict as the order in which they're
1643
# returned is not defined. This matches the current (deterministic)
1645
expected_data_list = [
1646
# version, options, length, parents
1647
('text-d', ['line-delta'], 84, ('text-c',)),
1648
('text-b', ['line-delta'], 84, ('text-a',)),
1650
# Note that even though we request the revision IDs in a particular
1651
# order, the data stream may return them in any order it likes. In this
1652
# case, they'll be in the order they were inserted into the knit.
1653
format, data_list, reader_callable = k1.get_data_stream(
1654
['text-d', 'text-b'])
1655
self.assertEqual('knit-plain', format)
1656
self.assertEqual(expected_data_list, data_list)
1657
# must match order they're returned
1658
self.assertRecordContentEqual(k1, 'text-d', reader_callable(84))
1659
self.assertRecordContentEqual(k1, 'text-b', reader_callable(84))
1660
self.assertEqual('', reader_callable(None),
1661
"There should be no more bytes left to read.")
1663
def test_get_stream_all(self):
1664
"""Get a data stream for all the records in a knit.
1666
This exercises fulltext records, line-delta records, records with
1667
various numbers of parents, and reading multiple records out of the
1668
callable. These cases ought to all be exercised individually by the
1669
other test_get_stream_* tests; this test is basically just paranoia.
1671
k1 = self.make_test_knit()
1672
# Insert the same data as test_knit_join, as they seem to cover a range
1673
# of cases (no parents, one parent, multiple parents).
1675
('text-a', [], TEXT_1),
1676
('text-b', ['text-a'], TEXT_1),
1677
('text-c', [], TEXT_1),
1678
('text-d', ['text-c'], TEXT_1),
1679
('text-m', ['text-b', 'text-d'], TEXT_1),
1681
for version_id, parents, lines in test_data:
1682
k1.add_lines(version_id, parents, split_lines(lines))
1684
# This test is actually a bit strict as the order in which they're
1685
# returned is not defined. This matches the current (deterministic)
1687
expected_data_list = [
1688
# version, options, length, parents
1689
('text-a', ['fulltext'], 122, ()),
1690
('text-b', ['line-delta'], 84, ('text-a',)),
1691
('text-m', ['line-delta'], 84, ('text-b', 'text-d')),
1692
('text-c', ['fulltext'], 121, ()),
1693
('text-d', ['line-delta'], 84, ('text-c',)),
1695
format, data_list, reader_callable = k1.get_data_stream(
1696
['text-a', 'text-b', 'text-c', 'text-d', 'text-m'])
1697
self.assertEqual('knit-plain', format)
1698
self.assertEqual(expected_data_list, data_list)
1699
for version_id, options, length, parents in expected_data_list:
1700
bytes = reader_callable(length)
1701
self.assertRecordContentEqual(k1, version_id, bytes)
1704
class InsertDataStreamTests(KnitTests):
1705
"""Tests for insert_data_stream."""
1707
def assertKnitFilesEqual(self, knit1, knit2):
1708
"""Assert that the contents of the index and data files of two knits are
1712
knit1.transport.get_bytes(knit1._data._access._filename),
1713
knit2.transport.get_bytes(knit2._data._access._filename))
1715
knit1.transport.get_bytes(knit1._index._filename),
1716
knit2.transport.get_bytes(knit2._index._filename))
1718
def assertKnitValuesEqual(self, left, right):
1719
"""Assert that the texts, annotations and graph of left and right are
1722
self.assertEqual(set(left.versions()), set(right.versions()))
1723
for version in left.versions():
1724
self.assertEqual(left.get_parents_with_ghosts(version),
1725
right.get_parents_with_ghosts(version))
1726
self.assertEqual(left.get_lines(version),
1727
right.get_lines(version))
1728
self.assertEqual(left.annotate(version),
1729
right.annotate(version))
1731
def test_empty_stream(self):
1732
"""Inserting a data stream with no records should not put any data into
1735
k1 = self.make_test_knit()
1736
k1.insert_data_stream(
1737
(k1.get_format_signature(), [], lambda ignored: ''))
1738
self.assertEqual('', k1.transport.get_bytes(k1._data._access._filename),
1739
"The .knit should be completely empty.")
1740
self.assertEqual(k1._index.HEADER,
1741
k1.transport.get_bytes(k1._index._filename),
1742
"The .kndx should have nothing apart from the header.")
1744
def test_one_record(self):
1745
"""Inserting a data stream with one record from a knit with one record
1746
results in byte-identical files.
1748
source = self.make_test_knit(name='source')
1749
source.add_lines('text-a', [], split_lines(TEXT_1))
1750
data_stream = source.get_data_stream(['text-a'])
1751
target = self.make_test_knit(name='target')
1752
target.insert_data_stream(data_stream)
1753
self.assertKnitFilesEqual(source, target)
1755
def test_annotated_stream_into_unannotated_knit(self):
1756
"""Inserting an annotated datastream to an unannotated knit works."""
1757
# case one - full texts.
1758
source = self.make_test_knit(name='source', annotate=True)
1759
target = self.make_test_knit(name='target', annotate=False)
1760
source.add_lines('text-a', [], split_lines(TEXT_1))
1761
target.insert_data_stream(source.get_data_stream(['text-a']))
1762
self.assertKnitValuesEqual(source, target)
1763
# case two - deltas.
1764
source.add_lines('text-b', ['text-a'], split_lines(TEXT_2))
1765
target.insert_data_stream(source.get_data_stream(['text-b']))
1766
self.assertKnitValuesEqual(source, target)
1768
def test_unannotated_stream_into_annotated_knit(self):
1769
"""Inserting an unannotated datastream to an annotated knit works."""
1770
# case one - full texts.
1771
source = self.make_test_knit(name='source', annotate=False)
1772
target = self.make_test_knit(name='target', annotate=True)
1773
source.add_lines('text-a', [], split_lines(TEXT_1))
1774
target.insert_data_stream(source.get_data_stream(['text-a']))
1775
self.assertKnitValuesEqual(source, target)
1776
# case two - deltas.
1777
source.add_lines('text-b', ['text-a'], split_lines(TEXT_2))
1778
target.insert_data_stream(source.get_data_stream(['text-b']))
1779
self.assertKnitValuesEqual(source, target)
1781
def test_records_already_present(self):
1782
"""Insert a data stream where some records are alreday present in the
1783
target, and some not. Only the new records are inserted.
1785
source = self.make_test_knit(name='source')
1786
target = self.make_test_knit(name='target')
1787
# Insert 'text-a' into both source and target
1788
source.add_lines('text-a', [], split_lines(TEXT_1))
1789
target.insert_data_stream(source.get_data_stream(['text-a']))
1790
# Insert 'text-b' into just the source.
1791
source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
1792
# Get a data stream of both text-a and text-b, and insert it.
1793
data_stream = source.get_data_stream(['text-a', 'text-b'])
1794
target.insert_data_stream(data_stream)
1795
# The source and target will now be identical. This means the text-a
1796
# record was not added a second time.
1797
self.assertKnitFilesEqual(source, target)
1799
def test_multiple_records(self):
1800
"""Inserting a data stream of all records from a knit with multiple
1801
records results in byte-identical files.
1803
source = self.make_test_knit(name='source')
1804
source.add_lines('text-a', [], split_lines(TEXT_1))
1805
source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
1806
source.add_lines('text-c', [], split_lines(TEXT_1))
1807
data_stream = source.get_data_stream(['text-a', 'text-b', 'text-c'])
1809
target = self.make_test_knit(name='target')
1810
target.insert_data_stream(data_stream)
1812
self.assertKnitFilesEqual(source, target)
1814
def test_ghost_parent(self):
1815
"""Insert a data stream with a record that has a ghost parent."""
1816
# Make a knit with a record, text-a, that has a ghost parent.
1817
source = self.make_test_knit(name='source')
1818
source.add_lines_with_ghosts('text-a', ['text-ghost'],
1819
split_lines(TEXT_1))
1820
data_stream = source.get_data_stream(['text-a'])
1822
target = self.make_test_knit(name='target')
1823
target.insert_data_stream(data_stream)
1825
self.assertKnitFilesEqual(source, target)
1827
# The target knit object is in a consistent state, i.e. the record we
1828
# just added is immediately visible.
1829
self.assertTrue(target.has_version('text-a'))
1830
self.assertFalse(target.has_version('text-ghost'))
1831
self.assertEqual({'text-a':('text-ghost',)},
1832
target.get_parent_map(['text-a', 'text-ghost']))
1833
self.assertEqual(split_lines(TEXT_1), target.get_lines('text-a'))
1835
def test_inconsistent_version_lines(self):
1836
"""Inserting a data stream which has different content for a version_id
1837
than already exists in the knit will raise KnitCorrupt.
1839
source = self.make_test_knit(name='source')
1840
target = self.make_test_knit(name='target')
1841
# Insert a different 'text-a' into both source and target
1842
source.add_lines('text-a', [], split_lines(TEXT_1))
1843
target.add_lines('text-a', [], split_lines(TEXT_2))
1844
# Insert a data stream with conflicting content into the target
1845
data_stream = source.get_data_stream(['text-a'])
1847
errors.KnitCorrupt, target.insert_data_stream, data_stream)
1849
def test_inconsistent_version_parents(self):
1850
"""Inserting a data stream which has different parents for a version_id
1851
than already exists in the knit will raise KnitCorrupt.
1853
source = self.make_test_knit(name='source')
1854
target = self.make_test_knit(name='target')
1855
# Insert a different 'text-a' into both source and target. They differ
1856
# only by the parents list, the content is the same.
1857
source.add_lines_with_ghosts('text-a', [], split_lines(TEXT_1))
1858
target.add_lines_with_ghosts('text-a', ['a-ghost'], split_lines(TEXT_1))
1859
# Insert a data stream with conflicting content into the target
1860
data_stream = source.get_data_stream(['text-a'])
1862
errors.KnitCorrupt, target.insert_data_stream, data_stream)
1864
def test_unknown_stream_format(self):
1865
"""A data stream in a different format to the target knit cannot be
1868
It will raise KnitDataStreamUnknown because the fallback code will fail
1869
to make a knit. In future we may need KnitDataStreamIncompatible again,
1870
for more exotic cases.
1872
data_stream = ('fake-format-signature', [], lambda _: '')
1873
target = self.make_test_knit(name='target')
1875
errors.KnitDataStreamUnknown,
1876
target.insert_data_stream, data_stream)
1878
def test_bug_208418(self):
1879
"""You can insert a stream with an incompatible format, even when:
1880
* the stream has a line-delta record,
1881
* whose parent is in the target, also stored as a line-delta
1883
See <https://launchpad.net/bugs/208418>.
1885
base_lines = split_lines(TEXT_1)
1887
target = self.make_test_knit(name='target', annotate=True)
1888
target.add_lines('version-1', [], base_lines)
1889
target.add_lines('version-2', ['version-1'], base_lines + ['a\n'])
1890
# The second record should be a delta.
1891
self.assertEqual('line-delta', target._index.get_method('version-2'))
1893
# Make a source, with a different format, but the same data
1894
source = self.make_test_knit(name='source', annotate=False)
1895
source.add_lines('version-1', [], base_lines)
1896
source.add_lines('version-2', ['version-1'], base_lines + ['a\n'])
1897
# Now add another record, which should be stored as a delta against
1899
source.add_lines('version-3', ['version-2'], base_lines + ['b\n'])
1900
self.assertEqual('line-delta', source._index.get_method('version-3'))
1902
# Make a stream of the new version
1903
data_stream = source.get_data_stream(['version-3'])
1904
# And insert into the target
1905
target.insert_data_stream(data_stream)
1906
# No errors should have been raised.
1908
def test_line_delta_record_into_non_delta_knit(self):
1909
# Make a data stream with a line-delta record
1910
source = self.make_test_knit(name='source', delta=True)
1911
base_lines = split_lines(TEXT_1)
1912
source.add_lines('version-1', [], base_lines)
1913
source.add_lines('version-2', ['version-1'], base_lines + ['a\n'])
1914
# The second record should be a delta.
1915
self.assertEqual('line-delta', source._index.get_method('version-2'))
1916
data_stream = source.get_data_stream(['version-1', 'version-2'])
1918
# Insert the stream into a non-delta knit.
1919
target = self.make_test_knit(name='target', delta=False)
1920
target.insert_data_stream(data_stream)
1922
# Both versions are fulltexts in the target
1923
self.assertEqual('fulltext', target._index.get_method('version-1'))
1924
self.assertEqual('fulltext', target._index.get_method('version-2'))
1927
class DataStreamTests(KnitTests):
1929
def assertMadeStreamKnit(self, source_knit, versions, target_knit):
1930
"""Assert that a knit made from a stream is as expected."""
1931
a_stream = source_knit.get_data_stream(versions)
1932
expected_data = a_stream[2](None)
1933
a_stream = source_knit.get_data_stream(versions)
1934
a_knit = target_knit._knit_from_datastream(a_stream)
1935
self.assertEqual(source_knit.factory.__class__,
1936
a_knit.factory.__class__)
1937
self.assertIsInstance(a_knit._data._access, _StreamAccess)
1938
self.assertIsInstance(a_knit._index, _StreamIndex)
1939
self.assertEqual(a_knit._index.data_list, a_stream[1])
1940
self.assertEqual(a_knit._data._access.data, expected_data)
1941
self.assertEqual(a_knit.filename, target_knit.filename)
1942
self.assertEqual(a_knit.transport, target_knit.transport)
1943
self.assertEqual(a_knit._index, a_knit._data._access.stream_index)
1944
self.assertEqual(target_knit, a_knit._data._access.backing_knit)
1945
self.assertIsInstance(a_knit._data._access.orig_factory,
1946
source_knit.factory.__class__)
1948
def test__knit_from_data_stream_empty(self):
1949
"""Create a knit object from a datastream."""
1950
annotated = self.make_test_knit(name='source', annotate=True)
1951
plain = self.make_test_knit(name='target', annotate=False)
1952
# case 1: annotated source
1953
self.assertMadeStreamKnit(annotated, [], annotated)
1954
self.assertMadeStreamKnit(annotated, [], plain)
1955
# case 2: plain source
1956
self.assertMadeStreamKnit(plain, [], annotated)
1957
self.assertMadeStreamKnit(plain, [], plain)
1959
def test__knit_from_data_stream_unknown_format(self):
1960
annotated = self.make_test_knit(name='source', annotate=True)
1961
self.assertRaises(errors.KnitDataStreamUnknown,
1962
annotated._knit_from_datastream, ("unknown", None, None))
444
1966
Banana cup cakes:
540
2062
self.failIf(WeaveToKnit.is_compatible(k, k))
543
class TestKnitCaching(KnitTests):
2065
class TestKnitIndex(KnitTests):
2067
def test_add_versions_dictionary_compresses(self):
2068
"""Adding versions to the index should update the lookup dict"""
2069
knit = self.make_test_knit()
2071
idx.add_version('a-1', ['fulltext'], (None, 0, 0), [])
2072
self.check_file_contents('test.kndx',
2073
'# bzr knit index 8\n'
2075
'a-1 fulltext 0 0 :'
2077
idx.add_versions([('a-2', ['fulltext'], (None, 0, 0), ['a-1']),
2078
('a-3', ['fulltext'], (None, 0, 0), ['a-2']),
2080
self.check_file_contents('test.kndx',
2081
'# bzr knit index 8\n'
2083
'a-1 fulltext 0 0 :\n'
2084
'a-2 fulltext 0 0 0 :\n'
2085
'a-3 fulltext 0 0 1 :'
2087
self.assertEqual(['a-1', 'a-2', 'a-3'], idx._history)
2088
self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, (), 0),
2089
'a-2':('a-2', ['fulltext'], 0, 0, ('a-1',), 1),
2090
'a-3':('a-3', ['fulltext'], 0, 0, ('a-2',), 2),
2093
def test_add_versions_fails_clean(self):
2094
"""If add_versions fails in the middle, it restores a pristine state.
2096
Any modifications that are made to the index are reset if all versions
2099
# This cheats a little bit by passing in a generator which will
2100
# raise an exception before the processing finishes
2101
# Other possibilities would be to have an version with the wrong number
2102
# of entries, or to make the backing transport unable to write any
2105
knit = self.make_test_knit()
2107
idx.add_version('a-1', ['fulltext'], (None, 0, 0), [])
2109
class StopEarly(Exception):
2112
def generate_failure():
2113
"""Add some entries and then raise an exception"""
2114
yield ('a-2', ['fulltext'], (None, 0, 0), ('a-1',))
2115
yield ('a-3', ['fulltext'], (None, 0, 0), ('a-2',))
2118
# Assert the pre-condition
2119
self.assertEqual(['a-1'], idx._history)
2120
self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, (), 0)}, idx._cache)
2122
self.assertRaises(StopEarly, idx.add_versions, generate_failure())
2124
# And it shouldn't be modified
2125
self.assertEqual(['a-1'], idx._history)
2126
self.assertEqual({'a-1':('a-1', ['fulltext'], 0, 0, (), 0)}, idx._cache)
2128
def test_knit_index_ignores_empty_files(self):
2129
# There was a race condition in older bzr, where a ^C at the right time
2130
# could leave an empty .kndx file, which bzr would later claim was a
2131
# corrupted file since the header was not present. In reality, the file
2132
# just wasn't created, so it should be ignored.
2133
t = get_transport('.')
2134
t.put_bytes('test.kndx', '')
2136
knit = self.make_test_knit()
2138
def test_knit_index_checks_header(self):
2139
t = get_transport('.')
2140
t.put_bytes('test.kndx', '# not really a knit header\n\n')
2142
self.assertRaises(KnitHeaderError, self.make_test_knit)
2145
class TestGraphIndexKnit(KnitTests):
2146
"""Tests for knits using a GraphIndex rather than a KnitIndex."""
2148
def make_g_index(self, name, ref_lists=0, nodes=[]):
2149
builder = GraphIndexBuilder(ref_lists)
2150
for node, references, value in nodes:
2151
builder.add_node(node, references, value)
2152
stream = builder.finish()
2153
trans = self.get_transport()
2154
size = trans.put_file(name, stream)
2155
return GraphIndex(trans, name, size)
2157
def two_graph_index(self, deltas=False, catch_adds=False):
2158
"""Build a two-graph index.
2160
:param deltas: If true, use underlying indices with two node-ref
2161
lists and 'parent' set to a delta-compressed against tail.
2163
# build a complex graph across several indices.
2165
# delta compression inn the index
2166
index1 = self.make_g_index('1', 2, [
2167
(('tip', ), 'N0 100', ([('parent', )], [], )),
2168
(('tail', ), '', ([], []))])
2169
index2 = self.make_g_index('2', 2, [
2170
(('parent', ), ' 100 78', ([('tail', ), ('ghost', )], [('tail', )])),
2171
(('separate', ), '', ([], []))])
2173
# just blob location and graph in the index.
2174
index1 = self.make_g_index('1', 1, [
2175
(('tip', ), 'N0 100', ([('parent', )], )),
2176
(('tail', ), '', ([], ))])
2177
index2 = self.make_g_index('2', 1, [
2178
(('parent', ), ' 100 78', ([('tail', ), ('ghost', )], )),
2179
(('separate', ), '', ([], ))])
2180
combined_index = CombinedGraphIndex([index1, index2])
2182
self.combined_index = combined_index
2183
self.caught_entries = []
2184
add_callback = self.catch_add
2187
return KnitGraphIndex(combined_index, deltas=deltas,
2188
add_callback=add_callback)
2190
def test_get_ancestry(self):
2191
# get_ancestry is defined as eliding ghosts, not erroring.
2192
index = self.two_graph_index()
2193
self.assertEqual([], index.get_ancestry([]))
2194
self.assertEqual(['separate'], index.get_ancestry(['separate']))
2195
self.assertEqual(['tail'], index.get_ancestry(['tail']))
2196
self.assertEqual(['tail', 'parent'], index.get_ancestry(['parent']))
2197
self.assertEqual(['tail', 'parent', 'tip'], index.get_ancestry(['tip']))
2198
self.assertTrue(index.get_ancestry(['tip', 'separate']) in
2199
(['tail', 'parent', 'tip', 'separate'],
2200
['separate', 'tail', 'parent', 'tip'],
2202
# and without topo_sort
2203
self.assertEqual(set(['separate']),
2204
set(index.get_ancestry(['separate'], topo_sorted=False)))
2205
self.assertEqual(set(['tail']),
2206
set(index.get_ancestry(['tail'], topo_sorted=False)))
2207
self.assertEqual(set(['tail', 'parent']),
2208
set(index.get_ancestry(['parent'], topo_sorted=False)))
2209
self.assertEqual(set(['tail', 'parent', 'tip']),
2210
set(index.get_ancestry(['tip'], topo_sorted=False)))
2211
self.assertEqual(set(['separate', 'tail', 'parent', 'tip']),
2212
set(index.get_ancestry(['tip', 'separate'])))
2213
# asking for a ghost makes it go boom.
2214
self.assertRaises(errors.RevisionNotPresent, index.get_ancestry, ['ghost'])
2216
def test_get_ancestry_with_ghosts(self):
2217
index = self.two_graph_index()
2218
self.assertEqual([], index.get_ancestry_with_ghosts([]))
2219
self.assertEqual(['separate'], index.get_ancestry_with_ghosts(['separate']))
2220
self.assertEqual(['tail'], index.get_ancestry_with_ghosts(['tail']))
2221
self.assertTrue(index.get_ancestry_with_ghosts(['parent']) in
2222
(['tail', 'ghost', 'parent'],
2223
['ghost', 'tail', 'parent'],
2225
self.assertTrue(index.get_ancestry_with_ghosts(['tip']) in
2226
(['tail', 'ghost', 'parent', 'tip'],
2227
['ghost', 'tail', 'parent', 'tip'],
2229
self.assertTrue(index.get_ancestry_with_ghosts(['tip', 'separate']) in
2230
(['tail', 'ghost', 'parent', 'tip', 'separate'],
2231
['ghost', 'tail', 'parent', 'tip', 'separate'],
2232
['separate', 'tail', 'ghost', 'parent', 'tip'],
2233
['separate', 'ghost', 'tail', 'parent', 'tip'],
2235
# asking for a ghost makes it go boom.
2236
self.assertRaises(errors.RevisionNotPresent, index.get_ancestry_with_ghosts, ['ghost'])
2238
def test_num_versions(self):
2239
index = self.two_graph_index()
2240
self.assertEqual(4, index.num_versions())
2242
def test_get_versions(self):
2243
index = self.two_graph_index()
2244
self.assertEqual(set(['tail', 'tip', 'parent', 'separate']),
2245
set(index.get_versions()))
2247
def test_has_version(self):
2248
index = self.two_graph_index()
2249
self.assertTrue(index.has_version('tail'))
2250
self.assertFalse(index.has_version('ghost'))
2252
def test_get_position(self):
2253
index = self.two_graph_index()
2254
self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position('tip'))
2255
self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position('parent'))
2257
def test_get_method_deltas(self):
2258
index = self.two_graph_index(deltas=True)
2259
self.assertEqual('fulltext', index.get_method('tip'))
2260
self.assertEqual('line-delta', index.get_method('parent'))
2262
def test_get_method_no_deltas(self):
2263
# check that the parent-history lookup is ignored with deltas=False.
2264
index = self.two_graph_index(deltas=False)
2265
self.assertEqual('fulltext', index.get_method('tip'))
2266
self.assertEqual('fulltext', index.get_method('parent'))
2268
def test_get_options_deltas(self):
2269
index = self.two_graph_index(deltas=True)
2270
self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
2271
self.assertEqual(['line-delta'], index.get_options('parent'))
2273
def test_get_options_no_deltas(self):
2274
# check that the parent-history lookup is ignored with deltas=False.
2275
index = self.two_graph_index(deltas=False)
2276
self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
2277
self.assertEqual(['fulltext'], index.get_options('parent'))
2279
def test_get_parents_with_ghosts(self):
2280
index = self.two_graph_index()
2281
self.assertEqual(('tail', 'ghost'), index.get_parents_with_ghosts('parent'))
2282
# and errors on ghosts.
2283
self.assertRaises(errors.RevisionNotPresent,
2284
index.get_parents_with_ghosts, 'ghost')
2286
def test_check_versions_present(self):
2287
# ghosts should not be considered present
2288
index = self.two_graph_index()
2289
self.assertRaises(RevisionNotPresent, index.check_versions_present,
2291
self.assertRaises(RevisionNotPresent, index.check_versions_present,
2293
index.check_versions_present(['tail', 'separate'])
2295
def catch_add(self, entries):
2296
self.caught_entries.append(entries)
2298
def test_add_no_callback_errors(self):
2299
index = self.two_graph_index()
2300
self.assertRaises(errors.ReadOnlyError, index.add_version,
2301
'new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
2303
def test_add_version_smoke(self):
2304
index = self.two_graph_index(catch_adds=True)
2305
index.add_version('new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
2306
self.assertEqual([[(('new', ), 'N50 60', ((('separate',),),))]],
2307
self.caught_entries)
2309
def test_add_version_delta_not_delta_index(self):
2310
index = self.two_graph_index(catch_adds=True)
2311
self.assertRaises(errors.KnitCorrupt, index.add_version,
2312
'new', 'no-eol,line-delta', (None, 0, 100), ['parent'])
2313
self.assertEqual([], self.caught_entries)
2315
def test_add_version_same_dup(self):
2316
index = self.two_graph_index(catch_adds=True)
2317
# options can be spelt two different ways
2318
index.add_version('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])
2319
index.add_version('tip', 'no-eol,fulltext', (None, 0, 100), ['parent'])
2320
# but neither should have added data.
2321
self.assertEqual([[], []], self.caught_entries)
2323
def test_add_version_different_dup(self):
2324
index = self.two_graph_index(deltas=True, catch_adds=True)
2326
self.assertRaises(errors.KnitCorrupt, index.add_version,
2327
'tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])
2328
self.assertRaises(errors.KnitCorrupt, index.add_version,
2329
'tip', 'line-delta,no-eol', (None, 0, 100), ['parent'])
2330
self.assertRaises(errors.KnitCorrupt, index.add_version,
2331
'tip', 'fulltext', (None, 0, 100), ['parent'])
2333
self.assertRaises(errors.KnitCorrupt, index.add_version,
2334
'tip', 'fulltext,no-eol', (None, 50, 100), ['parent'])
2335
self.assertRaises(errors.KnitCorrupt, index.add_version,
2336
'tip', 'fulltext,no-eol', (None, 0, 1000), ['parent'])
2338
self.assertRaises(errors.KnitCorrupt, index.add_version,
2339
'tip', 'fulltext,no-eol', (None, 0, 100), [])
2340
self.assertEqual([], self.caught_entries)
2342
def test_add_versions_nodeltas(self):
2343
index = self.two_graph_index(catch_adds=True)
2344
index.add_versions([
2345
('new', 'fulltext,no-eol', (None, 50, 60), ['separate']),
2346
('new2', 'fulltext', (None, 0, 6), ['new']),
2348
self.assertEqual([(('new', ), 'N50 60', ((('separate',),),)),
2349
(('new2', ), ' 0 6', ((('new',),),))],
2350
sorted(self.caught_entries[0]))
2351
self.assertEqual(1, len(self.caught_entries))
2353
def test_add_versions_deltas(self):
2354
index = self.two_graph_index(deltas=True, catch_adds=True)
2355
index.add_versions([
2356
('new', 'fulltext,no-eol', (None, 50, 60), ['separate']),
2357
('new2', 'line-delta', (None, 0, 6), ['new']),
2359
self.assertEqual([(('new', ), 'N50 60', ((('separate',),), ())),
2360
(('new2', ), ' 0 6', ((('new',),), (('new',),), ))],
2361
sorted(self.caught_entries[0]))
2362
self.assertEqual(1, len(self.caught_entries))
2364
def test_add_versions_delta_not_delta_index(self):
2365
index = self.two_graph_index(catch_adds=True)
2366
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2367
[('new', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
2368
self.assertEqual([], self.caught_entries)
2370
def test_add_versions_random_id_accepted(self):
2371
index = self.two_graph_index(catch_adds=True)
2372
index.add_versions([], random_id=True)
2374
def test_add_versions_same_dup(self):
2375
index = self.two_graph_index(catch_adds=True)
2376
# options can be spelt two different ways
2377
index.add_versions([('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])])
2378
index.add_versions([('tip', 'no-eol,fulltext', (None, 0, 100), ['parent'])])
2379
# but neither should have added data.
2380
self.assertEqual([[], []], self.caught_entries)
2382
def test_add_versions_different_dup(self):
2383
index = self.two_graph_index(deltas=True, catch_adds=True)
2385
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2386
[('tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
2387
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2388
[('tip', 'line-delta,no-eol', (None, 0, 100), ['parent'])])
2389
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2390
[('tip', 'fulltext', (None, 0, 100), ['parent'])])
2392
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2393
[('tip', 'fulltext,no-eol', (None, 50, 100), ['parent'])])
2394
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2395
[('tip', 'fulltext,no-eol', (None, 0, 1000), ['parent'])])
2397
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2398
[('tip', 'fulltext,no-eol', (None, 0, 100), [])])
2399
# change options in the second record
2400
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2401
[('tip', 'fulltext,no-eol', (None, 0, 100), ['parent']),
2402
('tip', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
2403
self.assertEqual([], self.caught_entries)
2405
class TestNoParentsGraphIndexKnit(KnitTests):
2406
"""Tests for knits using KnitGraphIndex with no parents."""
2408
def make_g_index(self, name, ref_lists=0, nodes=[]):
2409
builder = GraphIndexBuilder(ref_lists)
2410
for node, references in nodes:
2411
builder.add_node(node, references)
2412
stream = builder.finish()
2413
trans = self.get_transport()
2414
size = trans.put_file(name, stream)
2415
return GraphIndex(trans, name, size)
2417
def test_parents_deltas_incompatible(self):
2418
index = CombinedGraphIndex([])
2419
self.assertRaises(errors.KnitError, KnitGraphIndex, index,
2420
deltas=True, parents=False)
2422
def two_graph_index(self, catch_adds=False):
2423
"""Build a two-graph index.
2425
:param deltas: If true, use underlying indices with two node-ref
2426
lists and 'parent' set to a delta-compressed against tail.
2428
# put several versions in the index.
2429
index1 = self.make_g_index('1', 0, [
2430
(('tip', ), 'N0 100'),
2432
index2 = self.make_g_index('2', 0, [
2433
(('parent', ), ' 100 78'),
2434
(('separate', ), '')])
2435
combined_index = CombinedGraphIndex([index1, index2])
2437
self.combined_index = combined_index
2438
self.caught_entries = []
2439
add_callback = self.catch_add
2442
return KnitGraphIndex(combined_index, parents=False,
2443
add_callback=add_callback)
2445
def test_get_ancestry(self):
2446
# with no parents, ancestry is always just the key.
2447
index = self.two_graph_index()
2448
self.assertEqual([], index.get_ancestry([]))
2449
self.assertEqual(['separate'], index.get_ancestry(['separate']))
2450
self.assertEqual(['tail'], index.get_ancestry(['tail']))
2451
self.assertEqual(['parent'], index.get_ancestry(['parent']))
2452
self.assertEqual(['tip'], index.get_ancestry(['tip']))
2453
self.assertTrue(index.get_ancestry(['tip', 'separate']) in
2454
(['tip', 'separate'],
2455
['separate', 'tip'],
2457
# asking for a ghost makes it go boom.
2458
self.assertRaises(errors.RevisionNotPresent, index.get_ancestry, ['ghost'])
2460
def test_get_ancestry_with_ghosts(self):
2461
index = self.two_graph_index()
2462
self.assertEqual([], index.get_ancestry_with_ghosts([]))
2463
self.assertEqual(['separate'], index.get_ancestry_with_ghosts(['separate']))
2464
self.assertEqual(['tail'], index.get_ancestry_with_ghosts(['tail']))
2465
self.assertEqual(['parent'], index.get_ancestry_with_ghosts(['parent']))
2466
self.assertEqual(['tip'], index.get_ancestry_with_ghosts(['tip']))
2467
self.assertTrue(index.get_ancestry_with_ghosts(['tip', 'separate']) in
2468
(['tip', 'separate'],
2469
['separate', 'tip'],
2471
# asking for a ghost makes it go boom.
2472
self.assertRaises(errors.RevisionNotPresent, index.get_ancestry_with_ghosts, ['ghost'])
2474
def test_num_versions(self):
2475
index = self.two_graph_index()
2476
self.assertEqual(4, index.num_versions())
2478
def test_get_versions(self):
2479
index = self.two_graph_index()
2480
self.assertEqual(set(['tail', 'tip', 'parent', 'separate']),
2481
set(index.get_versions()))
2483
def test_has_version(self):
2484
index = self.two_graph_index()
2485
self.assertTrue(index.has_version('tail'))
2486
self.assertFalse(index.has_version('ghost'))
2488
def test_get_position(self):
2489
index = self.two_graph_index()
2490
self.assertEqual((index._graph_index._indices[0], 0, 100), index.get_position('tip'))
2491
self.assertEqual((index._graph_index._indices[1], 100, 78), index.get_position('parent'))
2493
def test_get_method(self):
2494
index = self.two_graph_index()
2495
self.assertEqual('fulltext', index.get_method('tip'))
2496
self.assertEqual(['fulltext'], index.get_options('parent'))
2498
def test_get_options(self):
2499
index = self.two_graph_index()
2500
self.assertEqual(['fulltext', 'no-eol'], index.get_options('tip'))
2501
self.assertEqual(['fulltext'], index.get_options('parent'))
2503
def test_get_parents_with_ghosts(self):
2504
index = self.two_graph_index()
2505
self.assertEqual((), index.get_parents_with_ghosts('parent'))
2506
# and errors on ghosts.
2507
self.assertRaises(errors.RevisionNotPresent,
2508
index.get_parents_with_ghosts, 'ghost')
2510
def test_check_versions_present(self):
2511
index = self.two_graph_index()
2512
self.assertRaises(RevisionNotPresent, index.check_versions_present,
2514
self.assertRaises(RevisionNotPresent, index.check_versions_present,
2515
['tail', 'missing'])
2516
index.check_versions_present(['tail', 'separate'])
2518
def catch_add(self, entries):
2519
self.caught_entries.append(entries)
2521
def test_add_no_callback_errors(self):
2522
index = self.two_graph_index()
2523
self.assertRaises(errors.ReadOnlyError, index.add_version,
2524
'new', 'fulltext,no-eol', (None, 50, 60), ['separate'])
2526
def test_add_version_smoke(self):
2527
index = self.two_graph_index(catch_adds=True)
2528
index.add_version('new', 'fulltext,no-eol', (None, 50, 60), [])
2529
self.assertEqual([[(('new', ), 'N50 60')]],
2530
self.caught_entries)
2532
def test_add_version_delta_not_delta_index(self):
2533
index = self.two_graph_index(catch_adds=True)
2534
self.assertRaises(errors.KnitCorrupt, index.add_version,
2535
'new', 'no-eol,line-delta', (None, 0, 100), [])
2536
self.assertEqual([], self.caught_entries)
2538
def test_add_version_same_dup(self):
2539
index = self.two_graph_index(catch_adds=True)
2540
# options can be spelt two different ways
2541
index.add_version('tip', 'fulltext,no-eol', (None, 0, 100), [])
2542
index.add_version('tip', 'no-eol,fulltext', (None, 0, 100), [])
2543
# but neither should have added data.
2544
self.assertEqual([[], []], self.caught_entries)
2546
def test_add_version_different_dup(self):
2547
index = self.two_graph_index(catch_adds=True)
2549
self.assertRaises(errors.KnitCorrupt, index.add_version,
2550
'tip', 'no-eol,line-delta', (None, 0, 100), [])
2551
self.assertRaises(errors.KnitCorrupt, index.add_version,
2552
'tip', 'line-delta,no-eol', (None, 0, 100), [])
2553
self.assertRaises(errors.KnitCorrupt, index.add_version,
2554
'tip', 'fulltext', (None, 0, 100), [])
2556
self.assertRaises(errors.KnitCorrupt, index.add_version,
2557
'tip', 'fulltext,no-eol', (None, 50, 100), [])
2558
self.assertRaises(errors.KnitCorrupt, index.add_version,
2559
'tip', 'fulltext,no-eol', (None, 0, 1000), [])
2561
self.assertRaises(errors.KnitCorrupt, index.add_version,
2562
'tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])
2563
self.assertEqual([], self.caught_entries)
2565
def test_add_versions(self):
2566
index = self.two_graph_index(catch_adds=True)
2567
index.add_versions([
2568
('new', 'fulltext,no-eol', (None, 50, 60), []),
2569
('new2', 'fulltext', (None, 0, 6), []),
2571
self.assertEqual([(('new', ), 'N50 60'), (('new2', ), ' 0 6')],
2572
sorted(self.caught_entries[0]))
2573
self.assertEqual(1, len(self.caught_entries))
2575
def test_add_versions_delta_not_delta_index(self):
2576
index = self.two_graph_index(catch_adds=True)
2577
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2578
[('new', 'no-eol,line-delta', (None, 0, 100), ['parent'])])
2579
self.assertEqual([], self.caught_entries)
2581
def test_add_versions_parents_not_parents_index(self):
2582
index = self.two_graph_index(catch_adds=True)
2583
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2584
[('new', 'no-eol,fulltext', (None, 0, 100), ['parent'])])
2585
self.assertEqual([], self.caught_entries)
2587
def test_add_versions_random_id_accepted(self):
2588
index = self.two_graph_index(catch_adds=True)
2589
index.add_versions([], random_id=True)
2591
def test_add_versions_same_dup(self):
2592
index = self.two_graph_index(catch_adds=True)
2593
# options can be spelt two different ways
2594
index.add_versions([('tip', 'fulltext,no-eol', (None, 0, 100), [])])
2595
index.add_versions([('tip', 'no-eol,fulltext', (None, 0, 100), [])])
2596
# but neither should have added data.
2597
self.assertEqual([[], []], self.caught_entries)
2599
def test_add_versions_different_dup(self):
2600
index = self.two_graph_index(catch_adds=True)
2602
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2603
[('tip', 'no-eol,line-delta', (None, 0, 100), [])])
2604
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2605
[('tip', 'line-delta,no-eol', (None, 0, 100), [])])
2606
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2607
[('tip', 'fulltext', (None, 0, 100), [])])
2609
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2610
[('tip', 'fulltext,no-eol', (None, 50, 100), [])])
2611
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2612
[('tip', 'fulltext,no-eol', (None, 0, 1000), [])])
2614
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2615
[('tip', 'fulltext,no-eol', (None, 0, 100), ['parent'])])
2616
# change options in the second record
2617
self.assertRaises(errors.KnitCorrupt, index.add_versions,
2618
[('tip', 'fulltext,no-eol', (None, 0, 100), []),
2619
('tip', 'no-eol,line-delta', (None, 0, 100), [])])
2620
self.assertEqual([], self.caught_entries)
2622
class TestPackKnits(KnitTests):
2623
"""Tests that use a _PackAccess and KnitGraphIndex."""
2625
def test_get_data_stream_packs_ignores_pack_overhead(self):
2626
# Packs have an encoding overhead that should not be included in the
2627
# 'size' field of a data stream, because it is not returned by the
2628
# raw_reading functions - it is why index_memo's are opaque, and
2629
# get_data_stream was abusing this.
2630
packname = 'test.pack'
2631
transport = self.get_transport()
2632
def write_data(bytes):
2633
transport.append_bytes(packname, bytes)
2634
writer = pack.ContainerWriter(write_data)
2636
index = InMemoryGraphIndex(2)
2637
knit_index = KnitGraphIndex(index, add_callback=index.add_nodes,
2639
indices = {index:(transport, packname)}
2640
access = _PackAccess(indices, writer=(writer, index))
2641
k = KnitVersionedFile('test', get_transport('.'),
2642
delta=True, create=True, index=knit_index, access_method=access)
2643
# insert something into the knit
2644
k.add_lines('text-1', [], ["foo\n"])
2645
# get a data stream for it
2646
stream = k.get_data_stream(['text-1'])
2647
# if the stream has been incorrectly assembled, we will get a short read
2648
# reading from the stream (as streams have no trailer)
2649
expected_length = stream[1][0][2]
2650
# we use -1 to do the read, so that if a trailer is added this test
2651
# will fail and we'll adjust it to handle that case correctly, rather
2652
# than allowing an over-read that is bogus.
2653
self.assertEqual(expected_length, len(stream[2](-1)))
2656
class Test_StreamIndex(KnitTests):
2658
def get_index(self, knit, stream):
2659
"""Get a _StreamIndex from knit and stream."""
2660
return knit._knit_from_datastream(stream)._index
2662
def assertIndexVersions(self, knit, versions):
2663
"""Check that the _StreamIndex versions are those of the stream."""
2664
index = self.get_index(knit, knit.get_data_stream(versions))
2665
self.assertEqual(set(index.get_versions()), set(versions))
2666
# check we didn't get duplicates
2667
self.assertEqual(len(index.get_versions()), len(versions))
2669
def assertIndexAncestry(self, knit, ancestry_versions, versions, result):
2670
"""Check the result of a get_ancestry call on knit."""
2671
index = self.get_index(knit, knit.get_data_stream(versions))
2674
set(index.get_ancestry(ancestry_versions, False)))
2676
def assertGetMethod(self, knit, versions, version, result):
2677
index = self.get_index(knit, knit.get_data_stream(versions))
2678
self.assertEqual(result, index.get_method(version))
2680
def assertGetOptions(self, knit, version, options):
2681
index = self.get_index(knit, knit.get_data_stream(version))
2682
self.assertEqual(options, index.get_options(version))
2684
def assertGetPosition(self, knit, versions, version, result):
2685
index = self.get_index(knit, knit.get_data_stream(versions))
2686
if result[1] is None:
2687
result = (result[0], index, result[2], result[3])
2688
self.assertEqual(result, index.get_position(version))
2690
def assertGetParentsWithGhosts(self, knit, versions, version, parents):
2691
index = self.get_index(knit, knit.get_data_stream(versions))
2692
self.assertEqual(parents, index.get_parents_with_ghosts(version))
2694
def make_knit_with_4_versions_2_dags(self):
2695
knit = self.make_test_knit()
2696
knit.add_lines('a', [], ["foo"])
2697
knit.add_lines('b', [], [])
2698
knit.add_lines('c', ['b', 'a'], [])
2699
knit.add_lines_with_ghosts('d', ['e', 'f'], [])
2702
def test_versions(self):
2703
"""The versions of a StreamIndex are those of the datastream."""
2704
knit = self.make_knit_with_4_versions_2_dags()
2705
# ask for most permutations, which catches bugs like falling back to the
2706
# target knit, or showing ghosts, etc.
2707
self.assertIndexVersions(knit, [])
2708
self.assertIndexVersions(knit, ['a'])
2709
self.assertIndexVersions(knit, ['b'])
2710
self.assertIndexVersions(knit, ['c'])
2711
self.assertIndexVersions(knit, ['d'])
2712
self.assertIndexVersions(knit, ['a', 'b'])
2713
self.assertIndexVersions(knit, ['b', 'c'])
2714
self.assertIndexVersions(knit, ['a', 'c'])
2715
self.assertIndexVersions(knit, ['a', 'b', 'c'])
2716
self.assertIndexVersions(knit, ['a', 'b', 'c', 'd'])
2718
def test_construct(self):
2719
"""Constructing a StreamIndex generates index data."""
2720
data_list = [('text-a', ['fulltext'], 127, []),
2721
('text-b', ['option'], 128, ['text-c'])]
2722
index = _StreamIndex(data_list, None)
2723
self.assertEqual({'text-a':(['fulltext'], (0, 127), []),
2724
'text-b':(['option'], (127, 127 + 128), ['text-c'])},
2727
def test_get_ancestry(self):
2728
knit = self.make_knit_with_4_versions_2_dags()
2729
self.assertIndexAncestry(knit, ['a'], ['a'], ['a'])
2730
self.assertIndexAncestry(knit, ['b'], ['b'], ['b'])
2731
self.assertIndexAncestry(knit, ['c'], ['c'], ['c'])
2732
self.assertIndexAncestry(knit, ['c'], ['a', 'b', 'c'],
2733
set(['a', 'b', 'c']))
2734
self.assertIndexAncestry(knit, ['c', 'd'], ['a', 'b', 'c', 'd'],
2735
set(['a', 'b', 'c', 'd']))
2737
def test_get_method(self):
2738
knit = self.make_knit_with_4_versions_2_dags()
2739
self.assertGetMethod(knit, ['a'], 'a', 'fulltext')
2740
self.assertGetMethod(knit, ['c'], 'c', 'line-delta')
2741
# get_method on a basis that is not in the datastream (but in the
2742
# backing knit) returns 'fulltext', because thats what we'll create as
2744
self.assertGetMethod(knit, ['c'], 'b', 'fulltext')
2746
def test_get_options(self):
2747
knit = self.make_knit_with_4_versions_2_dags()
2748
self.assertGetOptions(knit, 'a', ['no-eol', 'fulltext'])
2749
self.assertGetOptions(knit, 'c', ['line-delta'])
2751
def test_get_parents_with_ghosts(self):
2752
knit = self.make_knit_with_4_versions_2_dags()
2753
self.assertGetParentsWithGhosts(knit, ['a'], 'a', ())
2754
self.assertGetParentsWithGhosts(knit, ['c'], 'c', ('b', 'a'))
2755
self.assertGetParentsWithGhosts(knit, ['d'], 'd', ('e', 'f'))
2757
def test_get_position(self):
2758
knit = self.make_knit_with_4_versions_2_dags()
2759
# get_position returns (thunk_flag, index(can be None), start, end) for
2760
# _StreamAccess to use.
2761
self.assertGetPosition(knit, ['a'], 'a', (False, None, 0, 78))
2762
self.assertGetPosition(knit, ['a', 'c'], 'c', (False, None, 78, 156))
2763
# get_position on a text that is not in the datastream (but in the
2764
# backing knit) returns (True, 'versionid', None, None) - and then the
2765
# access object can construct the relevant data as needed.
2766
self.assertGetPosition(knit, ['a', 'c'], 'b', (True, 'b', None, None))
2769
class Test_StreamAccess(KnitTests):
2771
def get_index_access(self, knit, stream):
2772
"""Get a _StreamAccess from knit and stream."""
2773
knit = knit._knit_from_datastream(stream)
2774
return knit._index, knit._data._access
2776
def assertGetRawRecords(self, knit, versions):
2777
index, access = self.get_index_access(knit,
2778
knit.get_data_stream(versions))
2779
# check that every version asked for can be obtained from the resulting
2783
for version in versions:
2784
memos.append(knit._index.get_position(version))
2786
for version, data in zip(
2787
versions, knit._data._access.get_raw_records(memos)):
2788
original[version] = data
2790
for version in versions:
2791
memos.append(index.get_position(version))
2793
for version, data in zip(versions, access.get_raw_records(memos)):
2794
streamed[version] = data
2795
self.assertEqual(original, streamed)
2797
for version in versions:
2798
data = list(access.get_raw_records(
2799
[index.get_position(version)]))[0]
2800
self.assertEqual(original[version], data)
2802
def make_knit_with_two_versions(self):
2803
knit = self.make_test_knit()
2804
knit.add_lines('a', [], ["foo"])
2805
knit.add_lines('b', [], ["bar"])
2808
def test_get_raw_records(self):
2809
knit = self.make_knit_with_two_versions()
2810
self.assertGetRawRecords(knit, ['a', 'b'])
2811
self.assertGetRawRecords(knit, ['a'])
2812
self.assertGetRawRecords(knit, ['b'])
545
def create_knit(self, cache_add=False):
546
k = self.make_test_knit(True)
550
k.add_lines('text-1', [], split_lines(TEXT_1))
551
k.add_lines('text-2', [], split_lines(TEXT_2))
554
def test_no_caching(self):
555
k = self.create_knit()
556
# Nothing should be cached without setting 'enable_cache'
557
self.assertEqual({}, k._data._cache)
559
def test_cache_add_and_clear(self):
560
k = self.create_knit(True)
562
self.assertEqual(['text-1', 'text-2'], sorted(k._data._cache.keys()))
565
self.assertEqual({}, k._data._cache)
567
def test_cache_data_read_raw(self):
568
k = self.create_knit()
573
def read_one_raw(version):
574
pos_map = k._get_components_positions([version])
575
method, pos, size, next = pos_map[version]
576
lst = list(k._data.read_records_iter_raw([(version, pos, size)]))
577
self.assertEqual(1, len(lst))
580
val = read_one_raw('text-1')
581
self.assertEqual({'text-1':val[1]}, k._data._cache)
584
# After clear, new reads are not cached
585
self.assertEqual({}, k._data._cache)
587
val2 = read_one_raw('text-1')
588
self.assertEqual(val, val2)
589
self.assertEqual({}, k._data._cache)
591
def test_cache_data_read(self):
592
k = self.create_knit()
594
def read_one(version):
595
pos_map = k._get_components_positions([version])
596
method, pos, size, next = pos_map[version]
597
lst = list(k._data.read_records_iter([(version, pos, size)]))
598
self.assertEqual(1, len(lst))
604
val = read_one('text-2')
605
self.assertEqual(['text-2'], k._data._cache.keys())
606
self.assertEqual('text-2', val[0])
607
content, digest = k._data._parse_record('text-2',
608
k._data._cache['text-2'])
609
self.assertEqual(content, val[1])
610
self.assertEqual(digest, val[2])
613
self.assertEqual({}, k._data._cache)
615
val2 = read_one('text-2')
616
self.assertEqual(val, val2)
617
self.assertEqual({}, k._data._cache)
619
def test_cache_read(self):
620
k = self.create_knit()
623
text = k.get_text('text-1')
624
self.assertEqual(TEXT_1, text)
625
self.assertEqual(['text-1'], k._data._cache.keys())
628
self.assertEqual({}, k._data._cache)
630
text = k.get_text('text-1')
631
self.assertEqual(TEXT_1, text)
632
self.assertEqual({}, k._data._cache)
2814
def test_get_raw_record_from_backing_knit(self):
2815
# the thunk layer should create an artificial A on-demand when needed.
2816
source_knit = self.make_test_knit(name='plain', annotate=False)
2817
target_knit = self.make_test_knit(name='annotated', annotate=True)
2818
source_knit.add_lines("A", [], ["Foo\n"])
2819
# Give the target A, so we can try to thunk across to it.
2820
target_knit.join(source_knit)
2821
index, access = self.get_index_access(target_knit,
2822
source_knit.get_data_stream([]))
2823
raw_data = list(access.get_raw_records([(True, "A", None, None)]))[0]
2824
df = GzipFile(mode='rb', fileobj=StringIO(raw_data))
2826
'version A 1 5d36b88bb697a2d778f024048bafabd443d74503\n'
2830
def test_asking_for_thunk_stream_is_not_plain_errors(self):
2831
knit = self.make_test_knit(name='annotated', annotate=True)
2832
knit.add_lines("A", [], ["Foo\n"])
2833
index, access = self.get_index_access(knit,
2834
knit.get_data_stream([]))
2835
self.assertRaises(errors.KnitCorrupt,
2836
list, access.get_raw_records([(True, "A", None, None)]))
2839
class TestFormatSignatures(KnitTests):
2841
def test_knit_format_signatures(self):
2842
"""Different formats of knit have different signature strings."""
2843
knit = self.make_test_knit(name='a', annotate=True)
2844
self.assertEqual('knit-annotated', knit.get_format_signature())
2845
knit = self.make_test_knit(name='p', annotate=False)
2846
self.assertEqual('knit-plain', knit.get_format_signature())