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']))