summaryrefslogtreecommitdiff
path: root/bzrlib/tests/per_versionedfile.py
diff options
context:
space:
mode:
Diffstat (limited to 'bzrlib/tests/per_versionedfile.py')
-rw-r--r--bzrlib/tests/per_versionedfile.py2858
1 files changed, 2858 insertions, 0 deletions
diff --git a/bzrlib/tests/per_versionedfile.py b/bzrlib/tests/per_versionedfile.py
new file mode 100644
index 0000000..1f06c30
--- /dev/null
+++ b/bzrlib/tests/per_versionedfile.py
@@ -0,0 +1,2858 @@
+# Copyright (C) 2006-2011 Canonical Ltd
+#
+# Authors:
+# Johan Rydberg <jrydberg@gnu.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+# TODO: might be nice to create a versionedfile with some type of corruption
+# considered typical and check that it can be detected/corrected.
+
+from gzip import GzipFile
+from itertools import chain, izip
+from StringIO import StringIO
+
+from bzrlib import (
+ errors,
+ graph as _mod_graph,
+ groupcompress,
+ knit as _mod_knit,
+ osutils,
+ progress,
+ transport,
+ ui,
+ )
+from bzrlib.errors import (
+ RevisionNotPresent,
+ RevisionAlreadyPresent,
+ )
+from bzrlib.knit import (
+ cleanup_pack_knit,
+ make_file_factory,
+ make_pack_factory,
+ )
+from bzrlib.tests import (
+ TestCase,
+ TestCaseWithMemoryTransport,
+ TestNotApplicable,
+ TestSkipped,
+ )
+from bzrlib.tests.http_utils import TestCaseWithWebserver
+from bzrlib.transport.memory import MemoryTransport
+import bzrlib.versionedfile as versionedfile
+from bzrlib.versionedfile import (
+ ConstantMapper,
+ HashEscapedPrefixMapper,
+ PrefixMapper,
+ VirtualVersionedFiles,
+ make_versioned_files_factory,
+ )
+from bzrlib.weave import WeaveFile
+from bzrlib.weavefile import write_weave
+from bzrlib.tests.scenarios import load_tests_apply_scenarios
+
+
+load_tests = load_tests_apply_scenarios
+
+
+def get_diamond_vf(f, trailing_eol=True, left_only=False):
+ """Get a diamond graph to exercise deltas and merges.
+
+ :param trailing_eol: If True end the last line with \n.
+ """
+ parents = {
+ 'origin': (),
+ 'base': (('origin',),),
+ 'left': (('base',),),
+ 'right': (('base',),),
+ 'merged': (('left',), ('right',)),
+ }
+ # insert a diamond graph to exercise deltas and merges.
+ if trailing_eol:
+ last_char = '\n'
+ else:
+ last_char = ''
+ f.add_lines('origin', [], ['origin' + last_char])
+ f.add_lines('base', ['origin'], ['base' + last_char])
+ f.add_lines('left', ['base'], ['base\n', 'left' + last_char])
+ if not left_only:
+ f.add_lines('right', ['base'],
+ ['base\n', 'right' + last_char])
+ f.add_lines('merged', ['left', 'right'],
+ ['base\n', 'left\n', 'right\n', 'merged' + last_char])
+ return f, parents
+
+
+def get_diamond_files(files, key_length, trailing_eol=True, left_only=False,
+ nograph=False, nokeys=False):
+ """Get a diamond graph to exercise deltas and merges.
+
+ This creates a 5-node graph in files. If files supports 2-length keys two
+ graphs are made to exercise the support for multiple ids.
+
+ :param trailing_eol: If True end the last line with \n.
+ :param key_length: The length of keys in files. Currently supports length 1
+ and 2 keys.
+ :param left_only: If True do not add the right and merged nodes.
+ :param nograph: If True, do not provide parents to the add_lines calls;
+ this is useful for tests that need inserted data but have graphless
+ stores.
+ :param nokeys: If True, pass None is as the key for all insertions.
+ Currently implies nograph.
+ :return: The results of the add_lines calls.
+ """
+ if nokeys:
+ nograph = True
+ if key_length == 1:
+ prefixes = [()]
+ else:
+ prefixes = [('FileA',), ('FileB',)]
+ # insert a diamond graph to exercise deltas and merges.
+ if trailing_eol:
+ last_char = '\n'
+ else:
+ last_char = ''
+ result = []
+ def get_parents(suffix_list):
+ if nograph:
+ return ()
+ else:
+ result = [prefix + suffix for suffix in suffix_list]
+ return result
+ def get_key(suffix):
+ if nokeys:
+ return (None, )
+ else:
+ return (suffix,)
+ # we loop over each key because that spreads the inserts across prefixes,
+ # which is how commit operates.
+ for prefix in prefixes:
+ result.append(files.add_lines(prefix + get_key('origin'), (),
+ ['origin' + last_char]))
+ for prefix in prefixes:
+ result.append(files.add_lines(prefix + get_key('base'),
+ get_parents([('origin',)]), ['base' + last_char]))
+ for prefix in prefixes:
+ result.append(files.add_lines(prefix + get_key('left'),
+ get_parents([('base',)]),
+ ['base\n', 'left' + last_char]))
+ if not left_only:
+ for prefix in prefixes:
+ result.append(files.add_lines(prefix + get_key('right'),
+ get_parents([('base',)]),
+ ['base\n', 'right' + last_char]))
+ for prefix in prefixes:
+ result.append(files.add_lines(prefix + get_key('merged'),
+ get_parents([('left',), ('right',)]),
+ ['base\n', 'left\n', 'right\n', 'merged' + last_char]))
+ return result
+
+
+class VersionedFileTestMixIn(object):
+ """A mixin test class for testing VersionedFiles.
+
+ This is not an adaptor-style test at this point because
+ theres no dynamic substitution of versioned file implementations,
+ they are strictly controlled by their owning repositories.
+ """
+
+ def get_transaction(self):
+ if not hasattr(self, '_transaction'):
+ self._transaction = None
+ return self._transaction
+
+ def test_add(self):
+ f = self.get_file()
+ f.add_lines('r0', [], ['a\n', 'b\n'])
+ f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
+ def verify_file(f):
+ versions = f.versions()
+ self.assertTrue('r0' in versions)
+ self.assertTrue('r1' in versions)
+ self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
+ self.assertEquals(f.get_text('r0'), 'a\nb\n')
+ self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
+ self.assertEqual(2, len(f))
+ self.assertEqual(2, f.num_versions())
+
+ self.assertRaises(RevisionNotPresent,
+ f.add_lines, 'r2', ['foo'], [])
+ self.assertRaises(RevisionAlreadyPresent,
+ f.add_lines, 'r1', [], [])
+ verify_file(f)
+ # this checks that reopen with create=True does not break anything.
+ f = self.reopen_file(create=True)
+ verify_file(f)
+
+ def test_adds_with_parent_texts(self):
+ f = self.get_file()
+ parent_texts = {}
+ _, _, parent_texts['r0'] = f.add_lines('r0', [], ['a\n', 'b\n'])
+ try:
+ _, _, parent_texts['r1'] = f.add_lines_with_ghosts('r1',
+ ['r0', 'ghost'], ['b\n', 'c\n'], parent_texts=parent_texts)
+ except NotImplementedError:
+ # if the format doesn't support ghosts, just add normally.
+ _, _, parent_texts['r1'] = f.add_lines('r1',
+ ['r0'], ['b\n', 'c\n'], parent_texts=parent_texts)
+ f.add_lines('r2', ['r1'], ['c\n', 'd\n'], parent_texts=parent_texts)
+ self.assertNotEqual(None, parent_texts['r0'])
+ self.assertNotEqual(None, parent_texts['r1'])
+ def verify_file(f):
+ versions = f.versions()
+ self.assertTrue('r0' in versions)
+ self.assertTrue('r1' in versions)
+ self.assertTrue('r2' in versions)
+ self.assertEquals(f.get_lines('r0'), ['a\n', 'b\n'])
+ self.assertEquals(f.get_lines('r1'), ['b\n', 'c\n'])
+ self.assertEquals(f.get_lines('r2'), ['c\n', 'd\n'])
+ self.assertEqual(3, f.num_versions())
+ origins = f.annotate('r1')
+ self.assertEquals(origins[0][0], 'r0')
+ self.assertEquals(origins[1][0], 'r1')
+ origins = f.annotate('r2')
+ self.assertEquals(origins[0][0], 'r1')
+ self.assertEquals(origins[1][0], 'r2')
+
+ verify_file(f)
+ f = self.reopen_file()
+ verify_file(f)
+
+ def test_add_unicode_content(self):
+ # unicode content is not permitted in versioned files.
+ # versioned files version sequences of bytes only.
+ vf = self.get_file()
+ self.assertRaises(errors.BzrBadParameterUnicode,
+ vf.add_lines, 'a', [], ['a\n', u'b\n', 'c\n'])
+ self.assertRaises(
+ (errors.BzrBadParameterUnicode, NotImplementedError),
+ vf.add_lines_with_ghosts, 'a', [], ['a\n', u'b\n', 'c\n'])
+
+ def test_add_follows_left_matching_blocks(self):
+ """If we change left_matching_blocks, delta changes
+
+ Note: There are multiple correct deltas in this case, because
+ we start with 1 "a" and we get 3.
+ """
+ vf = self.get_file()
+ if isinstance(vf, WeaveFile):
+ raise TestSkipped("WeaveFile ignores left_matching_blocks")
+ vf.add_lines('1', [], ['a\n'])
+ vf.add_lines('2', ['1'], ['a\n', 'a\n', 'a\n'],
+ left_matching_blocks=[(0, 0, 1), (1, 3, 0)])
+ self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('2'))
+ vf.add_lines('3', ['1'], ['a\n', 'a\n', 'a\n'],
+ left_matching_blocks=[(0, 2, 1), (1, 3, 0)])
+ self.assertEqual(['a\n', 'a\n', 'a\n'], vf.get_lines('3'))
+
+ def test_inline_newline_throws(self):
+ # \r characters are not permitted in lines being added
+ vf = self.get_file()
+ self.assertRaises(errors.BzrBadParameterContainsNewline,
+ vf.add_lines, 'a', [], ['a\n\n'])
+ self.assertRaises(
+ (errors.BzrBadParameterContainsNewline, NotImplementedError),
+ vf.add_lines_with_ghosts, 'a', [], ['a\n\n'])
+ # but inline CR's are allowed
+ vf.add_lines('a', [], ['a\r\n'])
+ try:
+ vf.add_lines_with_ghosts('b', [], ['a\r\n'])
+ except NotImplementedError:
+ pass
+
+ def test_add_reserved(self):
+ vf = self.get_file()
+ self.assertRaises(errors.ReservedId,
+ vf.add_lines, 'a:', [], ['a\n', 'b\n', 'c\n'])
+
+ def test_add_lines_nostoresha(self):
+ """When nostore_sha is supplied using old content raises."""
+ vf = self.get_file()
+ empty_text = ('a', [])
+ sample_text_nl = ('b', ["foo\n", "bar\n"])
+ sample_text_no_nl = ('c', ["foo\n", "bar"])
+ shas = []
+ for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
+ sha, _, _ = vf.add_lines(version, [], lines)
+ shas.append(sha)
+ # we now have a copy of all the lines in the vf.
+ for sha, (version, lines) in zip(
+ shas, (empty_text, sample_text_nl, sample_text_no_nl)):
+ self.assertRaises(errors.ExistingContent,
+ vf.add_lines, version + "2", [], lines,
+ nostore_sha=sha)
+ # and no new version should have been added.
+ self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
+ version + "2")
+
+ def test_add_lines_with_ghosts_nostoresha(self):
+ """When nostore_sha is supplied using old content raises."""
+ vf = self.get_file()
+ empty_text = ('a', [])
+ sample_text_nl = ('b', ["foo\n", "bar\n"])
+ sample_text_no_nl = ('c', ["foo\n", "bar"])
+ shas = []
+ for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
+ sha, _, _ = vf.add_lines(version, [], lines)
+ shas.append(sha)
+ # we now have a copy of all the lines in the vf.
+ # is the test applicable to this vf implementation?
+ try:
+ vf.add_lines_with_ghosts('d', [], [])
+ except NotImplementedError:
+ raise TestSkipped("add_lines_with_ghosts is optional")
+ for sha, (version, lines) in zip(
+ shas, (empty_text, sample_text_nl, sample_text_no_nl)):
+ self.assertRaises(errors.ExistingContent,
+ vf.add_lines_with_ghosts, version + "2", [], lines,
+ nostore_sha=sha)
+ # and no new version should have been added.
+ self.assertRaises(errors.RevisionNotPresent, vf.get_lines,
+ version + "2")
+
+ def test_add_lines_return_value(self):
+ # add_lines should return the sha1 and the text size.
+ vf = self.get_file()
+ empty_text = ('a', [])
+ sample_text_nl = ('b', ["foo\n", "bar\n"])
+ sample_text_no_nl = ('c', ["foo\n", "bar"])
+ # check results for the three cases:
+ for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
+ # the first two elements are the same for all versioned files:
+ # - the digest and the size of the text. For some versioned files
+ # additional data is returned in additional tuple elements.
+ result = vf.add_lines(version, [], lines)
+ self.assertEqual(3, len(result))
+ self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
+ result[0:2])
+ # parents should not affect the result:
+ lines = sample_text_nl[1]
+ self.assertEqual((osutils.sha_strings(lines), sum(map(len, lines))),
+ vf.add_lines('d', ['b', 'c'], lines)[0:2])
+
+ def test_get_reserved(self):
+ vf = self.get_file()
+ self.assertRaises(errors.ReservedId, vf.get_texts, ['b:'])
+ self.assertRaises(errors.ReservedId, vf.get_lines, 'b:')
+ self.assertRaises(errors.ReservedId, vf.get_text, 'b:')
+
+ def test_add_unchanged_last_line_noeol_snapshot(self):
+ """Add a text with an unchanged last line with no eol should work."""
+ # Test adding this in a number of chain lengths; because the interface
+ # for VersionedFile does not allow forcing a specific chain length, we
+ # just use a small base to get the first snapshot, then a much longer
+ # first line for the next add (which will make the third add snapshot)
+ # and so on. 20 has been chosen as an aribtrary figure - knits use 200
+ # as a capped delta length, but ideally we would have some way of
+ # tuning the test to the store (e.g. keep going until a snapshot
+ # happens).
+ for length in range(20):
+ version_lines = {}
+ vf = self.get_file('case-%d' % length)
+ prefix = 'step-%d'
+ parents = []
+ for step in range(length):
+ version = prefix % step
+ lines = (['prelude \n'] * step) + ['line']
+ vf.add_lines(version, parents, lines)
+ version_lines[version] = lines
+ parents = [version]
+ vf.add_lines('no-eol', parents, ['line'])
+ vf.get_texts(version_lines.keys())
+ self.assertEqualDiff('line', vf.get_text('no-eol'))
+
+ def test_get_texts_eol_variation(self):
+ # similar to the failure in <http://bugs.launchpad.net/234748>
+ vf = self.get_file()
+ sample_text_nl = ["line\n"]
+ sample_text_no_nl = ["line"]
+ versions = []
+ version_lines = {}
+ parents = []
+ for i in range(4):
+ version = 'v%d' % i
+ if i % 2:
+ lines = sample_text_nl
+ else:
+ lines = sample_text_no_nl
+ # left_matching blocks is an internal api; it operates on the
+ # *internal* representation for a knit, which is with *all* lines
+ # being normalised to end with \n - even the final line in a no_nl
+ # file. Using it here ensures that a broken internal implementation
+ # (which is what this test tests) will generate a correct line
+ # delta (which is to say, an empty delta).
+ vf.add_lines(version, parents, lines,
+ left_matching_blocks=[(0, 0, 1)])
+ parents = [version]
+ versions.append(version)
+ version_lines[version] = lines
+ vf.check()
+ vf.get_texts(versions)
+ vf.get_texts(reversed(versions))
+
+ def test_add_lines_with_matching_blocks_noeol_last_line(self):
+ """Add a text with an unchanged last line with no eol should work."""
+ from bzrlib import multiparent
+ # Hand verified sha1 of the text we're adding.
+ sha1 = '6a1d115ec7b60afb664dc14890b5af5ce3c827a4'
+ # Create a mpdiff which adds a new line before the trailing line, and
+ # reuse the last line unaltered (which can cause annotation reuse).
+ # Test adding this in two situations:
+ # On top of a new insertion
+ vf = self.get_file('fulltext')
+ vf.add_lines('noeol', [], ['line'])
+ vf.add_lines('noeol2', ['noeol'], ['newline\n', 'line'],
+ left_matching_blocks=[(0, 1, 1)])
+ self.assertEqualDiff('newline\nline', vf.get_text('noeol2'))
+ # On top of a delta
+ vf = self.get_file('delta')
+ vf.add_lines('base', [], ['line'])
+ vf.add_lines('noeol', ['base'], ['prelude\n', 'line'])
+ vf.add_lines('noeol2', ['noeol'], ['newline\n', 'line'],
+ left_matching_blocks=[(1, 1, 1)])
+ self.assertEqualDiff('newline\nline', vf.get_text('noeol2'))
+
+ def test_make_mpdiffs(self):
+ from bzrlib import multiparent
+ vf = self.get_file('foo')
+ sha1s = self._setup_for_deltas(vf)
+ new_vf = self.get_file('bar')
+ for version in multiparent.topo_iter(vf):
+ mpdiff = vf.make_mpdiffs([version])[0]
+ new_vf.add_mpdiffs([(version, vf.get_parent_map([version])[version],
+ vf.get_sha1s([version])[version], mpdiff)])
+ self.assertEqualDiff(vf.get_text(version),
+ new_vf.get_text(version))
+
+ def test_make_mpdiffs_with_ghosts(self):
+ vf = self.get_file('foo')
+ try:
+ vf.add_lines_with_ghosts('text', ['ghost'], ['line\n'])
+ except NotImplementedError:
+ # old Weave formats do not allow ghosts
+ return
+ self.assertRaises(errors.RevisionNotPresent, vf.make_mpdiffs, ['ghost'])
+
+ def _setup_for_deltas(self, f):
+ self.assertFalse(f.has_version('base'))
+ # add texts that should trip the knit maximum delta chain threshold
+ # as well as doing parallel chains of data in knits.
+ # this is done by two chains of 25 insertions
+ f.add_lines('base', [], ['line\n'])
+ f.add_lines('noeol', ['base'], ['line'])
+ # detailed eol tests:
+ # shared last line with parent no-eol
+ f.add_lines('noeolsecond', ['noeol'], ['line\n', 'line'])
+ # differing last line with parent, both no-eol
+ f.add_lines('noeolnotshared', ['noeolsecond'], ['line\n', 'phone'])
+ # add eol following a noneol parent, change content
+ f.add_lines('eol', ['noeol'], ['phone\n'])
+ # add eol following a noneol parent, no change content
+ f.add_lines('eolline', ['noeol'], ['line\n'])
+ # noeol with no parents:
+ f.add_lines('noeolbase', [], ['line'])
+ # noeol preceeding its leftmost parent in the output:
+ # this is done by making it a merge of two parents with no common
+ # anestry: noeolbase and noeol with the
+ # later-inserted parent the leftmost.
+ f.add_lines('eolbeforefirstparent', ['noeolbase', 'noeol'], ['line'])
+ # two identical eol texts
+ f.add_lines('noeoldup', ['noeol'], ['line'])
+ next_parent = 'base'
+ text_name = 'chain1-'
+ text = ['line\n']
+ sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
+ 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
+ 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
+ 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
+ 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
+ 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
+ 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
+ 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
+ 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
+ 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
+ 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
+ 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
+ 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
+ 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
+ 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
+ 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
+ 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
+ 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
+ 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
+ 19:'1ebed371807ba5935958ad0884595126e8c4e823',
+ 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
+ 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
+ 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
+ 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
+ 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
+ 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
+ }
+ for depth in range(26):
+ new_version = text_name + '%s' % depth
+ text = text + ['line\n']
+ f.add_lines(new_version, [next_parent], text)
+ next_parent = new_version
+ next_parent = 'base'
+ text_name = 'chain2-'
+ text = ['line\n']
+ for depth in range(26):
+ new_version = text_name + '%s' % depth
+ text = text + ['line\n']
+ f.add_lines(new_version, [next_parent], text)
+ next_parent = new_version
+ return sha1s
+
+ def test_ancestry(self):
+ f = self.get_file()
+ self.assertEqual([], f.get_ancestry([]))
+ f.add_lines('r0', [], ['a\n', 'b\n'])
+ f.add_lines('r1', ['r0'], ['b\n', 'c\n'])
+ f.add_lines('r2', ['r0'], ['b\n', 'c\n'])
+ f.add_lines('r3', ['r2'], ['b\n', 'c\n'])
+ f.add_lines('rM', ['r1', 'r2'], ['b\n', 'c\n'])
+ self.assertEqual([], f.get_ancestry([]))
+ versions = f.get_ancestry(['rM'])
+ # there are some possibilities:
+ # r0 r1 r2 rM r3
+ # r0 r1 r2 r3 rM
+ # etc
+ # so we check indexes
+ r0 = versions.index('r0')
+ r1 = versions.index('r1')
+ r2 = versions.index('r2')
+ self.assertFalse('r3' in versions)
+ rM = versions.index('rM')
+ self.assertTrue(r0 < r1)
+ self.assertTrue(r0 < r2)
+ self.assertTrue(r1 < rM)
+ self.assertTrue(r2 < rM)
+
+ self.assertRaises(RevisionNotPresent,
+ f.get_ancestry, ['rM', 'rX'])
+
+ self.assertEqual(set(f.get_ancestry('rM')),
+ set(f.get_ancestry('rM', topo_sorted=False)))
+
+ def test_mutate_after_finish(self):
+ self._transaction = 'before'
+ f = self.get_file()
+ self._transaction = 'after'
+ self.assertRaises(errors.OutSideTransaction, f.add_lines, '', [], [])
+ self.assertRaises(errors.OutSideTransaction, f.add_lines_with_ghosts, '', [], [])
+
+ def test_copy_to(self):
+ f = self.get_file()
+ f.add_lines('0', [], ['a\n'])
+ t = MemoryTransport()
+ f.copy_to('foo', t)
+ for suffix in self.get_factory().get_suffixes():
+ self.assertTrue(t.has('foo' + suffix))
+
+ def test_get_suffixes(self):
+ f = self.get_file()
+ # and should be a list
+ self.assertTrue(isinstance(self.get_factory().get_suffixes(), list))
+
+ def test_get_parent_map(self):
+ f = self.get_file()
+ f.add_lines('r0', [], ['a\n', 'b\n'])
+ self.assertEqual(
+ {'r0':()}, f.get_parent_map(['r0']))
+ f.add_lines('r1', ['r0'], ['a\n', 'b\n'])
+ self.assertEqual(
+ {'r1':('r0',)}, f.get_parent_map(['r1']))
+ self.assertEqual(
+ {'r0':(),
+ 'r1':('r0',)},
+ f.get_parent_map(['r0', 'r1']))
+ f.add_lines('r2', [], ['a\n', 'b\n'])
+ f.add_lines('r3', [], ['a\n', 'b\n'])
+ f.add_lines('m', ['r0', 'r1', 'r2', 'r3'], ['a\n', 'b\n'])
+ self.assertEqual(
+ {'m':('r0', 'r1', 'r2', 'r3')}, f.get_parent_map(['m']))
+ self.assertEqual({}, f.get_parent_map('y'))
+ self.assertEqual(
+ {'r0':(),
+ 'r1':('r0',)},
+ f.get_parent_map(['r0', 'y', 'r1']))
+
+ def test_annotate(self):
+ f = self.get_file()
+ f.add_lines('r0', [], ['a\n', 'b\n'])
+ f.add_lines('r1', ['r0'], ['c\n', 'b\n'])
+ origins = f.annotate('r1')
+ self.assertEquals(origins[0][0], 'r1')
+ self.assertEquals(origins[1][0], 'r0')
+
+ self.assertRaises(RevisionNotPresent,
+ f.annotate, 'foo')
+
+ def test_detection(self):
+ # Test weaves detect corruption.
+ #
+ # Weaves contain a checksum of their texts.
+ # When a text is extracted, this checksum should be
+ # verified.
+
+ w = self.get_file_corrupted_text()
+
+ self.assertEqual('hello\n', w.get_text('v1'))
+ self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
+ self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
+ self.assertRaises(errors.WeaveInvalidChecksum, w.check)
+
+ w = self.get_file_corrupted_checksum()
+
+ self.assertEqual('hello\n', w.get_text('v1'))
+ self.assertRaises(errors.WeaveInvalidChecksum, w.get_text, 'v2')
+ self.assertRaises(errors.WeaveInvalidChecksum, w.get_lines, 'v2')
+ self.assertRaises(errors.WeaveInvalidChecksum, w.check)
+
+ def get_file_corrupted_text(self):
+ """Return a versioned file with corrupt text but valid metadata."""
+ raise NotImplementedError(self.get_file_corrupted_text)
+
+ def reopen_file(self, name='foo'):
+ """Open the versioned file from disk again."""
+ raise NotImplementedError(self.reopen_file)
+
+ def test_iter_lines_added_or_present_in_versions(self):
+ # test that we get at least an equalset of the lines added by
+ # versions in the weave
+ # the ordering here is to make a tree so that dumb searches have
+ # more changes to muck up.
+
+ class InstrumentedProgress(progress.ProgressTask):
+
+ def __init__(self):
+ progress.ProgressTask.__init__(self)
+ self.updates = []
+
+ def update(self, msg=None, current=None, total=None):
+ self.updates.append((msg, current, total))
+
+ vf = self.get_file()
+ # add a base to get included
+ vf.add_lines('base', [], ['base\n'])
+ # add a ancestor to be included on one side
+ vf.add_lines('lancestor', [], ['lancestor\n'])
+ # add a ancestor to be included on the other side
+ vf.add_lines('rancestor', ['base'], ['rancestor\n'])
+ # add a child of rancestor with no eofile-nl
+ vf.add_lines('child', ['rancestor'], ['base\n', 'child\n'])
+ # add a child of lancestor and base to join the two roots
+ vf.add_lines('otherchild',
+ ['lancestor', 'base'],
+ ['base\n', 'lancestor\n', 'otherchild\n'])
+ def iter_with_versions(versions, expected):
+ # now we need to see what lines are returned, and how often.
+ lines = {}
+ progress = InstrumentedProgress()
+ # iterate over the lines
+ for line in vf.iter_lines_added_or_present_in_versions(versions,
+ pb=progress):
+ lines.setdefault(line, 0)
+ lines[line] += 1
+ if []!= progress.updates:
+ self.assertEqual(expected, progress.updates)
+ return lines
+ lines = iter_with_versions(['child', 'otherchild'],
+ [('Walking content', 0, 2),
+ ('Walking content', 1, 2),
+ ('Walking content', 2, 2)])
+ # we must see child and otherchild
+ self.assertTrue(lines[('child\n', 'child')] > 0)
+ self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
+ # we dont care if we got more than that.
+
+ # test all lines
+ lines = iter_with_versions(None, [('Walking content', 0, 5),
+ ('Walking content', 1, 5),
+ ('Walking content', 2, 5),
+ ('Walking content', 3, 5),
+ ('Walking content', 4, 5),
+ ('Walking content', 5, 5)])
+ # all lines must be seen at least once
+ self.assertTrue(lines[('base\n', 'base')] > 0)
+ self.assertTrue(lines[('lancestor\n', 'lancestor')] > 0)
+ self.assertTrue(lines[('rancestor\n', 'rancestor')] > 0)
+ self.assertTrue(lines[('child\n', 'child')] > 0)
+ self.assertTrue(lines[('otherchild\n', 'otherchild')] > 0)
+
+ def test_add_lines_with_ghosts(self):
+ # some versioned file formats allow lines to be added with parent
+ # information that is > than that in the format. Formats that do
+ # not support this need to raise NotImplementedError on the
+ # add_lines_with_ghosts api.
+ vf = self.get_file()
+ # add a revision with ghost parents
+ # The preferred form is utf8, but we should translate when needed
+ parent_id_unicode = u'b\xbfse'
+ parent_id_utf8 = parent_id_unicode.encode('utf8')
+ try:
+ vf.add_lines_with_ghosts('notbxbfse', [parent_id_utf8], [])
+ except NotImplementedError:
+ # check the other ghost apis are also not implemented
+ self.assertRaises(NotImplementedError, vf.get_ancestry_with_ghosts, ['foo'])
+ self.assertRaises(NotImplementedError, vf.get_parents_with_ghosts, 'foo')
+ return
+ vf = self.reopen_file()
+ # test key graph related apis: getncestry, _graph, get_parents
+ # has_version
+ # - these are ghost unaware and must not be reflect ghosts
+ self.assertEqual(['notbxbfse'], vf.get_ancestry('notbxbfse'))
+ self.assertFalse(vf.has_version(parent_id_utf8))
+ # we have _with_ghost apis to give us ghost information.
+ self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry_with_ghosts(['notbxbfse']))
+ self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
+ # if we add something that is a ghost of another, it should correct the
+ # results of the prior apis
+ vf.add_lines(parent_id_utf8, [], [])
+ self.assertEqual([parent_id_utf8, 'notbxbfse'], vf.get_ancestry(['notbxbfse']))
+ self.assertEqual({'notbxbfse':(parent_id_utf8,)},
+ vf.get_parent_map(['notbxbfse']))
+ self.assertTrue(vf.has_version(parent_id_utf8))
+ # we have _with_ghost apis to give us ghost information.
+ self.assertEqual([parent_id_utf8, 'notbxbfse'],
+ vf.get_ancestry_with_ghosts(['notbxbfse']))
+ self.assertEqual([parent_id_utf8], vf.get_parents_with_ghosts('notbxbfse'))
+
+ def test_add_lines_with_ghosts_after_normal_revs(self):
+ # some versioned file formats allow lines to be added with parent
+ # information that is > than that in the format. Formats that do
+ # not support this need to raise NotImplementedError on the
+ # add_lines_with_ghosts api.
+ vf = self.get_file()
+ # probe for ghost support
+ try:
+ vf.add_lines_with_ghosts('base', [], ['line\n', 'line_b\n'])
+ except NotImplementedError:
+ return
+ vf.add_lines_with_ghosts('references_ghost',
+ ['base', 'a_ghost'],
+ ['line\n', 'line_b\n', 'line_c\n'])
+ origins = vf.annotate('references_ghost')
+ self.assertEquals(('base', 'line\n'), origins[0])
+ self.assertEquals(('base', 'line_b\n'), origins[1])
+ self.assertEquals(('references_ghost', 'line_c\n'), origins[2])
+
+ def test_readonly_mode(self):
+ t = self.get_transport()
+ factory = self.get_factory()
+ vf = factory('id', t, 0777, create=True, access_mode='w')
+ vf = factory('id', t, access_mode='r')
+ self.assertRaises(errors.ReadOnlyError, vf.add_lines, 'base', [], [])
+ self.assertRaises(errors.ReadOnlyError,
+ vf.add_lines_with_ghosts,
+ 'base',
+ [],
+ [])
+
+ def test_get_sha1s(self):
+ # check the sha1 data is available
+ vf = self.get_file()
+ # a simple file
+ vf.add_lines('a', [], ['a\n'])
+ # the same file, different metadata
+ vf.add_lines('b', ['a'], ['a\n'])
+ # a file differing only in last newline.
+ vf.add_lines('c', [], ['a'])
+ self.assertEqual({
+ 'a': '3f786850e387550fdab836ed7e6dc881de23001b',
+ 'c': '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8',
+ 'b': '3f786850e387550fdab836ed7e6dc881de23001b',
+ },
+ vf.get_sha1s(['a', 'c', 'b']))
+
+
+class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
+
+ def get_file(self, name='foo'):
+ return WeaveFile(name, self.get_transport(),
+ create=True,
+ get_scope=self.get_transaction)
+
+ def get_file_corrupted_text(self):
+ w = WeaveFile('foo', self.get_transport(),
+ create=True,
+ get_scope=self.get_transaction)
+ w.add_lines('v1', [], ['hello\n'])
+ w.add_lines('v2', ['v1'], ['hello\n', 'there\n'])
+
+ # We are going to invasively corrupt the text
+ # Make sure the internals of weave are the same
+ self.assertEqual([('{', 0)
+ , 'hello\n'
+ , ('}', None)
+ , ('{', 1)
+ , 'there\n'
+ , ('}', None)
+ ], w._weave)
+
+ self.assertEqual(['f572d396fae9206628714fb2ce00f72e94f2258f'
+ , '90f265c6e75f1c8f9ab76dcf85528352c5f215ef'
+ ], w._sha1s)
+ w.check()
+
+ # Corrupted
+ w._weave[4] = 'There\n'
+ return w
+
+ def get_file_corrupted_checksum(self):
+ w = self.get_file_corrupted_text()
+ # Corrected
+ w._weave[4] = 'there\n'
+ self.assertEqual('hello\nthere\n', w.get_text('v2'))
+
+ #Invalid checksum, first digit changed
+ w._sha1s[1] = 'f0f265c6e75f1c8f9ab76dcf85528352c5f215ef'
+ return w
+
+ def reopen_file(self, name='foo', create=False):
+ return WeaveFile(name, self.get_transport(),
+ create=create,
+ get_scope=self.get_transaction)
+
+ def test_no_implicit_create(self):
+ self.assertRaises(errors.NoSuchFile,
+ WeaveFile,
+ 'foo',
+ self.get_transport(),
+ get_scope=self.get_transaction)
+
+ def get_factory(self):
+ return WeaveFile
+
+
+class TestPlanMergeVersionedFile(TestCaseWithMemoryTransport):
+
+ def setUp(self):
+ TestCaseWithMemoryTransport.setUp(self)
+ mapper = PrefixMapper()
+ factory = make_file_factory(True, mapper)
+ self.vf1 = factory(self.get_transport('root-1'))
+ self.vf2 = factory(self.get_transport('root-2'))
+ self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root')
+ self.plan_merge_vf.fallback_versionedfiles.extend([self.vf1, self.vf2])
+
+ def test_add_lines(self):
+ self.plan_merge_vf.add_lines(('root', 'a:'), [], [])
+ self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
+ ('root', 'a'), [], [])
+ self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
+ ('root', 'a:'), None, [])
+ self.assertRaises(ValueError, self.plan_merge_vf.add_lines,
+ ('root', 'a:'), [], None)
+
+ def setup_abcde(self):
+ self.vf1.add_lines(('root', 'A'), [], ['a'])
+ self.vf1.add_lines(('root', 'B'), [('root', 'A')], ['b'])
+ self.vf2.add_lines(('root', 'C'), [], ['c'])
+ self.vf2.add_lines(('root', 'D'), [('root', 'C')], ['d'])
+ self.plan_merge_vf.add_lines(('root', 'E:'),
+ [('root', 'B'), ('root', 'D')], ['e'])
+
+ def test_get_parents(self):
+ self.setup_abcde()
+ self.assertEqual({('root', 'B'):(('root', 'A'),)},
+ self.plan_merge_vf.get_parent_map([('root', 'B')]))
+ self.assertEqual({('root', 'D'):(('root', 'C'),)},
+ self.plan_merge_vf.get_parent_map([('root', 'D')]))
+ self.assertEqual({('root', 'E:'):(('root', 'B'),('root', 'D'))},
+ self.plan_merge_vf.get_parent_map([('root', 'E:')]))
+ self.assertEqual({},
+ self.plan_merge_vf.get_parent_map([('root', 'F')]))
+ self.assertEqual({
+ ('root', 'B'):(('root', 'A'),),
+ ('root', 'D'):(('root', 'C'),),
+ ('root', 'E:'):(('root', 'B'),('root', 'D')),
+ },
+ self.plan_merge_vf.get_parent_map(
+ [('root', 'B'), ('root', 'D'), ('root', 'E:'), ('root', 'F')]))
+
+ def test_get_record_stream(self):
+ self.setup_abcde()
+ def get_record(suffix):
+ return self.plan_merge_vf.get_record_stream(
+ [('root', suffix)], 'unordered', True).next()
+ self.assertEqual('a', get_record('A').get_bytes_as('fulltext'))
+ self.assertEqual('c', get_record('C').get_bytes_as('fulltext'))
+ self.assertEqual('e', get_record('E:').get_bytes_as('fulltext'))
+ self.assertEqual('absent', get_record('F').storage_kind)
+
+
+class TestReadonlyHttpMixin(object):
+
+ def get_transaction(self):
+ return 1
+
+ def test_readonly_http_works(self):
+ # we should be able to read from http with a versioned file.
+ vf = self.get_file()
+ # try an empty file access
+ readonly_vf = self.get_factory()('foo',
+ transport.get_transport_from_url(self.get_readonly_url('.')))
+ self.assertEqual([], readonly_vf.versions())
+
+ def test_readonly_http_works_with_feeling(self):
+ # we should be able to read from http with a versioned file.
+ vf = self.get_file()
+ # now with feeling.
+ vf.add_lines('1', [], ['a\n'])
+ vf.add_lines('2', ['1'], ['b\n', 'a\n'])
+ readonly_vf = self.get_factory()('foo',
+ transport.get_transport_from_url(self.get_readonly_url('.')))
+ self.assertEqual(['1', '2'], vf.versions())
+ self.assertEqual(['1', '2'], readonly_vf.versions())
+ for version in readonly_vf.versions():
+ readonly_vf.get_lines(version)
+
+
+class TestWeaveHTTP(TestCaseWithWebserver, TestReadonlyHttpMixin):
+
+ def get_file(self):
+ return WeaveFile('foo', self.get_transport(),
+ create=True,
+ get_scope=self.get_transaction)
+
+ def get_factory(self):
+ return WeaveFile
+
+
+class MergeCasesMixin(object):
+
+ def doMerge(self, base, a, b, mp):
+ from cStringIO import StringIO
+ from textwrap import dedent
+
+ def addcrlf(x):
+ return x + '\n'
+
+ w = self.get_file()
+ w.add_lines('text0', [], map(addcrlf, base))
+ w.add_lines('text1', ['text0'], map(addcrlf, a))
+ w.add_lines('text2', ['text0'], map(addcrlf, b))
+
+ self.log_contents(w)
+
+ self.log('merge plan:')
+ p = list(w.plan_merge('text1', 'text2'))
+ for state, line in p:
+ if line:
+ self.log('%12s | %s' % (state, line[:-1]))
+
+ self.log('merge:')
+ mt = StringIO()
+ mt.writelines(w.weave_merge(p))
+ mt.seek(0)
+ self.log(mt.getvalue())
+
+ mp = map(addcrlf, mp)
+ self.assertEqual(mt.readlines(), mp)
+
+
+ def testOneInsert(self):
+ self.doMerge([],
+ ['aa'],
+ [],
+ ['aa'])
+
+ def testSeparateInserts(self):
+ self.doMerge(['aaa', 'bbb', 'ccc'],
+ ['aaa', 'xxx', 'bbb', 'ccc'],
+ ['aaa', 'bbb', 'yyy', 'ccc'],
+ ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
+
+ def testSameInsert(self):
+ self.doMerge(['aaa', 'bbb', 'ccc'],
+ ['aaa', 'xxx', 'bbb', 'ccc'],
+ ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'],
+ ['aaa', 'xxx', 'bbb', 'yyy', 'ccc'])
+ overlappedInsertExpected = ['aaa', 'xxx', 'yyy', 'bbb']
+ def testOverlappedInsert(self):
+ self.doMerge(['aaa', 'bbb'],
+ ['aaa', 'xxx', 'yyy', 'bbb'],
+ ['aaa', 'xxx', 'bbb'], self.overlappedInsertExpected)
+
+ # really it ought to reduce this to
+ # ['aaa', 'xxx', 'yyy', 'bbb']
+
+
+ def testClashReplace(self):
+ self.doMerge(['aaa'],
+ ['xxx'],
+ ['yyy', 'zzz'],
+ ['<<<<<<< ', 'xxx', '=======', 'yyy', 'zzz',
+ '>>>>>>> '])
+
+ def testNonClashInsert1(self):
+ self.doMerge(['aaa'],
+ ['xxx', 'aaa'],
+ ['yyy', 'zzz'],
+ ['<<<<<<< ', 'xxx', 'aaa', '=======', 'yyy', 'zzz',
+ '>>>>>>> '])
+
+ def testNonClashInsert2(self):
+ self.doMerge(['aaa'],
+ ['aaa'],
+ ['yyy', 'zzz'],
+ ['yyy', 'zzz'])
+
+
+ def testDeleteAndModify(self):
+ """Clashing delete and modification.
+
+ If one side modifies a region and the other deletes it then
+ there should be a conflict with one side blank.
+ """
+
+ #######################################
+ # skippd, not working yet
+ return
+
+ self.doMerge(['aaa', 'bbb', 'ccc'],
+ ['aaa', 'ddd', 'ccc'],
+ ['aaa', 'ccc'],
+ ['<<<<<<<< ', 'aaa', '=======', '>>>>>>> ', 'ccc'])
+
+ def _test_merge_from_strings(self, base, a, b, expected):
+ w = self.get_file()
+ w.add_lines('text0', [], base.splitlines(True))
+ w.add_lines('text1', ['text0'], a.splitlines(True))
+ w.add_lines('text2', ['text0'], b.splitlines(True))
+ self.log('merge plan:')
+ p = list(w.plan_merge('text1', 'text2'))
+ for state, line in p:
+ if line:
+ self.log('%12s | %s' % (state, line[:-1]))
+ self.log('merge result:')
+ result_text = ''.join(w.weave_merge(p))
+ self.log(result_text)
+ self.assertEqualDiff(result_text, expected)
+
+ def test_weave_merge_conflicts(self):
+ # does weave merge properly handle plans that end with unchanged?
+ result = ''.join(self.get_file().weave_merge([('new-a', 'hello\n')]))
+ self.assertEqual(result, 'hello\n')
+
+ def test_deletion_extended(self):
+ """One side deletes, the other deletes more.
+ """
+ base = """\
+ line 1
+ line 2
+ line 3
+ """
+ a = """\
+ line 1
+ line 2
+ """
+ b = """\
+ line 1
+ """
+ result = """\
+ line 1
+<<<<<<<\x20
+ line 2
+=======
+>>>>>>>\x20
+ """
+ self._test_merge_from_strings(base, a, b, result)
+
+ def test_deletion_overlap(self):
+ """Delete overlapping regions with no other conflict.
+
+ Arguably it'd be better to treat these as agreement, rather than
+ conflict, but for now conflict is safer.
+ """
+ base = """\
+ start context
+ int a() {}
+ int b() {}
+ int c() {}
+ end context
+ """
+ a = """\
+ start context
+ int a() {}
+ end context
+ """
+ b = """\
+ start context
+ int c() {}
+ end context
+ """
+ result = """\
+ start context
+<<<<<<<\x20
+ int a() {}
+=======
+ int c() {}
+>>>>>>>\x20
+ end context
+ """
+ self._test_merge_from_strings(base, a, b, result)
+
+ def test_agreement_deletion(self):
+ """Agree to delete some lines, without conflicts."""
+ base = """\
+ start context
+ base line 1
+ base line 2
+ end context
+ """
+ a = """\
+ start context
+ base line 1
+ end context
+ """
+ b = """\
+ start context
+ base line 1
+ end context
+ """
+ result = """\
+ start context
+ base line 1
+ end context
+ """
+ self._test_merge_from_strings(base, a, b, result)
+
+ def test_sync_on_deletion(self):
+ """Specific case of merge where we can synchronize incorrectly.
+
+ A previous version of the weave merge concluded that the two versions
+ agreed on deleting line 2, and this could be a synchronization point.
+ Line 1 was then considered in isolation, and thought to be deleted on
+ both sides.
+
+ It's better to consider the whole thing as a disagreement region.
+ """
+ base = """\
+ start context
+ base line 1
+ base line 2
+ end context
+ """
+ a = """\
+ start context
+ base line 1
+ a's replacement line 2
+ end context
+ """
+ b = """\
+ start context
+ b replaces
+ both lines
+ end context
+ """
+ result = """\
+ start context
+<<<<<<<\x20
+ base line 1
+ a's replacement line 2
+=======
+ b replaces
+ both lines
+>>>>>>>\x20
+ end context
+ """
+ self._test_merge_from_strings(base, a, b, result)
+
+
+class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
+
+ def get_file(self, name='foo'):
+ return WeaveFile(name, self.get_transport(),
+ create=True)
+
+ def log_contents(self, w):
+ self.log('weave is:')
+ tmpf = StringIO()
+ write_weave(w, tmpf)
+ self.log(tmpf.getvalue())
+
+ overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======',
+ 'xxx', '>>>>>>> ', 'bbb']
+
+
+class TestContentFactoryAdaption(TestCaseWithMemoryTransport):
+
+ def test_select_adaptor(self):
+ """Test expected adapters exist."""
+ # One scenario for each lookup combination we expect to use.
+ # Each is source_kind, requested_kind, adapter class
+ scenarios = [
+ ('knit-delta-gz', 'fulltext', _mod_knit.DeltaPlainToFullText),
+ ('knit-ft-gz', 'fulltext', _mod_knit.FTPlainToFullText),
+ ('knit-annotated-delta-gz', 'knit-delta-gz',
+ _mod_knit.DeltaAnnotatedToUnannotated),
+ ('knit-annotated-delta-gz', 'fulltext',
+ _mod_knit.DeltaAnnotatedToFullText),
+ ('knit-annotated-ft-gz', 'knit-ft-gz',
+ _mod_knit.FTAnnotatedToUnannotated),
+ ('knit-annotated-ft-gz', 'fulltext',
+ _mod_knit.FTAnnotatedToFullText),
+ ]
+ for source, requested, klass in scenarios:
+ adapter_factory = versionedfile.adapter_registry.get(
+ (source, requested))
+ adapter = adapter_factory(None)
+ self.assertIsInstance(adapter, klass)
+
+ def get_knit(self, annotated=True):
+ mapper = ConstantMapper('knit')
+ transport = self.get_transport()
+ return make_file_factory(annotated, mapper)(transport)
+
+ def helpGetBytes(self, f, ft_adapter, delta_adapter):
+ """Grab the interested adapted texts for tests."""
+ # origin is a fulltext
+ entries = f.get_record_stream([('origin',)], 'unordered', False)
+ base = entries.next()
+ ft_data = ft_adapter.get_bytes(base)
+ # merged is both a delta and multiple parents.
+ entries = f.get_record_stream([('merged',)], 'unordered', False)
+ merged = entries.next()
+ delta_data = delta_adapter.get_bytes(merged)
+ return ft_data, delta_data
+
+ def test_deannotation_noeol(self):
+ """Test converting annotated knits to unannotated knits."""
+ # we need a full text, and a delta
+ f = self.get_knit()
+ get_diamond_files(f, 1, trailing_eol=False)
+ ft_data, delta_data = self.helpGetBytes(f,
+ _mod_knit.FTAnnotatedToUnannotated(None),
+ _mod_knit.DeltaAnnotatedToUnannotated(None))
+ self.assertEqual(
+ 'version origin 1 b284f94827db1fa2970d9e2014f080413b547a7e\n'
+ 'origin\n'
+ 'end origin\n',
+ GzipFile(mode='rb', fileobj=StringIO(ft_data)).read())
+ self.assertEqual(
+ 'version merged 4 32c2e79763b3f90e8ccde37f9710b6629c25a796\n'
+ '1,2,3\nleft\nright\nmerged\nend merged\n',
+ GzipFile(mode='rb', fileobj=StringIO(delta_data)).read())
+
+ def test_deannotation(self):
+ """Test converting annotated knits to unannotated knits."""
+ # we need a full text, and a delta
+ f = self.get_knit()
+ get_diamond_files(f, 1)
+ ft_data, delta_data = self.helpGetBytes(f,
+ _mod_knit.FTAnnotatedToUnannotated(None),
+ _mod_knit.DeltaAnnotatedToUnannotated(None))
+ self.assertEqual(
+ 'version origin 1 00e364d235126be43292ab09cb4686cf703ddc17\n'
+ 'origin\n'
+ 'end origin\n',
+ GzipFile(mode='rb', fileobj=StringIO(ft_data)).read())
+ self.assertEqual(
+ 'version merged 3 ed8bce375198ea62444dc71952b22cfc2b09226d\n'
+ '2,2,2\nright\nmerged\nend merged\n',
+ GzipFile(mode='rb', fileobj=StringIO(delta_data)).read())
+
+ def test_annotated_to_fulltext_no_eol(self):
+ """Test adapting annotated knits to full texts (for -> weaves)."""
+ # we need a full text, and a delta
+ f = self.get_knit()
+ get_diamond_files(f, 1, trailing_eol=False)
+ # Reconstructing a full text requires a backing versioned file, and it
+ # must have the base lines requested from it.
+ logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
+ ft_data, delta_data = self.helpGetBytes(f,
+ _mod_knit.FTAnnotatedToFullText(None),
+ _mod_knit.DeltaAnnotatedToFullText(logged_vf))
+ self.assertEqual('origin', ft_data)
+ self.assertEqual('base\nleft\nright\nmerged', delta_data)
+ self.assertEqual([('get_record_stream', [('left',)], 'unordered',
+ True)], logged_vf.calls)
+
+ def test_annotated_to_fulltext(self):
+ """Test adapting annotated knits to full texts (for -> weaves)."""
+ # we need a full text, and a delta
+ f = self.get_knit()
+ get_diamond_files(f, 1)
+ # Reconstructing a full text requires a backing versioned file, and it
+ # must have the base lines requested from it.
+ logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
+ ft_data, delta_data = self.helpGetBytes(f,
+ _mod_knit.FTAnnotatedToFullText(None),
+ _mod_knit.DeltaAnnotatedToFullText(logged_vf))
+ self.assertEqual('origin\n', ft_data)
+ self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
+ self.assertEqual([('get_record_stream', [('left',)], 'unordered',
+ True)], logged_vf.calls)
+
+ def test_unannotated_to_fulltext(self):
+ """Test adapting unannotated knits to full texts.
+
+ This is used for -> weaves, and for -> annotated knits.
+ """
+ # we need a full text, and a delta
+ f = self.get_knit(annotated=False)
+ get_diamond_files(f, 1)
+ # Reconstructing a full text requires a backing versioned file, and it
+ # must have the base lines requested from it.
+ logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
+ ft_data, delta_data = self.helpGetBytes(f,
+ _mod_knit.FTPlainToFullText(None),
+ _mod_knit.DeltaPlainToFullText(logged_vf))
+ self.assertEqual('origin\n', ft_data)
+ self.assertEqual('base\nleft\nright\nmerged\n', delta_data)
+ self.assertEqual([('get_record_stream', [('left',)], 'unordered',
+ True)], logged_vf.calls)
+
+ def test_unannotated_to_fulltext_no_eol(self):
+ """Test adapting unannotated knits to full texts.
+
+ This is used for -> weaves, and for -> annotated knits.
+ """
+ # we need a full text, and a delta
+ f = self.get_knit(annotated=False)
+ get_diamond_files(f, 1, trailing_eol=False)
+ # Reconstructing a full text requires a backing versioned file, and it
+ # must have the base lines requested from it.
+ logged_vf = versionedfile.RecordingVersionedFilesDecorator(f)
+ ft_data, delta_data = self.helpGetBytes(f,
+ _mod_knit.FTPlainToFullText(None),
+ _mod_knit.DeltaPlainToFullText(logged_vf))
+ self.assertEqual('origin', ft_data)
+ self.assertEqual('base\nleft\nright\nmerged', delta_data)
+ self.assertEqual([('get_record_stream', [('left',)], 'unordered',
+ True)], logged_vf.calls)
+
+
+class TestKeyMapper(TestCaseWithMemoryTransport):
+ """Tests for various key mapping logic."""
+
+ def test_identity_mapper(self):
+ mapper = versionedfile.ConstantMapper("inventory")
+ self.assertEqual("inventory", mapper.map(('foo@ar',)))
+ self.assertEqual("inventory", mapper.map(('quux',)))
+
+ def test_prefix_mapper(self):
+ #format5: plain
+ mapper = versionedfile.PrefixMapper()
+ self.assertEqual("file-id", mapper.map(("file-id", "revision-id")))
+ self.assertEqual("new-id", mapper.map(("new-id", "revision-id")))
+ self.assertEqual(('file-id',), mapper.unmap("file-id"))
+ self.assertEqual(('new-id',), mapper.unmap("new-id"))
+
+ def test_hash_prefix_mapper(self):
+ #format6: hash + plain
+ mapper = versionedfile.HashPrefixMapper()
+ self.assertEqual("9b/file-id", mapper.map(("file-id", "revision-id")))
+ self.assertEqual("45/new-id", mapper.map(("new-id", "revision-id")))
+ self.assertEqual(('file-id',), mapper.unmap("9b/file-id"))
+ self.assertEqual(('new-id',), mapper.unmap("45/new-id"))
+
+ def test_hash_escaped_mapper(self):
+ #knit1: hash + escaped
+ mapper = versionedfile.HashEscapedPrefixMapper()
+ self.assertEqual("88/%2520", mapper.map((" ", "revision-id")))
+ self.assertEqual("ed/fil%2545-%2549d", mapper.map(("filE-Id",
+ "revision-id")))
+ self.assertEqual("88/ne%2557-%2549d", mapper.map(("neW-Id",
+ "revision-id")))
+ self.assertEqual(('filE-Id',), mapper.unmap("ed/fil%2545-%2549d"))
+ self.assertEqual(('neW-Id',), mapper.unmap("88/ne%2557-%2549d"))
+
+
+class TestVersionedFiles(TestCaseWithMemoryTransport):
+ """Tests for the multiple-file variant of VersionedFile."""
+
+ # We want to be sure of behaviour for:
+ # weaves prefix layout (weave texts)
+ # individually named weaves (weave inventories)
+ # annotated knits - prefix|hash|hash-escape layout, we test the third only
+ # as it is the most complex mapper.
+ # individually named knits
+ # individual no-graph knits in packs (signatures)
+ # individual graph knits in packs (inventories)
+ # individual graph nocompression knits in packs (revisions)
+ # plain text knits in packs (texts)
+ len_one_scenarios = [
+ ('weave-named', {
+ 'cleanup':None,
+ 'factory':make_versioned_files_factory(WeaveFile,
+ ConstantMapper('inventory')),
+ 'graph':True,
+ 'key_length':1,
+ 'support_partial_insertion': False,
+ }),
+ ('named-knit', {
+ 'cleanup':None,
+ 'factory':make_file_factory(False, ConstantMapper('revisions')),
+ 'graph':True,
+ 'key_length':1,
+ 'support_partial_insertion': False,
+ }),
+ ('named-nograph-nodelta-knit-pack', {
+ 'cleanup':cleanup_pack_knit,
+ 'factory':make_pack_factory(False, False, 1),
+ 'graph':False,
+ 'key_length':1,
+ 'support_partial_insertion': False,
+ }),
+ ('named-graph-knit-pack', {
+ 'cleanup':cleanup_pack_knit,
+ 'factory':make_pack_factory(True, True, 1),
+ 'graph':True,
+ 'key_length':1,
+ 'support_partial_insertion': True,
+ }),
+ ('named-graph-nodelta-knit-pack', {
+ 'cleanup':cleanup_pack_knit,
+ 'factory':make_pack_factory(True, False, 1),
+ 'graph':True,
+ 'key_length':1,
+ 'support_partial_insertion': False,
+ }),
+ ('groupcompress-nograph', {
+ 'cleanup':groupcompress.cleanup_pack_group,
+ 'factory':groupcompress.make_pack_factory(False, False, 1),
+ 'graph': False,
+ 'key_length':1,
+ 'support_partial_insertion':False,
+ }),
+ ]
+ len_two_scenarios = [
+ ('weave-prefix', {
+ 'cleanup':None,
+ 'factory':make_versioned_files_factory(WeaveFile,
+ PrefixMapper()),
+ 'graph':True,
+ 'key_length':2,
+ 'support_partial_insertion': False,
+ }),
+ ('annotated-knit-escape', {
+ 'cleanup':None,
+ 'factory':make_file_factory(True, HashEscapedPrefixMapper()),
+ 'graph':True,
+ 'key_length':2,
+ 'support_partial_insertion': False,
+ }),
+ ('plain-knit-pack', {
+ 'cleanup':cleanup_pack_knit,
+ 'factory':make_pack_factory(True, True, 2),
+ 'graph':True,
+ 'key_length':2,
+ 'support_partial_insertion': True,
+ }),
+ ('groupcompress', {
+ 'cleanup':groupcompress.cleanup_pack_group,
+ 'factory':groupcompress.make_pack_factory(True, False, 1),
+ 'graph': True,
+ 'key_length':1,
+ 'support_partial_insertion':False,
+ }),
+ ]
+
+ scenarios = len_one_scenarios + len_two_scenarios
+
+ def get_versionedfiles(self, relpath='files'):
+ transport = self.get_transport(relpath)
+ if relpath != '.':
+ transport.mkdir('.')
+ files = self.factory(transport)
+ if self.cleanup is not None:
+ self.addCleanup(self.cleanup, files)
+ return files
+
+ def get_simple_key(self, suffix):
+ """Return a key for the object under test."""
+ if self.key_length == 1:
+ return (suffix,)
+ else:
+ return ('FileA',) + (suffix,)
+
+ def test_add_fallback_implies_without_fallbacks(self):
+ f = self.get_versionedfiles('files')
+ if getattr(f, 'add_fallback_versioned_files', None) is None:
+ raise TestNotApplicable("%s doesn't support fallbacks"
+ % (f.__class__.__name__,))
+ g = self.get_versionedfiles('fallback')
+ key_a = self.get_simple_key('a')
+ g.add_lines(key_a, [], ['\n'])
+ f.add_fallback_versioned_files(g)
+ self.assertTrue(key_a in f.get_parent_map([key_a]))
+ self.assertFalse(key_a in f.without_fallbacks().get_parent_map([key_a]))
+
+ def test_add_lines(self):
+ f = self.get_versionedfiles()
+ key0 = self.get_simple_key('r0')
+ key1 = self.get_simple_key('r1')
+ key2 = self.get_simple_key('r2')
+ keyf = self.get_simple_key('foo')
+ f.add_lines(key0, [], ['a\n', 'b\n'])
+ if self.graph:
+ f.add_lines(key1, [key0], ['b\n', 'c\n'])
+ else:
+ f.add_lines(key1, [], ['b\n', 'c\n'])
+ keys = f.keys()
+ self.assertTrue(key0 in keys)
+ self.assertTrue(key1 in keys)
+ records = []
+ for record in f.get_record_stream([key0, key1], 'unordered', True):
+ records.append((record.key, record.get_bytes_as('fulltext')))
+ records.sort()
+ self.assertEqual([(key0, 'a\nb\n'), (key1, 'b\nc\n')], records)
+
+ def test__add_text(self):
+ f = self.get_versionedfiles()
+ key0 = self.get_simple_key('r0')
+ key1 = self.get_simple_key('r1')
+ key2 = self.get_simple_key('r2')
+ keyf = self.get_simple_key('foo')
+ f._add_text(key0, [], 'a\nb\n')
+ if self.graph:
+ f._add_text(key1, [key0], 'b\nc\n')
+ else:
+ f._add_text(key1, [], 'b\nc\n')
+ keys = f.keys()
+ self.assertTrue(key0 in keys)
+ self.assertTrue(key1 in keys)
+ records = []
+ for record in f.get_record_stream([key0, key1], 'unordered', True):
+ records.append((record.key, record.get_bytes_as('fulltext')))
+ records.sort()
+ self.assertEqual([(key0, 'a\nb\n'), (key1, 'b\nc\n')], records)
+
+ def test_annotate(self):
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ if self.key_length == 1:
+ prefix = ()
+ else:
+ prefix = ('FileA',)
+ # introduced full text
+ origins = files.annotate(prefix + ('origin',))
+ self.assertEqual([
+ (prefix + ('origin',), 'origin\n')],
+ origins)
+ # a delta
+ origins = files.annotate(prefix + ('base',))
+ self.assertEqual([
+ (prefix + ('base',), 'base\n')],
+ origins)
+ # a merge
+ origins = files.annotate(prefix + ('merged',))
+ if self.graph:
+ self.assertEqual([
+ (prefix + ('base',), 'base\n'),
+ (prefix + ('left',), 'left\n'),
+ (prefix + ('right',), 'right\n'),
+ (prefix + ('merged',), 'merged\n')
+ ],
+ origins)
+ else:
+ # Without a graph everything is new.
+ self.assertEqual([
+ (prefix + ('merged',), 'base\n'),
+ (prefix + ('merged',), 'left\n'),
+ (prefix + ('merged',), 'right\n'),
+ (prefix + ('merged',), 'merged\n')
+ ],
+ origins)
+ self.assertRaises(RevisionNotPresent,
+ files.annotate, prefix + ('missing-key',))
+
+ def test_check_no_parameters(self):
+ files = self.get_versionedfiles()
+
+ def test_check_progressbar_parameter(self):
+ """A progress bar can be supplied because check can be a generator."""
+ pb = ui.ui_factory.nested_progress_bar()
+ self.addCleanup(pb.finished)
+ files = self.get_versionedfiles()
+ files.check(progress_bar=pb)
+
+ def test_check_with_keys_becomes_generator(self):
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ keys = files.keys()
+ entries = files.check(keys=keys)
+ seen = set()
+ # Texts output should be fulltexts.
+ self.capture_stream(files, entries, seen.add,
+ files.get_parent_map(keys), require_fulltext=True)
+ # All texts should be output.
+ self.assertEqual(set(keys), seen)
+
+ def test_clear_cache(self):
+ files = self.get_versionedfiles()
+ files.clear_cache()
+
+ def test_construct(self):
+ """Each parameterised test can be constructed on a transport."""
+ files = self.get_versionedfiles()
+
+ def get_diamond_files(self, files, trailing_eol=True, left_only=False,
+ nokeys=False):
+ return get_diamond_files(files, self.key_length,
+ trailing_eol=trailing_eol, nograph=not self.graph,
+ left_only=left_only, nokeys=nokeys)
+
+ def _add_content_nostoresha(self, add_lines):
+ """When nostore_sha is supplied using old content raises."""
+ vf = self.get_versionedfiles()
+ empty_text = ('a', [])
+ sample_text_nl = ('b', ["foo\n", "bar\n"])
+ sample_text_no_nl = ('c', ["foo\n", "bar"])
+ shas = []
+ for version, lines in (empty_text, sample_text_nl, sample_text_no_nl):
+ if add_lines:
+ sha, _, _ = vf.add_lines(self.get_simple_key(version), [],
+ lines)
+ else:
+ sha, _, _ = vf._add_text(self.get_simple_key(version), [],
+ ''.join(lines))
+ shas.append(sha)
+ # we now have a copy of all the lines in the vf.
+ for sha, (version, lines) in zip(
+ shas, (empty_text, sample_text_nl, sample_text_no_nl)):
+ new_key = self.get_simple_key(version + "2")
+ self.assertRaises(errors.ExistingContent,
+ vf.add_lines, new_key, [], lines,
+ nostore_sha=sha)
+ self.assertRaises(errors.ExistingContent,
+ vf._add_text, new_key, [], ''.join(lines),
+ nostore_sha=sha)
+ # and no new version should have been added.
+ record = vf.get_record_stream([new_key], 'unordered', True).next()
+ self.assertEqual('absent', record.storage_kind)
+
+ def test_add_lines_nostoresha(self):
+ self._add_content_nostoresha(add_lines=True)
+
+ def test__add_text_nostoresha(self):
+ self._add_content_nostoresha(add_lines=False)
+
+ def test_add_lines_return(self):
+ files = self.get_versionedfiles()
+ # save code by using the stock data insertion helper.
+ adds = self.get_diamond_files(files)
+ results = []
+ # We can only validate the first 2 elements returned from add_lines.
+ for add in adds:
+ self.assertEqual(3, len(add))
+ results.append(add[:2])
+ if self.key_length == 1:
+ self.assertEqual([
+ ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
+ ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
+ ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
+ ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
+ ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
+ results)
+ elif self.key_length == 2:
+ self.assertEqual([
+ ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
+ ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
+ ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
+ ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
+ ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
+ ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
+ ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
+ ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
+ ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23),
+ ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
+ results)
+
+ def test_add_lines_no_key_generates_chk_key(self):
+ files = self.get_versionedfiles()
+ # save code by using the stock data insertion helper.
+ adds = self.get_diamond_files(files, nokeys=True)
+ results = []
+ # We can only validate the first 2 elements returned from add_lines.
+ for add in adds:
+ self.assertEqual(3, len(add))
+ results.append(add[:2])
+ if self.key_length == 1:
+ self.assertEqual([
+ ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
+ ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
+ ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
+ ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
+ ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
+ results)
+ # Check the added items got CHK keys.
+ self.assertEqual(set([
+ ('sha1:00e364d235126be43292ab09cb4686cf703ddc17',),
+ ('sha1:51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44',),
+ ('sha1:9ef09dfa9d86780bdec9219a22560c6ece8e0ef1',),
+ ('sha1:a8478686da38e370e32e42e8a0c220e33ee9132f',),
+ ('sha1:ed8bce375198ea62444dc71952b22cfc2b09226d',),
+ ]),
+ files.keys())
+ elif self.key_length == 2:
+ self.assertEqual([
+ ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
+ ('00e364d235126be43292ab09cb4686cf703ddc17', 7),
+ ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
+ ('51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44', 5),
+ ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
+ ('a8478686da38e370e32e42e8a0c220e33ee9132f', 10),
+ ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
+ ('9ef09dfa9d86780bdec9219a22560c6ece8e0ef1', 11),
+ ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23),
+ ('ed8bce375198ea62444dc71952b22cfc2b09226d', 23)],
+ results)
+ # Check the added items got CHK keys.
+ self.assertEqual(set([
+ ('FileA', 'sha1:00e364d235126be43292ab09cb4686cf703ddc17'),
+ ('FileA', 'sha1:51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44'),
+ ('FileA', 'sha1:9ef09dfa9d86780bdec9219a22560c6ece8e0ef1'),
+ ('FileA', 'sha1:a8478686da38e370e32e42e8a0c220e33ee9132f'),
+ ('FileA', 'sha1:ed8bce375198ea62444dc71952b22cfc2b09226d'),
+ ('FileB', 'sha1:00e364d235126be43292ab09cb4686cf703ddc17'),
+ ('FileB', 'sha1:51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44'),
+ ('FileB', 'sha1:9ef09dfa9d86780bdec9219a22560c6ece8e0ef1'),
+ ('FileB', 'sha1:a8478686da38e370e32e42e8a0c220e33ee9132f'),
+ ('FileB', 'sha1:ed8bce375198ea62444dc71952b22cfc2b09226d'),
+ ]),
+ files.keys())
+
+ def test_empty_lines(self):
+ """Empty files can be stored."""
+ f = self.get_versionedfiles()
+ key_a = self.get_simple_key('a')
+ f.add_lines(key_a, [], [])
+ self.assertEqual('',
+ f.get_record_stream([key_a], 'unordered', True
+ ).next().get_bytes_as('fulltext'))
+ key_b = self.get_simple_key('b')
+ f.add_lines(key_b, self.get_parents([key_a]), [])
+ self.assertEqual('',
+ f.get_record_stream([key_b], 'unordered', True
+ ).next().get_bytes_as('fulltext'))
+
+ def test_newline_only(self):
+ f = self.get_versionedfiles()
+ key_a = self.get_simple_key('a')
+ f.add_lines(key_a, [], ['\n'])
+ self.assertEqual('\n',
+ f.get_record_stream([key_a], 'unordered', True
+ ).next().get_bytes_as('fulltext'))
+ key_b = self.get_simple_key('b')
+ f.add_lines(key_b, self.get_parents([key_a]), ['\n'])
+ self.assertEqual('\n',
+ f.get_record_stream([key_b], 'unordered', True
+ ).next().get_bytes_as('fulltext'))
+
+ def test_get_known_graph_ancestry(self):
+ f = self.get_versionedfiles()
+ if not self.graph:
+ raise TestNotApplicable('ancestry info only relevant with graph.')
+ key_a = self.get_simple_key('a')
+ key_b = self.get_simple_key('b')
+ key_c = self.get_simple_key('c')
+ # A
+ # |\
+ # | B
+ # |/
+ # C
+ f.add_lines(key_a, [], ['\n'])
+ f.add_lines(key_b, [key_a], ['\n'])
+ f.add_lines(key_c, [key_a, key_b], ['\n'])
+ kg = f.get_known_graph_ancestry([key_c])
+ self.assertIsInstance(kg, _mod_graph.KnownGraph)
+ self.assertEqual([key_a, key_b, key_c], list(kg.topo_sort()))
+
+ def test_known_graph_with_fallbacks(self):
+ f = self.get_versionedfiles('files')
+ if not self.graph:
+ raise TestNotApplicable('ancestry info only relevant with graph.')
+ if getattr(f, 'add_fallback_versioned_files', None) is None:
+ raise TestNotApplicable("%s doesn't support fallbacks"
+ % (f.__class__.__name__,))
+ key_a = self.get_simple_key('a')
+ key_b = self.get_simple_key('b')
+ key_c = self.get_simple_key('c')
+ # A only in fallback
+ # |\
+ # | B
+ # |/
+ # C
+ g = self.get_versionedfiles('fallback')
+ g.add_lines(key_a, [], ['\n'])
+ f.add_fallback_versioned_files(g)
+ f.add_lines(key_b, [key_a], ['\n'])
+ f.add_lines(key_c, [key_a, key_b], ['\n'])
+ kg = f.get_known_graph_ancestry([key_c])
+ self.assertEqual([key_a, key_b, key_c], list(kg.topo_sort()))
+
+ def test_get_record_stream_empty(self):
+ """An empty stream can be requested without error."""
+ f = self.get_versionedfiles()
+ entries = f.get_record_stream([], 'unordered', False)
+ self.assertEqual([], list(entries))
+
+ def assertValidStorageKind(self, storage_kind):
+ """Assert that storage_kind is a valid storage_kind."""
+ self.assertSubset([storage_kind],
+ ['mpdiff', 'knit-annotated-ft', 'knit-annotated-delta',
+ 'knit-ft', 'knit-delta', 'chunked', 'fulltext',
+ 'knit-annotated-ft-gz', 'knit-annotated-delta-gz', 'knit-ft-gz',
+ 'knit-delta-gz',
+ 'knit-delta-closure', 'knit-delta-closure-ref',
+ 'groupcompress-block', 'groupcompress-block-ref'])
+
+ def capture_stream(self, f, entries, on_seen, parents,
+ require_fulltext=False):
+ """Capture a stream for testing."""
+ for factory in entries:
+ on_seen(factory.key)
+ self.assertValidStorageKind(factory.storage_kind)
+ if factory.sha1 is not None:
+ self.assertEqual(f.get_sha1s([factory.key])[factory.key],
+ factory.sha1)
+ self.assertEqual(parents[factory.key], factory.parents)
+ self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
+ str)
+ if require_fulltext:
+ factory.get_bytes_as('fulltext')
+
+ def test_get_record_stream_interface(self):
+ """each item in a stream has to provide a regular interface."""
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ keys, _ = self.get_keys_and_sort_order()
+ parent_map = files.get_parent_map(keys)
+ entries = files.get_record_stream(keys, 'unordered', False)
+ seen = set()
+ self.capture_stream(files, entries, seen.add, parent_map)
+ self.assertEqual(set(keys), seen)
+
+ def get_keys_and_sort_order(self):
+ """Get diamond test keys list, and their sort ordering."""
+ if self.key_length == 1:
+ keys = [('merged',), ('left',), ('right',), ('base',)]
+ sort_order = {('merged',):2, ('left',):1, ('right',):1, ('base',):0}
+ else:
+ keys = [
+ ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
+ ('FileA', 'base'),
+ ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
+ ('FileB', 'base'),
+ ]
+ sort_order = {
+ ('FileA', 'merged'):2, ('FileA', 'left'):1, ('FileA', 'right'):1,
+ ('FileA', 'base'):0,
+ ('FileB', 'merged'):2, ('FileB', 'left'):1, ('FileB', 'right'):1,
+ ('FileB', 'base'):0,
+ }
+ return keys, sort_order
+
+ def get_keys_and_groupcompress_sort_order(self):
+ """Get diamond test keys list, and their groupcompress sort ordering."""
+ if self.key_length == 1:
+ keys = [('merged',), ('left',), ('right',), ('base',)]
+ sort_order = {('merged',):0, ('left',):1, ('right',):1, ('base',):2}
+ else:
+ keys = [
+ ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
+ ('FileA', 'base'),
+ ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
+ ('FileB', 'base'),
+ ]
+ sort_order = {
+ ('FileA', 'merged'):0, ('FileA', 'left'):1, ('FileA', 'right'):1,
+ ('FileA', 'base'):2,
+ ('FileB', 'merged'):3, ('FileB', 'left'):4, ('FileB', 'right'):4,
+ ('FileB', 'base'):5,
+ }
+ return keys, sort_order
+
+ def test_get_record_stream_interface_ordered(self):
+ """each item in a stream has to provide a regular interface."""
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ keys, sort_order = self.get_keys_and_sort_order()
+ parent_map = files.get_parent_map(keys)
+ entries = files.get_record_stream(keys, 'topological', False)
+ seen = []
+ self.capture_stream(files, entries, seen.append, parent_map)
+ self.assertStreamOrder(sort_order, seen, keys)
+
+ def test_get_record_stream_interface_ordered_with_delta_closure(self):
+ """each item must be accessible as a fulltext."""
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ keys, sort_order = self.get_keys_and_sort_order()
+ parent_map = files.get_parent_map(keys)
+ entries = files.get_record_stream(keys, 'topological', True)
+ seen = []
+ for factory in entries:
+ seen.append(factory.key)
+ self.assertValidStorageKind(factory.storage_kind)
+ self.assertSubset([factory.sha1],
+ [None, files.get_sha1s([factory.key])[factory.key]])
+ self.assertEqual(parent_map[factory.key], factory.parents)
+ # self.assertEqual(files.get_text(factory.key),
+ ft_bytes = factory.get_bytes_as('fulltext')
+ self.assertIsInstance(ft_bytes, str)
+ chunked_bytes = factory.get_bytes_as('chunked')
+ self.assertEqualDiff(ft_bytes, ''.join(chunked_bytes))
+
+ self.assertStreamOrder(sort_order, seen, keys)
+
+ def test_get_record_stream_interface_groupcompress(self):
+ """each item in a stream has to provide a regular interface."""
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ keys, sort_order = self.get_keys_and_groupcompress_sort_order()
+ parent_map = files.get_parent_map(keys)
+ entries = files.get_record_stream(keys, 'groupcompress', False)
+ seen = []
+ self.capture_stream(files, entries, seen.append, parent_map)
+ self.assertStreamOrder(sort_order, seen, keys)
+
+ def assertStreamOrder(self, sort_order, seen, keys):
+ self.assertEqual(len(set(seen)), len(keys))
+ if self.key_length == 1:
+ lows = {():0}
+ else:
+ lows = {('FileA',):0, ('FileB',):0}
+ if not self.graph:
+ self.assertEqual(set(keys), set(seen))
+ else:
+ for key in seen:
+ sort_pos = sort_order[key]
+ self.assertTrue(sort_pos >= lows[key[:-1]],
+ "Out of order in sorted stream: %r, %r" % (key, seen))
+ lows[key[:-1]] = sort_pos
+
+ def test_get_record_stream_unknown_storage_kind_raises(self):
+ """Asking for a storage kind that the stream cannot supply raises."""
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ if self.key_length == 1:
+ keys = [('merged',), ('left',), ('right',), ('base',)]
+ else:
+ keys = [
+ ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
+ ('FileA', 'base'),
+ ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
+ ('FileB', 'base'),
+ ]
+ parent_map = files.get_parent_map(keys)
+ entries = files.get_record_stream(keys, 'unordered', False)
+ # We track the contents because we should be able to try, fail a
+ # particular kind and then ask for one that works and continue.
+ seen = set()
+ for factory in entries:
+ seen.add(factory.key)
+ self.assertValidStorageKind(factory.storage_kind)
+ if factory.sha1 is not None:
+ self.assertEqual(files.get_sha1s([factory.key])[factory.key],
+ factory.sha1)
+ self.assertEqual(parent_map[factory.key], factory.parents)
+ # currently no stream emits mpdiff
+ self.assertRaises(errors.UnavailableRepresentation,
+ factory.get_bytes_as, 'mpdiff')
+ self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
+ str)
+ self.assertEqual(set(keys), seen)
+
+ def test_get_record_stream_missing_records_are_absent(self):
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ if self.key_length == 1:
+ keys = [('merged',), ('left',), ('right',), ('absent',), ('base',)]
+ else:
+ keys = [
+ ('FileA', 'merged'), ('FileA', 'left'), ('FileA', 'right'),
+ ('FileA', 'absent'), ('FileA', 'base'),
+ ('FileB', 'merged'), ('FileB', 'left'), ('FileB', 'right'),
+ ('FileB', 'absent'), ('FileB', 'base'),
+ ('absent', 'absent'),
+ ]
+ parent_map = files.get_parent_map(keys)
+ entries = files.get_record_stream(keys, 'unordered', False)
+ self.assertAbsentRecord(files, keys, parent_map, entries)
+ entries = files.get_record_stream(keys, 'topological', False)
+ self.assertAbsentRecord(files, keys, parent_map, entries)
+
+ def assertRecordHasContent(self, record, bytes):
+ """Assert that record has the bytes bytes."""
+ self.assertEqual(bytes, record.get_bytes_as('fulltext'))
+ self.assertEqual(bytes, ''.join(record.get_bytes_as('chunked')))
+
+ def test_get_record_stream_native_formats_are_wire_ready_one_ft(self):
+ files = self.get_versionedfiles()
+ key = self.get_simple_key('foo')
+ files.add_lines(key, (), ['my text\n', 'content'])
+ stream = files.get_record_stream([key], 'unordered', False)
+ record = stream.next()
+ if record.storage_kind in ('chunked', 'fulltext'):
+ # chunked and fulltext representations are for direct use not wire
+ # serialisation: check they are able to be used directly. To send
+ # such records over the wire translation will be needed.
+ self.assertRecordHasContent(record, "my text\ncontent")
+ else:
+ bytes = [record.get_bytes_as(record.storage_kind)]
+ network_stream = versionedfile.NetworkRecordStream(bytes).read()
+ source_record = record
+ records = []
+ for record in network_stream:
+ records.append(record)
+ self.assertEqual(source_record.storage_kind,
+ record.storage_kind)
+ self.assertEqual(source_record.parents, record.parents)
+ self.assertEqual(
+ source_record.get_bytes_as(source_record.storage_kind),
+ record.get_bytes_as(record.storage_kind))
+ self.assertEqual(1, len(records))
+
+ def assertStreamMetaEqual(self, records, expected, stream):
+ """Assert that streams expected and stream have the same records.
+
+ :param records: A list to collect the seen records.
+ :return: A generator of the records in stream.
+ """
+ # We make assertions during copying to catch things early for
+ # easier debugging.
+ for record, ref_record in izip(stream, expected):
+ records.append(record)
+ self.assertEqual(ref_record.key, record.key)
+ self.assertEqual(ref_record.storage_kind, record.storage_kind)
+ self.assertEqual(ref_record.parents, record.parents)
+ yield record
+
+ def stream_to_bytes_or_skip_counter(self, skipped_records, full_texts,
+ stream):
+ """Convert a stream to a bytes iterator.
+
+ :param skipped_records: A list with one element to increment when a
+ record is skipped.
+ :param full_texts: A dict from key->fulltext representation, for
+ checking chunked or fulltext stored records.
+ :param stream: A record_stream.
+ :return: An iterator over the bytes of each record.
+ """
+ for record in stream:
+ if record.storage_kind in ('chunked', 'fulltext'):
+ skipped_records[0] += 1
+ # check the content is correct for direct use.
+ self.assertRecordHasContent(record, full_texts[record.key])
+ else:
+ yield record.get_bytes_as(record.storage_kind)
+
+ def test_get_record_stream_native_formats_are_wire_ready_ft_delta(self):
+ files = self.get_versionedfiles()
+ target_files = self.get_versionedfiles('target')
+ key = self.get_simple_key('ft')
+ key_delta = self.get_simple_key('delta')
+ files.add_lines(key, (), ['my text\n', 'content'])
+ if self.graph:
+ delta_parents = (key,)
+ else:
+ delta_parents = ()
+ files.add_lines(key_delta, delta_parents, ['different\n', 'content\n'])
+ local = files.get_record_stream([key, key_delta], 'unordered', False)
+ ref = files.get_record_stream([key, key_delta], 'unordered', False)
+ skipped_records = [0]
+ full_texts = {
+ key: "my text\ncontent",
+ key_delta: "different\ncontent\n",
+ }
+ byte_stream = self.stream_to_bytes_or_skip_counter(
+ skipped_records, full_texts, local)
+ network_stream = versionedfile.NetworkRecordStream(byte_stream).read()
+ records = []
+ # insert the stream from the network into a versioned files object so we can
+ # check the content was carried across correctly without doing delta
+ # inspection.
+ target_files.insert_record_stream(
+ self.assertStreamMetaEqual(records, ref, network_stream))
+ # No duplicates on the wire thank you!
+ self.assertEqual(2, len(records) + skipped_records[0])
+ if len(records):
+ # if any content was copied it all must have all been.
+ self.assertIdenticalVersionedFile(files, target_files)
+
+ def test_get_record_stream_native_formats_are_wire_ready_delta(self):
+ # copy a delta over the wire
+ files = self.get_versionedfiles()
+ target_files = self.get_versionedfiles('target')
+ key = self.get_simple_key('ft')
+ key_delta = self.get_simple_key('delta')
+ files.add_lines(key, (), ['my text\n', 'content'])
+ if self.graph:
+ delta_parents = (key,)
+ else:
+ delta_parents = ()
+ files.add_lines(key_delta, delta_parents, ['different\n', 'content\n'])
+ # Copy the basis text across so we can reconstruct the delta during
+ # insertion into target.
+ target_files.insert_record_stream(files.get_record_stream([key],
+ 'unordered', False))
+ local = files.get_record_stream([key_delta], 'unordered', False)
+ ref = files.get_record_stream([key_delta], 'unordered', False)
+ skipped_records = [0]
+ full_texts = {
+ key_delta: "different\ncontent\n",
+ }
+ byte_stream = self.stream_to_bytes_or_skip_counter(
+ skipped_records, full_texts, local)
+ network_stream = versionedfile.NetworkRecordStream(byte_stream).read()
+ records = []
+ # insert the stream from the network into a versioned files object so we can
+ # check the content was carried across correctly without doing delta
+ # inspection during check_stream.
+ target_files.insert_record_stream(
+ self.assertStreamMetaEqual(records, ref, network_stream))
+ # No duplicates on the wire thank you!
+ self.assertEqual(1, len(records) + skipped_records[0])
+ if len(records):
+ # if any content was copied it all must have all been
+ self.assertIdenticalVersionedFile(files, target_files)
+
+ def test_get_record_stream_wire_ready_delta_closure_included(self):
+ # copy a delta over the wire with the ability to get its full text.
+ files = self.get_versionedfiles()
+ key = self.get_simple_key('ft')
+ key_delta = self.get_simple_key('delta')
+ files.add_lines(key, (), ['my text\n', 'content'])
+ if self.graph:
+ delta_parents = (key,)
+ else:
+ delta_parents = ()
+ files.add_lines(key_delta, delta_parents, ['different\n', 'content\n'])
+ local = files.get_record_stream([key_delta], 'unordered', True)
+ ref = files.get_record_stream([key_delta], 'unordered', True)
+ skipped_records = [0]
+ full_texts = {
+ key_delta: "different\ncontent\n",
+ }
+ byte_stream = self.stream_to_bytes_or_skip_counter(
+ skipped_records, full_texts, local)
+ network_stream = versionedfile.NetworkRecordStream(byte_stream).read()
+ records = []
+ # insert the stream from the network into a versioned files object so we can
+ # check the content was carried across correctly without doing delta
+ # inspection during check_stream.
+ for record in self.assertStreamMetaEqual(records, ref, network_stream):
+ # we have to be able to get the full text out:
+ self.assertRecordHasContent(record, full_texts[record.key])
+ # No duplicates on the wire thank you!
+ self.assertEqual(1, len(records) + skipped_records[0])
+
+ def assertAbsentRecord(self, files, keys, parents, entries):
+ """Helper for test_get_record_stream_missing_records_are_absent."""
+ seen = set()
+ for factory in entries:
+ seen.add(factory.key)
+ if factory.key[-1] == 'absent':
+ self.assertEqual('absent', factory.storage_kind)
+ self.assertEqual(None, factory.sha1)
+ self.assertEqual(None, factory.parents)
+ else:
+ self.assertValidStorageKind(factory.storage_kind)
+ if factory.sha1 is not None:
+ sha1 = files.get_sha1s([factory.key])[factory.key]
+ self.assertEqual(sha1, factory.sha1)
+ self.assertEqual(parents[factory.key], factory.parents)
+ self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
+ str)
+ self.assertEqual(set(keys), seen)
+
+ def test_filter_absent_records(self):
+ """Requested missing records can be filter trivially."""
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ keys, _ = self.get_keys_and_sort_order()
+ parent_map = files.get_parent_map(keys)
+ # Add an absent record in the middle of the present keys. (We don't ask
+ # for just absent keys to ensure that content before and after the
+ # absent keys is still delivered).
+ present_keys = list(keys)
+ if self.key_length == 1:
+ keys.insert(2, ('extra',))
+ else:
+ keys.insert(2, ('extra', 'extra'))
+ entries = files.get_record_stream(keys, 'unordered', False)
+ seen = set()
+ self.capture_stream(files, versionedfile.filter_absent(entries), seen.add,
+ parent_map)
+ self.assertEqual(set(present_keys), seen)
+
+ def get_mapper(self):
+ """Get a mapper suitable for the key length of the test interface."""
+ if self.key_length == 1:
+ return ConstantMapper('source')
+ else:
+ return HashEscapedPrefixMapper()
+
+ def get_parents(self, parents):
+ """Get parents, taking self.graph into consideration."""
+ if self.graph:
+ return parents
+ else:
+ return None
+
+ def test_get_annotator(self):
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ origin_key = self.get_simple_key('origin')
+ base_key = self.get_simple_key('base')
+ left_key = self.get_simple_key('left')
+ right_key = self.get_simple_key('right')
+ merged_key = self.get_simple_key('merged')
+ # annotator = files.get_annotator()
+ # introduced full text
+ origins, lines = files.get_annotator().annotate(origin_key)
+ self.assertEqual([(origin_key,)], origins)
+ self.assertEqual(['origin\n'], lines)
+ # a delta
+ origins, lines = files.get_annotator().annotate(base_key)
+ self.assertEqual([(base_key,)], origins)
+ # a merge
+ origins, lines = files.get_annotator().annotate(merged_key)
+ if self.graph:
+ self.assertEqual([
+ (base_key,),
+ (left_key,),
+ (right_key,),
+ (merged_key,),
+ ], origins)
+ else:
+ # Without a graph everything is new.
+ self.assertEqual([
+ (merged_key,),
+ (merged_key,),
+ (merged_key,),
+ (merged_key,),
+ ], origins)
+ self.assertRaises(RevisionNotPresent,
+ files.get_annotator().annotate, self.get_simple_key('missing-key'))
+
+ def test_get_parent_map(self):
+ files = self.get_versionedfiles()
+ if self.key_length == 1:
+ parent_details = [
+ (('r0',), self.get_parents(())),
+ (('r1',), self.get_parents((('r0',),))),
+ (('r2',), self.get_parents(())),
+ (('r3',), self.get_parents(())),
+ (('m',), self.get_parents((('r0',),('r1',),('r2',),('r3',)))),
+ ]
+ else:
+ parent_details = [
+ (('FileA', 'r0'), self.get_parents(())),
+ (('FileA', 'r1'), self.get_parents((('FileA', 'r0'),))),
+ (('FileA', 'r2'), self.get_parents(())),
+ (('FileA', 'r3'), self.get_parents(())),
+ (('FileA', 'm'), self.get_parents((('FileA', 'r0'),
+ ('FileA', 'r1'), ('FileA', 'r2'), ('FileA', 'r3')))),
+ ]
+ for key, parents in parent_details:
+ files.add_lines(key, parents, [])
+ # immediately after adding it should be queryable.
+ self.assertEqual({key:parents}, files.get_parent_map([key]))
+ # We can ask for an empty set
+ self.assertEqual({}, files.get_parent_map([]))
+ # We can ask for many keys
+ all_parents = dict(parent_details)
+ self.assertEqual(all_parents, files.get_parent_map(all_parents.keys()))
+ # Absent keys are just not included in the result.
+ keys = all_parents.keys()
+ if self.key_length == 1:
+ keys.insert(1, ('missing',))
+ else:
+ keys.insert(1, ('missing', 'missing'))
+ # Absent keys are just ignored
+ self.assertEqual(all_parents, files.get_parent_map(keys))
+
+ def test_get_sha1s(self):
+ files = self.get_versionedfiles()
+ self.get_diamond_files(files)
+ if self.key_length == 1:
+ keys = [('base',), ('origin',), ('left',), ('merged',), ('right',)]
+ else:
+ # ask for shas from different prefixes.
+ keys = [
+ ('FileA', 'base'), ('FileB', 'origin'), ('FileA', 'left'),
+ ('FileA', 'merged'), ('FileB', 'right'),
+ ]
+ self.assertEqual({
+ keys[0]: '51c64a6f4fc375daf0d24aafbabe4d91b6f4bb44',
+ keys[1]: '00e364d235126be43292ab09cb4686cf703ddc17',
+ keys[2]: 'a8478686da38e370e32e42e8a0c220e33ee9132f',
+ keys[3]: 'ed8bce375198ea62444dc71952b22cfc2b09226d',
+ keys[4]: '9ef09dfa9d86780bdec9219a22560c6ece8e0ef1',
+ },
+ files.get_sha1s(keys))
+
+ def test_insert_record_stream_empty(self):
+ """Inserting an empty record stream should work."""
+ files = self.get_versionedfiles()
+ files.insert_record_stream([])
+
+ def assertIdenticalVersionedFile(self, expected, actual):
+ """Assert that left and right have the same contents."""
+ self.assertEqual(set(actual.keys()), set(expected.keys()))
+ actual_parents = actual.get_parent_map(actual.keys())
+ if self.graph:
+ self.assertEqual(actual_parents, expected.get_parent_map(expected.keys()))
+ else:
+ for key, parents in actual_parents.items():
+ self.assertEqual(None, parents)
+ for key in actual.keys():
+ actual_text = actual.get_record_stream(
+ [key], 'unordered', True).next().get_bytes_as('fulltext')
+ expected_text = expected.get_record_stream(
+ [key], 'unordered', True).next().get_bytes_as('fulltext')
+ self.assertEqual(actual_text, expected_text)
+
+ def test_insert_record_stream_fulltexts(self):
+ """Any file should accept a stream of fulltexts."""
+ files = self.get_versionedfiles()
+ mapper = self.get_mapper()
+ source_transport = self.get_transport('source')
+ source_transport.mkdir('.')
+ # weaves always output fulltexts.
+ source = make_versioned_files_factory(WeaveFile, mapper)(
+ source_transport)
+ self.get_diamond_files(source, trailing_eol=False)
+ stream = source.get_record_stream(source.keys(), 'topological',
+ False)
+ files.insert_record_stream(stream)
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_fulltexts_noeol(self):
+ """Any file should accept a stream of fulltexts."""
+ files = self.get_versionedfiles()
+ mapper = self.get_mapper()
+ source_transport = self.get_transport('source')
+ source_transport.mkdir('.')
+ # weaves always output fulltexts.
+ source = make_versioned_files_factory(WeaveFile, mapper)(
+ source_transport)
+ self.get_diamond_files(source, trailing_eol=False)
+ stream = source.get_record_stream(source.keys(), 'topological',
+ False)
+ files.insert_record_stream(stream)
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_annotated_knits(self):
+ """Any file should accept a stream from plain knits."""
+ files = self.get_versionedfiles()
+ mapper = self.get_mapper()
+ source_transport = self.get_transport('source')
+ source_transport.mkdir('.')
+ source = make_file_factory(True, mapper)(source_transport)
+ self.get_diamond_files(source)
+ stream = source.get_record_stream(source.keys(), 'topological',
+ False)
+ files.insert_record_stream(stream)
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_annotated_knits_noeol(self):
+ """Any file should accept a stream from plain knits."""
+ files = self.get_versionedfiles()
+ mapper = self.get_mapper()
+ source_transport = self.get_transport('source')
+ source_transport.mkdir('.')
+ source = make_file_factory(True, mapper)(source_transport)
+ self.get_diamond_files(source, trailing_eol=False)
+ stream = source.get_record_stream(source.keys(), 'topological',
+ False)
+ files.insert_record_stream(stream)
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_plain_knits(self):
+ """Any file should accept a stream from plain knits."""
+ files = self.get_versionedfiles()
+ mapper = self.get_mapper()
+ source_transport = self.get_transport('source')
+ source_transport.mkdir('.')
+ source = make_file_factory(False, mapper)(source_transport)
+ self.get_diamond_files(source)
+ stream = source.get_record_stream(source.keys(), 'topological',
+ False)
+ files.insert_record_stream(stream)
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_plain_knits_noeol(self):
+ """Any file should accept a stream from plain knits."""
+ files = self.get_versionedfiles()
+ mapper = self.get_mapper()
+ source_transport = self.get_transport('source')
+ source_transport.mkdir('.')
+ source = make_file_factory(False, mapper)(source_transport)
+ self.get_diamond_files(source, trailing_eol=False)
+ stream = source.get_record_stream(source.keys(), 'topological',
+ False)
+ files.insert_record_stream(stream)
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_existing_keys(self):
+ """Inserting keys already in a file should not error."""
+ files = self.get_versionedfiles()
+ source = self.get_versionedfiles('source')
+ self.get_diamond_files(source)
+ # insert some keys into f.
+ self.get_diamond_files(files, left_only=True)
+ stream = source.get_record_stream(source.keys(), 'topological',
+ False)
+ files.insert_record_stream(stream)
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_missing_keys(self):
+ """Inserting a stream with absent keys should raise an error."""
+ files = self.get_versionedfiles()
+ source = self.get_versionedfiles('source')
+ stream = source.get_record_stream([('missing',) * self.key_length],
+ 'topological', False)
+ self.assertRaises(errors.RevisionNotPresent, files.insert_record_stream,
+ stream)
+
+ def test_insert_record_stream_out_of_order(self):
+ """An out of order stream can either error or work."""
+ files = self.get_versionedfiles()
+ source = self.get_versionedfiles('source')
+ self.get_diamond_files(source)
+ if self.key_length == 1:
+ origin_keys = [('origin',)]
+ end_keys = [('merged',), ('left',)]
+ start_keys = [('right',), ('base',)]
+ else:
+ origin_keys = [('FileA', 'origin'), ('FileB', 'origin')]
+ end_keys = [('FileA', 'merged',), ('FileA', 'left',),
+ ('FileB', 'merged',), ('FileB', 'left',)]
+ start_keys = [('FileA', 'right',), ('FileA', 'base',),
+ ('FileB', 'right',), ('FileB', 'base',)]
+ origin_entries = source.get_record_stream(origin_keys, 'unordered', False)
+ end_entries = source.get_record_stream(end_keys, 'topological', False)
+ start_entries = source.get_record_stream(start_keys, 'topological', False)
+ entries = chain(origin_entries, end_entries, start_entries)
+ try:
+ files.insert_record_stream(entries)
+ except RevisionNotPresent:
+ # Must not have corrupted the file.
+ files.check()
+ else:
+ self.assertIdenticalVersionedFile(source, files)
+
+ def test_insert_record_stream_long_parent_chain_out_of_order(self):
+ """An out of order stream can either error or work."""
+ if not self.graph:
+ raise TestNotApplicable('ancestry info only relevant with graph.')
+ # Create a reasonably long chain of records based on each other, where
+ # most will be deltas.
+ source = self.get_versionedfiles('source')
+ parents = ()
+ keys = []
+ content = [('same same %d\n' % n) for n in range(500)]
+ for letter in 'abcdefghijklmnopqrstuvwxyz':
+ key = ('key-' + letter,)
+ if self.key_length == 2:
+ key = ('prefix',) + key
+ content.append('content for ' + letter + '\n')
+ source.add_lines(key, parents, content)
+ keys.append(key)
+ parents = (key,)
+ # Create a stream of these records, excluding the first record that the
+ # rest ultimately depend upon, and insert it into a new vf.
+ streams = []
+ for key in reversed(keys):
+ streams.append(source.get_record_stream([key], 'unordered', False))
+ deltas = chain(*streams[:-1])
+ files = self.get_versionedfiles()
+ try:
+ files.insert_record_stream(deltas)
+ except RevisionNotPresent:
+ # Must not have corrupted the file.
+ files.check()
+ else:
+ # Must only report either just the first key as a missing parent,
+ # no key as missing (for nodelta scenarios).
+ missing = set(files.get_missing_compression_parent_keys())
+ missing.discard(keys[0])
+ self.assertEqual(set(), missing)
+
+ def get_knit_delta_source(self):
+ """Get a source that can produce a stream with knit delta records,
+ regardless of this test's scenario.
+ """
+ mapper = self.get_mapper()
+ source_transport = self.get_transport('source')
+ source_transport.mkdir('.')
+ source = make_file_factory(False, mapper)(source_transport)
+ get_diamond_files(source, self.key_length, trailing_eol=True,
+ nograph=False, left_only=False)
+ return source
+
+ def test_insert_record_stream_delta_missing_basis_no_corruption(self):
+ """Insertion where a needed basis is not included notifies the caller
+ of the missing basis. In the meantime a record missing its basis is
+ not added.
+ """
+ source = self.get_knit_delta_source()
+ keys = [self.get_simple_key('origin'), self.get_simple_key('merged')]
+ entries = source.get_record_stream(keys, 'unordered', False)
+ files = self.get_versionedfiles()
+ if self.support_partial_insertion:
+ self.assertEqual([],
+ list(files.get_missing_compression_parent_keys()))
+ files.insert_record_stream(entries)
+ missing_bases = files.get_missing_compression_parent_keys()
+ self.assertEqual(set([self.get_simple_key('left')]),
+ set(missing_bases))
+ self.assertEqual(set(keys), set(files.get_parent_map(keys)))
+ else:
+ self.assertRaises(
+ errors.RevisionNotPresent, files.insert_record_stream, entries)
+ files.check()
+
+ def test_insert_record_stream_delta_missing_basis_can_be_added_later(self):
+ """Insertion where a needed basis is not included notifies the caller
+ of the missing basis. That basis can be added in a second
+ insert_record_stream call that does not need to repeat records present
+ in the previous stream. The record(s) that required that basis are
+ fully inserted once their basis is no longer missing.
+ """
+ if not self.support_partial_insertion:
+ raise TestNotApplicable(
+ 'versioned file scenario does not support partial insertion')
+ source = self.get_knit_delta_source()
+ entries = source.get_record_stream([self.get_simple_key('origin'),
+ self.get_simple_key('merged')], 'unordered', False)
+ files = self.get_versionedfiles()
+ files.insert_record_stream(entries)
+ missing_bases = files.get_missing_compression_parent_keys()
+ self.assertEqual(set([self.get_simple_key('left')]),
+ set(missing_bases))
+ # 'merged' is inserted (although a commit of a write group involving
+ # this versionedfiles would fail).
+ merged_key = self.get_simple_key('merged')
+ self.assertEqual(
+ [merged_key], files.get_parent_map([merged_key]).keys())
+ # Add the full delta closure of the missing records
+ missing_entries = source.get_record_stream(
+ missing_bases, 'unordered', True)
+ files.insert_record_stream(missing_entries)
+ # Now 'merged' is fully inserted (and a commit would succeed).
+ self.assertEqual([], list(files.get_missing_compression_parent_keys()))
+ self.assertEqual(
+ [merged_key], files.get_parent_map([merged_key]).keys())
+ files.check()
+
+ def test_iter_lines_added_or_present_in_keys(self):
+ # test that we get at least an equalset of the lines added by
+ # versions in the store.
+ # the ordering here is to make a tree so that dumb searches have
+ # more changes to muck up.
+
+ class InstrumentedProgress(progress.ProgressTask):
+
+ def __init__(self):
+ progress.ProgressTask.__init__(self)
+ self.updates = []
+
+ def update(self, msg=None, current=None, total=None):
+ self.updates.append((msg, current, total))
+
+ files = self.get_versionedfiles()
+ # add a base to get included
+ files.add_lines(self.get_simple_key('base'), (), ['base\n'])
+ # add a ancestor to be included on one side
+ files.add_lines(self.get_simple_key('lancestor'), (), ['lancestor\n'])
+ # add a ancestor to be included on the other side
+ files.add_lines(self.get_simple_key('rancestor'),
+ self.get_parents([self.get_simple_key('base')]), ['rancestor\n'])
+ # add a child of rancestor with no eofile-nl
+ files.add_lines(self.get_simple_key('child'),
+ self.get_parents([self.get_simple_key('rancestor')]),
+ ['base\n', 'child\n'])
+ # add a child of lancestor and base to join the two roots
+ files.add_lines(self.get_simple_key('otherchild'),
+ self.get_parents([self.get_simple_key('lancestor'),
+ self.get_simple_key('base')]),
+ ['base\n', 'lancestor\n', 'otherchild\n'])
+ def iter_with_keys(keys, expected):
+ # now we need to see what lines are returned, and how often.
+ lines = {}
+ progress = InstrumentedProgress()
+ # iterate over the lines
+ for line in files.iter_lines_added_or_present_in_keys(keys,
+ pb=progress):
+ lines.setdefault(line, 0)
+ lines[line] += 1
+ if []!= progress.updates:
+ self.assertEqual(expected, progress.updates)
+ return lines
+ lines = iter_with_keys(
+ [self.get_simple_key('child'), self.get_simple_key('otherchild')],
+ [('Walking content', 0, 2),
+ ('Walking content', 1, 2),
+ ('Walking content', 2, 2)])
+ # we must see child and otherchild
+ self.assertTrue(lines[('child\n', self.get_simple_key('child'))] > 0)
+ self.assertTrue(
+ lines[('otherchild\n', self.get_simple_key('otherchild'))] > 0)
+ # we dont care if we got more than that.
+
+ # test all lines
+ lines = iter_with_keys(files.keys(),
+ [('Walking content', 0, 5),
+ ('Walking content', 1, 5),
+ ('Walking content', 2, 5),
+ ('Walking content', 3, 5),
+ ('Walking content', 4, 5),
+ ('Walking content', 5, 5)])
+ # all lines must be seen at least once
+ self.assertTrue(lines[('base\n', self.get_simple_key('base'))] > 0)
+ self.assertTrue(
+ lines[('lancestor\n', self.get_simple_key('lancestor'))] > 0)
+ self.assertTrue(
+ lines[('rancestor\n', self.get_simple_key('rancestor'))] > 0)
+ self.assertTrue(lines[('child\n', self.get_simple_key('child'))] > 0)
+ self.assertTrue(
+ lines[('otherchild\n', self.get_simple_key('otherchild'))] > 0)
+
+ def test_make_mpdiffs(self):
+ from bzrlib import multiparent
+ files = self.get_versionedfiles('source')
+ # add texts that should trip the knit maximum delta chain threshold
+ # as well as doing parallel chains of data in knits.
+ # this is done by two chains of 25 insertions
+ files.add_lines(self.get_simple_key('base'), [], ['line\n'])
+ files.add_lines(self.get_simple_key('noeol'),
+ self.get_parents([self.get_simple_key('base')]), ['line'])
+ # detailed eol tests:
+ # shared last line with parent no-eol
+ files.add_lines(self.get_simple_key('noeolsecond'),
+ self.get_parents([self.get_simple_key('noeol')]),
+ ['line\n', 'line'])
+ # differing last line with parent, both no-eol
+ files.add_lines(self.get_simple_key('noeolnotshared'),
+ self.get_parents([self.get_simple_key('noeolsecond')]),
+ ['line\n', 'phone'])
+ # add eol following a noneol parent, change content
+ files.add_lines(self.get_simple_key('eol'),
+ self.get_parents([self.get_simple_key('noeol')]), ['phone\n'])
+ # add eol following a noneol parent, no change content
+ files.add_lines(self.get_simple_key('eolline'),
+ self.get_parents([self.get_simple_key('noeol')]), ['line\n'])
+ # noeol with no parents:
+ files.add_lines(self.get_simple_key('noeolbase'), [], ['line'])
+ # noeol preceeding its leftmost parent in the output:
+ # this is done by making it a merge of two parents with no common
+ # anestry: noeolbase and noeol with the
+ # later-inserted parent the leftmost.
+ files.add_lines(self.get_simple_key('eolbeforefirstparent'),
+ self.get_parents([self.get_simple_key('noeolbase'),
+ self.get_simple_key('noeol')]),
+ ['line'])
+ # two identical eol texts
+ files.add_lines(self.get_simple_key('noeoldup'),
+ self.get_parents([self.get_simple_key('noeol')]), ['line'])
+ next_parent = self.get_simple_key('base')
+ text_name = 'chain1-'
+ text = ['line\n']
+ sha1s = {0 :'da6d3141cb4a5e6f464bf6e0518042ddc7bfd079',
+ 1 :'45e21ea146a81ea44a821737acdb4f9791c8abe7',
+ 2 :'e1f11570edf3e2a070052366c582837a4fe4e9fa',
+ 3 :'26b4b8626da827088c514b8f9bbe4ebf181edda1',
+ 4 :'e28a5510be25ba84d31121cff00956f9970ae6f6',
+ 5 :'d63ec0ce22e11dcf65a931b69255d3ac747a318d',
+ 6 :'2c2888d288cb5e1d98009d822fedfe6019c6a4ea',
+ 7 :'95c14da9cafbf828e3e74a6f016d87926ba234ab',
+ 8 :'779e9a0b28f9f832528d4b21e17e168c67697272',
+ 9 :'1f8ff4e5c6ff78ac106fcfe6b1e8cb8740ff9a8f',
+ 10:'131a2ae712cf51ed62f143e3fbac3d4206c25a05',
+ 11:'c5a9d6f520d2515e1ec401a8f8a67e6c3c89f199',
+ 12:'31a2286267f24d8bedaa43355f8ad7129509ea85',
+ 13:'dc2a7fe80e8ec5cae920973973a8ee28b2da5e0a',
+ 14:'2c4b1736566b8ca6051e668de68650686a3922f2',
+ 15:'5912e4ecd9b0c07be4d013e7e2bdcf9323276cde',
+ 16:'b0d2e18d3559a00580f6b49804c23fea500feab3',
+ 17:'8e1d43ad72f7562d7cb8f57ee584e20eb1a69fc7',
+ 18:'5cf64a3459ae28efa60239e44b20312d25b253f3',
+ 19:'1ebed371807ba5935958ad0884595126e8c4e823',
+ 20:'2aa62a8b06fb3b3b892a3292a068ade69d5ee0d3',
+ 21:'01edc447978004f6e4e962b417a4ae1955b6fe5d',
+ 22:'d8d8dc49c4bf0bab401e0298bb5ad827768618bb',
+ 23:'c21f62b1c482862983a8ffb2b0c64b3451876e3f',
+ 24:'c0593fe795e00dff6b3c0fe857a074364d5f04fc',
+ 25:'dd1a1cf2ba9cc225c3aff729953e6364bf1d1855',
+ }
+ for depth in range(26):
+ new_version = self.get_simple_key(text_name + '%s' % depth)
+ text = text + ['line\n']
+ files.add_lines(new_version, self.get_parents([next_parent]), text)
+ next_parent = new_version
+ next_parent = self.get_simple_key('base')
+ text_name = 'chain2-'
+ text = ['line\n']
+ for depth in range(26):
+ new_version = self.get_simple_key(text_name + '%s' % depth)
+ text = text + ['line\n']
+ files.add_lines(new_version, self.get_parents([next_parent]), text)
+ next_parent = new_version
+ target = self.get_versionedfiles('target')
+ for key in multiparent.topo_iter_keys(files, files.keys()):
+ mpdiff = files.make_mpdiffs([key])[0]
+ parents = files.get_parent_map([key])[key] or []
+ target.add_mpdiffs(
+ [(key, parents, files.get_sha1s([key])[key], mpdiff)])
+ self.assertEqualDiff(
+ files.get_record_stream([key], 'unordered',
+ True).next().get_bytes_as('fulltext'),
+ target.get_record_stream([key], 'unordered',
+ True).next().get_bytes_as('fulltext')
+ )
+
+ def test_keys(self):
+ # While use is discouraged, versions() is still needed by aspects of
+ # bzr.
+ files = self.get_versionedfiles()
+ self.assertEqual(set(), set(files.keys()))
+ if self.key_length == 1:
+ key = ('foo',)
+ else:
+ key = ('foo', 'bar',)
+ files.add_lines(key, (), [])
+ self.assertEqual(set([key]), set(files.keys()))
+
+
+class VirtualVersionedFilesTests(TestCase):
+ """Basic tests for the VirtualVersionedFiles implementations."""
+
+ def _get_parent_map(self, keys):
+ ret = {}
+ for k in keys:
+ if k in self._parent_map:
+ ret[k] = self._parent_map[k]
+ return ret
+
+ def setUp(self):
+ TestCase.setUp(self)
+ self._lines = {}
+ self._parent_map = {}
+ self.texts = VirtualVersionedFiles(self._get_parent_map,
+ self._lines.get)
+
+ def test_add_lines(self):
+ self.assertRaises(NotImplementedError,
+ self.texts.add_lines, "foo", [], [])
+
+ def test_add_mpdiffs(self):
+ self.assertRaises(NotImplementedError,
+ self.texts.add_mpdiffs, [])
+
+ def test_check_noerrors(self):
+ self.texts.check()
+
+ def test_insert_record_stream(self):
+ self.assertRaises(NotImplementedError, self.texts.insert_record_stream,
+ [])
+
+ def test_get_sha1s_nonexistent(self):
+ self.assertEquals({}, self.texts.get_sha1s([("NONEXISTENT",)]))
+
+ def test_get_sha1s(self):
+ self._lines["key"] = ["dataline1", "dataline2"]
+ self.assertEquals({("key",): osutils.sha_strings(self._lines["key"])},
+ self.texts.get_sha1s([("key",)]))
+
+ def test_get_parent_map(self):
+ self._parent_map = {"G": ("A", "B")}
+ self.assertEquals({("G",): (("A",),("B",))},
+ self.texts.get_parent_map([("G",), ("L",)]))
+
+ def test_get_record_stream(self):
+ self._lines["A"] = ["FOO", "BAR"]
+ it = self.texts.get_record_stream([("A",)], "unordered", True)
+ record = it.next()
+ self.assertEquals("chunked", record.storage_kind)
+ self.assertEquals("FOOBAR", record.get_bytes_as("fulltext"))
+ self.assertEquals(["FOO", "BAR"], record.get_bytes_as("chunked"))
+
+ def test_get_record_stream_absent(self):
+ it = self.texts.get_record_stream([("A",)], "unordered", True)
+ record = it.next()
+ self.assertEquals("absent", record.storage_kind)
+
+ def test_iter_lines_added_or_present_in_keys(self):
+ self._lines["A"] = ["FOO", "BAR"]
+ self._lines["B"] = ["HEY"]
+ self._lines["C"] = ["Alberta"]
+ it = self.texts.iter_lines_added_or_present_in_keys([("A",), ("B",)])
+ self.assertEquals(sorted([("FOO", "A"), ("BAR", "A"), ("HEY", "B")]),
+ sorted(list(it)))
+
+
+class TestOrderingVersionedFilesDecorator(TestCaseWithMemoryTransport):
+
+ def get_ordering_vf(self, key_priority):
+ builder = self.make_branch_builder('test')
+ builder.start_series()
+ builder.build_snapshot('A', None, [
+ ('add', ('', 'TREE_ROOT', 'directory', None))])
+ builder.build_snapshot('B', ['A'], [])
+ builder.build_snapshot('C', ['B'], [])
+ builder.build_snapshot('D', ['C'], [])
+ builder.finish_series()
+ b = builder.get_branch()
+ b.lock_read()
+ self.addCleanup(b.unlock)
+ vf = b.repository.inventories
+ return versionedfile.OrderingVersionedFilesDecorator(vf, key_priority)
+
+ def test_get_empty(self):
+ vf = self.get_ordering_vf({})
+ self.assertEqual([], vf.calls)
+
+ def test_get_record_stream_topological(self):
+ vf = self.get_ordering_vf({('A',): 3, ('B',): 2, ('C',): 4, ('D',): 1})
+ request_keys = [('B',), ('C',), ('D',), ('A',)]
+ keys = [r.key for r in vf.get_record_stream(request_keys,
+ 'topological', False)]
+ # We should have gotten the keys in topological order
+ self.assertEqual([('A',), ('B',), ('C',), ('D',)], keys)
+ # And recorded that the request was made
+ self.assertEqual([('get_record_stream', request_keys, 'topological',
+ False)], vf.calls)
+
+ def test_get_record_stream_ordered(self):
+ vf = self.get_ordering_vf({('A',): 3, ('B',): 2, ('C',): 4, ('D',): 1})
+ request_keys = [('B',), ('C',), ('D',), ('A',)]
+ keys = [r.key for r in vf.get_record_stream(request_keys,
+ 'unordered', False)]
+ # They should be returned based on their priority
+ self.assertEqual([('D',), ('B',), ('A',), ('C',)], keys)
+ # And the request recorded
+ self.assertEqual([('get_record_stream', request_keys, 'unordered',
+ False)], vf.calls)
+
+ def test_get_record_stream_implicit_order(self):
+ vf = self.get_ordering_vf({('B',): 2, ('D',): 1})
+ request_keys = [('B',), ('C',), ('D',), ('A',)]
+ keys = [r.key for r in vf.get_record_stream(request_keys,
+ 'unordered', False)]
+ # A and C are not in the map, so they get sorted to the front. A comes
+ # before C alphabetically, so it comes back first
+ self.assertEqual([('A',), ('C',), ('D',), ('B',)], keys)
+ # And the request recorded
+ self.assertEqual([('get_record_stream', request_keys, 'unordered',
+ False)], vf.calls)