1# Copyright (C) 2006-2013, 2016 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Tests for remote bzrdir/branch/repo/etc
18
19These are proxy objects which act on remote objects by sending messages
20through a smart client.  The proxies are to be created when attempting to open
21the object given a transport that supports smartserver rpc operations.
22
23These tests correspond to tests.test_smart, which exercises the server side.
24"""
25
26import base64
27import bz2
28from io import BytesIO
29import tarfile
30import zlib
31
32from ... import (
33    bencode,
34    branch,
35    config,
36    controldir,
37    errors,
38    repository,
39    tests,
40    transport,
41    treebuilder,
42    )
43from ...branch import Branch
44from .. import (
45    bzrdir,
46    inventory,
47    inventory_delta,
48    remote,
49    versionedfile,
50    vf_search,
51    )
52from ..bzrdir import (
53    BzrDir,
54    BzrDirFormat,
55    )
56from .. import (
57    RemoteBzrProber,
58    )
59from ..chk_serializer import chk_bencode_serializer
60from ..remote import (
61    RemoteBranch,
62    RemoteBranchFormat,
63    RemoteBzrDir,
64    RemoteBzrDirFormat,
65    RemoteRepository,
66    RemoteRepositoryFormat,
67    )
68from .. import groupcompress_repo, knitpack_repo
69from ...revision import (
70    NULL_REVISION,
71    Revision,
72    )
73from ..smart import medium, request
74from ..smart.client import _SmartClient
75from ..smart.repository import (
76    SmartServerRepositoryGetParentMap,
77    SmartServerRepositoryGetStream_1_19,
78    _stream_to_byte_stream,
79    )
80from ...tests import (
81    test_server,
82    )
83from ...tests.scenarios import load_tests_apply_scenarios
84from ...transport.memory import MemoryTransport
85from ...transport.remote import (
86    RemoteTransport,
87    RemoteSSHTransport,
88    RemoteTCPTransport,
89    )
90
91
92load_tests = load_tests_apply_scenarios
93
94
95class BasicRemoteObjectTests(tests.TestCaseWithTransport):
96
97    scenarios = [
98        ('HPSS-v2',
99            {'transport_server':
100                test_server.SmartTCPServer_for_testing_v2_only}),
101        ('HPSS-v3',
102            {'transport_server': test_server.SmartTCPServer_for_testing})]
103
104    def setUp(self):
105        super(BasicRemoteObjectTests, self).setUp()
106        self.transport = self.get_transport()
107        # make a branch that can be opened over the smart transport
108        self.local_wt = BzrDir.create_standalone_workingtree('.')
109        self.addCleanup(self.transport.disconnect)
110
111    def test_create_remote_bzrdir(self):
112        b = remote.RemoteBzrDir(self.transport, RemoteBzrDirFormat())
113        self.assertIsInstance(b, BzrDir)
114
115    def test_open_remote_branch(self):
116        # open a standalone branch in the working directory
117        b = remote.RemoteBzrDir(self.transport, RemoteBzrDirFormat())
118        branch = b.open_branch()
119        self.assertIsInstance(branch, Branch)
120
121    def test_remote_repository(self):
122        b = BzrDir.open_from_transport(self.transport)
123        repo = b.open_repository()
124        revid = u'\xc823123123'.encode('utf8')
125        self.assertFalse(repo.has_revision(revid))
126        self.local_wt.commit(message='test commit', rev_id=revid)
127        self.assertTrue(repo.has_revision(revid))
128
129    def test_find_correct_format(self):
130        """Should open a RemoteBzrDir over a RemoteTransport"""
131        fmt = BzrDirFormat.find_format(self.transport)
132        self.assertIn(RemoteBzrProber, controldir.ControlDirFormat._probers)
133        self.assertIsInstance(fmt, RemoteBzrDirFormat)
134
135    def test_open_detected_smart_format(self):
136        fmt = BzrDirFormat.find_format(self.transport)
137        d = fmt.open(self.transport)
138        self.assertIsInstance(d, BzrDir)
139
140    def test_remote_branch_repr(self):
141        b = BzrDir.open_from_transport(self.transport).open_branch()
142        self.assertStartsWith(str(b), 'RemoteBranch(')
143
144    def test_remote_bzrdir_repr(self):
145        b = BzrDir.open_from_transport(self.transport)
146        self.assertStartsWith(str(b), 'RemoteBzrDir(')
147
148    def test_remote_branch_format_supports_stacking(self):
149        t = self.transport
150        self.make_branch('unstackable', format='pack-0.92')
151        b = BzrDir.open_from_transport(t.clone('unstackable')).open_branch()
152        self.assertFalse(b._format.supports_stacking())
153        self.make_branch('stackable', format='1.9')
154        b = BzrDir.open_from_transport(t.clone('stackable')).open_branch()
155        self.assertTrue(b._format.supports_stacking())
156
157    def test_remote_repo_format_supports_external_references(self):
158        t = self.transport
159        bd = self.make_controldir('unstackable', format='pack-0.92')
160        r = bd.create_repository()
161        self.assertFalse(r._format.supports_external_lookups)
162        r = BzrDir.open_from_transport(
163            t.clone('unstackable')).open_repository()
164        self.assertFalse(r._format.supports_external_lookups)
165        bd = self.make_controldir('stackable', format='1.9')
166        r = bd.create_repository()
167        self.assertTrue(r._format.supports_external_lookups)
168        r = BzrDir.open_from_transport(t.clone('stackable')).open_repository()
169        self.assertTrue(r._format.supports_external_lookups)
170
171    def test_remote_branch_set_append_revisions_only(self):
172        # Make a format 1.9 branch, which supports append_revisions_only
173        branch = self.make_branch('branch', format='1.9')
174        branch.set_append_revisions_only(True)
175        config = branch.get_config_stack()
176        self.assertEqual(
177            True, config.get('append_revisions_only'))
178        branch.set_append_revisions_only(False)
179        config = branch.get_config_stack()
180        self.assertEqual(
181            False, config.get('append_revisions_only'))
182
183    def test_remote_branch_set_append_revisions_only_upgrade_reqd(self):
184        branch = self.make_branch('branch', format='knit')
185        self.assertRaises(
186            errors.UpgradeRequired, branch.set_append_revisions_only, True)
187
188
189class FakeProtocol(object):
190    """Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
191
192    def __init__(self, body, fake_client):
193        self.body = body
194        self._body_buffer = None
195        self._fake_client = fake_client
196
197    def read_body_bytes(self, count=-1):
198        if self._body_buffer is None:
199            self._body_buffer = BytesIO(self.body)
200        bytes = self._body_buffer.read(count)
201        if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
202            self._fake_client.expecting_body = False
203        return bytes
204
205    def cancel_read_body(self):
206        self._fake_client.expecting_body = False
207
208    def read_streamed_body(self):
209        return self.body
210
211
212class FakeClient(_SmartClient):
213    """Lookalike for _SmartClient allowing testing."""
214
215    def __init__(self, fake_medium_base='fake base'):
216        """Create a FakeClient."""
217        self.responses = []
218        self._calls = []
219        self.expecting_body = False
220        # if non-None, this is the list of expected calls, with only the
221        # method name and arguments included.  the body might be hard to
222        # compute so is not included. If a call is None, that call can
223        # be anything.
224        self._expected_calls = None
225        _SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
226
227    def add_expected_call(self, call_name, call_args, response_type,
228                          response_args, response_body=None):
229        if self._expected_calls is None:
230            self._expected_calls = []
231        self._expected_calls.append((call_name, call_args))
232        self.responses.append((response_type, response_args, response_body))
233
234    def add_success_response(self, *args):
235        self.responses.append((b'success', args, None))
236
237    def add_success_response_with_body(self, body, *args):
238        self.responses.append((b'success', args, body))
239        if self._expected_calls is not None:
240            self._expected_calls.append(None)
241
242    def add_error_response(self, *args):
243        self.responses.append((b'error', args))
244
245    def add_unknown_method_response(self, verb):
246        self.responses.append((b'unknown', verb))
247
248    def finished_test(self):
249        if self._expected_calls:
250            raise AssertionError("%r finished but was still expecting %r"
251                                 % (self, self._expected_calls[0]))
252
253    def _get_next_response(self):
254        try:
255            response_tuple = self.responses.pop(0)
256        except IndexError:
257            raise AssertionError("%r didn't expect any more calls" % (self,))
258        if response_tuple[0] == b'unknown':
259            raise errors.UnknownSmartMethod(response_tuple[1])
260        elif response_tuple[0] == b'error':
261            raise errors.ErrorFromSmartServer(response_tuple[1])
262        return response_tuple
263
264    def _check_call(self, method, args):
265        if self._expected_calls is None:
266            # the test should be updated to say what it expects
267            return
268        try:
269            next_call = self._expected_calls.pop(0)
270        except IndexError:
271            raise AssertionError("%r didn't expect any more calls "
272                                 "but got %r%r"
273                                 % (self, method, args,))
274        if next_call is None:
275            return
276        if method != next_call[0] or args != next_call[1]:
277            raise AssertionError(
278                "%r expected %r%r but got %r%r" %
279                (self, next_call[0], next_call[1], method, args,))
280
281    def call(self, method, *args):
282        self._check_call(method, args)
283        self._calls.append(('call', method, args))
284        return self._get_next_response()[1]
285
286    def call_expecting_body(self, method, *args):
287        self._check_call(method, args)
288        self._calls.append(('call_expecting_body', method, args))
289        result = self._get_next_response()
290        self.expecting_body = True
291        return result[1], FakeProtocol(result[2], self)
292
293    def call_with_body_bytes(self, method, args, body):
294        self._check_call(method, args)
295        self._calls.append(('call_with_body_bytes', method, args, body))
296        result = self._get_next_response()
297        return result[1], FakeProtocol(result[2], self)
298
299    def call_with_body_bytes_expecting_body(self, method, args, body):
300        self._check_call(method, args)
301        self._calls.append(('call_with_body_bytes_expecting_body', method,
302                            args, body))
303        result = self._get_next_response()
304        self.expecting_body = True
305        return result[1], FakeProtocol(result[2], self)
306
307    def call_with_body_stream(self, args, stream):
308        # Explicitly consume the stream before checking for an error, because
309        # that's what happens a real medium.
310        stream = list(stream)
311        self._check_call(args[0], args[1:])
312        self._calls.append(
313            ('call_with_body_stream', args[0], args[1:], stream))
314        result = self._get_next_response()
315        # The second value returned from call_with_body_stream is supposed to
316        # be a response_handler object, but so far no tests depend on that.
317        response_handler = None
318        return result[1], response_handler
319
320
321class FakeMedium(medium.SmartClientMedium):
322
323    def __init__(self, client_calls, base):
324        medium.SmartClientMedium.__init__(self, base)
325        self._client_calls = client_calls
326
327    def disconnect(self):
328        self._client_calls.append(('disconnect medium',))
329
330
331class TestVfsHas(tests.TestCase):
332
333    def test_unicode_path(self):
334        client = FakeClient('/')
335        client.add_success_response(b'yes',)
336        transport = RemoteTransport('bzr://localhost/', _client=client)
337        filename = u'/hell\u00d8'
338        result = transport.has(filename)
339        self.assertEqual(
340            [('call', b'has', (filename.encode('utf-8'),))],
341            client._calls)
342        self.assertTrue(result)
343
344
345class TestRemote(tests.TestCaseWithMemoryTransport):
346
347    def get_branch_format(self):
348        reference_bzrdir_format = controldir.format_registry.get('default')()
349        return reference_bzrdir_format.get_branch_format()
350
351    def get_repo_format(self):
352        reference_bzrdir_format = controldir.format_registry.get('default')()
353        return reference_bzrdir_format.repository_format
354
355    def assertFinished(self, fake_client):
356        """Assert that all of a FakeClient's expected calls have occurred."""
357        fake_client.finished_test()
358
359
360class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
361    """Tests for the behaviour of client_medium.remote_path_from_transport."""
362
363    def assertRemotePath(self, expected, client_base, transport_base):
364        """Assert that the result of
365        SmartClientMedium.remote_path_from_transport is the expected value for
366        a given client_base and transport_base.
367        """
368        client_medium = medium.SmartClientMedium(client_base)
369        t = transport.get_transport(transport_base)
370        result = client_medium.remote_path_from_transport(t)
371        self.assertEqual(expected, result)
372
373    def test_remote_path_from_transport(self):
374        """SmartClientMedium.remote_path_from_transport calculates a URL for
375        the given transport relative to the root of the client base URL.
376        """
377        self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
378        self.assertRemotePath(
379            'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
380
381    def assertRemotePathHTTP(self, expected, transport_base, relpath):
382        """Assert that the result of
383        HttpTransportBase.remote_path_from_transport is the expected value for
384        a given transport_base and relpath of that transport.  (Note that
385        HttpTransportBase is a subclass of SmartClientMedium)
386        """
387        base_transport = transport.get_transport(transport_base)
388        client_medium = base_transport.get_smart_medium()
389        cloned_transport = base_transport.clone(relpath)
390        result = client_medium.remote_path_from_transport(cloned_transport)
391        self.assertEqual(expected, result)
392
393    def test_remote_path_from_transport_http(self):
394        """Remote paths for HTTP transports are calculated differently to other
395        transports.  They are just relative to the client base, not the root
396        directory of the host.
397        """
398        for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
399            self.assertRemotePathHTTP(
400                '../xyz/', scheme + '//host/path', '../xyz/')
401            self.assertRemotePathHTTP(
402                'xyz/', scheme + '//host/path', 'xyz/')
403
404
405class Test_ClientMedium_remote_is_at_least(tests.TestCase):
406    """Tests for the behaviour of client_medium.remote_is_at_least."""
407
408    def test_initially_unlimited(self):
409        """A fresh medium assumes that the remote side supports all
410        versions.
411        """
412        client_medium = medium.SmartClientMedium('dummy base')
413        self.assertFalse(client_medium._is_remote_before((99, 99)))
414
415    def test__remember_remote_is_before(self):
416        """Calling _remember_remote_is_before ratchets down the known remote
417        version.
418        """
419        client_medium = medium.SmartClientMedium('dummy base')
420        # Mark the remote side as being less than 1.6.  The remote side may
421        # still be 1.5.
422        client_medium._remember_remote_is_before((1, 6))
423        self.assertTrue(client_medium._is_remote_before((1, 6)))
424        self.assertFalse(client_medium._is_remote_before((1, 5)))
425        # Calling _remember_remote_is_before again with a lower value works.
426        client_medium._remember_remote_is_before((1, 5))
427        self.assertTrue(client_medium._is_remote_before((1, 5)))
428        # If you call _remember_remote_is_before with a higher value it logs a
429        # warning, and continues to remember the lower value.
430        self.assertNotContainsRe(self.get_log(), '_remember_remote_is_before')
431        client_medium._remember_remote_is_before((1, 9))
432        self.assertContainsRe(self.get_log(), '_remember_remote_is_before')
433        self.assertTrue(client_medium._is_remote_before((1, 5)))
434
435
436class TestBzrDirCloningMetaDir(TestRemote):
437
438    def test_backwards_compat(self):
439        self.setup_smart_server_with_call_log()
440        a_dir = self.make_controldir('.')
441        self.reset_smart_call_log()
442        verb = b'BzrDir.cloning_metadir'
443        self.disable_verb(verb)
444        a_dir.cloning_metadir()
445        call_count = len([call for call in self.hpss_calls if
446                          call.call.method == verb])
447        self.assertEqual(1, call_count)
448
449    def test_branch_reference(self):
450        transport = self.get_transport('quack')
451        referenced = self.make_branch('referenced')
452        expected = referenced.controldir.cloning_metadir()
453        client = FakeClient(transport.base)
454        client.add_expected_call(
455            b'BzrDir.cloning_metadir', (b'quack/', b'False'),
456            b'error', (b'BranchReference',)),
457        client.add_expected_call(
458            b'BzrDir.open_branchV3', (b'quack/',),
459            b'success', (b'ref', self.get_url('referenced').encode('utf-8'))),
460        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
461                                    _client=client)
462        result = a_controldir.cloning_metadir()
463        # We should have got a control dir matching the referenced branch.
464        self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
465        self.assertEqual(expected._repository_format,
466                         result._repository_format)
467        self.assertEqual(expected._branch_format, result._branch_format)
468        self.assertFinished(client)
469
470    def test_current_server(self):
471        transport = self.get_transport('.')
472        transport = transport.clone('quack')
473        self.make_controldir('quack')
474        client = FakeClient(transport.base)
475        reference_bzrdir_format = controldir.format_registry.get('default')()
476        control_name = reference_bzrdir_format.network_name()
477        client.add_expected_call(
478            b'BzrDir.cloning_metadir', (b'quack/', b'False'),
479            b'success', (control_name, b'', (b'branch', b''))),
480        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
481                                    _client=client)
482        result = a_controldir.cloning_metadir()
483        # We should have got a reference control dir with default branch and
484        # repository formats.
485        # This pokes a little, just to be sure.
486        self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
487        self.assertEqual(None, result._repository_format)
488        self.assertEqual(None, result._branch_format)
489        self.assertFinished(client)
490
491    def test_unknown(self):
492        transport = self.get_transport('quack')
493        referenced = self.make_branch('referenced')
494        referenced.controldir.cloning_metadir()
495        client = FakeClient(transport.base)
496        client.add_expected_call(
497            b'BzrDir.cloning_metadir', (b'quack/', b'False'),
498            b'success', (b'unknown', b'unknown', (b'branch', b''))),
499        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
500                                    _client=client)
501        self.assertRaises(errors.UnknownFormatError,
502                          a_controldir.cloning_metadir)
503
504
505class TestBzrDirCheckoutMetaDir(TestRemote):
506
507    def test__get_checkout_format(self):
508        transport = MemoryTransport()
509        client = FakeClient(transport.base)
510        reference_bzrdir_format = controldir.format_registry.get('default')()
511        control_name = reference_bzrdir_format.network_name()
512        client.add_expected_call(
513            b'BzrDir.checkout_metadir', (b'quack/', ),
514            b'success', (control_name, b'', b''))
515        transport.mkdir('quack')
516        transport = transport.clone('quack')
517        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
518                                    _client=client)
519        result = a_controldir.checkout_metadir()
520        # We should have got a reference control dir with default branch and
521        # repository formats.
522        self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
523        self.assertEqual(None, result._repository_format)
524        self.assertEqual(None, result._branch_format)
525        self.assertFinished(client)
526
527    def test_unknown_format(self):
528        transport = MemoryTransport()
529        client = FakeClient(transport.base)
530        client.add_expected_call(
531            b'BzrDir.checkout_metadir', (b'quack/',),
532            b'success', (b'dontknow', b'', b''))
533        transport.mkdir('quack')
534        transport = transport.clone('quack')
535        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
536                                    _client=client)
537        self.assertRaises(errors.UnknownFormatError,
538                          a_controldir.checkout_metadir)
539        self.assertFinished(client)
540
541
542class TestBzrDirGetBranches(TestRemote):
543
544    def test_get_branches(self):
545        transport = MemoryTransport()
546        client = FakeClient(transport.base)
547        reference_bzrdir_format = controldir.format_registry.get('default')()
548        branch_name = reference_bzrdir_format.get_branch_format().network_name()
549        client.add_success_response_with_body(
550            bencode.bencode({
551                b"foo": (b"branch", branch_name),
552                b"": (b"branch", branch_name)}), b"success")
553        client.add_success_response(
554            b'ok', b'', b'no', b'no', b'no',
555            reference_bzrdir_format.repository_format.network_name())
556        client.add_error_response(b'NotStacked')
557        client.add_success_response(
558            b'ok', b'', b'no', b'no', b'no',
559            reference_bzrdir_format.repository_format.network_name())
560        client.add_error_response(b'NotStacked')
561        transport.mkdir('quack')
562        transport = transport.clone('quack')
563        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
564                                    _client=client)
565        result = a_controldir.get_branches()
566        self.assertEqual({"", "foo"}, set(result.keys()))
567        self.assertEqual(
568            [('call_expecting_body', b'BzrDir.get_branches', (b'quack/',)),
569             ('call', b'BzrDir.find_repositoryV3', (b'quack/', )),
570             ('call', b'Branch.get_stacked_on_url', (b'quack/', )),
571             ('call', b'BzrDir.find_repositoryV3', (b'quack/', )),
572             ('call', b'Branch.get_stacked_on_url', (b'quack/', ))],
573            client._calls)
574
575
576class TestBzrDirDestroyBranch(TestRemote):
577
578    def test_destroy_default(self):
579        transport = self.get_transport('quack')
580        self.make_branch('referenced')
581        client = FakeClient(transport.base)
582        client.add_expected_call(
583            b'BzrDir.destroy_branch', (b'quack/', ),
584            b'success', (b'ok',)),
585        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
586                                    _client=client)
587        a_controldir.destroy_branch()
588        self.assertFinished(client)
589
590
591class TestBzrDirHasWorkingTree(TestRemote):
592
593    def test_has_workingtree(self):
594        transport = self.get_transport('quack')
595        client = FakeClient(transport.base)
596        client.add_expected_call(
597            b'BzrDir.has_workingtree', (b'quack/',),
598            b'success', (b'yes',)),
599        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
600                                    _client=client)
601        self.assertTrue(a_controldir.has_workingtree())
602        self.assertFinished(client)
603
604    def test_no_workingtree(self):
605        transport = self.get_transport('quack')
606        client = FakeClient(transport.base)
607        client.add_expected_call(
608            b'BzrDir.has_workingtree', (b'quack/',),
609            b'success', (b'no',)),
610        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
611                                    _client=client)
612        self.assertFalse(a_controldir.has_workingtree())
613        self.assertFinished(client)
614
615
616class TestBzrDirDestroyRepository(TestRemote):
617
618    def test_destroy_repository(self):
619        transport = self.get_transport('quack')
620        client = FakeClient(transport.base)
621        client.add_expected_call(
622            b'BzrDir.destroy_repository', (b'quack/',),
623            b'success', (b'ok',)),
624        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
625                                    _client=client)
626        a_controldir.destroy_repository()
627        self.assertFinished(client)
628
629
630class TestBzrDirOpen(TestRemote):
631
632    def make_fake_client_and_transport(self, path='quack'):
633        transport = MemoryTransport()
634        transport.mkdir(path)
635        transport = transport.clone(path)
636        client = FakeClient(transport.base)
637        return client, transport
638
639    def test_absent(self):
640        client, transport = self.make_fake_client_and_transport()
641        client.add_expected_call(
642            b'BzrDir.open_2.1', (b'quack/',), b'success', (b'no',))
643        self.assertRaises(errors.NotBranchError, RemoteBzrDir, transport,
644                          RemoteBzrDirFormat(), _client=client,
645                          _force_probe=True)
646        self.assertFinished(client)
647
648    def test_present_without_workingtree(self):
649        client, transport = self.make_fake_client_and_transport()
650        client.add_expected_call(
651            b'BzrDir.open_2.1', (b'quack/',), b'success', (b'yes', b'no'))
652        bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
653                          _client=client, _force_probe=True)
654        self.assertIsInstance(bd, RemoteBzrDir)
655        self.assertFalse(bd.has_workingtree())
656        self.assertRaises(errors.NoWorkingTree, bd.open_workingtree)
657        self.assertFinished(client)
658
659    def test_present_with_workingtree(self):
660        client, transport = self.make_fake_client_and_transport()
661        client.add_expected_call(
662            b'BzrDir.open_2.1', (b'quack/',), b'success', (b'yes', b'yes'))
663        bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
664                          _client=client, _force_probe=True)
665        self.assertIsInstance(bd, RemoteBzrDir)
666        self.assertTrue(bd.has_workingtree())
667        self.assertRaises(errors.NotLocalUrl, bd.open_workingtree)
668        self.assertFinished(client)
669
670    def test_backwards_compat(self):
671        client, transport = self.make_fake_client_and_transport()
672        client.add_expected_call(
673            b'BzrDir.open_2.1', (b'quack/',), b'unknown',
674            (b'BzrDir.open_2.1',))
675        client.add_expected_call(
676            b'BzrDir.open', (b'quack/',), b'success', (b'yes',))
677        bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
678                          _client=client, _force_probe=True)
679        self.assertIsInstance(bd, RemoteBzrDir)
680        self.assertFinished(client)
681
682    def test_backwards_compat_hpss_v2(self):
683        client, transport = self.make_fake_client_and_transport()
684        # Monkey-patch fake client to simulate real-world behaviour with v2
685        # server: upon first RPC call detect the protocol version, and because
686        # the version is 2 also do _remember_remote_is_before((1, 6)) before
687        # continuing with the RPC.
688        orig_check_call = client._check_call
689
690        def check_call(method, args):
691            client._medium._protocol_version = 2
692            client._medium._remember_remote_is_before((1, 6))
693            client._check_call = orig_check_call
694            client._check_call(method, args)
695        client._check_call = check_call
696        client.add_expected_call(
697            b'BzrDir.open_2.1', (b'quack/',), b'unknown',
698            (b'BzrDir.open_2.1',))
699        client.add_expected_call(
700            b'BzrDir.open', (b'quack/',), b'success', (b'yes',))
701        bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
702                          _client=client, _force_probe=True)
703        self.assertIsInstance(bd, RemoteBzrDir)
704        self.assertFinished(client)
705
706
707class TestBzrDirOpenBranch(TestRemote):
708
709    def test_backwards_compat(self):
710        self.setup_smart_server_with_call_log()
711        self.make_branch('.')
712        a_dir = BzrDir.open(self.get_url('.'))
713        self.reset_smart_call_log()
714        verb = b'BzrDir.open_branchV3'
715        self.disable_verb(verb)
716        a_dir.open_branch()
717        call_count = len([call for call in self.hpss_calls if
718                          call.call.method == verb])
719        self.assertEqual(1, call_count)
720
721    def test_branch_present(self):
722        reference_format = self.get_repo_format()
723        network_name = reference_format.network_name()
724        branch_network_name = self.get_branch_format().network_name()
725        transport = MemoryTransport()
726        transport.mkdir('quack')
727        transport = transport.clone('quack')
728        client = FakeClient(transport.base)
729        client.add_expected_call(
730            b'BzrDir.open_branchV3', (b'quack/',),
731            b'success', (b'branch', branch_network_name))
732        client.add_expected_call(
733            b'BzrDir.find_repositoryV3', (b'quack/',),
734            b'success', (b'ok', b'', b'no', b'no', b'no', network_name))
735        client.add_expected_call(
736            b'Branch.get_stacked_on_url', (b'quack/',),
737            b'error', (b'NotStacked',))
738        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
739                              _client=client)
740        result = bzrdir.open_branch()
741        self.assertIsInstance(result, RemoteBranch)
742        self.assertEqual(bzrdir, result.controldir)
743        self.assertFinished(client)
744
745    def test_branch_missing(self):
746        transport = MemoryTransport()
747        transport.mkdir('quack')
748        transport = transport.clone('quack')
749        client = FakeClient(transport.base)
750        client.add_error_response(b'nobranch')
751        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
752                              _client=client)
753        self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
754        self.assertEqual(
755            [('call', b'BzrDir.open_branchV3', (b'quack/',))],
756            client._calls)
757
758    def test__get_tree_branch(self):
759        # _get_tree_branch is a form of open_branch, but it should only ask for
760        # branch opening, not any other network requests.
761        calls = []
762
763        def open_branch(name=None, possible_transports=None):
764            calls.append("Called")
765            return "a-branch"
766        transport = MemoryTransport()
767        # no requests on the network - catches other api calls being made.
768        client = FakeClient(transport.base)
769        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
770                              _client=client)
771        # patch the open_branch call to record that it was called.
772        bzrdir.open_branch = open_branch
773        self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
774        self.assertEqual(["Called"], calls)
775        self.assertEqual([], client._calls)
776
777    def test_url_quoting_of_path(self):
778        # Relpaths on the wire should not be URL-escaped.  So "~" should be
779        # transmitted as "~", not "%7E".
780        transport = RemoteTCPTransport('bzr://localhost/~hello/')
781        client = FakeClient(transport.base)
782        reference_format = self.get_repo_format()
783        network_name = reference_format.network_name()
784        branch_network_name = self.get_branch_format().network_name()
785        client.add_expected_call(
786            b'BzrDir.open_branchV3', (b'~hello/',),
787            b'success', (b'branch', branch_network_name))
788        client.add_expected_call(
789            b'BzrDir.find_repositoryV3', (b'~hello/',),
790            b'success', (b'ok', b'', b'no', b'no', b'no', network_name))
791        client.add_expected_call(
792            b'Branch.get_stacked_on_url', (b'~hello/',),
793            b'error', (b'NotStacked',))
794        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
795                              _client=client)
796        bzrdir.open_branch()
797        self.assertFinished(client)
798
799    def check_open_repository(self, rich_root, subtrees,
800                              external_lookup=b'no'):
801        reference_format = self.get_repo_format()
802        network_name = reference_format.network_name()
803        transport = MemoryTransport()
804        transport.mkdir('quack')
805        transport = transport.clone('quack')
806        if rich_root:
807            rich_response = b'yes'
808        else:
809            rich_response = b'no'
810        if subtrees:
811            subtree_response = b'yes'
812        else:
813            subtree_response = b'no'
814        client = FakeClient(transport.base)
815        client.add_success_response(
816            b'ok', b'', rich_response, subtree_response, external_lookup,
817            network_name)
818        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
819                              _client=client)
820        result = bzrdir.open_repository()
821        self.assertEqual(
822            [('call', b'BzrDir.find_repositoryV3', (b'quack/',))],
823            client._calls)
824        self.assertIsInstance(result, RemoteRepository)
825        self.assertEqual(bzrdir, result.controldir)
826        self.assertEqual(rich_root, result._format.rich_root_data)
827        self.assertEqual(subtrees, result._format.supports_tree_reference)
828
829    def test_open_repository_sets_format_attributes(self):
830        self.check_open_repository(True, True)
831        self.check_open_repository(False, True)
832        self.check_open_repository(True, False)
833        self.check_open_repository(False, False)
834        self.check_open_repository(False, False, b'yes')
835
836    def test_old_server(self):
837        """RemoteBzrDirFormat should fail to probe if the server version is too
838        old.
839        """
840        self.assertRaises(
841            errors.NotBranchError,
842            RemoteBzrProber.probe_transport, OldServerTransport())
843
844
845class TestBzrDirCreateBranch(TestRemote):
846
847    def test_backwards_compat(self):
848        self.setup_smart_server_with_call_log()
849        repo = self.make_repository('.')
850        self.reset_smart_call_log()
851        self.disable_verb(b'BzrDir.create_branch')
852        repo.controldir.create_branch()
853        create_branch_call_count = len(
854            [call for call in self.hpss_calls
855             if call.call.method == b'BzrDir.create_branch'])
856        self.assertEqual(1, create_branch_call_count)
857
858    def test_current_server(self):
859        transport = self.get_transport('.')
860        transport = transport.clone('quack')
861        self.make_repository('quack')
862        client = FakeClient(transport.base)
863        reference_bzrdir_format = controldir.format_registry.get('default')()
864        reference_format = reference_bzrdir_format.get_branch_format()
865        network_name = reference_format.network_name()
866        reference_repo_fmt = reference_bzrdir_format.repository_format
867        reference_repo_name = reference_repo_fmt.network_name()
868        client.add_expected_call(
869            b'BzrDir.create_branch', (b'quack/', network_name),
870            b'success', (b'ok', network_name, b'', b'no', b'no', b'yes',
871                         reference_repo_name))
872        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
873                                    _client=client)
874        branch = a_controldir.create_branch()
875        # We should have got a remote branch
876        self.assertIsInstance(branch, remote.RemoteBranch)
877        # its format should have the settings from the response
878        format = branch._format
879        self.assertEqual(network_name, format.network_name())
880
881    def test_already_open_repo_and_reused_medium(self):
882        """Bug 726584: create_branch(..., repository=repo) should work
883        regardless of what the smart medium's base URL is.
884        """
885        self.transport_server = test_server.SmartTCPServer_for_testing
886        transport = self.get_transport('.')
887        repo = self.make_repository('quack')
888        # Client's medium rooted a transport root (not at the bzrdir)
889        client = FakeClient(transport.base)
890        transport = transport.clone('quack')
891        reference_bzrdir_format = controldir.format_registry.get('default')()
892        reference_format = reference_bzrdir_format.get_branch_format()
893        network_name = reference_format.network_name()
894        reference_repo_fmt = reference_bzrdir_format.repository_format
895        reference_repo_name = reference_repo_fmt.network_name()
896        client.add_expected_call(
897            b'BzrDir.create_branch', (b'extra/quack/', network_name),
898            b'success', (b'ok', network_name, b'', b'no', b'no', b'yes',
899                         reference_repo_name))
900        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
901                                    _client=client)
902        branch = a_controldir.create_branch(repository=repo)
903        # We should have got a remote branch
904        self.assertIsInstance(branch, remote.RemoteBranch)
905        # its format should have the settings from the response
906        format = branch._format
907        self.assertEqual(network_name, format.network_name())
908
909
910class TestBzrDirCreateRepository(TestRemote):
911
912    def test_backwards_compat(self):
913        self.setup_smart_server_with_call_log()
914        bzrdir = self.make_controldir('.')
915        self.reset_smart_call_log()
916        self.disable_verb(b'BzrDir.create_repository')
917        bzrdir.create_repository()
918        create_repo_call_count = len([call for call in self.hpss_calls if
919                                      call.call.method == b'BzrDir.create_repository'])
920        self.assertEqual(1, create_repo_call_count)
921
922    def test_current_server(self):
923        transport = self.get_transport('.')
924        transport = transport.clone('quack')
925        self.make_controldir('quack')
926        client = FakeClient(transport.base)
927        reference_bzrdir_format = controldir.format_registry.get('default')()
928        reference_format = reference_bzrdir_format.repository_format
929        network_name = reference_format.network_name()
930        client.add_expected_call(
931            b'BzrDir.create_repository', (b'quack/',
932                                          b'Bazaar repository format 2a (needs bzr 1.16 or later)\n',
933                                          b'False'),
934            b'success', (b'ok', b'yes', b'yes', b'yes', network_name))
935        a_controldir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
936                                    _client=client)
937        repo = a_controldir.create_repository()
938        # We should have got a remote repository
939        self.assertIsInstance(repo, remote.RemoteRepository)
940        # its format should have the settings from the response
941        format = repo._format
942        self.assertTrue(format.rich_root_data)
943        self.assertTrue(format.supports_tree_reference)
944        self.assertTrue(format.supports_external_lookups)
945        self.assertEqual(network_name, format.network_name())
946
947
948class TestBzrDirOpenRepository(TestRemote):
949
950    def test_backwards_compat_1_2_3(self):
951        # fallback all the way to the first version.
952        reference_format = self.get_repo_format()
953        network_name = reference_format.network_name()
954        server_url = 'bzr://example.com/'
955        self.permit_url(server_url)
956        client = FakeClient(server_url)
957        client.add_unknown_method_response(b'BzrDir.find_repositoryV3')
958        client.add_unknown_method_response(b'BzrDir.find_repositoryV2')
959        client.add_success_response(b'ok', b'', b'no', b'no')
960        # A real repository instance will be created to determine the network
961        # name.
962        client.add_success_response_with_body(
963            b"Bazaar-NG meta directory, format 1\n", b'ok')
964        client.add_success_response(b'stat', b'0', b'65535')
965        client.add_success_response_with_body(
966            reference_format.get_format_string(), b'ok')
967        # PackRepository wants to do a stat
968        client.add_success_response(b'stat', b'0', b'65535')
969        remote_transport = RemoteTransport(server_url + 'quack/', medium=False,
970                                           _client=client)
971        bzrdir = RemoteBzrDir(remote_transport, RemoteBzrDirFormat(),
972                              _client=client)
973        repo = bzrdir.open_repository()
974        self.assertEqual(
975            [('call', b'BzrDir.find_repositoryV3', (b'quack/',)),
976             ('call', b'BzrDir.find_repositoryV2', (b'quack/',)),
977             ('call', b'BzrDir.find_repository', (b'quack/',)),
978             ('call_expecting_body', b'get', (b'/quack/.bzr/branch-format',)),
979             ('call', b'stat', (b'/quack/.bzr',)),
980             ('call_expecting_body', b'get', (b'/quack/.bzr/repository/format',)),
981             ('call', b'stat', (b'/quack/.bzr/repository',)),
982             ],
983            client._calls)
984        self.assertEqual(network_name, repo._format.network_name())
985
986    def test_backwards_compat_2(self):
987        # fallback to find_repositoryV2
988        reference_format = self.get_repo_format()
989        network_name = reference_format.network_name()
990        server_url = 'bzr://example.com/'
991        self.permit_url(server_url)
992        client = FakeClient(server_url)
993        client.add_unknown_method_response(b'BzrDir.find_repositoryV3')
994        client.add_success_response(b'ok', b'', b'no', b'no', b'no')
995        # A real repository instance will be created to determine the network
996        # name.
997        client.add_success_response_with_body(
998            b"Bazaar-NG meta directory, format 1\n", b'ok')
999        client.add_success_response(b'stat', b'0', b'65535')
1000        client.add_success_response_with_body(
1001            reference_format.get_format_string(), b'ok')
1002        # PackRepository wants to do a stat
1003        client.add_success_response(b'stat', b'0', b'65535')
1004        remote_transport = RemoteTransport(server_url + 'quack/', medium=False,
1005                                           _client=client)
1006        bzrdir = RemoteBzrDir(remote_transport, RemoteBzrDirFormat(),
1007                              _client=client)
1008        repo = bzrdir.open_repository()
1009        self.assertEqual(
1010            [('call', b'BzrDir.find_repositoryV3', (b'quack/',)),
1011             ('call', b'BzrDir.find_repositoryV2', (b'quack/',)),
1012             ('call_expecting_body', b'get', (b'/quack/.bzr/branch-format',)),
1013             ('call', b'stat', (b'/quack/.bzr',)),
1014             ('call_expecting_body', b'get',
1015                 (b'/quack/.bzr/repository/format',)),
1016             ('call', b'stat', (b'/quack/.bzr/repository',)),
1017             ],
1018            client._calls)
1019        self.assertEqual(network_name, repo._format.network_name())
1020
1021    def test_current_server(self):
1022        reference_format = self.get_repo_format()
1023        network_name = reference_format.network_name()
1024        transport = MemoryTransport()
1025        transport.mkdir('quack')
1026        transport = transport.clone('quack')
1027        client = FakeClient(transport.base)
1028        client.add_success_response(
1029            b'ok', b'', b'no', b'no', b'no', network_name)
1030        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
1031                              _client=client)
1032        repo = bzrdir.open_repository()
1033        self.assertEqual(
1034            [('call', b'BzrDir.find_repositoryV3', (b'quack/',))],
1035            client._calls)
1036        self.assertEqual(network_name, repo._format.network_name())
1037
1038
1039class TestBzrDirFormatInitializeEx(TestRemote):
1040
1041    def test_success(self):
1042        """Simple test for typical successful call."""
1043        fmt = RemoteBzrDirFormat()
1044        default_format_name = BzrDirFormat.get_default_format().network_name()
1045        transport = self.get_transport()
1046        client = FakeClient(transport.base)
1047        client.add_expected_call(
1048            b'BzrDirFormat.initialize_ex_1.16',
1049            (default_format_name, b'path', b'False', b'False', b'False', b'',
1050             b'', b'', b'', b'False'),
1051            b'success',
1052            (b'.', b'no', b'no', b'yes', b'repo fmt', b'repo bzrdir fmt',
1053             b'bzrdir fmt', b'False', b'', b'', b'repo lock token'))
1054        # XXX: It would be better to call fmt.initialize_on_transport_ex, but
1055        # it's currently hard to test that without supplying a real remote
1056        # transport connected to a real server.
1057        fmt._initialize_on_transport_ex_rpc(
1058            client, b'path', transport, False, False, False, None, None, None,
1059            None, False)
1060        self.assertFinished(client)
1061
1062    def test_error(self):
1063        """Error responses are translated, e.g. 'PermissionDenied' raises the
1064        corresponding error from the client.
1065        """
1066        fmt = RemoteBzrDirFormat()
1067        default_format_name = BzrDirFormat.get_default_format().network_name()
1068        transport = self.get_transport()
1069        client = FakeClient(transport.base)
1070        client.add_expected_call(
1071            b'BzrDirFormat.initialize_ex_1.16',
1072            (default_format_name, b'path', b'False', b'False', b'False', b'',
1073             b'', b'', b'', b'False'),
1074            b'error',
1075            (b'PermissionDenied', b'path', b'extra info'))
1076        # XXX: It would be better to call fmt.initialize_on_transport_ex, but
1077        # it's currently hard to test that without supplying a real remote
1078        # transport connected to a real server.
1079        err = self.assertRaises(
1080            errors.PermissionDenied,
1081            fmt._initialize_on_transport_ex_rpc, client, b'path', transport,
1082            False, False, False, None, None, None, None, False)
1083        self.assertEqual('path', err.path)
1084        self.assertEqual(': extra info', err.extra)
1085        self.assertFinished(client)
1086
1087    def test_error_from_real_server(self):
1088        """Integration test for error translation."""
1089        transport = self.make_smart_server('foo')
1090        transport = transport.clone('no-such-path')
1091        fmt = RemoteBzrDirFormat()
1092        self.assertRaises(
1093            errors.NoSuchFile, fmt.initialize_on_transport_ex, transport,
1094            create_prefix=False)
1095
1096
1097class OldSmartClient(object):
1098    """A fake smart client for test_old_version that just returns a version one
1099    response to the 'hello' (query version) command.
1100    """
1101
1102    def get_request(self):
1103        input_file = BytesIO(b'ok\x011\n')
1104        output_file = BytesIO()
1105        client_medium = medium.SmartSimplePipesClientMedium(
1106            input_file, output_file)
1107        return medium.SmartClientStreamMediumRequest(client_medium)
1108
1109    def protocol_version(self):
1110        return 1
1111
1112
1113class OldServerTransport(object):
1114    """A fake transport for test_old_server that reports it's smart server
1115    protocol version as version one.
1116    """
1117
1118    def __init__(self):
1119        self.base = 'fake:'
1120
1121    def get_smart_client(self):
1122        return OldSmartClient()
1123
1124
1125class RemoteBzrDirTestCase(TestRemote):
1126
1127    def make_remote_bzrdir(self, transport, client):
1128        """Make a RemotebzrDir using 'client' as the _client."""
1129        return RemoteBzrDir(transport, RemoteBzrDirFormat(),
1130                            _client=client)
1131
1132
1133class RemoteBranchTestCase(RemoteBzrDirTestCase):
1134
1135    def lock_remote_branch(self, branch):
1136        """Trick a RemoteBranch into thinking it is locked."""
1137        branch._lock_mode = 'w'
1138        branch._lock_count = 2
1139        branch._lock_token = b'branch token'
1140        branch._repo_lock_token = b'repo token'
1141        branch.repository._lock_mode = 'w'
1142        branch.repository._lock_count = 2
1143        branch.repository._lock_token = b'repo token'
1144
1145    def make_remote_branch(self, transport, client):
1146        """Make a RemoteBranch using 'client' as its _SmartClient.
1147
1148        A RemoteBzrDir and RemoteRepository will also be created to fill out
1149        the RemoteBranch, albeit with stub values for some of their attributes.
1150        """
1151        # we do not want bzrdir to make any remote calls, so use False as its
1152        # _client.  If it tries to make a remote call, this will fail
1153        # immediately.
1154        bzrdir = self.make_remote_bzrdir(transport, False)
1155        repo = RemoteRepository(bzrdir, None, _client=client)
1156        branch_format = self.get_branch_format()
1157        format = RemoteBranchFormat(network_name=branch_format.network_name())
1158        return RemoteBranch(bzrdir, repo, _client=client, format=format)
1159
1160
1161class TestBranchBreakLock(RemoteBranchTestCase):
1162
1163    def test_break_lock(self):
1164        transport = MemoryTransport()
1165        client = FakeClient(transport.base)
1166        client.add_expected_call(
1167            b'Branch.get_stacked_on_url', (b'quack/',),
1168            b'error', (b'NotStacked',))
1169        client.add_expected_call(
1170            b'Branch.break_lock', (b'quack/',),
1171            b'success', (b'ok',))
1172        transport.mkdir('quack')
1173        transport = transport.clone('quack')
1174        branch = self.make_remote_branch(transport, client)
1175        branch.break_lock()
1176        self.assertFinished(client)
1177
1178
1179class TestBranchGetPhysicalLockStatus(RemoteBranchTestCase):
1180
1181    def test_get_physical_lock_status_yes(self):
1182        transport = MemoryTransport()
1183        client = FakeClient(transport.base)
1184        client.add_expected_call(
1185            b'Branch.get_stacked_on_url', (b'quack/',),
1186            b'error', (b'NotStacked',))
1187        client.add_expected_call(
1188            b'Branch.get_physical_lock_status', (b'quack/',),
1189            b'success', (b'yes',))
1190        transport.mkdir('quack')
1191        transport = transport.clone('quack')
1192        branch = self.make_remote_branch(transport, client)
1193        result = branch.get_physical_lock_status()
1194        self.assertFinished(client)
1195        self.assertEqual(True, result)
1196
1197    def test_get_physical_lock_status_no(self):
1198        transport = MemoryTransport()
1199        client = FakeClient(transport.base)
1200        client.add_expected_call(
1201            b'Branch.get_stacked_on_url', (b'quack/',),
1202            b'error', (b'NotStacked',))
1203        client.add_expected_call(
1204            b'Branch.get_physical_lock_status', (b'quack/',),
1205            b'success', (b'no',))
1206        transport.mkdir('quack')
1207        transport = transport.clone('quack')
1208        branch = self.make_remote_branch(transport, client)
1209        result = branch.get_physical_lock_status()
1210        self.assertFinished(client)
1211        self.assertEqual(False, result)
1212
1213
1214class TestBranchGetParent(RemoteBranchTestCase):
1215
1216    def test_no_parent(self):
1217        # in an empty branch we decode the response properly
1218        transport = MemoryTransport()
1219        client = FakeClient(transport.base)
1220        client.add_expected_call(
1221            b'Branch.get_stacked_on_url', (b'quack/',),
1222            b'error', (b'NotStacked',))
1223        client.add_expected_call(
1224            b'Branch.get_parent', (b'quack/',),
1225            b'success', (b'',))
1226        transport.mkdir('quack')
1227        transport = transport.clone('quack')
1228        branch = self.make_remote_branch(transport, client)
1229        result = branch.get_parent()
1230        self.assertFinished(client)
1231        self.assertEqual(None, result)
1232
1233    def test_parent_relative(self):
1234        transport = MemoryTransport()
1235        client = FakeClient(transport.base)
1236        client.add_expected_call(
1237            b'Branch.get_stacked_on_url', (b'kwaak/',),
1238            b'error', (b'NotStacked',))
1239        client.add_expected_call(
1240            b'Branch.get_parent', (b'kwaak/',),
1241            b'success', (b'../foo/',))
1242        transport.mkdir('kwaak')
1243        transport = transport.clone('kwaak')
1244        branch = self.make_remote_branch(transport, client)
1245        result = branch.get_parent()
1246        self.assertEqual(transport.clone('../foo').base, result)
1247
1248    def test_parent_absolute(self):
1249        transport = MemoryTransport()
1250        client = FakeClient(transport.base)
1251        client.add_expected_call(
1252            b'Branch.get_stacked_on_url', (b'kwaak/',),
1253            b'error', (b'NotStacked',))
1254        client.add_expected_call(
1255            b'Branch.get_parent', (b'kwaak/',),
1256            b'success', (b'http://foo/',))
1257        transport.mkdir('kwaak')
1258        transport = transport.clone('kwaak')
1259        branch = self.make_remote_branch(transport, client)
1260        result = branch.get_parent()
1261        self.assertEqual('http://foo/', result)
1262        self.assertFinished(client)
1263
1264
1265class TestBranchSetParentLocation(RemoteBranchTestCase):
1266
1267    def test_no_parent(self):
1268        # We call the verb when setting parent to None
1269        transport = MemoryTransport()
1270        client = FakeClient(transport.base)
1271        client.add_expected_call(
1272            b'Branch.get_stacked_on_url', (b'quack/',),
1273            b'error', (b'NotStacked',))
1274        client.add_expected_call(
1275            b'Branch.set_parent_location', (b'quack/', b'b', b'r', b''),
1276            b'success', ())
1277        transport.mkdir('quack')
1278        transport = transport.clone('quack')
1279        branch = self.make_remote_branch(transport, client)
1280        branch._lock_token = b'b'
1281        branch._repo_lock_token = b'r'
1282        branch._set_parent_location(None)
1283        self.assertFinished(client)
1284
1285    def test_parent(self):
1286        transport = MemoryTransport()
1287        client = FakeClient(transport.base)
1288        client.add_expected_call(
1289            b'Branch.get_stacked_on_url', (b'kwaak/',),
1290            b'error', (b'NotStacked',))
1291        client.add_expected_call(
1292            b'Branch.set_parent_location', (b'kwaak/', b'b', b'r', b'foo'),
1293            b'success', ())
1294        transport.mkdir('kwaak')
1295        transport = transport.clone('kwaak')
1296        branch = self.make_remote_branch(transport, client)
1297        branch._lock_token = b'b'
1298        branch._repo_lock_token = b'r'
1299        branch._set_parent_location('foo')
1300        self.assertFinished(client)
1301
1302    def test_backwards_compat(self):
1303        self.setup_smart_server_with_call_log()
1304        branch = self.make_branch('.')
1305        self.reset_smart_call_log()
1306        verb = b'Branch.set_parent_location'
1307        self.disable_verb(verb)
1308        branch.set_parent('http://foo/')
1309        self.assertLength(14, self.hpss_calls)
1310
1311
1312class TestBranchGetTagsBytes(RemoteBranchTestCase):
1313
1314    def test_backwards_compat(self):
1315        self.setup_smart_server_with_call_log()
1316        branch = self.make_branch('.')
1317        self.reset_smart_call_log()
1318        verb = b'Branch.get_tags_bytes'
1319        self.disable_verb(verb)
1320        branch.tags.get_tag_dict()
1321        call_count = len([call for call in self.hpss_calls if
1322                          call.call.method == verb])
1323        self.assertEqual(1, call_count)
1324
1325    def test_trivial(self):
1326        transport = MemoryTransport()
1327        client = FakeClient(transport.base)
1328        client.add_expected_call(
1329            b'Branch.get_stacked_on_url', (b'quack/',),
1330            b'error', (b'NotStacked',))
1331        client.add_expected_call(
1332            b'Branch.get_tags_bytes', (b'quack/',),
1333            b'success', (b'',))
1334        transport.mkdir('quack')
1335        transport = transport.clone('quack')
1336        branch = self.make_remote_branch(transport, client)
1337        result = branch.tags.get_tag_dict()
1338        self.assertFinished(client)
1339        self.assertEqual({}, result)
1340
1341
1342class TestBranchSetTagsBytes(RemoteBranchTestCase):
1343
1344    def test_trivial(self):
1345        transport = MemoryTransport()
1346        client = FakeClient(transport.base)
1347        client.add_expected_call(
1348            b'Branch.get_stacked_on_url', (b'quack/',),
1349            b'error', (b'NotStacked',))
1350        client.add_expected_call(
1351            b'Branch.set_tags_bytes', (b'quack/',
1352                                       b'branch token', b'repo token'),
1353            b'success', ('',))
1354        transport.mkdir('quack')
1355        transport = transport.clone('quack')
1356        branch = self.make_remote_branch(transport, client)
1357        self.lock_remote_branch(branch)
1358        branch._set_tags_bytes(b'tags bytes')
1359        self.assertFinished(client)
1360        self.assertEqual(b'tags bytes', client._calls[-1][-1])
1361
1362    def test_backwards_compatible(self):
1363        transport = MemoryTransport()
1364        client = FakeClient(transport.base)
1365        client.add_expected_call(
1366            b'Branch.get_stacked_on_url', (b'quack/',),
1367            b'error', (b'NotStacked',))
1368        client.add_expected_call(
1369            b'Branch.set_tags_bytes', (b'quack/',
1370                                       b'branch token', b'repo token'),
1371            b'unknown', (b'Branch.set_tags_bytes',))
1372        transport.mkdir('quack')
1373        transport = transport.clone('quack')
1374        branch = self.make_remote_branch(transport, client)
1375        self.lock_remote_branch(branch)
1376
1377        class StubRealBranch(object):
1378            def __init__(self):
1379                self.calls = []
1380
1381            def _set_tags_bytes(self, bytes):
1382                self.calls.append(('set_tags_bytes', bytes))
1383        real_branch = StubRealBranch()
1384        branch._real_branch = real_branch
1385        branch._set_tags_bytes(b'tags bytes')
1386        # Call a second time, to exercise the 'remote version already inferred'
1387        # code path.
1388        branch._set_tags_bytes(b'tags bytes')
1389        self.assertFinished(client)
1390        self.assertEqual(
1391            [('set_tags_bytes', b'tags bytes')] * 2, real_branch.calls)
1392
1393
1394class TestBranchHeadsToFetch(RemoteBranchTestCase):
1395
1396    def test_uses_last_revision_info_and_tags_by_default(self):
1397        transport = MemoryTransport()
1398        client = FakeClient(transport.base)
1399        client.add_expected_call(
1400            b'Branch.get_stacked_on_url', (b'quack/',),
1401            b'error', (b'NotStacked',))
1402        client.add_expected_call(
1403            b'Branch.last_revision_info', (b'quack/',),
1404            b'success', (b'ok', b'1', b'rev-tip'))
1405        client.add_expected_call(
1406            b'Branch.get_config_file', (b'quack/',),
1407            b'success', (b'ok',), b'')
1408        transport.mkdir('quack')
1409        transport = transport.clone('quack')
1410        branch = self.make_remote_branch(transport, client)
1411        result = branch.heads_to_fetch()
1412        self.assertFinished(client)
1413        self.assertEqual(({b'rev-tip'}, set()), result)
1414
1415    def test_uses_last_revision_info_and_tags_when_set(self):
1416        transport = MemoryTransport()
1417        client = FakeClient(transport.base)
1418        client.add_expected_call(
1419            b'Branch.get_stacked_on_url', (b'quack/',),
1420            b'error', (b'NotStacked',))
1421        client.add_expected_call(
1422            b'Branch.last_revision_info', (b'quack/',),
1423            b'success', (b'ok', b'1', b'rev-tip'))
1424        client.add_expected_call(
1425            b'Branch.get_config_file', (b'quack/',),
1426            b'success', (b'ok',), b'branch.fetch_tags = True')
1427        # XXX: this will break if the default format's serialization of tags
1428        # changes, or if the RPC for fetching tags changes from get_tags_bytes.
1429        client.add_expected_call(
1430            b'Branch.get_tags_bytes', (b'quack/',),
1431            b'success', (b'd5:tag-17:rev-foo5:tag-27:rev-bare',))
1432        transport.mkdir('quack')
1433        transport = transport.clone('quack')
1434        branch = self.make_remote_branch(transport, client)
1435        result = branch.heads_to_fetch()
1436        self.assertFinished(client)
1437        self.assertEqual(
1438            ({b'rev-tip'}, {b'rev-foo', b'rev-bar'}), result)
1439
1440    def test_uses_rpc_for_formats_with_non_default_heads_to_fetch(self):
1441        transport = MemoryTransport()
1442        client = FakeClient(transport.base)
1443        client.add_expected_call(
1444            b'Branch.get_stacked_on_url', (b'quack/',),
1445            b'error', (b'NotStacked',))
1446        client.add_expected_call(
1447            b'Branch.heads_to_fetch', (b'quack/',),
1448            b'success', ([b'tip'], [b'tagged-1', b'tagged-2']))
1449        transport.mkdir('quack')
1450        transport = transport.clone('quack')
1451        branch = self.make_remote_branch(transport, client)
1452        branch._format._use_default_local_heads_to_fetch = lambda: False
1453        result = branch.heads_to_fetch()
1454        self.assertFinished(client)
1455        self.assertEqual(({b'tip'}, {b'tagged-1', b'tagged-2'}), result)
1456
1457    def make_branch_with_tags(self):
1458        self.setup_smart_server_with_call_log()
1459        # Make a branch with a single revision.
1460        builder = self.make_branch_builder('foo')
1461        builder.start_series()
1462        builder.build_snapshot(None, [
1463            ('add', ('', b'root-id', 'directory', ''))],
1464            revision_id=b'tip')
1465        builder.finish_series()
1466        branch = builder.get_branch()
1467        # Add two tags to that branch
1468        branch.tags.set_tag('tag-1', b'rev-1')
1469        branch.tags.set_tag('tag-2', b'rev-2')
1470        return branch
1471
1472    def test_backwards_compatible(self):
1473        br = self.make_branch_with_tags()
1474        br.get_config_stack().set('branch.fetch_tags', True)
1475        self.addCleanup(br.lock_read().unlock)
1476        # Disable the heads_to_fetch verb
1477        verb = b'Branch.heads_to_fetch'
1478        self.disable_verb(verb)
1479        self.reset_smart_call_log()
1480        result = br.heads_to_fetch()
1481        self.assertEqual(({b'tip'}, {b'rev-1', b'rev-2'}), result)
1482        self.assertEqual(
1483            [b'Branch.last_revision_info', b'Branch.get_tags_bytes'],
1484            [call.call.method for call in self.hpss_calls])
1485
1486    def test_backwards_compatible_no_tags(self):
1487        br = self.make_branch_with_tags()
1488        br.get_config_stack().set('branch.fetch_tags', False)
1489        self.addCleanup(br.lock_read().unlock)
1490        # Disable the heads_to_fetch verb
1491        verb = b'Branch.heads_to_fetch'
1492        self.disable_verb(verb)
1493        self.reset_smart_call_log()
1494        result = br.heads_to_fetch()
1495        self.assertEqual(({b'tip'}, set()), result)
1496        self.assertEqual(
1497            [b'Branch.last_revision_info'],
1498            [call.call.method for call in self.hpss_calls])
1499
1500
1501class TestBranchLastRevisionInfo(RemoteBranchTestCase):
1502
1503    def test_empty_branch(self):
1504        # in an empty branch we decode the response properly
1505        transport = MemoryTransport()
1506        client = FakeClient(transport.base)
1507        client.add_expected_call(
1508            b'Branch.get_stacked_on_url', (b'quack/',),
1509            b'error', (b'NotStacked',))
1510        client.add_expected_call(
1511            b'Branch.last_revision_info', (b'quack/',),
1512            b'success', (b'ok', b'0', b'null:'))
1513        transport.mkdir('quack')
1514        transport = transport.clone('quack')
1515        branch = self.make_remote_branch(transport, client)
1516        result = branch.last_revision_info()
1517        self.assertFinished(client)
1518        self.assertEqual((0, NULL_REVISION), result)
1519
1520    def test_non_empty_branch(self):
1521        # in a non-empty branch we also decode the response properly
1522        revid = u'\xc8'.encode('utf8')
1523        transport = MemoryTransport()
1524        client = FakeClient(transport.base)
1525        client.add_expected_call(
1526            b'Branch.get_stacked_on_url', (b'kwaak/',),
1527            b'error', (b'NotStacked',))
1528        client.add_expected_call(
1529            b'Branch.last_revision_info', (b'kwaak/',),
1530            b'success', (b'ok', b'2', revid))
1531        transport.mkdir('kwaak')
1532        transport = transport.clone('kwaak')
1533        branch = self.make_remote_branch(transport, client)
1534        result = branch.last_revision_info()
1535        self.assertEqual((2, revid), result)
1536
1537
1538class TestBranch_get_stacked_on_url(TestRemote):
1539    """Test Branch._get_stacked_on_url rpc"""
1540
1541    def test_get_stacked_on_invalid_url(self):
1542        # test that asking for a stacked on url the server can't access works.
1543        # This isn't perfect, but then as we're in the same process there
1544        # really isn't anything we can do to be 100% sure that the server
1545        # doesn't just open in - this test probably needs to be rewritten using
1546        # a spawn()ed server.
1547        stacked_branch = self.make_branch('stacked', format='1.9')
1548        self.make_branch('base', format='1.9')
1549        vfs_url = self.get_vfs_only_url('base')
1550        stacked_branch.set_stacked_on_url(vfs_url)
1551        transport = stacked_branch.controldir.root_transport
1552        client = FakeClient(transport.base)
1553        client.add_expected_call(
1554            b'Branch.get_stacked_on_url', (b'stacked/',),
1555            b'success', (b'ok', vfs_url.encode('utf-8')))
1556        # XXX: Multiple calls are bad, this second call documents what is
1557        # today.
1558        client.add_expected_call(
1559            b'Branch.get_stacked_on_url', (b'stacked/',),
1560            b'success', (b'ok', vfs_url.encode('utf-8')))
1561        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
1562                              _client=client)
1563        repo_fmt = remote.RemoteRepositoryFormat()
1564        repo_fmt._custom_format = stacked_branch.repository._format
1565        branch = RemoteBranch(bzrdir, RemoteRepository(bzrdir, repo_fmt),
1566                              _client=client)
1567        result = branch.get_stacked_on_url()
1568        self.assertEqual(vfs_url, result)
1569
1570    def test_backwards_compatible(self):
1571        # like with bzr1.6 with no Branch.get_stacked_on_url rpc
1572        self.make_branch('base', format='1.6')
1573        stacked_branch = self.make_branch('stacked', format='1.6')
1574        stacked_branch.set_stacked_on_url('../base')
1575        client = FakeClient(self.get_url())
1576        branch_network_name = self.get_branch_format().network_name()
1577        client.add_expected_call(
1578            b'BzrDir.open_branchV3', (b'stacked/',),
1579            b'success', (b'branch', branch_network_name))
1580        client.add_expected_call(
1581            b'BzrDir.find_repositoryV3', (b'stacked/',),
1582            b'success', (b'ok', b'', b'no', b'no', b'yes',
1583                         stacked_branch.repository._format.network_name()))
1584        # called twice, once from constructor and then again by us
1585        client.add_expected_call(
1586            b'Branch.get_stacked_on_url', (b'stacked/',),
1587            b'unknown', (b'Branch.get_stacked_on_url',))
1588        client.add_expected_call(
1589            b'Branch.get_stacked_on_url', (b'stacked/',),
1590            b'unknown', (b'Branch.get_stacked_on_url',))
1591        # this will also do vfs access, but that goes direct to the transport
1592        # and isn't seen by the FakeClient.
1593        bzrdir = RemoteBzrDir(self.get_transport('stacked'),
1594                              RemoteBzrDirFormat(), _client=client)
1595        branch = bzrdir.open_branch()
1596        result = branch.get_stacked_on_url()
1597        self.assertEqual('../base', result)
1598        self.assertFinished(client)
1599        # it's in the fallback list both for the RemoteRepository and its vfs
1600        # repository
1601        self.assertEqual(1, len(branch.repository._fallback_repositories))
1602        self.assertEqual(1,
1603                         len(branch.repository._real_repository._fallback_repositories))
1604
1605    def test_get_stacked_on_real_branch(self):
1606        self.make_branch('base')
1607        stacked_branch = self.make_branch('stacked')
1608        stacked_branch.set_stacked_on_url('../base')
1609        reference_format = self.get_repo_format()
1610        network_name = reference_format.network_name()
1611        client = FakeClient(self.get_url())
1612        branch_network_name = self.get_branch_format().network_name()
1613        client.add_expected_call(
1614            b'BzrDir.open_branchV3', (b'stacked/',),
1615            b'success', (b'branch', branch_network_name))
1616        client.add_expected_call(
1617            b'BzrDir.find_repositoryV3', (b'stacked/',),
1618            b'success', (b'ok', b'', b'yes', b'no', b'yes', network_name))
1619        # called twice, once from constructor and then again by us
1620        client.add_expected_call(
1621            b'Branch.get_stacked_on_url', (b'stacked/',),
1622            b'success', (b'ok', b'../base'))
1623        client.add_expected_call(
1624            b'Branch.get_stacked_on_url', (b'stacked/',),
1625            b'success', (b'ok', b'../base'))
1626        bzrdir = RemoteBzrDir(self.get_transport('stacked'),
1627                              RemoteBzrDirFormat(), _client=client)
1628        branch = bzrdir.open_branch()
1629        result = branch.get_stacked_on_url()
1630        self.assertEqual('../base', result)
1631        self.assertFinished(client)
1632        # it's in the fallback list both for the RemoteRepository.
1633        self.assertEqual(1, len(branch.repository._fallback_repositories))
1634        # And we haven't had to construct a real repository.
1635        self.assertEqual(None, branch.repository._real_repository)
1636
1637
1638class TestBranchSetLastRevision(RemoteBranchTestCase):
1639
1640    def test_set_empty(self):
1641        # _set_last_revision_info('null:') is translated to calling
1642        # Branch.set_last_revision(path, '') on the wire.
1643        transport = MemoryTransport()
1644        transport.mkdir('branch')
1645        transport = transport.clone('branch')
1646
1647        client = FakeClient(transport.base)
1648        client.add_expected_call(
1649            b'Branch.get_stacked_on_url', (b'branch/',),
1650            b'error', (b'NotStacked',))
1651        client.add_expected_call(
1652            b'Branch.lock_write', (b'branch/', b'', b''),
1653            b'success', (b'ok', b'branch token', b'repo token'))
1654        client.add_expected_call(
1655            b'Branch.last_revision_info',
1656            (b'branch/',),
1657            b'success', (b'ok', b'0', b'null:'))
1658        client.add_expected_call(
1659            b'Branch.set_last_revision', (b'branch/',
1660                                          b'branch token', b'repo token', b'null:',),
1661            b'success', (b'ok',))
1662        client.add_expected_call(
1663            b'Branch.unlock', (b'branch/', b'branch token', b'repo token'),
1664            b'success', (b'ok',))
1665        branch = self.make_remote_branch(transport, client)
1666        branch.lock_write()
1667        result = branch._set_last_revision(NULL_REVISION)
1668        branch.unlock()
1669        self.assertEqual(None, result)
1670        self.assertFinished(client)
1671
1672    def test_set_nonempty(self):
1673        # set_last_revision_info(N, rev-idN) is translated to calling
1674        # Branch.set_last_revision(path, rev-idN) on the wire.
1675        transport = MemoryTransport()
1676        transport.mkdir('branch')
1677        transport = transport.clone('branch')
1678
1679        client = FakeClient(transport.base)
1680        client.add_expected_call(
1681            b'Branch.get_stacked_on_url', (b'branch/',),
1682            b'error', (b'NotStacked',))
1683        client.add_expected_call(
1684            b'Branch.lock_write', (b'branch/', b'', b''),
1685            b'success', (b'ok', b'branch token', b'repo token'))
1686        client.add_expected_call(
1687            b'Branch.last_revision_info',
1688            (b'branch/',),
1689            b'success', (b'ok', b'0', b'null:'))
1690        lines = [b'rev-id2']
1691        encoded_body = bz2.compress(b'\n'.join(lines))
1692        client.add_success_response_with_body(encoded_body, b'ok')
1693        client.add_expected_call(
1694            b'Branch.set_last_revision', (b'branch/',
1695                                          b'branch token', b'repo token', b'rev-id2',),
1696            b'success', (b'ok',))
1697        client.add_expected_call(
1698            b'Branch.unlock', (b'branch/', b'branch token', b'repo token'),
1699            b'success', (b'ok',))
1700        branch = self.make_remote_branch(transport, client)
1701        # Lock the branch, reset the record of remote calls.
1702        branch.lock_write()
1703        result = branch._set_last_revision(b'rev-id2')
1704        branch.unlock()
1705        self.assertEqual(None, result)
1706        self.assertFinished(client)
1707
1708    def test_no_such_revision(self):
1709        transport = MemoryTransport()
1710        transport.mkdir('branch')
1711        transport = transport.clone('branch')
1712        # A response of 'NoSuchRevision' is translated into an exception.
1713        client = FakeClient(transport.base)
1714        client.add_expected_call(
1715            b'Branch.get_stacked_on_url', (b'branch/',),
1716            b'error', (b'NotStacked',))
1717        client.add_expected_call(
1718            b'Branch.lock_write', (b'branch/', b'', b''),
1719            b'success', (b'ok', b'branch token', b'repo token'))
1720        client.add_expected_call(
1721            b'Branch.last_revision_info',
1722            (b'branch/',),
1723            b'success', (b'ok', b'0', b'null:'))
1724        # get_graph calls to construct the revision history, for the set_rh
1725        # hook
1726        lines = [b'rev-id']
1727        encoded_body = bz2.compress(b'\n'.join(lines))
1728        client.add_success_response_with_body(encoded_body, b'ok')
1729        client.add_expected_call(
1730            b'Branch.set_last_revision', (b'branch/',
1731                                          b'branch token', b'repo token', b'rev-id',),
1732            b'error', (b'NoSuchRevision', b'rev-id'))
1733        client.add_expected_call(
1734            b'Branch.unlock', (b'branch/', b'branch token', b'repo token'),
1735            b'success', (b'ok',))
1736
1737        branch = self.make_remote_branch(transport, client)
1738        branch.lock_write()
1739        self.assertRaises(
1740            errors.NoSuchRevision, branch._set_last_revision, b'rev-id')
1741        branch.unlock()
1742        self.assertFinished(client)
1743
1744    def test_tip_change_rejected(self):
1745        """TipChangeRejected responses cause a TipChangeRejected exception to
1746        be raised.
1747        """
1748        transport = MemoryTransport()
1749        transport.mkdir('branch')
1750        transport = transport.clone('branch')
1751        client = FakeClient(transport.base)
1752        rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
1753        rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
1754        client.add_expected_call(
1755            b'Branch.get_stacked_on_url', (b'branch/',),
1756            b'error', (b'NotStacked',))
1757        client.add_expected_call(
1758            b'Branch.lock_write', (b'branch/', b'', b''),
1759            b'success', (b'ok', b'branch token', b'repo token'))
1760        client.add_expected_call(
1761            b'Branch.last_revision_info',
1762            (b'branch/',),
1763            b'success', (b'ok', b'0', b'null:'))
1764        lines = [b'rev-id']
1765        encoded_body = bz2.compress(b'\n'.join(lines))
1766        client.add_success_response_with_body(encoded_body, b'ok')
1767        client.add_expected_call(
1768            b'Branch.set_last_revision', (b'branch/',
1769                                          b'branch token', b'repo token', b'rev-id',),
1770            b'error', (b'TipChangeRejected', rejection_msg_utf8))
1771        client.add_expected_call(
1772            b'Branch.unlock', (b'branch/', b'branch token', b'repo token'),
1773            b'success', (b'ok',))
1774        branch = self.make_remote_branch(transport, client)
1775        branch.lock_write()
1776        # The 'TipChangeRejected' error response triggered by calling
1777        # set_last_revision_info causes a TipChangeRejected exception.
1778        err = self.assertRaises(
1779            errors.TipChangeRejected,
1780            branch._set_last_revision, b'rev-id')
1781        # The UTF-8 message from the response has been decoded into a unicode
1782        # object.
1783        self.assertIsInstance(err.msg, str)
1784        self.assertEqual(rejection_msg_unicode, err.msg)
1785        branch.unlock()
1786        self.assertFinished(client)
1787
1788
1789class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
1790
1791    def test_set_last_revision_info(self):
1792        # set_last_revision_info(num, b'rev-id') is translated to calling
1793        # Branch.set_last_revision_info(num, 'rev-id') on the wire.
1794        transport = MemoryTransport()
1795        transport.mkdir('branch')
1796        transport = transport.clone('branch')
1797        client = FakeClient(transport.base)
1798        # get_stacked_on_url
1799        client.add_error_response(b'NotStacked')
1800        # lock_write
1801        client.add_success_response(b'ok', b'branch token', b'repo token')
1802        # query the current revision
1803        client.add_success_response(b'ok', b'0', b'null:')
1804        # set_last_revision
1805        client.add_success_response(b'ok')
1806        # unlock
1807        client.add_success_response(b'ok')
1808
1809        branch = self.make_remote_branch(transport, client)
1810        # Lock the branch, reset the record of remote calls.
1811        branch.lock_write()
1812        client._calls = []
1813        result = branch.set_last_revision_info(1234, b'a-revision-id')
1814        self.assertEqual(
1815            [('call', b'Branch.last_revision_info', (b'branch/',)),
1816             ('call', b'Branch.set_last_revision_info',
1817                (b'branch/', b'branch token', b'repo token',
1818                 b'1234', b'a-revision-id'))],
1819            client._calls)
1820        self.assertEqual(None, result)
1821
1822    def test_no_such_revision(self):
1823        # A response of 'NoSuchRevision' is translated into an exception.
1824        transport = MemoryTransport()
1825        transport.mkdir('branch')
1826        transport = transport.clone('branch')
1827        client = FakeClient(transport.base)
1828        # get_stacked_on_url
1829        client.add_error_response(b'NotStacked')
1830        # lock_write
1831        client.add_success_response(b'ok', b'branch token', b'repo token')
1832        # set_last_revision
1833        client.add_error_response(b'NoSuchRevision', b'revid')
1834        # unlock
1835        client.add_success_response(b'ok')
1836
1837        branch = self.make_remote_branch(transport, client)
1838        # Lock the branch, reset the record of remote calls.
1839        branch.lock_write()
1840        client._calls = []
1841
1842        self.assertRaises(
1843            errors.NoSuchRevision, branch.set_last_revision_info, 123, b'revid')
1844        branch.unlock()
1845
1846    def test_backwards_compatibility(self):
1847        """If the server does not support the Branch.set_last_revision_info
1848        verb (which is new in 1.4), then the client falls back to VFS methods.
1849        """
1850        # This test is a little messy.  Unlike most tests in this file, it
1851        # doesn't purely test what a Remote* object sends over the wire, and
1852        # how it reacts to responses from the wire.  It instead relies partly
1853        # on asserting that the RemoteBranch will call
1854        # self._real_branch.set_last_revision_info(...).
1855
1856        # First, set up our RemoteBranch with a FakeClient that raises
1857        # UnknownSmartMethod, and a StubRealBranch that logs how it is called.
1858        transport = MemoryTransport()
1859        transport.mkdir('branch')
1860        transport = transport.clone('branch')
1861        client = FakeClient(transport.base)
1862        client.add_expected_call(
1863            b'Branch.get_stacked_on_url', (b'branch/',),
1864            b'error', (b'NotStacked',))
1865        client.add_expected_call(
1866            b'Branch.last_revision_info',
1867            (b'branch/',),
1868            b'success', (b'ok', b'0', b'null:'))
1869        client.add_expected_call(
1870            b'Branch.set_last_revision_info',
1871            (b'branch/', b'branch token', b'repo token', b'1234', b'a-revision-id',),
1872            b'unknown', b'Branch.set_last_revision_info')
1873
1874        branch = self.make_remote_branch(transport, client)
1875
1876        class StubRealBranch(object):
1877            def __init__(self):
1878                self.calls = []
1879
1880            def set_last_revision_info(self, revno, revision_id):
1881                self.calls.append(
1882                    ('set_last_revision_info', revno, revision_id))
1883
1884            def _clear_cached_state(self):
1885                pass
1886        real_branch = StubRealBranch()
1887        branch._real_branch = real_branch
1888        self.lock_remote_branch(branch)
1889
1890        # Call set_last_revision_info, and verify it behaved as expected.
1891        branch.set_last_revision_info(1234, b'a-revision-id')
1892        self.assertEqual(
1893            [('set_last_revision_info', 1234, b'a-revision-id')],
1894            real_branch.calls)
1895        self.assertFinished(client)
1896
1897    def test_unexpected_error(self):
1898        # If the server sends an error the client doesn't understand, it gets
1899        # turned into an UnknownErrorFromSmartServer, which is presented as a
1900        # non-internal error to the user.
1901        transport = MemoryTransport()
1902        transport.mkdir('branch')
1903        transport = transport.clone('branch')
1904        client = FakeClient(transport.base)
1905        # get_stacked_on_url
1906        client.add_error_response(b'NotStacked')
1907        # lock_write
1908        client.add_success_response(b'ok', b'branch token', b'repo token')
1909        # set_last_revision
1910        client.add_error_response(b'UnexpectedError')
1911        # unlock
1912        client.add_success_response(b'ok')
1913
1914        branch = self.make_remote_branch(transport, client)
1915        # Lock the branch, reset the record of remote calls.
1916        branch.lock_write()
1917        client._calls = []
1918
1919        err = self.assertRaises(
1920            errors.UnknownErrorFromSmartServer,
1921            branch.set_last_revision_info, 123, b'revid')
1922        self.assertEqual((b'UnexpectedError',), err.error_tuple)
1923        branch.unlock()
1924
1925    def test_tip_change_rejected(self):
1926        """TipChangeRejected responses cause a TipChangeRejected exception to
1927        be raised.
1928        """
1929        transport = MemoryTransport()
1930        transport.mkdir('branch')
1931        transport = transport.clone('branch')
1932        client = FakeClient(transport.base)
1933        # get_stacked_on_url
1934        client.add_error_response(b'NotStacked')
1935        # lock_write
1936        client.add_success_response(b'ok', b'branch token', b'repo token')
1937        # set_last_revision
1938        client.add_error_response(b'TipChangeRejected', b'rejection message')
1939        # unlock
1940        client.add_success_response(b'ok')
1941
1942        branch = self.make_remote_branch(transport, client)
1943        # Lock the branch, reset the record of remote calls.
1944        branch.lock_write()
1945        self.addCleanup(branch.unlock)
1946        client._calls = []
1947
1948        # The 'TipChangeRejected' error response triggered by calling
1949        # set_last_revision_info causes a TipChangeRejected exception.
1950        err = self.assertRaises(
1951            errors.TipChangeRejected,
1952            branch.set_last_revision_info, 123, b'revid')
1953        self.assertEqual('rejection message', err.msg)
1954
1955
1956class TestBranchGetSetConfig(RemoteBranchTestCase):
1957
1958    def test_get_branch_conf(self):
1959        # in an empty branch we decode the response properly
1960        client = FakeClient()
1961        client.add_expected_call(
1962            b'Branch.get_stacked_on_url', (b'memory:///',),
1963            b'error', (b'NotStacked',),)
1964        client.add_success_response_with_body(b'# config file body', b'ok')
1965        transport = MemoryTransport()
1966        branch = self.make_remote_branch(transport, client)
1967        config = branch.get_config()
1968        config.has_explicit_nickname()
1969        self.assertEqual(
1970            [('call', b'Branch.get_stacked_on_url', (b'memory:///',)),
1971             ('call_expecting_body', b'Branch.get_config_file', (b'memory:///',))],
1972            client._calls)
1973
1974    def test_get_multi_line_branch_conf(self):
1975        # Make sure that multiple-line branch.conf files are supported
1976        #
1977        # https://bugs.launchpad.net/bzr/+bug/354075
1978        client = FakeClient()
1979        client.add_expected_call(
1980            b'Branch.get_stacked_on_url', (b'memory:///',),
1981            b'error', (b'NotStacked',),)
1982        client.add_success_response_with_body(b'a = 1\nb = 2\nc = 3\n', b'ok')
1983        transport = MemoryTransport()
1984        branch = self.make_remote_branch(transport, client)
1985        config = branch.get_config()
1986        self.assertEqual(u'2', config.get_user_option('b'))
1987
1988    def test_set_option(self):
1989        client = FakeClient()
1990        client.add_expected_call(
1991            b'Branch.get_stacked_on_url', (b'memory:///',),
1992            b'error', (b'NotStacked',),)
1993        client.add_expected_call(
1994            b'Branch.lock_write', (b'memory:///', b'', b''),
1995            b'success', (b'ok', b'branch token', b'repo token'))
1996        client.add_expected_call(
1997            b'Branch.set_config_option', (b'memory:///', b'branch token',
1998                                          b'repo token', b'foo', b'bar', b''),
1999            b'success', ())
2000        client.add_expected_call(
2001            b'Branch.unlock', (b'memory:///', b'branch token', b'repo token'),
2002            b'success', (b'ok',))
2003        transport = MemoryTransport()
2004        branch = self.make_remote_branch(transport, client)
2005        branch.lock_write()
2006        config = branch._get_config()
2007        config.set_option('foo', 'bar')
2008        branch.unlock()
2009        self.assertFinished(client)
2010
2011    def test_set_option_with_dict(self):
2012        client = FakeClient()
2013        client.add_expected_call(
2014            b'Branch.get_stacked_on_url', (b'memory:///',),
2015            b'error', (b'NotStacked',),)
2016        client.add_expected_call(
2017            b'Branch.lock_write', (b'memory:///', b'', b''),
2018            b'success', (b'ok', b'branch token', b'repo token'))
2019        encoded_dict_value = b'd5:ascii1:a11:unicode \xe2\x8c\x9a3:\xe2\x80\xbde'
2020        client.add_expected_call(
2021            b'Branch.set_config_option_dict', (b'memory:///', b'branch token',
2022                                               b'repo token', encoded_dict_value, b'foo', b''),
2023            b'success', ())
2024        client.add_expected_call(
2025            b'Branch.unlock', (b'memory:///', b'branch token', b'repo token'),
2026            b'success', (b'ok',))
2027        transport = MemoryTransport()
2028        branch = self.make_remote_branch(transport, client)
2029        branch.lock_write()
2030        config = branch._get_config()
2031        config.set_option(
2032            {'ascii': 'a', u'unicode \N{WATCH}': u'\N{INTERROBANG}'},
2033            'foo')
2034        branch.unlock()
2035        self.assertFinished(client)
2036
2037    def test_set_option_with_bool(self):
2038        client = FakeClient()
2039        client.add_expected_call(
2040            b'Branch.get_stacked_on_url', (b'memory:///',),
2041            b'error', (b'NotStacked',),)
2042        client.add_expected_call(
2043            b'Branch.lock_write', (b'memory:///', b'', b''),
2044            b'success', (b'ok', b'branch token', b'repo token'))
2045        client.add_expected_call(
2046            b'Branch.set_config_option', (b'memory:///', b'branch token',
2047                                          b'repo token', b'True', b'foo', b''),
2048            b'success', ())
2049        client.add_expected_call(
2050            b'Branch.unlock', (b'memory:///', b'branch token', b'repo token'),
2051            b'success', (b'ok',))
2052        transport = MemoryTransport()
2053        branch = self.make_remote_branch(transport, client)
2054        branch.lock_write()
2055        config = branch._get_config()
2056        config.set_option(True, 'foo')
2057        branch.unlock()
2058        self.assertFinished(client)
2059
2060    def test_backwards_compat_set_option(self):
2061        self.setup_smart_server_with_call_log()
2062        branch = self.make_branch('.')
2063        verb = b'Branch.set_config_option'
2064        self.disable_verb(verb)
2065        branch.lock_write()
2066        self.addCleanup(branch.unlock)
2067        self.reset_smart_call_log()
2068        branch._get_config().set_option('value', 'name')
2069        self.assertLength(11, self.hpss_calls)
2070        self.assertEqual('value', branch._get_config().get_option('name'))
2071
2072    def test_backwards_compat_set_option_with_dict(self):
2073        self.setup_smart_server_with_call_log()
2074        branch = self.make_branch('.')
2075        verb = b'Branch.set_config_option_dict'
2076        self.disable_verb(verb)
2077        branch.lock_write()
2078        self.addCleanup(branch.unlock)
2079        self.reset_smart_call_log()
2080        config = branch._get_config()
2081        value_dict = {'ascii': 'a', u'unicode \N{WATCH}': u'\N{INTERROBANG}'}
2082        config.set_option(value_dict, 'name')
2083        self.assertLength(11, self.hpss_calls)
2084        self.assertEqual(value_dict, branch._get_config().get_option('name'))
2085
2086
2087class TestBranchGetPutConfigStore(RemoteBranchTestCase):
2088
2089    def test_get_branch_conf(self):
2090        # in an empty branch we decode the response properly
2091        client = FakeClient()
2092        client.add_expected_call(
2093            b'Branch.get_stacked_on_url', (b'memory:///',),
2094            b'error', (b'NotStacked',),)
2095        client.add_success_response_with_body(b'# config file body', b'ok')
2096        transport = MemoryTransport()
2097        branch = self.make_remote_branch(transport, client)
2098        config = branch.get_config_stack()
2099        config.get("email")
2100        config.get("log_format")
2101        self.assertEqual(
2102            [('call', b'Branch.get_stacked_on_url', (b'memory:///',)),
2103             ('call_expecting_body', b'Branch.get_config_file', (b'memory:///',))],
2104            client._calls)
2105
2106    def test_set_branch_conf(self):
2107        client = FakeClient()
2108        client.add_expected_call(
2109            b'Branch.get_stacked_on_url', (b'memory:///',),
2110            b'error', (b'NotStacked',),)
2111        client.add_expected_call(
2112            b'Branch.lock_write', (b'memory:///', b'', b''),
2113            b'success', (b'ok', b'branch token', b'repo token'))
2114        client.add_expected_call(
2115            b'Branch.get_config_file', (b'memory:///', ),
2116            b'success', (b'ok', ), b"# line 1\n")
2117        client.add_expected_call(
2118            b'Branch.get_config_file', (b'memory:///', ),
2119            b'success', (b'ok', ), b"# line 1\n")
2120        client.add_expected_call(
2121            b'Branch.put_config_file', (b'memory:///', b'branch token',
2122                                        b'repo token'),
2123            b'success', (b'ok',))
2124        client.add_expected_call(
2125            b'Branch.unlock', (b'memory:///', b'branch token', b'repo token'),
2126            b'success', (b'ok',))
2127        transport = MemoryTransport()
2128        branch = self.make_remote_branch(transport, client)
2129        branch.lock_write()
2130        config = branch.get_config_stack()
2131        config.set('email', 'The Dude <lebowski@example.com>')
2132        branch.unlock()
2133        self.assertFinished(client)
2134        self.assertEqual(
2135            [('call', b'Branch.get_stacked_on_url', (b'memory:///',)),
2136             ('call', b'Branch.lock_write', (b'memory:///', b'', b'')),
2137             ('call_expecting_body', b'Branch.get_config_file',
2138                 (b'memory:///',)),
2139             ('call_expecting_body', b'Branch.get_config_file',
2140                 (b'memory:///',)),
2141             ('call_with_body_bytes_expecting_body', b'Branch.put_config_file',
2142                 (b'memory:///', b'branch token', b'repo token'),
2143                 b'# line 1\nemail = The Dude <lebowski@example.com>\n'),
2144             ('call', b'Branch.unlock',
2145                 (b'memory:///', b'branch token', b'repo token'))],
2146            client._calls)
2147
2148
2149class TestBranchLockWrite(RemoteBranchTestCase):
2150
2151    def test_lock_write_unlockable(self):
2152        transport = MemoryTransport()
2153        client = FakeClient(transport.base)
2154        client.add_expected_call(
2155            b'Branch.get_stacked_on_url', (b'quack/',),
2156            b'error', (b'NotStacked',),)
2157        client.add_expected_call(
2158            b'Branch.lock_write', (b'quack/', b'', b''),
2159            b'error', (b'UnlockableTransport',))
2160        transport.mkdir('quack')
2161        transport = transport.clone('quack')
2162        branch = self.make_remote_branch(transport, client)
2163        self.assertRaises(errors.UnlockableTransport, branch.lock_write)
2164        self.assertFinished(client)
2165
2166
2167class TestBranchRevisionIdToRevno(RemoteBranchTestCase):
2168
2169    def test_simple(self):
2170        transport = MemoryTransport()
2171        client = FakeClient(transport.base)
2172        client.add_expected_call(
2173            b'Branch.get_stacked_on_url', (b'quack/',),
2174            b'error', (b'NotStacked',),)
2175        client.add_expected_call(
2176            b'Branch.revision_id_to_revno', (b'quack/', b'null:'),
2177            b'success', (b'ok', b'0',),)
2178        client.add_expected_call(
2179            b'Branch.revision_id_to_revno', (b'quack/', b'unknown'),
2180            b'error', (b'NoSuchRevision', b'unknown',),)
2181        transport.mkdir('quack')
2182        transport = transport.clone('quack')
2183        branch = self.make_remote_branch(transport, client)
2184        self.assertEqual(0, branch.revision_id_to_revno(b'null:'))
2185        self.assertRaises(errors.NoSuchRevision,
2186                          branch.revision_id_to_revno, b'unknown')
2187        self.assertFinished(client)
2188
2189    def test_dotted(self):
2190        transport = MemoryTransport()
2191        client = FakeClient(transport.base)
2192        client.add_expected_call(
2193            b'Branch.get_stacked_on_url', (b'quack/',),
2194            b'error', (b'NotStacked',),)
2195        client.add_expected_call(
2196            b'Branch.revision_id_to_revno', (b'quack/', b'null:'),
2197            b'success', (b'ok', b'0',),)
2198        client.add_expected_call(
2199            b'Branch.revision_id_to_revno', (b'quack/', b'unknown'),
2200            b'error', (b'NoSuchRevision', b'unknown',),)
2201        transport.mkdir('quack')
2202        transport = transport.clone('quack')
2203        branch = self.make_remote_branch(transport, client)
2204        self.assertEqual((0, ), branch.revision_id_to_dotted_revno(b'null:'))
2205        self.assertRaises(errors.NoSuchRevision,
2206                          branch.revision_id_to_dotted_revno, b'unknown')
2207        self.assertFinished(client)
2208
2209    def test_ghost_revid(self):
2210        transport = MemoryTransport()
2211        client = FakeClient(transport.base)
2212        client.add_expected_call(
2213            b'Branch.get_stacked_on_url', (b'quack/',),
2214            b'error', (b'NotStacked',),)
2215        # Some older versions of bzr/brz didn't explicitly return
2216        # GhostRevisionsHaveNoRevno
2217        client.add_expected_call(
2218            b'Branch.revision_id_to_revno', (b'quack/', b'revid'),
2219            b'error', (b'error', b'GhostRevisionsHaveNoRevno',
2220                       b'The reivison {revid} was not found because there was '
2221                       b'a ghost at {ghost-revid}'))
2222        client.add_expected_call(
2223            b'Branch.revision_id_to_revno', (b'quack/', b'revid'),
2224            b'error', (b'GhostRevisionsHaveNoRevno', b'revid', b'ghost-revid',))
2225        transport.mkdir('quack')
2226        transport = transport.clone('quack')
2227        branch = self.make_remote_branch(transport, client)
2228        self.assertRaises(errors.GhostRevisionsHaveNoRevno,
2229                          branch.revision_id_to_dotted_revno, b'revid')
2230        self.assertRaises(errors.GhostRevisionsHaveNoRevno,
2231                          branch.revision_id_to_dotted_revno, b'revid')
2232        self.assertFinished(client)
2233
2234    def test_dotted_no_smart_verb(self):
2235        self.setup_smart_server_with_call_log()
2236        branch = self.make_branch('.')
2237        self.disable_verb(b'Branch.revision_id_to_revno')
2238        self.reset_smart_call_log()
2239        self.assertEqual((0, ),
2240                         branch.revision_id_to_dotted_revno(b'null:'))
2241        self.assertLength(8, self.hpss_calls)
2242
2243
2244class TestBzrDirGetSetConfig(RemoteBzrDirTestCase):
2245
2246    def test__get_config(self):
2247        client = FakeClient()
2248        client.add_success_response_with_body(b'default_stack_on = /\n', b'ok')
2249        transport = MemoryTransport()
2250        bzrdir = self.make_remote_bzrdir(transport, client)
2251        config = bzrdir.get_config()
2252        self.assertEqual('/', config.get_default_stack_on())
2253        self.assertEqual(
2254            [('call_expecting_body', b'BzrDir.get_config_file',
2255                (b'memory:///',))],
2256            client._calls)
2257
2258    def test_set_option_uses_vfs(self):
2259        self.setup_smart_server_with_call_log()
2260        bzrdir = self.make_controldir('.')
2261        self.reset_smart_call_log()
2262        config = bzrdir.get_config()
2263        config.set_default_stack_on('/')
2264        self.assertLength(4, self.hpss_calls)
2265
2266    def test_backwards_compat_get_option(self):
2267        self.setup_smart_server_with_call_log()
2268        bzrdir = self.make_controldir('.')
2269        verb = b'BzrDir.get_config_file'
2270        self.disable_verb(verb)
2271        self.reset_smart_call_log()
2272        self.assertEqual(None,
2273                         bzrdir._get_config().get_option('default_stack_on'))
2274        self.assertLength(4, self.hpss_calls)
2275
2276
2277class TestTransportIsReadonly(tests.TestCase):
2278
2279    def test_true(self):
2280        client = FakeClient()
2281        client.add_success_response(b'yes')
2282        transport = RemoteTransport('bzr://example.com/', medium=False,
2283                                    _client=client)
2284        self.assertEqual(True, transport.is_readonly())
2285        self.assertEqual(
2286            [('call', b'Transport.is_readonly', ())],
2287            client._calls)
2288
2289    def test_false(self):
2290        client = FakeClient()
2291        client.add_success_response(b'no')
2292        transport = RemoteTransport('bzr://example.com/', medium=False,
2293                                    _client=client)
2294        self.assertEqual(False, transport.is_readonly())
2295        self.assertEqual(
2296            [('call', b'Transport.is_readonly', ())],
2297            client._calls)
2298
2299    def test_error_from_old_server(self):
2300        """bzr 0.15 and earlier servers don't recognise the is_readonly verb.
2301
2302        Clients should treat it as a "no" response, because is_readonly is only
2303        advisory anyway (a transport could be read-write, but then the
2304        underlying filesystem could be readonly anyway).
2305        """
2306        client = FakeClient()
2307        client.add_unknown_method_response(b'Transport.is_readonly')
2308        transport = RemoteTransport('bzr://example.com/', medium=False,
2309                                    _client=client)
2310        self.assertEqual(False, transport.is_readonly())
2311        self.assertEqual(
2312            [('call', b'Transport.is_readonly', ())],
2313            client._calls)
2314
2315
2316class TestTransportMkdir(tests.TestCase):
2317
2318    def test_permissiondenied(self):
2319        client = FakeClient()
2320        client.add_error_response(
2321            b'PermissionDenied', b'remote path', b'extra')
2322        transport = RemoteTransport('bzr://example.com/', medium=False,
2323                                    _client=client)
2324        exc = self.assertRaises(
2325            errors.PermissionDenied, transport.mkdir, 'client path')
2326        expected_error = errors.PermissionDenied('/client path', 'extra')
2327        self.assertEqual(expected_error, exc)
2328
2329
2330class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
2331
2332    def test_defaults_to_none(self):
2333        t = RemoteSSHTransport('bzr+ssh://example.com')
2334        self.assertIs(None, t._get_credentials()[0])
2335
2336    def test_uses_authentication_config(self):
2337        conf = config.AuthenticationConfig()
2338        conf._get_config().update(
2339            {'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
2340                             'example.com'}})
2341        conf._save()
2342        t = RemoteSSHTransport('bzr+ssh://example.com')
2343        self.assertEqual('bar', t._get_credentials()[0])
2344
2345
2346class TestRemoteRepository(TestRemote):
2347    """Base for testing RemoteRepository protocol usage.
2348
2349    These tests contain frozen requests and responses.  We want any changes to
2350    what is sent or expected to be require a thoughtful update to these tests
2351    because they might break compatibility with different-versioned servers.
2352    """
2353
2354    def setup_fake_client_and_repository(self, transport_path):
2355        """Create the fake client and repository for testing with.
2356
2357        There's no real server here; we just have canned responses sent
2358        back one by one.
2359
2360        :param transport_path: Path below the root of the MemoryTransport
2361            where the repository will be created.
2362        """
2363        transport = MemoryTransport()
2364        transport.mkdir(transport_path)
2365        client = FakeClient(transport.base)
2366        transport = transport.clone(transport_path)
2367        # we do not want bzrdir to make any remote calls
2368        bzrdir = RemoteBzrDir(transport, RemoteBzrDirFormat(),
2369                              _client=False)
2370        repo = RemoteRepository(bzrdir, None, _client=client)
2371        return repo, client
2372
2373
2374def remoted_description(format):
2375    return 'Remote: ' + format.get_format_description()
2376
2377
2378class TestBranchFormat(tests.TestCase):
2379
2380    def test_get_format_description(self):
2381        remote_format = RemoteBranchFormat()
2382        real_format = branch.format_registry.get_default()
2383        remote_format._network_name = real_format.network_name()
2384        self.assertEqual(remoted_description(real_format),
2385                         remote_format.get_format_description())
2386
2387
2388class TestRepositoryFormat(TestRemoteRepository):
2389
2390    def test_fast_delta(self):
2391        true_name = groupcompress_repo.RepositoryFormat2a().network_name()
2392        true_format = RemoteRepositoryFormat()
2393        true_format._network_name = true_name
2394        self.assertEqual(True, true_format.fast_deltas)
2395        false_name = knitpack_repo.RepositoryFormatKnitPack1().network_name()
2396        false_format = RemoteRepositoryFormat()
2397        false_format._network_name = false_name
2398        self.assertEqual(False, false_format.fast_deltas)
2399
2400    def test_get_format_description(self):
2401        remote_repo_format = RemoteRepositoryFormat()
2402        real_format = repository.format_registry.get_default()
2403        remote_repo_format._network_name = real_format.network_name()
2404        self.assertEqual(remoted_description(real_format),
2405                         remote_repo_format.get_format_description())
2406
2407
2408class TestRepositoryAllRevisionIds(TestRemoteRepository):
2409
2410    def test_empty(self):
2411        transport_path = 'quack'
2412        repo, client = self.setup_fake_client_and_repository(transport_path)
2413        client.add_success_response_with_body(b'', b'ok')
2414        self.assertEqual([], repo.all_revision_ids())
2415        self.assertEqual(
2416            [('call_expecting_body', b'Repository.all_revision_ids',
2417              (b'quack/',))],
2418            client._calls)
2419
2420    def test_with_some_content(self):
2421        transport_path = 'quack'
2422        repo, client = self.setup_fake_client_and_repository(transport_path)
2423        client.add_success_response_with_body(
2424            b'rev1\nrev2\nanotherrev\n', b'ok')
2425        self.assertEqual(
2426            set([b"rev1", b"rev2", b"anotherrev"]),
2427            set(repo.all_revision_ids()))
2428        self.assertEqual(
2429            [('call_expecting_body', b'Repository.all_revision_ids',
2430              (b'quack/',))],
2431            client._calls)
2432
2433
2434class TestRepositoryGatherStats(TestRemoteRepository):
2435
2436    def test_revid_none(self):
2437        # ('ok',), body with revisions and size
2438        transport_path = 'quack'
2439        repo, client = self.setup_fake_client_and_repository(transport_path)
2440        client.add_success_response_with_body(
2441            b'revisions: 2\nsize: 18\n', b'ok')
2442        result = repo.gather_stats(None)
2443        self.assertEqual(
2444            [('call_expecting_body', b'Repository.gather_stats',
2445              (b'quack/', b'', b'no'))],
2446            client._calls)
2447        self.assertEqual({'revisions': 2, 'size': 18}, result)
2448
2449    def test_revid_no_committers(self):
2450        # ('ok',), body without committers
2451        body = (b'firstrev: 123456.300 3600\n'
2452                b'latestrev: 654231.400 0\n'
2453                b'revisions: 2\n'
2454                b'size: 18\n')
2455        transport_path = 'quick'
2456        revid = u'\xc8'.encode('utf8')
2457        repo, client = self.setup_fake_client_and_repository(transport_path)
2458        client.add_success_response_with_body(body, b'ok')
2459        result = repo.gather_stats(revid)
2460        self.assertEqual(
2461            [('call_expecting_body', b'Repository.gather_stats',
2462              (b'quick/', revid, b'no'))],
2463            client._calls)
2464        self.assertEqual({'revisions': 2, 'size': 18,
2465                          'firstrev': (123456.300, 3600),
2466                          'latestrev': (654231.400, 0), },
2467                         result)
2468
2469    def test_revid_with_committers(self):
2470        # ('ok',), body with committers
2471        body = (b'committers: 128\n'
2472                b'firstrev: 123456.300 3600\n'
2473                b'latestrev: 654231.400 0\n'
2474                b'revisions: 2\n'
2475                b'size: 18\n')
2476        transport_path = 'buick'
2477        revid = u'\xc8'.encode('utf8')
2478        repo, client = self.setup_fake_client_and_repository(transport_path)
2479        client.add_success_response_with_body(body, b'ok')
2480        result = repo.gather_stats(revid, True)
2481        self.assertEqual(
2482            [('call_expecting_body', b'Repository.gather_stats',
2483              (b'buick/', revid, b'yes'))],
2484            client._calls)
2485        self.assertEqual({'revisions': 2, 'size': 18,
2486                          'committers': 128,
2487                          'firstrev': (123456.300, 3600),
2488                          'latestrev': (654231.400, 0), },
2489                         result)
2490
2491
2492class TestRepositoryBreakLock(TestRemoteRepository):
2493
2494    def test_break_lock(self):
2495        transport_path = 'quack'
2496        repo, client = self.setup_fake_client_and_repository(transport_path)
2497        client.add_success_response(b'ok')
2498        repo.break_lock()
2499        self.assertEqual(
2500            [('call', b'Repository.break_lock', (b'quack/',))],
2501            client._calls)
2502
2503
2504class TestRepositoryGetSerializerFormat(TestRemoteRepository):
2505
2506    def test_get_serializer_format(self):
2507        transport_path = 'hill'
2508        repo, client = self.setup_fake_client_and_repository(transport_path)
2509        client.add_success_response(b'ok', b'7')
2510        self.assertEqual(b'7', repo.get_serializer_format())
2511        self.assertEqual(
2512            [('call', b'VersionedFileRepository.get_serializer_format',
2513              (b'hill/', ))],
2514            client._calls)
2515
2516
2517class TestRepositoryReconcile(TestRemoteRepository):
2518
2519    def test_reconcile(self):
2520        transport_path = 'hill'
2521        repo, client = self.setup_fake_client_and_repository(transport_path)
2522        body = (b"garbage_inventories: 2\n"
2523                b"inconsistent_parents: 3\n")
2524        client.add_expected_call(
2525            b'Repository.lock_write', (b'hill/', b''),
2526            b'success', (b'ok', b'a token'))
2527        client.add_success_response_with_body(body, b'ok')
2528        reconciler = repo.reconcile()
2529        self.assertEqual(
2530            [('call', b'Repository.lock_write', (b'hill/', b'')),
2531             ('call_expecting_body', b'Repository.reconcile',
2532                (b'hill/', b'a token'))],
2533            client._calls)
2534        self.assertEqual(2, reconciler.garbage_inventories)
2535        self.assertEqual(3, reconciler.inconsistent_parents)
2536
2537
2538class TestRepositoryGetRevisionSignatureText(TestRemoteRepository):
2539
2540    def test_text(self):
2541        # ('ok',), body with signature text
2542        transport_path = 'quack'
2543        repo, client = self.setup_fake_client_and_repository(transport_path)
2544        client.add_success_response_with_body(
2545            b'THETEXT', b'ok')
2546        self.assertEqual(b"THETEXT", repo.get_signature_text(b"revid"))
2547        self.assertEqual(
2548            [('call_expecting_body', b'Repository.get_revision_signature_text',
2549              (b'quack/', b'revid'))],
2550            client._calls)
2551
2552    def test_no_signature(self):
2553        transport_path = 'quick'
2554        repo, client = self.setup_fake_client_and_repository(transport_path)
2555        client.add_error_response(b'nosuchrevision', b'unknown')
2556        self.assertRaises(errors.NoSuchRevision, repo.get_signature_text,
2557                          b"unknown")
2558        self.assertEqual(
2559            [('call_expecting_body', b'Repository.get_revision_signature_text',
2560              (b'quick/', b'unknown'))],
2561            client._calls)
2562
2563
2564class TestRepositoryGetGraph(TestRemoteRepository):
2565
2566    def test_get_graph(self):
2567        # get_graph returns a graph with a custom parents provider.
2568        transport_path = 'quack'
2569        repo, client = self.setup_fake_client_and_repository(transport_path)
2570        graph = repo.get_graph()
2571        self.assertNotEqual(graph._parents_provider, repo)
2572
2573
2574class TestRepositoryAddSignatureText(TestRemoteRepository):
2575
2576    def test_add_signature_text(self):
2577        transport_path = 'quack'
2578        repo, client = self.setup_fake_client_and_repository(transport_path)
2579        client.add_expected_call(
2580            b'Repository.lock_write', (b'quack/', b''),
2581            b'success', (b'ok', b'a token'))
2582        client.add_expected_call(
2583            b'Repository.start_write_group', (b'quack/', b'a token'),
2584            b'success', (b'ok', (b'token1', )))
2585        client.add_expected_call(
2586            b'Repository.add_signature_text', (b'quack/', b'a token', b'rev1',
2587                                               b'token1'),
2588            b'success', (b'ok', ), None)
2589        repo.lock_write()
2590        repo.start_write_group()
2591        self.assertIs(
2592            None, repo.add_signature_text(b"rev1", b"every bloody emperor"))
2593        self.assertEqual(
2594            ('call_with_body_bytes_expecting_body',
2595             b'Repository.add_signature_text',
2596                (b'quack/', b'a token', b'rev1', b'token1'),
2597             b'every bloody emperor'),
2598            client._calls[-1])
2599
2600
2601class TestRepositoryGetParentMap(TestRemoteRepository):
2602
2603    def test_get_parent_map_caching(self):
2604        # get_parent_map returns from cache until unlock()
2605        # setup a reponse with two revisions
2606        r1 = u'\u0e33'.encode('utf8')
2607        r2 = u'\u0dab'.encode('utf8')
2608        lines = [b' '.join([r2, r1]), r1]
2609        encoded_body = bz2.compress(b'\n'.join(lines))
2610
2611        transport_path = 'quack'
2612        repo, client = self.setup_fake_client_and_repository(transport_path)
2613        client.add_success_response_with_body(encoded_body, b'ok')
2614        client.add_success_response_with_body(encoded_body, b'ok')
2615        repo.lock_read()
2616        graph = repo.get_graph()
2617        parents = graph.get_parent_map([r2])
2618        self.assertEqual({r2: (r1,)}, parents)
2619        # locking and unlocking deeper should not reset
2620        repo.lock_read()
2621        repo.unlock()
2622        parents = graph.get_parent_map([r1])
2623        self.assertEqual({r1: (NULL_REVISION,)}, parents)
2624        self.assertEqual(
2625            [('call_with_body_bytes_expecting_body',
2626              b'Repository.get_parent_map', (b'quack/',
2627                                             b'include-missing:', r2),
2628              b'\n\n0')],
2629            client._calls)
2630        repo.unlock()
2631        # now we call again, and it should use the second response.
2632        repo.lock_read()
2633        graph = repo.get_graph()
2634        parents = graph.get_parent_map([r1])
2635        self.assertEqual({r1: (NULL_REVISION,)}, parents)
2636        self.assertEqual(
2637            [('call_with_body_bytes_expecting_body',
2638              b'Repository.get_parent_map', (b'quack/',
2639                                             b'include-missing:', r2),
2640              b'\n\n0'),
2641             ('call_with_body_bytes_expecting_body',
2642              b'Repository.get_parent_map', (b'quack/',
2643                                             b'include-missing:', r1),
2644              b'\n\n0'),
2645             ],
2646            client._calls)
2647        repo.unlock()
2648
2649    def test_get_parent_map_reconnects_if_unknown_method(self):
2650        transport_path = 'quack'
2651        rev_id = b'revision-id'
2652        repo, client = self.setup_fake_client_and_repository(transport_path)
2653        client.add_unknown_method_response(b'Repository.get_parent_map')
2654        client.add_success_response_with_body(rev_id, b'ok')
2655        self.assertFalse(client._medium._is_remote_before((1, 2)))
2656        parents = repo.get_parent_map([rev_id])
2657        self.assertEqual(
2658            [('call_with_body_bytes_expecting_body',
2659              b'Repository.get_parent_map',
2660              (b'quack/', b'include-missing:', rev_id), b'\n\n0'),
2661             ('disconnect medium',),
2662             ('call_expecting_body', b'Repository.get_revision_graph',
2663              (b'quack/', b''))],
2664            client._calls)
2665        # The medium is now marked as being connected to an older server
2666        self.assertTrue(client._medium._is_remote_before((1, 2)))
2667        self.assertEqual({rev_id: (b'null:',)}, parents)
2668
2669    def test_get_parent_map_fallback_parentless_node(self):
2670        """get_parent_map falls back to get_revision_graph on old servers.  The
2671        results from get_revision_graph are tweaked to match the get_parent_map
2672        API.
2673
2674        Specifically, a {key: ()} result from get_revision_graph means "no
2675        parents" for that key, which in get_parent_map results should be
2676        represented as {key: ('null:',)}.
2677
2678        This is the test for https://bugs.launchpad.net/bzr/+bug/214894
2679        """
2680        rev_id = b'revision-id'
2681        transport_path = 'quack'
2682        repo, client = self.setup_fake_client_and_repository(transport_path)
2683        client.add_success_response_with_body(rev_id, b'ok')
2684        client._medium._remember_remote_is_before((1, 2))
2685        parents = repo.get_parent_map([rev_id])
2686        self.assertEqual(
2687            [('call_expecting_body', b'Repository.get_revision_graph',
2688              (b'quack/', b''))],
2689            client._calls)
2690        self.assertEqual({rev_id: (b'null:',)}, parents)
2691
2692    def test_get_parent_map_unexpected_response(self):
2693        repo, client = self.setup_fake_client_and_repository('path')
2694        client.add_success_response(b'something unexpected!')
2695        self.assertRaises(
2696            errors.UnexpectedSmartServerResponse,
2697            repo.get_parent_map, [b'a-revision-id'])
2698
2699    def test_get_parent_map_negative_caches_missing_keys(self):
2700        self.setup_smart_server_with_call_log()
2701        repo = self.make_repository('foo')
2702        self.assertIsInstance(repo, RemoteRepository)
2703        repo.lock_read()
2704        self.addCleanup(repo.unlock)
2705        self.reset_smart_call_log()
2706        graph = repo.get_graph()
2707        self.assertEqual(
2708            {}, graph.get_parent_map([b'some-missing', b'other-missing']))
2709        self.assertLength(1, self.hpss_calls)
2710        # No call if we repeat this
2711        self.reset_smart_call_log()
2712        graph = repo.get_graph()
2713        self.assertEqual(
2714            {}, graph.get_parent_map([b'some-missing', b'other-missing']))
2715        self.assertLength(0, self.hpss_calls)
2716        # Asking for more unknown keys makes a request.
2717        self.reset_smart_call_log()
2718        graph = repo.get_graph()
2719        self.assertEqual(
2720            {}, graph.get_parent_map([b'some-missing', b'other-missing',
2721                                     b'more-missing']))
2722        self.assertLength(1, self.hpss_calls)
2723
2724    def disableExtraResults(self):
2725        self.overrideAttr(SmartServerRepositoryGetParentMap,
2726                          'no_extra_results', True)
2727
2728    def test_null_cached_missing_and_stop_key(self):
2729        self.setup_smart_server_with_call_log()
2730        # Make a branch with a single revision.
2731        builder = self.make_branch_builder('foo')
2732        builder.start_series()
2733        builder.build_snapshot(None, [
2734            ('add', ('', b'root-id', 'directory', ''))],
2735            revision_id=b'first')
2736        builder.finish_series()
2737        branch = builder.get_branch()
2738        repo = branch.repository
2739        self.assertIsInstance(repo, RemoteRepository)
2740        # Stop the server from sending extra results.
2741        self.disableExtraResults()
2742        repo.lock_read()
2743        self.addCleanup(repo.unlock)
2744        self.reset_smart_call_log()
2745        graph = repo.get_graph()
2746        # Query for b'first' and b'null:'.  Because b'null:' is a parent of
2747        # 'first' it will be a candidate for the stop_keys of subsequent
2748        # requests, and because b'null:' was queried but not returned it will
2749        # be cached as missing.
2750        self.assertEqual({b'first': (b'null:',)},
2751                         graph.get_parent_map([b'first', b'null:']))
2752        # Now query for another key.  This request will pass along a recipe of
2753        # start and stop keys describing the already cached results, and this
2754        # recipe's revision count must be correct (or else it will trigger an
2755        # error from the server).
2756        self.assertEqual({}, graph.get_parent_map([b'another-key']))
2757        # This assertion guards against disableExtraResults silently failing to
2758        # work, thus invalidating the test.
2759        self.assertLength(2, self.hpss_calls)
2760
2761    def test_get_parent_map_gets_ghosts_from_result(self):
2762        # asking for a revision should negatively cache close ghosts in its
2763        # ancestry.
2764        self.setup_smart_server_with_call_log()
2765        tree = self.make_branch_and_memory_tree('foo')
2766        with tree.lock_write():
2767            builder = treebuilder.TreeBuilder()
2768            builder.start_tree(tree)
2769            builder.build([])
2770            builder.finish_tree()
2771            tree.set_parent_ids([b'non-existant'],
2772                                allow_leftmost_as_ghost=True)
2773            rev_id = tree.commit('')
2774        tree.lock_read()
2775        self.addCleanup(tree.unlock)
2776        repo = tree.branch.repository
2777        self.assertIsInstance(repo, RemoteRepository)
2778        # ask for rev_id
2779        repo.get_parent_map([rev_id])
2780        self.reset_smart_call_log()
2781        # Now asking for rev_id's ghost parent should not make calls
2782        self.assertEqual({}, repo.get_parent_map([b'non-existant']))
2783        self.assertLength(0, self.hpss_calls)
2784
2785    def test_exposes_get_cached_parent_map(self):
2786        """RemoteRepository exposes get_cached_parent_map from
2787        _unstacked_provider
2788        """
2789        r1 = u'\u0e33'.encode('utf8')
2790        r2 = u'\u0dab'.encode('utf8')
2791        lines = [b' '.join([r2, r1]), r1]
2792        encoded_body = bz2.compress(b'\n'.join(lines))
2793
2794        transport_path = 'quack'
2795        repo, client = self.setup_fake_client_and_repository(transport_path)
2796        client.add_success_response_with_body(encoded_body, b'ok')
2797        repo.lock_read()
2798        # get_cached_parent_map should *not* trigger an RPC
2799        self.assertEqual({}, repo.get_cached_parent_map([r1]))
2800        self.assertEqual([], client._calls)
2801        self.assertEqual({r2: (r1,)}, repo.get_parent_map([r2]))
2802        self.assertEqual({r1: (NULL_REVISION,)},
2803                         repo.get_cached_parent_map([r1]))
2804        self.assertEqual(
2805            [('call_with_body_bytes_expecting_body',
2806              b'Repository.get_parent_map', (b'quack/',
2807                                             b'include-missing:', r2),
2808              b'\n\n0')],
2809            client._calls)
2810        repo.unlock()
2811
2812
2813class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
2814
2815    def test_allows_new_revisions(self):
2816        """get_parent_map's results can be updated by commit."""
2817        smart_server = test_server.SmartTCPServer_for_testing()
2818        self.start_server(smart_server)
2819        self.make_branch('branch')
2820        branch = Branch.open(smart_server.get_url() + '/branch')
2821        tree = branch.create_checkout('tree', lightweight=True)
2822        tree.lock_write()
2823        self.addCleanup(tree.unlock)
2824        graph = tree.branch.repository.get_graph()
2825        # This provides an opportunity for the missing rev-id to be cached.
2826        self.assertEqual({}, graph.get_parent_map([b'rev1']))
2827        tree.commit('message', rev_id=b'rev1')
2828        graph = tree.branch.repository.get_graph()
2829        self.assertEqual({b'rev1': (b'null:',)},
2830                         graph.get_parent_map([b'rev1']))
2831
2832
2833class TestRepositoryGetRevisions(TestRemoteRepository):
2834
2835    def test_hpss_missing_revision(self):
2836        transport_path = 'quack'
2837        repo, client = self.setup_fake_client_and_repository(transport_path)
2838        client.add_success_response_with_body(
2839            b'', b'ok', b'10')
2840        self.assertRaises(errors.NoSuchRevision, repo.get_revisions,
2841                          [b'somerev1', b'anotherrev2'])
2842        self.assertEqual(
2843            [('call_with_body_bytes_expecting_body',
2844              b'Repository.iter_revisions', (b'quack/', ),
2845              b"somerev1\nanotherrev2")],
2846            client._calls)
2847
2848    def test_hpss_get_single_revision(self):
2849        transport_path = 'quack'
2850        repo, client = self.setup_fake_client_and_repository(transport_path)
2851        somerev1 = Revision(b"somerev1")
2852        somerev1.committer = "Joe Committer <joe@example.com>"
2853        somerev1.timestamp = 1321828927
2854        somerev1.timezone = -60
2855        somerev1.inventory_sha1 = b"691b39be74c67b1212a75fcb19c433aaed903c2b"
2856        somerev1.message = "Message"
2857        body = zlib.compress(b''.join(chk_bencode_serializer.write_revision_to_lines(
2858            somerev1)))
2859        # Split up body into two bits to make sure the zlib compression object
2860        # gets data fed twice.
2861        client.add_success_response_with_body(
2862            [body[:10], body[10:]], b'ok', b'10')
2863        revs = repo.get_revisions([b'somerev1'])
2864        self.assertEqual(revs, [somerev1])
2865        self.assertEqual(
2866            [('call_with_body_bytes_expecting_body',
2867              b'Repository.iter_revisions',
2868              (b'quack/', ), b"somerev1")],
2869            client._calls)
2870
2871
2872class TestRepositoryGetRevisionGraph(TestRemoteRepository):
2873
2874    def test_null_revision(self):
2875        # a null revision has the predictable result {}, we should have no wire
2876        # traffic when calling it with this argument
2877        transport_path = 'empty'
2878        repo, client = self.setup_fake_client_and_repository(transport_path)
2879        client.add_success_response(b'notused')
2880        # actual RemoteRepository.get_revision_graph is gone, but there's an
2881        # equivalent private method for testing
2882        result = repo._get_revision_graph(NULL_REVISION)
2883        self.assertEqual([], client._calls)
2884        self.assertEqual({}, result)
2885
2886    def test_none_revision(self):
2887        # with none we want the entire graph
2888        r1 = u'\u0e33'.encode('utf8')
2889        r2 = u'\u0dab'.encode('utf8')
2890        lines = [b' '.join([r2, r1]), r1]
2891        encoded_body = b'\n'.join(lines)
2892
2893        transport_path = 'sinhala'
2894        repo, client = self.setup_fake_client_and_repository(transport_path)
2895        client.add_success_response_with_body(encoded_body, b'ok')
2896        # actual RemoteRepository.get_revision_graph is gone, but there's an
2897        # equivalent private method for testing
2898        result = repo._get_revision_graph(None)
2899        self.assertEqual(
2900            [('call_expecting_body', b'Repository.get_revision_graph',
2901              (b'sinhala/', b''))],
2902            client._calls)
2903        self.assertEqual({r1: (), r2: (r1, )}, result)
2904
2905    def test_specific_revision(self):
2906        # with a specific revision we want the graph for that
2907        # with none we want the entire graph
2908        r11 = u'\u0e33'.encode('utf8')
2909        r12 = u'\xc9'.encode('utf8')
2910        r2 = u'\u0dab'.encode('utf8')
2911        lines = [b' '.join([r2, r11, r12]), r11, r12]
2912        encoded_body = b'\n'.join(lines)
2913
2914        transport_path = 'sinhala'
2915        repo, client = self.setup_fake_client_and_repository(transport_path)
2916        client.add_success_response_with_body(encoded_body, b'ok')
2917        result = repo._get_revision_graph(r2)
2918        self.assertEqual(
2919            [('call_expecting_body', b'Repository.get_revision_graph',
2920              (b'sinhala/', r2))],
2921            client._calls)
2922        self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
2923
2924    def test_no_such_revision(self):
2925        revid = b'123'
2926        transport_path = 'sinhala'
2927        repo, client = self.setup_fake_client_and_repository(transport_path)
2928        client.add_error_response(b'nosuchrevision', revid)
2929        # also check that the right revision is reported in the error
2930        self.assertRaises(errors.NoSuchRevision,
2931                          repo._get_revision_graph, revid)
2932        self.assertEqual(
2933            [('call_expecting_body', b'Repository.get_revision_graph',
2934              (b'sinhala/', revid))],
2935            client._calls)
2936
2937    def test_unexpected_error(self):
2938        revid = '123'
2939        transport_path = 'sinhala'
2940        repo, client = self.setup_fake_client_and_repository(transport_path)
2941        client.add_error_response(b'AnUnexpectedError')
2942        e = self.assertRaises(errors.UnknownErrorFromSmartServer,
2943                              repo._get_revision_graph, revid)
2944        self.assertEqual((b'AnUnexpectedError',), e.error_tuple)
2945
2946
2947class TestRepositoryGetRevIdForRevno(TestRemoteRepository):
2948
2949    def test_ok(self):
2950        repo, client = self.setup_fake_client_and_repository('quack')
2951        client.add_expected_call(
2952            b'Repository.get_rev_id_for_revno', (b'quack/',
2953                                                 5, (42, b'rev-foo')),
2954            b'success', (b'ok', b'rev-five'))
2955        result = repo.get_rev_id_for_revno(5, (42, b'rev-foo'))
2956        self.assertEqual((True, b'rev-five'), result)
2957        self.assertFinished(client)
2958
2959    def test_history_incomplete(self):
2960        repo, client = self.setup_fake_client_and_repository('quack')
2961        client.add_expected_call(
2962            b'Repository.get_rev_id_for_revno', (b'quack/',
2963                                                 5, (42, b'rev-foo')),
2964            b'success', (b'history-incomplete', 10, b'rev-ten'))
2965        result = repo.get_rev_id_for_revno(5, (42, b'rev-foo'))
2966        self.assertEqual((False, (10, b'rev-ten')), result)
2967        self.assertFinished(client)
2968
2969    def test_history_incomplete_with_fallback(self):
2970        """A 'history-incomplete' response causes the fallback repository to be
2971        queried too, if one is set.
2972        """
2973        # Make a repo with a fallback repo, both using a FakeClient.
2974        format = remote.response_tuple_to_repo_format(
2975            (b'yes', b'no', b'yes', self.get_repo_format().network_name()))
2976        repo, client = self.setup_fake_client_and_repository('quack')
2977        repo._format = format
2978        fallback_repo, ignored = self.setup_fake_client_and_repository(
2979            'fallback')
2980        fallback_repo._client = client
2981        fallback_repo._format = format
2982        repo.add_fallback_repository(fallback_repo)
2983        # First the client should ask the primary repo
2984        client.add_expected_call(
2985            b'Repository.get_rev_id_for_revno', (b'quack/',
2986                                                 1, (42, b'rev-foo')),
2987            b'success', (b'history-incomplete', 2, b'rev-two'))
2988        # Then it should ask the fallback, using revno/revid from the
2989        # history-incomplete response as the known revno/revid.
2990        client.add_expected_call(
2991            b'Repository.get_rev_id_for_revno', (
2992                b'fallback/', 1, (2, b'rev-two')),
2993            b'success', (b'ok', b'rev-one'))
2994        result = repo.get_rev_id_for_revno(1, (42, b'rev-foo'))
2995        self.assertEqual((True, b'rev-one'), result)
2996        self.assertFinished(client)
2997
2998    def test_nosuchrevision(self):
2999        # 'nosuchrevision' is returned when the known-revid is not found in the
3000        # remote repo.  The client translates that response to NoSuchRevision.
3001        repo, client = self.setup_fake_client_and_repository('quack')
3002        client.add_expected_call(
3003            b'Repository.get_rev_id_for_revno', (b'quack/',
3004                                                 5, (42, b'rev-foo')),
3005            b'error', (b'nosuchrevision', b'rev-foo'))
3006        self.assertRaises(
3007            errors.NoSuchRevision,
3008            repo.get_rev_id_for_revno, 5, (42, b'rev-foo'))
3009        self.assertFinished(client)
3010
3011    def test_outofbounds(self):
3012        repo, client = self.setup_fake_client_and_repository('quack')
3013        client.add_expected_call(
3014            b'Repository.get_rev_id_for_revno', (b'quack/',
3015                                                 43, (42, b'rev-foo')),
3016            b'error', (b'revno-outofbounds', 43, 0, 42))
3017        self.assertRaises(
3018            errors.RevnoOutOfBounds,
3019            repo.get_rev_id_for_revno, 43, (42, b'rev-foo'))
3020        self.assertFinished(client)
3021
3022    def test_outofbounds_old(self):
3023        # Older versions of bzr didn't support RevnoOutOfBounds
3024        repo, client = self.setup_fake_client_and_repository('quack')
3025        client.add_expected_call(
3026            b'Repository.get_rev_id_for_revno', (b'quack/',
3027                                                 43, (42, b'rev-foo')),
3028            b'error', (
3029                b'error', b'ValueError',
3030                b'requested revno (43) is later than given known revno (42)'))
3031        self.assertRaises(
3032            errors.RevnoOutOfBounds,
3033            repo.get_rev_id_for_revno, 43, (42, b'rev-foo'))
3034        self.assertFinished(client)
3035
3036    def test_branch_fallback_locking(self):
3037        """RemoteBranch.get_rev_id takes a read lock, and tries to call the
3038        get_rev_id_for_revno verb.  If the verb is unknown the VFS fallback
3039        will be invoked, which will fail if the repo is unlocked.
3040        """
3041        self.setup_smart_server_with_call_log()
3042        tree = self.make_branch_and_memory_tree('.')
3043        tree.lock_write()
3044        tree.add('')
3045        rev1 = tree.commit('First')
3046        tree.commit('Second')
3047        tree.unlock()
3048        branch = tree.branch
3049        self.assertFalse(branch.is_locked())
3050        self.reset_smart_call_log()
3051        verb = b'Repository.get_rev_id_for_revno'
3052        self.disable_verb(verb)
3053        self.assertEqual(rev1, branch.get_rev_id(1))
3054        self.assertLength(1, [call for call in self.hpss_calls if
3055                              call.call.method == verb])
3056
3057
3058class TestRepositoryHasSignatureForRevisionId(TestRemoteRepository):
3059
3060    def test_has_signature_for_revision_id(self):
3061        # ('yes', ) for Repository.has_signature_for_revision_id -> 'True'.
3062        transport_path = 'quack'
3063        repo, client = self.setup_fake_client_and_repository(transport_path)
3064        client.add_success_response(b'yes')
3065        result = repo.has_signature_for_revision_id(b'A')
3066        self.assertEqual(
3067            [('call', b'Repository.has_signature_for_revision_id',
3068              (b'quack/', b'A'))],
3069            client._calls)
3070        self.assertEqual(True, result)
3071
3072    def test_is_not_shared(self):
3073        # ('no', ) for Repository.has_signature_for_revision_id -> 'False'.
3074        transport_path = 'qwack'
3075        repo, client = self.setup_fake_client_and_repository(transport_path)
3076        client.add_success_response(b'no')
3077        result = repo.has_signature_for_revision_id(b'A')
3078        self.assertEqual(
3079            [('call', b'Repository.has_signature_for_revision_id',
3080              (b'qwack/', b'A'))],
3081            client._calls)
3082        self.assertEqual(False, result)
3083
3084
3085class TestRepositoryPhysicalLockStatus(TestRemoteRepository):
3086
3087    def test_get_physical_lock_status_yes(self):
3088        transport_path = 'qwack'
3089        repo, client = self.setup_fake_client_and_repository(transport_path)
3090        client.add_success_response(b'yes')
3091        result = repo.get_physical_lock_status()
3092        self.assertEqual(
3093            [('call', b'Repository.get_physical_lock_status',
3094              (b'qwack/', ))],
3095            client._calls)
3096        self.assertEqual(True, result)
3097
3098    def test_get_physical_lock_status_no(self):
3099        transport_path = 'qwack'
3100        repo, client = self.setup_fake_client_and_repository(transport_path)
3101        client.add_success_response(b'no')
3102        result = repo.get_physical_lock_status()
3103        self.assertEqual(
3104            [('call', b'Repository.get_physical_lock_status',
3105              (b'qwack/', ))],
3106            client._calls)
3107        self.assertEqual(False, result)
3108
3109
3110class TestRepositoryIsShared(TestRemoteRepository):
3111
3112    def test_is_shared(self):
3113        # ('yes', ) for Repository.is_shared -> 'True'.
3114        transport_path = 'quack'
3115        repo, client = self.setup_fake_client_and_repository(transport_path)
3116        client.add_success_response(b'yes')
3117        result = repo.is_shared()
3118        self.assertEqual(
3119            [('call', b'Repository.is_shared', (b'quack/',))],
3120            client._calls)
3121        self.assertEqual(True, result)
3122
3123    def test_is_not_shared(self):
3124        # ('no', ) for Repository.is_shared -> 'False'.
3125        transport_path = 'qwack'
3126        repo, client = self.setup_fake_client_and_repository(transport_path)
3127        client.add_success_response(b'no')
3128        result = repo.is_shared()
3129        self.assertEqual(
3130            [('call', b'Repository.is_shared', (b'qwack/',))],
3131            client._calls)
3132        self.assertEqual(False, result)
3133
3134
3135class TestRepositoryMakeWorkingTrees(TestRemoteRepository):
3136
3137    def test_make_working_trees(self):
3138        # ('yes', ) for Repository.make_working_trees -> 'True'.
3139        transport_path = 'quack'
3140        repo, client = self.setup_fake_client_and_repository(transport_path)
3141        client.add_success_response(b'yes')
3142        result = repo.make_working_trees()
3143        self.assertEqual(
3144            [('call', b'Repository.make_working_trees', (b'quack/',))],
3145            client._calls)
3146        self.assertEqual(True, result)
3147
3148    def test_no_working_trees(self):
3149        # ('no', ) for Repository.make_working_trees -> 'False'.
3150        transport_path = 'qwack'
3151        repo, client = self.setup_fake_client_and_repository(transport_path)
3152        client.add_success_response(b'no')
3153        result = repo.make_working_trees()
3154        self.assertEqual(
3155            [('call', b'Repository.make_working_trees', (b'qwack/',))],
3156            client._calls)
3157        self.assertEqual(False, result)
3158
3159
3160class TestRepositoryLockWrite(TestRemoteRepository):
3161
3162    def test_lock_write(self):
3163        transport_path = 'quack'
3164        repo, client = self.setup_fake_client_and_repository(transport_path)
3165        client.add_success_response(b'ok', b'a token')
3166        token = repo.lock_write().repository_token
3167        self.assertEqual(
3168            [('call', b'Repository.lock_write', (b'quack/', b''))],
3169            client._calls)
3170        self.assertEqual(b'a token', token)
3171
3172    def test_lock_write_already_locked(self):
3173        transport_path = 'quack'
3174        repo, client = self.setup_fake_client_and_repository(transport_path)
3175        client.add_error_response(b'LockContention')
3176        self.assertRaises(errors.LockContention, repo.lock_write)
3177        self.assertEqual(
3178            [('call', b'Repository.lock_write', (b'quack/', b''))],
3179            client._calls)
3180
3181    def test_lock_write_unlockable(self):
3182        transport_path = 'quack'
3183        repo, client = self.setup_fake_client_and_repository(transport_path)
3184        client.add_error_response(b'UnlockableTransport')
3185        self.assertRaises(errors.UnlockableTransport, repo.lock_write)
3186        self.assertEqual(
3187            [('call', b'Repository.lock_write', (b'quack/', b''))],
3188            client._calls)
3189
3190
3191class TestRepositoryWriteGroups(TestRemoteRepository):
3192
3193    def test_start_write_group(self):
3194        transport_path = 'quack'
3195        repo, client = self.setup_fake_client_and_repository(transport_path)
3196        client.add_expected_call(
3197            b'Repository.lock_write', (b'quack/', b''),
3198            b'success', (b'ok', b'a token'))
3199        client.add_expected_call(
3200            b'Repository.start_write_group', (b'quack/', b'a token'),
3201            b'success', (b'ok', (b'token1', )))
3202        repo.lock_write()
3203        repo.start_write_group()
3204
3205    def test_start_write_group_unsuspendable(self):
3206        # Some repositories do not support suspending write
3207        # groups. For those, fall back to the "real" repository.
3208        transport_path = 'quack'
3209        repo, client = self.setup_fake_client_and_repository(transport_path)
3210
3211        def stub_ensure_real():
3212            client._calls.append(('_ensure_real',))
3213            repo._real_repository = _StubRealPackRepository(client._calls)
3214        repo._ensure_real = stub_ensure_real
3215        client.add_expected_call(
3216            b'Repository.lock_write', (b'quack/', b''),
3217            b'success', (b'ok', b'a token'))
3218        client.add_expected_call(
3219            b'Repository.start_write_group', (b'quack/', b'a token'),
3220            b'error', (b'UnsuspendableWriteGroup',))
3221        repo.lock_write()
3222        repo.start_write_group()
3223        self.assertEqual(client._calls[-2:], [
3224            ('_ensure_real',),
3225            ('start_write_group',)])
3226
3227    def test_commit_write_group(self):
3228        transport_path = 'quack'
3229        repo, client = self.setup_fake_client_and_repository(transport_path)
3230        client.add_expected_call(
3231            b'Repository.lock_write', (b'quack/', b''),
3232            b'success', (b'ok', b'a token'))
3233        client.add_expected_call(
3234            b'Repository.start_write_group', (b'quack/', b'a token'),
3235            b'success', (b'ok', [b'token1']))
3236        client.add_expected_call(
3237            b'Repository.commit_write_group', (b'quack/',
3238                                               b'a token', [b'token1']),
3239            b'success', (b'ok',))
3240        repo.lock_write()
3241        repo.start_write_group()
3242        repo.commit_write_group()
3243
3244    def test_abort_write_group(self):
3245        transport_path = 'quack'
3246        repo, client = self.setup_fake_client_and_repository(transport_path)
3247        client.add_expected_call(
3248            b'Repository.lock_write', (b'quack/', b''),
3249            b'success', (b'ok', b'a token'))
3250        client.add_expected_call(
3251            b'Repository.start_write_group', (b'quack/', b'a token'),
3252            b'success', (b'ok', [b'token1']))
3253        client.add_expected_call(
3254            b'Repository.abort_write_group', (b'quack/',
3255                                              b'a token', [b'token1']),
3256            b'success', (b'ok',))
3257        repo.lock_write()
3258        repo.start_write_group()
3259        repo.abort_write_group(False)
3260
3261    def test_suspend_write_group(self):
3262        transport_path = 'quack'
3263        repo, client = self.setup_fake_client_and_repository(transport_path)
3264        self.assertEqual([], repo.suspend_write_group())
3265
3266    def test_resume_write_group(self):
3267        transport_path = 'quack'
3268        repo, client = self.setup_fake_client_and_repository(transport_path)
3269        client.add_expected_call(
3270            b'Repository.lock_write', (b'quack/', b''),
3271            b'success', (b'ok', b'a token'))
3272        client.add_expected_call(
3273            b'Repository.check_write_group', (b'quack/',
3274                                              b'a token', [b'token1']),
3275            b'success', (b'ok',))
3276        repo.lock_write()
3277        repo.resume_write_group(['token1'])
3278
3279
3280class TestRepositorySetMakeWorkingTrees(TestRemoteRepository):
3281
3282    def test_backwards_compat(self):
3283        self.setup_smart_server_with_call_log()
3284        repo = self.make_repository('.')
3285        self.reset_smart_call_log()
3286        verb = b'Repository.set_make_working_trees'
3287        self.disable_verb(verb)
3288        repo.set_make_working_trees(True)
3289        call_count = len([call for call in self.hpss_calls if
3290                          call.call.method == verb])
3291        self.assertEqual(1, call_count)
3292
3293    def test_current(self):
3294        transport_path = 'quack'
3295        repo, client = self.setup_fake_client_and_repository(transport_path)
3296        client.add_expected_call(
3297            b'Repository.set_make_working_trees', (b'quack/', b'True'),
3298            b'success', (b'ok',))
3299        client.add_expected_call(
3300            b'Repository.set_make_working_trees', (b'quack/', b'False'),
3301            b'success', (b'ok',))
3302        repo.set_make_working_trees(True)
3303        repo.set_make_working_trees(False)
3304
3305
3306class TestRepositoryUnlock(TestRemoteRepository):
3307
3308    def test_unlock(self):
3309        transport_path = 'quack'
3310        repo, client = self.setup_fake_client_and_repository(transport_path)
3311        client.add_success_response(b'ok', b'a token')
3312        client.add_success_response(b'ok')
3313        repo.lock_write()
3314        repo.unlock()
3315        self.assertEqual(
3316            [('call', b'Repository.lock_write', (b'quack/', b'')),
3317             ('call', b'Repository.unlock', (b'quack/', b'a token'))],
3318            client._calls)
3319
3320    def test_unlock_wrong_token(self):
3321        # If somehow the token is wrong, unlock will raise TokenMismatch.
3322        transport_path = 'quack'
3323        repo, client = self.setup_fake_client_and_repository(transport_path)
3324        client.add_success_response(b'ok', b'a token')
3325        client.add_error_response(b'TokenMismatch')
3326        repo.lock_write()
3327        self.assertRaises(errors.TokenMismatch, repo.unlock)
3328
3329
3330class TestRepositoryHasRevision(TestRemoteRepository):
3331
3332    def test_none(self):
3333        # repo.has_revision(None) should not cause any traffic.
3334        transport_path = 'quack'
3335        repo, client = self.setup_fake_client_and_repository(transport_path)
3336
3337        # The null revision is always there, so has_revision(None) == True.
3338        self.assertEqual(True, repo.has_revision(NULL_REVISION))
3339
3340        # The remote repo shouldn't be accessed.
3341        self.assertEqual([], client._calls)
3342
3343
3344class TestRepositoryIterFilesBytes(TestRemoteRepository):
3345    """Test Repository.iter_file_bytes."""
3346
3347    def test_single(self):
3348        transport_path = 'quack'
3349        repo, client = self.setup_fake_client_and_repository(transport_path)
3350        client.add_expected_call(
3351            b'Repository.iter_files_bytes', (b'quack/', ),
3352            b'success', (b'ok',), iter([b"ok\x000", b"\n", zlib.compress(b"mydata" * 10)]))
3353        for (identifier, byte_stream) in repo.iter_files_bytes([(b"somefile",
3354                                                                 b"somerev", b"myid")]):
3355            self.assertEqual(b"myid", identifier)
3356            self.assertEqual(b"".join(byte_stream), b"mydata" * 10)
3357
3358    def test_missing(self):
3359        transport_path = 'quack'
3360        repo, client = self.setup_fake_client_and_repository(transport_path)
3361        client.add_expected_call(
3362            b'Repository.iter_files_bytes',
3363            (b'quack/', ),
3364            b'error', (b'RevisionNotPresent', b'somefile', b'somerev'),
3365            iter([b"absent\0somefile\0somerev\n"]))
3366        self.assertRaises(errors.RevisionNotPresent, list,
3367                          repo.iter_files_bytes(
3368                              [(b"somefile", b"somerev", b"myid")]))
3369
3370
3371class TestRepositoryInsertStreamBase(TestRemoteRepository):
3372    """Base class for Repository.insert_stream and .insert_stream_1.19
3373    tests.
3374    """
3375
3376    def checkInsertEmptyStream(self, repo, client):
3377        """Insert an empty stream, checking the result.
3378
3379        This checks that there are no resume_tokens or missing_keys, and that
3380        the client is finished.
3381        """
3382        sink = repo._get_sink()
3383        fmt = repository.format_registry.get_default()
3384        resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
3385        self.assertEqual([], resume_tokens)
3386        self.assertEqual(set(), missing_keys)
3387        self.assertFinished(client)
3388
3389
3390class TestRepositoryInsertStream(TestRepositoryInsertStreamBase):
3391    """Tests for using Repository.insert_stream verb when the _1.19 variant is
3392    not available.
3393
3394    This test case is very similar to TestRepositoryInsertStream_1_19.
3395    """
3396
3397    def setUp(self):
3398        super(TestRepositoryInsertStream, self).setUp()
3399        self.disable_verb(b'Repository.insert_stream_1.19')
3400
3401    def test_unlocked_repo(self):
3402        transport_path = 'quack'
3403        repo, client = self.setup_fake_client_and_repository(transport_path)
3404        client.add_expected_call(
3405            b'Repository.insert_stream_1.19', (b'quack/', b''),
3406            b'unknown', (b'Repository.insert_stream_1.19',))
3407        client.add_expected_call(
3408            b'Repository.insert_stream', (b'quack/', b''),
3409            b'success', (b'ok',))
3410        client.add_expected_call(
3411            b'Repository.insert_stream', (b'quack/', b''),
3412            b'success', (b'ok',))
3413        self.checkInsertEmptyStream(repo, client)
3414
3415    def test_locked_repo_with_no_lock_token(self):
3416        transport_path = 'quack'
3417        repo, client = self.setup_fake_client_and_repository(transport_path)
3418        client.add_expected_call(
3419            b'Repository.lock_write', (b'quack/', b''),
3420            b'success', (b'ok', b''))
3421        client.add_expected_call(
3422            b'Repository.insert_stream_1.19', (b'quack/', b''),
3423            b'unknown', (b'Repository.insert_stream_1.19',))
3424        client.add_expected_call(
3425            b'Repository.insert_stream', (b'quack/', b''),
3426            b'success', (b'ok',))
3427        client.add_expected_call(
3428            b'Repository.insert_stream', (b'quack/', b''),
3429            b'success', (b'ok',))
3430        repo.lock_write()
3431        self.checkInsertEmptyStream(repo, client)
3432
3433    def test_locked_repo_with_lock_token(self):
3434        transport_path = 'quack'
3435        repo, client = self.setup_fake_client_and_repository(transport_path)
3436        client.add_expected_call(
3437            b'Repository.lock_write', (b'quack/', b''),
3438            b'success', (b'ok', b'a token'))
3439        client.add_expected_call(
3440            b'Repository.insert_stream_1.19', (b'quack/', b'', b'a token'),
3441            b'unknown', (b'Repository.insert_stream_1.19',))
3442        client.add_expected_call(
3443            b'Repository.insert_stream_locked', (b'quack/', b'', b'a token'),
3444            b'success', (b'ok',))
3445        client.add_expected_call(
3446            b'Repository.insert_stream_locked', (b'quack/', b'', b'a token'),
3447            b'success', (b'ok',))
3448        repo.lock_write()
3449        self.checkInsertEmptyStream(repo, client)
3450
3451    def test_stream_with_inventory_deltas(self):
3452        """'inventory-deltas' substreams cannot be sent to the
3453        Repository.insert_stream verb, because not all servers that implement
3454        that verb will accept them.  So when one is encountered the RemoteSink
3455        immediately stops using that verb and falls back to VFS insert_stream.
3456        """
3457        transport_path = 'quack'
3458        repo, client = self.setup_fake_client_and_repository(transport_path)
3459        client.add_expected_call(
3460            b'Repository.insert_stream_1.19', (b'quack/', b''),
3461            b'unknown', (b'Repository.insert_stream_1.19',))
3462        client.add_expected_call(
3463            b'Repository.insert_stream', (b'quack/', b''),
3464            b'success', (b'ok',))
3465        client.add_expected_call(
3466            b'Repository.insert_stream', (b'quack/', b''),
3467            b'success', (b'ok',))
3468        # Create a fake real repository for insert_stream to fall back on, so
3469        # that we can directly see the records the RemoteSink passes to the
3470        # real sink.
3471
3472        class FakeRealSink:
3473            def __init__(self):
3474                self.records = []
3475
3476            def insert_stream(self, stream, src_format, resume_tokens):
3477                for substream_kind, substream in stream:
3478                    self.records.append(
3479                        (substream_kind, [record.key for record in substream]))
3480                return [b'fake tokens'], [b'fake missing keys']
3481        fake_real_sink = FakeRealSink()
3482
3483        class FakeRealRepository:
3484            def _get_sink(self):
3485                return fake_real_sink
3486
3487            def is_in_write_group(self):
3488                return False
3489
3490            def refresh_data(self):
3491                return True
3492        repo._real_repository = FakeRealRepository()
3493        sink = repo._get_sink()
3494        fmt = repository.format_registry.get_default()
3495        stream = self.make_stream_with_inv_deltas(fmt)
3496        resume_tokens, missing_keys = sink.insert_stream(stream, fmt, [])
3497        # Every record from the first inventory delta should have been sent to
3498        # the VFS sink.
3499        expected_records = [
3500            ('inventory-deltas', [(b'rev2',), (b'rev3',)]),
3501            ('texts', [(b'some-rev', b'some-file')])]
3502        self.assertEqual(expected_records, fake_real_sink.records)
3503        # The return values from the real sink's insert_stream are propagated
3504        # back to the original caller.
3505        self.assertEqual([b'fake tokens'], resume_tokens)
3506        self.assertEqual([b'fake missing keys'], missing_keys)
3507        self.assertFinished(client)
3508
3509    def make_stream_with_inv_deltas(self, fmt):
3510        """Make a simple stream with an inventory delta followed by more
3511        records and more substreams to test that all records and substreams
3512        from that point on are used.
3513
3514        This sends, in order:
3515           * inventories substream: rev1, rev2, rev3.  rev2 and rev3 are
3516             inventory-deltas.
3517           * texts substream: (some-rev, some-file)
3518        """
3519        # Define a stream using generators so that it isn't rewindable.
3520        inv = inventory.Inventory(revision_id=b'rev1')
3521        inv.root.revision = b'rev1'
3522
3523        def stream_with_inv_delta():
3524            yield ('inventories', inventories_substream())
3525            yield ('inventory-deltas', inventory_delta_substream())
3526            yield ('texts', [
3527                versionedfile.FulltextContentFactory(
3528                    (b'some-rev', b'some-file'), (), None, b'content')])
3529
3530        def inventories_substream():
3531            # An empty inventory fulltext.  This will be streamed normally.
3532            chunks = fmt._serializer.write_inventory_to_lines(inv)
3533            yield versionedfile.ChunkedContentFactory(
3534                (b'rev1',), (), None, chunks, chunks_are_lines=True)
3535
3536        def inventory_delta_substream():
3537            # An inventory delta.  This can't be streamed via this verb, so it
3538            # will trigger a fallback to VFS insert_stream.
3539            entry = inv.make_entry(
3540                'directory', 'newdir', inv.root.file_id, b'newdir-id')
3541            entry.revision = b'ghost'
3542            delta = [(None, 'newdir', b'newdir-id', entry)]
3543            serializer = inventory_delta.InventoryDeltaSerializer(
3544                versioned_root=True, tree_references=False)
3545            lines = serializer.delta_to_lines(b'rev1', b'rev2', delta)
3546            yield versionedfile.ChunkedContentFactory(
3547                (b'rev2',), ((b'rev1',)), None, lines)
3548            # Another delta.
3549            lines = serializer.delta_to_lines(b'rev1', b'rev3', delta)
3550            yield versionedfile.ChunkedContentFactory(
3551                (b'rev3',), ((b'rev1',)), None, lines)
3552        return stream_with_inv_delta()
3553
3554
3555class TestRepositoryInsertStream_1_19(TestRepositoryInsertStreamBase):
3556
3557    def test_unlocked_repo(self):
3558        transport_path = 'quack'
3559        repo, client = self.setup_fake_client_and_repository(transport_path)
3560        client.add_expected_call(
3561            b'Repository.insert_stream_1.19', (b'quack/', b''),
3562            b'success', (b'ok',))
3563        client.add_expected_call(
3564            b'Repository.insert_stream_1.19', (b'quack/', b''),
3565            b'success', (b'ok',))
3566        self.checkInsertEmptyStream(repo, client)
3567
3568    def test_locked_repo_with_no_lock_token(self):
3569        transport_path = 'quack'
3570        repo, client = self.setup_fake_client_and_repository(transport_path)
3571        client.add_expected_call(
3572            b'Repository.lock_write', (b'quack/', b''),
3573            b'success', (b'ok', b''))
3574        client.add_expected_call(
3575            b'Repository.insert_stream_1.19', (b'quack/', b''),
3576            b'success', (b'ok',))
3577        client.add_expected_call(
3578            b'Repository.insert_stream_1.19', (b'quack/', b''),
3579            b'success', (b'ok',))
3580        repo.lock_write()
3581        self.checkInsertEmptyStream(repo, client)
3582
3583    def test_locked_repo_with_lock_token(self):
3584        transport_path = 'quack'
3585        repo, client = self.setup_fake_client_and_repository(transport_path)
3586        client.add_expected_call(
3587            b'Repository.lock_write', (b'quack/', b''),
3588            b'success', (b'ok', b'a token'))
3589        client.add_expected_call(
3590            b'Repository.insert_stream_1.19', (b'quack/', b'', b'a token'),
3591            b'success', (b'ok',))
3592        client.add_expected_call(
3593            b'Repository.insert_stream_1.19', (b'quack/', b'', b'a token'),
3594            b'success', (b'ok',))
3595        repo.lock_write()
3596        self.checkInsertEmptyStream(repo, client)
3597
3598
3599class TestRepositoryTarball(TestRemoteRepository):
3600
3601    # This is a canned tarball reponse we can validate against
3602    tarball_content = base64.b64decode(
3603        'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
3604        'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
3605        'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
3606        'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
3607        'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
3608        'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
3609        'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
3610        'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
3611        '0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
3612        'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
3613        'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
3614        'nWQ7QH/F3JFOFCQ0aSPfA='
3615        )
3616
3617    def test_repository_tarball(self):
3618        # Test that Repository.tarball generates the right operations
3619        transport_path = 'repo'
3620        expected_calls = [('call_expecting_body', b'Repository.tarball',
3621                           (b'repo/', b'bz2',),),
3622                          ]
3623        repo, client = self.setup_fake_client_and_repository(transport_path)
3624        client.add_success_response_with_body(self.tarball_content, b'ok')
3625        # Now actually ask for the tarball
3626        tarball_file = repo._get_tarball('bz2')
3627        try:
3628            self.assertEqual(expected_calls, client._calls)
3629            self.assertEqual(self.tarball_content, tarball_file.read())
3630        finally:
3631            tarball_file.close()
3632
3633
3634class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
3635    """RemoteRepository.copy_content_into optimizations"""
3636
3637    def test_copy_content_remote_to_local(self):
3638        self.transport_server = test_server.SmartTCPServer_for_testing
3639        src_repo = self.make_repository('repo1')
3640        src_repo = repository.Repository.open(self.get_url('repo1'))
3641        # At the moment the tarball-based copy_content_into can't write back
3642        # into a smart server.  It would be good if it could upload the
3643        # tarball; once that works we'd have to create repositories of
3644        # different formats. -- mbp 20070410
3645        dest_url = self.get_vfs_only_url('repo2')
3646        dest_bzrdir = BzrDir.create(dest_url)
3647        dest_repo = dest_bzrdir.create_repository()
3648        self.assertFalse(isinstance(dest_repo, RemoteRepository))
3649        self.assertTrue(isinstance(src_repo, RemoteRepository))
3650        src_repo.copy_content_into(dest_repo)
3651
3652
3653class _StubRealPackRepository(object):
3654
3655    def __init__(self, calls):
3656        self.calls = calls
3657        self._pack_collection = _StubPackCollection(calls)
3658
3659    def start_write_group(self):
3660        self.calls.append(('start_write_group',))
3661
3662    def is_in_write_group(self):
3663        return False
3664
3665    def refresh_data(self):
3666        self.calls.append(('pack collection reload_pack_names',))
3667
3668
3669class _StubPackCollection(object):
3670
3671    def __init__(self, calls):
3672        self.calls = calls
3673
3674    def autopack(self):
3675        self.calls.append(('pack collection autopack',))
3676
3677
3678class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
3679    """Tests for RemoteRepository.autopack implementation."""
3680
3681    def test_ok(self):
3682        """When the server returns 'ok' and there's no _real_repository, then
3683        nothing else happens: the autopack method is done.
3684        """
3685        transport_path = 'quack'
3686        repo, client = self.setup_fake_client_and_repository(transport_path)
3687        client.add_expected_call(
3688            b'PackRepository.autopack', (b'quack/',), b'success', (b'ok',))
3689        repo.autopack()
3690        self.assertFinished(client)
3691
3692    def test_ok_with_real_repo(self):
3693        """When the server returns 'ok' and there is a _real_repository, then
3694        the _real_repository's reload_pack_name's method will be called.
3695        """
3696        transport_path = 'quack'
3697        repo, client = self.setup_fake_client_and_repository(transport_path)
3698        client.add_expected_call(
3699            b'PackRepository.autopack', (b'quack/',),
3700            b'success', (b'ok',))
3701        repo._real_repository = _StubRealPackRepository(client._calls)
3702        repo.autopack()
3703        self.assertEqual(
3704            [('call', b'PackRepository.autopack', (b'quack/',)),
3705             ('pack collection reload_pack_names',)],
3706            client._calls)
3707
3708    def test_backwards_compatibility(self):
3709        """If the server does not recognise the PackRepository.autopack verb,
3710        fallback to the real_repository's implementation.
3711        """
3712        transport_path = 'quack'
3713        repo, client = self.setup_fake_client_and_repository(transport_path)
3714        client.add_unknown_method_response(b'PackRepository.autopack')
3715
3716        def stub_ensure_real():
3717            client._calls.append(('_ensure_real',))
3718            repo._real_repository = _StubRealPackRepository(client._calls)
3719        repo._ensure_real = stub_ensure_real
3720        repo.autopack()
3721        self.assertEqual(
3722            [('call', b'PackRepository.autopack', (b'quack/',)),
3723             ('_ensure_real',),
3724             ('pack collection autopack',)],
3725            client._calls)
3726
3727    def test_oom_error_reporting(self):
3728        """An out-of-memory condition on the server is reported clearly"""
3729        transport_path = 'quack'
3730        repo, client = self.setup_fake_client_and_repository(transport_path)
3731        client.add_expected_call(
3732            b'PackRepository.autopack', (b'quack/',),
3733            b'error', (b'MemoryError',))
3734        err = self.assertRaises(errors.BzrError, repo.autopack)
3735        self.assertContainsRe(str(err), "^remote server out of mem")
3736
3737
3738class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
3739    """Base class for unit tests for breezy.bzr.remote._translate_error."""
3740
3741    def translateTuple(self, error_tuple, **context):
3742        """Call _translate_error with an ErrorFromSmartServer built from the
3743        given error_tuple.
3744
3745        :param error_tuple: A tuple of a smart server response, as would be
3746            passed to an ErrorFromSmartServer.
3747        :kwargs context: context items to call _translate_error with.
3748
3749        :returns: The error raised by _translate_error.
3750        """
3751        # Raise the ErrorFromSmartServer before passing it as an argument,
3752        # because _translate_error may need to re-raise it with a bare 'raise'
3753        # statement.
3754        server_error = errors.ErrorFromSmartServer(error_tuple)
3755        translated_error = self.translateErrorFromSmartServer(
3756            server_error, **context)
3757        return translated_error
3758
3759    def translateErrorFromSmartServer(self, error_object, **context):
3760        """Like translateTuple, but takes an already constructed
3761        ErrorFromSmartServer rather than a tuple.
3762        """
3763        try:
3764            raise error_object
3765        except errors.ErrorFromSmartServer as server_error:
3766            translated_error = self.assertRaises(
3767                errors.BzrError, remote._translate_error, server_error,
3768                **context)
3769        return translated_error
3770
3771
3772class TestErrorTranslationSuccess(TestErrorTranslationBase):
3773    """Unit tests for breezy.bzr.remote._translate_error.
3774
3775    Given an ErrorFromSmartServer (which has an error tuple from a smart
3776    server) and some context, _translate_error raises more specific errors from
3777    breezy.errors.
3778
3779    This test case covers the cases where _translate_error succeeds in
3780    translating an ErrorFromSmartServer to something better.  See
3781    TestErrorTranslationRobustness for other cases.
3782    """
3783
3784    def test_NoSuchRevision(self):
3785        branch = self.make_branch('')
3786        revid = b'revid'
3787        translated_error = self.translateTuple(
3788            (b'NoSuchRevision', revid), branch=branch)
3789        expected_error = errors.NoSuchRevision(branch, revid)
3790        self.assertEqual(expected_error, translated_error)
3791
3792    def test_nosuchrevision(self):
3793        repository = self.make_repository('')
3794        revid = b'revid'
3795        translated_error = self.translateTuple(
3796            (b'nosuchrevision', revid), repository=repository)
3797        expected_error = errors.NoSuchRevision(repository, revid)
3798        self.assertEqual(expected_error, translated_error)
3799
3800    def test_nobranch(self):
3801        bzrdir = self.make_controldir('')
3802        translated_error = self.translateTuple((b'nobranch',), bzrdir=bzrdir)
3803        expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
3804        self.assertEqual(expected_error, translated_error)
3805
3806    def test_nobranch_one_arg(self):
3807        bzrdir = self.make_controldir('')
3808        translated_error = self.translateTuple(
3809            (b'nobranch', b'extra detail'), bzrdir=bzrdir)
3810        expected_error = errors.NotBranchError(
3811            path=bzrdir.root_transport.base,
3812            detail='extra detail')
3813        self.assertEqual(expected_error, translated_error)
3814
3815    def test_norepository(self):
3816        bzrdir = self.make_controldir('')
3817        translated_error = self.translateTuple((b'norepository',),
3818                                               bzrdir=bzrdir)
3819        expected_error = errors.NoRepositoryPresent(bzrdir)
3820        self.assertEqual(expected_error, translated_error)
3821
3822    def test_LockContention(self):
3823        translated_error = self.translateTuple((b'LockContention',))
3824        expected_error = errors.LockContention('(remote lock)')
3825        self.assertEqual(expected_error, translated_error)
3826
3827    def test_UnlockableTransport(self):
3828        bzrdir = self.make_controldir('')
3829        translated_error = self.translateTuple(
3830            (b'UnlockableTransport',), bzrdir=bzrdir)
3831        expected_error = errors.UnlockableTransport(bzrdir.root_transport)
3832        self.assertEqual(expected_error, translated_error)
3833
3834    def test_LockFailed(self):
3835        lock = 'str() of a server lock'
3836        why = 'str() of why'
3837        translated_error = self.translateTuple(
3838            (b'LockFailed', lock.encode('ascii'), why.encode('ascii')))
3839        expected_error = errors.LockFailed(lock, why)
3840        self.assertEqual(expected_error, translated_error)
3841
3842    def test_TokenMismatch(self):
3843        token = 'a lock token'
3844        translated_error = self.translateTuple(
3845            (b'TokenMismatch',), token=token)
3846        expected_error = errors.TokenMismatch(token, '(remote token)')
3847        self.assertEqual(expected_error, translated_error)
3848
3849    def test_Diverged(self):
3850        branch = self.make_branch('a')
3851        other_branch = self.make_branch('b')
3852        translated_error = self.translateTuple(
3853            (b'Diverged',), branch=branch, other_branch=other_branch)
3854        expected_error = errors.DivergedBranches(branch, other_branch)
3855        self.assertEqual(expected_error, translated_error)
3856
3857    def test_NotStacked(self):
3858        branch = self.make_branch('')
3859        translated_error = self.translateTuple((b'NotStacked',), branch=branch)
3860        expected_error = errors.NotStacked(branch)
3861        self.assertEqual(expected_error, translated_error)
3862
3863    def test_ReadError_no_args(self):
3864        path = 'a path'
3865        translated_error = self.translateTuple((b'ReadError',), path=path)
3866        expected_error = errors.ReadError(path)
3867        self.assertEqual(expected_error, translated_error)
3868
3869    def test_ReadError(self):
3870        path = 'a path'
3871        translated_error = self.translateTuple(
3872            (b'ReadError', path.encode('utf-8')))
3873        expected_error = errors.ReadError(path)
3874        self.assertEqual(expected_error, translated_error)
3875
3876    def test_IncompatibleRepositories(self):
3877        translated_error = self.translateTuple((b'IncompatibleRepositories',
3878                                                b"repo1", b"repo2", b"details here"))
3879        expected_error = errors.IncompatibleRepositories("repo1", "repo2",
3880                                                         "details here")
3881        self.assertEqual(expected_error, translated_error)
3882
3883    def test_GhostRevisionsHaveNoRevno(self):
3884        translated_error = self.translateTuple((b'GhostRevisionsHaveNoRevno',
3885                                                b"revid1", b"revid2"))
3886        expected_error = errors.GhostRevisionsHaveNoRevno(b"revid1", b"revid2")
3887        self.assertEqual(expected_error, translated_error)
3888
3889    def test_PermissionDenied_no_args(self):
3890        path = 'a path'
3891        translated_error = self.translateTuple((b'PermissionDenied',),
3892                                               path=path)
3893        expected_error = errors.PermissionDenied(path)
3894        self.assertEqual(expected_error, translated_error)
3895
3896    def test_PermissionDenied_one_arg(self):
3897        path = 'a path'
3898        translated_error = self.translateTuple(
3899            (b'PermissionDenied', path.encode('utf-8')))
3900        expected_error = errors.PermissionDenied(path)
3901        self.assertEqual(expected_error, translated_error)
3902
3903    def test_PermissionDenied_one_arg_and_context(self):
3904        """Given a choice between a path from the local context and a path on
3905        the wire, _translate_error prefers the path from the local context.
3906        """
3907        local_path = 'local path'
3908        remote_path = 'remote path'
3909        translated_error = self.translateTuple(
3910            (b'PermissionDenied', remote_path.encode('utf-8')), path=local_path)
3911        expected_error = errors.PermissionDenied(local_path)
3912        self.assertEqual(expected_error, translated_error)
3913
3914    def test_PermissionDenied_two_args(self):
3915        path = 'a path'
3916        extra = 'a string with extra info'
3917        translated_error = self.translateTuple(
3918            (b'PermissionDenied', path.encode('utf-8'), extra.encode('utf-8')))
3919        expected_error = errors.PermissionDenied(path, extra)
3920        self.assertEqual(expected_error, translated_error)
3921
3922    # GZ 2011-03-02: TODO test for PermissionDenied with non-ascii 'extra'
3923
3924    def test_NoSuchFile_context_path(self):
3925        local_path = "local path"
3926        translated_error = self.translateTuple((b'ReadError', b"remote path"),
3927                                               path=local_path)
3928        expected_error = errors.ReadError(local_path)
3929        self.assertEqual(expected_error, translated_error)
3930
3931    def test_NoSuchFile_without_context(self):
3932        remote_path = "remote path"
3933        translated_error = self.translateTuple(
3934            (b'ReadError', remote_path.encode('utf-8')))
3935        expected_error = errors.ReadError(remote_path)
3936        self.assertEqual(expected_error, translated_error)
3937
3938    def test_ReadOnlyError(self):
3939        translated_error = self.translateTuple((b'ReadOnlyError',))
3940        expected_error = errors.TransportNotPossible("readonly transport")
3941        self.assertEqual(expected_error, translated_error)
3942
3943    def test_MemoryError(self):
3944        translated_error = self.translateTuple((b'MemoryError',))
3945        self.assertStartsWith(str(translated_error),
3946                              "remote server out of memory")
3947
3948    def test_generic_IndexError_no_classname(self):
3949        err = errors.ErrorFromSmartServer(
3950            (b'error', b"list index out of range"))
3951        translated_error = self.translateErrorFromSmartServer(err)
3952        expected_error = errors.UnknownErrorFromSmartServer(err)
3953        self.assertEqual(expected_error, translated_error)
3954
3955    # GZ 2011-03-02: TODO test generic non-ascii error string
3956
3957    def test_generic_KeyError(self):
3958        err = errors.ErrorFromSmartServer((b'error', b'KeyError', b"1"))
3959        translated_error = self.translateErrorFromSmartServer(err)
3960        expected_error = errors.UnknownErrorFromSmartServer(err)
3961        self.assertEqual(expected_error, translated_error)
3962
3963    def test_RevnoOutOfBounds(self):
3964        translated_error = self.translateTuple(
3965            ((b'revno-outofbounds', 5, 0, 3)), path=b'path')
3966        expected_error = errors.RevnoOutOfBounds(5, (0, 3))
3967        self.assertEqual(expected_error, translated_error)
3968
3969
3970class TestErrorTranslationRobustness(TestErrorTranslationBase):
3971    """Unit tests for breezy.bzr.remote._translate_error's robustness.
3972
3973    TestErrorTranslationSuccess is for cases where _translate_error can
3974    translate successfully.  This class about how _translate_err behaves when
3975    it fails to translate: it re-raises the original error.
3976    """
3977
3978    def test_unrecognised_server_error(self):
3979        """If the error code from the server is not recognised, the original
3980        ErrorFromSmartServer is propagated unmodified.
3981        """
3982        error_tuple = (b'An unknown error tuple',)
3983        server_error = errors.ErrorFromSmartServer(error_tuple)
3984        translated_error = self.translateErrorFromSmartServer(server_error)
3985        expected_error = errors.UnknownErrorFromSmartServer(server_error)
3986        self.assertEqual(expected_error, translated_error)
3987
3988    def test_context_missing_a_key(self):
3989        """In case of a bug in the client, or perhaps an unexpected response
3990        from a server, _translate_error returns the original error tuple from
3991        the server and mutters a warning.
3992        """
3993        # To translate a NoSuchRevision error _translate_error needs a 'branch'
3994        # in the context dict.  So let's give it an empty context dict instead
3995        # to exercise its error recovery.
3996        error_tuple = (b'NoSuchRevision', b'revid')
3997        server_error = errors.ErrorFromSmartServer(error_tuple)
3998        translated_error = self.translateErrorFromSmartServer(server_error)
3999        self.assertEqual(server_error, translated_error)
4000        # In addition to re-raising ErrorFromSmartServer, some debug info has
4001        # been muttered to the log file for developer to look at.
4002        self.assertContainsRe(
4003            self.get_log(),
4004            "Missing key 'branch' in context")
4005
4006    def test_path_missing(self):
4007        """Some translations (PermissionDenied, ReadError) can determine the
4008        'path' variable from either the wire or the local context.  If neither
4009        has it, then an error is raised.
4010        """
4011        error_tuple = (b'ReadError',)
4012        server_error = errors.ErrorFromSmartServer(error_tuple)
4013        translated_error = self.translateErrorFromSmartServer(server_error)
4014        self.assertEqual(server_error, translated_error)
4015        # In addition to re-raising ErrorFromSmartServer, some debug info has
4016        # been muttered to the log file for developer to look at.
4017        self.assertContainsRe(self.get_log(), "Missing key 'path' in context")
4018
4019
4020class TestStacking(tests.TestCaseWithTransport):
4021    """Tests for operations on stacked remote repositories.
4022
4023    The underlying format type must support stacking.
4024    """
4025
4026    def test_access_stacked_remote(self):
4027        # based on <http://launchpad.net/bugs/261315>
4028        # make a branch stacked on another repository containing an empty
4029        # revision, then open it over hpss - we should be able to see that
4030        # revision.
4031        base_builder = self.make_branch_builder('base', format='1.9')
4032        base_builder.start_series()
4033        base_revid = base_builder.build_snapshot(None,
4034                                                 [('add', ('', None, 'directory', None))],
4035                                                 'message', revision_id=b'rev-id')
4036        base_builder.finish_series()
4037        stacked_branch = self.make_branch('stacked', format='1.9')
4038        stacked_branch.set_stacked_on_url('../base')
4039        # start a server looking at this
4040        smart_server = test_server.SmartTCPServer_for_testing()
4041        self.start_server(smart_server)
4042        remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
4043        # can get its branch and repository
4044        remote_branch = remote_bzrdir.open_branch()
4045        remote_repo = remote_branch.repository
4046        remote_repo.lock_read()
4047        try:
4048            # it should have an appropriate fallback repository, which should also
4049            # be a RemoteRepository
4050            self.assertLength(1, remote_repo._fallback_repositories)
4051            self.assertIsInstance(remote_repo._fallback_repositories[0],
4052                                  RemoteRepository)
4053            # and it has the revision committed to the underlying repository;
4054            # these have varying implementations so we try several of them
4055            self.assertTrue(remote_repo.has_revisions([base_revid]))
4056            self.assertTrue(remote_repo.has_revision(base_revid))
4057            self.assertEqual(remote_repo.get_revision(base_revid).message,
4058                             'message')
4059        finally:
4060            remote_repo.unlock()
4061
4062    def prepare_stacked_remote_branch(self):
4063        """Get stacked_upon and stacked branches with content in each."""
4064        self.setup_smart_server_with_call_log()
4065        tree1 = self.make_branch_and_tree('tree1', format='1.9')
4066        tree1.commit('rev1', rev_id=b'rev1')
4067        tree2 = tree1.branch.controldir.sprout('tree2', stacked=True
4068                                               ).open_workingtree()
4069        local_tree = tree2.branch.create_checkout('local')
4070        local_tree.commit('local changes make me feel good.')
4071        branch2 = Branch.open(self.get_url('tree2'))
4072        branch2.lock_read()
4073        self.addCleanup(branch2.unlock)
4074        return tree1.branch, branch2
4075
4076    def test_stacked_get_parent_map(self):
4077        # the public implementation of get_parent_map obeys stacking
4078        _, branch = self.prepare_stacked_remote_branch()
4079        repo = branch.repository
4080        self.assertEqual({b'rev1'}, set(repo.get_parent_map([b'rev1'])))
4081
4082    def test_unstacked_get_parent_map(self):
4083        # _unstacked_provider.get_parent_map ignores stacking
4084        _, branch = self.prepare_stacked_remote_branch()
4085        provider = branch.repository._unstacked_provider
4086        self.assertEqual(set(), set(provider.get_parent_map([b'rev1'])))
4087
4088    def fetch_stream_to_rev_order(self, stream):
4089        result = []
4090        for kind, substream in stream:
4091            if not kind == 'revisions':
4092                list(substream)
4093            else:
4094                for content in substream:
4095                    result.append(content.key[-1])
4096        return result
4097
4098    def get_ordered_revs(self, format, order, branch_factory=None):
4099        """Get a list of the revisions in a stream to format format.
4100
4101        :param format: The format of the target.
4102        :param order: the order that target should have requested.
4103        :param branch_factory: A callable to create a trunk and stacked branch
4104            to fetch from. If none, self.prepare_stacked_remote_branch is used.
4105        :result: The revision ids in the stream, in the order seen,
4106            the topological order of revisions in the source.
4107        """
4108        unordered_format = controldir.format_registry.get(format)()
4109        target_repository_format = unordered_format.repository_format
4110        # Cross check
4111        self.assertEqual(order, target_repository_format._fetch_order)
4112        if branch_factory is None:
4113            branch_factory = self.prepare_stacked_remote_branch
4114        _, stacked = branch_factory()
4115        source = stacked.repository._get_source(target_repository_format)
4116        tip = stacked.last_revision()
4117        stacked.repository._ensure_real()
4118        graph = stacked.repository.get_graph()
4119        revs = [r for (r, ps) in graph.iter_ancestry([tip])
4120                if r != NULL_REVISION]
4121        revs.reverse()
4122        search = vf_search.PendingAncestryResult([tip], stacked.repository)
4123        self.reset_smart_call_log()
4124        stream = source.get_stream(search)
4125        # We trust that if a revision is in the stream the rest of the new
4126        # content for it is too, as per our main fetch tests; here we are
4127        # checking that the revisions are actually included at all, and their
4128        # order.
4129        return self.fetch_stream_to_rev_order(stream), revs
4130
4131    def test_stacked_get_stream_unordered(self):
4132        # Repository._get_source.get_stream() from a stacked repository with
4133        # unordered yields the full data from both stacked and stacked upon
4134        # sources.
4135        rev_ord, expected_revs = self.get_ordered_revs('1.9', 'unordered')
4136        self.assertEqual(set(expected_revs), set(rev_ord))
4137        # Getting unordered results should have made a streaming data request
4138        # from the server, then one from the backing branch.
4139        self.assertLength(2, self.hpss_calls)
4140
4141    def test_stacked_on_stacked_get_stream_unordered(self):
4142        # Repository._get_source.get_stream() from a stacked repository which
4143        # is itself stacked yields the full data from all three sources.
4144        def make_stacked_stacked():
4145            _, stacked = self.prepare_stacked_remote_branch()
4146            tree = stacked.controldir.sprout('tree3', stacked=True
4147                                             ).open_workingtree()
4148            local_tree = tree.branch.create_checkout('local-tree3')
4149            local_tree.commit('more local changes are better')
4150            branch = Branch.open(self.get_url('tree3'))
4151            branch.lock_read()
4152            self.addCleanup(branch.unlock)
4153            return None, branch
4154        rev_ord, expected_revs = self.get_ordered_revs(
4155            '1.9', 'unordered', branch_factory=make_stacked_stacked)
4156        self.assertEqual(set(expected_revs), set(rev_ord))
4157        # Getting unordered results should have made a streaming data request
4158        # from the server, and one from each backing repo
4159        self.assertLength(3, self.hpss_calls)
4160
4161    def test_stacked_get_stream_topological(self):
4162        # Repository._get_source.get_stream() from a stacked repository with
4163        # topological sorting yields the full data from both stacked and
4164        # stacked upon sources in topological order.
4165        rev_ord, expected_revs = self.get_ordered_revs('knit', 'topological')
4166        self.assertEqual(expected_revs, rev_ord)
4167        # Getting topological sort requires VFS calls still - one of which is
4168        # pushing up from the bound branch.
4169        self.assertLength(14, self.hpss_calls)
4170
4171    def test_stacked_get_stream_groupcompress(self):
4172        # Repository._get_source.get_stream() from a stacked repository with
4173        # groupcompress sorting yields the full data from both stacked and
4174        # stacked upon sources in groupcompress order.
4175        raise tests.TestSkipped('No groupcompress ordered format available')
4176        rev_ord, expected_revs = self.get_ordered_revs('dev5', 'groupcompress')
4177        self.assertEqual(expected_revs, reversed(rev_ord))
4178        # Getting unordered results should have made a streaming data request
4179        # from the backing branch, and one from the stacked on branch.
4180        self.assertLength(2, self.hpss_calls)
4181
4182    def test_stacked_pull_more_than_stacking_has_bug_360791(self):
4183        # When pulling some fixed amount of content that is more than the
4184        # source has (because some is coming from a fallback branch, no error
4185        # should be received. This was reported as bug 360791.
4186        # Need three branches: a trunk, a stacked branch, and a preexisting
4187        # branch pulling content from stacked and trunk.
4188        self.setup_smart_server_with_call_log()
4189        trunk = self.make_branch_and_tree('trunk', format="1.9-rich-root")
4190        trunk.commit('start')
4191        stacked_branch = trunk.branch.create_clone_on_transport(
4192            self.get_transport('stacked'), stacked_on=trunk.branch.base)
4193        local = self.make_branch('local', format='1.9-rich-root')
4194        local.repository.fetch(stacked_branch.repository,
4195                               stacked_branch.last_revision())
4196
4197
4198class TestRemoteBranchEffort(tests.TestCaseWithTransport):
4199
4200    def setUp(self):
4201        super(TestRemoteBranchEffort, self).setUp()
4202        # Create a smart server that publishes whatever the backing VFS server
4203        # does.
4204        self.smart_server = test_server.SmartTCPServer_for_testing()
4205        self.start_server(self.smart_server, self.get_server())
4206        # Log all HPSS calls into self.hpss_calls.
4207        _SmartClient.hooks.install_named_hook(
4208            'call', self.capture_hpss_call, None)
4209        self.hpss_calls = []
4210
4211    def capture_hpss_call(self, params):
4212        self.hpss_calls.append(params.method)
4213
4214    def test_copy_content_into_avoids_revision_history(self):
4215        local = self.make_branch('local')
4216        builder = self.make_branch_builder('remote')
4217        builder.build_commit(message="Commit.")
4218        remote_branch_url = self.smart_server.get_url() + 'remote'
4219        remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
4220        local.repository.fetch(remote_branch.repository)
4221        self.hpss_calls = []
4222        remote_branch.copy_content_into(local)
4223        self.assertFalse(b'Branch.revision_history' in self.hpss_calls)
4224
4225    def test_fetch_everything_needs_just_one_call(self):
4226        local = self.make_branch('local')
4227        builder = self.make_branch_builder('remote')
4228        builder.build_commit(message="Commit.")
4229        remote_branch_url = self.smart_server.get_url() + 'remote'
4230        remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
4231        self.hpss_calls = []
4232        local.repository.fetch(
4233            remote_branch.repository,
4234            fetch_spec=vf_search.EverythingResult(remote_branch.repository))
4235        self.assertEqual([b'Repository.get_stream_1.19'], self.hpss_calls)
4236
4237    def override_verb(self, verb_name, verb):
4238        request_handlers = request.request_handlers
4239        orig_verb = request_handlers.get(verb_name)
4240        orig_info = request_handlers.get_info(verb_name)
4241        request_handlers.register(verb_name, verb, override_existing=True)
4242        self.addCleanup(request_handlers.register, verb_name, orig_verb,
4243                        override_existing=True, info=orig_info)
4244
4245    def test_fetch_everything_backwards_compat(self):
4246        """Can fetch with EverythingResult even with pre 2.4 servers.
4247
4248        Pre-2.4 do not support 'everything' searches with the
4249        Repository.get_stream_1.19 verb.
4250        """
4251        verb_log = []
4252
4253        class OldGetStreamVerb(SmartServerRepositoryGetStream_1_19):
4254            """A version of the Repository.get_stream_1.19 verb patched to
4255            reject 'everything' searches the way 2.3 and earlier do.
4256            """
4257
4258            def recreate_search(self, repository, search_bytes,
4259                                discard_excess=False):
4260                verb_log.append(search_bytes.split(b'\n', 1)[0])
4261                if search_bytes == b'everything':
4262                    return (None,
4263                            request.FailedSmartServerResponse((b'BadSearch',)))
4264                return super(OldGetStreamVerb,
4265                             self).recreate_search(repository, search_bytes,
4266                                                   discard_excess=discard_excess)
4267        self.override_verb(b'Repository.get_stream_1.19', OldGetStreamVerb)
4268        local = self.make_branch('local')
4269        builder = self.make_branch_builder('remote')
4270        builder.build_commit(message="Commit.")
4271        remote_branch_url = self.smart_server.get_url() + 'remote'
4272        remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
4273        self.hpss_calls = []
4274        local.repository.fetch(
4275            remote_branch.repository,
4276            fetch_spec=vf_search.EverythingResult(remote_branch.repository))
4277        # make sure the overridden verb was used
4278        self.assertLength(1, verb_log)
4279        # more than one HPSS call is needed, but because it's a VFS callback
4280        # its hard to predict exactly how many.
4281        self.assertTrue(len(self.hpss_calls) > 1)
4282
4283
4284class TestUpdateBoundBranchWithModifiedBoundLocation(
4285        tests.TestCaseWithTransport):
4286    """Ensure correct handling of bound_location modifications.
4287
4288    This is tested against a smart server as http://pad.lv/786980 was about a
4289    ReadOnlyError (write attempt during a read-only transaction) which can only
4290    happen in this context.
4291    """
4292
4293    def setUp(self):
4294        super(TestUpdateBoundBranchWithModifiedBoundLocation, self).setUp()
4295        self.transport_server = test_server.SmartTCPServer_for_testing
4296
4297    def make_master_and_checkout(self, master_name, checkout_name):
4298        # Create the master branch and its associated checkout
4299        self.master = self.make_branch_and_tree(master_name)
4300        self.checkout = self.master.branch.create_checkout(checkout_name)
4301        # Modify the master branch so there is something to update
4302        self.master.commit('add stuff')
4303        self.last_revid = self.master.commit('even more stuff')
4304        self.bound_location = self.checkout.branch.get_bound_location()
4305
4306    def assertUpdateSucceeds(self, new_location):
4307        self.checkout.branch.set_bound_location(new_location)
4308        self.checkout.update()
4309        self.assertEqual(self.last_revid, self.checkout.last_revision())
4310
4311    def test_without_final_slash(self):
4312        self.make_master_and_checkout('master', 'checkout')
4313        # For unclear reasons some users have a bound_location without a final
4314        # '/', simulate that by forcing such a value
4315        self.assertEndsWith(self.bound_location, '/')
4316        self.assertUpdateSucceeds(self.bound_location.rstrip('/'))
4317
4318    def test_plus_sign(self):
4319        self.make_master_and_checkout('+master', 'checkout')
4320        self.assertUpdateSucceeds(self.bound_location.replace('%2B', '+', 1))
4321
4322    def test_tilda(self):
4323        # Embed ~ in the middle of the path just to avoid any $HOME
4324        # interpretation
4325        self.make_master_and_checkout('mas~ter', 'checkout')
4326        self.assertUpdateSucceeds(self.bound_location.replace('%2E', '~', 1))
4327
4328
4329class TestWithCustomErrorHandler(RemoteBranchTestCase):
4330
4331    def test_no_context(self):
4332        class OutOfCoffee(errors.BzrError):
4333            """A dummy exception for testing."""
4334
4335            def __init__(self, urgency):
4336                self.urgency = urgency
4337        remote.no_context_error_translators.register(b"OutOfCoffee",
4338                                                     lambda err: OutOfCoffee(err.error_args[0]))
4339        transport = MemoryTransport()
4340        client = FakeClient(transport.base)
4341        client.add_expected_call(
4342            b'Branch.get_stacked_on_url', (b'quack/',),
4343            b'error', (b'NotStacked',))
4344        client.add_expected_call(
4345            b'Branch.last_revision_info',
4346            (b'quack/',),
4347            b'error', (b'OutOfCoffee', b'low'))
4348        transport.mkdir('quack')
4349        transport = transport.clone('quack')
4350        branch = self.make_remote_branch(transport, client)
4351        self.assertRaises(OutOfCoffee, branch.last_revision_info)
4352        self.assertFinished(client)
4353
4354    def test_with_context(self):
4355        class OutOfTea(errors.BzrError):
4356            def __init__(self, branch, urgency):
4357                self.branch = branch
4358                self.urgency = urgency
4359        remote.error_translators.register(b"OutOfTea",
4360                                          lambda err, find, path: OutOfTea(
4361                                              err.error_args[0].decode(
4362                                                  'utf-8'),
4363                                              find("branch")))
4364        transport = MemoryTransport()
4365        client = FakeClient(transport.base)
4366        client.add_expected_call(
4367            b'Branch.get_stacked_on_url', (b'quack/',),
4368            b'error', (b'NotStacked',))
4369        client.add_expected_call(
4370            b'Branch.last_revision_info',
4371            (b'quack/',),
4372            b'error', (b'OutOfTea', b'low'))
4373        transport.mkdir('quack')
4374        transport = transport.clone('quack')
4375        branch = self.make_remote_branch(transport, client)
4376        self.assertRaises(OutOfTea, branch.last_revision_info)
4377        self.assertFinished(client)
4378
4379
4380class TestRepositoryPack(TestRemoteRepository):
4381
4382    def test_pack(self):
4383        transport_path = 'quack'
4384        repo, client = self.setup_fake_client_and_repository(transport_path)
4385        client.add_expected_call(
4386            b'Repository.lock_write', (b'quack/', b''),
4387            b'success', (b'ok', b'token'))
4388        client.add_expected_call(
4389            b'Repository.pack', (b'quack/', b'token', b'False'),
4390            b'success', (b'ok',), )
4391        client.add_expected_call(
4392            b'Repository.unlock', (b'quack/', b'token'),
4393            b'success', (b'ok', ))
4394        repo.pack()
4395
4396    def test_pack_with_hint(self):
4397        transport_path = 'quack'
4398        repo, client = self.setup_fake_client_and_repository(transport_path)
4399        client.add_expected_call(
4400            b'Repository.lock_write', (b'quack/', b''),
4401            b'success', (b'ok', b'token'))
4402        client.add_expected_call(
4403            b'Repository.pack', (b'quack/', b'token', b'False'),
4404            b'success', (b'ok',), )
4405        client.add_expected_call(
4406            b'Repository.unlock', (b'quack/', b'token', b'False'),
4407            b'success', (b'ok', ))
4408        repo.pack(['hinta', 'hintb'])
4409
4410
4411class TestRepositoryIterInventories(TestRemoteRepository):
4412    """Test Repository.iter_inventories."""
4413
4414    def _serialize_inv_delta(self, old_name, new_name, delta):
4415        serializer = inventory_delta.InventoryDeltaSerializer(True, False)
4416        return b"".join(serializer.delta_to_lines(old_name, new_name, delta))
4417
4418    def test_single_empty(self):
4419        transport_path = 'quack'
4420        repo, client = self.setup_fake_client_and_repository(transport_path)
4421        fmt = controldir.format_registry.get('2a')().repository_format
4422        repo._format = fmt
4423        stream = [('inventory-deltas', [
4424            versionedfile.FulltextContentFactory(b'somerevid', None, None,
4425                                                 self._serialize_inv_delta(b'null:', b'somerevid', []))])]
4426        client.add_expected_call(
4427            b'VersionedFileRepository.get_inventories', (
4428                b'quack/', b'unordered'),
4429            b'success', (b'ok', ),
4430            _stream_to_byte_stream(stream, fmt))
4431        ret = list(repo.iter_inventories([b"somerevid"]))
4432        self.assertLength(1, ret)
4433        inv = ret[0]
4434        self.assertEqual(b"somerevid", inv.revision_id)
4435
4436    def test_empty(self):
4437        transport_path = 'quack'
4438        repo, client = self.setup_fake_client_and_repository(transport_path)
4439        ret = list(repo.iter_inventories([]))
4440        self.assertEqual(ret, [])
4441
4442    def test_missing(self):
4443        transport_path = 'quack'
4444        repo, client = self.setup_fake_client_and_repository(transport_path)
4445        client.add_expected_call(
4446            b'VersionedFileRepository.get_inventories', (
4447                b'quack/', b'unordered'),
4448            b'success', (b'ok', ), iter([]))
4449        self.assertRaises(errors.NoSuchRevision, list, repo.iter_inventories(
4450            [b"somerevid"]))
4451
4452
4453class TestRepositoryRevisionTreeArchive(TestRemoteRepository):
4454    """Test Repository.iter_inventories."""
4455
4456    def _serialize_inv_delta(self, old_name, new_name, delta):
4457        serializer = inventory_delta.InventoryDeltaSerializer(True, False)
4458        return b"".join(serializer.delta_to_lines(old_name, new_name, delta))
4459
4460    def test_simple(self):
4461        transport_path = 'quack'
4462        repo, client = self.setup_fake_client_and_repository(transport_path)
4463        fmt = controldir.format_registry.get('2a')().repository_format
4464        repo._format = fmt
4465        stream = [('inventory-deltas', [
4466            versionedfile.FulltextContentFactory(b'somerevid', None, None,
4467                                                 self._serialize_inv_delta(b'null:', b'somerevid', []))])]
4468        client.add_expected_call(
4469            b'VersionedFileRepository.get_inventories', (
4470                b'quack/', b'unordered'),
4471            b'success', (b'ok', ),
4472            _stream_to_byte_stream(stream, fmt))
4473        f = BytesIO()
4474        with tarfile.open(mode='w', fileobj=f) as tf:
4475            info = tarfile.TarInfo('somefile')
4476            info.mtime = 432432
4477            contents = b'some data'
4478            info.type = tarfile.REGTYPE
4479            info.mode = 0o644
4480            info.size = len(contents)
4481            tf.addfile(info, BytesIO(contents))
4482        client.add_expected_call(
4483            b'Repository.revision_archive', (b'quack/',
4484                                             b'somerevid', b'tar', b'foo.tar', b'', b'', None),
4485            b'success', (b'ok', ),
4486            f.getvalue())
4487        tree = repo.revision_tree(b'somerevid')
4488        self.assertEqual(f.getvalue(), b''.join(
4489            tree.archive('tar', 'foo.tar')))
4490
4491
4492class TestRepositoryAnnotate(TestRemoteRepository):
4493    """Test RemoteRevisionTree.annotate.."""
4494
4495    def _serialize_inv_delta(self, old_name, new_name, delta):
4496        serializer = inventory_delta.InventoryDeltaSerializer(True, False)
4497        return b"".join(serializer.delta_to_lines(old_name, new_name, delta))
4498
4499    def test_simple(self):
4500        transport_path = 'quack'
4501        repo, client = self.setup_fake_client_and_repository(transport_path)
4502        fmt = controldir.format_registry.get('2a')().repository_format
4503        repo._format = fmt
4504        stream = [
4505            ('inventory-deltas', [
4506                versionedfile.FulltextContentFactory(
4507                    b'somerevid', None, None,
4508                    self._serialize_inv_delta(b'null:', b'somerevid', []))])]
4509        client.add_expected_call(
4510            b'VersionedFileRepository.get_inventories', (
4511                b'quack/', b'unordered'),
4512            b'success', (b'ok', ),
4513            _stream_to_byte_stream(stream, fmt))
4514        client.add_expected_call(
4515            b'Repository.annotate_file_revision',
4516            (b'quack/', b'somerevid', b'filename', b'', b'current:'),
4517            b'success', (b'ok', ),
4518            bencode.bencode([[b'baserevid', b'line 1\n'],
4519                             [b'somerevid', b'line2\n']]))
4520        tree = repo.revision_tree(b'somerevid')
4521        self.assertEqual([
4522            (b'baserevid', b'line 1\n'),
4523            (b'somerevid', b'line2\n')],
4524            list(tree.annotate_iter('filename')))
4525
4526
4527class TestBranchGetAllReferenceInfo(RemoteBranchTestCase):
4528
4529    def test_get_all_reference_info(self):
4530        transport = MemoryTransport()
4531        client = FakeClient(transport.base)
4532        client.add_expected_call(
4533            b'Branch.get_stacked_on_url', (b'quack/',),
4534            b'error', (b'NotStacked',))
4535        client.add_expected_call(
4536            b'Branch.get_all_reference_info', (b'quack/',),
4537            b'success', (b'ok',), bencode.bencode([
4538                (b'file-id', b'https://www.example.com/', b'')]))
4539        transport.mkdir('quack')
4540        transport = transport.clone('quack')
4541        branch = self.make_remote_branch(transport, client)
4542        result = branch._get_all_reference_info()
4543        self.assertFinished(client)
4544        self.assertEqual({b'file-id': ('https://www.example.com/', None)}, result)
4545