summaryrefslogtreecommitdiff
path: root/bzrlib/tests/test_remote.py
diff options
context:
space:
mode:
Diffstat (limited to 'bzrlib/tests/test_remote.py')
-rw-r--r--bzrlib/tests/test_remote.py4294
1 files changed, 4294 insertions, 0 deletions
diff --git a/bzrlib/tests/test_remote.py b/bzrlib/tests/test_remote.py
new file mode 100644
index 0000000..f9a162a
--- /dev/null
+++ b/bzrlib/tests/test_remote.py
@@ -0,0 +1,4294 @@
+# Copyright (C) 2006-2012 Canonical Ltd
+#
+# 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
+
+"""Tests for remote bzrdir/branch/repo/etc
+
+These are proxy objects which act on remote objects by sending messages
+through a smart client. The proxies are to be created when attempting to open
+the object given a transport that supports smartserver rpc operations.
+
+These tests correspond to tests.test_smart, which exercises the server side.
+"""
+
+import bz2
+from cStringIO import StringIO
+import zlib
+
+from bzrlib import (
+ bencode,
+ branch,
+ bzrdir,
+ config,
+ controldir,
+ errors,
+ inventory,
+ inventory_delta,
+ remote,
+ repository,
+ tests,
+ transport,
+ treebuilder,
+ versionedfile,
+ vf_search,
+ )
+from bzrlib.branch import Branch
+from bzrlib.bzrdir import (
+ BzrDir,
+ BzrDirFormat,
+ RemoteBzrProber,
+ )
+from bzrlib.chk_serializer import chk_bencode_serializer
+from bzrlib.remote import (
+ RemoteBranch,
+ RemoteBranchFormat,
+ RemoteBzrDir,
+ RemoteBzrDirFormat,
+ RemoteRepository,
+ RemoteRepositoryFormat,
+ )
+from bzrlib.repofmt import groupcompress_repo, knitpack_repo
+from bzrlib.revision import (
+ NULL_REVISION,
+ Revision,
+ )
+from bzrlib.smart import medium, request
+from bzrlib.smart.client import _SmartClient
+from bzrlib.smart.repository import (
+ SmartServerRepositoryGetParentMap,
+ SmartServerRepositoryGetStream_1_19,
+ _stream_to_byte_stream,
+ )
+from bzrlib.symbol_versioning import deprecated_in
+from bzrlib.tests import (
+ test_server,
+ )
+from bzrlib.tests.scenarios import load_tests_apply_scenarios
+from bzrlib.transport.memory import MemoryTransport
+from bzrlib.transport.remote import (
+ RemoteTransport,
+ RemoteSSHTransport,
+ RemoteTCPTransport,
+ )
+
+
+load_tests = load_tests_apply_scenarios
+
+
+class BasicRemoteObjectTests(tests.TestCaseWithTransport):
+
+ scenarios = [
+ ('HPSS-v2',
+ {'transport_server': test_server.SmartTCPServer_for_testing_v2_only}),
+ ('HPSS-v3',
+ {'transport_server': test_server.SmartTCPServer_for_testing})]
+
+
+ def setUp(self):
+ super(BasicRemoteObjectTests, self).setUp()
+ self.transport = self.get_transport()
+ # make a branch that can be opened over the smart transport
+ self.local_wt = BzrDir.create_standalone_workingtree('.')
+ self.addCleanup(self.transport.disconnect)
+
+ def test_create_remote_bzrdir(self):
+ b = remote.RemoteBzrDir(self.transport, RemoteBzrDirFormat())
+ self.assertIsInstance(b, BzrDir)
+
+ def test_open_remote_branch(self):
+ # open a standalone branch in the working directory
+ b = remote.RemoteBzrDir(self.transport, RemoteBzrDirFormat())
+ branch = b.open_branch()
+ self.assertIsInstance(branch, Branch)
+
+ def test_remote_repository(self):
+ b = BzrDir.open_from_transport(self.transport)
+ repo = b.open_repository()
+ revid = u'\xc823123123'.encode('utf8')
+ self.assertFalse(repo.has_revision(revid))
+ self.local_wt.commit(message='test commit', rev_id=revid)
+ self.assertTrue(repo.has_revision(revid))
+
+ def test_find_correct_format(self):
+ """Should open a RemoteBzrDir over a RemoteTransport"""
+ fmt = BzrDirFormat.find_format(self.transport)
+ self.assertTrue(bzrdir.RemoteBzrProber
+ in controldir.ControlDirFormat._server_probers)
+ self.assertIsInstance(fmt, RemoteBzrDirFormat)
+
+ def test_open_detected_smart_format(self):
+ fmt = BzrDirFormat.find_format(self.transport)
+ d = fmt.open(self.transport)
+ self.assertIsInstance(d, BzrDir)
+
+ def test_remote_branch_repr(self):
+ b = BzrDir.open_from_transport(self.transport).open_branch()
+ self.assertStartsWith(str(b), 'RemoteBranch(')
+
+ def test_remote_bzrdir_repr(self):
+ b = BzrDir.open_from_transport(self.transport)
+ self.assertStartsWith(str(b), 'RemoteBzrDir(')
+
+ def test_remote_branch_format_supports_stacking(self):
+ t = self.transport
+ self.make_branch('unstackable', format='pack-0.92')
+ b = BzrDir.open_from_transport(t.clone('unstackable')).open_branch()
+ self.assertFalse(b._format.supports_stacking())
+ self.make_branch('stackable', format='1.9')
+ b = BzrDir.open_from_transport(t.clone('stackable')).open_branch()
+ self.assertTrue(b._format.supports_stacking())
+
+ def test_remote_repo_format_supports_external_references(self):
+ t = self.transport
+ bd = self.make_bzrdir('unstackable', format='pack-0.92')
+ r = bd.create_repository()
+ self.assertFalse(r._format.supports_external_lookups)
+ r = BzrDir.open_from_transport(t.clone('unstackable')).open_repository()
+ self.assertFalse(r._format.supports_external_lookups)
+ bd = self.make_bzrdir('stackable', format='1.9')
+ r = bd.create_repository()
+ self.assertTrue(r._format.supports_external_lookups)
+ r = BzrDir.open_from_transport(t.clone('stackable')).open_repository()
+ self.assertTrue(r._format.supports_external_lookups)
+
+ def test_remote_branch_set_append_revisions_only(self):
+ # Make a format 1.9 branch, which supports append_revisions_only
+ branch = self.make_branch('branch', format='1.9')
+ branch.set_append_revisions_only(True)
+ config = branch.get_config_stack()
+ self.assertEqual(
+ True, config.get('append_revisions_only'))
+ branch.set_append_revisions_only(False)
+ config = branch.get_config_stack()
+ self.assertEqual(
+ False, config.get('append_revisions_only'))
+
+ def test_remote_branch_set_append_revisions_only_upgrade_reqd(self):
+ branch = self.make_branch('branch', format='knit')
+ self.assertRaises(
+ errors.UpgradeRequired, branch.set_append_revisions_only, True)
+
+
+class FakeProtocol(object):
+ """Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
+
+ def __init__(self, body, fake_client):
+ self.body = body
+ self._body_buffer = None
+ self._fake_client = fake_client
+
+ def read_body_bytes(self, count=-1):
+ if self._body_buffer is None:
+ self._body_buffer = StringIO(self.body)
+ bytes = self._body_buffer.read(count)
+ if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
+ self._fake_client.expecting_body = False
+ return bytes
+
+ def cancel_read_body(self):
+ self._fake_client.expecting_body = False
+
+ def read_streamed_body(self):
+ return self.body
+
+
+class FakeClient(_SmartClient):
+ """Lookalike for _SmartClient allowing testing."""
+
+ def __init__(self, fake_medium_base='fake base'):
+ """Create a FakeClient."""
+ self.responses = []
+ self._calls = []
+ self.expecting_body = False
+ # if non-None, this is the list of expected calls, with only the
+ # method name and arguments included. the body might be hard to
+ # compute so is not included. If a call is None, that call can
+ # be anything.
+ self._expected_calls = None
+ _SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
+
+ def add_expected_call(self, call_name, call_args, response_type,
+ response_args, response_body=None):
+ if self._expected_calls is None:
+ self._expected_calls = []
+ self._expected_calls.append((call_name, call_args))
+ self.responses.append((response_type, response_args, response_body))
+
+ def add_success_response(self, *args):
+ self.responses.append(('success', args, None))
+
+ def add_success_response_with_body(self, body, *args):
+ self.responses.append(('success', args, body))
+ if self._expected_calls is not None:
+ self._expected_calls.append(None)
+
+ def add_error_response(self, *args):
+ self.responses.append(('error', args))
+
+ def add_unknown_method_response(self, verb):
+ self.responses.append(('unknown', verb))
+
+ def finished_test(self):
+ if self._expected_calls:
+ raise AssertionError("%r finished but was still expecting %r"
+ % (self, self._expected_calls[0]))
+
+ def _get_next_response(self):
+ try:
+ response_tuple = self.responses.pop(0)
+ except IndexError, e:
+ raise AssertionError("%r didn't expect any more calls"
+ % (self,))
+ if response_tuple[0] == 'unknown':
+ raise errors.UnknownSmartMethod(response_tuple[1])
+ elif response_tuple[0] == 'error':
+ raise errors.ErrorFromSmartServer(response_tuple[1])
+ return response_tuple
+
+ def _check_call(self, method, args):
+ if self._expected_calls is None:
+ # the test should be updated to say what it expects
+ return
+ try:
+ next_call = self._expected_calls.pop(0)
+ except IndexError:
+ raise AssertionError("%r didn't expect any more calls "
+ "but got %r%r"
+ % (self, method, args,))
+ if next_call is None:
+ return
+ if method != next_call[0] or args != next_call[1]:
+ raise AssertionError("%r expected %r%r "
+ "but got %r%r"
+ % (self, next_call[0], next_call[1], method, args,))
+
+ def call(self, method, *args):
+ self._check_call(method, args)
+ self._calls.append(('call', method, args))
+ return self._get_next_response()[1]
+
+ def call_expecting_body(self, method, *args):
+ self._check_call(method, args)
+ self._calls.append(('call_expecting_body', method, args))
+ result = self._get_next_response()
+ self.expecting_body = True
+ return result[1], FakeProtocol(result[2], self)
+
+ def call_with_body_bytes(self, method, args, body):
+ self._check_call(method, args)
+ self._calls.append(('call_with_body_bytes', method, args, body))
+ result = self._get_next_response()
+ return result[1], FakeProtocol(result[2], self)
+
+ def call_with_body_bytes_expecting_body(self, method, args, body):
+ self._check_call(method, args)
+ self._calls.append(('call_with_body_bytes_expecting_body', method,
+ args, body))
+ result = self._get_next_response()
+ self.expecting_body = True
+ return result[1], FakeProtocol(result[2], self)
+
+ def call_with_body_stream(self, args, stream):
+ # Explicitly consume the stream before checking for an error, because
+ # that's what happens a real medium.
+ stream = list(stream)
+ self._check_call(args[0], args[1:])
+ self._calls.append(('call_with_body_stream', args[0], args[1:], stream))
+ result = self._get_next_response()
+ # The second value returned from call_with_body_stream is supposed to
+ # be a response_handler object, but so far no tests depend on that.
+ response_handler = None
+ return result[1], response_handler
+
+
+class FakeMedium(medium.SmartClientMedium):
+
+ def __init__(self, client_calls, base):
+ medium.SmartClientMedium.__init__(self, base)
+ self._client_calls = client_calls
+
+ def disconnect(self):
+ self._client_calls.append(('disconnect medium',))
+
+
+class TestVfsHas(tests.TestCase):
+
+ def test_unicode_path(self):
+ client = FakeClient('/')
+ client.add_success_response('yes',)
+ transport = RemoteTransport('bzr://localhost/', _client=client)
+ filename = u'/hell\u00d8'.encode('utf8')
+ result = transport.has(filename)
+ self.assertEqual(
+ [('call', 'has', (filename,))],
+ client._calls)
+ self.assertTrue(result)
+
+
+class TestRemote(tests.TestCaseWithMemoryTransport):
+
+ def get_branch_format(self):
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ return reference_bzrdir_format.get_branch_format()
+
+ def get_repo_format(self):
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ return reference_bzrdir_format.repository_format
+
+ def assertFinished(self, fake_client):
+ """Assert that all of a FakeClient's expected calls have occurred."""
+ fake_client.finished_test()
+
+
+class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
+ """Tests for the behaviour of client_medium.remote_path_from_transport."""
+
+ def assertRemotePath(self, expected, client_base, transport_base):
+ """Assert that the result of
+ SmartClientMedium.remote_path_from_transport is the expected value for
+ a given client_base and transport_base.
+ """
+ client_medium = medium.SmartClientMedium(client_base)
+ t = transport.get_transport(transport_base)
+ result = client_medium.remote_path_from_transport(t)
+ self.assertEqual(expected, result)
+
+ def test_remote_path_from_transport(self):
+ """SmartClientMedium.remote_path_from_transport calculates a URL for
+ the given transport relative to the root of the client base URL.
+ """
+ self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
+ self.assertRemotePath(
+ 'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
+
+ def assertRemotePathHTTP(self, expected, transport_base, relpath):
+ """Assert that the result of
+ HttpTransportBase.remote_path_from_transport is the expected value for
+ a given transport_base and relpath of that transport. (Note that
+ HttpTransportBase is a subclass of SmartClientMedium)
+ """
+ base_transport = transport.get_transport(transport_base)
+ client_medium = base_transport.get_smart_medium()
+ cloned_transport = base_transport.clone(relpath)
+ result = client_medium.remote_path_from_transport(cloned_transport)
+ self.assertEqual(expected, result)
+
+ def test_remote_path_from_transport_http(self):
+ """Remote paths for HTTP transports are calculated differently to other
+ transports. They are just relative to the client base, not the root
+ directory of the host.
+ """
+ for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
+ self.assertRemotePathHTTP(
+ '../xyz/', scheme + '//host/path', '../xyz/')
+ self.assertRemotePathHTTP(
+ 'xyz/', scheme + '//host/path', 'xyz/')
+
+
+class Test_ClientMedium_remote_is_at_least(tests.TestCase):
+ """Tests for the behaviour of client_medium.remote_is_at_least."""
+
+ def test_initially_unlimited(self):
+ """A fresh medium assumes that the remote side supports all
+ versions.
+ """
+ client_medium = medium.SmartClientMedium('dummy base')
+ self.assertFalse(client_medium._is_remote_before((99, 99)))
+
+ def test__remember_remote_is_before(self):
+ """Calling _remember_remote_is_before ratchets down the known remote
+ version.
+ """
+ client_medium = medium.SmartClientMedium('dummy base')
+ # Mark the remote side as being less than 1.6. The remote side may
+ # still be 1.5.
+ client_medium._remember_remote_is_before((1, 6))
+ self.assertTrue(client_medium._is_remote_before((1, 6)))
+ self.assertFalse(client_medium._is_remote_before((1, 5)))
+ # Calling _remember_remote_is_before again with a lower value works.
+ client_medium._remember_remote_is_before((1, 5))
+ self.assertTrue(client_medium._is_remote_before((1, 5)))
+ # If you call _remember_remote_is_before with a higher value it logs a
+ # warning, and continues to remember the lower value.
+ self.assertNotContainsRe(self.get_log(), '_remember_remote_is_before')
+ client_medium._remember_remote_is_before((1, 9))
+ self.assertContainsRe(self.get_log(), '_remember_remote_is_before')
+ self.assertTrue(client_medium._is_remote_before((1, 5)))
+
+
+class TestBzrDirCloningMetaDir(TestRemote):
+
+ def test_backwards_compat(self):
+ self.setup_smart_server_with_call_log()
+ a_dir = self.make_bzrdir('.')
+ self.reset_smart_call_log()
+ verb = 'BzrDir.cloning_metadir'
+ self.disable_verb(verb)
+ format = a_dir.cloning_metadir()
+ call_count = len([call for call in self.hpss_calls if
+ call.call.method == verb])
+ self.assertEqual(1, call_count)
+
+ def test_branch_reference(self):
+ transport = self.get_transport('quack')
+ referenced = self.make_branch('referenced')
+ expected = referenced.bzrdir.cloning_metadir()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.cloning_metadir', ('quack/', 'False'),
+ 'error', ('BranchReference',)),
+ client.add_expected_call(
+ 'BzrDir.open_branchV3', ('quack/',),
+ 'success', ('ref', self.get_url('referenced'))),
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ result = a_bzrdir.cloning_metadir()
+ # We should have got a control dir matching the referenced branch.
+ self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
+ self.assertEqual(expected._repository_format, result._repository_format)
+ self.assertEqual(expected._branch_format, result._branch_format)
+ self.assertFinished(client)
+
+ def test_current_server(self):
+ transport = self.get_transport('.')
+ transport = transport.clone('quack')
+ self.make_bzrdir('quack')
+ client = FakeClient(transport.base)
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ control_name = reference_bzrdir_format.network_name()
+ client.add_expected_call(
+ 'BzrDir.cloning_metadir', ('quack/', 'False'),
+ 'success', (control_name, '', ('branch', ''))),
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ result = a_bzrdir.cloning_metadir()
+ # We should have got a reference control dir with default branch and
+ # repository formats.
+ # This pokes a little, just to be sure.
+ self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
+ self.assertEqual(None, result._repository_format)
+ self.assertEqual(None, result._branch_format)
+ self.assertFinished(client)
+
+ def test_unknown(self):
+ transport = self.get_transport('quack')
+ referenced = self.make_branch('referenced')
+ expected = referenced.bzrdir.cloning_metadir()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.cloning_metadir', ('quack/', 'False'),
+ 'success', ('unknown', 'unknown', ('branch', ''))),
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ self.assertRaises(errors.UnknownFormatError, a_bzrdir.cloning_metadir)
+
+
+class TestBzrDirCheckoutMetaDir(TestRemote):
+
+ def test__get_checkout_format(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ control_name = reference_bzrdir_format.network_name()
+ client.add_expected_call(
+ 'BzrDir.checkout_metadir', ('quack/', ),
+ 'success', (control_name, '', ''))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ result = a_bzrdir.checkout_metadir()
+ # We should have got a reference control dir with default branch and
+ # repository formats.
+ self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
+ self.assertEqual(None, result._repository_format)
+ self.assertEqual(None, result._branch_format)
+ self.assertFinished(client)
+
+ def test_unknown_format(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.checkout_metadir', ('quack/',),
+ 'success', ('dontknow', '', ''))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ self.assertRaises(errors.UnknownFormatError,
+ a_bzrdir.checkout_metadir)
+ self.assertFinished(client)
+
+
+class TestBzrDirGetBranches(TestRemote):
+
+ def test_get_branches(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ branch_name = reference_bzrdir_format.get_branch_format().network_name()
+ client.add_success_response_with_body(
+ bencode.bencode({
+ "foo": ("branch", branch_name),
+ "": ("branch", branch_name)}), "success")
+ client.add_success_response(
+ 'ok', '', 'no', 'no', 'no',
+ reference_bzrdir_format.repository_format.network_name())
+ client.add_error_response('NotStacked')
+ client.add_success_response(
+ 'ok', '', 'no', 'no', 'no',
+ reference_bzrdir_format.repository_format.network_name())
+ client.add_error_response('NotStacked')
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ result = a_bzrdir.get_branches()
+ self.assertEquals(set(["", "foo"]), set(result.keys()))
+ self.assertEqual(
+ [('call_expecting_body', 'BzrDir.get_branches', ('quack/',)),
+ ('call', 'BzrDir.find_repositoryV3', ('quack/', )),
+ ('call', 'Branch.get_stacked_on_url', ('quack/', )),
+ ('call', 'BzrDir.find_repositoryV3', ('quack/', )),
+ ('call', 'Branch.get_stacked_on_url', ('quack/', ))],
+ client._calls)
+
+
+class TestBzrDirDestroyBranch(TestRemote):
+
+ def test_destroy_default(self):
+ transport = self.get_transport('quack')
+ referenced = self.make_branch('referenced')
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.destroy_branch', ('quack/', ),
+ 'success', ('ok',)),
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ a_bzrdir.destroy_branch()
+ self.assertFinished(client)
+
+
+class TestBzrDirHasWorkingTree(TestRemote):
+
+ def test_has_workingtree(self):
+ transport = self.get_transport('quack')
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.has_workingtree', ('quack/',),
+ 'success', ('yes',)),
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ self.assertTrue(a_bzrdir.has_workingtree())
+ self.assertFinished(client)
+
+ def test_no_workingtree(self):
+ transport = self.get_transport('quack')
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.has_workingtree', ('quack/',),
+ 'success', ('no',)),
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ self.assertFalse(a_bzrdir.has_workingtree())
+ self.assertFinished(client)
+
+
+class TestBzrDirDestroyRepository(TestRemote):
+
+ def test_destroy_repository(self):
+ transport = self.get_transport('quack')
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.destroy_repository', ('quack/',),
+ 'success', ('ok',)),
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ a_bzrdir.destroy_repository()
+ self.assertFinished(client)
+
+
+class TestBzrDirOpen(TestRemote):
+
+ def make_fake_client_and_transport(self, path='quack'):
+ transport = MemoryTransport()
+ transport.mkdir(path)
+ transport = transport.clone(path)
+ client = FakeClient(transport.base)
+ return client, transport
+
+ def test_absent(self):
+ client, transport = self.make_fake_client_and_transport()
+ client.add_expected_call(
+ 'BzrDir.open_2.1', ('quack/',), 'success', ('no',))
+ self.assertRaises(errors.NotBranchError, RemoteBzrDir, transport,
+ RemoteBzrDirFormat(), _client=client, _force_probe=True)
+ self.assertFinished(client)
+
+ def test_present_without_workingtree(self):
+ client, transport = self.make_fake_client_and_transport()
+ client.add_expected_call(
+ 'BzrDir.open_2.1', ('quack/',), 'success', ('yes', 'no'))
+ bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client, _force_probe=True)
+ self.assertIsInstance(bd, RemoteBzrDir)
+ self.assertFalse(bd.has_workingtree())
+ self.assertRaises(errors.NoWorkingTree, bd.open_workingtree)
+ self.assertFinished(client)
+
+ def test_present_with_workingtree(self):
+ client, transport = self.make_fake_client_and_transport()
+ client.add_expected_call(
+ 'BzrDir.open_2.1', ('quack/',), 'success', ('yes', 'yes'))
+ bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client, _force_probe=True)
+ self.assertIsInstance(bd, RemoteBzrDir)
+ self.assertTrue(bd.has_workingtree())
+ self.assertRaises(errors.NotLocalUrl, bd.open_workingtree)
+ self.assertFinished(client)
+
+ def test_backwards_compat(self):
+ client, transport = self.make_fake_client_and_transport()
+ client.add_expected_call(
+ 'BzrDir.open_2.1', ('quack/',), 'unknown', ('BzrDir.open_2.1',))
+ client.add_expected_call(
+ 'BzrDir.open', ('quack/',), 'success', ('yes',))
+ bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client, _force_probe=True)
+ self.assertIsInstance(bd, RemoteBzrDir)
+ self.assertFinished(client)
+
+ def test_backwards_compat_hpss_v2(self):
+ client, transport = self.make_fake_client_and_transport()
+ # Monkey-patch fake client to simulate real-world behaviour with v2
+ # server: upon first RPC call detect the protocol version, and because
+ # the version is 2 also do _remember_remote_is_before((1, 6)) before
+ # continuing with the RPC.
+ orig_check_call = client._check_call
+ def check_call(method, args):
+ client._medium._protocol_version = 2
+ client._medium._remember_remote_is_before((1, 6))
+ client._check_call = orig_check_call
+ client._check_call(method, args)
+ client._check_call = check_call
+ client.add_expected_call(
+ 'BzrDir.open_2.1', ('quack/',), 'unknown', ('BzrDir.open_2.1',))
+ client.add_expected_call(
+ 'BzrDir.open', ('quack/',), 'success', ('yes',))
+ bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client, _force_probe=True)
+ self.assertIsInstance(bd, RemoteBzrDir)
+ self.assertFinished(client)
+
+
+class TestBzrDirOpenBranch(TestRemote):
+
+ def test_backwards_compat(self):
+ self.setup_smart_server_with_call_log()
+ self.make_branch('.')
+ a_dir = BzrDir.open(self.get_url('.'))
+ self.reset_smart_call_log()
+ verb = 'BzrDir.open_branchV3'
+ self.disable_verb(verb)
+ format = a_dir.open_branch()
+ call_count = len([call for call in self.hpss_calls if
+ call.call.method == verb])
+ self.assertEqual(1, call_count)
+
+ def test_branch_present(self):
+ reference_format = self.get_repo_format()
+ network_name = reference_format.network_name()
+ branch_network_name = self.get_branch_format().network_name()
+ transport = MemoryTransport()
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDir.open_branchV3', ('quack/',),
+ 'success', ('branch', branch_network_name))
+ client.add_expected_call(
+ 'BzrDir.find_repositoryV3', ('quack/',),
+ 'success', ('ok', '', 'no', 'no', 'no', network_name))
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ result = bzrdir.open_branch()
+ self.assertIsInstance(result, RemoteBranch)
+ self.assertEqual(bzrdir, result.bzrdir)
+ self.assertFinished(client)
+
+ def test_branch_missing(self):
+ transport = MemoryTransport()
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ client = FakeClient(transport.base)
+ client.add_error_response('nobranch')
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
+ self.assertEqual(
+ [('call', 'BzrDir.open_branchV3', ('quack/',))],
+ client._calls)
+
+ def test__get_tree_branch(self):
+ # _get_tree_branch is a form of open_branch, but it should only ask for
+ # branch opening, not any other network requests.
+ calls = []
+ def open_branch(name=None, possible_transports=None):
+ calls.append("Called")
+ return "a-branch"
+ transport = MemoryTransport()
+ # no requests on the network - catches other api calls being made.
+ client = FakeClient(transport.base)
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ # patch the open_branch call to record that it was called.
+ bzrdir.open_branch = open_branch
+ self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
+ self.assertEqual(["Called"], calls)
+ self.assertEqual([], client._calls)
+
+ def test_url_quoting_of_path(self):
+ # Relpaths on the wire should not be URL-escaped. So "~" should be
+ # transmitted as "~", not "%7E".
+ transport = RemoteTCPTransport('bzr://localhost/~hello/')
+ client = FakeClient(transport.base)
+ reference_format = self.get_repo_format()
+ network_name = reference_format.network_name()
+ branch_network_name = self.get_branch_format().network_name()
+ client.add_expected_call(
+ 'BzrDir.open_branchV3', ('~hello/',),
+ 'success', ('branch', branch_network_name))
+ client.add_expected_call(
+ 'BzrDir.find_repositoryV3', ('~hello/',),
+ 'success', ('ok', '', 'no', 'no', 'no', network_name))
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('~hello/',),
+ 'error', ('NotStacked',))
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ result = bzrdir.open_branch()
+ self.assertFinished(client)
+
+ def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
+ reference_format = self.get_repo_format()
+ network_name = reference_format.network_name()
+ transport = MemoryTransport()
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ if rich_root:
+ rich_response = 'yes'
+ else:
+ rich_response = 'no'
+ if subtrees:
+ subtree_response = 'yes'
+ else:
+ subtree_response = 'no'
+ client = FakeClient(transport.base)
+ client.add_success_response(
+ 'ok', '', rich_response, subtree_response, external_lookup,
+ network_name)
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ result = bzrdir.open_repository()
+ self.assertEqual(
+ [('call', 'BzrDir.find_repositoryV3', ('quack/',))],
+ client._calls)
+ self.assertIsInstance(result, RemoteRepository)
+ self.assertEqual(bzrdir, result.bzrdir)
+ self.assertEqual(rich_root, result._format.rich_root_data)
+ self.assertEqual(subtrees, result._format.supports_tree_reference)
+
+ def test_open_repository_sets_format_attributes(self):
+ self.check_open_repository(True, True)
+ self.check_open_repository(False, True)
+ self.check_open_repository(True, False)
+ self.check_open_repository(False, False)
+ self.check_open_repository(False, False, 'yes')
+
+ def test_old_server(self):
+ """RemoteBzrDirFormat should fail to probe if the server version is too
+ old.
+ """
+ self.assertRaises(errors.NotBranchError,
+ RemoteBzrProber.probe_transport, OldServerTransport())
+
+
+class TestBzrDirCreateBranch(TestRemote):
+
+ def test_backwards_compat(self):
+ self.setup_smart_server_with_call_log()
+ repo = self.make_repository('.')
+ self.reset_smart_call_log()
+ self.disable_verb('BzrDir.create_branch')
+ branch = repo.bzrdir.create_branch()
+ create_branch_call_count = len([call for call in self.hpss_calls if
+ call.call.method == 'BzrDir.create_branch'])
+ self.assertEqual(1, create_branch_call_count)
+
+ def test_current_server(self):
+ transport = self.get_transport('.')
+ transport = transport.clone('quack')
+ self.make_repository('quack')
+ client = FakeClient(transport.base)
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ reference_format = reference_bzrdir_format.get_branch_format()
+ network_name = reference_format.network_name()
+ reference_repo_fmt = reference_bzrdir_format.repository_format
+ reference_repo_name = reference_repo_fmt.network_name()
+ client.add_expected_call(
+ 'BzrDir.create_branch', ('quack/', network_name),
+ 'success', ('ok', network_name, '', 'no', 'no', 'yes',
+ reference_repo_name))
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ branch = a_bzrdir.create_branch()
+ # We should have got a remote branch
+ self.assertIsInstance(branch, remote.RemoteBranch)
+ # its format should have the settings from the response
+ format = branch._format
+ self.assertEqual(network_name, format.network_name())
+
+ def test_already_open_repo_and_reused_medium(self):
+ """Bug 726584: create_branch(..., repository=repo) should work
+ regardless of what the smart medium's base URL is.
+ """
+ self.transport_server = test_server.SmartTCPServer_for_testing
+ transport = self.get_transport('.')
+ repo = self.make_repository('quack')
+ # Client's medium rooted a transport root (not at the bzrdir)
+ client = FakeClient(transport.base)
+ transport = transport.clone('quack')
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ reference_format = reference_bzrdir_format.get_branch_format()
+ network_name = reference_format.network_name()
+ reference_repo_fmt = reference_bzrdir_format.repository_format
+ reference_repo_name = reference_repo_fmt.network_name()
+ client.add_expected_call(
+ 'BzrDir.create_branch', ('extra/quack/', network_name),
+ 'success', ('ok', network_name, '', 'no', 'no', 'yes',
+ reference_repo_name))
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ branch = a_bzrdir.create_branch(repository=repo)
+ # We should have got a remote branch
+ self.assertIsInstance(branch, remote.RemoteBranch)
+ # its format should have the settings from the response
+ format = branch._format
+ self.assertEqual(network_name, format.network_name())
+
+
+class TestBzrDirCreateRepository(TestRemote):
+
+ def test_backwards_compat(self):
+ self.setup_smart_server_with_call_log()
+ bzrdir = self.make_bzrdir('.')
+ self.reset_smart_call_log()
+ self.disable_verb('BzrDir.create_repository')
+ repo = bzrdir.create_repository()
+ create_repo_call_count = len([call for call in self.hpss_calls if
+ call.call.method == 'BzrDir.create_repository'])
+ self.assertEqual(1, create_repo_call_count)
+
+ def test_current_server(self):
+ transport = self.get_transport('.')
+ transport = transport.clone('quack')
+ self.make_bzrdir('quack')
+ client = FakeClient(transport.base)
+ reference_bzrdir_format = controldir.format_registry.get('default')()
+ reference_format = reference_bzrdir_format.repository_format
+ network_name = reference_format.network_name()
+ client.add_expected_call(
+ 'BzrDir.create_repository', ('quack/',
+ 'Bazaar repository format 2a (needs bzr 1.16 or later)\n',
+ 'False'),
+ 'success', ('ok', 'yes', 'yes', 'yes', network_name))
+ a_bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ repo = a_bzrdir.create_repository()
+ # We should have got a remote repository
+ self.assertIsInstance(repo, remote.RemoteRepository)
+ # its format should have the settings from the response
+ format = repo._format
+ self.assertTrue(format.rich_root_data)
+ self.assertTrue(format.supports_tree_reference)
+ self.assertTrue(format.supports_external_lookups)
+ self.assertEqual(network_name, format.network_name())
+
+
+class TestBzrDirOpenRepository(TestRemote):
+
+ def test_backwards_compat_1_2_3(self):
+ # fallback all the way to the first version.
+ reference_format = self.get_repo_format()
+ network_name = reference_format.network_name()
+ server_url = 'bzr://example.com/'
+ self.permit_url(server_url)
+ client = FakeClient(server_url)
+ client.add_unknown_method_response('BzrDir.find_repositoryV3')
+ client.add_unknown_method_response('BzrDir.find_repositoryV2')
+ client.add_success_response('ok', '', 'no', 'no')
+ # A real repository instance will be created to determine the network
+ # name.
+ client.add_success_response_with_body(
+ "Bazaar-NG meta directory, format 1\n", 'ok')
+ client.add_success_response('stat', '0', '65535')
+ client.add_success_response_with_body(
+ reference_format.get_format_string(), 'ok')
+ # PackRepository wants to do a stat
+ client.add_success_response('stat', '0', '65535')
+ remote_transport = RemoteTransport(server_url + 'quack/', medium=False,
+ _client=client)
+ bzrdir = RemoteBzrDir(remote_transport, RemoteBzrDirFormat(),
+ _client=client)
+ repo = bzrdir.open_repository()
+ self.assertEqual(
+ [('call', 'BzrDir.find_repositoryV3', ('quack/',)),
+ ('call', 'BzrDir.find_repositoryV2', ('quack/',)),
+ ('call', 'BzrDir.find_repository', ('quack/',)),
+ ('call_expecting_body', 'get', ('/quack/.bzr/branch-format',)),
+ ('call', 'stat', ('/quack/.bzr',)),
+ ('call_expecting_body', 'get', ('/quack/.bzr/repository/format',)),
+ ('call', 'stat', ('/quack/.bzr/repository',)),
+ ],
+ client._calls)
+ self.assertEqual(network_name, repo._format.network_name())
+
+ def test_backwards_compat_2(self):
+ # fallback to find_repositoryV2
+ reference_format = self.get_repo_format()
+ network_name = reference_format.network_name()
+ server_url = 'bzr://example.com/'
+ self.permit_url(server_url)
+ client = FakeClient(server_url)
+ client.add_unknown_method_response('BzrDir.find_repositoryV3')
+ client.add_success_response('ok', '', 'no', 'no', 'no')
+ # A real repository instance will be created to determine the network
+ # name.
+ client.add_success_response_with_body(
+ "Bazaar-NG meta directory, format 1\n", 'ok')
+ client.add_success_response('stat', '0', '65535')
+ client.add_success_response_with_body(
+ reference_format.get_format_string(), 'ok')
+ # PackRepository wants to do a stat
+ client.add_success_response('stat', '0', '65535')
+ remote_transport = RemoteTransport(server_url + 'quack/', medium=False,
+ _client=client)
+ bzrdir = RemoteBzrDir(remote_transport, RemoteBzrDirFormat(),
+ _client=client)
+ repo = bzrdir.open_repository()
+ self.assertEqual(
+ [('call', 'BzrDir.find_repositoryV3', ('quack/',)),
+ ('call', 'BzrDir.find_repositoryV2', ('quack/',)),
+ ('call_expecting_body', 'get', ('/quack/.bzr/branch-format',)),
+ ('call', 'stat', ('/quack/.bzr',)),
+ ('call_expecting_body', 'get', ('/quack/.bzr/repository/format',)),
+ ('call', 'stat', ('/quack/.bzr/repository',)),
+ ],
+ client._calls)
+ self.assertEqual(network_name, repo._format.network_name())
+
+ def test_current_server(self):
+ reference_format = self.get_repo_format()
+ network_name = reference_format.network_name()
+ transport = MemoryTransport()
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ client = FakeClient(transport.base)
+ client.add_success_response('ok', '', 'no', 'no', 'no', network_name)
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ repo = bzrdir.open_repository()
+ self.assertEqual(
+ [('call', 'BzrDir.find_repositoryV3', ('quack/',))],
+ client._calls)
+ self.assertEqual(network_name, repo._format.network_name())
+
+
+class TestBzrDirFormatInitializeEx(TestRemote):
+
+ def test_success(self):
+ """Simple test for typical successful call."""
+ fmt = RemoteBzrDirFormat()
+ default_format_name = BzrDirFormat.get_default_format().network_name()
+ transport = self.get_transport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDirFormat.initialize_ex_1.16',
+ (default_format_name, 'path', 'False', 'False', 'False', '',
+ '', '', '', 'False'),
+ 'success',
+ ('.', 'no', 'no', 'yes', 'repo fmt', 'repo bzrdir fmt',
+ 'bzrdir fmt', 'False', '', '', 'repo lock token'))
+ # XXX: It would be better to call fmt.initialize_on_transport_ex, but
+ # it's currently hard to test that without supplying a real remote
+ # transport connected to a real server.
+ result = fmt._initialize_on_transport_ex_rpc(client, 'path',
+ transport, False, False, False, None, None, None, None, False)
+ self.assertFinished(client)
+
+ def test_error(self):
+ """Error responses are translated, e.g. 'PermissionDenied' raises the
+ corresponding error from the client.
+ """
+ fmt = RemoteBzrDirFormat()
+ default_format_name = BzrDirFormat.get_default_format().network_name()
+ transport = self.get_transport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'BzrDirFormat.initialize_ex_1.16',
+ (default_format_name, 'path', 'False', 'False', 'False', '',
+ '', '', '', 'False'),
+ 'error',
+ ('PermissionDenied', 'path', 'extra info'))
+ # XXX: It would be better to call fmt.initialize_on_transport_ex, but
+ # it's currently hard to test that without supplying a real remote
+ # transport connected to a real server.
+ err = self.assertRaises(errors.PermissionDenied,
+ fmt._initialize_on_transport_ex_rpc, client, 'path', transport,
+ False, False, False, None, None, None, None, False)
+ self.assertEqual('path', err.path)
+ self.assertEqual(': extra info', err.extra)
+ self.assertFinished(client)
+
+ def test_error_from_real_server(self):
+ """Integration test for error translation."""
+ transport = self.make_smart_server('foo')
+ transport = transport.clone('no-such-path')
+ fmt = RemoteBzrDirFormat()
+ err = self.assertRaises(errors.NoSuchFile,
+ fmt.initialize_on_transport_ex, transport, create_prefix=False)
+
+
+class OldSmartClient(object):
+ """A fake smart client for test_old_version that just returns a version one
+ response to the 'hello' (query version) command.
+ """
+
+ def get_request(self):
+ input_file = StringIO('ok\x011\n')
+ output_file = StringIO()
+ client_medium = medium.SmartSimplePipesClientMedium(
+ input_file, output_file)
+ return medium.SmartClientStreamMediumRequest(client_medium)
+
+ def protocol_version(self):
+ return 1
+
+
+class OldServerTransport(object):
+ """A fake transport for test_old_server that reports it's smart server
+ protocol version as version one.
+ """
+
+ def __init__(self):
+ self.base = 'fake:'
+
+ def get_smart_client(self):
+ return OldSmartClient()
+
+
+class RemoteBzrDirTestCase(TestRemote):
+
+ def make_remote_bzrdir(self, transport, client):
+ """Make a RemotebzrDir using 'client' as the _client."""
+ return RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+
+
+class RemoteBranchTestCase(RemoteBzrDirTestCase):
+
+ def lock_remote_branch(self, branch):
+ """Trick a RemoteBranch into thinking it is locked."""
+ branch._lock_mode = 'w'
+ branch._lock_count = 2
+ branch._lock_token = 'branch token'
+ branch._repo_lock_token = 'repo token'
+ branch.repository._lock_mode = 'w'
+ branch.repository._lock_count = 2
+ branch.repository._lock_token = 'repo token'
+
+ def make_remote_branch(self, transport, client):
+ """Make a RemoteBranch using 'client' as its _SmartClient.
+
+ A RemoteBzrDir and RemoteRepository will also be created to fill out
+ the RemoteBranch, albeit with stub values for some of their attributes.
+ """
+ # we do not want bzrdir to make any remote calls, so use False as its
+ # _client. If it tries to make a remote call, this will fail
+ # immediately.
+ bzrdir = self.make_remote_bzrdir(transport, False)
+ repo = RemoteRepository(bzrdir, None, _client=client)
+ branch_format = self.get_branch_format()
+ format = RemoteBranchFormat(network_name=branch_format.network_name())
+ return RemoteBranch(bzrdir, repo, _client=client, format=format)
+
+
+class TestBranchBreakLock(RemoteBranchTestCase):
+
+ def test_break_lock(self):
+ transport_path = 'quack'
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.break_lock', ('quack/',),
+ 'success', ('ok',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ branch.break_lock()
+ self.assertFinished(client)
+
+
+class TestBranchGetPhysicalLockStatus(RemoteBranchTestCase):
+
+ def test_get_physical_lock_status_yes(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.get_physical_lock_status', ('quack/',),
+ 'success', ('yes',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.get_physical_lock_status()
+ self.assertFinished(client)
+ self.assertEqual(True, result)
+
+ def test_get_physical_lock_status_no(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.get_physical_lock_status', ('quack/',),
+ 'success', ('no',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.get_physical_lock_status()
+ self.assertFinished(client)
+ self.assertEqual(False, result)
+
+
+class TestBranchGetParent(RemoteBranchTestCase):
+
+ def test_no_parent(self):
+ # in an empty branch we decode the response properly
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.get_parent', ('quack/',),
+ 'success', ('',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.get_parent()
+ self.assertFinished(client)
+ self.assertEqual(None, result)
+
+ def test_parent_relative(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('kwaak/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.get_parent', ('kwaak/',),
+ 'success', ('../foo/',))
+ transport.mkdir('kwaak')
+ transport = transport.clone('kwaak')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.get_parent()
+ self.assertEqual(transport.clone('../foo').base, result)
+
+ def test_parent_absolute(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('kwaak/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.get_parent', ('kwaak/',),
+ 'success', ('http://foo/',))
+ transport.mkdir('kwaak')
+ transport = transport.clone('kwaak')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.get_parent()
+ self.assertEqual('http://foo/', result)
+ self.assertFinished(client)
+
+
+class TestBranchSetParentLocation(RemoteBranchTestCase):
+
+ def test_no_parent(self):
+ # We call the verb when setting parent to None
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.set_parent_location', ('quack/', 'b', 'r', ''),
+ 'success', ())
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ branch._lock_token = 'b'
+ branch._repo_lock_token = 'r'
+ branch._set_parent_location(None)
+ self.assertFinished(client)
+
+ def test_parent(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('kwaak/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.set_parent_location', ('kwaak/', 'b', 'r', 'foo'),
+ 'success', ())
+ transport.mkdir('kwaak')
+ transport = transport.clone('kwaak')
+ branch = self.make_remote_branch(transport, client)
+ branch._lock_token = 'b'
+ branch._repo_lock_token = 'r'
+ branch._set_parent_location('foo')
+ self.assertFinished(client)
+
+ def test_backwards_compat(self):
+ self.setup_smart_server_with_call_log()
+ branch = self.make_branch('.')
+ self.reset_smart_call_log()
+ verb = 'Branch.set_parent_location'
+ self.disable_verb(verb)
+ branch.set_parent('http://foo/')
+ self.assertLength(14, self.hpss_calls)
+
+
+class TestBranchGetTagsBytes(RemoteBranchTestCase):
+
+ def test_backwards_compat(self):
+ self.setup_smart_server_with_call_log()
+ branch = self.make_branch('.')
+ self.reset_smart_call_log()
+ verb = 'Branch.get_tags_bytes'
+ self.disable_verb(verb)
+ branch.tags.get_tag_dict()
+ call_count = len([call for call in self.hpss_calls if
+ call.call.method == verb])
+ self.assertEqual(1, call_count)
+
+ def test_trivial(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.get_tags_bytes', ('quack/',),
+ 'success', ('',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.tags.get_tag_dict()
+ self.assertFinished(client)
+ self.assertEqual({}, result)
+
+
+class TestBranchSetTagsBytes(RemoteBranchTestCase):
+
+ def test_trivial(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.set_tags_bytes', ('quack/', 'branch token', 'repo token'),
+ 'success', ('',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ self.lock_remote_branch(branch)
+ branch._set_tags_bytes('tags bytes')
+ self.assertFinished(client)
+ self.assertEqual('tags bytes', client._calls[-1][-1])
+
+ def test_backwards_compatible(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.set_tags_bytes', ('quack/', 'branch token', 'repo token'),
+ 'unknown', ('Branch.set_tags_bytes',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ self.lock_remote_branch(branch)
+ class StubRealBranch(object):
+ def __init__(self):
+ self.calls = []
+ def _set_tags_bytes(self, bytes):
+ self.calls.append(('set_tags_bytes', bytes))
+ real_branch = StubRealBranch()
+ branch._real_branch = real_branch
+ branch._set_tags_bytes('tags bytes')
+ # Call a second time, to exercise the 'remote version already inferred'
+ # code path.
+ branch._set_tags_bytes('tags bytes')
+ self.assertFinished(client)
+ self.assertEqual(
+ [('set_tags_bytes', 'tags bytes')] * 2, real_branch.calls)
+
+
+class TestBranchHeadsToFetch(RemoteBranchTestCase):
+
+ def test_uses_last_revision_info_and_tags_by_default(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info', ('quack/',),
+ 'success', ('ok', '1', 'rev-tip'))
+ client.add_expected_call(
+ 'Branch.get_config_file', ('quack/',),
+ 'success', ('ok',), '')
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.heads_to_fetch()
+ self.assertFinished(client)
+ self.assertEqual((set(['rev-tip']), set()), result)
+
+ def test_uses_last_revision_info_and_tags_when_set(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info', ('quack/',),
+ 'success', ('ok', '1', 'rev-tip'))
+ client.add_expected_call(
+ 'Branch.get_config_file', ('quack/',),
+ 'success', ('ok',), 'branch.fetch_tags = True')
+ # XXX: this will break if the default format's serialization of tags
+ # changes, or if the RPC for fetching tags changes from get_tags_bytes.
+ client.add_expected_call(
+ 'Branch.get_tags_bytes', ('quack/',),
+ 'success', ('d5:tag-17:rev-foo5:tag-27:rev-bare',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.heads_to_fetch()
+ self.assertFinished(client)
+ self.assertEqual(
+ (set(['rev-tip']), set(['rev-foo', 'rev-bar'])), result)
+
+ def test_uses_rpc_for_formats_with_non_default_heads_to_fetch(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.heads_to_fetch', ('quack/',),
+ 'success', (['tip'], ['tagged-1', 'tagged-2']))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ branch._format._use_default_local_heads_to_fetch = lambda: False
+ result = branch.heads_to_fetch()
+ self.assertFinished(client)
+ self.assertEqual((set(['tip']), set(['tagged-1', 'tagged-2'])), result)
+
+ def make_branch_with_tags(self):
+ self.setup_smart_server_with_call_log()
+ # Make a branch with a single revision.
+ builder = self.make_branch_builder('foo')
+ builder.start_series()
+ builder.build_snapshot('tip', None, [
+ ('add', ('', 'root-id', 'directory', ''))])
+ builder.finish_series()
+ branch = builder.get_branch()
+ # Add two tags to that branch
+ branch.tags.set_tag('tag-1', 'rev-1')
+ branch.tags.set_tag('tag-2', 'rev-2')
+ return branch
+
+ def test_backwards_compatible(self):
+ br = self.make_branch_with_tags()
+ br.get_config_stack().set('branch.fetch_tags', True)
+ self.addCleanup(br.lock_read().unlock)
+ # Disable the heads_to_fetch verb
+ verb = 'Branch.heads_to_fetch'
+ self.disable_verb(verb)
+ self.reset_smart_call_log()
+ result = br.heads_to_fetch()
+ self.assertEqual((set(['tip']), set(['rev-1', 'rev-2'])), result)
+ self.assertEqual(
+ ['Branch.last_revision_info', 'Branch.get_tags_bytes'],
+ [call.call.method for call in self.hpss_calls])
+
+ def test_backwards_compatible_no_tags(self):
+ br = self.make_branch_with_tags()
+ br.get_config_stack().set('branch.fetch_tags', False)
+ self.addCleanup(br.lock_read().unlock)
+ # Disable the heads_to_fetch verb
+ verb = 'Branch.heads_to_fetch'
+ self.disable_verb(verb)
+ self.reset_smart_call_log()
+ result = br.heads_to_fetch()
+ self.assertEqual((set(['tip']), set()), result)
+ self.assertEqual(
+ ['Branch.last_revision_info'],
+ [call.call.method for call in self.hpss_calls])
+
+
+class TestBranchLastRevisionInfo(RemoteBranchTestCase):
+
+ def test_empty_branch(self):
+ # in an empty branch we decode the response properly
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info', ('quack/',),
+ 'success', ('ok', '0', 'null:'))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.last_revision_info()
+ self.assertFinished(client)
+ self.assertEqual((0, NULL_REVISION), result)
+
+ def test_non_empty_branch(self):
+ # in a non-empty branch we also decode the response properly
+ revid = u'\xc8'.encode('utf8')
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('kwaak/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info', ('kwaak/',),
+ 'success', ('ok', '2', revid))
+ transport.mkdir('kwaak')
+ transport = transport.clone('kwaak')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.last_revision_info()
+ self.assertEqual((2, revid), result)
+
+
+class TestBranch_get_stacked_on_url(TestRemote):
+ """Test Branch._get_stacked_on_url rpc"""
+
+ def test_get_stacked_on_invalid_url(self):
+ # test that asking for a stacked on url the server can't access works.
+ # This isn't perfect, but then as we're in the same process there
+ # really isn't anything we can do to be 100% sure that the server
+ # doesn't just open in - this test probably needs to be rewritten using
+ # a spawn()ed server.
+ stacked_branch = self.make_branch('stacked', format='1.9')
+ memory_branch = self.make_branch('base', format='1.9')
+ vfs_url = self.get_vfs_only_url('base')
+ stacked_branch.set_stacked_on_url(vfs_url)
+ transport = stacked_branch.bzrdir.root_transport
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('stacked/',),
+ 'success', ('ok', vfs_url))
+ # XXX: Multiple calls are bad, this second call documents what is
+ # today.
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('stacked/',),
+ 'success', ('ok', vfs_url))
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=client)
+ repo_fmt = remote.RemoteRepositoryFormat()
+ repo_fmt._custom_format = stacked_branch.repository._format
+ branch = RemoteBranch(bzrdir, RemoteRepository(bzrdir, repo_fmt),
+ _client=client)
+ result = branch.get_stacked_on_url()
+ self.assertEqual(vfs_url, result)
+
+ def test_backwards_compatible(self):
+ # like with bzr1.6 with no Branch.get_stacked_on_url rpc
+ base_branch = self.make_branch('base', format='1.6')
+ stacked_branch = self.make_branch('stacked', format='1.6')
+ stacked_branch.set_stacked_on_url('../base')
+ client = FakeClient(self.get_url())
+ branch_network_name = self.get_branch_format().network_name()
+ client.add_expected_call(
+ 'BzrDir.open_branchV3', ('stacked/',),
+ 'success', ('branch', branch_network_name))
+ client.add_expected_call(
+ 'BzrDir.find_repositoryV3', ('stacked/',),
+ 'success', ('ok', '', 'no', 'no', 'yes',
+ stacked_branch.repository._format.network_name()))
+ # called twice, once from constructor and then again by us
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('stacked/',),
+ 'unknown', ('Branch.get_stacked_on_url',))
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('stacked/',),
+ 'unknown', ('Branch.get_stacked_on_url',))
+ # this will also do vfs access, but that goes direct to the transport
+ # and isn't seen by the FakeClient.
+ bzrdir = RemoteBzrDir(self.get_transport('stacked'),
+ RemoteBzrDirFormat(), _client=client)
+ branch = bzrdir.open_branch()
+ result = branch.get_stacked_on_url()
+ self.assertEqual('../base', result)
+ self.assertFinished(client)
+ # it's in the fallback list both for the RemoteRepository and its vfs
+ # repository
+ self.assertEqual(1, len(branch.repository._fallback_repositories))
+ self.assertEqual(1,
+ len(branch.repository._real_repository._fallback_repositories))
+
+ def test_get_stacked_on_real_branch(self):
+ base_branch = self.make_branch('base')
+ stacked_branch = self.make_branch('stacked')
+ stacked_branch.set_stacked_on_url('../base')
+ reference_format = self.get_repo_format()
+ network_name = reference_format.network_name()
+ client = FakeClient(self.get_url())
+ branch_network_name = self.get_branch_format().network_name()
+ client.add_expected_call(
+ 'BzrDir.open_branchV3', ('stacked/',),
+ 'success', ('branch', branch_network_name))
+ client.add_expected_call(
+ 'BzrDir.find_repositoryV3', ('stacked/',),
+ 'success', ('ok', '', 'yes', 'no', 'yes', network_name))
+ # called twice, once from constructor and then again by us
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('stacked/',),
+ 'success', ('ok', '../base'))
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('stacked/',),
+ 'success', ('ok', '../base'))
+ bzrdir = RemoteBzrDir(self.get_transport('stacked'),
+ RemoteBzrDirFormat(), _client=client)
+ branch = bzrdir.open_branch()
+ result = branch.get_stacked_on_url()
+ self.assertEqual('../base', result)
+ self.assertFinished(client)
+ # it's in the fallback list both for the RemoteRepository.
+ self.assertEqual(1, len(branch.repository._fallback_repositories))
+ # And we haven't had to construct a real repository.
+ self.assertEqual(None, branch.repository._real_repository)
+
+
+class TestBranchSetLastRevision(RemoteBranchTestCase):
+
+ def test_set_empty(self):
+ # _set_last_revision_info('null:') is translated to calling
+ # Branch.set_last_revision(path, '') on the wire.
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('branch/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.lock_write', ('branch/', '', ''),
+ 'success', ('ok', 'branch token', 'repo token'))
+ client.add_expected_call(
+ 'Branch.last_revision_info',
+ ('branch/',),
+ 'success', ('ok', '0', 'null:'))
+ client.add_expected_call(
+ 'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Branch.unlock', ('branch/', 'branch token', 'repo token'),
+ 'success', ('ok',))
+ branch = self.make_remote_branch(transport, client)
+ branch.lock_write()
+ result = branch._set_last_revision(NULL_REVISION)
+ branch.unlock()
+ self.assertEqual(None, result)
+ self.assertFinished(client)
+
+ def test_set_nonempty(self):
+ # set_last_revision_info(N, rev-idN) is translated to calling
+ # Branch.set_last_revision(path, rev-idN) on the wire.
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('branch/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.lock_write', ('branch/', '', ''),
+ 'success', ('ok', 'branch token', 'repo token'))
+ client.add_expected_call(
+ 'Branch.last_revision_info',
+ ('branch/',),
+ 'success', ('ok', '0', 'null:'))
+ lines = ['rev-id2']
+ encoded_body = bz2.compress('\n'.join(lines))
+ client.add_success_response_with_body(encoded_body, 'ok')
+ client.add_expected_call(
+ 'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Branch.unlock', ('branch/', 'branch token', 'repo token'),
+ 'success', ('ok',))
+ branch = self.make_remote_branch(transport, client)
+ # Lock the branch, reset the record of remote calls.
+ branch.lock_write()
+ result = branch._set_last_revision('rev-id2')
+ branch.unlock()
+ self.assertEqual(None, result)
+ self.assertFinished(client)
+
+ def test_no_such_revision(self):
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+ # A response of 'NoSuchRevision' is translated into an exception.
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('branch/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.lock_write', ('branch/', '', ''),
+ 'success', ('ok', 'branch token', 'repo token'))
+ client.add_expected_call(
+ 'Branch.last_revision_info',
+ ('branch/',),
+ 'success', ('ok', '0', 'null:'))
+ # get_graph calls to construct the revision history, for the set_rh
+ # hook
+ lines = ['rev-id']
+ encoded_body = bz2.compress('\n'.join(lines))
+ client.add_success_response_with_body(encoded_body, 'ok')
+ client.add_expected_call(
+ 'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
+ 'error', ('NoSuchRevision', 'rev-id'))
+ client.add_expected_call(
+ 'Branch.unlock', ('branch/', 'branch token', 'repo token'),
+ 'success', ('ok',))
+
+ branch = self.make_remote_branch(transport, client)
+ branch.lock_write()
+ self.assertRaises(
+ errors.NoSuchRevision, branch._set_last_revision, 'rev-id')
+ branch.unlock()
+ self.assertFinished(client)
+
+ def test_tip_change_rejected(self):
+ """TipChangeRejected responses cause a TipChangeRejected exception to
+ be raised.
+ """
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+ client = FakeClient(transport.base)
+ rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
+ rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('branch/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.lock_write', ('branch/', '', ''),
+ 'success', ('ok', 'branch token', 'repo token'))
+ client.add_expected_call(
+ 'Branch.last_revision_info',
+ ('branch/',),
+ 'success', ('ok', '0', 'null:'))
+ lines = ['rev-id']
+ encoded_body = bz2.compress('\n'.join(lines))
+ client.add_success_response_with_body(encoded_body, 'ok')
+ client.add_expected_call(
+ 'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
+ 'error', ('TipChangeRejected', rejection_msg_utf8))
+ client.add_expected_call(
+ 'Branch.unlock', ('branch/', 'branch token', 'repo token'),
+ 'success', ('ok',))
+ branch = self.make_remote_branch(transport, client)
+ branch.lock_write()
+ # The 'TipChangeRejected' error response triggered by calling
+ # set_last_revision_info causes a TipChangeRejected exception.
+ err = self.assertRaises(
+ errors.TipChangeRejected,
+ branch._set_last_revision, 'rev-id')
+ # The UTF-8 message from the response has been decoded into a unicode
+ # object.
+ self.assertIsInstance(err.msg, unicode)
+ self.assertEqual(rejection_msg_unicode, err.msg)
+ branch.unlock()
+ self.assertFinished(client)
+
+
+class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
+
+ def test_set_last_revision_info(self):
+ # set_last_revision_info(num, 'rev-id') is translated to calling
+ # Branch.set_last_revision_info(num, 'rev-id') on the wire.
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+ client = FakeClient(transport.base)
+ # get_stacked_on_url
+ client.add_error_response('NotStacked')
+ # lock_write
+ client.add_success_response('ok', 'branch token', 'repo token')
+ # query the current revision
+ client.add_success_response('ok', '0', 'null:')
+ # set_last_revision
+ client.add_success_response('ok')
+ # unlock
+ client.add_success_response('ok')
+
+ branch = self.make_remote_branch(transport, client)
+ # Lock the branch, reset the record of remote calls.
+ branch.lock_write()
+ client._calls = []
+ result = branch.set_last_revision_info(1234, 'a-revision-id')
+ self.assertEqual(
+ [('call', 'Branch.last_revision_info', ('branch/',)),
+ ('call', 'Branch.set_last_revision_info',
+ ('branch/', 'branch token', 'repo token',
+ '1234', 'a-revision-id'))],
+ client._calls)
+ self.assertEqual(None, result)
+
+ def test_no_such_revision(self):
+ # A response of 'NoSuchRevision' is translated into an exception.
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+ client = FakeClient(transport.base)
+ # get_stacked_on_url
+ client.add_error_response('NotStacked')
+ # lock_write
+ client.add_success_response('ok', 'branch token', 'repo token')
+ # set_last_revision
+ client.add_error_response('NoSuchRevision', 'revid')
+ # unlock
+ client.add_success_response('ok')
+
+ branch = self.make_remote_branch(transport, client)
+ # Lock the branch, reset the record of remote calls.
+ branch.lock_write()
+ client._calls = []
+
+ self.assertRaises(
+ errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
+ branch.unlock()
+
+ def test_backwards_compatibility(self):
+ """If the server does not support the Branch.set_last_revision_info
+ verb (which is new in 1.4), then the client falls back to VFS methods.
+ """
+ # This test is a little messy. Unlike most tests in this file, it
+ # doesn't purely test what a Remote* object sends over the wire, and
+ # how it reacts to responses from the wire. It instead relies partly
+ # on asserting that the RemoteBranch will call
+ # self._real_branch.set_last_revision_info(...).
+
+ # First, set up our RemoteBranch with a FakeClient that raises
+ # UnknownSmartMethod, and a StubRealBranch that logs how it is called.
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('branch/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info',
+ ('branch/',),
+ 'success', ('ok', '0', 'null:'))
+ client.add_expected_call(
+ 'Branch.set_last_revision_info',
+ ('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
+ 'unknown', 'Branch.set_last_revision_info')
+
+ branch = self.make_remote_branch(transport, client)
+ class StubRealBranch(object):
+ def __init__(self):
+ self.calls = []
+ def set_last_revision_info(self, revno, revision_id):
+ self.calls.append(
+ ('set_last_revision_info', revno, revision_id))
+ def _clear_cached_state(self):
+ pass
+ real_branch = StubRealBranch()
+ branch._real_branch = real_branch
+ self.lock_remote_branch(branch)
+
+ # Call set_last_revision_info, and verify it behaved as expected.
+ result = branch.set_last_revision_info(1234, 'a-revision-id')
+ self.assertEqual(
+ [('set_last_revision_info', 1234, 'a-revision-id')],
+ real_branch.calls)
+ self.assertFinished(client)
+
+ def test_unexpected_error(self):
+ # If the server sends an error the client doesn't understand, it gets
+ # turned into an UnknownErrorFromSmartServer, which is presented as a
+ # non-internal error to the user.
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+ client = FakeClient(transport.base)
+ # get_stacked_on_url
+ client.add_error_response('NotStacked')
+ # lock_write
+ client.add_success_response('ok', 'branch token', 'repo token')
+ # set_last_revision
+ client.add_error_response('UnexpectedError')
+ # unlock
+ client.add_success_response('ok')
+
+ branch = self.make_remote_branch(transport, client)
+ # Lock the branch, reset the record of remote calls.
+ branch.lock_write()
+ client._calls = []
+
+ err = self.assertRaises(
+ errors.UnknownErrorFromSmartServer,
+ branch.set_last_revision_info, 123, 'revid')
+ self.assertEqual(('UnexpectedError',), err.error_tuple)
+ branch.unlock()
+
+ def test_tip_change_rejected(self):
+ """TipChangeRejected responses cause a TipChangeRejected exception to
+ be raised.
+ """
+ transport = MemoryTransport()
+ transport.mkdir('branch')
+ transport = transport.clone('branch')
+ client = FakeClient(transport.base)
+ # get_stacked_on_url
+ client.add_error_response('NotStacked')
+ # lock_write
+ client.add_success_response('ok', 'branch token', 'repo token')
+ # set_last_revision
+ client.add_error_response('TipChangeRejected', 'rejection message')
+ # unlock
+ client.add_success_response('ok')
+
+ branch = self.make_remote_branch(transport, client)
+ # Lock the branch, reset the record of remote calls.
+ branch.lock_write()
+ self.addCleanup(branch.unlock)
+ client._calls = []
+
+ # The 'TipChangeRejected' error response triggered by calling
+ # set_last_revision_info causes a TipChangeRejected exception.
+ err = self.assertRaises(
+ errors.TipChangeRejected,
+ branch.set_last_revision_info, 123, 'revid')
+ self.assertEqual('rejection message', err.msg)
+
+
+class TestBranchGetSetConfig(RemoteBranchTestCase):
+
+ def test_get_branch_conf(self):
+ # in an empty branch we decode the response properly
+ client = FakeClient()
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('memory:///',),
+ 'error', ('NotStacked',),)
+ client.add_success_response_with_body('# config file body', 'ok')
+ transport = MemoryTransport()
+ branch = self.make_remote_branch(transport, client)
+ config = branch.get_config()
+ config.has_explicit_nickname()
+ self.assertEqual(
+ [('call', 'Branch.get_stacked_on_url', ('memory:///',)),
+ ('call_expecting_body', 'Branch.get_config_file', ('memory:///',))],
+ client._calls)
+
+ def test_get_multi_line_branch_conf(self):
+ # Make sure that multiple-line branch.conf files are supported
+ #
+ # https://bugs.launchpad.net/bzr/+bug/354075
+ client = FakeClient()
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('memory:///',),
+ 'error', ('NotStacked',),)
+ client.add_success_response_with_body('a = 1\nb = 2\nc = 3\n', 'ok')
+ transport = MemoryTransport()
+ branch = self.make_remote_branch(transport, client)
+ config = branch.get_config()
+ self.assertEqual(u'2', config.get_user_option('b'))
+
+ def test_set_option(self):
+ client = FakeClient()
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('memory:///',),
+ 'error', ('NotStacked',),)
+ client.add_expected_call(
+ 'Branch.lock_write', ('memory:///', '', ''),
+ 'success', ('ok', 'branch token', 'repo token'))
+ client.add_expected_call(
+ 'Branch.set_config_option', ('memory:///', 'branch token',
+ 'repo token', 'foo', 'bar', ''),
+ 'success', ())
+ client.add_expected_call(
+ 'Branch.unlock', ('memory:///', 'branch token', 'repo token'),
+ 'success', ('ok',))
+ transport = MemoryTransport()
+ branch = self.make_remote_branch(transport, client)
+ branch.lock_write()
+ config = branch._get_config()
+ config.set_option('foo', 'bar')
+ branch.unlock()
+ self.assertFinished(client)
+
+ def test_set_option_with_dict(self):
+ client = FakeClient()
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('memory:///',),
+ 'error', ('NotStacked',),)
+ client.add_expected_call(
+ 'Branch.lock_write', ('memory:///', '', ''),
+ 'success', ('ok', 'branch token', 'repo token'))
+ encoded_dict_value = 'd5:ascii1:a11:unicode \xe2\x8c\x9a3:\xe2\x80\xbde'
+ client.add_expected_call(
+ 'Branch.set_config_option_dict', ('memory:///', 'branch token',
+ 'repo token', encoded_dict_value, 'foo', ''),
+ 'success', ())
+ client.add_expected_call(
+ 'Branch.unlock', ('memory:///', 'branch token', 'repo token'),
+ 'success', ('ok',))
+ transport = MemoryTransport()
+ branch = self.make_remote_branch(transport, client)
+ branch.lock_write()
+ config = branch._get_config()
+ config.set_option(
+ {'ascii': 'a', u'unicode \N{WATCH}': u'\N{INTERROBANG}'},
+ 'foo')
+ branch.unlock()
+ self.assertFinished(client)
+
+ def test_backwards_compat_set_option(self):
+ self.setup_smart_server_with_call_log()
+ branch = self.make_branch('.')
+ verb = 'Branch.set_config_option'
+ self.disable_verb(verb)
+ branch.lock_write()
+ self.addCleanup(branch.unlock)
+ self.reset_smart_call_log()
+ branch._get_config().set_option('value', 'name')
+ self.assertLength(11, self.hpss_calls)
+ self.assertEqual('value', branch._get_config().get_option('name'))
+
+ def test_backwards_compat_set_option_with_dict(self):
+ self.setup_smart_server_with_call_log()
+ branch = self.make_branch('.')
+ verb = 'Branch.set_config_option_dict'
+ self.disable_verb(verb)
+ branch.lock_write()
+ self.addCleanup(branch.unlock)
+ self.reset_smart_call_log()
+ config = branch._get_config()
+ value_dict = {'ascii': 'a', u'unicode \N{WATCH}': u'\N{INTERROBANG}'}
+ config.set_option(value_dict, 'name')
+ self.assertLength(11, self.hpss_calls)
+ self.assertEqual(value_dict, branch._get_config().get_option('name'))
+
+
+class TestBranchGetPutConfigStore(RemoteBranchTestCase):
+
+ def test_get_branch_conf(self):
+ # in an empty branch we decode the response properly
+ client = FakeClient()
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('memory:///',),
+ 'error', ('NotStacked',),)
+ client.add_success_response_with_body('# config file body', 'ok')
+ transport = MemoryTransport()
+ branch = self.make_remote_branch(transport, client)
+ config = branch.get_config_stack()
+ config.get("email")
+ config.get("log_format")
+ self.assertEqual(
+ [('call', 'Branch.get_stacked_on_url', ('memory:///',)),
+ ('call_expecting_body', 'Branch.get_config_file', ('memory:///',))],
+ client._calls)
+
+ def test_set_branch_conf(self):
+ client = FakeClient()
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('memory:///',),
+ 'error', ('NotStacked',),)
+ client.add_expected_call(
+ 'Branch.lock_write', ('memory:///', '', ''),
+ 'success', ('ok', 'branch token', 'repo token'))
+ client.add_expected_call(
+ 'Branch.get_config_file', ('memory:///', ),
+ 'success', ('ok', ), "# line 1\n")
+ client.add_expected_call(
+ 'Branch.get_config_file', ('memory:///', ),
+ 'success', ('ok', ), "# line 1\n")
+ client.add_expected_call(
+ 'Branch.put_config_file', ('memory:///', 'branch token',
+ 'repo token'),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Branch.unlock', ('memory:///', 'branch token', 'repo token'),
+ 'success', ('ok',))
+ transport = MemoryTransport()
+ branch = self.make_remote_branch(transport, client)
+ branch.lock_write()
+ config = branch.get_config_stack()
+ config.set('email', 'The Dude <lebowski@example.com>')
+ branch.unlock()
+ self.assertFinished(client)
+ self.assertEqual(
+ [('call', 'Branch.get_stacked_on_url', ('memory:///',)),
+ ('call', 'Branch.lock_write', ('memory:///', '', '')),
+ ('call_expecting_body', 'Branch.get_config_file', ('memory:///',)),
+ ('call_expecting_body', 'Branch.get_config_file', ('memory:///',)),
+ ('call_with_body_bytes_expecting_body', 'Branch.put_config_file',
+ ('memory:///', 'branch token', 'repo token'),
+ '# line 1\nemail = The Dude <lebowski@example.com>\n'),
+ ('call', 'Branch.unlock', ('memory:///', 'branch token', 'repo token'))],
+ client._calls)
+
+
+class TestBranchLockWrite(RemoteBranchTestCase):
+
+ def test_lock_write_unlockable(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',),)
+ client.add_expected_call(
+ 'Branch.lock_write', ('quack/', '', ''),
+ 'error', ('UnlockableTransport',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ self.assertRaises(errors.UnlockableTransport, branch.lock_write)
+ self.assertFinished(client)
+
+
+class TestBranchRevisionIdToRevno(RemoteBranchTestCase):
+
+ def test_simple(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',),)
+ client.add_expected_call(
+ 'Branch.revision_id_to_revno', ('quack/', 'null:'),
+ 'success', ('ok', '0',),)
+ client.add_expected_call(
+ 'Branch.revision_id_to_revno', ('quack/', 'unknown'),
+ 'error', ('NoSuchRevision', 'unknown',),)
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ self.assertEquals(0, branch.revision_id_to_revno('null:'))
+ self.assertRaises(errors.NoSuchRevision,
+ branch.revision_id_to_revno, 'unknown')
+ self.assertFinished(client)
+
+ def test_dotted(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',),)
+ client.add_expected_call(
+ 'Branch.revision_id_to_revno', ('quack/', 'null:'),
+ 'success', ('ok', '0',),)
+ client.add_expected_call(
+ 'Branch.revision_id_to_revno', ('quack/', 'unknown'),
+ 'error', ('NoSuchRevision', 'unknown',),)
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ self.assertEquals((0, ), branch.revision_id_to_dotted_revno('null:'))
+ self.assertRaises(errors.NoSuchRevision,
+ branch.revision_id_to_dotted_revno, 'unknown')
+ self.assertFinished(client)
+
+ def test_dotted_no_smart_verb(self):
+ self.setup_smart_server_with_call_log()
+ branch = self.make_branch('.')
+ self.disable_verb('Branch.revision_id_to_revno')
+ self.reset_smart_call_log()
+ self.assertEquals((0, ),
+ branch.revision_id_to_dotted_revno('null:'))
+ self.assertLength(8, self.hpss_calls)
+
+
+class TestBzrDirGetSetConfig(RemoteBzrDirTestCase):
+
+ def test__get_config(self):
+ client = FakeClient()
+ client.add_success_response_with_body('default_stack_on = /\n', 'ok')
+ transport = MemoryTransport()
+ bzrdir = self.make_remote_bzrdir(transport, client)
+ config = bzrdir.get_config()
+ self.assertEqual('/', config.get_default_stack_on())
+ self.assertEqual(
+ [('call_expecting_body', 'BzrDir.get_config_file', ('memory:///',))],
+ client._calls)
+
+ def test_set_option_uses_vfs(self):
+ self.setup_smart_server_with_call_log()
+ bzrdir = self.make_bzrdir('.')
+ self.reset_smart_call_log()
+ config = bzrdir.get_config()
+ config.set_default_stack_on('/')
+ self.assertLength(4, self.hpss_calls)
+
+ def test_backwards_compat_get_option(self):
+ self.setup_smart_server_with_call_log()
+ bzrdir = self.make_bzrdir('.')
+ verb = 'BzrDir.get_config_file'
+ self.disable_verb(verb)
+ self.reset_smart_call_log()
+ self.assertEqual(None,
+ bzrdir._get_config().get_option('default_stack_on'))
+ self.assertLength(4, self.hpss_calls)
+
+
+class TestTransportIsReadonly(tests.TestCase):
+
+ def test_true(self):
+ client = FakeClient()
+ client.add_success_response('yes')
+ transport = RemoteTransport('bzr://example.com/', medium=False,
+ _client=client)
+ self.assertEqual(True, transport.is_readonly())
+ self.assertEqual(
+ [('call', 'Transport.is_readonly', ())],
+ client._calls)
+
+ def test_false(self):
+ client = FakeClient()
+ client.add_success_response('no')
+ transport = RemoteTransport('bzr://example.com/', medium=False,
+ _client=client)
+ self.assertEqual(False, transport.is_readonly())
+ self.assertEqual(
+ [('call', 'Transport.is_readonly', ())],
+ client._calls)
+
+ def test_error_from_old_server(self):
+ """bzr 0.15 and earlier servers don't recognise the is_readonly verb.
+
+ Clients should treat it as a "no" response, because is_readonly is only
+ advisory anyway (a transport could be read-write, but then the
+ underlying filesystem could be readonly anyway).
+ """
+ client = FakeClient()
+ client.add_unknown_method_response('Transport.is_readonly')
+ transport = RemoteTransport('bzr://example.com/', medium=False,
+ _client=client)
+ self.assertEqual(False, transport.is_readonly())
+ self.assertEqual(
+ [('call', 'Transport.is_readonly', ())],
+ client._calls)
+
+
+class TestTransportMkdir(tests.TestCase):
+
+ def test_permissiondenied(self):
+ client = FakeClient()
+ client.add_error_response('PermissionDenied', 'remote path', 'extra')
+ transport = RemoteTransport('bzr://example.com/', medium=False,
+ _client=client)
+ exc = self.assertRaises(
+ errors.PermissionDenied, transport.mkdir, 'client path')
+ expected_error = errors.PermissionDenied('/client path', 'extra')
+ self.assertEqual(expected_error, exc)
+
+
+class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
+
+ def test_defaults_to_none(self):
+ t = RemoteSSHTransport('bzr+ssh://example.com')
+ self.assertIs(None, t._get_credentials()[0])
+
+ def test_uses_authentication_config(self):
+ conf = config.AuthenticationConfig()
+ conf._get_config().update(
+ {'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
+ 'example.com'}})
+ conf._save()
+ t = RemoteSSHTransport('bzr+ssh://example.com')
+ self.assertEqual('bar', t._get_credentials()[0])
+
+
+class TestRemoteRepository(TestRemote):
+ """Base for testing RemoteRepository protocol usage.
+
+ These tests contain frozen requests and responses. We want any changes to
+ what is sent or expected to be require a thoughtful update to these tests
+ because they might break compatibility with different-versioned servers.
+ """
+
+ def setup_fake_client_and_repository(self, transport_path):
+ """Create the fake client and repository for testing with.
+
+ There's no real server here; we just have canned responses sent
+ back one by one.
+
+ :param transport_path: Path below the root of the MemoryTransport
+ where the repository will be created.
+ """
+ transport = MemoryTransport()
+ transport.mkdir(transport_path)
+ client = FakeClient(transport.base)
+ transport = transport.clone(transport_path)
+ # we do not want bzrdir to make any remote calls
+ bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
+ _client=False)
+ repo = RemoteRepository(bzrdir, None, _client=client)
+ return repo, client
+
+
+def remoted_description(format):
+ return 'Remote: ' + format.get_format_description()
+
+
+class TestBranchFormat(tests.TestCase):
+
+ def test_get_format_description(self):
+ remote_format = RemoteBranchFormat()
+ real_format = branch.format_registry.get_default()
+ remote_format._network_name = real_format.network_name()
+ self.assertEqual(remoted_description(real_format),
+ remote_format.get_format_description())
+
+
+class TestRepositoryFormat(TestRemoteRepository):
+
+ def test_fast_delta(self):
+ true_name = groupcompress_repo.RepositoryFormat2a().network_name()
+ true_format = RemoteRepositoryFormat()
+ true_format._network_name = true_name
+ self.assertEqual(True, true_format.fast_deltas)
+ false_name = knitpack_repo.RepositoryFormatKnitPack1().network_name()
+ false_format = RemoteRepositoryFormat()
+ false_format._network_name = false_name
+ self.assertEqual(False, false_format.fast_deltas)
+
+ def test_get_format_description(self):
+ remote_repo_format = RemoteRepositoryFormat()
+ real_format = repository.format_registry.get_default()
+ remote_repo_format._network_name = real_format.network_name()
+ self.assertEqual(remoted_description(real_format),
+ remote_repo_format.get_format_description())
+
+
+class TestRepositoryAllRevisionIds(TestRemoteRepository):
+
+ def test_empty(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body('', 'ok')
+ self.assertEquals([], repo.all_revision_ids())
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.all_revision_ids',
+ ('quack/',))],
+ client._calls)
+
+ def test_with_some_content(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(
+ 'rev1\nrev2\nanotherrev\n', 'ok')
+ self.assertEquals(["rev1", "rev2", "anotherrev"],
+ repo.all_revision_ids())
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.all_revision_ids',
+ ('quack/',))],
+ client._calls)
+
+
+class TestRepositoryGatherStats(TestRemoteRepository):
+
+ def test_revid_none(self):
+ # ('ok',), body with revisions and size
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(
+ 'revisions: 2\nsize: 18\n', 'ok')
+ result = repo.gather_stats(None)
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.gather_stats',
+ ('quack/','','no'))],
+ client._calls)
+ self.assertEqual({'revisions': 2, 'size': 18}, result)
+
+ def test_revid_no_committers(self):
+ # ('ok',), body without committers
+ body = ('firstrev: 123456.300 3600\n'
+ 'latestrev: 654231.400 0\n'
+ 'revisions: 2\n'
+ 'size: 18\n')
+ transport_path = 'quick'
+ revid = u'\xc8'.encode('utf8')
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(body, 'ok')
+ result = repo.gather_stats(revid)
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.gather_stats',
+ ('quick/', revid, 'no'))],
+ client._calls)
+ self.assertEqual({'revisions': 2, 'size': 18,
+ 'firstrev': (123456.300, 3600),
+ 'latestrev': (654231.400, 0),},
+ result)
+
+ def test_revid_with_committers(self):
+ # ('ok',), body with committers
+ body = ('committers: 128\n'
+ 'firstrev: 123456.300 3600\n'
+ 'latestrev: 654231.400 0\n'
+ 'revisions: 2\n'
+ 'size: 18\n')
+ transport_path = 'buick'
+ revid = u'\xc8'.encode('utf8')
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(body, 'ok')
+ result = repo.gather_stats(revid, True)
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.gather_stats',
+ ('buick/', revid, 'yes'))],
+ client._calls)
+ self.assertEqual({'revisions': 2, 'size': 18,
+ 'committers': 128,
+ 'firstrev': (123456.300, 3600),
+ 'latestrev': (654231.400, 0),},
+ result)
+
+
+class TestRepositoryBreakLock(TestRemoteRepository):
+
+ def test_break_lock(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('ok')
+ repo.break_lock()
+ self.assertEqual(
+ [('call', 'Repository.break_lock', ('quack/',))],
+ client._calls)
+
+
+class TestRepositoryGetSerializerFormat(TestRemoteRepository):
+
+ def test_get_serializer_format(self):
+ transport_path = 'hill'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('ok', '7')
+ self.assertEquals('7', repo.get_serializer_format())
+ self.assertEqual(
+ [('call', 'VersionedFileRepository.get_serializer_format',
+ ('hill/', ))],
+ client._calls)
+
+
+class TestRepositoryReconcile(TestRemoteRepository):
+
+ def test_reconcile(self):
+ transport_path = 'hill'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ body = ("garbage_inventories: 2\n"
+ "inconsistent_parents: 3\n")
+ client.add_expected_call(
+ 'Repository.lock_write', ('hill/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_success_response_with_body(body, 'ok')
+ reconciler = repo.reconcile()
+ self.assertEqual(
+ [('call', 'Repository.lock_write', ('hill/', '')),
+ ('call_expecting_body', 'Repository.reconcile',
+ ('hill/', 'a token'))],
+ client._calls)
+ self.assertEquals(2, reconciler.garbage_inventories)
+ self.assertEquals(3, reconciler.inconsistent_parents)
+
+
+class TestRepositoryGetRevisionSignatureText(TestRemoteRepository):
+
+ def test_text(self):
+ # ('ok',), body with signature text
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(
+ 'THETEXT', 'ok')
+ self.assertEquals("THETEXT", repo.get_signature_text("revid"))
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.get_revision_signature_text',
+ ('quack/', 'revid'))],
+ client._calls)
+
+ def test_no_signature(self):
+ transport_path = 'quick'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_error_response('nosuchrevision', 'unknown')
+ self.assertRaises(errors.NoSuchRevision, repo.get_signature_text,
+ "unknown")
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.get_revision_signature_text',
+ ('quick/', 'unknown'))],
+ client._calls)
+
+
+class TestRepositoryGetGraph(TestRemoteRepository):
+
+ def test_get_graph(self):
+ # get_graph returns a graph with a custom parents provider.
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ graph = repo.get_graph()
+ self.assertNotEqual(graph._parents_provider, repo)
+
+
+class TestRepositoryAddSignatureText(TestRemoteRepository):
+
+ def test_add_signature_text(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.start_write_group', ('quack/', 'a token'),
+ 'success', ('ok', ('token1', )))
+ client.add_expected_call(
+ 'Repository.add_signature_text', ('quack/', 'a token', 'rev1',
+ 'token1'),
+ 'success', ('ok', ), None)
+ repo.lock_write()
+ repo.start_write_group()
+ self.assertIs(None,
+ repo.add_signature_text("rev1", "every bloody emperor"))
+ self.assertEqual(
+ ('call_with_body_bytes_expecting_body',
+ 'Repository.add_signature_text',
+ ('quack/', 'a token', 'rev1', 'token1'),
+ 'every bloody emperor'),
+ client._calls[-1])
+
+
+class TestRepositoryGetParentMap(TestRemoteRepository):
+
+ def test_get_parent_map_caching(self):
+ # get_parent_map returns from cache until unlock()
+ # setup a reponse with two revisions
+ r1 = u'\u0e33'.encode('utf8')
+ r2 = u'\u0dab'.encode('utf8')
+ lines = [' '.join([r2, r1]), r1]
+ encoded_body = bz2.compress('\n'.join(lines))
+
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(encoded_body, 'ok')
+ client.add_success_response_with_body(encoded_body, 'ok')
+ repo.lock_read()
+ graph = repo.get_graph()
+ parents = graph.get_parent_map([r2])
+ self.assertEqual({r2: (r1,)}, parents)
+ # locking and unlocking deeper should not reset
+ repo.lock_read()
+ repo.unlock()
+ parents = graph.get_parent_map([r1])
+ self.assertEqual({r1: (NULL_REVISION,)}, parents)
+ self.assertEqual(
+ [('call_with_body_bytes_expecting_body',
+ 'Repository.get_parent_map', ('quack/', 'include-missing:', r2),
+ '\n\n0')],
+ client._calls)
+ repo.unlock()
+ # now we call again, and it should use the second response.
+ repo.lock_read()
+ graph = repo.get_graph()
+ parents = graph.get_parent_map([r1])
+ self.assertEqual({r1: (NULL_REVISION,)}, parents)
+ self.assertEqual(
+ [('call_with_body_bytes_expecting_body',
+ 'Repository.get_parent_map', ('quack/', 'include-missing:', r2),
+ '\n\n0'),
+ ('call_with_body_bytes_expecting_body',
+ 'Repository.get_parent_map', ('quack/', 'include-missing:', r1),
+ '\n\n0'),
+ ],
+ client._calls)
+ repo.unlock()
+
+ def test_get_parent_map_reconnects_if_unknown_method(self):
+ transport_path = 'quack'
+ rev_id = 'revision-id'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_unknown_method_response('Repository.get_parent_map')
+ client.add_success_response_with_body(rev_id, 'ok')
+ self.assertFalse(client._medium._is_remote_before((1, 2)))
+ parents = repo.get_parent_map([rev_id])
+ self.assertEqual(
+ [('call_with_body_bytes_expecting_body',
+ 'Repository.get_parent_map',
+ ('quack/', 'include-missing:', rev_id), '\n\n0'),
+ ('disconnect medium',),
+ ('call_expecting_body', 'Repository.get_revision_graph',
+ ('quack/', ''))],
+ client._calls)
+ # The medium is now marked as being connected to an older server
+ self.assertTrue(client._medium._is_remote_before((1, 2)))
+ self.assertEqual({rev_id: ('null:',)}, parents)
+
+ def test_get_parent_map_fallback_parentless_node(self):
+ """get_parent_map falls back to get_revision_graph on old servers. The
+ results from get_revision_graph are tweaked to match the get_parent_map
+ API.
+
+ Specifically, a {key: ()} result from get_revision_graph means "no
+ parents" for that key, which in get_parent_map results should be
+ represented as {key: ('null:',)}.
+
+ This is the test for https://bugs.launchpad.net/bzr/+bug/214894
+ """
+ rev_id = 'revision-id'
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(rev_id, 'ok')
+ client._medium._remember_remote_is_before((1, 2))
+ parents = repo.get_parent_map([rev_id])
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.get_revision_graph',
+ ('quack/', ''))],
+ client._calls)
+ self.assertEqual({rev_id: ('null:',)}, parents)
+
+ def test_get_parent_map_unexpected_response(self):
+ repo, client = self.setup_fake_client_and_repository('path')
+ client.add_success_response('something unexpected!')
+ self.assertRaises(
+ errors.UnexpectedSmartServerResponse,
+ repo.get_parent_map, ['a-revision-id'])
+
+ def test_get_parent_map_negative_caches_missing_keys(self):
+ self.setup_smart_server_with_call_log()
+ repo = self.make_repository('foo')
+ self.assertIsInstance(repo, RemoteRepository)
+ repo.lock_read()
+ self.addCleanup(repo.unlock)
+ self.reset_smart_call_log()
+ graph = repo.get_graph()
+ self.assertEqual({},
+ graph.get_parent_map(['some-missing', 'other-missing']))
+ self.assertLength(1, self.hpss_calls)
+ # No call if we repeat this
+ self.reset_smart_call_log()
+ graph = repo.get_graph()
+ self.assertEqual({},
+ graph.get_parent_map(['some-missing', 'other-missing']))
+ self.assertLength(0, self.hpss_calls)
+ # Asking for more unknown keys makes a request.
+ self.reset_smart_call_log()
+ graph = repo.get_graph()
+ self.assertEqual({},
+ graph.get_parent_map(['some-missing', 'other-missing',
+ 'more-missing']))
+ self.assertLength(1, self.hpss_calls)
+
+ def disableExtraResults(self):
+ self.overrideAttr(SmartServerRepositoryGetParentMap,
+ 'no_extra_results', True)
+
+ def test_null_cached_missing_and_stop_key(self):
+ self.setup_smart_server_with_call_log()
+ # Make a branch with a single revision.
+ builder = self.make_branch_builder('foo')
+ builder.start_series()
+ builder.build_snapshot('first', None, [
+ ('add', ('', 'root-id', 'directory', ''))])
+ builder.finish_series()
+ branch = builder.get_branch()
+ repo = branch.repository
+ self.assertIsInstance(repo, RemoteRepository)
+ # Stop the server from sending extra results.
+ self.disableExtraResults()
+ repo.lock_read()
+ self.addCleanup(repo.unlock)
+ self.reset_smart_call_log()
+ graph = repo.get_graph()
+ # Query for 'first' and 'null:'. Because 'null:' is a parent of
+ # 'first' it will be a candidate for the stop_keys of subsequent
+ # requests, and because 'null:' was queried but not returned it will be
+ # cached as missing.
+ self.assertEqual({'first': ('null:',)},
+ graph.get_parent_map(['first', 'null:']))
+ # Now query for another key. This request will pass along a recipe of
+ # start and stop keys describing the already cached results, and this
+ # recipe's revision count must be correct (or else it will trigger an
+ # error from the server).
+ self.assertEqual({}, graph.get_parent_map(['another-key']))
+ # This assertion guards against disableExtraResults silently failing to
+ # work, thus invalidating the test.
+ self.assertLength(2, self.hpss_calls)
+
+ def test_get_parent_map_gets_ghosts_from_result(self):
+ # asking for a revision should negatively cache close ghosts in its
+ # ancestry.
+ self.setup_smart_server_with_call_log()
+ tree = self.make_branch_and_memory_tree('foo')
+ tree.lock_write()
+ try:
+ builder = treebuilder.TreeBuilder()
+ builder.start_tree(tree)
+ builder.build([])
+ builder.finish_tree()
+ tree.set_parent_ids(['non-existant'], allow_leftmost_as_ghost=True)
+ rev_id = tree.commit('')
+ finally:
+ tree.unlock()
+ tree.lock_read()
+ self.addCleanup(tree.unlock)
+ repo = tree.branch.repository
+ self.assertIsInstance(repo, RemoteRepository)
+ # ask for rev_id
+ repo.get_parent_map([rev_id])
+ self.reset_smart_call_log()
+ # Now asking for rev_id's ghost parent should not make calls
+ self.assertEqual({}, repo.get_parent_map(['non-existant']))
+ self.assertLength(0, self.hpss_calls)
+
+ def test_exposes_get_cached_parent_map(self):
+ """RemoteRepository exposes get_cached_parent_map from
+ _unstacked_provider
+ """
+ r1 = u'\u0e33'.encode('utf8')
+ r2 = u'\u0dab'.encode('utf8')
+ lines = [' '.join([r2, r1]), r1]
+ encoded_body = bz2.compress('\n'.join(lines))
+
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(encoded_body, 'ok')
+ repo.lock_read()
+ # get_cached_parent_map should *not* trigger an RPC
+ self.assertEqual({}, repo.get_cached_parent_map([r1]))
+ self.assertEqual([], client._calls)
+ self.assertEqual({r2: (r1,)}, repo.get_parent_map([r2]))
+ self.assertEqual({r1: (NULL_REVISION,)},
+ repo.get_cached_parent_map([r1]))
+ self.assertEqual(
+ [('call_with_body_bytes_expecting_body',
+ 'Repository.get_parent_map', ('quack/', 'include-missing:', r2),
+ '\n\n0')],
+ client._calls)
+ repo.unlock()
+
+
+class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
+
+ def test_allows_new_revisions(self):
+ """get_parent_map's results can be updated by commit."""
+ smart_server = test_server.SmartTCPServer_for_testing()
+ self.start_server(smart_server)
+ self.make_branch('branch')
+ branch = Branch.open(smart_server.get_url() + '/branch')
+ tree = branch.create_checkout('tree', lightweight=True)
+ tree.lock_write()
+ self.addCleanup(tree.unlock)
+ graph = tree.branch.repository.get_graph()
+ # This provides an opportunity for the missing rev-id to be cached.
+ self.assertEqual({}, graph.get_parent_map(['rev1']))
+ tree.commit('message', rev_id='rev1')
+ graph = tree.branch.repository.get_graph()
+ self.assertEqual({'rev1': ('null:',)}, graph.get_parent_map(['rev1']))
+
+
+class TestRepositoryGetRevisions(TestRemoteRepository):
+
+ def test_hpss_missing_revision(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(
+ '', 'ok', '10')
+ self.assertRaises(errors.NoSuchRevision, repo.get_revisions,
+ ['somerev1', 'anotherrev2'])
+ self.assertEqual(
+ [('call_with_body_bytes_expecting_body', 'Repository.iter_revisions',
+ ('quack/', ), "somerev1\nanotherrev2")],
+ client._calls)
+
+ def test_hpss_get_single_revision(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ somerev1 = Revision("somerev1")
+ somerev1.committer = "Joe Committer <joe@example.com>"
+ somerev1.timestamp = 1321828927
+ somerev1.timezone = -60
+ somerev1.inventory_sha1 = "691b39be74c67b1212a75fcb19c433aaed903c2b"
+ somerev1.message = "Message"
+ body = zlib.compress(chk_bencode_serializer.write_revision_to_string(
+ somerev1))
+ # Split up body into two bits to make sure the zlib compression object
+ # gets data fed twice.
+ client.add_success_response_with_body(
+ [body[:10], body[10:]], 'ok', '10')
+ revs = repo.get_revisions(['somerev1'])
+ self.assertEquals(revs, [somerev1])
+ self.assertEqual(
+ [('call_with_body_bytes_expecting_body', 'Repository.iter_revisions',
+ ('quack/', ), "somerev1")],
+ client._calls)
+
+
+class TestRepositoryGetRevisionGraph(TestRemoteRepository):
+
+ def test_null_revision(self):
+ # a null revision has the predictable result {}, we should have no wire
+ # traffic when calling it with this argument
+ transport_path = 'empty'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('notused')
+ # actual RemoteRepository.get_revision_graph is gone, but there's an
+ # equivalent private method for testing
+ result = repo._get_revision_graph(NULL_REVISION)
+ self.assertEqual([], client._calls)
+ self.assertEqual({}, result)
+
+ def test_none_revision(self):
+ # with none we want the entire graph
+ r1 = u'\u0e33'.encode('utf8')
+ r2 = u'\u0dab'.encode('utf8')
+ lines = [' '.join([r2, r1]), r1]
+ encoded_body = '\n'.join(lines)
+
+ transport_path = 'sinhala'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(encoded_body, 'ok')
+ # actual RemoteRepository.get_revision_graph is gone, but there's an
+ # equivalent private method for testing
+ result = repo._get_revision_graph(None)
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.get_revision_graph',
+ ('sinhala/', ''))],
+ client._calls)
+ self.assertEqual({r1: (), r2: (r1, )}, result)
+
+ def test_specific_revision(self):
+ # with a specific revision we want the graph for that
+ # with none we want the entire graph
+ r11 = u'\u0e33'.encode('utf8')
+ r12 = u'\xc9'.encode('utf8')
+ r2 = u'\u0dab'.encode('utf8')
+ lines = [' '.join([r2, r11, r12]), r11, r12]
+ encoded_body = '\n'.join(lines)
+
+ transport_path = 'sinhala'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(encoded_body, 'ok')
+ result = repo._get_revision_graph(r2)
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.get_revision_graph',
+ ('sinhala/', r2))],
+ client._calls)
+ self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
+
+ def test_no_such_revision(self):
+ revid = '123'
+ transport_path = 'sinhala'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_error_response('nosuchrevision', revid)
+ # also check that the right revision is reported in the error
+ self.assertRaises(errors.NoSuchRevision,
+ repo._get_revision_graph, revid)
+ self.assertEqual(
+ [('call_expecting_body', 'Repository.get_revision_graph',
+ ('sinhala/', revid))],
+ client._calls)
+
+ def test_unexpected_error(self):
+ revid = '123'
+ transport_path = 'sinhala'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_error_response('AnUnexpectedError')
+ e = self.assertRaises(errors.UnknownErrorFromSmartServer,
+ repo._get_revision_graph, revid)
+ self.assertEqual(('AnUnexpectedError',), e.error_tuple)
+
+
+class TestRepositoryGetRevIdForRevno(TestRemoteRepository):
+
+ def test_ok(self):
+ repo, client = self.setup_fake_client_and_repository('quack')
+ client.add_expected_call(
+ 'Repository.get_rev_id_for_revno', ('quack/', 5, (42, 'rev-foo')),
+ 'success', ('ok', 'rev-five'))
+ result = repo.get_rev_id_for_revno(5, (42, 'rev-foo'))
+ self.assertEqual((True, 'rev-five'), result)
+ self.assertFinished(client)
+
+ def test_history_incomplete(self):
+ repo, client = self.setup_fake_client_and_repository('quack')
+ client.add_expected_call(
+ 'Repository.get_rev_id_for_revno', ('quack/', 5, (42, 'rev-foo')),
+ 'success', ('history-incomplete', 10, 'rev-ten'))
+ result = repo.get_rev_id_for_revno(5, (42, 'rev-foo'))
+ self.assertEqual((False, (10, 'rev-ten')), result)
+ self.assertFinished(client)
+
+ def test_history_incomplete_with_fallback(self):
+ """A 'history-incomplete' response causes the fallback repository to be
+ queried too, if one is set.
+ """
+ # Make a repo with a fallback repo, both using a FakeClient.
+ format = remote.response_tuple_to_repo_format(
+ ('yes', 'no', 'yes', self.get_repo_format().network_name()))
+ repo, client = self.setup_fake_client_and_repository('quack')
+ repo._format = format
+ fallback_repo, ignored = self.setup_fake_client_and_repository(
+ 'fallback')
+ fallback_repo._client = client
+ fallback_repo._format = format
+ repo.add_fallback_repository(fallback_repo)
+ # First the client should ask the primary repo
+ client.add_expected_call(
+ 'Repository.get_rev_id_for_revno', ('quack/', 1, (42, 'rev-foo')),
+ 'success', ('history-incomplete', 2, 'rev-two'))
+ # Then it should ask the fallback, using revno/revid from the
+ # history-incomplete response as the known revno/revid.
+ client.add_expected_call(
+ 'Repository.get_rev_id_for_revno',('fallback/', 1, (2, 'rev-two')),
+ 'success', ('ok', 'rev-one'))
+ result = repo.get_rev_id_for_revno(1, (42, 'rev-foo'))
+ self.assertEqual((True, 'rev-one'), result)
+ self.assertFinished(client)
+
+ def test_nosuchrevision(self):
+ # 'nosuchrevision' is returned when the known-revid is not found in the
+ # remote repo. The client translates that response to NoSuchRevision.
+ repo, client = self.setup_fake_client_and_repository('quack')
+ client.add_expected_call(
+ 'Repository.get_rev_id_for_revno', ('quack/', 5, (42, 'rev-foo')),
+ 'error', ('nosuchrevision', 'rev-foo'))
+ self.assertRaises(
+ errors.NoSuchRevision,
+ repo.get_rev_id_for_revno, 5, (42, 'rev-foo'))
+ self.assertFinished(client)
+
+ def test_branch_fallback_locking(self):
+ """RemoteBranch.get_rev_id takes a read lock, and tries to call the
+ get_rev_id_for_revno verb. If the verb is unknown the VFS fallback
+ will be invoked, which will fail if the repo is unlocked.
+ """
+ self.setup_smart_server_with_call_log()
+ tree = self.make_branch_and_memory_tree('.')
+ tree.lock_write()
+ tree.add('')
+ rev1 = tree.commit('First')
+ rev2 = tree.commit('Second')
+ tree.unlock()
+ branch = tree.branch
+ self.assertFalse(branch.is_locked())
+ self.reset_smart_call_log()
+ verb = 'Repository.get_rev_id_for_revno'
+ self.disable_verb(verb)
+ self.assertEqual(rev1, branch.get_rev_id(1))
+ self.assertLength(1, [call for call in self.hpss_calls if
+ call.call.method == verb])
+
+
+class TestRepositoryHasSignatureForRevisionId(TestRemoteRepository):
+
+ def test_has_signature_for_revision_id(self):
+ # ('yes', ) for Repository.has_signature_for_revision_id -> 'True'.
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('yes')
+ result = repo.has_signature_for_revision_id('A')
+ self.assertEqual(
+ [('call', 'Repository.has_signature_for_revision_id',
+ ('quack/', 'A'))],
+ client._calls)
+ self.assertEqual(True, result)
+
+ def test_is_not_shared(self):
+ # ('no', ) for Repository.has_signature_for_revision_id -> 'False'.
+ transport_path = 'qwack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('no')
+ result = repo.has_signature_for_revision_id('A')
+ self.assertEqual(
+ [('call', 'Repository.has_signature_for_revision_id',
+ ('qwack/', 'A'))],
+ client._calls)
+ self.assertEqual(False, result)
+
+
+class TestRepositoryPhysicalLockStatus(TestRemoteRepository):
+
+ def test_get_physical_lock_status_yes(self):
+ transport_path = 'qwack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('yes')
+ result = repo.get_physical_lock_status()
+ self.assertEqual(
+ [('call', 'Repository.get_physical_lock_status',
+ ('qwack/', ))],
+ client._calls)
+ self.assertEqual(True, result)
+
+ def test_get_physical_lock_status_no(self):
+ transport_path = 'qwack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('no')
+ result = repo.get_physical_lock_status()
+ self.assertEqual(
+ [('call', 'Repository.get_physical_lock_status',
+ ('qwack/', ))],
+ client._calls)
+ self.assertEqual(False, result)
+
+
+class TestRepositoryIsShared(TestRemoteRepository):
+
+ def test_is_shared(self):
+ # ('yes', ) for Repository.is_shared -> 'True'.
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('yes')
+ result = repo.is_shared()
+ self.assertEqual(
+ [('call', 'Repository.is_shared', ('quack/',))],
+ client._calls)
+ self.assertEqual(True, result)
+
+ def test_is_not_shared(self):
+ # ('no', ) for Repository.is_shared -> 'False'.
+ transport_path = 'qwack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('no')
+ result = repo.is_shared()
+ self.assertEqual(
+ [('call', 'Repository.is_shared', ('qwack/',))],
+ client._calls)
+ self.assertEqual(False, result)
+
+
+class TestRepositoryMakeWorkingTrees(TestRemoteRepository):
+
+ def test_make_working_trees(self):
+ # ('yes', ) for Repository.make_working_trees -> 'True'.
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('yes')
+ result = repo.make_working_trees()
+ self.assertEqual(
+ [('call', 'Repository.make_working_trees', ('quack/',))],
+ client._calls)
+ self.assertEqual(True, result)
+
+ def test_no_working_trees(self):
+ # ('no', ) for Repository.make_working_trees -> 'False'.
+ transport_path = 'qwack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('no')
+ result = repo.make_working_trees()
+ self.assertEqual(
+ [('call', 'Repository.make_working_trees', ('qwack/',))],
+ client._calls)
+ self.assertEqual(False, result)
+
+
+class TestRepositoryLockWrite(TestRemoteRepository):
+
+ def test_lock_write(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('ok', 'a token')
+ token = repo.lock_write().repository_token
+ self.assertEqual(
+ [('call', 'Repository.lock_write', ('quack/', ''))],
+ client._calls)
+ self.assertEqual('a token', token)
+
+ def test_lock_write_already_locked(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_error_response('LockContention')
+ self.assertRaises(errors.LockContention, repo.lock_write)
+ self.assertEqual(
+ [('call', 'Repository.lock_write', ('quack/', ''))],
+ client._calls)
+
+ def test_lock_write_unlockable(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_error_response('UnlockableTransport')
+ self.assertRaises(errors.UnlockableTransport, repo.lock_write)
+ self.assertEqual(
+ [('call', 'Repository.lock_write', ('quack/', ''))],
+ client._calls)
+
+
+class TestRepositoryWriteGroups(TestRemoteRepository):
+
+ def test_start_write_group(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.start_write_group', ('quack/', 'a token'),
+ 'success', ('ok', ('token1', )))
+ repo.lock_write()
+ repo.start_write_group()
+
+ def test_start_write_group_unsuspendable(self):
+ # Some repositories do not support suspending write
+ # groups. For those, fall back to the "real" repository.
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ def stub_ensure_real():
+ client._calls.append(('_ensure_real',))
+ repo._real_repository = _StubRealPackRepository(client._calls)
+ repo._ensure_real = stub_ensure_real
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.start_write_group', ('quack/', 'a token'),
+ 'error', ('UnsuspendableWriteGroup',))
+ repo.lock_write()
+ repo.start_write_group()
+ self.assertEquals(client._calls[-2:], [
+ ('_ensure_real',),
+ ('start_write_group',)])
+
+ def test_commit_write_group(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.start_write_group', ('quack/', 'a token'),
+ 'success', ('ok', ['token1']))
+ client.add_expected_call(
+ 'Repository.commit_write_group', ('quack/', 'a token', ['token1']),
+ 'success', ('ok',))
+ repo.lock_write()
+ repo.start_write_group()
+ repo.commit_write_group()
+
+ def test_abort_write_group(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.start_write_group', ('quack/', 'a token'),
+ 'success', ('ok', ['token1']))
+ client.add_expected_call(
+ 'Repository.abort_write_group', ('quack/', 'a token', ['token1']),
+ 'success', ('ok',))
+ repo.lock_write()
+ repo.start_write_group()
+ repo.abort_write_group(False)
+
+ def test_suspend_write_group(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ self.assertEquals([], repo.suspend_write_group())
+
+ def test_resume_write_group(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.check_write_group', ('quack/', 'a token', ['token1']),
+ 'success', ('ok',))
+ repo.lock_write()
+ repo.resume_write_group(['token1'])
+
+
+class TestRepositorySetMakeWorkingTrees(TestRemoteRepository):
+
+ def test_backwards_compat(self):
+ self.setup_smart_server_with_call_log()
+ repo = self.make_repository('.')
+ self.reset_smart_call_log()
+ verb = 'Repository.set_make_working_trees'
+ self.disable_verb(verb)
+ repo.set_make_working_trees(True)
+ call_count = len([call for call in self.hpss_calls if
+ call.call.method == verb])
+ self.assertEqual(1, call_count)
+
+ def test_current(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.set_make_working_trees', ('quack/', 'True'),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.set_make_working_trees', ('quack/', 'False'),
+ 'success', ('ok',))
+ repo.set_make_working_trees(True)
+ repo.set_make_working_trees(False)
+
+
+class TestRepositoryUnlock(TestRemoteRepository):
+
+ def test_unlock(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('ok', 'a token')
+ client.add_success_response('ok')
+ repo.lock_write()
+ repo.unlock()
+ self.assertEqual(
+ [('call', 'Repository.lock_write', ('quack/', '')),
+ ('call', 'Repository.unlock', ('quack/', 'a token'))],
+ client._calls)
+
+ def test_unlock_wrong_token(self):
+ # If somehow the token is wrong, unlock will raise TokenMismatch.
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response('ok', 'a token')
+ client.add_error_response('TokenMismatch')
+ repo.lock_write()
+ self.assertRaises(errors.TokenMismatch, repo.unlock)
+
+
+class TestRepositoryHasRevision(TestRemoteRepository):
+
+ def test_none(self):
+ # repo.has_revision(None) should not cause any traffic.
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+
+ # The null revision is always there, so has_revision(None) == True.
+ self.assertEqual(True, repo.has_revision(NULL_REVISION))
+
+ # The remote repo shouldn't be accessed.
+ self.assertEqual([], client._calls)
+
+
+class TestRepositoryIterFilesBytes(TestRemoteRepository):
+ """Test Repository.iter_file_bytes."""
+
+ def test_single(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.iter_files_bytes', ('quack/', ),
+ 'success', ('ok',), iter(["ok\x000", "\n", zlib.compress("mydata" * 10)]))
+ for (identifier, byte_stream) in repo.iter_files_bytes([("somefile",
+ "somerev", "myid")]):
+ self.assertEquals("myid", identifier)
+ self.assertEquals("".join(byte_stream), "mydata" * 10)
+
+ def test_missing(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.iter_files_bytes',
+ ('quack/', ),
+ 'error', ('RevisionNotPresent', 'somefile', 'somerev'),
+ iter(["absent\0somefile\0somerev\n"]))
+ self.assertRaises(errors.RevisionNotPresent, list,
+ repo.iter_files_bytes(
+ [("somefile", "somerev", "myid")]))
+
+
+class TestRepositoryInsertStreamBase(TestRemoteRepository):
+ """Base class for Repository.insert_stream and .insert_stream_1.19
+ tests.
+ """
+
+ def checkInsertEmptyStream(self, repo, client):
+ """Insert an empty stream, checking the result.
+
+ This checks that there are no resume_tokens or missing_keys, and that
+ the client is finished.
+ """
+ sink = repo._get_sink()
+ fmt = repository.format_registry.get_default()
+ resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
+ self.assertEqual([], resume_tokens)
+ self.assertEqual(set(), missing_keys)
+ self.assertFinished(client)
+
+
+class TestRepositoryInsertStream(TestRepositoryInsertStreamBase):
+ """Tests for using Repository.insert_stream verb when the _1.19 variant is
+ not available.
+
+ This test case is very similar to TestRepositoryInsertStream_1_19.
+ """
+
+ def setUp(self):
+ TestRemoteRepository.setUp(self)
+ self.disable_verb('Repository.insert_stream_1.19')
+
+ def test_unlocked_repo(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', ''),
+ 'unknown', ('Repository.insert_stream_1.19',))
+ client.add_expected_call(
+ 'Repository.insert_stream', ('quack/', ''),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.insert_stream', ('quack/', ''),
+ 'success', ('ok',))
+ self.checkInsertEmptyStream(repo, client)
+
+ def test_locked_repo_with_no_lock_token(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', ''))
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', ''),
+ 'unknown', ('Repository.insert_stream_1.19',))
+ client.add_expected_call(
+ 'Repository.insert_stream', ('quack/', ''),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.insert_stream', ('quack/', ''),
+ 'success', ('ok',))
+ repo.lock_write()
+ self.checkInsertEmptyStream(repo, client)
+
+ def test_locked_repo_with_lock_token(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', '', 'a token'),
+ 'unknown', ('Repository.insert_stream_1.19',))
+ client.add_expected_call(
+ 'Repository.insert_stream_locked', ('quack/', '', 'a token'),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.insert_stream_locked', ('quack/', '', 'a token'),
+ 'success', ('ok',))
+ repo.lock_write()
+ self.checkInsertEmptyStream(repo, client)
+
+ def test_stream_with_inventory_deltas(self):
+ """'inventory-deltas' substreams cannot be sent to the
+ Repository.insert_stream verb, because not all servers that implement
+ that verb will accept them. So when one is encountered the RemoteSink
+ immediately stops using that verb and falls back to VFS insert_stream.
+ """
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', ''),
+ 'unknown', ('Repository.insert_stream_1.19',))
+ client.add_expected_call(
+ 'Repository.insert_stream', ('quack/', ''),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.insert_stream', ('quack/', ''),
+ 'success', ('ok',))
+ # Create a fake real repository for insert_stream to fall back on, so
+ # that we can directly see the records the RemoteSink passes to the
+ # real sink.
+ class FakeRealSink:
+ def __init__(self):
+ self.records = []
+ def insert_stream(self, stream, src_format, resume_tokens):
+ for substream_kind, substream in stream:
+ self.records.append(
+ (substream_kind, [record.key for record in substream]))
+ return ['fake tokens'], ['fake missing keys']
+ fake_real_sink = FakeRealSink()
+ class FakeRealRepository:
+ def _get_sink(self):
+ return fake_real_sink
+ def is_in_write_group(self):
+ return False
+ def refresh_data(self):
+ return True
+ repo._real_repository = FakeRealRepository()
+ sink = repo._get_sink()
+ fmt = repository.format_registry.get_default()
+ stream = self.make_stream_with_inv_deltas(fmt)
+ resume_tokens, missing_keys = sink.insert_stream(stream, fmt, [])
+ # Every record from the first inventory delta should have been sent to
+ # the VFS sink.
+ expected_records = [
+ ('inventory-deltas', [('rev2',), ('rev3',)]),
+ ('texts', [('some-rev', 'some-file')])]
+ self.assertEqual(expected_records, fake_real_sink.records)
+ # The return values from the real sink's insert_stream are propagated
+ # back to the original caller.
+ self.assertEqual(['fake tokens'], resume_tokens)
+ self.assertEqual(['fake missing keys'], missing_keys)
+ self.assertFinished(client)
+
+ def make_stream_with_inv_deltas(self, fmt):
+ """Make a simple stream with an inventory delta followed by more
+ records and more substreams to test that all records and substreams
+ from that point on are used.
+
+ This sends, in order:
+ * inventories substream: rev1, rev2, rev3. rev2 and rev3 are
+ inventory-deltas.
+ * texts substream: (some-rev, some-file)
+ """
+ # Define a stream using generators so that it isn't rewindable.
+ inv = inventory.Inventory(revision_id='rev1')
+ inv.root.revision = 'rev1'
+ def stream_with_inv_delta():
+ yield ('inventories', inventories_substream())
+ yield ('inventory-deltas', inventory_delta_substream())
+ yield ('texts', [
+ versionedfile.FulltextContentFactory(
+ ('some-rev', 'some-file'), (), None, 'content')])
+ def inventories_substream():
+ # An empty inventory fulltext. This will be streamed normally.
+ text = fmt._serializer.write_inventory_to_string(inv)
+ yield versionedfile.FulltextContentFactory(
+ ('rev1',), (), None, text)
+ def inventory_delta_substream():
+ # An inventory delta. This can't be streamed via this verb, so it
+ # will trigger a fallback to VFS insert_stream.
+ entry = inv.make_entry(
+ 'directory', 'newdir', inv.root.file_id, 'newdir-id')
+ entry.revision = 'ghost'
+ delta = [(None, 'newdir', 'newdir-id', entry)]
+ serializer = inventory_delta.InventoryDeltaSerializer(
+ versioned_root=True, tree_references=False)
+ lines = serializer.delta_to_lines('rev1', 'rev2', delta)
+ yield versionedfile.ChunkedContentFactory(
+ ('rev2',), (('rev1',)), None, lines)
+ # Another delta.
+ lines = serializer.delta_to_lines('rev1', 'rev3', delta)
+ yield versionedfile.ChunkedContentFactory(
+ ('rev3',), (('rev1',)), None, lines)
+ return stream_with_inv_delta()
+
+
+class TestRepositoryInsertStream_1_19(TestRepositoryInsertStreamBase):
+
+ def test_unlocked_repo(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', ''),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', ''),
+ 'success', ('ok',))
+ self.checkInsertEmptyStream(repo, client)
+
+ def test_locked_repo_with_no_lock_token(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', ''))
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', ''),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', ''),
+ 'success', ('ok',))
+ repo.lock_write()
+ self.checkInsertEmptyStream(repo, client)
+
+ def test_locked_repo_with_lock_token(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'a token'))
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', '', 'a token'),
+ 'success', ('ok',))
+ client.add_expected_call(
+ 'Repository.insert_stream_1.19', ('quack/', '', 'a token'),
+ 'success', ('ok',))
+ repo.lock_write()
+ self.checkInsertEmptyStream(repo, client)
+
+
+class TestRepositoryTarball(TestRemoteRepository):
+
+ # This is a canned tarball reponse we can validate against
+ tarball_content = (
+ 'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
+ 'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
+ 'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
+ 'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
+ 'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
+ 'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
+ 'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
+ 'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
+ '0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
+ 'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
+ 'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
+ 'nWQ7QH/F3JFOFCQ0aSPfA='
+ ).decode('base64')
+
+ def test_repository_tarball(self):
+ # Test that Repository.tarball generates the right operations
+ transport_path = 'repo'
+ expected_calls = [('call_expecting_body', 'Repository.tarball',
+ ('repo/', 'bz2',),),
+ ]
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_success_response_with_body(self.tarball_content, 'ok')
+ # Now actually ask for the tarball
+ tarball_file = repo._get_tarball('bz2')
+ try:
+ self.assertEqual(expected_calls, client._calls)
+ self.assertEqual(self.tarball_content, tarball_file.read())
+ finally:
+ tarball_file.close()
+
+
+class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
+ """RemoteRepository.copy_content_into optimizations"""
+
+ def test_copy_content_remote_to_local(self):
+ self.transport_server = test_server.SmartTCPServer_for_testing
+ src_repo = self.make_repository('repo1')
+ src_repo = repository.Repository.open(self.get_url('repo1'))
+ # At the moment the tarball-based copy_content_into can't write back
+ # into a smart server. It would be good if it could upload the
+ # tarball; once that works we'd have to create repositories of
+ # different formats. -- mbp 20070410
+ dest_url = self.get_vfs_only_url('repo2')
+ dest_bzrdir = BzrDir.create(dest_url)
+ dest_repo = dest_bzrdir.create_repository()
+ self.assertFalse(isinstance(dest_repo, RemoteRepository))
+ self.assertTrue(isinstance(src_repo, RemoteRepository))
+ src_repo.copy_content_into(dest_repo)
+
+
+class _StubRealPackRepository(object):
+
+ def __init__(self, calls):
+ self.calls = calls
+ self._pack_collection = _StubPackCollection(calls)
+
+ def start_write_group(self):
+ self.calls.append(('start_write_group',))
+
+ def is_in_write_group(self):
+ return False
+
+ def refresh_data(self):
+ self.calls.append(('pack collection reload_pack_names',))
+
+
+class _StubPackCollection(object):
+
+ def __init__(self, calls):
+ self.calls = calls
+
+ def autopack(self):
+ self.calls.append(('pack collection autopack',))
+
+
+class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
+ """Tests for RemoteRepository.autopack implementation."""
+
+ def test_ok(self):
+ """When the server returns 'ok' and there's no _real_repository, then
+ nothing else happens: the autopack method is done.
+ """
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'PackRepository.autopack', ('quack/',), 'success', ('ok',))
+ repo.autopack()
+ self.assertFinished(client)
+
+ def test_ok_with_real_repo(self):
+ """When the server returns 'ok' and there is a _real_repository, then
+ the _real_repository's reload_pack_name's method will be called.
+ """
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'PackRepository.autopack', ('quack/',),
+ 'success', ('ok',))
+ repo._real_repository = _StubRealPackRepository(client._calls)
+ repo.autopack()
+ self.assertEqual(
+ [('call', 'PackRepository.autopack', ('quack/',)),
+ ('pack collection reload_pack_names',)],
+ client._calls)
+
+ def test_backwards_compatibility(self):
+ """If the server does not recognise the PackRepository.autopack verb,
+ fallback to the real_repository's implementation.
+ """
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_unknown_method_response('PackRepository.autopack')
+ def stub_ensure_real():
+ client._calls.append(('_ensure_real',))
+ repo._real_repository = _StubRealPackRepository(client._calls)
+ repo._ensure_real = stub_ensure_real
+ repo.autopack()
+ self.assertEqual(
+ [('call', 'PackRepository.autopack', ('quack/',)),
+ ('_ensure_real',),
+ ('pack collection autopack',)],
+ client._calls)
+
+ def test_oom_error_reporting(self):
+ """An out-of-memory condition on the server is reported clearly"""
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'PackRepository.autopack', ('quack/',),
+ 'error', ('MemoryError',))
+ err = self.assertRaises(errors.BzrError, repo.autopack)
+ self.assertContainsRe(str(err), "^remote server out of mem")
+
+
+class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
+ """Base class for unit tests for bzrlib.remote._translate_error."""
+
+ def translateTuple(self, error_tuple, **context):
+ """Call _translate_error with an ErrorFromSmartServer built from the
+ given error_tuple.
+
+ :param error_tuple: A tuple of a smart server response, as would be
+ passed to an ErrorFromSmartServer.
+ :kwargs context: context items to call _translate_error with.
+
+ :returns: The error raised by _translate_error.
+ """
+ # Raise the ErrorFromSmartServer before passing it as an argument,
+ # because _translate_error may need to re-raise it with a bare 'raise'
+ # statement.
+ server_error = errors.ErrorFromSmartServer(error_tuple)
+ translated_error = self.translateErrorFromSmartServer(
+ server_error, **context)
+ return translated_error
+
+ def translateErrorFromSmartServer(self, error_object, **context):
+ """Like translateTuple, but takes an already constructed
+ ErrorFromSmartServer rather than a tuple.
+ """
+ try:
+ raise error_object
+ except errors.ErrorFromSmartServer, server_error:
+ translated_error = self.assertRaises(
+ errors.BzrError, remote._translate_error, server_error,
+ **context)
+ return translated_error
+
+
+class TestErrorTranslationSuccess(TestErrorTranslationBase):
+ """Unit tests for bzrlib.remote._translate_error.
+
+ Given an ErrorFromSmartServer (which has an error tuple from a smart
+ server) and some context, _translate_error raises more specific errors from
+ bzrlib.errors.
+
+ This test case covers the cases where _translate_error succeeds in
+ translating an ErrorFromSmartServer to something better. See
+ TestErrorTranslationRobustness for other cases.
+ """
+
+ def test_NoSuchRevision(self):
+ branch = self.make_branch('')
+ revid = 'revid'
+ translated_error = self.translateTuple(
+ ('NoSuchRevision', revid), branch=branch)
+ expected_error = errors.NoSuchRevision(branch, revid)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_nosuchrevision(self):
+ repository = self.make_repository('')
+ revid = 'revid'
+ translated_error = self.translateTuple(
+ ('nosuchrevision', revid), repository=repository)
+ expected_error = errors.NoSuchRevision(repository, revid)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_nobranch(self):
+ bzrdir = self.make_bzrdir('')
+ translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
+ expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_nobranch_one_arg(self):
+ bzrdir = self.make_bzrdir('')
+ translated_error = self.translateTuple(
+ ('nobranch', 'extra detail'), bzrdir=bzrdir)
+ expected_error = errors.NotBranchError(
+ path=bzrdir.root_transport.base,
+ detail='extra detail')
+ self.assertEqual(expected_error, translated_error)
+
+ def test_norepository(self):
+ bzrdir = self.make_bzrdir('')
+ translated_error = self.translateTuple(('norepository',),
+ bzrdir=bzrdir)
+ expected_error = errors.NoRepositoryPresent(bzrdir)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_LockContention(self):
+ translated_error = self.translateTuple(('LockContention',))
+ expected_error = errors.LockContention('(remote lock)')
+ self.assertEqual(expected_error, translated_error)
+
+ def test_UnlockableTransport(self):
+ bzrdir = self.make_bzrdir('')
+ translated_error = self.translateTuple(
+ ('UnlockableTransport',), bzrdir=bzrdir)
+ expected_error = errors.UnlockableTransport(bzrdir.root_transport)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_LockFailed(self):
+ lock = 'str() of a server lock'
+ why = 'str() of why'
+ translated_error = self.translateTuple(('LockFailed', lock, why))
+ expected_error = errors.LockFailed(lock, why)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_TokenMismatch(self):
+ token = 'a lock token'
+ translated_error = self.translateTuple(('TokenMismatch',), token=token)
+ expected_error = errors.TokenMismatch(token, '(remote token)')
+ self.assertEqual(expected_error, translated_error)
+
+ def test_Diverged(self):
+ branch = self.make_branch('a')
+ other_branch = self.make_branch('b')
+ translated_error = self.translateTuple(
+ ('Diverged',), branch=branch, other_branch=other_branch)
+ expected_error = errors.DivergedBranches(branch, other_branch)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_NotStacked(self):
+ branch = self.make_branch('')
+ translated_error = self.translateTuple(('NotStacked',), branch=branch)
+ expected_error = errors.NotStacked(branch)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_ReadError_no_args(self):
+ path = 'a path'
+ translated_error = self.translateTuple(('ReadError',), path=path)
+ expected_error = errors.ReadError(path)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_ReadError(self):
+ path = 'a path'
+ translated_error = self.translateTuple(('ReadError', path))
+ expected_error = errors.ReadError(path)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_IncompatibleRepositories(self):
+ translated_error = self.translateTuple(('IncompatibleRepositories',
+ "repo1", "repo2", "details here"))
+ expected_error = errors.IncompatibleRepositories("repo1", "repo2",
+ "details here")
+ self.assertEqual(expected_error, translated_error)
+
+ def test_PermissionDenied_no_args(self):
+ path = 'a path'
+ translated_error = self.translateTuple(('PermissionDenied',),
+ path=path)
+ expected_error = errors.PermissionDenied(path)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_PermissionDenied_one_arg(self):
+ path = 'a path'
+ translated_error = self.translateTuple(('PermissionDenied', path))
+ expected_error = errors.PermissionDenied(path)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_PermissionDenied_one_arg_and_context(self):
+ """Given a choice between a path from the local context and a path on
+ the wire, _translate_error prefers the path from the local context.
+ """
+ local_path = 'local path'
+ remote_path = 'remote path'
+ translated_error = self.translateTuple(
+ ('PermissionDenied', remote_path), path=local_path)
+ expected_error = errors.PermissionDenied(local_path)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_PermissionDenied_two_args(self):
+ path = 'a path'
+ extra = 'a string with extra info'
+ translated_error = self.translateTuple(
+ ('PermissionDenied', path, extra))
+ expected_error = errors.PermissionDenied(path, extra)
+ self.assertEqual(expected_error, translated_error)
+
+ # GZ 2011-03-02: TODO test for PermissionDenied with non-ascii 'extra'
+
+ def test_NoSuchFile_context_path(self):
+ local_path = "local path"
+ translated_error = self.translateTuple(('ReadError', "remote path"),
+ path=local_path)
+ expected_error = errors.ReadError(local_path)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_NoSuchFile_without_context(self):
+ remote_path = "remote path"
+ translated_error = self.translateTuple(('ReadError', remote_path))
+ expected_error = errors.ReadError(remote_path)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_ReadOnlyError(self):
+ translated_error = self.translateTuple(('ReadOnlyError',))
+ expected_error = errors.TransportNotPossible("readonly transport")
+ self.assertEqual(expected_error, translated_error)
+
+ def test_MemoryError(self):
+ translated_error = self.translateTuple(('MemoryError',))
+ self.assertStartsWith(str(translated_error),
+ "remote server out of memory")
+
+ def test_generic_IndexError_no_classname(self):
+ err = errors.ErrorFromSmartServer(('error', "list index out of range"))
+ translated_error = self.translateErrorFromSmartServer(err)
+ expected_error = errors.UnknownErrorFromSmartServer(err)
+ self.assertEqual(expected_error, translated_error)
+
+ # GZ 2011-03-02: TODO test generic non-ascii error string
+
+ def test_generic_KeyError(self):
+ err = errors.ErrorFromSmartServer(('error', 'KeyError', "1"))
+ translated_error = self.translateErrorFromSmartServer(err)
+ expected_error = errors.UnknownErrorFromSmartServer(err)
+ self.assertEqual(expected_error, translated_error)
+
+
+class TestErrorTranslationRobustness(TestErrorTranslationBase):
+ """Unit tests for bzrlib.remote._translate_error's robustness.
+
+ TestErrorTranslationSuccess is for cases where _translate_error can
+ translate successfully. This class about how _translate_err behaves when
+ it fails to translate: it re-raises the original error.
+ """
+
+ def test_unrecognised_server_error(self):
+ """If the error code from the server is not recognised, the original
+ ErrorFromSmartServer is propagated unmodified.
+ """
+ error_tuple = ('An unknown error tuple',)
+ server_error = errors.ErrorFromSmartServer(error_tuple)
+ translated_error = self.translateErrorFromSmartServer(server_error)
+ expected_error = errors.UnknownErrorFromSmartServer(server_error)
+ self.assertEqual(expected_error, translated_error)
+
+ def test_context_missing_a_key(self):
+ """In case of a bug in the client, or perhaps an unexpected response
+ from a server, _translate_error returns the original error tuple from
+ the server and mutters a warning.
+ """
+ # To translate a NoSuchRevision error _translate_error needs a 'branch'
+ # in the context dict. So let's give it an empty context dict instead
+ # to exercise its error recovery.
+ empty_context = {}
+ error_tuple = ('NoSuchRevision', 'revid')
+ server_error = errors.ErrorFromSmartServer(error_tuple)
+ translated_error = self.translateErrorFromSmartServer(server_error)
+ self.assertEqual(server_error, translated_error)
+ # In addition to re-raising ErrorFromSmartServer, some debug info has
+ # been muttered to the log file for developer to look at.
+ self.assertContainsRe(
+ self.get_log(),
+ "Missing key 'branch' in context")
+
+ def test_path_missing(self):
+ """Some translations (PermissionDenied, ReadError) can determine the
+ 'path' variable from either the wire or the local context. If neither
+ has it, then an error is raised.
+ """
+ error_tuple = ('ReadError',)
+ server_error = errors.ErrorFromSmartServer(error_tuple)
+ translated_error = self.translateErrorFromSmartServer(server_error)
+ self.assertEqual(server_error, translated_error)
+ # In addition to re-raising ErrorFromSmartServer, some debug info has
+ # been muttered to the log file for developer to look at.
+ self.assertContainsRe(self.get_log(), "Missing key 'path' in context")
+
+
+class TestStacking(tests.TestCaseWithTransport):
+ """Tests for operations on stacked remote repositories.
+
+ The underlying format type must support stacking.
+ """
+
+ def test_access_stacked_remote(self):
+ # based on <http://launchpad.net/bugs/261315>
+ # make a branch stacked on another repository containing an empty
+ # revision, then open it over hpss - we should be able to see that
+ # revision.
+ base_transport = self.get_transport()
+ base_builder = self.make_branch_builder('base', format='1.9')
+ base_builder.start_series()
+ base_revid = base_builder.build_snapshot('rev-id', None,
+ [('add', ('', None, 'directory', None))],
+ 'message')
+ base_builder.finish_series()
+ stacked_branch = self.make_branch('stacked', format='1.9')
+ stacked_branch.set_stacked_on_url('../base')
+ # start a server looking at this
+ smart_server = test_server.SmartTCPServer_for_testing()
+ self.start_server(smart_server)
+ remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
+ # can get its branch and repository
+ remote_branch = remote_bzrdir.open_branch()
+ remote_repo = remote_branch.repository
+ remote_repo.lock_read()
+ try:
+ # it should have an appropriate fallback repository, which should also
+ # be a RemoteRepository
+ self.assertLength(1, remote_repo._fallback_repositories)
+ self.assertIsInstance(remote_repo._fallback_repositories[0],
+ RemoteRepository)
+ # and it has the revision committed to the underlying repository;
+ # these have varying implementations so we try several of them
+ self.assertTrue(remote_repo.has_revisions([base_revid]))
+ self.assertTrue(remote_repo.has_revision(base_revid))
+ self.assertEqual(remote_repo.get_revision(base_revid).message,
+ 'message')
+ finally:
+ remote_repo.unlock()
+
+ def prepare_stacked_remote_branch(self):
+ """Get stacked_upon and stacked branches with content in each."""
+ self.setup_smart_server_with_call_log()
+ tree1 = self.make_branch_and_tree('tree1', format='1.9')
+ tree1.commit('rev1', rev_id='rev1')
+ tree2 = tree1.branch.bzrdir.sprout('tree2', stacked=True
+ ).open_workingtree()
+ local_tree = tree2.branch.create_checkout('local')
+ local_tree.commit('local changes make me feel good.')
+ branch2 = Branch.open(self.get_url('tree2'))
+ branch2.lock_read()
+ self.addCleanup(branch2.unlock)
+ return tree1.branch, branch2
+
+ def test_stacked_get_parent_map(self):
+ # the public implementation of get_parent_map obeys stacking
+ _, branch = self.prepare_stacked_remote_branch()
+ repo = branch.repository
+ self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
+
+ def test_unstacked_get_parent_map(self):
+ # _unstacked_provider.get_parent_map ignores stacking
+ _, branch = self.prepare_stacked_remote_branch()
+ provider = branch.repository._unstacked_provider
+ self.assertEqual([], provider.get_parent_map(['rev1']).keys())
+
+ def fetch_stream_to_rev_order(self, stream):
+ result = []
+ for kind, substream in stream:
+ if not kind == 'revisions':
+ list(substream)
+ else:
+ for content in substream:
+ result.append(content.key[-1])
+ return result
+
+ def get_ordered_revs(self, format, order, branch_factory=None):
+ """Get a list of the revisions in a stream to format format.
+
+ :param format: The format of the target.
+ :param order: the order that target should have requested.
+ :param branch_factory: A callable to create a trunk and stacked branch
+ to fetch from. If none, self.prepare_stacked_remote_branch is used.
+ :result: The revision ids in the stream, in the order seen,
+ the topological order of revisions in the source.
+ """
+ unordered_format = controldir.format_registry.get(format)()
+ target_repository_format = unordered_format.repository_format
+ # Cross check
+ self.assertEqual(order, target_repository_format._fetch_order)
+ if branch_factory is None:
+ branch_factory = self.prepare_stacked_remote_branch
+ _, stacked = branch_factory()
+ source = stacked.repository._get_source(target_repository_format)
+ tip = stacked.last_revision()
+ stacked.repository._ensure_real()
+ graph = stacked.repository.get_graph()
+ revs = [r for (r,ps) in graph.iter_ancestry([tip])
+ if r != NULL_REVISION]
+ revs.reverse()
+ search = vf_search.PendingAncestryResult([tip], stacked.repository)
+ self.reset_smart_call_log()
+ stream = source.get_stream(search)
+ # We trust that if a revision is in the stream the rest of the new
+ # content for it is too, as per our main fetch tests; here we are
+ # checking that the revisions are actually included at all, and their
+ # order.
+ return self.fetch_stream_to_rev_order(stream), revs
+
+ def test_stacked_get_stream_unordered(self):
+ # Repository._get_source.get_stream() from a stacked repository with
+ # unordered yields the full data from both stacked and stacked upon
+ # sources.
+ rev_ord, expected_revs = self.get_ordered_revs('1.9', 'unordered')
+ self.assertEqual(set(expected_revs), set(rev_ord))
+ # Getting unordered results should have made a streaming data request
+ # from the server, then one from the backing branch.
+ self.assertLength(2, self.hpss_calls)
+
+ def test_stacked_on_stacked_get_stream_unordered(self):
+ # Repository._get_source.get_stream() from a stacked repository which
+ # is itself stacked yields the full data from all three sources.
+ def make_stacked_stacked():
+ _, stacked = self.prepare_stacked_remote_branch()
+ tree = stacked.bzrdir.sprout('tree3', stacked=True
+ ).open_workingtree()
+ local_tree = tree.branch.create_checkout('local-tree3')
+ local_tree.commit('more local changes are better')
+ branch = Branch.open(self.get_url('tree3'))
+ branch.lock_read()
+ self.addCleanup(branch.unlock)
+ return None, branch
+ rev_ord, expected_revs = self.get_ordered_revs('1.9', 'unordered',
+ branch_factory=make_stacked_stacked)
+ self.assertEqual(set(expected_revs), set(rev_ord))
+ # Getting unordered results should have made a streaming data request
+ # from the server, and one from each backing repo
+ self.assertLength(3, self.hpss_calls)
+
+ def test_stacked_get_stream_topological(self):
+ # Repository._get_source.get_stream() from a stacked repository with
+ # topological sorting yields the full data from both stacked and
+ # stacked upon sources in topological order.
+ rev_ord, expected_revs = self.get_ordered_revs('knit', 'topological')
+ self.assertEqual(expected_revs, rev_ord)
+ # Getting topological sort requires VFS calls still - one of which is
+ # pushing up from the bound branch.
+ self.assertLength(14, self.hpss_calls)
+
+ def test_stacked_get_stream_groupcompress(self):
+ # Repository._get_source.get_stream() from a stacked repository with
+ # groupcompress sorting yields the full data from both stacked and
+ # stacked upon sources in groupcompress order.
+ raise tests.TestSkipped('No groupcompress ordered format available')
+ rev_ord, expected_revs = self.get_ordered_revs('dev5', 'groupcompress')
+ self.assertEqual(expected_revs, reversed(rev_ord))
+ # Getting unordered results should have made a streaming data request
+ # from the backing branch, and one from the stacked on branch.
+ self.assertLength(2, self.hpss_calls)
+
+ def test_stacked_pull_more_than_stacking_has_bug_360791(self):
+ # When pulling some fixed amount of content that is more than the
+ # source has (because some is coming from a fallback branch, no error
+ # should be received. This was reported as bug 360791.
+ # Need three branches: a trunk, a stacked branch, and a preexisting
+ # branch pulling content from stacked and trunk.
+ self.setup_smart_server_with_call_log()
+ trunk = self.make_branch_and_tree('trunk', format="1.9-rich-root")
+ r1 = trunk.commit('start')
+ stacked_branch = trunk.branch.create_clone_on_transport(
+ self.get_transport('stacked'), stacked_on=trunk.branch.base)
+ local = self.make_branch('local', format='1.9-rich-root')
+ local.repository.fetch(stacked_branch.repository,
+ stacked_branch.last_revision())
+
+
+class TestRemoteBranchEffort(tests.TestCaseWithTransport):
+
+ def setUp(self):
+ super(TestRemoteBranchEffort, self).setUp()
+ # Create a smart server that publishes whatever the backing VFS server
+ # does.
+ self.smart_server = test_server.SmartTCPServer_for_testing()
+ self.start_server(self.smart_server, self.get_server())
+ # Log all HPSS calls into self.hpss_calls.
+ _SmartClient.hooks.install_named_hook(
+ 'call', self.capture_hpss_call, None)
+ self.hpss_calls = []
+
+ def capture_hpss_call(self, params):
+ self.hpss_calls.append(params.method)
+
+ def test_copy_content_into_avoids_revision_history(self):
+ local = self.make_branch('local')
+ builder = self.make_branch_builder('remote')
+ builder.build_commit(message="Commit.")
+ remote_branch_url = self.smart_server.get_url() + 'remote'
+ remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
+ local.repository.fetch(remote_branch.repository)
+ self.hpss_calls = []
+ remote_branch.copy_content_into(local)
+ self.assertFalse('Branch.revision_history' in self.hpss_calls)
+
+ def test_fetch_everything_needs_just_one_call(self):
+ local = self.make_branch('local')
+ builder = self.make_branch_builder('remote')
+ builder.build_commit(message="Commit.")
+ remote_branch_url = self.smart_server.get_url() + 'remote'
+ remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
+ self.hpss_calls = []
+ local.repository.fetch(
+ remote_branch.repository,
+ fetch_spec=vf_search.EverythingResult(remote_branch.repository))
+ self.assertEqual(['Repository.get_stream_1.19'], self.hpss_calls)
+
+ def override_verb(self, verb_name, verb):
+ request_handlers = request.request_handlers
+ orig_verb = request_handlers.get(verb_name)
+ orig_info = request_handlers.get_info(verb_name)
+ request_handlers.register(verb_name, verb, override_existing=True)
+ self.addCleanup(request_handlers.register, verb_name, orig_verb,
+ override_existing=True, info=orig_info)
+
+ def test_fetch_everything_backwards_compat(self):
+ """Can fetch with EverythingResult even with pre 2.4 servers.
+
+ Pre-2.4 do not support 'everything' searches with the
+ Repository.get_stream_1.19 verb.
+ """
+ verb_log = []
+ class OldGetStreamVerb(SmartServerRepositoryGetStream_1_19):
+ """A version of the Repository.get_stream_1.19 verb patched to
+ reject 'everything' searches the way 2.3 and earlier do.
+ """
+ def recreate_search(self, repository, search_bytes,
+ discard_excess=False):
+ verb_log.append(search_bytes.split('\n', 1)[0])
+ if search_bytes == 'everything':
+ return (None,
+ request.FailedSmartServerResponse(('BadSearch',)))
+ return super(OldGetStreamVerb,
+ self).recreate_search(repository, search_bytes,
+ discard_excess=discard_excess)
+ self.override_verb('Repository.get_stream_1.19', OldGetStreamVerb)
+ local = self.make_branch('local')
+ builder = self.make_branch_builder('remote')
+ builder.build_commit(message="Commit.")
+ remote_branch_url = self.smart_server.get_url() + 'remote'
+ remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
+ self.hpss_calls = []
+ local.repository.fetch(
+ remote_branch.repository,
+ fetch_spec=vf_search.EverythingResult(remote_branch.repository))
+ # make sure the overridden verb was used
+ self.assertLength(1, verb_log)
+ # more than one HPSS call is needed, but because it's a VFS callback
+ # its hard to predict exactly how many.
+ self.assertTrue(len(self.hpss_calls) > 1)
+
+
+class TestUpdateBoundBranchWithModifiedBoundLocation(
+ tests.TestCaseWithTransport):
+ """Ensure correct handling of bound_location modifications.
+
+ This is tested against a smart server as http://pad.lv/786980 was about a
+ ReadOnlyError (write attempt during a read-only transaction) which can only
+ happen in this context.
+ """
+
+ def setUp(self):
+ super(TestUpdateBoundBranchWithModifiedBoundLocation, self).setUp()
+ self.transport_server = test_server.SmartTCPServer_for_testing
+
+ def make_master_and_checkout(self, master_name, checkout_name):
+ # Create the master branch and its associated checkout
+ self.master = self.make_branch_and_tree(master_name)
+ self.checkout = self.master.branch.create_checkout(checkout_name)
+ # Modify the master branch so there is something to update
+ self.master.commit('add stuff')
+ self.last_revid = self.master.commit('even more stuff')
+ self.bound_location = self.checkout.branch.get_bound_location()
+
+ def assertUpdateSucceeds(self, new_location):
+ self.checkout.branch.set_bound_location(new_location)
+ self.checkout.update()
+ self.assertEquals(self.last_revid, self.checkout.last_revision())
+
+ def test_without_final_slash(self):
+ self.make_master_and_checkout('master', 'checkout')
+ # For unclear reasons some users have a bound_location without a final
+ # '/', simulate that by forcing such a value
+ self.assertEndsWith(self.bound_location, '/')
+ self.assertUpdateSucceeds(self.bound_location.rstrip('/'))
+
+ def test_plus_sign(self):
+ self.make_master_and_checkout('+master', 'checkout')
+ self.assertUpdateSucceeds(self.bound_location.replace('%2B', '+', 1))
+
+ def test_tilda(self):
+ # Embed ~ in the middle of the path just to avoid any $HOME
+ # interpretation
+ self.make_master_and_checkout('mas~ter', 'checkout')
+ self.assertUpdateSucceeds(self.bound_location.replace('%2E', '~', 1))
+
+
+class TestWithCustomErrorHandler(RemoteBranchTestCase):
+
+ def test_no_context(self):
+ class OutOfCoffee(errors.BzrError):
+ """A dummy exception for testing."""
+
+ def __init__(self, urgency):
+ self.urgency = urgency
+ remote.no_context_error_translators.register("OutOfCoffee",
+ lambda err: OutOfCoffee(err.error_args[0]))
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info',
+ ('quack/',),
+ 'error', ('OutOfCoffee', 'low'))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ self.assertRaises(OutOfCoffee, branch.last_revision_info)
+ self.assertFinished(client)
+
+ def test_with_context(self):
+ class OutOfTea(errors.BzrError):
+ def __init__(self, branch, urgency):
+ self.branch = branch
+ self.urgency = urgency
+ remote.error_translators.register("OutOfTea",
+ lambda err, find, path: OutOfTea(err.error_args[0],
+ find("branch")))
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info',
+ ('quack/',),
+ 'error', ('OutOfTea', 'low'))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ self.assertRaises(OutOfTea, branch.last_revision_info)
+ self.assertFinished(client)
+
+
+class TestRepositoryPack(TestRemoteRepository):
+
+ def test_pack(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'token'))
+ client.add_expected_call(
+ 'Repository.pack', ('quack/', 'token', 'False'),
+ 'success', ('ok',), )
+ client.add_expected_call(
+ 'Repository.unlock', ('quack/', 'token'),
+ 'success', ('ok', ))
+ repo.pack()
+
+ def test_pack_with_hint(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'Repository.lock_write', ('quack/', ''),
+ 'success', ('ok', 'token'))
+ client.add_expected_call(
+ 'Repository.pack', ('quack/', 'token', 'False'),
+ 'success', ('ok',), )
+ client.add_expected_call(
+ 'Repository.unlock', ('quack/', 'token', 'False'),
+ 'success', ('ok', ))
+ repo.pack(['hinta', 'hintb'])
+
+
+class TestRepositoryIterInventories(TestRemoteRepository):
+ """Test Repository.iter_inventories."""
+
+ def _serialize_inv_delta(self, old_name, new_name, delta):
+ serializer = inventory_delta.InventoryDeltaSerializer(True, False)
+ return "".join(serializer.delta_to_lines(old_name, new_name, delta))
+
+ def test_single_empty(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ fmt = controldir.format_registry.get('2a')().repository_format
+ repo._format = fmt
+ stream = [('inventory-deltas', [
+ versionedfile.FulltextContentFactory('somerevid', None, None,
+ self._serialize_inv_delta('null:', 'somerevid', []))])]
+ client.add_expected_call(
+ 'VersionedFileRepository.get_inventories', ('quack/', 'unordered'),
+ 'success', ('ok', ),
+ _stream_to_byte_stream(stream, fmt))
+ ret = list(repo.iter_inventories(["somerevid"]))
+ self.assertLength(1, ret)
+ inv = ret[0]
+ self.assertEquals("somerevid", inv.revision_id)
+
+ def test_empty(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ ret = list(repo.iter_inventories([]))
+ self.assertEquals(ret, [])
+
+ def test_missing(self):
+ transport_path = 'quack'
+ repo, client = self.setup_fake_client_and_repository(transport_path)
+ client.add_expected_call(
+ 'VersionedFileRepository.get_inventories', ('quack/', 'unordered'),
+ 'success', ('ok', ), iter([]))
+ self.assertRaises(errors.NoSuchRevision, list, repo.iter_inventories(
+ ["somerevid"]))