16
16
# You should have received a copy of the GNU General Public License
17
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
21
# TODO: might be nice to create a versionedfile with some type of corruption
22
22
# considered typical and check that it can be detected/corrected.
24
from gzip import GzipFile
25
from itertools import chain, izip
24
from itertools import chain
26
25
from StringIO import StringIO
28
28
from bzrlib import (
38
33
from bzrlib.errors import (
39
34
RevisionNotPresent,
40
35
RevisionAlreadyPresent,
38
from bzrlib import knit as _mod_knit
42
39
from bzrlib.knit import (
46
from bzrlib.symbol_versioning import one_four, one_five
47
47
from bzrlib.tests import (
49
48
TestCaseWithMemoryTransport,
52
split_suite_by_condition,
53
55
from bzrlib.tests.http_utils import TestCaseWithWebserver
56
from bzrlib.trace import mutter
57
from bzrlib.transport import get_transport
54
58
from bzrlib.transport.memory import MemoryTransport
59
from bzrlib.tsort import topo_sort
60
from bzrlib.tuned_gzip import GzipFile
55
61
import bzrlib.versionedfile as versionedfile
56
62
from bzrlib.versionedfile import (
58
64
HashEscapedPrefixMapper,
60
VirtualVersionedFiles,
61
66
make_versioned_files_factory,
63
68
from bzrlib.weave import WeaveFile
64
from bzrlib.weavefile import write_weave
65
from bzrlib.tests.scenarios import load_tests_apply_scenarios
68
load_tests = load_tests_apply_scenarios
69
from bzrlib.weavefile import read_weave, write_weave
72
def load_tests(standard_tests, module, loader):
73
"""Parameterize VersionedFiles tests for different implementations."""
74
to_adapt, result = split_suite_by_condition(
75
standard_tests, condition_isinstance(TestVersionedFiles))
76
len_one_adapter = TestScenarioApplier()
77
len_two_adapter = TestScenarioApplier()
78
# We want to be sure of behaviour for:
79
# weaves prefix layout (weave texts)
80
# individually named weaves (weave inventories)
81
# annotated knits - prefix|hash|hash-escape layout, we test the third only
82
# as it is the most complex mapper.
83
# individually named knits
84
# individual no-graph knits in packs (signatures)
85
# individual graph knits in packs (inventories)
86
# individual graph nocompression knits in packs (revisions)
87
# plain text knits in packs (texts)
88
len_one_adapter.scenarios = [
91
'factory':make_versioned_files_factory(WeaveFile,
92
ConstantMapper('inventory')),
98
'factory':make_file_factory(False, ConstantMapper('revisions')),
102
('named-nograph-knit-pack', {
103
'cleanup':cleanup_pack_knit,
104
'factory':make_pack_factory(False, False, 1),
108
('named-graph-knit-pack', {
109
'cleanup':cleanup_pack_knit,
110
'factory':make_pack_factory(True, True, 1),
114
('named-graph-nodelta-knit-pack', {
115
'cleanup':cleanup_pack_knit,
116
'factory':make_pack_factory(True, False, 1),
121
len_two_adapter.scenarios = [
124
'factory':make_versioned_files_factory(WeaveFile,
129
('annotated-knit-escape', {
131
'factory':make_file_factory(True, HashEscapedPrefixMapper()),
135
('plain-knit-pack', {
136
'cleanup':cleanup_pack_knit,
137
'factory':make_pack_factory(True, True, 2),
142
for test in iter_suite_tests(to_adapt):
143
result.addTests(len_one_adapter.adapt(test))
144
result.addTests(len_two_adapter.adapt(test))
71
148
def get_diamond_vf(f, trailing_eol=True, left_only=False):
72
149
"""Get a diamond graph to exercise deltas and merges.
74
151
:param trailing_eol: If True end the last line with \n.
133
206
result = [prefix + suffix for suffix in suffix_list]
140
208
# we loop over each key because that spreads the inserts across prefixes,
141
209
# which is how commit operates.
142
210
for prefix in prefixes:
143
result.append(files.add_lines(prefix + get_key('origin'), (),
211
result.append(files.add_lines(prefix + ('origin',), (),
144
212
['origin' + last_char]))
145
213
for prefix in prefixes:
146
result.append(files.add_lines(prefix + get_key('base'),
214
result.append(files.add_lines(prefix + ('base',),
147
215
get_parents([('origin',)]), ['base' + last_char]))
148
216
for prefix in prefixes:
149
result.append(files.add_lines(prefix + get_key('left'),
217
result.append(files.add_lines(prefix + ('left',),
150
218
get_parents([('base',)]),
151
219
['base\n', 'left' + last_char]))
152
220
if not left_only:
153
221
for prefix in prefixes:
154
result.append(files.add_lines(prefix + get_key('right'),
222
result.append(files.add_lines(prefix + ('right',),
155
223
get_parents([('base',)]),
156
224
['base\n', 'right' + last_char]))
157
225
for prefix in prefixes:
158
result.append(files.add_lines(prefix + get_key('merged'),
226
result.append(files.add_lines(prefix + ('merged',),
159
227
get_parents([('left',), ('right',)]),
160
228
['base\n', 'left\n', 'right\n', 'merged' + last_char]))
671
740
self.assertEqual(expected, progress.updates)
673
742
lines = iter_with_versions(['child', 'otherchild'],
674
[('Walking content', 0, 2),
675
('Walking content', 1, 2),
676
('Walking content', 2, 2)])
743
[('Walking content.', 0, 2),
744
('Walking content.', 1, 2),
745
('Walking content.', 2, 2)])
677
746
# we must see child and otherchild
678
747
self.assertTrue(lines[('child\n', 'child')] > 0)
679
748
self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
680
749
# we dont care if we got more than that.
683
lines = iter_with_versions(None, [('Walking content', 0, 5),
684
('Walking content', 1, 5),
685
('Walking content', 2, 5),
686
('Walking content', 3, 5),
687
('Walking content', 4, 5),
688
('Walking content', 5, 5)])
752
lines = iter_with_versions(None, [('Walking content.', 0, 5),
753
('Walking content.', 1, 5),
754
('Walking content.', 2, 5),
755
('Walking content.', 3, 5),
756
('Walking content.', 4, 5),
757
('Walking content.', 5, 5)])
689
758
# all lines must be seen at least once
690
759
self.assertTrue(lines[('base\n', 'base')] > 0)
691
760
self.assertTrue(lines[('lancestor\n', 'lancestor')] > 0)
1377
1431
class TestVersionedFiles(TestCaseWithMemoryTransport):
1378
1432
"""Tests for the multiple-file variant of VersionedFile."""
1380
# We want to be sure of behaviour for:
1381
# weaves prefix layout (weave texts)
1382
# individually named weaves (weave inventories)
1383
# annotated knits - prefix|hash|hash-escape layout, we test the third only
1384
# as it is the most complex mapper.
1385
# individually named knits
1386
# individual no-graph knits in packs (signatures)
1387
# individual graph knits in packs (inventories)
1388
# individual graph nocompression knits in packs (revisions)
1389
# plain text knits in packs (texts)
1390
len_one_scenarios = [
1393
'factory':make_versioned_files_factory(WeaveFile,
1394
ConstantMapper('inventory')),
1397
'support_partial_insertion': False,
1401
'factory':make_file_factory(False, ConstantMapper('revisions')),
1404
'support_partial_insertion': False,
1406
('named-nograph-nodelta-knit-pack', {
1407
'cleanup':cleanup_pack_knit,
1408
'factory':make_pack_factory(False, False, 1),
1411
'support_partial_insertion': False,
1413
('named-graph-knit-pack', {
1414
'cleanup':cleanup_pack_knit,
1415
'factory':make_pack_factory(True, True, 1),
1418
'support_partial_insertion': True,
1420
('named-graph-nodelta-knit-pack', {
1421
'cleanup':cleanup_pack_knit,
1422
'factory':make_pack_factory(True, False, 1),
1425
'support_partial_insertion': False,
1427
('groupcompress-nograph', {
1428
'cleanup':groupcompress.cleanup_pack_group,
1429
'factory':groupcompress.make_pack_factory(False, False, 1),
1432
'support_partial_insertion':False,
1435
len_two_scenarios = [
1438
'factory':make_versioned_files_factory(WeaveFile,
1442
'support_partial_insertion': False,
1444
('annotated-knit-escape', {
1446
'factory':make_file_factory(True, HashEscapedPrefixMapper()),
1449
'support_partial_insertion': False,
1451
('plain-knit-pack', {
1452
'cleanup':cleanup_pack_knit,
1453
'factory':make_pack_factory(True, True, 2),
1456
'support_partial_insertion': True,
1459
'cleanup':groupcompress.cleanup_pack_group,
1460
'factory':groupcompress.make_pack_factory(True, False, 1),
1463
'support_partial_insertion':False,
1467
scenarios = len_one_scenarios + len_two_scenarios
1469
1434
def get_versionedfiles(self, relpath='files'):
1470
1435
transport = self.get_transport(relpath)
1471
1436
if relpath != '.':
1472
1437
transport.mkdir('.')
1473
1438
files = self.factory(transport)
1474
1439
if self.cleanup is not None:
1475
self.addCleanup(self.cleanup, files)
1440
self.addCleanup(lambda:self.cleanup(files))
1478
def get_simple_key(self, suffix):
1479
"""Return a key for the object under test."""
1480
if self.key_length == 1:
1483
return ('FileA',) + (suffix,)
1485
def test_add_lines(self):
1486
f = self.get_versionedfiles()
1487
key0 = self.get_simple_key('r0')
1488
key1 = self.get_simple_key('r1')
1489
key2 = self.get_simple_key('r2')
1490
keyf = self.get_simple_key('foo')
1491
f.add_lines(key0, [], ['a\n', 'b\n'])
1493
f.add_lines(key1, [key0], ['b\n', 'c\n'])
1495
f.add_lines(key1, [], ['b\n', 'c\n'])
1497
self.assertTrue(key0 in keys)
1498
self.assertTrue(key1 in keys)
1500
for record in f.get_record_stream([key0, key1], 'unordered', True):
1501
records.append((record.key, record.get_bytes_as('fulltext')))
1503
self.assertEqual([(key0, 'a\nb\n'), (key1, 'b\nc\n')], records)
1505
def test__add_text(self):
1506
f = self.get_versionedfiles()
1507
key0 = self.get_simple_key('r0')
1508
key1 = self.get_simple_key('r1')
1509
key2 = self.get_simple_key('r2')
1510
keyf = self.get_simple_key('foo')
1511
f._add_text(key0, [], 'a\nb\n')
1513
f._add_text(key1, [key0], 'b\nc\n')
1515
f._add_text(key1, [], 'b\nc\n')
1517
self.assertTrue(key0 in keys)
1518
self.assertTrue(key1 in keys)
1520
for record in f.get_record_stream([key0, key1], 'unordered', True):
1521
records.append((record.key, record.get_bytes_as('fulltext')))
1523
self.assertEqual([(key0, 'a\nb\n'), (key1, 'b\nc\n')], records)
1525
1443
def test_annotate(self):
1526
1444
files = self.get_versionedfiles()
1527
1445
self.get_diamond_files(files)
1561
1479
self.assertRaises(RevisionNotPresent,
1562
1480
files.annotate, prefix + ('missing-key',))
1564
def test_check_no_parameters(self):
1565
files = self.get_versionedfiles()
1567
def test_check_progressbar_parameter(self):
1568
"""A progress bar can be supplied because check can be a generator."""
1569
pb = ui.ui_factory.nested_progress_bar()
1570
self.addCleanup(pb.finished)
1571
files = self.get_versionedfiles()
1572
files.check(progress_bar=pb)
1574
def test_check_with_keys_becomes_generator(self):
1575
files = self.get_versionedfiles()
1576
self.get_diamond_files(files)
1578
entries = files.check(keys=keys)
1580
# Texts output should be fulltexts.
1581
self.capture_stream(files, entries, seen.add,
1582
files.get_parent_map(keys), require_fulltext=True)
1583
# All texts should be output.
1584
self.assertEqual(set(keys), seen)
1586
def test_clear_cache(self):
1587
files = self.get_versionedfiles()
1590
1482
def test_construct(self):
1591
1483
"""Each parameterised test can be constructed on a transport."""
1592
1484
files = self.get_versionedfiles()
1594
def get_diamond_files(self, files, trailing_eol=True, left_only=False,
1486
def get_diamond_files(self, files, trailing_eol=True, left_only=False):
1596
1487
return get_diamond_files(files, self.key_length,
1597
1488
trailing_eol=trailing_eol, nograph=not self.graph,
1598
left_only=left_only, nokeys=nokeys)
1600
def _add_content_nostoresha(self, add_lines):
1601
"""When nostore_sha is supplied using old content raises."""
1602
vf = self.get_versionedfiles()
1603
empty_text = ('a', [])
1604
sample_text_nl = ('b', ["foo\n", "bar\n"])
1605
sample_text_no_nl = ('c', ["foo\n", "bar"])
1607
for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
1609
sha, _, _ = vf.add_lines(self.get_simple_key(version), [],
1612
sha, _, _ = vf._add_text(self.get_simple_key(version), [],
1615
# we now have a copy of all the lines in the vf.
1616
for sha, (version, lines) in zip(
1617
shas, (empty_text, sample_text_nl, sample_text_no_nl)):
1618
new_key = self.get_simple_key(version + "2")
1619
self.assertRaises(errors.ExistingContent,
1620
vf.add_lines, new_key, [], lines,
1622
self.assertRaises(errors.ExistingContent,
1623
vf._add_text, new_key, [], ''.join(lines),
1625
# and no new version should have been added.
1626
record = vf.get_record_stream([new_key], 'unordered', True).next()
1627
self.assertEqual('absent', record.storage_kind)
1629
def test_add_lines_nostoresha(self):
1630
self._add_content_nostoresha(add_lines=True)
1632
def test__add_text_nostoresha(self):
1633
self._add_content_nostoresha(add_lines=False)
1489
left_only=left_only)
1635
1491
def test_add_lines_return(self):
1636
1492
files = self.get_versionedfiles()
1663
1519
('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
1666
def test_add_lines_no_key_generates_chk_key(self):
1667
files = self.get_versionedfiles()
1668
# save code by using the stock data insertion helper.
1669
adds = self.get_diamond_files(files, nokeys=True)
1671
# We can only validate the first 2 elements returned from add_lines.
1673
self.assertEqual(3, len(add))
1674
results.append(add[:2])
1675
if self.key_length == 1:
1677
('00e364d235126be43292ab09cb4686cf703ddc17', 7),
1678
('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
1679
('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
1680
('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
1681
('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
1683
# Check the added items got CHK keys.
1684
self.assertEqual(set([
1685
('sha1:00e364d235126be43292ab09cb4686cf703ddc17',),
1686
('sha1:51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44',),
1687
('sha1:9ef09dfa9d86780bdec9219a22560c6ece8e0ef1',),
1688
('sha1:a8478686da38e370e32e42e8a0c220e33ee9132f',),
1689
('sha1:ed8bce375198ea62444dc71952b22cfc2b09226d',),
1692
elif self.key_length == 2:
1694
('00e364d235126be43292ab09cb4686cf703ddc17', 7),
1695
('00e364d235126be43292ab09cb4686cf703ddc17', 7),
1696
('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
1697
('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
1698
('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
1699
('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
1700
('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
1701
('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
1702
('ed8bce375198ea62444dc71952b22cfc2b09226d', 23),
1703
('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
1705
# Check the added items got CHK keys.
1706
self.assertEqual(set([
1707
('FileA', 'sha1:00e364d235126be43292ab09cb4686cf703ddc17'),
1708
('FileA', 'sha1:51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44'),
1709
('FileA', 'sha1:9ef09dfa9d86780bdec9219a22560c6ece8e0ef1'),
1710
('FileA', 'sha1:a8478686da38e370e32e42e8a0c220e33ee9132f'),
1711
('FileA', 'sha1:ed8bce375198ea62444dc71952b22cfc2b09226d'),
1712
('FileB', 'sha1:00e364d235126be43292ab09cb4686cf703ddc17'),
1713
('FileB', 'sha1:51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44'),
1714
('FileB', 'sha1:9ef09dfa9d86780bdec9219a22560c6ece8e0ef1'),
1715
('FileB', 'sha1:a8478686da38e370e32e42e8a0c220e33ee9132f'),
1716
('FileB', 'sha1:ed8bce375198ea62444dc71952b22cfc2b09226d'),
1720
1522
def test_empty_lines(self):
1721
1523
"""Empty files can be stored."""
1722
1524
f = self.get_versionedfiles()
1744
1546
f.get_record_stream([key_b], 'unordered', True
1745
1547
).next().get_bytes_as('fulltext'))
1747
def test_get_known_graph_ancestry(self):
1748
f = self.get_versionedfiles()
1750
raise TestNotApplicable('ancestry info only relevant with graph.')
1751
key_a = self.get_simple_key('a')
1752
key_b = self.get_simple_key('b')
1753
key_c = self.get_simple_key('c')
1759
f.add_lines(key_a, [], ['\n'])
1760
f.add_lines(key_b, [key_a], ['\n'])
1761
f.add_lines(key_c, [key_a, key_b], ['\n'])
1762
kg = f.get_known_graph_ancestry([key_c])
1763
self.assertIsInstance(kg, _mod_graph.KnownGraph)
1764
self.assertEqual([key_a, key_b, key_c], list(kg.topo_sort()))
1766
def test_known_graph_with_fallbacks(self):
1767
f = self.get_versionedfiles('files')
1769
raise TestNotApplicable('ancestry info only relevant with graph.')
1770
if getattr(f, 'add_fallback_versioned_files', None) is None:
1771
raise TestNotApplicable("%s doesn't support fallbacks"
1772
% (f.__class__.__name__,))
1773
key_a = self.get_simple_key('a')
1774
key_b = self.get_simple_key('b')
1775
key_c = self.get_simple_key('c')
1776
# A only in fallback
1781
g = self.get_versionedfiles('fallback')
1782
g.add_lines(key_a, [], ['\n'])
1783
f.add_fallback_versioned_files(g)
1784
f.add_lines(key_b, [key_a], ['\n'])
1785
f.add_lines(key_c, [key_a, key_b], ['\n'])
1786
kg = f.get_known_graph_ancestry([key_c])
1787
self.assertEqual([key_a, key_b, key_c], list(kg.topo_sort()))
1789
1549
def test_get_record_stream_empty(self):
1790
1550
"""An empty stream can be requested without error."""
1791
1551
f = self.get_versionedfiles()
1796
1556
"""Assert that storage_kind is a valid storage_kind."""
1797
1557
self.assertSubset([storage_kind],
1798
1558
['mpdiff', 'knit-annotated-ft', 'knit-annotated-delta',
1799
'knit-ft', 'knit-delta', 'chunked', 'fulltext',
1800
'knit-annotated-ft-gz', 'knit-annotated-delta-gz', 'knit-ft-gz',
1802
'knit-delta-closure', 'knit-delta-closure-ref',
1803
'groupcompress-block', 'groupcompress-block-ref'])
1559
'knit-ft', 'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
1560
'knit-annotated-delta-gz', 'knit-ft-gz', 'knit-delta-gz'])
1805
def capture_stream(self, f, entries, on_seen, parents,
1806
require_fulltext=False):
1562
def capture_stream(self, f, entries, on_seen, parents):
1807
1563
"""Capture a stream for testing."""
1808
1564
for factory in entries:
1809
1565
on_seen(factory.key)
1810
1566
self.assertValidStorageKind(factory.storage_kind)
1811
if factory.sha1 is not None:
1812
self.assertEqual(f.get_sha1s([factory.key])[factory.key],
1567
self.assertEqual(f.get_sha1s([factory.key])[factory.key],
1814
1569
self.assertEqual(parents[factory.key], factory.parents)
1815
1570
self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
1817
if require_fulltext:
1818
factory.get_bytes_as('fulltext')
1820
1573
def test_get_record_stream_interface(self):
1821
1574
"""each item in a stream has to provide a regular interface."""
1894
1634
[None, files.get_sha1s([factory.key])[factory.key]])
1895
1635
self.assertEqual(parent_map[factory.key], factory.parents)
1896
1636
# self.assertEqual(files.get_text(factory.key),
1897
ft_bytes = factory.get_bytes_as('fulltext')
1898
self.assertIsInstance(ft_bytes, str)
1899
chunked_bytes = factory.get_bytes_as('chunked')
1900
self.assertEqualDiff(ft_bytes, ''.join(chunked_bytes))
1902
self.assertStreamOrder(sort_order, seen, keys)
1904
def test_get_record_stream_interface_groupcompress(self):
1905
"""each item in a stream has to provide a regular interface."""
1906
files = self.get_versionedfiles()
1907
self.get_diamond_files(files)
1908
keys, sort_order = self.get_keys_and_groupcompress_sort_order()
1909
parent_map = files.get_parent_map(keys)
1910
entries = files.get_record_stream(keys, 'groupcompress', False)
1912
self.capture_stream(files, entries, seen.append, parent_map)
1637
self.assertIsInstance(factory.get_bytes_as('fulltext'), str)
1638
self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
1913
1640
self.assertStreamOrder(sort_order, seen, keys)
1915
1642
def assertStreamOrder(self, sort_order, seen, keys):
1978
1704
entries = files.get_record_stream(keys, 'topological', False)
1979
1705
self.assertAbsentRecord(files, keys, parent_map, entries)
1981
def assertRecordHasContent(self, record, bytes):
1982
"""Assert that record has the bytes bytes."""
1983
self.assertEqual(bytes, record.get_bytes_as('fulltext'))
1984
self.assertEqual(bytes, ''.join(record.get_bytes_as('chunked')))
1986
def test_get_record_stream_native_formats_are_wire_ready_one_ft(self):
1987
files = self.get_versionedfiles()
1988
key = self.get_simple_key('foo')
1989
files.add_lines(key, (), ['my text\n', 'content'])
1990
stream = files.get_record_stream([key], 'unordered', False)
1991
record = stream.next()
1992
if record.storage_kind in ('chunked', 'fulltext'):
1993
# chunked and fulltext representations are for direct use not wire
1994
# serialisation: check they are able to be used directly. To send
1995
# such records over the wire translation will be needed.
1996
self.assertRecordHasContent(record, "my text\ncontent")
1998
bytes = [record.get_bytes_as(record.storage_kind)]
1999
network_stream = versionedfile.NetworkRecordStream(bytes).read()
2000
source_record = record
2002
for record in network_stream:
2003
records.append(record)
2004
self.assertEqual(source_record.storage_kind,
2005
record.storage_kind)
2006
self.assertEqual(source_record.parents, record.parents)
2008
source_record.get_bytes_as(source_record.storage_kind),
2009
record.get_bytes_as(record.storage_kind))
2010
self.assertEqual(1, len(records))
2012
def assertStreamMetaEqual(self, records, expected, stream):
2013
"""Assert that streams expected and stream have the same records.
2015
:param records: A list to collect the seen records.
2016
:return: A generator of the records in stream.
2018
# We make assertions during copying to catch things early for
2020
for record, ref_record in izip(stream, expected):
2021
records.append(record)
2022
self.assertEqual(ref_record.key, record.key)
2023
self.assertEqual(ref_record.storage_kind, record.storage_kind)
2024
self.assertEqual(ref_record.parents, record.parents)
2027
def stream_to_bytes_or_skip_counter(self, skipped_records, full_texts,
2029
"""Convert a stream to a bytes iterator.
2031
:param skipped_records: A list with one element to increment when a
2033
:param full_texts: A dict from key->fulltext representation, for
2034
checking chunked or fulltext stored records.
2035
:param stream: A record_stream.
2036
:return: An iterator over the bytes of each record.
2038
for record in stream:
2039
if record.storage_kind in ('chunked', 'fulltext'):
2040
skipped_records[0] += 1
2041
# check the content is correct for direct use.
2042
self.assertRecordHasContent(record, full_texts[record.key])
2044
yield record.get_bytes_as(record.storage_kind)
2046
def test_get_record_stream_native_formats_are_wire_ready_ft_delta(self):
2047
files = self.get_versionedfiles()
2048
target_files = self.get_versionedfiles('target')
2049
key = self.get_simple_key('ft')
2050
key_delta = self.get_simple_key('delta')
2051
files.add_lines(key, (), ['my text\n', 'content'])
2053
delta_parents = (key,)
2056
files.add_lines(key_delta, delta_parents, ['different\n', 'content\n'])
2057
local = files.get_record_stream([key, key_delta], 'unordered', False)
2058
ref = files.get_record_stream([key, key_delta], 'unordered', False)
2059
skipped_records = [0]
2061
key: "my text\ncontent",
2062
key_delta: "different\ncontent\n",
2064
byte_stream = self.stream_to_bytes_or_skip_counter(
2065
skipped_records, full_texts, local)
2066
network_stream = versionedfile.NetworkRecordStream(byte_stream).read()
2068
# insert the stream from the network into a versioned files object so we can
2069
# check the content was carried across correctly without doing delta
2071
target_files.insert_record_stream(
2072
self.assertStreamMetaEqual(records, ref, network_stream))
2073
# No duplicates on the wire thank you!
2074
self.assertEqual(2, len(records) + skipped_records[0])
2076
# if any content was copied it all must have all been.
2077
self.assertIdenticalVersionedFile(files, target_files)
2079
def test_get_record_stream_native_formats_are_wire_ready_delta(self):
2080
# copy a delta over the wire
2081
files = self.get_versionedfiles()
2082
target_files = self.get_versionedfiles('target')
2083
key = self.get_simple_key('ft')
2084
key_delta = self.get_simple_key('delta')
2085
files.add_lines(key, (), ['my text\n', 'content'])
2087
delta_parents = (key,)
2090
files.add_lines(key_delta, delta_parents, ['different\n', 'content\n'])
2091
# Copy the basis text across so we can reconstruct the delta during
2092
# insertion into target.
2093
target_files.insert_record_stream(files.get_record_stream([key],
2094
'unordered', False))
2095
local = files.get_record_stream([key_delta], 'unordered', False)
2096
ref = files.get_record_stream([key_delta], 'unordered', False)
2097
skipped_records = [0]
2099
key_delta: "different\ncontent\n",
2101
byte_stream = self.stream_to_bytes_or_skip_counter(
2102
skipped_records, full_texts, local)
2103
network_stream = versionedfile.NetworkRecordStream(byte_stream).read()
2105
# insert the stream from the network into a versioned files object so we can
2106
# check the content was carried across correctly without doing delta
2107
# inspection during check_stream.
2108
target_files.insert_record_stream(
2109
self.assertStreamMetaEqual(records, ref, network_stream))
2110
# No duplicates on the wire thank you!
2111
self.assertEqual(1, len(records) + skipped_records[0])
2113
# if any content was copied it all must have all been
2114
self.assertIdenticalVersionedFile(files, target_files)
2116
def test_get_record_stream_wire_ready_delta_closure_included(self):
2117
# copy a delta over the wire with the ability to get its full text.
2118
files = self.get_versionedfiles()
2119
key = self.get_simple_key('ft')
2120
key_delta = self.get_simple_key('delta')
2121
files.add_lines(key, (), ['my text\n', 'content'])
2123
delta_parents = (key,)
2126
files.add_lines(key_delta, delta_parents, ['different\n', 'content\n'])
2127
local = files.get_record_stream([key_delta], 'unordered', True)
2128
ref = files.get_record_stream([key_delta], 'unordered', True)
2129
skipped_records = [0]
2131
key_delta: "different\ncontent\n",
2133
byte_stream = self.stream_to_bytes_or_skip_counter(
2134
skipped_records, full_texts, local)
2135
network_stream = versionedfile.NetworkRecordStream(byte_stream).read()
2137
# insert the stream from the network into a versioned files object so we can
2138
# check the content was carried across correctly without doing delta
2139
# inspection during check_stream.
2140
for record in self.assertStreamMetaEqual(records, ref, network_stream):
2141
# we have to be able to get the full text out:
2142
self.assertRecordHasContent(record, full_texts[record.key])
2143
# No duplicates on the wire thank you!
2144
self.assertEqual(1, len(records) + skipped_records[0])
2146
1707
def assertAbsentRecord(self, files, keys, parents, entries):
2147
1708
"""Helper for test_get_record_stream_missing_records_are_absent."""
2441
1965
self.assertIdenticalVersionedFile(source, files)
2443
def test_insert_record_stream_long_parent_chain_out_of_order(self):
2444
"""An out of order stream can either error or work."""
2446
raise TestNotApplicable('ancestry info only relevant with graph.')
2447
# Create a reasonably long chain of records based on each other, where
2448
# most will be deltas.
2449
source = self.get_versionedfiles('source')
2452
content = [('same same %d\n' % n) for n in range(500)]
2453
for letter in 'abcdefghijklmnopqrstuvwxyz':
2454
key = ('key-' + letter,)
2455
if self.key_length == 2:
2456
key = ('prefix',) + key
2457
content.append('content for ' + letter + '\n')
2458
source.add_lines(key, parents, content)
2461
# Create a stream of these records, excluding the first record that the
2462
# rest ultimately depend upon, and insert it into a new vf.
2464
for key in reversed(keys):
2465
streams.append(source.get_record_stream([key], 'unordered', False))
2466
deltas = chain(*streams[:-1])
2467
files = self.get_versionedfiles()
2469
files.insert_record_stream(deltas)
2470
except RevisionNotPresent:
2471
# Must not have corrupted the file.
2474
# Must only report either just the first key as a missing parent,
2475
# no key as missing (for nodelta scenarios).
2476
missing = set(files.get_missing_compression_parent_keys())
2477
missing.discard(keys[0])
2478
self.assertEqual(set(), missing)
2480
def get_knit_delta_source(self):
2481
"""Get a source that can produce a stream with knit delta records,
2482
regardless of this test's scenario.
2484
mapper = self.get_mapper()
2485
source_transport = self.get_transport('source')
2486
source_transport.mkdir('.')
2487
source = make_file_factory(False, mapper)(source_transport)
2488
get_diamond_files(source, self.key_length, trailing_eol=True,
2489
nograph=False, left_only=False)
2492
1967
def test_insert_record_stream_delta_missing_basis_no_corruption(self):
2493
"""Insertion where a needed basis is not included notifies the caller
2494
of the missing basis. In the meantime a record missing its basis is
2497
source = self.get_knit_delta_source()
2498
keys = [self.get_simple_key('origin'), self.get_simple_key('merged')]
2499
entries = source.get_record_stream(keys, 'unordered', False)
2500
files = self.get_versionedfiles()
2501
if self.support_partial_insertion:
2502
self.assertEqual([],
2503
list(files.get_missing_compression_parent_keys()))
2504
files.insert_record_stream(entries)
2505
missing_bases = files.get_missing_compression_parent_keys()
2506
self.assertEqual(set([self.get_simple_key('left')]),
2508
self.assertEqual(set(keys), set(files.get_parent_map(keys)))
2511
errors.RevisionNotPresent, files.insert_record_stream, entries)
2514
def test_insert_record_stream_delta_missing_basis_can_be_added_later(self):
2515
"""Insertion where a needed basis is not included notifies the caller
2516
of the missing basis. That basis can be added in a second
2517
insert_record_stream call that does not need to repeat records present
2518
in the previous stream. The record(s) that required that basis are
2519
fully inserted once their basis is no longer missing.
2521
if not self.support_partial_insertion:
2522
raise TestNotApplicable(
2523
'versioned file scenario does not support partial insertion')
2524
source = self.get_knit_delta_source()
2525
entries = source.get_record_stream([self.get_simple_key('origin'),
2526
self.get_simple_key('merged')], 'unordered', False)
2527
files = self.get_versionedfiles()
2528
files.insert_record_stream(entries)
2529
missing_bases = files.get_missing_compression_parent_keys()
2530
self.assertEqual(set([self.get_simple_key('left')]),
2532
# 'merged' is inserted (although a commit of a write group involving
2533
# this versionedfiles would fail).
2534
merged_key = self.get_simple_key('merged')
2536
[merged_key], files.get_parent_map([merged_key]).keys())
2537
# Add the full delta closure of the missing records
2538
missing_entries = source.get_record_stream(
2539
missing_bases, 'unordered', True)
2540
files.insert_record_stream(missing_entries)
2541
# Now 'merged' is fully inserted (and a commit would succeed).
2542
self.assertEqual([], list(files.get_missing_compression_parent_keys()))
2544
[merged_key], files.get_parent_map([merged_key]).keys())
1968
"""Insertion where a needed basis is not included aborts safely."""
1969
# We use a knit always here to be sure we are getting a binary delta.
1970
mapper = self.get_mapper()
1971
source_transport = self.get_transport('source')
1972
source_transport.mkdir('.')
1973
source = make_file_factory(False, mapper)(source_transport)
1974
self.get_diamond_files(source)
1975
entries = source.get_record_stream(['origin', 'merged'], 'unordered', False)
1976
files = self.get_versionedfiles()
1977
self.assertRaises(RevisionNotPresent, files.insert_record_stream,
1980
self.assertEqual({}, files.get_parent_map([]))
2547
1982
def test_iter_lines_added_or_present_in_keys(self):
2548
1983
# test that we get at least an equalset of the lines added by
2591
2027
lines = iter_with_keys(
2592
2028
[self.get_simple_key('child'), self.get_simple_key('otherchild')],
2593
[('Walking content', 0, 2),
2594
('Walking content', 1, 2),
2595
('Walking content', 2, 2)])
2029
[('Walking content.', 0, 2),
2030
('Walking content.', 1, 2),
2031
('Walking content.', 2, 2)])
2596
2032
# we must see child and otherchild
2597
2033
self.assertTrue(lines[('child\n', self.get_simple_key('child'))] > 0)
2598
2034
self.assertTrue(
2599
2035
lines[('otherchild\n', self.get_simple_key('otherchild'))] > 0)
2600
2036
# we dont care if we got more than that.
2602
2038
# test all lines
2603
2039
lines = iter_with_keys(files.keys(),
2604
[('Walking content', 0, 5),
2605
('Walking content', 1, 5),
2606
('Walking content', 2, 5),
2607
('Walking content', 3, 5),
2608
('Walking content', 4, 5),
2609
('Walking content', 5, 5)])
2040
[('Walking content.', 0, 5),
2041
('Walking content.', 1, 5),
2042
('Walking content.', 2, 5),
2043
('Walking content.', 3, 5),
2044
('Walking content.', 4, 5),
2045
('Walking content.', 5, 5)])
2610
2046
# all lines must be seen at least once
2611
2047
self.assertTrue(lines[('base\n', self.get_simple_key('base'))] > 0)
2612
2048
self.assertTrue(
2721
2157
key = ('foo', 'bar',)
2722
2158
files.add_lines(key, (), [])
2723
2159
self.assertEqual(set([key]), set(files.keys()))
2726
class VirtualVersionedFilesTests(TestCase):
2727
"""Basic tests for the VirtualVersionedFiles implementations."""
2729
def _get_parent_map(self, keys):
2732
if k in self._parent_map:
2733
ret[k] = self._parent_map[k]
2737
TestCase.setUp(self)
2739
self._parent_map = {}
2740
self.texts = VirtualVersionedFiles(self._get_parent_map,
2743
def test_add_lines(self):
2744
self.assertRaises(NotImplementedError,
2745
self.texts.add_lines, "foo", [], [])
2747
def test_add_mpdiffs(self):
2748
self.assertRaises(NotImplementedError,
2749
self.texts.add_mpdiffs, [])
2751
def test_check_noerrors(self):
2754
def test_insert_record_stream(self):
2755
self.assertRaises(NotImplementedError, self.texts.insert_record_stream,
2758
def test_get_sha1s_nonexistent(self):
2759
self.assertEquals({}, self.texts.get_sha1s([("NONEXISTENT",)]))
2761
def test_get_sha1s(self):
2762
self._lines["key"] = ["dataline1", "dataline2"]
2763
self.assertEquals({("key",): osutils.sha_strings(self._lines["key"])},
2764
self.texts.get_sha1s([("key",)]))
2766
def test_get_parent_map(self):
2767
self._parent_map = {"G": ("A", "B")}
2768
self.assertEquals({("G",): (("A",),("B",))},
2769
self.texts.get_parent_map([("G",), ("L",)]))
2771
def test_get_record_stream(self):
2772
self._lines["A"] = ["FOO", "BAR"]
2773
it = self.texts.get_record_stream([("A",)], "unordered", True)
2775
self.assertEquals("chunked", record.storage_kind)
2776
self.assertEquals("FOOBAR", record.get_bytes_as("fulltext"))
2777
self.assertEquals(["FOO", "BAR"], record.get_bytes_as("chunked"))
2779
def test_get_record_stream_absent(self):
2780
it = self.texts.get_record_stream([("A",)], "unordered", True)
2782
self.assertEquals("absent", record.storage_kind)
2784
def test_iter_lines_added_or_present_in_keys(self):
2785
self._lines["A"] = ["FOO", "BAR"]
2786
self._lines["B"] = ["HEY"]
2787
self._lines["C"] = ["Alberta"]
2788
it = self.texts.iter_lines_added_or_present_in_keys([("A",), ("B",)])
2789
self.assertEquals(sorted([("FOO", "A"), ("BAR", "A"), ("HEY", "B")]),
2793
class TestOrderingVersionedFilesDecorator(TestCaseWithMemoryTransport):
2795
def get_ordering_vf(self, key_priority):
2796
builder = self.make_branch_builder('test')
2797
builder.start_series()
2798
builder.build_snapshot('A', None, [
2799
('add', ('', 'TREE_ROOT', 'directory', None))])
2800
builder.build_snapshot('B', ['A'], [])
2801
builder.build_snapshot('C', ['B'], [])
2802
builder.build_snapshot('D', ['C'], [])
2803
builder.finish_series()
2804
b = builder.get_branch()
2806
self.addCleanup(b.unlock)
2807
vf = b.repository.inventories
2808
return versionedfile.OrderingVersionedFilesDecorator(vf, key_priority)
2810
def test_get_empty(self):
2811
vf = self.get_ordering_vf({})
2812
self.assertEqual([], vf.calls)
2814
def test_get_record_stream_topological(self):
2815
vf = self.get_ordering_vf({('A',): 3, ('B',): 2, ('C',): 4, ('D',): 1})
2816
request_keys = [('B',), ('C',), ('D',), ('A',)]
2817
keys = [r.key for r in vf.get_record_stream(request_keys,
2818
'topological', False)]
2819
# We should have gotten the keys in topological order
2820
self.assertEqual([('A',), ('B',), ('C',), ('D',)], keys)
2821
# And recorded that the request was made
2822
self.assertEqual([('get_record_stream', request_keys, 'topological',
2825
def test_get_record_stream_ordered(self):
2826
vf = self.get_ordering_vf({('A',): 3, ('B',): 2, ('C',): 4, ('D',): 1})
2827
request_keys = [('B',), ('C',), ('D',), ('A',)]
2828
keys = [r.key for r in vf.get_record_stream(request_keys,
2829
'unordered', False)]
2830
# They should be returned based on their priority
2831
self.assertEqual([('D',), ('B',), ('A',), ('C',)], keys)
2832
# And the request recorded
2833
self.assertEqual([('get_record_stream', request_keys, 'unordered',
2836
def test_get_record_stream_implicit_order(self):
2837
vf = self.get_ordering_vf({('B',): 2, ('D',): 1})
2838
request_keys = [('B',), ('C',), ('D',), ('A',)]
2839
keys = [r.key for r in vf.get_record_stream(request_keys,
2840
'unordered', False)]
2841
# A and C are not in the map, so they get sorted to the front. A comes
2842
# before C alphabetically, so it comes back first
2843
self.assertEqual([('A',), ('C',), ('D',), ('B',)], keys)
2844
# And the request recorded
2845
self.assertEqual([('get_record_stream', request_keys, 'unordered',