141
141
call, using the second element of the tuple as the verb in the
144
self.responses = responses
146
146
self.expecting_body = False
147
_SmartClient.__init__(self, FakeMedium(self._calls), fake_medium_base)
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))
149
161
def _get_next_response(self):
150
162
response_tuple = self.responses.pop(0)
151
if response_tuple[0][0] == 'unknown verb':
152
raise errors.UnknownSmartMethod(response_tuple[0][1])
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])
153
167
return response_tuple
155
169
def call(self, method, *args):
156
170
self._calls.append(('call', method, args))
157
return self._get_next_response()[0]
171
return self._get_next_response()[1]
159
173
def call_expecting_body(self, method, *args):
160
174
self._calls.append(('call_expecting_body', method, args))
161
175
result = self._get_next_response()
162
176
self.expecting_body = True
163
return result[0], FakeProtocol(result[1], self)
177
return result[1], FakeProtocol(result[2], self)
165
179
def call_with_body_bytes_expecting_body(self, method, args, body):
166
180
self._calls.append(('call_with_body_bytes_expecting_body', method,
168
182
result = self._get_next_response()
169
183
self.expecting_body = True
170
return result[0], FakeProtocol(result[1], self)
173
class FakeMedium(object):
175
def __init__(self, client_calls):
176
self._remote_is_at_least_1_2 = True
184
return result[1], FakeProtocol(result[2], self)
187
class FakeMedium(medium.SmartClientMedium):
189
def __init__(self, client_calls, base):
190
medium.SmartClientMedium.__init__(self, base)
177
191
self._client_calls = client_calls
179
193
def disconnect(self):
193
208
self.assertTrue(result)
196
class Test_SmartClient_remote_path_from_transport(tests.TestCase):
197
"""Tests for the behaviour of _SmartClient.remote_path_from_transport."""
211
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
212
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
199
214
def assertRemotePath(self, expected, client_base, transport_base):
200
"""Assert that the result of _SmartClient.remote_path_from_transport
201
is the expected value for a given client_base and transport_base.
215
"""Assert that the result of
216
SmartClientMedium.remote_path_from_transport is the expected value for
217
a given client_base and transport_base.
203
dummy_medium = 'dummy medium'
204
client = _SmartClient(dummy_medium, client_base)
219
client_medium = medium.SmartClientMedium(client_base)
205
220
transport = get_transport(transport_base)
206
result = client.remote_path_from_transport(transport)
221
result = client_medium.remote_path_from_transport(transport)
207
222
self.assertEqual(expected, result)
209
224
def test_remote_path_from_transport(self):
210
"""_SmartClient.remote_path_from_transport calculates a URL for the
211
given transport relative to the root of the client base URL.
225
"""SmartClientMedium.remote_path_from_transport calculates a URL for
226
the given transport relative to the root of the client base URL.
213
228
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
214
229
self.assertRemotePath(
215
230
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
232
def assertRemotePathHTTP(self, expected, transport_base, relpath):
233
"""Assert that the result of
234
HttpTransportBase.remote_path_from_transport is the expected value for
235
a given transport_base and relpath of that transport. (Note that
236
HttpTransportBase is a subclass of SmartClientMedium)
238
base_transport = get_transport(transport_base)
239
client_medium = base_transport.get_smart_medium()
240
cloned_transport = base_transport.clone(relpath)
241
result = client_medium.remote_path_from_transport(cloned_transport)
242
self.assertEqual(expected, result)
217
244
def test_remote_path_from_transport_http(self):
218
245
"""Remote paths for HTTP transports are calculated differently to other
219
246
transports. They are just relative to the client base, not the root
220
247
directory of the host.
222
249
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
223
self.assertRemotePath(
224
'../xyz/', scheme + '//host/path', scheme + '//host/xyz')
225
self.assertRemotePath(
226
'xyz/', scheme + '//host/path', scheme + '//host/path/xyz')
250
self.assertRemotePathHTTP(
251
'../xyz/', scheme + '//host/path', '../xyz/')
252
self.assertRemotePathHTTP(
253
'xyz/', scheme + '//host/path', 'xyz/')
256
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
257
"""Tests for the behaviour of client_medium.remote_is_at_least."""
259
def test_initially_unlimited(self):
260
"""A fresh medium assumes that the remote side supports all
263
client_medium = medium.SmartClientMedium('dummy base')
264
self.assertFalse(client_medium._is_remote_before((99, 99)))
266
def test__remember_remote_is_before(self):
267
"""Calling _remember_remote_is_before ratchets down the known remote
270
client_medium = medium.SmartClientMedium('dummy base')
271
# Mark the remote side as being less than 1.6. The remote side may
273
client_medium._remember_remote_is_before((1, 6))
274
self.assertTrue(client_medium._is_remote_before((1, 6)))
275
self.assertFalse(client_medium._is_remote_before((1, 5)))
276
# Calling _remember_remote_is_before again with a lower value works.
277
client_medium._remember_remote_is_before((1, 5))
278
self.assertTrue(client_medium._is_remote_before((1, 5)))
279
# You cannot call _remember_remote_is_before with a larger value.
281
AssertionError, client_medium._remember_remote_is_before, (1, 9))
229
284
class TestBzrDirOpenBranch(tests.TestCase):
601
694
[('set_last_revision_info', 1234, 'a-revision-id')],
602
695
real_branch.calls)
697
def test_unexpected_error(self):
698
# A response of 'NoSuchRevision' is translated into an exception.
699
transport = MemoryTransport()
700
transport.mkdir('branch')
701
transport = transport.clone('branch')
702
client = FakeClient(transport.base)
704
client.add_success_response('ok', 'branch token', 'repo token')
706
client.add_error_response('UnexpectedError')
708
client.add_success_response('ok')
710
bzrdir = RemoteBzrDir(transport, _client=False)
711
repo = RemoteRepository(bzrdir, None, _client=client)
712
branch = RemoteBranch(bzrdir, repo, _client=client)
713
# This is a hack to work around the problem that RemoteBranch currently
714
# unnecessarily invokes _ensure_real upon a call to lock_write.
715
branch._ensure_real = lambda: None
716
# Lock the branch, reset the record of remote calls.
720
err = self.assertRaises(
721
errors.ErrorFromSmartServer,
722
branch.set_last_revision_info, 123, 'revid')
723
self.assertEqual(('UnexpectedError',), err.error_tuple)
726
def test_tip_change_rejected(self):
727
"""TipChangeRejected responses cause a TipChangeRejected exception to
730
transport = MemoryTransport()
731
transport.mkdir('branch')
732
transport = transport.clone('branch')
733
client = FakeClient(transport.base)
735
client.add_success_response('ok', 'branch token', 'repo token')
737
client.add_error_response('TipChangeRejected', 'rejection message')
739
client.add_success_response('ok')
741
bzrdir = RemoteBzrDir(transport, _client=False)
742
repo = RemoteRepository(bzrdir, None, _client=client)
743
branch = RemoteBranch(bzrdir, repo, _client=client)
744
# This is a hack to work around the problem that RemoteBranch currently
745
# unnecessarily invokes _ensure_real upon a call to lock_write.
746
branch._ensure_real = lambda: None
747
# Lock the branch, reset the record of remote calls.
749
self.addCleanup(branch.unlock)
752
# The 'TipChangeRejected' error response triggered by calling
753
# set_last_revision_info causes a TipChangeRejected exception.
754
err = self.assertRaises(
755
errors.TipChangeRejected,
756
branch.set_last_revision_info, 123, 'revid')
757
self.assertEqual('rejection message', err.msg)
605
760
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
606
761
"""Getting the branch configuration should use an abstract method not vfs.
609
764
def test_get_branch_conf(self):
610
765
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
611
# We should see that branch.get_config() does a single rpc to get the
612
# remote configuration file, abstracting away where that is stored on
613
# the server. However at the moment it always falls back to using the
614
# vfs, and this would need some changes in config.py.
766
## # We should see that branch.get_config() does a single rpc to get the
767
## # remote configuration file, abstracting away where that is stored on
768
## # the server. However at the moment it always falls back to using the
769
## # vfs, and this would need some changes in config.py.
616
# in an empty branch we decode the response properly
617
client = FakeClient([(('ok', ), '# config file body')], self.get_url())
618
# we need to make a real branch because the remote_branch.control_files
619
# will trigger _ensure_real.
620
branch = self.make_branch('quack')
621
transport = branch.bzrdir.root_transport
622
# we do not want bzrdir to make any remote calls
623
bzrdir = RemoteBzrDir(transport, _client=False)
624
branch = RemoteBranch(bzrdir, None, _client=client)
625
config = branch.get_config()
627
[('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
771
## # in an empty branch we decode the response properly
772
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
773
## # we need to make a real branch because the remote_branch.control_files
774
## # will trigger _ensure_real.
775
## branch = self.make_branch('quack')
776
## transport = branch.bzrdir.root_transport
777
## # we do not want bzrdir to make any remote calls
778
## bzrdir = RemoteBzrDir(transport, _client=False)
779
## branch = RemoteBranch(bzrdir, None, _client=client)
780
## config = branch.get_config()
782
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
631
786
class TestBranchLockWrite(tests.TestCase):
633
788
def test_lock_write_unlockable(self):
634
789
transport = MemoryTransport()
635
client = FakeClient([(('UnlockableTransport', ), '')], transport.base)
790
client = FakeClient(transport.base)
791
client.add_error_response('UnlockableTransport')
636
792
transport.mkdir('quack')
637
793
transport = transport.clone('quack')
638
794
# we do not want bzrdir to make any remote calls
639
795
bzrdir = RemoteBzrDir(transport, _client=False)
640
branch = RemoteBranch(bzrdir, None, _client=client)
796
repo = RemoteRepository(bzrdir, None, _client=client)
797
branch = RemoteBranch(bzrdir, repo, _client=client)
641
798
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
642
799
self.assertEqual(
643
800
[('call', 'Branch.lock_write', ('quack/', '', ''))],
1109
1255
src_repo.copy_content_into(dest_repo)
1112
class TestRepositoryStreamKnitData(TestRemoteRepository):
1114
def make_pack_file(self, records):
1115
pack_file = StringIO()
1116
pack_writer = pack.ContainerWriter(pack_file.write)
1118
for bytes, names in records:
1119
pack_writer.add_bytes_record(bytes, names)
1124
def make_pack_stream(self, records):
1125
pack_serialiser = pack.ContainerSerialiser()
1126
yield pack_serialiser.begin()
1127
for bytes, names in records:
1128
yield pack_serialiser.bytes_record(bytes, names)
1129
yield pack_serialiser.end()
1131
def test_bad_pack_from_server(self):
1132
"""A response with invalid data (e.g. it has a record with multiple
1133
names) triggers an exception.
1258
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1259
"""Base class for unit tests for bzrlib.remote._translate_error."""
1261
def translateTuple(self, error_tuple, **context):
1262
"""Call _translate_error with an ErrorFromSmartServer built from the
1265
:param error_tuple: A tuple of a smart server response, as would be
1266
passed to an ErrorFromSmartServer.
1267
:kwargs context: context items to call _translate_error with.
1269
:returns: The error raised by _translate_error.
1271
# Raise the ErrorFromSmartServer before passing it as an argument,
1272
# because _translate_error may need to re-raise it with a bare 'raise'
1274
server_error = errors.ErrorFromSmartServer(error_tuple)
1275
translated_error = self.translateErrorFromSmartServer(
1276
server_error, **context)
1277
return translated_error
1279
def translateErrorFromSmartServer(self, error_object, **context):
1280
"""Like translateTuple, but takes an already constructed
1281
ErrorFromSmartServer rather than a tuple.
1285
except errors.ErrorFromSmartServer, server_error:
1286
translated_error = self.assertRaises(
1287
errors.BzrError, remote._translate_error, server_error,
1289
return translated_error
1292
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1293
"""Unit tests for bzrlib.remote._translate_error.
1295
Given an ErrorFromSmartServer (which has an error tuple from a smart
1296
server) and some context, _translate_error raises more specific errors from
1299
This test case covers the cases where _translate_error succeeds in
1300
translating an ErrorFromSmartServer to something better. See
1301
TestErrorTranslationRobustness for other cases.
1304
def test_NoSuchRevision(self):
1305
branch = self.make_branch('')
1307
translated_error = self.translateTuple(
1308
('NoSuchRevision', revid), branch=branch)
1309
expected_error = errors.NoSuchRevision(branch, revid)
1310
self.assertEqual(expected_error, translated_error)
1312
def test_nosuchrevision(self):
1313
repository = self.make_repository('')
1315
translated_error = self.translateTuple(
1316
('nosuchrevision', revid), repository=repository)
1317
expected_error = errors.NoSuchRevision(repository, revid)
1318
self.assertEqual(expected_error, translated_error)
1320
def test_nobranch(self):
1321
bzrdir = self.make_bzrdir('')
1322
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1323
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1324
self.assertEqual(expected_error, translated_error)
1326
def test_LockContention(self):
1327
translated_error = self.translateTuple(('LockContention',))
1328
expected_error = errors.LockContention('(remote lock)')
1329
self.assertEqual(expected_error, translated_error)
1331
def test_UnlockableTransport(self):
1332
bzrdir = self.make_bzrdir('')
1333
translated_error = self.translateTuple(
1334
('UnlockableTransport',), bzrdir=bzrdir)
1335
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1336
self.assertEqual(expected_error, translated_error)
1338
def test_LockFailed(self):
1339
lock = 'str() of a server lock'
1340
why = 'str() of why'
1341
translated_error = self.translateTuple(('LockFailed', lock, why))
1342
expected_error = errors.LockFailed(lock, why)
1343
self.assertEqual(expected_error, translated_error)
1345
def test_TokenMismatch(self):
1346
token = 'a lock token'
1347
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1348
expected_error = errors.TokenMismatch(token, '(remote token)')
1349
self.assertEqual(expected_error, translated_error)
1351
def test_Diverged(self):
1352
branch = self.make_branch('a')
1353
other_branch = self.make_branch('b')
1354
translated_error = self.translateTuple(
1355
('Diverged',), branch=branch, other_branch=other_branch)
1356
expected_error = errors.DivergedBranches(branch, other_branch)
1357
self.assertEqual(expected_error, translated_error)
1360
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1361
"""Unit tests for bzrlib.remote._translate_error's robustness.
1363
TestErrorTranslationSuccess is for cases where _translate_error can
1364
translate successfully. This class about how _translate_err behaves when
1365
it fails to translate: it re-raises the original error.
1368
def test_unrecognised_server_error(self):
1369
"""If the error code from the server is not recognised, the original
1370
ErrorFromSmartServer is propagated unmodified.
1372
error_tuple = ('An unknown error tuple',)
1373
server_error = errors.ErrorFromSmartServer(error_tuple)
1374
translated_error = self.translateErrorFromSmartServer(server_error)
1375
self.assertEqual(server_error, translated_error)
1377
def test_context_missing_a_key(self):
1378
"""In case of a bug in the client, or perhaps an unexpected response
1379
from a server, _translate_error returns the original error tuple from
1380
the server and mutters a warning.
1382
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1383
# in the context dict. So let's give it an empty context dict instead
1384
# to exercise its error recovery.
1386
error_tuple = ('NoSuchRevision', 'revid')
1387
server_error = errors.ErrorFromSmartServer(error_tuple)
1388
translated_error = self.translateErrorFromSmartServer(server_error)
1389
self.assertEqual(server_error, translated_error)
1390
# In addition to re-raising ErrorFromSmartServer, some debug info has
1391
# been muttered to the log file for developer to look at.
1392
self.assertContainsRe(
1393
self._get_log(keep_log_file=True),
1394
"Missing key 'branch' in context")
1135
Not all possible errors will be caught at this stage, but obviously
1136
malformed data should be.
1138
record = ('bytes', [('name1',), ('name2',)])
1139
pack_stream = self.make_pack_stream([record])
1140
responses = [(('ok',), pack_stream), ]
1141
transport_path = 'quack'
1142
repo, client = self.setup_fake_client_and_repository(
1143
responses, transport_path)
1144
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1145
stream = repo.get_data_stream_for_search(search)
1146
self.assertRaises(errors.SmartProtocolError, list, stream)
1148
def test_backwards_compatibility(self):
1149
"""If the server doesn't recognise this request, fallback to VFS."""
1151
(('unknown verb', 'Repository.stream_revisions_chunked'), '')]
1152
repo, client = self.setup_fake_client_and_repository(
1154
self.mock_called = False
1155
repo._real_repository = MockRealRepository(self)
1156
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1157
repo.get_data_stream_for_search(search)
1158
self.assertTrue(self.mock_called)
1159
self.failIf(client.expecting_body,
1160
"The protocol has been left in an unclean state that will cause "
1161
"TooManyConcurrentRequests errors.")
1164
class MockRealRepository(object):
1165
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
1167
def __init__(self, test):
1170
def get_data_stream_for_search(self, search):
1171
self.test.assertEqual(set(['revid']), search.get_keys())
1172
self.test.mock_called = True