1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
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.
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.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests for remote bzrdir/branch/repo/etc
19
These are proxy objects which act on remote objects by sending messages
20
through a smart client. The proxies are to be created when attempting to open
21
the object given a transport that supports smartserver rpc operations.
23
These tests correspond to tests.test_smart, which exercises the server side.
27
from cStringIO import StringIO
37
from bzrlib.branch import Branch
38
from bzrlib.bzrdir import BzrDir, BzrDirFormat
39
from bzrlib.remote import (
45
from bzrlib.revision import NULL_REVISION
46
from bzrlib.smart import server, medium
47
from bzrlib.smart.client import _SmartClient
48
from bzrlib.symbol_versioning import one_four
49
from bzrlib.transport import get_transport, http
50
from bzrlib.transport.memory import MemoryTransport
51
from bzrlib.transport.remote import RemoteTransport, RemoteTCPTransport
54
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
57
self.transport_server = server.SmartTCPServer_for_testing
58
super(BasicRemoteObjectTests, self).setUp()
59
self.transport = self.get_transport()
60
# make a branch that can be opened over the smart transport
61
self.local_wt = BzrDir.create_standalone_workingtree('.')
64
self.transport.disconnect()
65
tests.TestCaseWithTransport.tearDown(self)
67
def test_create_remote_bzrdir(self):
68
b = remote.RemoteBzrDir(self.transport)
69
self.assertIsInstance(b, BzrDir)
71
def test_open_remote_branch(self):
72
# open a standalone branch in the working directory
73
b = remote.RemoteBzrDir(self.transport)
74
branch = b.open_branch()
75
self.assertIsInstance(branch, Branch)
77
def test_remote_repository(self):
78
b = BzrDir.open_from_transport(self.transport)
79
repo = b.open_repository()
80
revid = u'\xc823123123'.encode('utf8')
81
self.assertFalse(repo.has_revision(revid))
82
self.local_wt.commit(message='test commit', rev_id=revid)
83
self.assertTrue(repo.has_revision(revid))
85
def test_remote_branch_revision_history(self):
86
b = BzrDir.open_from_transport(self.transport).open_branch()
87
self.assertEqual([], b.revision_history())
88
r1 = self.local_wt.commit('1st commit')
89
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
90
self.assertEqual([r1, r2], b.revision_history())
92
def test_find_correct_format(self):
93
"""Should open a RemoteBzrDir over a RemoteTransport"""
94
fmt = BzrDirFormat.find_format(self.transport)
95
self.assertTrue(RemoteBzrDirFormat
96
in BzrDirFormat._control_server_formats)
97
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
99
def test_open_detected_smart_format(self):
100
fmt = BzrDirFormat.find_format(self.transport)
101
d = fmt.open(self.transport)
102
self.assertIsInstance(d, BzrDir)
104
def test_remote_branch_repr(self):
105
b = BzrDir.open_from_transport(self.transport).open_branch()
106
self.assertStartsWith(str(b), 'RemoteBranch(')
109
class FakeProtocol(object):
110
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
112
def __init__(self, body, fake_client):
114
self._body_buffer = None
115
self._fake_client = fake_client
117
def read_body_bytes(self, count=-1):
118
if self._body_buffer is None:
119
self._body_buffer = StringIO(self.body)
120
bytes = self._body_buffer.read(count)
121
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
122
self._fake_client.expecting_body = False
125
def cancel_read_body(self):
126
self._fake_client.expecting_body = False
128
def read_streamed_body(self):
132
class FakeClient(_SmartClient):
133
"""Lookalike for _SmartClient allowing testing."""
135
def __init__(self, fake_medium_base='fake base'):
136
"""Create a FakeClient.
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
146
self.expecting_body = False
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
169
def call(self, method, *args):
170
self._calls.append(('call', method, args))
171
return self._get_next_response()[1]
173
def call_expecting_body(self, method, *args):
174
self._calls.append(('call_expecting_body', method, args))
175
result = self._get_next_response()
176
self.expecting_body = True
177
return result[1], FakeProtocol(result[2], self)
179
def call_with_body_bytes_expecting_body(self, method, args, body):
180
self._calls.append(('call_with_body_bytes_expecting_body', method,
182
result = self._get_next_response()
183
self.expecting_body = 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)
191
self._client_calls = client_calls
193
def disconnect(self):
194
self._client_calls.append(('disconnect medium',))
197
class TestVfsHas(tests.TestCase):
199
def test_unicode_path(self):
200
client = FakeClient('/')
201
client.add_success_response('yes',)
202
transport = RemoteTransport('bzr://localhost/', _client=client)
203
filename = u'/hell\u00d8'.encode('utf8')
204
result = transport.has(filename)
206
[('call', 'has', (filename,))],
208
self.assertTrue(result)
211
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
212
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
214
def assertRemotePath(self, expected, client_base, 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.
219
client_medium = medium.SmartClientMedium(client_base)
220
transport = get_transport(transport_base)
221
result = client_medium.remote_path_from_transport(transport)
222
self.assertEqual(expected, result)
224
def test_remote_path_from_transport(self):
225
"""SmartClientMedium.remote_path_from_transport calculates a URL for
226
the given transport relative to the root of the client base URL.
228
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
229
self.assertRemotePath(
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)
244
def test_remote_path_from_transport_http(self):
245
"""Remote paths for HTTP transports are calculated differently to other
246
transports. They are just relative to the client base, not the root
247
directory of the host.
249
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
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))
284
class TestBzrDirOpenBranch(tests.TestCase):
286
def test_branch_present(self):
287
transport = MemoryTransport()
288
transport.mkdir('quack')
289
transport = transport.clone('quack')
290
client = FakeClient(transport.base)
291
client.add_success_response('ok', '')
292
client.add_success_response('ok', '', 'no', 'no', 'no')
293
bzrdir = RemoteBzrDir(transport, _client=client)
294
result = bzrdir.open_branch()
296
[('call', 'BzrDir.open_branch', ('quack/',)),
297
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
299
self.assertIsInstance(result, RemoteBranch)
300
self.assertEqual(bzrdir, result.bzrdir)
302
def test_branch_missing(self):
303
transport = MemoryTransport()
304
transport.mkdir('quack')
305
transport = transport.clone('quack')
306
client = FakeClient(transport.base)
307
client.add_error_response('nobranch')
308
bzrdir = RemoteBzrDir(transport, _client=client)
309
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
311
[('call', 'BzrDir.open_branch', ('quack/',))],
314
def test__get_tree_branch(self):
315
# _get_tree_branch is a form of open_branch, but it should only ask for
316
# branch opening, not any other network requests.
319
calls.append("Called")
321
transport = MemoryTransport()
322
# no requests on the network - catches other api calls being made.
323
client = FakeClient(transport.base)
324
bzrdir = RemoteBzrDir(transport, _client=client)
325
# patch the open_branch call to record that it was called.
326
bzrdir.open_branch = open_branch
327
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
328
self.assertEqual(["Called"], calls)
329
self.assertEqual([], client._calls)
331
def test_url_quoting_of_path(self):
332
# Relpaths on the wire should not be URL-escaped. So "~" should be
333
# transmitted as "~", not "%7E".
334
transport = RemoteTCPTransport('bzr://localhost/~hello/')
335
client = FakeClient(transport.base)
336
client.add_success_response('ok', '')
337
client.add_success_response('ok', '', 'no', 'no', 'no')
338
bzrdir = RemoteBzrDir(transport, _client=client)
339
result = bzrdir.open_branch()
341
[('call', 'BzrDir.open_branch', ('~hello/',)),
342
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
345
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
346
transport = MemoryTransport()
347
transport.mkdir('quack')
348
transport = transport.clone('quack')
350
rich_response = 'yes'
354
subtree_response = 'yes'
356
subtree_response = 'no'
357
client = FakeClient(transport.base)
358
client.add_success_response(
359
'ok', '', rich_response, subtree_response, external_lookup)
360
bzrdir = RemoteBzrDir(transport, _client=client)
361
result = bzrdir.open_repository()
363
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
365
self.assertIsInstance(result, RemoteRepository)
366
self.assertEqual(bzrdir, result.bzrdir)
367
self.assertEqual(rich_root, result._format.rich_root_data)
368
self.assertEqual(subtrees, result._format.supports_tree_reference)
370
def test_open_repository_sets_format_attributes(self):
371
self.check_open_repository(True, True)
372
self.check_open_repository(False, True)
373
self.check_open_repository(True, False)
374
self.check_open_repository(False, False)
375
self.check_open_repository(False, False, 'yes')
377
def test_old_server(self):
378
"""RemoteBzrDirFormat should fail to probe if the server version is too
381
self.assertRaises(errors.NotBranchError,
382
RemoteBzrDirFormat.probe_transport, OldServerTransport())
385
class TestBzrDirOpenRepository(tests.TestCase):
387
def test_backwards_compat_1_2(self):
388
transport = MemoryTransport()
389
transport.mkdir('quack')
390
transport = transport.clone('quack')
391
client = FakeClient(transport.base)
392
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
393
client.add_success_response('ok', '', 'no', 'no')
394
bzrdir = RemoteBzrDir(transport, _client=client)
395
repo = bzrdir.open_repository()
397
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
398
('call', 'BzrDir.find_repository', ('quack/',))],
402
class OldSmartClient(object):
403
"""A fake smart client for test_old_version that just returns a version one
404
response to the 'hello' (query version) command.
407
def get_request(self):
408
input_file = StringIO('ok\x011\n')
409
output_file = StringIO()
410
client_medium = medium.SmartSimplePipesClientMedium(
411
input_file, output_file)
412
return medium.SmartClientStreamMediumRequest(client_medium)
414
def protocol_version(self):
418
class OldServerTransport(object):
419
"""A fake transport for test_old_server that reports it's smart server
420
protocol version as version one.
426
def get_smart_client(self):
427
return OldSmartClient()
430
class RemoteBranchTestCase(tests.TestCase):
432
def make_remote_branch(self, transport, client):
433
"""Make a RemoteBranch using 'client' as its _SmartClient.
435
A RemoteBzrDir and RemoteRepository will also be created to fill out
436
the RemoteBranch, albeit with stub values for some of their attributes.
438
# we do not want bzrdir to make any remote calls, so use False as its
439
# _client. If it tries to make a remote call, this will fail
441
bzrdir = RemoteBzrDir(transport, _client=False)
442
repo = RemoteRepository(bzrdir, None, _client=client)
443
return RemoteBranch(bzrdir, repo, _client=client)
446
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
448
def test_empty_branch(self):
449
# in an empty branch we decode the response properly
450
transport = MemoryTransport()
451
client = FakeClient(transport.base)
452
client.add_success_response('ok', '0', 'null:')
453
transport.mkdir('quack')
454
transport = transport.clone('quack')
455
branch = self.make_remote_branch(transport, client)
456
result = branch.last_revision_info()
459
[('call', 'Branch.last_revision_info', ('quack/',))],
461
self.assertEqual((0, NULL_REVISION), result)
463
def test_non_empty_branch(self):
464
# in a non-empty branch we also decode the response properly
465
revid = u'\xc8'.encode('utf8')
466
transport = MemoryTransport()
467
client = FakeClient(transport.base)
468
client.add_success_response('ok', '2', revid)
469
transport.mkdir('kwaak')
470
transport = transport.clone('kwaak')
471
branch = self.make_remote_branch(transport, client)
472
result = branch.last_revision_info()
475
[('call', 'Branch.last_revision_info', ('kwaak/',))],
477
self.assertEqual((2, revid), result)
480
class TestBranchSetLastRevision(RemoteBranchTestCase):
482
def test_set_empty(self):
483
# set_revision_history([]) is translated to calling
484
# Branch.set_last_revision(path, '') on the wire.
485
transport = MemoryTransport()
486
transport.mkdir('branch')
487
transport = transport.clone('branch')
489
client = FakeClient(transport.base)
491
client.add_success_response('ok', 'branch token', 'repo token')
493
client.add_success_response('ok')
495
client.add_success_response('ok')
496
branch = self.make_remote_branch(transport, client)
499
result = branch.set_revision_history([])
501
[('call', 'Branch.set_last_revision',
502
('branch/', 'branch token', 'repo token', 'null:'))],
505
self.assertEqual(None, result)
507
def test_set_nonempty(self):
508
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
509
# Branch.set_last_revision(path, rev-idN) on the wire.
510
transport = MemoryTransport()
511
transport.mkdir('branch')
512
transport = transport.clone('branch')
514
client = FakeClient(transport.base)
516
client.add_success_response('ok', 'branch token', 'repo token')
518
client.add_success_response('ok')
520
client.add_success_response('ok')
521
branch = self.make_remote_branch(transport, client)
522
# Lock the branch, reset the record of remote calls.
526
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
528
[('call', 'Branch.set_last_revision',
529
('branch/', 'branch token', 'repo token', 'rev-id2'))],
532
self.assertEqual(None, result)
534
def test_no_such_revision(self):
535
transport = MemoryTransport()
536
transport.mkdir('branch')
537
transport = transport.clone('branch')
538
# A response of 'NoSuchRevision' is translated into an exception.
539
client = FakeClient(transport.base)
541
client.add_success_response('ok', 'branch token', 'repo token')
543
client.add_error_response('NoSuchRevision', 'rev-id')
545
client.add_success_response('ok')
547
branch = self.make_remote_branch(transport, client)
552
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
555
def test_tip_change_rejected(self):
556
"""TipChangeRejected responses cause a TipChangeRejected exception to
559
transport = MemoryTransport()
560
transport.mkdir('branch')
561
transport = transport.clone('branch')
562
client = FakeClient(transport.base)
564
client.add_success_response('ok', 'branch token', 'repo token')
566
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
567
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
568
client.add_error_response('TipChangeRejected', rejection_msg_utf8)
570
client.add_success_response('ok')
572
branch = self.make_remote_branch(transport, client)
574
self.addCleanup(branch.unlock)
577
# The 'TipChangeRejected' error response triggered by calling
578
# set_revision_history causes a TipChangeRejected exception.
579
err = self.assertRaises(
580
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
581
# The UTF-8 message from the response has been decoded into a unicode
583
self.assertIsInstance(err.msg, unicode)
584
self.assertEqual(rejection_msg_unicode, err.msg)
587
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
589
def test_set_last_revision_info(self):
590
# set_last_revision_info(num, 'rev-id') is translated to calling
591
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
592
transport = MemoryTransport()
593
transport.mkdir('branch')
594
transport = transport.clone('branch')
595
client = FakeClient(transport.base)
597
client.add_success_response('ok', 'branch token', 'repo token')
599
client.add_success_response('ok')
601
client.add_success_response('ok')
603
branch = self.make_remote_branch(transport, client)
604
# Lock the branch, reset the record of remote calls.
607
result = branch.set_last_revision_info(1234, 'a-revision-id')
609
[('call', 'Branch.set_last_revision_info',
610
('branch/', 'branch token', 'repo token',
611
'1234', 'a-revision-id'))],
613
self.assertEqual(None, result)
615
def test_no_such_revision(self):
616
# A response of 'NoSuchRevision' is translated into an exception.
617
transport = MemoryTransport()
618
transport.mkdir('branch')
619
transport = transport.clone('branch')
620
client = FakeClient(transport.base)
622
client.add_success_response('ok', 'branch token', 'repo token')
624
client.add_error_response('NoSuchRevision', 'revid')
626
client.add_success_response('ok')
628
branch = self.make_remote_branch(transport, client)
629
# Lock the branch, reset the record of remote calls.
634
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
637
def lock_remote_branch(self, branch):
638
"""Trick a RemoteBranch into thinking it is locked."""
639
branch._lock_mode = 'w'
640
branch._lock_count = 2
641
branch._lock_token = 'branch token'
642
branch._repo_lock_token = 'repo token'
643
branch.repository._lock_mode = 'w'
644
branch.repository._lock_count = 2
645
branch.repository._lock_token = 'repo token'
647
def test_backwards_compatibility(self):
648
"""If the server does not support the Branch.set_last_revision_info
649
verb (which is new in 1.4), then the client falls back to VFS methods.
651
# This test is a little messy. Unlike most tests in this file, it
652
# doesn't purely test what a Remote* object sends over the wire, and
653
# how it reacts to responses from the wire. It instead relies partly
654
# on asserting that the RemoteBranch will call
655
# self._real_branch.set_last_revision_info(...).
657
# First, set up our RemoteBranch with a FakeClient that raises
658
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
659
transport = MemoryTransport()
660
transport.mkdir('branch')
661
transport = transport.clone('branch')
662
client = FakeClient(transport.base)
663
client.add_unknown_method_response('Branch.set_last_revision_info')
664
branch = self.make_remote_branch(transport, client)
665
class StubRealBranch(object):
668
def set_last_revision_info(self, revno, revision_id):
670
('set_last_revision_info', revno, revision_id))
671
def _clear_cached_state(self):
673
real_branch = StubRealBranch()
674
branch._real_branch = real_branch
675
self.lock_remote_branch(branch)
677
# Call set_last_revision_info, and verify it behaved as expected.
678
result = branch.set_last_revision_info(1234, 'a-revision-id')
680
[('call', 'Branch.set_last_revision_info',
681
('branch/', 'branch token', 'repo token',
682
'1234', 'a-revision-id')),],
685
[('set_last_revision_info', 1234, 'a-revision-id')],
688
def test_unexpected_error(self):
689
# A response of 'NoSuchRevision' is translated into an exception.
690
transport = MemoryTransport()
691
transport.mkdir('branch')
692
transport = transport.clone('branch')
693
client = FakeClient(transport.base)
695
client.add_success_response('ok', 'branch token', 'repo token')
697
client.add_error_response('UnexpectedError')
699
client.add_success_response('ok')
701
branch = self.make_remote_branch(transport, client)
702
# Lock the branch, reset the record of remote calls.
706
err = self.assertRaises(
707
errors.UnknownErrorFromSmartServer,
708
branch.set_last_revision_info, 123, 'revid')
709
self.assertEqual(('UnexpectedError',), err.error_tuple)
712
def test_tip_change_rejected(self):
713
"""TipChangeRejected responses cause a TipChangeRejected exception to
716
transport = MemoryTransport()
717
transport.mkdir('branch')
718
transport = transport.clone('branch')
719
client = FakeClient(transport.base)
721
client.add_success_response('ok', 'branch token', 'repo token')
723
client.add_error_response('TipChangeRejected', 'rejection message')
725
client.add_success_response('ok')
727
branch = self.make_remote_branch(transport, client)
728
# Lock the branch, reset the record of remote calls.
730
self.addCleanup(branch.unlock)
733
# The 'TipChangeRejected' error response triggered by calling
734
# set_last_revision_info causes a TipChangeRejected exception.
735
err = self.assertRaises(
736
errors.TipChangeRejected,
737
branch.set_last_revision_info, 123, 'revid')
738
self.assertEqual('rejection message', err.msg)
741
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
742
"""Getting the branch configuration should use an abstract method not vfs.
745
def test_get_branch_conf(self):
746
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
747
## # We should see that branch.get_config() does a single rpc to get the
748
## # remote configuration file, abstracting away where that is stored on
749
## # the server. However at the moment it always falls back to using the
750
## # vfs, and this would need some changes in config.py.
752
## # in an empty branch we decode the response properly
753
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
754
## # we need to make a real branch because the remote_branch.control_files
755
## # will trigger _ensure_real.
756
## branch = self.make_branch('quack')
757
## transport = branch.bzrdir.root_transport
758
## # we do not want bzrdir to make any remote calls
759
## bzrdir = RemoteBzrDir(transport, _client=False)
760
## branch = RemoteBranch(bzrdir, None, _client=client)
761
## config = branch.get_config()
763
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
767
class TestBranchLockWrite(RemoteBranchTestCase):
769
def test_lock_write_unlockable(self):
770
transport = MemoryTransport()
771
client = FakeClient(transport.base)
772
client.add_error_response('UnlockableTransport')
773
transport.mkdir('quack')
774
transport = transport.clone('quack')
775
branch = self.make_remote_branch(transport, client)
776
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
778
[('call', 'Branch.lock_write', ('quack/', '', ''))],
782
class TestTransportIsReadonly(tests.TestCase):
785
client = FakeClient()
786
client.add_success_response('yes')
787
transport = RemoteTransport('bzr://example.com/', medium=False,
789
self.assertEqual(True, transport.is_readonly())
791
[('call', 'Transport.is_readonly', ())],
794
def test_false(self):
795
client = FakeClient()
796
client.add_success_response('no')
797
transport = RemoteTransport('bzr://example.com/', medium=False,
799
self.assertEqual(False, transport.is_readonly())
801
[('call', 'Transport.is_readonly', ())],
804
def test_error_from_old_server(self):
805
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
807
Clients should treat it as a "no" response, because is_readonly is only
808
advisory anyway (a transport could be read-write, but then the
809
underlying filesystem could be readonly anyway).
811
client = FakeClient()
812
client.add_unknown_method_response('Transport.is_readonly')
813
transport = RemoteTransport('bzr://example.com/', medium=False,
815
self.assertEqual(False, transport.is_readonly())
817
[('call', 'Transport.is_readonly', ())],
821
class TestRemoteRepository(tests.TestCase):
822
"""Base for testing RemoteRepository protocol usage.
824
These tests contain frozen requests and responses. We want any changes to
825
what is sent or expected to be require a thoughtful update to these tests
826
because they might break compatibility with different-versioned servers.
829
def setup_fake_client_and_repository(self, transport_path):
830
"""Create the fake client and repository for testing with.
832
There's no real server here; we just have canned responses sent
835
:param transport_path: Path below the root of the MemoryTransport
836
where the repository will be created.
838
transport = MemoryTransport()
839
transport.mkdir(transport_path)
840
client = FakeClient(transport.base)
841
transport = transport.clone(transport_path)
842
# we do not want bzrdir to make any remote calls
843
bzrdir = RemoteBzrDir(transport, _client=False)
844
repo = RemoteRepository(bzrdir, None, _client=client)
848
class TestRepositoryGatherStats(TestRemoteRepository):
850
def test_revid_none(self):
851
# ('ok',), body with revisions and size
852
transport_path = 'quack'
853
repo, client = self.setup_fake_client_and_repository(transport_path)
854
client.add_success_response_with_body(
855
'revisions: 2\nsize: 18\n', 'ok')
856
result = repo.gather_stats(None)
858
[('call_expecting_body', 'Repository.gather_stats',
859
('quack/','','no'))],
861
self.assertEqual({'revisions': 2, 'size': 18}, result)
863
def test_revid_no_committers(self):
864
# ('ok',), body without committers
865
body = ('firstrev: 123456.300 3600\n'
866
'latestrev: 654231.400 0\n'
869
transport_path = 'quick'
870
revid = u'\xc8'.encode('utf8')
871
repo, client = self.setup_fake_client_and_repository(transport_path)
872
client.add_success_response_with_body(body, 'ok')
873
result = repo.gather_stats(revid)
875
[('call_expecting_body', 'Repository.gather_stats',
876
('quick/', revid, 'no'))],
878
self.assertEqual({'revisions': 2, 'size': 18,
879
'firstrev': (123456.300, 3600),
880
'latestrev': (654231.400, 0),},
883
def test_revid_with_committers(self):
884
# ('ok',), body with committers
885
body = ('committers: 128\n'
886
'firstrev: 123456.300 3600\n'
887
'latestrev: 654231.400 0\n'
890
transport_path = 'buick'
891
revid = u'\xc8'.encode('utf8')
892
repo, client = self.setup_fake_client_and_repository(transport_path)
893
client.add_success_response_with_body(body, 'ok')
894
result = repo.gather_stats(revid, True)
896
[('call_expecting_body', 'Repository.gather_stats',
897
('buick/', revid, 'yes'))],
899
self.assertEqual({'revisions': 2, 'size': 18,
901
'firstrev': (123456.300, 3600),
902
'latestrev': (654231.400, 0),},
906
class TestRepositoryGetGraph(TestRemoteRepository):
908
def test_get_graph(self):
909
# get_graph returns a graph with the repository as the
911
transport_path = 'quack'
912
repo, client = self.setup_fake_client_and_repository(transport_path)
913
graph = repo.get_graph()
914
self.assertEqual(graph._parents_provider, repo)
917
class TestRepositoryGetParentMap(TestRemoteRepository):
919
def test_get_parent_map_caching(self):
920
# get_parent_map returns from cache until unlock()
921
# setup a reponse with two revisions
922
r1 = u'\u0e33'.encode('utf8')
923
r2 = u'\u0dab'.encode('utf8')
924
lines = [' '.join([r2, r1]), r1]
925
encoded_body = bz2.compress('\n'.join(lines))
927
transport_path = 'quack'
928
repo, client = self.setup_fake_client_and_repository(transport_path)
929
client.add_success_response_with_body(encoded_body, 'ok')
930
client.add_success_response_with_body(encoded_body, 'ok')
932
graph = repo.get_graph()
933
parents = graph.get_parent_map([r2])
934
self.assertEqual({r2: (r1,)}, parents)
935
# locking and unlocking deeper should not reset
938
parents = graph.get_parent_map([r1])
939
self.assertEqual({r1: (NULL_REVISION,)}, parents)
941
[('call_with_body_bytes_expecting_body',
942
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
945
# now we call again, and it should use the second response.
947
graph = repo.get_graph()
948
parents = graph.get_parent_map([r1])
949
self.assertEqual({r1: (NULL_REVISION,)}, parents)
951
[('call_with_body_bytes_expecting_body',
952
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
953
('call_with_body_bytes_expecting_body',
954
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
959
def test_get_parent_map_reconnects_if_unknown_method(self):
960
transport_path = 'quack'
961
repo, client = self.setup_fake_client_and_repository(transport_path)
962
client.add_unknown_method_response('Repository,get_parent_map')
963
client.add_success_response_with_body('', 'ok')
964
self.assertFalse(client._medium._is_remote_before((1, 2)))
965
rev_id = 'revision-id'
966
expected_deprecations = [
967
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
969
parents = self.callDeprecated(
970
expected_deprecations, repo.get_parent_map, [rev_id])
972
[('call_with_body_bytes_expecting_body',
973
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
974
('disconnect medium',),
975
('call_expecting_body', 'Repository.get_revision_graph',
978
# The medium is now marked as being connected to an older server
979
self.assertTrue(client._medium._is_remote_before((1, 2)))
981
def test_get_parent_map_fallback_parentless_node(self):
982
"""get_parent_map falls back to get_revision_graph on old servers. The
983
results from get_revision_graph are tweaked to match the get_parent_map
986
Specifically, a {key: ()} result from get_revision_graph means "no
987
parents" for that key, which in get_parent_map results should be
988
represented as {key: ('null:',)}.
990
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
992
rev_id = 'revision-id'
993
transport_path = 'quack'
994
repo, client = self.setup_fake_client_and_repository(transport_path)
995
client.add_success_response_with_body(rev_id, 'ok')
996
client._medium._remember_remote_is_before((1, 2))
997
expected_deprecations = [
998
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1000
parents = self.callDeprecated(
1001
expected_deprecations, repo.get_parent_map, [rev_id])
1003
[('call_expecting_body', 'Repository.get_revision_graph',
1006
self.assertEqual({rev_id: ('null:',)}, parents)
1008
def test_get_parent_map_unexpected_response(self):
1009
repo, client = self.setup_fake_client_and_repository('path')
1010
client.add_success_response('something unexpected!')
1012
errors.UnexpectedSmartServerResponse,
1013
repo.get_parent_map, ['a-revision-id'])
1016
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1018
def test_null_revision(self):
1019
# a null revision has the predictable result {}, we should have no wire
1020
# traffic when calling it with this argument
1021
transport_path = 'empty'
1022
repo, client = self.setup_fake_client_and_repository(transport_path)
1023
client.add_success_response('notused')
1024
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1026
self.assertEqual([], client._calls)
1027
self.assertEqual({}, result)
1029
def test_none_revision(self):
1030
# with none we want the entire graph
1031
r1 = u'\u0e33'.encode('utf8')
1032
r2 = u'\u0dab'.encode('utf8')
1033
lines = [' '.join([r2, r1]), r1]
1034
encoded_body = '\n'.join(lines)
1036
transport_path = 'sinhala'
1037
repo, client = self.setup_fake_client_and_repository(transport_path)
1038
client.add_success_response_with_body(encoded_body, 'ok')
1039
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1041
[('call_expecting_body', 'Repository.get_revision_graph',
1044
self.assertEqual({r1: (), r2: (r1, )}, result)
1046
def test_specific_revision(self):
1047
# with a specific revision we want the graph for that
1048
# with none we want the entire graph
1049
r11 = u'\u0e33'.encode('utf8')
1050
r12 = u'\xc9'.encode('utf8')
1051
r2 = u'\u0dab'.encode('utf8')
1052
lines = [' '.join([r2, r11, r12]), r11, r12]
1053
encoded_body = '\n'.join(lines)
1055
transport_path = 'sinhala'
1056
repo, client = self.setup_fake_client_and_repository(transport_path)
1057
client.add_success_response_with_body(encoded_body, 'ok')
1058
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1060
[('call_expecting_body', 'Repository.get_revision_graph',
1063
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1065
def test_no_such_revision(self):
1067
transport_path = 'sinhala'
1068
repo, client = self.setup_fake_client_and_repository(transport_path)
1069
client.add_error_response('nosuchrevision', revid)
1070
# also check that the right revision is reported in the error
1071
self.assertRaises(errors.NoSuchRevision,
1072
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1074
[('call_expecting_body', 'Repository.get_revision_graph',
1075
('sinhala/', revid))],
1078
def test_unexpected_error(self):
1080
transport_path = 'sinhala'
1081
repo, client = self.setup_fake_client_and_repository(transport_path)
1082
client.add_error_response('AnUnexpectedError')
1083
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1084
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1085
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1088
class TestRepositoryIsShared(TestRemoteRepository):
1090
def test_is_shared(self):
1091
# ('yes', ) for Repository.is_shared -> 'True'.
1092
transport_path = 'quack'
1093
repo, client = self.setup_fake_client_and_repository(transport_path)
1094
client.add_success_response('yes')
1095
result = repo.is_shared()
1097
[('call', 'Repository.is_shared', ('quack/',))],
1099
self.assertEqual(True, result)
1101
def test_is_not_shared(self):
1102
# ('no', ) for Repository.is_shared -> 'False'.
1103
transport_path = 'qwack'
1104
repo, client = self.setup_fake_client_and_repository(transport_path)
1105
client.add_success_response('no')
1106
result = repo.is_shared()
1108
[('call', 'Repository.is_shared', ('qwack/',))],
1110
self.assertEqual(False, result)
1113
class TestRepositoryLockWrite(TestRemoteRepository):
1115
def test_lock_write(self):
1116
transport_path = 'quack'
1117
repo, client = self.setup_fake_client_and_repository(transport_path)
1118
client.add_success_response('ok', 'a token')
1119
result = repo.lock_write()
1121
[('call', 'Repository.lock_write', ('quack/', ''))],
1123
self.assertEqual('a token', result)
1125
def test_lock_write_already_locked(self):
1126
transport_path = 'quack'
1127
repo, client = self.setup_fake_client_and_repository(transport_path)
1128
client.add_error_response('LockContention')
1129
self.assertRaises(errors.LockContention, repo.lock_write)
1131
[('call', 'Repository.lock_write', ('quack/', ''))],
1134
def test_lock_write_unlockable(self):
1135
transport_path = 'quack'
1136
repo, client = self.setup_fake_client_and_repository(transport_path)
1137
client.add_error_response('UnlockableTransport')
1138
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1140
[('call', 'Repository.lock_write', ('quack/', ''))],
1144
class TestRepositoryUnlock(TestRemoteRepository):
1146
def test_unlock(self):
1147
transport_path = 'quack'
1148
repo, client = self.setup_fake_client_and_repository(transport_path)
1149
client.add_success_response('ok', 'a token')
1150
client.add_success_response('ok')
1154
[('call', 'Repository.lock_write', ('quack/', '')),
1155
('call', 'Repository.unlock', ('quack/', 'a token'))],
1158
def test_unlock_wrong_token(self):
1159
# If somehow the token is wrong, unlock will raise TokenMismatch.
1160
transport_path = 'quack'
1161
repo, client = self.setup_fake_client_and_repository(transport_path)
1162
client.add_success_response('ok', 'a token')
1163
client.add_error_response('TokenMismatch')
1165
self.assertRaises(errors.TokenMismatch, repo.unlock)
1168
class TestRepositoryHasRevision(TestRemoteRepository):
1170
def test_none(self):
1171
# repo.has_revision(None) should not cause any traffic.
1172
transport_path = 'quack'
1173
repo, client = self.setup_fake_client_and_repository(transport_path)
1175
# The null revision is always there, so has_revision(None) == True.
1176
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1178
# The remote repo shouldn't be accessed.
1179
self.assertEqual([], client._calls)
1182
class TestRepositoryTarball(TestRemoteRepository):
1184
# This is a canned tarball reponse we can validate against
1186
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1187
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1188
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1189
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1190
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1191
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1192
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1193
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1194
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1195
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1196
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1197
'nWQ7QH/F3JFOFCQ0aSPfA='
1200
def test_repository_tarball(self):
1201
# Test that Repository.tarball generates the right operations
1202
transport_path = 'repo'
1203
expected_calls = [('call_expecting_body', 'Repository.tarball',
1204
('repo/', 'bz2',),),
1206
repo, client = self.setup_fake_client_and_repository(transport_path)
1207
client.add_success_response_with_body(self.tarball_content, 'ok')
1208
# Now actually ask for the tarball
1209
tarball_file = repo._get_tarball('bz2')
1211
self.assertEqual(expected_calls, client._calls)
1212
self.assertEqual(self.tarball_content, tarball_file.read())
1214
tarball_file.close()
1217
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1218
"""RemoteRepository.copy_content_into optimizations"""
1220
def test_copy_content_remote_to_local(self):
1221
self.transport_server = server.SmartTCPServer_for_testing
1222
src_repo = self.make_repository('repo1')
1223
src_repo = repository.Repository.open(self.get_url('repo1'))
1224
# At the moment the tarball-based copy_content_into can't write back
1225
# into a smart server. It would be good if it could upload the
1226
# tarball; once that works we'd have to create repositories of
1227
# different formats. -- mbp 20070410
1228
dest_url = self.get_vfs_only_url('repo2')
1229
dest_bzrdir = BzrDir.create(dest_url)
1230
dest_repo = dest_bzrdir.create_repository()
1231
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1232
self.assertTrue(isinstance(src_repo, RemoteRepository))
1233
src_repo.copy_content_into(dest_repo)
1236
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1237
"""Base class for unit tests for bzrlib.remote._translate_error."""
1239
def translateTuple(self, error_tuple, **context):
1240
"""Call _translate_error with an ErrorFromSmartServer built from the
1243
:param error_tuple: A tuple of a smart server response, as would be
1244
passed to an ErrorFromSmartServer.
1245
:kwargs context: context items to call _translate_error with.
1247
:returns: The error raised by _translate_error.
1249
# Raise the ErrorFromSmartServer before passing it as an argument,
1250
# because _translate_error may need to re-raise it with a bare 'raise'
1252
server_error = errors.ErrorFromSmartServer(error_tuple)
1253
translated_error = self.translateErrorFromSmartServer(
1254
server_error, **context)
1255
return translated_error
1257
def translateErrorFromSmartServer(self, error_object, **context):
1258
"""Like translateTuple, but takes an already constructed
1259
ErrorFromSmartServer rather than a tuple.
1263
except errors.ErrorFromSmartServer, server_error:
1264
translated_error = self.assertRaises(
1265
errors.BzrError, remote._translate_error, server_error,
1267
return translated_error
1270
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1271
"""Unit tests for bzrlib.remote._translate_error.
1273
Given an ErrorFromSmartServer (which has an error tuple from a smart
1274
server) and some context, _translate_error raises more specific errors from
1277
This test case covers the cases where _translate_error succeeds in
1278
translating an ErrorFromSmartServer to something better. See
1279
TestErrorTranslationRobustness for other cases.
1282
def test_NoSuchRevision(self):
1283
branch = self.make_branch('')
1285
translated_error = self.translateTuple(
1286
('NoSuchRevision', revid), branch=branch)
1287
expected_error = errors.NoSuchRevision(branch, revid)
1288
self.assertEqual(expected_error, translated_error)
1290
def test_nosuchrevision(self):
1291
repository = self.make_repository('')
1293
translated_error = self.translateTuple(
1294
('nosuchrevision', revid), repository=repository)
1295
expected_error = errors.NoSuchRevision(repository, revid)
1296
self.assertEqual(expected_error, translated_error)
1298
def test_nobranch(self):
1299
bzrdir = self.make_bzrdir('')
1300
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1301
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1302
self.assertEqual(expected_error, translated_error)
1304
def test_LockContention(self):
1305
translated_error = self.translateTuple(('LockContention',))
1306
expected_error = errors.LockContention('(remote lock)')
1307
self.assertEqual(expected_error, translated_error)
1309
def test_UnlockableTransport(self):
1310
bzrdir = self.make_bzrdir('')
1311
translated_error = self.translateTuple(
1312
('UnlockableTransport',), bzrdir=bzrdir)
1313
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1314
self.assertEqual(expected_error, translated_error)
1316
def test_LockFailed(self):
1317
lock = 'str() of a server lock'
1318
why = 'str() of why'
1319
translated_error = self.translateTuple(('LockFailed', lock, why))
1320
expected_error = errors.LockFailed(lock, why)
1321
self.assertEqual(expected_error, translated_error)
1323
def test_TokenMismatch(self):
1324
token = 'a lock token'
1325
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1326
expected_error = errors.TokenMismatch(token, '(remote token)')
1327
self.assertEqual(expected_error, translated_error)
1329
def test_Diverged(self):
1330
branch = self.make_branch('a')
1331
other_branch = self.make_branch('b')
1332
translated_error = self.translateTuple(
1333
('Diverged',), branch=branch, other_branch=other_branch)
1334
expected_error = errors.DivergedBranches(branch, other_branch)
1335
self.assertEqual(expected_error, translated_error)
1338
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1339
"""Unit tests for bzrlib.remote._translate_error's robustness.
1341
TestErrorTranslationSuccess is for cases where _translate_error can
1342
translate successfully. This class about how _translate_err behaves when
1343
it fails to translate: it re-raises the original error.
1346
def test_unrecognised_server_error(self):
1347
"""If the error code from the server is not recognised, the original
1348
ErrorFromSmartServer is propagated unmodified.
1350
error_tuple = ('An unknown error tuple',)
1351
server_error = errors.ErrorFromSmartServer(error_tuple)
1352
translated_error = self.translateErrorFromSmartServer(server_error)
1353
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1354
self.assertEqual(expected_error, translated_error)
1356
def test_context_missing_a_key(self):
1357
"""In case of a bug in the client, or perhaps an unexpected response
1358
from a server, _translate_error returns the original error tuple from
1359
the server and mutters a warning.
1361
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1362
# in the context dict. So let's give it an empty context dict instead
1363
# to exercise its error recovery.
1365
error_tuple = ('NoSuchRevision', 'revid')
1366
server_error = errors.ErrorFromSmartServer(error_tuple)
1367
translated_error = self.translateErrorFromSmartServer(server_error)
1368
self.assertEqual(server_error, translated_error)
1369
# In addition to re-raising ErrorFromSmartServer, some debug info has
1370
# been muttered to the log file for developer to look at.
1371
self.assertContainsRe(
1372
self._get_log(keep_log_file=True),
1373
"Missing key 'branch' in context")