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 TestBranchLastRevisionInfo(tests.TestCase):
432
def test_empty_branch(self):
433
# in an empty branch we decode the response properly
434
transport = MemoryTransport()
435
client = FakeClient(transport.base)
436
client.add_success_response('ok', '0', 'null:')
437
transport.mkdir('quack')
438
transport = transport.clone('quack')
439
# we do not want bzrdir to make any remote calls
440
bzrdir = RemoteBzrDir(transport, _client=False)
441
branch = RemoteBranch(bzrdir, None, _client=client)
442
result = branch.last_revision_info()
445
[('call', 'Branch.last_revision_info', ('quack/',))],
447
self.assertEqual((0, NULL_REVISION), result)
449
def test_non_empty_branch(self):
450
# in a non-empty branch we also decode the response properly
451
revid = u'\xc8'.encode('utf8')
452
transport = MemoryTransport()
453
client = FakeClient(transport.base)
454
client.add_success_response('ok', '2', revid)
455
transport.mkdir('kwaak')
456
transport = transport.clone('kwaak')
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.last_revision_info()
463
[('call', 'Branch.last_revision_info', ('kwaak/',))],
465
self.assertEqual((2, revid), result)
468
class TestBranchSetLastRevision(tests.TestCase):
470
def test_set_empty(self):
471
# set_revision_history([]) is translated to calling
472
# Branch.set_last_revision(path, '') on the wire.
473
transport = MemoryTransport()
474
transport.mkdir('branch')
475
transport = transport.clone('branch')
477
client = FakeClient(transport.base)
479
client.add_success_response('ok', 'branch token', 'repo token')
481
client.add_success_response('ok')
483
client.add_success_response('ok')
484
bzrdir = RemoteBzrDir(transport, _client=False)
485
branch = RemoteBranch(bzrdir, None, _client=client)
486
# This is a hack to work around the problem that RemoteBranch currently
487
# unnecessarily invokes _ensure_real upon a call to lock_write.
488
branch._ensure_real = lambda: None
491
result = branch.set_revision_history([])
493
[('call', 'Branch.set_last_revision',
494
('branch/', 'branch token', 'repo token', 'null:'))],
497
self.assertEqual(None, result)
499
def test_set_nonempty(self):
500
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
501
# Branch.set_last_revision(path, rev-idN) on the wire.
502
transport = MemoryTransport()
503
transport.mkdir('branch')
504
transport = transport.clone('branch')
506
client = FakeClient(transport.base)
508
client.add_success_response('ok', 'branch token', 'repo token')
510
client.add_success_response('ok')
512
client.add_success_response('ok')
513
bzrdir = RemoteBzrDir(transport, _client=False)
514
branch = RemoteBranch(bzrdir, None, _client=client)
515
# This is a hack to work around the problem that RemoteBranch currently
516
# unnecessarily invokes _ensure_real upon a call to lock_write.
517
branch._ensure_real = lambda: None
518
# Lock the branch, reset the record of remote calls.
522
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
524
[('call', 'Branch.set_last_revision',
525
('branch/', 'branch token', 'repo token', 'rev-id2'))],
528
self.assertEqual(None, result)
530
def test_no_such_revision(self):
531
transport = MemoryTransport()
532
transport.mkdir('branch')
533
transport = transport.clone('branch')
534
# A response of 'NoSuchRevision' is translated into an exception.
535
client = FakeClient(transport.base)
537
client.add_success_response('ok', 'branch token', 'repo token')
539
client.add_error_response('NoSuchRevision', 'rev-id')
541
client.add_success_response('ok')
543
bzrdir = RemoteBzrDir(transport, _client=False)
544
repo = RemoteRepository(bzrdir, None, _client=client)
545
branch = RemoteBranch(bzrdir, repo, _client=client)
546
branch._ensure_real = lambda: None
551
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
555
class TestBranchSetLastRevisionInfo(tests.TestCase):
557
def test_set_last_revision_info(self):
558
# set_last_revision_info(num, 'rev-id') is translated to calling
559
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
560
transport = MemoryTransport()
561
transport.mkdir('branch')
562
transport = transport.clone('branch')
563
client = FakeClient(transport.base)
565
client.add_success_response('ok', 'branch token', 'repo token')
567
client.add_success_response('ok')
569
client.add_success_response('ok')
571
bzrdir = RemoteBzrDir(transport, _client=False)
572
branch = RemoteBranch(bzrdir, None, _client=client)
573
# This is a hack to work around the problem that RemoteBranch currently
574
# unnecessarily invokes _ensure_real upon a call to lock_write.
575
branch._ensure_real = lambda: None
576
# Lock the branch, reset the record of remote calls.
579
result = branch.set_last_revision_info(1234, 'a-revision-id')
581
[('call', 'Branch.set_last_revision_info',
582
('branch/', 'branch token', 'repo token',
583
'1234', 'a-revision-id'))],
585
self.assertEqual(None, result)
587
def test_no_such_revision(self):
588
# A response of 'NoSuchRevision' is translated into an exception.
589
transport = MemoryTransport()
590
transport.mkdir('branch')
591
transport = transport.clone('branch')
592
client = FakeClient(transport.base)
594
client.add_success_response('ok', 'branch token', 'repo token')
596
client.add_error_response('NoSuchRevision', 'revid')
598
client.add_success_response('ok')
600
bzrdir = RemoteBzrDir(transport, _client=False)
601
repo = RemoteRepository(bzrdir, None, _client=client)
602
branch = RemoteBranch(bzrdir, repo, _client=client)
603
# This is a hack to work around the problem that RemoteBranch currently
604
# unnecessarily invokes _ensure_real upon a call to lock_write.
605
branch._ensure_real = lambda: None
606
# Lock the branch, reset the record of remote calls.
611
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
614
def lock_remote_branch(self, branch):
615
"""Trick a RemoteBranch into thinking it is locked."""
616
branch._lock_mode = 'w'
617
branch._lock_count = 2
618
branch._lock_token = 'branch token'
619
branch._repo_lock_token = 'repo token'
621
def test_backwards_compatibility(self):
622
"""If the server does not support the Branch.set_last_revision_info
623
verb (which is new in 1.4), then the client falls back to VFS methods.
625
# This test is a little messy. Unlike most tests in this file, it
626
# doesn't purely test what a Remote* object sends over the wire, and
627
# how it reacts to responses from the wire. It instead relies partly
628
# on asserting that the RemoteBranch will call
629
# self._real_branch.set_last_revision_info(...).
631
# First, set up our RemoteBranch with a FakeClient that raises
632
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
633
transport = MemoryTransport()
634
transport.mkdir('branch')
635
transport = transport.clone('branch')
636
client = FakeClient(transport.base)
637
client.add_unknown_method_response('Branch.set_last_revision_info')
638
bzrdir = RemoteBzrDir(transport, _client=False)
639
branch = RemoteBranch(bzrdir, None, _client=client)
640
class StubRealBranch(object):
643
def set_last_revision_info(self, revno, revision_id):
645
('set_last_revision_info', revno, revision_id))
646
def _clear_cached_state(self):
648
real_branch = StubRealBranch()
649
branch._real_branch = real_branch
650
self.lock_remote_branch(branch)
652
# Call set_last_revision_info, and verify it behaved as expected.
653
result = branch.set_last_revision_info(1234, 'a-revision-id')
655
[('call', 'Branch.set_last_revision_info',
656
('branch/', 'branch token', 'repo token',
657
'1234', 'a-revision-id')),],
660
[('set_last_revision_info', 1234, 'a-revision-id')],
663
def test_unexpected_error(self):
664
# A response of 'NoSuchRevision' is translated into an exception.
665
transport = MemoryTransport()
666
transport.mkdir('branch')
667
transport = transport.clone('branch')
668
client = FakeClient(transport.base)
670
client.add_success_response('ok', 'branch token', 'repo token')
672
client.add_error_response('UnexpectedError')
674
client.add_success_response('ok')
676
bzrdir = RemoteBzrDir(transport, _client=False)
677
repo = RemoteRepository(bzrdir, None, _client=client)
678
branch = RemoteBranch(bzrdir, repo, _client=client)
679
# This is a hack to work around the problem that RemoteBranch currently
680
# unnecessarily invokes _ensure_real upon a call to lock_write.
681
branch._ensure_real = lambda: None
682
# Lock the branch, reset the record of remote calls.
686
err = self.assertRaises(
687
errors.ErrorFromSmartServer,
688
branch.set_last_revision_info, 123, 'revid')
689
self.assertEqual(('UnexpectedError',), err.error_tuple)
693
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
694
"""Getting the branch configuration should use an abstract method not vfs.
697
def test_get_branch_conf(self):
698
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
699
## # We should see that branch.get_config() does a single rpc to get the
700
## # remote configuration file, abstracting away where that is stored on
701
## # the server. However at the moment it always falls back to using the
702
## # vfs, and this would need some changes in config.py.
704
## # in an empty branch we decode the response properly
705
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
706
## # we need to make a real branch because the remote_branch.control_files
707
## # will trigger _ensure_real.
708
## branch = self.make_branch('quack')
709
## transport = branch.bzrdir.root_transport
710
## # we do not want bzrdir to make any remote calls
711
## bzrdir = RemoteBzrDir(transport, _client=False)
712
## branch = RemoteBranch(bzrdir, None, _client=client)
713
## config = branch.get_config()
715
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
719
class TestBranchLockWrite(tests.TestCase):
721
def test_lock_write_unlockable(self):
722
transport = MemoryTransport()
723
client = FakeClient(transport.base)
724
client.add_error_response('UnlockableTransport')
725
transport.mkdir('quack')
726
transport = transport.clone('quack')
727
# we do not want bzrdir to make any remote calls
728
bzrdir = RemoteBzrDir(transport, _client=False)
729
repo = RemoteRepository(bzrdir, None, _client=client)
730
branch = RemoteBranch(bzrdir, repo, _client=client)
731
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
733
[('call', 'Branch.lock_write', ('quack/', '', ''))],
737
class TestTransportIsReadonly(tests.TestCase):
740
client = FakeClient()
741
client.add_success_response('yes')
742
transport = RemoteTransport('bzr://example.com/', medium=False,
744
self.assertEqual(True, transport.is_readonly())
746
[('call', 'Transport.is_readonly', ())],
749
def test_false(self):
750
client = FakeClient()
751
client.add_success_response('no')
752
transport = RemoteTransport('bzr://example.com/', medium=False,
754
self.assertEqual(False, transport.is_readonly())
756
[('call', 'Transport.is_readonly', ())],
759
def test_error_from_old_server(self):
760
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
762
Clients should treat it as a "no" response, because is_readonly is only
763
advisory anyway (a transport could be read-write, but then the
764
underlying filesystem could be readonly anyway).
766
client = FakeClient()
767
client.add_unknown_method_response('Transport.is_readonly')
768
transport = RemoteTransport('bzr://example.com/', medium=False,
770
self.assertEqual(False, transport.is_readonly())
772
[('call', 'Transport.is_readonly', ())],
776
class TestRemoteRepository(tests.TestCase):
777
"""Base for testing RemoteRepository protocol usage.
779
These tests contain frozen requests and responses. We want any changes to
780
what is sent or expected to be require a thoughtful update to these tests
781
because they might break compatibility with different-versioned servers.
784
def setup_fake_client_and_repository(self, transport_path):
785
"""Create the fake client and repository for testing with.
787
There's no real server here; we just have canned responses sent
790
:param transport_path: Path below the root of the MemoryTransport
791
where the repository will be created.
793
transport = MemoryTransport()
794
transport.mkdir(transport_path)
795
client = FakeClient(transport.base)
796
transport = transport.clone(transport_path)
797
# we do not want bzrdir to make any remote calls
798
bzrdir = RemoteBzrDir(transport, _client=False)
799
repo = RemoteRepository(bzrdir, None, _client=client)
803
class TestRepositoryGatherStats(TestRemoteRepository):
805
def test_revid_none(self):
806
# ('ok',), body with revisions and size
807
transport_path = 'quack'
808
repo, client = self.setup_fake_client_and_repository(transport_path)
809
client.add_success_response_with_body(
810
'revisions: 2\nsize: 18\n', 'ok')
811
result = repo.gather_stats(None)
813
[('call_expecting_body', 'Repository.gather_stats',
814
('quack/','','no'))],
816
self.assertEqual({'revisions': 2, 'size': 18}, result)
818
def test_revid_no_committers(self):
819
# ('ok',), body without committers
820
body = ('firstrev: 123456.300 3600\n'
821
'latestrev: 654231.400 0\n'
824
transport_path = 'quick'
825
revid = u'\xc8'.encode('utf8')
826
repo, client = self.setup_fake_client_and_repository(transport_path)
827
client.add_success_response_with_body(body, 'ok')
828
result = repo.gather_stats(revid)
830
[('call_expecting_body', 'Repository.gather_stats',
831
('quick/', revid, 'no'))],
833
self.assertEqual({'revisions': 2, 'size': 18,
834
'firstrev': (123456.300, 3600),
835
'latestrev': (654231.400, 0),},
838
def test_revid_with_committers(self):
839
# ('ok',), body with committers
840
body = ('committers: 128\n'
841
'firstrev: 123456.300 3600\n'
842
'latestrev: 654231.400 0\n'
845
transport_path = 'buick'
846
revid = u'\xc8'.encode('utf8')
847
repo, client = self.setup_fake_client_and_repository(transport_path)
848
client.add_success_response_with_body(body, 'ok')
849
result = repo.gather_stats(revid, True)
851
[('call_expecting_body', 'Repository.gather_stats',
852
('buick/', revid, 'yes'))],
854
self.assertEqual({'revisions': 2, 'size': 18,
856
'firstrev': (123456.300, 3600),
857
'latestrev': (654231.400, 0),},
861
class TestRepositoryGetGraph(TestRemoteRepository):
863
def test_get_graph(self):
864
# get_graph returns a graph with the repository as the
866
transport_path = 'quack'
867
repo, client = self.setup_fake_client_and_repository(transport_path)
868
graph = repo.get_graph()
869
self.assertEqual(graph._parents_provider, repo)
872
class TestRepositoryGetParentMap(TestRemoteRepository):
874
def test_get_parent_map_caching(self):
875
# get_parent_map returns from cache until unlock()
876
# setup a reponse with two revisions
877
r1 = u'\u0e33'.encode('utf8')
878
r2 = u'\u0dab'.encode('utf8')
879
lines = [' '.join([r2, r1]), r1]
880
encoded_body = bz2.compress('\n'.join(lines))
882
transport_path = 'quack'
883
repo, client = self.setup_fake_client_and_repository(transport_path)
884
client.add_success_response_with_body(encoded_body, 'ok')
885
client.add_success_response_with_body(encoded_body, 'ok')
887
graph = repo.get_graph()
888
parents = graph.get_parent_map([r2])
889
self.assertEqual({r2: (r1,)}, parents)
890
# locking and unlocking deeper should not reset
893
parents = graph.get_parent_map([r1])
894
self.assertEqual({r1: (NULL_REVISION,)}, parents)
896
[('call_with_body_bytes_expecting_body',
897
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
900
# now we call again, and it should use the second response.
902
graph = repo.get_graph()
903
parents = graph.get_parent_map([r1])
904
self.assertEqual({r1: (NULL_REVISION,)}, parents)
906
[('call_with_body_bytes_expecting_body',
907
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
908
('call_with_body_bytes_expecting_body',
909
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
914
def test_get_parent_map_reconnects_if_unknown_method(self):
915
transport_path = 'quack'
916
repo, client = self.setup_fake_client_and_repository(transport_path)
917
client.add_unknown_method_response('Repository,get_parent_map')
918
client.add_success_response_with_body('', 'ok')
919
self.assertFalse(client._medium._is_remote_before((1, 2)))
920
rev_id = 'revision-id'
921
expected_deprecations = [
922
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
924
parents = self.callDeprecated(
925
expected_deprecations, repo.get_parent_map, [rev_id])
927
[('call_with_body_bytes_expecting_body',
928
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
929
('disconnect medium',),
930
('call_expecting_body', 'Repository.get_revision_graph',
933
# The medium is now marked as being connected to an older server
934
self.assertTrue(client._medium._is_remote_before((1, 2)))
936
def test_get_parent_map_fallback_parentless_node(self):
937
"""get_parent_map falls back to get_revision_graph on old servers. The
938
results from get_revision_graph are tweaked to match the get_parent_map
941
Specifically, a {key: ()} result from get_revision_graph means "no
942
parents" for that key, which in get_parent_map results should be
943
represented as {key: ('null:',)}.
945
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
947
rev_id = 'revision-id'
948
transport_path = 'quack'
949
repo, client = self.setup_fake_client_and_repository(transport_path)
950
client.add_success_response_with_body(rev_id, 'ok')
951
client._medium._remember_remote_is_before((1, 2))
952
expected_deprecations = [
953
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
955
parents = self.callDeprecated(
956
expected_deprecations, repo.get_parent_map, [rev_id])
958
[('call_expecting_body', 'Repository.get_revision_graph',
961
self.assertEqual({rev_id: ('null:',)}, parents)
963
def test_get_parent_map_unexpected_response(self):
964
repo, client = self.setup_fake_client_and_repository('path')
965
client.add_success_response('something unexpected!')
967
errors.UnexpectedSmartServerResponse,
968
repo.get_parent_map, ['a-revision-id'])
971
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
973
def test_null_revision(self):
974
# a null revision has the predictable result {}, we should have no wire
975
# traffic when calling it with this argument
976
transport_path = 'empty'
977
repo, client = self.setup_fake_client_and_repository(transport_path)
978
client.add_success_response('notused')
979
result = self.applyDeprecated(one_four, repo.get_revision_graph,
981
self.assertEqual([], client._calls)
982
self.assertEqual({}, result)
984
def test_none_revision(self):
985
# with none we want the entire graph
986
r1 = u'\u0e33'.encode('utf8')
987
r2 = u'\u0dab'.encode('utf8')
988
lines = [' '.join([r2, r1]), r1]
989
encoded_body = '\n'.join(lines)
991
transport_path = 'sinhala'
992
repo, client = self.setup_fake_client_and_repository(transport_path)
993
client.add_success_response_with_body(encoded_body, 'ok')
994
result = self.applyDeprecated(one_four, repo.get_revision_graph)
996
[('call_expecting_body', 'Repository.get_revision_graph',
999
self.assertEqual({r1: (), r2: (r1, )}, result)
1001
def test_specific_revision(self):
1002
# with a specific revision we want the graph for that
1003
# with none we want the entire graph
1004
r11 = u'\u0e33'.encode('utf8')
1005
r12 = u'\xc9'.encode('utf8')
1006
r2 = u'\u0dab'.encode('utf8')
1007
lines = [' '.join([r2, r11, r12]), r11, r12]
1008
encoded_body = '\n'.join(lines)
1010
transport_path = 'sinhala'
1011
repo, client = self.setup_fake_client_and_repository(transport_path)
1012
client.add_success_response_with_body(encoded_body, 'ok')
1013
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1015
[('call_expecting_body', 'Repository.get_revision_graph',
1018
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1020
def test_no_such_revision(self):
1022
transport_path = 'sinhala'
1023
repo, client = self.setup_fake_client_and_repository(transport_path)
1024
client.add_error_response('nosuchrevision', revid)
1025
# also check that the right revision is reported in the error
1026
self.assertRaises(errors.NoSuchRevision,
1027
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1029
[('call_expecting_body', 'Repository.get_revision_graph',
1030
('sinhala/', revid))],
1033
def test_unexpected_error(self):
1035
transport_path = 'sinhala'
1036
repo, client = self.setup_fake_client_and_repository(transport_path)
1037
client.add_error_response('AnUnexpectedError')
1038
e = self.assertRaises(errors.ErrorFromSmartServer,
1039
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1040
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1043
class TestRepositoryIsShared(TestRemoteRepository):
1045
def test_is_shared(self):
1046
# ('yes', ) for Repository.is_shared -> 'True'.
1047
transport_path = 'quack'
1048
repo, client = self.setup_fake_client_and_repository(transport_path)
1049
client.add_success_response('yes')
1050
result = repo.is_shared()
1052
[('call', 'Repository.is_shared', ('quack/',))],
1054
self.assertEqual(True, result)
1056
def test_is_not_shared(self):
1057
# ('no', ) for Repository.is_shared -> 'False'.
1058
transport_path = 'qwack'
1059
repo, client = self.setup_fake_client_and_repository(transport_path)
1060
client.add_success_response('no')
1061
result = repo.is_shared()
1063
[('call', 'Repository.is_shared', ('qwack/',))],
1065
self.assertEqual(False, result)
1068
class TestRepositoryLockWrite(TestRemoteRepository):
1070
def test_lock_write(self):
1071
transport_path = 'quack'
1072
repo, client = self.setup_fake_client_and_repository(transport_path)
1073
client.add_success_response('ok', 'a token')
1074
result = repo.lock_write()
1076
[('call', 'Repository.lock_write', ('quack/', ''))],
1078
self.assertEqual('a token', result)
1080
def test_lock_write_already_locked(self):
1081
transport_path = 'quack'
1082
repo, client = self.setup_fake_client_and_repository(transport_path)
1083
client.add_error_response('LockContention')
1084
self.assertRaises(errors.LockContention, repo.lock_write)
1086
[('call', 'Repository.lock_write', ('quack/', ''))],
1089
def test_lock_write_unlockable(self):
1090
transport_path = 'quack'
1091
repo, client = self.setup_fake_client_and_repository(transport_path)
1092
client.add_error_response('UnlockableTransport')
1093
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1095
[('call', 'Repository.lock_write', ('quack/', ''))],
1099
class TestRepositoryUnlock(TestRemoteRepository):
1101
def test_unlock(self):
1102
transport_path = 'quack'
1103
repo, client = self.setup_fake_client_and_repository(transport_path)
1104
client.add_success_response('ok', 'a token')
1105
client.add_success_response('ok')
1109
[('call', 'Repository.lock_write', ('quack/', '')),
1110
('call', 'Repository.unlock', ('quack/', 'a token'))],
1113
def test_unlock_wrong_token(self):
1114
# If somehow the token is wrong, unlock will raise TokenMismatch.
1115
transport_path = 'quack'
1116
repo, client = self.setup_fake_client_and_repository(transport_path)
1117
client.add_success_response('ok', 'a token')
1118
client.add_error_response('TokenMismatch')
1120
self.assertRaises(errors.TokenMismatch, repo.unlock)
1123
class TestRepositoryHasRevision(TestRemoteRepository):
1125
def test_none(self):
1126
# repo.has_revision(None) should not cause any traffic.
1127
transport_path = 'quack'
1128
repo, client = self.setup_fake_client_and_repository(transport_path)
1130
# The null revision is always there, so has_revision(None) == True.
1131
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1133
# The remote repo shouldn't be accessed.
1134
self.assertEqual([], client._calls)
1137
class TestRepositoryTarball(TestRemoteRepository):
1139
# This is a canned tarball reponse we can validate against
1141
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1142
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1143
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1144
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1145
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1146
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1147
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1148
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1149
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1150
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1151
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1152
'nWQ7QH/F3JFOFCQ0aSPfA='
1155
def test_repository_tarball(self):
1156
# Test that Repository.tarball generates the right operations
1157
transport_path = 'repo'
1158
expected_calls = [('call_expecting_body', 'Repository.tarball',
1159
('repo/', 'bz2',),),
1161
repo, client = self.setup_fake_client_and_repository(transport_path)
1162
client.add_success_response_with_body(self.tarball_content, 'ok')
1163
# Now actually ask for the tarball
1164
tarball_file = repo._get_tarball('bz2')
1166
self.assertEqual(expected_calls, client._calls)
1167
self.assertEqual(self.tarball_content, tarball_file.read())
1169
tarball_file.close()
1172
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1173
"""RemoteRepository.copy_content_into optimizations"""
1175
def test_copy_content_remote_to_local(self):
1176
self.transport_server = server.SmartTCPServer_for_testing
1177
src_repo = self.make_repository('repo1')
1178
src_repo = repository.Repository.open(self.get_url('repo1'))
1179
# At the moment the tarball-based copy_content_into can't write back
1180
# into a smart server. It would be good if it could upload the
1181
# tarball; once that works we'd have to create repositories of
1182
# different formats. -- mbp 20070410
1183
dest_url = self.get_vfs_only_url('repo2')
1184
dest_bzrdir = BzrDir.create(dest_url)
1185
dest_repo = dest_bzrdir.create_repository()
1186
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1187
self.assertTrue(isinstance(src_repo, RemoteRepository))
1188
src_repo.copy_content_into(dest_repo)
1191
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1192
"""Base class for unit tests for bzrlib.remote._translate_error."""
1194
def translateTuple(self, error_tuple, **context):
1195
"""Call _translate_error with an ErrorFromSmartServer built from the
1198
:param error_tuple: A tuple of a smart server response, as would be
1199
passed to an ErrorFromSmartServer.
1200
:kwargs context: context items to call _translate_error with.
1202
:returns: The error raised by _translate_error.
1204
# Raise the ErrorFromSmartServer before passing it as an argument,
1205
# because _translate_error may need to re-raise it with a bare 'raise'
1207
server_error = errors.ErrorFromSmartServer(error_tuple)
1208
translated_error = self.translateErrorFromSmartServer(
1209
server_error, **context)
1210
return translated_error
1212
def translateErrorFromSmartServer(self, error_object, **context):
1213
"""Like translateTuple, but takes an already constructed
1214
ErrorFromSmartServer rather than a tuple.
1218
except errors.ErrorFromSmartServer, server_error:
1219
translated_error = self.assertRaises(
1220
errors.BzrError, remote._translate_error, server_error,
1222
return translated_error
1225
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1226
"""Unit tests for bzrlib.remote._translate_error.
1228
Given an ErrorFromSmartServer (which has an error tuple from a smart
1229
server) and some context, _translate_error raises more specific errors from
1232
This test case covers the cases where _translate_error succeeds in
1233
translating an ErrorFromSmartServer to something better. See
1234
TestErrorTranslationRobustness for other cases.
1237
def test_NoSuchRevision(self):
1238
branch = self.make_branch('')
1240
translated_error = self.translateTuple(
1241
('NoSuchRevision', revid), branch=branch)
1242
expected_error = errors.NoSuchRevision(branch, revid)
1243
self.assertEqual(expected_error, translated_error)
1245
def test_nosuchrevision(self):
1246
repository = self.make_repository('')
1248
translated_error = self.translateTuple(
1249
('nosuchrevision', revid), repository=repository)
1250
expected_error = errors.NoSuchRevision(repository, revid)
1251
self.assertEqual(expected_error, translated_error)
1253
def test_nobranch(self):
1254
bzrdir = self.make_bzrdir('')
1255
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1256
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1257
self.assertEqual(expected_error, translated_error)
1259
def test_LockContention(self):
1260
translated_error = self.translateTuple(('LockContention',))
1261
expected_error = errors.LockContention('(remote lock)')
1262
self.assertEqual(expected_error, translated_error)
1264
def test_UnlockableTransport(self):
1265
bzrdir = self.make_bzrdir('')
1266
translated_error = self.translateTuple(
1267
('UnlockableTransport',), bzrdir=bzrdir)
1268
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1269
self.assertEqual(expected_error, translated_error)
1271
def test_LockFailed(self):
1272
lock = 'str() of a server lock'
1273
why = 'str() of why'
1274
translated_error = self.translateTuple(('LockFailed', lock, why))
1275
expected_error = errors.LockFailed(lock, why)
1276
self.assertEqual(expected_error, translated_error)
1278
def test_TokenMismatch(self):
1279
token = 'a lock token'
1280
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1281
expected_error = errors.TokenMismatch(token, '(remote token)')
1282
self.assertEqual(expected_error, translated_error)
1284
def test_Diverged(self):
1285
branch = self.make_branch('a')
1286
other_branch = self.make_branch('b')
1287
translated_error = self.translateTuple(
1288
('Diverged',), branch=branch, other_branch=other_branch)
1289
expected_error = errors.DivergedBranches(branch, other_branch)
1290
self.assertEqual(expected_error, translated_error)
1293
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1294
"""Unit tests for bzrlib.remote._translate_error's robustness.
1296
TestErrorTranslationSuccess is for cases where _translate_error can
1297
translate successfully. This class about how _translate_err behaves when
1298
it fails to translate: it re-raises the original error.
1301
def test_unrecognised_server_error(self):
1302
"""If the error code from the server is not recognised, the original
1303
ErrorFromSmartServer is propagated unmodified.
1305
error_tuple = ('An unknown error tuple',)
1306
server_error = errors.ErrorFromSmartServer(error_tuple)
1307
translated_error = self.translateErrorFromSmartServer(server_error)
1308
self.assertEqual(server_error, translated_error)
1310
def test_context_missing_a_key(self):
1311
"""In case of a bug in the client, or perhaps an unexpected response
1312
from a server, _translate_error returns the original error tuple from
1313
the server and mutters a warning.
1315
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1316
# in the context dict. So let's give it an empty context dict instead
1317
# to exercise its error recovery.
1319
error_tuple = ('NoSuchRevision', 'revid')
1320
server_error = errors.ErrorFromSmartServer(error_tuple)
1321
translated_error = self.translateErrorFromSmartServer(server_error)
1322
self.assertEqual(server_error, translated_error)
1323
# In addition to re-raising ErrorFromSmartServer, some debug info has
1324
# been muttered to the log file for developer to look at.
1325
self.assertContainsRe(
1326
self._get_log(keep_log_file=True),
1327
"Missing key 'branch' in context")