131
132
class FakeClient(_SmartClient):
 
132
133
    """Lookalike for _SmartClient allowing testing."""
 
134
 
    def __init__(self, responses, fake_medium_base='fake base'):
 
 
135
    def __init__(self, fake_medium_base='fake base'):
 
135
136
        """Create a FakeClient.
 
137
138
        :param responses: A list of response-tuple, body-data pairs to be sent
 
 
139
            back to callers.  A special case is if the response-tuple is
 
 
140
            'unknown verb', then a UnknownSmartMethod will be raised for that
 
 
141
            call, using the second element of the tuple as the verb in the
 
140
 
        self.responses = responses
 
142
146
        self.expecting_body = False
 
143
 
        _SmartClient.__init__(self, FakeMedium(fake_medium_base, self._calls))
 
 
147
        _SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
 
 
149
    def add_success_response(self, *args):
 
 
150
        self.responses.append(('success', args, None))
 
 
152
    def add_success_response_with_body(self, body, *args):
 
 
153
        self.responses.append(('success', args, body))
 
 
155
    def add_error_response(self, *args):
 
 
156
        self.responses.append(('error', args))
 
 
158
    def add_unknown_method_response(self, verb):
 
 
159
        self.responses.append(('unknown', verb))
 
 
161
    def _get_next_response(self):
 
 
162
        response_tuple = self.responses.pop(0)
 
 
163
        if response_tuple[0] == 'unknown':
 
 
164
            raise errors.UnknownSmartMethod(response_tuple[1])
 
 
165
        elif response_tuple[0] == 'error':
 
 
166
            raise errors.ErrorFromSmartServer(response_tuple[1])
 
 
167
        return response_tuple
 
145
169
    def call(self, method, *args):
 
146
170
        self._calls.append(('call', method, args))
 
147
 
        return self.responses.pop(0)[0]
 
 
171
        return self._get_next_response()[1]
 
149
173
    def call_expecting_body(self, method, *args):
 
150
174
        self._calls.append(('call_expecting_body', method, args))
 
151
 
        result = self.responses.pop(0)
 
 
175
        result = self._get_next_response()
 
152
176
        self.expecting_body = True
 
153
 
        return result[0], FakeProtocol(result[1], self)
 
 
177
        return result[1], FakeProtocol(result[2], self)
 
155
179
    def call_with_body_bytes_expecting_body(self, method, args, body):
 
156
180
        self._calls.append(('call_with_body_bytes_expecting_body', method,
 
158
 
        result = self.responses.pop(0)
 
 
182
        result = self._get_next_response()
 
159
183
        self.expecting_body = True
 
160
 
        return result[0], FakeProtocol(result[1], self)
 
163
 
class FakeMedium(object):
 
165
 
    def __init__(self, base, client_calls):
 
 
184
        return result[1], FakeProtocol(result[2], self)
 
 
187
class FakeMedium(medium.SmartClientMedium):
 
 
189
    def __init__(self, client_calls, base):
 
 
190
        self._remote_is_at_least_1_2 = True
 
 
191
        self._client_calls = client_calls
 
167
 
        self.connection = FakeConnection(client_calls)
 
168
 
        self._client_calls = client_calls
 
171
 
class FakeConnection(object):
 
173
 
    def __init__(self, client_calls):
 
174
 
        self._remote_is_at_least_1_2 = True
 
175
 
        self._client_calls = client_calls
 
177
194
    def disconnect(self):
 
178
195
        self._client_calls.append(('disconnect medium',))
 
 
191
209
        self.assertTrue(result)
 
 
212
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
 
 
213
    """Tests for the behaviour of client_medium.remote_path_from_transport."""
 
 
215
    def assertRemotePath(self, expected, client_base, transport_base):
 
 
216
        """Assert that the result of
 
 
217
        SmartClientMedium.remote_path_from_transport is the expected value for
 
 
218
        a given client_base and transport_base.
 
 
220
        client_medium = medium.SmartClientMedium(client_base)
 
 
221
        transport = get_transport(transport_base)
 
 
222
        result = client_medium.remote_path_from_transport(transport)
 
 
223
        self.assertEqual(expected, result)
 
 
225
    def test_remote_path_from_transport(self):
 
 
226
        """SmartClientMedium.remote_path_from_transport calculates a URL for
 
 
227
        the given transport relative to the root of the client base URL.
 
 
229
        self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
 
 
230
        self.assertRemotePath(
 
 
231
            'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
 
 
233
    def assertRemotePathHTTP(self, expected, transport_base, relpath):
 
 
234
        """Assert that the result of
 
 
235
        HttpTransportBase.remote_path_from_transport is the expected value for
 
 
236
        a given transport_base and relpath of that transport.  (Note that
 
 
237
        HttpTransportBase is a subclass of SmartClientMedium)
 
 
239
        base_transport = get_transport(transport_base)
 
 
240
        client_medium = base_transport.get_smart_medium()
 
 
241
        cloned_transport = base_transport.clone(relpath)
 
 
242
        result = client_medium.remote_path_from_transport(cloned_transport)
 
 
243
        self.assertEqual(expected, result)
 
 
245
    def test_remote_path_from_transport_http(self):
 
 
246
        """Remote paths for HTTP transports are calculated differently to other
 
 
247
        transports.  They are just relative to the client base, not the root
 
 
248
        directory of the host.
 
 
250
        for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
 
 
251
            self.assertRemotePathHTTP(
 
 
252
                '../xyz/', scheme + '//host/path', '../xyz/')
 
 
253
            self.assertRemotePathHTTP(
 
 
254
                'xyz/', scheme + '//host/path', 'xyz/')
 
194
257
class TestBzrDirOpenBranch(tests.TestCase):
 
196
259
    def test_branch_present(self):
 
197
260
        transport = MemoryTransport()
 
198
261
        transport.mkdir('quack')
 
199
262
        transport = transport.clone('quack')
 
200
 
        client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
 
 
263
        client = FakeClient(transport.base)
 
 
264
        client.add_success_response('ok', '')
 
 
265
        client.add_success_response('ok', '', 'no', 'no', 'no')
 
202
266
        bzrdir = RemoteBzrDir(transport, _client=client)
 
203
267
        result = bzrdir.open_branch()
 
204
268
        self.assertEqual(
 
 
 
527
class TestBranchSetLastRevisionInfo(tests.TestCase):
 
 
529
    def test_set_last_revision_info(self):
 
 
530
        # set_last_revision_info(num, 'rev-id') is translated to calling
 
 
531
        # Branch.set_last_revision_info(num, 'rev-id') on the wire.
 
 
532
        transport = MemoryTransport()
 
 
533
        transport.mkdir('branch')
 
 
534
        transport = transport.clone('branch')
 
 
535
        client = FakeClient(transport.base)
 
 
537
        client.add_success_response('ok', 'branch token', 'repo token')
 
 
539
        client.add_success_response('ok')
 
 
541
        client.add_success_response('ok')
 
 
543
        bzrdir = RemoteBzrDir(transport, _client=False)
 
 
544
        branch = RemoteBranch(bzrdir, None, _client=client)
 
 
545
        # This is a hack to work around the problem that RemoteBranch currently
 
 
546
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
 
547
        branch._ensure_real = lambda: None
 
 
548
        # Lock the branch, reset the record of remote calls.
 
 
551
        result = branch.set_last_revision_info(1234, 'a-revision-id')
 
 
553
            [('call', 'Branch.set_last_revision_info',
 
 
554
                ('branch/', 'branch token', 'repo token',
 
 
555
                 '1234', 'a-revision-id'))],
 
 
557
        self.assertEqual(None, result)
 
 
559
    def test_no_such_revision(self):
 
 
560
        # A response of 'NoSuchRevision' is translated into an exception.
 
 
561
        transport = MemoryTransport()
 
 
562
        transport.mkdir('branch')
 
 
563
        transport = transport.clone('branch')
 
 
564
        client = FakeClient(transport.base)
 
 
566
        client.add_success_response('ok', 'branch token', 'repo token')
 
 
568
        client.add_error_response('NoSuchRevision', 'revid')
 
 
570
        client.add_success_response('ok')
 
 
572
        bzrdir = RemoteBzrDir(transport, _client=False)
 
 
573
        branch = RemoteBranch(bzrdir, None, _client=client)
 
 
574
        # This is a hack to work around the problem that RemoteBranch currently
 
 
575
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
 
576
        branch._ensure_real = lambda: None
 
 
577
        # Lock the branch, reset the record of remote calls.
 
 
582
            errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
 
 
585
    def lock_remote_branch(self, branch):
 
 
586
        """Trick a RemoteBranch into thinking it is locked."""
 
 
587
        branch._lock_mode = 'w'
 
 
588
        branch._lock_count = 2
 
 
589
        branch._lock_token = 'branch token'
 
 
590
        branch._repo_lock_token = 'repo token'
 
 
592
    def test_backwards_compatibility(self):
 
 
593
        """If the server does not support the Branch.set_last_revision_info
 
 
594
        verb (which is new in 1.4), then the client falls back to VFS methods.
 
 
596
        # This test is a little messy.  Unlike most tests in this file, it
 
 
597
        # doesn't purely test what a Remote* object sends over the wire, and
 
 
598
        # how it reacts to responses from the wire.  It instead relies partly
 
 
599
        # on asserting that the RemoteBranch will call
 
 
600
        # self._real_branch.set_last_revision_info(...).
 
 
602
        # First, set up our RemoteBranch with a FakeClient that raises
 
 
603
        # UnknownSmartMethod, and a StubRealBranch that logs how it is called.
 
 
604
        transport = MemoryTransport()
 
 
605
        transport.mkdir('branch')
 
 
606
        transport = transport.clone('branch')
 
 
607
        client = FakeClient(transport.base)
 
 
608
        client.add_unknown_method_response('Branch.set_last_revision_info')
 
 
609
        bzrdir = RemoteBzrDir(transport, _client=False)
 
 
610
        branch = RemoteBranch(bzrdir, None, _client=client)
 
 
611
        class StubRealBranch(object):
 
 
614
            def set_last_revision_info(self, revno, revision_id):
 
 
616
                    ('set_last_revision_info', revno, revision_id))
 
 
617
        real_branch = StubRealBranch()
 
 
618
        branch._real_branch = real_branch
 
 
619
        self.lock_remote_branch(branch)
 
 
621
        # Call set_last_revision_info, and verify it behaved as expected.
 
 
622
        result = branch.set_last_revision_info(1234, 'a-revision-id')
 
 
624
            [('call', 'Branch.set_last_revision_info',
 
 
625
                ('branch/', 'branch token', 'repo token',
 
 
626
                 '1234', 'a-revision-id')),],
 
 
629
            [('set_last_revision_info', 1234, 'a-revision-id')],
 
 
632
    def test_unexpected_error(self):
 
 
633
        # A response of 'NoSuchRevision' is translated into an exception.
 
 
634
        transport = MemoryTransport()
 
 
635
        transport.mkdir('branch')
 
 
636
        transport = transport.clone('branch')
 
 
637
        client = FakeClient(transport.base)
 
 
639
        client.add_success_response('ok', 'branch token', 'repo token')
 
 
641
        client.add_error_response('UnexpectedError')
 
 
643
        client.add_success_response('ok')
 
 
645
        bzrdir = RemoteBzrDir(transport, _client=False)
 
 
646
        branch = RemoteBranch(bzrdir, None, _client=client)
 
 
647
        # This is a hack to work around the problem that RemoteBranch currently
 
 
648
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
 
649
        branch._ensure_real = lambda: None
 
 
650
        # Lock the branch, reset the record of remote calls.
 
 
654
        err = self.assertRaises(
 
 
655
            errors.ErrorFromSmartServer,
 
 
656
            branch.set_last_revision_info, 123, 'revid')
 
 
657
        self.assertEqual(('UnexpectedError',), err.error_tuple)
 
441
661
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
 
442
 
    """Test branch.control_files api munging...
 
444
 
    We special case RemoteBranch.control_files.get('branch.conf') to
 
445
 
    call a specific API so that RemoteBranch's can intercept configuration
 
446
 
    file reading, allowing them to signal to the client about things like
 
447
 
    'email is configured for commits'.
 
 
662
    """Getting the branch configuration should use an abstract method not vfs.
 
450
665
    def test_get_branch_conf(self):
 
451
 
        # in an empty branch we decode the response properly
 
452
 
        client = FakeClient([(('ok', ), 'config file body')], self.get_url())
 
453
 
        # we need to make a real branch because the remote_branch.control_files
 
454
 
        # will trigger _ensure_real.
 
455
 
        branch = self.make_branch('quack')
 
456
 
        transport = branch.bzrdir.root_transport
 
457
 
        # we do not want bzrdir to make any remote calls
 
458
 
        bzrdir = RemoteBzrDir(transport, _client=False)
 
459
 
        branch = RemoteBranch(bzrdir, None, _client=client)
 
460
 
        result = branch.control_files.get('branch.conf')
 
462
 
            [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
 
464
 
        self.assertEqual('config file body', result.read())
 
 
666
        raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
 
 
667
        ## # We should see that branch.get_config() does a single rpc to get the
 
 
668
        ## # remote configuration file, abstracting away where that is stored on
 
 
669
        ## # the server.  However at the moment it always falls back to using the
 
 
670
        ## # vfs, and this would need some changes in config.py.
 
 
672
        ## # in an empty branch we decode the response properly
 
 
673
        ## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
 
 
674
        ## # we need to make a real branch because the remote_branch.control_files
 
 
675
        ## # will trigger _ensure_real.
 
 
676
        ## branch = self.make_branch('quack')
 
 
677
        ## transport = branch.bzrdir.root_transport
 
 
678
        ## # we do not want bzrdir to make any remote calls
 
 
679
        ## bzrdir = RemoteBzrDir(transport, _client=False)
 
 
680
        ## branch = RemoteBranch(bzrdir, None, _client=client)
 
 
681
        ## config = branch.get_config()
 
 
683
        ##     [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
 
467
687
class TestBranchLockWrite(tests.TestCase):
 
469
689
    def test_lock_write_unlockable(self):
 
470
690
        transport = MemoryTransport()
 
471
 
        client = FakeClient([(('UnlockableTransport', ), '')], transport.base)
 
 
691
        client = FakeClient(transport.base)
 
 
692
        client.add_error_response('UnlockableTransport')
 
472
693
        transport.mkdir('quack')
 
473
694
        transport = transport.clone('quack')
 
474
695
        # we do not want bzrdir to make any remote calls
 
 
693
897
             ('call_expecting_body', 'Repository.get_revision_graph',
 
694
898
              ('quack/', ''))],
 
 
900
        # The medium is now marked as being connected to an older server
 
 
901
        self.assertFalse(client._medium._remote_is_at_least_1_2)
 
 
903
    def test_get_parent_map_fallback_parentless_node(self):
 
 
904
        """get_parent_map falls back to get_revision_graph on old servers.  The
 
 
905
        results from get_revision_graph are tweaked to match the get_parent_map
 
 
908
        Specifically, a {key: ()} result from get_revision_graph means "no
 
 
909
        parents" for that key, which in get_parent_map results should be
 
 
910
        represented as {key: ('null:',)}.
 
 
912
        This is the test for https://bugs.launchpad.net/bzr/+bug/214894
 
 
914
        rev_id = 'revision-id'
 
 
915
        transport_path = 'quack'
 
 
916
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
 
917
        client.add_success_response_with_body(rev_id, 'ok')
 
 
918
        client._medium._remote_is_at_least_1_2 = False
 
 
919
        expected_deprecations = [
 
 
920
            'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
 
 
922
        parents = self.callDeprecated(
 
 
923
            expected_deprecations, repo.get_parent_map, [rev_id])
 
 
925
            [('call_expecting_body', 'Repository.get_revision_graph',
 
 
928
        self.assertEqual({rev_id: ('null:',)}, parents)
 
 
930
    def test_get_parent_map_unexpected_response(self):
 
 
931
        repo, client = self.setup_fake_client_and_repository('path')
 
 
932
        client.add_success_response('something unexpected!')
 
 
934
            errors.UnexpectedSmartServerResponse,
 
 
935
            repo.get_parent_map, ['a-revision-id'])
 
699
938
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
 
 
750
987
    def test_no_such_revision(self):
 
752
 
        responses = [(('nosuchrevision', revid), '')]
 
753
989
        transport_path = 'sinhala'
 
754
 
        repo, client = self.setup_fake_client_and_repository(
 
755
 
            responses, transport_path)
 
 
990
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
 
991
        client.add_error_response('nosuchrevision', revid)
 
756
992
        # also check that the right revision is reported in the error
 
757
993
        self.assertRaises(errors.NoSuchRevision,
 
758
 
            repo.get_revision_graph, revid)
 
 
994
            self.applyDeprecated, one_four, repo.get_revision_graph, revid)
 
759
995
        self.assertEqual(
 
760
996
            [('call_expecting_body', 'Repository.get_revision_graph',
 
761
997
             ('sinhala/', revid))],
 
 
1000
    def test_unexpected_error(self):
 
 
1002
        transport_path = 'sinhala'
 
 
1003
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
 
1004
        client.add_error_response('AnUnexpectedError')
 
 
1005
        e = self.assertRaises(errors.ErrorFromSmartServer,
 
 
1006
            self.applyDeprecated, one_four, repo.get_revision_graph, revid)
 
 
1007
        self.assertEqual(('AnUnexpectedError',), e.error_tuple)
 
765
1010
class TestRepositoryIsShared(TestRemoteRepository):
 
767
1012
    def test_is_shared(self):
 
768
1013
        # ('yes', ) for Repository.is_shared -> 'True'.
 
769
 
        responses = [(('yes', ), )]
 
770
1014
        transport_path = 'quack'
 
771
 
        repo, client = self.setup_fake_client_and_repository(
 
772
 
            responses, transport_path)
 
 
1015
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
 
1016
        client.add_success_response('yes')
 
773
1017
        result = repo.is_shared()
 
774
1018
        self.assertEqual(
 
775
1019
            [('call', 'Repository.is_shared', ('quack/',))],
 
 
950
1184
        record = ('bytes', [('name1',), ('name2',)])
 
951
1185
        pack_stream = self.make_pack_stream([record])
 
952
 
        responses = [(('ok',), pack_stream), ]
 
953
1186
        transport_path = 'quack'
 
954
 
        repo, client = self.setup_fake_client_and_repository(
 
955
 
            responses, transport_path)
 
 
1187
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
 
1188
        client.add_success_response_with_body(pack_stream, 'ok')
 
956
1189
        search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
 
957
1190
        stream = repo.get_data_stream_for_search(search)
 
958
1191
        self.assertRaises(errors.SmartProtocolError, list, stream)
 
960
1193
    def test_backwards_compatibility(self):
 
961
1194
        """If the server doesn't recognise this request, fallback to VFS."""
 
963
 
            "Generic bzr smart protocol error: "
 
964
 
            "bad request 'Repository.stream_revisions_chunked'")
 
966
 
            (('error', error_msg), '')]
 
967
 
        repo, client = self.setup_fake_client_and_repository(
 
 
1195
        repo, client = self.setup_fake_client_and_repository('path')
 
 
1196
        client.add_unknown_method_response(
 
 
1197
            'Repository.stream_revisions_chunked')
 
969
1198
        self.mock_called = False
 
970
1199
        repo._real_repository = MockRealRepository(self)
 
971
1200
        search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))