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
40
from bzrlib.branch import Branch
41
from bzrlib.bzrdir import BzrDir, BzrDirFormat
42
from bzrlib.remote import (
48
from bzrlib.revision import NULL_REVISION
49
from bzrlib.smart import server, medium
50
from bzrlib.smart.client import _SmartClient
51
from bzrlib.symbol_versioning import one_four
52
from bzrlib.transport import get_transport, http
53
from bzrlib.transport.memory import MemoryTransport
54
from bzrlib.transport.remote import (
61
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
64
self.transport_server = server.SmartTCPServer_for_testing
65
super(BasicRemoteObjectTests, self).setUp()
66
self.transport = self.get_transport()
67
# make a branch that can be opened over the smart transport
68
self.local_wt = BzrDir.create_standalone_workingtree('.')
71
self.transport.disconnect()
72
tests.TestCaseWithTransport.tearDown(self)
74
def test_create_remote_bzrdir(self):
75
b = remote.RemoteBzrDir(self.transport)
76
self.assertIsInstance(b, BzrDir)
78
def test_open_remote_branch(self):
79
# open a standalone branch in the working directory
80
b = remote.RemoteBzrDir(self.transport)
81
branch = b.open_branch()
82
self.assertIsInstance(branch, Branch)
84
def test_remote_repository(self):
85
b = BzrDir.open_from_transport(self.transport)
86
repo = b.open_repository()
87
revid = u'\xc823123123'.encode('utf8')
88
self.assertFalse(repo.has_revision(revid))
89
self.local_wt.commit(message='test commit', rev_id=revid)
90
self.assertTrue(repo.has_revision(revid))
92
def test_remote_branch_revision_history(self):
93
b = BzrDir.open_from_transport(self.transport).open_branch()
94
self.assertEqual([], b.revision_history())
95
r1 = self.local_wt.commit('1st commit')
96
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
97
self.assertEqual([r1, r2], b.revision_history())
99
def test_find_correct_format(self):
100
"""Should open a RemoteBzrDir over a RemoteTransport"""
101
fmt = BzrDirFormat.find_format(self.transport)
102
self.assertTrue(RemoteBzrDirFormat
103
in BzrDirFormat._control_server_formats)
104
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
106
def test_open_detected_smart_format(self):
107
fmt = BzrDirFormat.find_format(self.transport)
108
d = fmt.open(self.transport)
109
self.assertIsInstance(d, BzrDir)
111
def test_remote_branch_repr(self):
112
b = BzrDir.open_from_transport(self.transport).open_branch()
113
self.assertStartsWith(str(b), 'RemoteBranch(')
116
class FakeRemoteTransport(object):
117
"""This class provides the minimum support for use in place of a RemoteTransport.
119
It doesn't actually transmit requests, but rather expects them to be
120
handled by a FakeClient which holds canned responses. It does not allow
121
any vfs access, therefore is not suitable for testing any operation that
122
will fallback to vfs access. Backing the test by an instance of this
123
class guarantees that it's - done using non-vfs operations.
126
_default_url = 'fakeremotetransport://host/path/'
128
def __init__(self, url=None):
130
url = self._default_url
134
return "%r(%r)" % (self.__class__.__name__,
137
def clone(self, relpath):
138
return FakeRemoteTransport(urlutils.join(self.base, relpath))
140
def get(self, relpath):
141
# only get is specifically stubbed out, because it's usually the first
142
# thing we do. anything else will fail with an AttributeError.
143
raise AssertionError("%r doesn't support file access to %r"
148
class FakeProtocol(object):
149
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
151
def __init__(self, body, fake_client):
153
self._body_buffer = None
154
self._fake_client = fake_client
156
def read_body_bytes(self, count=-1):
157
if self._body_buffer is None:
158
self._body_buffer = StringIO(self.body)
159
bytes = self._body_buffer.read(count)
160
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
161
self._fake_client.expecting_body = False
164
def cancel_read_body(self):
165
self._fake_client.expecting_body = False
167
def read_streamed_body(self):
171
class FakeClient(_SmartClient):
172
"""Lookalike for _SmartClient allowing testing."""
174
def __init__(self, fake_medium_base='fake base'):
175
"""Create a FakeClient."""
178
self.expecting_body = False
179
# if non-None, this is the list of expected calls, with only the
180
# method name and arguments included. the body might be hard to
181
# compute so is not included
182
self._expected_calls = None
183
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
185
def add_expected_call(self, call_name, call_args, response_type,
186
response_args, response_body=None):
187
if self._expected_calls is None:
188
self._expected_calls = []
189
self._expected_calls.append((call_name, call_args))
190
self.responses.append((response_type, response_args, response_body))
192
def add_success_response(self, *args):
193
self.responses.append(('success', args, None))
195
def add_success_response_with_body(self, body, *args):
196
self.responses.append(('success', args, body))
198
def add_error_response(self, *args):
199
self.responses.append(('error', args))
201
def add_unknown_method_response(self, verb):
202
self.responses.append(('unknown', verb))
204
def finished_test(self):
205
if self._expected_calls:
206
raise AssertionError("%r finished but was still expecting %r"
207
% (self, self._expected_calls[0]))
209
def _get_next_response(self):
211
response_tuple = self.responses.pop(0)
212
except IndexError, e:
213
raise AssertionError("%r didn't expect any more calls"
215
if response_tuple[0] == 'unknown':
216
raise errors.UnknownSmartMethod(response_tuple[1])
217
elif response_tuple[0] == 'error':
218
raise errors.ErrorFromSmartServer(response_tuple[1])
219
return response_tuple
221
def _check_call(self, method, args):
222
if self._expected_calls is None:
223
# the test should be updated to say what it expects
226
next_call = self._expected_calls.pop(0)
228
raise AssertionError("%r didn't expect any more calls "
230
% (self, method, args,))
231
if method != next_call[0] or args != next_call[1]:
232
raise AssertionError("%r expected %r%r "
234
% (self, next_call[0], next_call[1], method, args,))
236
def call(self, method, *args):
237
self._check_call(method, args)
238
self._calls.append(('call', method, args))
239
return self._get_next_response()[1]
241
def call_expecting_body(self, method, *args):
242
self._check_call(method, args)
243
self._calls.append(('call_expecting_body', method, args))
244
result = self._get_next_response()
245
self.expecting_body = True
246
return result[1], FakeProtocol(result[2], self)
248
def call_with_body_bytes_expecting_body(self, method, args, body):
249
self._check_call(method, args)
250
self._calls.append(('call_with_body_bytes_expecting_body', method,
252
result = self._get_next_response()
253
self.expecting_body = True
254
return result[1], FakeProtocol(result[2], self)
257
class FakeMedium(medium.SmartClientMedium):
259
def __init__(self, client_calls, base):
260
medium.SmartClientMedium.__init__(self, base)
261
self._client_calls = client_calls
263
def disconnect(self):
264
self._client_calls.append(('disconnect medium',))
267
class TestVfsHas(tests.TestCase):
269
def test_unicode_path(self):
270
client = FakeClient('/')
271
client.add_success_response('yes',)
272
transport = RemoteTransport('bzr://localhost/', _client=client)
273
filename = u'/hell\u00d8'.encode('utf8')
274
result = transport.has(filename)
276
[('call', 'has', (filename,))],
278
self.assertTrue(result)
281
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
282
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
284
def assertRemotePath(self, expected, client_base, transport_base):
285
"""Assert that the result of
286
SmartClientMedium.remote_path_from_transport is the expected value for
287
a given client_base and transport_base.
289
client_medium = medium.SmartClientMedium(client_base)
290
transport = get_transport(transport_base)
291
result = client_medium.remote_path_from_transport(transport)
292
self.assertEqual(expected, result)
294
def test_remote_path_from_transport(self):
295
"""SmartClientMedium.remote_path_from_transport calculates a URL for
296
the given transport relative to the root of the client base URL.
298
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
299
self.assertRemotePath(
300
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
302
def assertRemotePathHTTP(self, expected, transport_base, relpath):
303
"""Assert that the result of
304
HttpTransportBase.remote_path_from_transport is the expected value for
305
a given transport_base and relpath of that transport. (Note that
306
HttpTransportBase is a subclass of SmartClientMedium)
308
base_transport = get_transport(transport_base)
309
client_medium = base_transport.get_smart_medium()
310
cloned_transport = base_transport.clone(relpath)
311
result = client_medium.remote_path_from_transport(cloned_transport)
312
self.assertEqual(expected, result)
314
def test_remote_path_from_transport_http(self):
315
"""Remote paths for HTTP transports are calculated differently to other
316
transports. They are just relative to the client base, not the root
317
directory of the host.
319
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
320
self.assertRemotePathHTTP(
321
'../xyz/', scheme + '//host/path', '../xyz/')
322
self.assertRemotePathHTTP(
323
'xyz/', scheme + '//host/path', 'xyz/')
326
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
327
"""Tests for the behaviour of client_medium.remote_is_at_least."""
329
def test_initially_unlimited(self):
330
"""A fresh medium assumes that the remote side supports all
333
client_medium = medium.SmartClientMedium('dummy base')
334
self.assertFalse(client_medium._is_remote_before((99, 99)))
336
def test__remember_remote_is_before(self):
337
"""Calling _remember_remote_is_before ratchets down the known remote
340
client_medium = medium.SmartClientMedium('dummy base')
341
# Mark the remote side as being less than 1.6. The remote side may
343
client_medium._remember_remote_is_before((1, 6))
344
self.assertTrue(client_medium._is_remote_before((1, 6)))
345
self.assertFalse(client_medium._is_remote_before((1, 5)))
346
# Calling _remember_remote_is_before again with a lower value works.
347
client_medium._remember_remote_is_before((1, 5))
348
self.assertTrue(client_medium._is_remote_before((1, 5)))
349
# You cannot call _remember_remote_is_before with a larger value.
351
AssertionError, client_medium._remember_remote_is_before, (1, 9))
354
class TestBzrDirOpenBranch(tests.TestCase):
356
def test_branch_present(self):
357
transport = MemoryTransport()
358
transport.mkdir('quack')
359
transport = transport.clone('quack')
360
client = FakeClient(transport.base)
361
client.add_expected_call(
362
'BzrDir.open_branch', ('quack/',),
363
'success', ('ok', ''))
364
client.add_expected_call(
365
'BzrDir.find_repositoryV2', ('quack/',),
366
'success', ('ok', '', 'no', 'no', 'no'))
367
client.add_expected_call(
368
'Branch.get_stacked_on_url', ('quack/',),
369
'error', ('NotStacked',))
370
bzrdir = RemoteBzrDir(transport, _client=client)
371
result = bzrdir.open_branch()
372
self.assertIsInstance(result, RemoteBranch)
373
self.assertEqual(bzrdir, result.bzrdir)
374
client.finished_test()
376
def test_branch_missing(self):
377
transport = MemoryTransport()
378
transport.mkdir('quack')
379
transport = transport.clone('quack')
380
client = FakeClient(transport.base)
381
client.add_error_response('nobranch')
382
bzrdir = RemoteBzrDir(transport, _client=client)
383
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
385
[('call', 'BzrDir.open_branch', ('quack/',))],
388
def test__get_tree_branch(self):
389
# _get_tree_branch is a form of open_branch, but it should only ask for
390
# branch opening, not any other network requests.
393
calls.append("Called")
395
transport = MemoryTransport()
396
# no requests on the network - catches other api calls being made.
397
client = FakeClient(transport.base)
398
bzrdir = RemoteBzrDir(transport, _client=client)
399
# patch the open_branch call to record that it was called.
400
bzrdir.open_branch = open_branch
401
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
402
self.assertEqual(["Called"], calls)
403
self.assertEqual([], client._calls)
405
def test_url_quoting_of_path(self):
406
# Relpaths on the wire should not be URL-escaped. So "~" should be
407
# transmitted as "~", not "%7E".
408
transport = RemoteTCPTransport('bzr://localhost/~hello/')
409
client = FakeClient(transport.base)
410
client.add_expected_call(
411
'BzrDir.open_branch', ('~hello/',),
412
'success', ('ok', ''))
413
client.add_expected_call(
414
'BzrDir.find_repositoryV2', ('~hello/',),
415
'success', ('ok', '', 'no', 'no', 'no'))
416
client.add_expected_call(
417
'Branch.get_stacked_on_url', ('~hello/',),
418
'error', ('NotStacked',))
419
bzrdir = RemoteBzrDir(transport, _client=client)
420
result = bzrdir.open_branch()
421
client.finished_test()
423
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
424
transport = MemoryTransport()
425
transport.mkdir('quack')
426
transport = transport.clone('quack')
428
rich_response = 'yes'
432
subtree_response = 'yes'
434
subtree_response = 'no'
435
client = FakeClient(transport.base)
436
client.add_success_response(
437
'ok', '', rich_response, subtree_response, external_lookup)
438
bzrdir = RemoteBzrDir(transport, _client=client)
439
result = bzrdir.open_repository()
441
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
443
self.assertIsInstance(result, RemoteRepository)
444
self.assertEqual(bzrdir, result.bzrdir)
445
self.assertEqual(rich_root, result._format.rich_root_data)
446
self.assertEqual(subtrees, result._format.supports_tree_reference)
448
def test_open_repository_sets_format_attributes(self):
449
self.check_open_repository(True, True)
450
self.check_open_repository(False, True)
451
self.check_open_repository(True, False)
452
self.check_open_repository(False, False)
453
self.check_open_repository(False, False, 'yes')
455
def test_old_server(self):
456
"""RemoteBzrDirFormat should fail to probe if the server version is too
459
self.assertRaises(errors.NotBranchError,
460
RemoteBzrDirFormat.probe_transport, OldServerTransport())
463
class TestBzrDirOpenRepository(tests.TestCase):
465
def test_backwards_compat_1_2(self):
466
transport = MemoryTransport()
467
transport.mkdir('quack')
468
transport = transport.clone('quack')
469
client = FakeClient(transport.base)
470
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
471
client.add_success_response('ok', '', 'no', 'no')
472
bzrdir = RemoteBzrDir(transport, _client=client)
473
repo = bzrdir.open_repository()
475
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
476
('call', 'BzrDir.find_repository', ('quack/',))],
480
class OldSmartClient(object):
481
"""A fake smart client for test_old_version that just returns a version one
482
response to the 'hello' (query version) command.
485
def get_request(self):
486
input_file = StringIO('ok\x011\n')
487
output_file = StringIO()
488
client_medium = medium.SmartSimplePipesClientMedium(
489
input_file, output_file)
490
return medium.SmartClientStreamMediumRequest(client_medium)
492
def protocol_version(self):
496
class OldServerTransport(object):
497
"""A fake transport for test_old_server that reports it's smart server
498
protocol version as version one.
504
def get_smart_client(self):
505
return OldSmartClient()
508
class RemoteBranchTestCase(tests.TestCase):
510
def make_remote_branch(self, transport, client):
511
"""Make a RemoteBranch using 'client' as its _SmartClient.
513
A RemoteBzrDir and RemoteRepository will also be created to fill out
514
the RemoteBranch, albeit with stub values for some of their attributes.
516
# we do not want bzrdir to make any remote calls, so use False as its
517
# _client. If it tries to make a remote call, this will fail
519
bzrdir = RemoteBzrDir(transport, _client=False)
520
repo = RemoteRepository(bzrdir, None, _client=client)
521
return RemoteBranch(bzrdir, repo, _client=client)
524
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
526
def test_empty_branch(self):
527
# in an empty branch we decode the response properly
528
transport = MemoryTransport()
529
client = FakeClient(transport.base)
530
client.add_expected_call(
531
'Branch.get_stacked_on_url', ('quack/',),
532
'error', ('NotStacked',))
533
client.add_expected_call(
534
'Branch.last_revision_info', ('quack/',),
535
'success', ('ok', '0', 'null:'))
536
transport.mkdir('quack')
537
transport = transport.clone('quack')
538
branch = self.make_remote_branch(transport, client)
539
result = branch.last_revision_info()
540
client.finished_test()
541
self.assertEqual((0, NULL_REVISION), result)
543
def test_non_empty_branch(self):
544
# in a non-empty branch we also decode the response properly
545
revid = u'\xc8'.encode('utf8')
546
transport = MemoryTransport()
547
client = FakeClient(transport.base)
548
client.add_expected_call(
549
'Branch.get_stacked_on_url', ('kwaak/',),
550
'error', ('NotStacked',))
551
client.add_expected_call(
552
'Branch.last_revision_info', ('kwaak/',),
553
'success', ('ok', '2', revid))
554
transport.mkdir('kwaak')
555
transport = transport.clone('kwaak')
556
branch = self.make_remote_branch(transport, client)
557
result = branch.last_revision_info()
558
self.assertEqual((2, revid), result)
561
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
562
"""Test Branch._get_stacked_on_url rpc"""
564
def test_get_stacked_on_invalid_url(self):
565
raise tests.KnownFailure('opening a branch requires the server to open the fallback repository')
566
transport = FakeRemoteTransport('fakeremotetransport:///')
567
client = FakeClient(transport.base)
568
client.add_expected_call(
569
'Branch.get_stacked_on_url', ('.',),
570
'success', ('ok', 'file:///stacked/on'))
571
bzrdir = RemoteBzrDir(transport, _client=client)
572
branch = RemoteBranch(bzrdir, None, _client=client)
573
result = branch.get_stacked_on_url()
575
'file:///stacked/on', result)
577
def test_backwards_compatible(self):
578
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
579
base_branch = self.make_branch('base', format='1.6')
580
stacked_branch = self.make_branch('stacked', format='1.6')
581
stacked_branch.set_stacked_on_url('../base')
582
client = FakeClient(self.get_url())
583
client.add_expected_call(
584
'BzrDir.open_branch', ('stacked/',),
585
'success', ('ok', ''))
586
client.add_expected_call(
587
'BzrDir.find_repositoryV2', ('stacked/',),
588
'success', ('ok', '', 'no', 'no', 'no'))
589
# called twice, once from constructor and then again by us
590
client.add_expected_call(
591
'Branch.get_stacked_on_url', ('stacked/',),
592
'unknown', ('Branch.get_stacked_on_url',))
593
client.add_expected_call(
594
'Branch.get_stacked_on_url', ('stacked/',),
595
'unknown', ('Branch.get_stacked_on_url',))
596
# this will also do vfs access, but that goes direct to the transport
597
# and isn't seen by the FakeClient.
598
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
599
branch = bzrdir.open_branch()
600
result = branch.get_stacked_on_url()
601
self.assertEqual('../base', result)
602
client.finished_test()
603
# it's in the fallback list both for the RemoteRepository and its vfs
605
self.assertEqual(1, len(branch.repository._fallback_repositories))
607
len(branch.repository._real_repository._fallback_repositories))
609
def test_get_stacked_on_real_branch(self):
610
base_branch = self.make_branch('base', format='1.6')
611
stacked_branch = self.make_branch('stacked', format='1.6')
612
stacked_branch.set_stacked_on_url('../base')
613
client = FakeClient(self.get_url())
614
client.add_expected_call(
615
'BzrDir.open_branch', ('stacked/',),
616
'success', ('ok', ''))
617
client.add_expected_call(
618
'BzrDir.find_repositoryV2', ('stacked/',),
619
'success', ('ok', '', 'no', 'no', 'no'))
620
# called twice, once from constructor and then again by us
621
client.add_expected_call(
622
'Branch.get_stacked_on_url', ('stacked/',),
623
'success', ('ok', '../base'))
624
client.add_expected_call(
625
'Branch.get_stacked_on_url', ('stacked/',),
626
'success', ('ok', '../base'))
627
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
628
branch = bzrdir.open_branch()
629
result = branch.get_stacked_on_url()
630
self.assertEqual('../base', result)
631
client.finished_test()
632
# it's in the fallback list both for the RemoteRepository and its vfs
634
self.assertEqual(1, len(branch.repository._fallback_repositories))
636
len(branch.repository._real_repository._fallback_repositories))
639
class TestBranchSetLastRevision(RemoteBranchTestCase):
641
def test_set_empty(self):
642
# set_revision_history([]) is translated to calling
643
# Branch.set_last_revision(path, '') on the wire.
644
transport = MemoryTransport()
645
transport.mkdir('branch')
646
transport = transport.clone('branch')
648
client = FakeClient(transport.base)
649
client.add_expected_call(
650
'Branch.get_stacked_on_url', ('branch/',),
651
'error', ('NotStacked',))
652
client.add_expected_call(
653
'Branch.lock_write', ('branch/', '', ''),
654
'success', ('ok', 'branch token', 'repo token'))
655
client.add_expected_call(
656
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
658
client.add_expected_call(
659
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
661
branch = self.make_remote_branch(transport, client)
662
# This is a hack to work around the problem that RemoteBranch currently
663
# unnecessarily invokes _ensure_real upon a call to lock_write.
664
branch._ensure_real = lambda: None
666
result = branch.set_revision_history([])
668
self.assertEqual(None, result)
669
client.finished_test()
671
def test_set_nonempty(self):
672
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
673
# Branch.set_last_revision(path, rev-idN) on the wire.
674
transport = MemoryTransport()
675
transport.mkdir('branch')
676
transport = transport.clone('branch')
678
client = FakeClient(transport.base)
679
client.add_expected_call(
680
'Branch.get_stacked_on_url', ('branch/',),
681
'error', ('NotStacked',))
682
client.add_expected_call(
683
'Branch.lock_write', ('branch/', '', ''),
684
'success', ('ok', 'branch token', 'repo token'))
685
client.add_expected_call(
686
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
688
client.add_expected_call(
689
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
691
branch = self.make_remote_branch(transport, client)
692
# This is a hack to work around the problem that RemoteBranch currently
693
# unnecessarily invokes _ensure_real upon a call to lock_write.
694
branch._ensure_real = lambda: None
695
# Lock the branch, reset the record of remote calls.
697
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
699
self.assertEqual(None, result)
700
client.finished_test()
702
def test_no_such_revision(self):
703
transport = MemoryTransport()
704
transport.mkdir('branch')
705
transport = transport.clone('branch')
706
# A response of 'NoSuchRevision' is translated into an exception.
707
client = FakeClient(transport.base)
708
client.add_expected_call(
709
'Branch.get_stacked_on_url', ('branch/',),
710
'error', ('NotStacked',))
711
client.add_expected_call(
712
'Branch.lock_write', ('branch/', '', ''),
713
'success', ('ok', 'branch token', 'repo token'))
714
client.add_expected_call(
715
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
716
'error', ('NoSuchRevision', 'rev-id'))
717
client.add_expected_call(
718
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
721
branch = self.make_remote_branch(transport, client)
724
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
726
client.finished_test()
728
def test_tip_change_rejected(self):
729
"""TipChangeRejected responses cause a TipChangeRejected exception to
732
transport = MemoryTransport()
733
transport.mkdir('branch')
734
transport = transport.clone('branch')
735
client = FakeClient(transport.base)
736
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
737
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
738
client.add_expected_call(
739
'Branch.get_stacked_on_url', ('branch/',),
740
'error', ('NotStacked',))
741
client.add_expected_call(
742
'Branch.lock_write', ('branch/', '', ''),
743
'success', ('ok', 'branch token', 'repo token'))
744
client.add_expected_call(
745
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
746
'error', ('TipChangeRejected', rejection_msg_utf8))
747
client.add_expected_call(
748
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
750
branch = self.make_remote_branch(transport, client)
751
branch._ensure_real = lambda: None
753
self.addCleanup(branch.unlock)
754
# The 'TipChangeRejected' error response triggered by calling
755
# set_revision_history causes a TipChangeRejected exception.
756
err = self.assertRaises(
757
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
758
# The UTF-8 message from the response has been decoded into a unicode
760
self.assertIsInstance(err.msg, unicode)
761
self.assertEqual(rejection_msg_unicode, err.msg)
763
client.finished_test()
766
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
768
def test_set_last_revision_info(self):
769
# set_last_revision_info(num, 'rev-id') is translated to calling
770
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
771
transport = MemoryTransport()
772
transport.mkdir('branch')
773
transport = transport.clone('branch')
774
client = FakeClient(transport.base)
776
client.add_error_response('NotStacked')
778
client.add_success_response('ok', 'branch token', 'repo token')
780
client.add_success_response('ok')
782
client.add_success_response('ok')
784
branch = self.make_remote_branch(transport, client)
785
# Lock the branch, reset the record of remote calls.
788
result = branch.set_last_revision_info(1234, 'a-revision-id')
790
[('call', 'Branch.set_last_revision_info',
791
('branch/', 'branch token', 'repo token',
792
'1234', 'a-revision-id'))],
794
self.assertEqual(None, result)
796
def test_no_such_revision(self):
797
# A response of 'NoSuchRevision' is translated into an exception.
798
transport = MemoryTransport()
799
transport.mkdir('branch')
800
transport = transport.clone('branch')
801
client = FakeClient(transport.base)
803
client.add_error_response('NotStacked')
805
client.add_success_response('ok', 'branch token', 'repo token')
807
client.add_error_response('NoSuchRevision', 'revid')
809
client.add_success_response('ok')
811
branch = self.make_remote_branch(transport, client)
812
# Lock the branch, reset the record of remote calls.
817
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
820
def lock_remote_branch(self, branch):
821
"""Trick a RemoteBranch into thinking it is locked."""
822
branch._lock_mode = 'w'
823
branch._lock_count = 2
824
branch._lock_token = 'branch token'
825
branch._repo_lock_token = 'repo token'
826
branch.repository._lock_mode = 'w'
827
branch.repository._lock_count = 2
828
branch.repository._lock_token = 'repo token'
830
def test_backwards_compatibility(self):
831
"""If the server does not support the Branch.set_last_revision_info
832
verb (which is new in 1.4), then the client falls back to VFS methods.
834
# This test is a little messy. Unlike most tests in this file, it
835
# doesn't purely test what a Remote* object sends over the wire, and
836
# how it reacts to responses from the wire. It instead relies partly
837
# on asserting that the RemoteBranch will call
838
# self._real_branch.set_last_revision_info(...).
840
# First, set up our RemoteBranch with a FakeClient that raises
841
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
842
transport = MemoryTransport()
843
transport.mkdir('branch')
844
transport = transport.clone('branch')
845
client = FakeClient(transport.base)
846
client.add_expected_call(
847
'Branch.get_stacked_on_url', ('branch/',),
848
'error', ('NotStacked',))
849
client.add_expected_call(
850
'Branch.set_last_revision_info',
851
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
852
'unknown', 'Branch.set_last_revision_info')
854
branch = self.make_remote_branch(transport, client)
855
class StubRealBranch(object):
858
def set_last_revision_info(self, revno, revision_id):
860
('set_last_revision_info', revno, revision_id))
861
def _clear_cached_state(self):
863
real_branch = StubRealBranch()
864
branch._real_branch = real_branch
865
self.lock_remote_branch(branch)
867
# Call set_last_revision_info, and verify it behaved as expected.
868
result = branch.set_last_revision_info(1234, 'a-revision-id')
870
[('set_last_revision_info', 1234, 'a-revision-id')],
872
client.finished_test()
874
def test_unexpected_error(self):
875
# If the server sends an error the client doesn't understand, it gets
876
# turned into an UnknownErrorFromSmartServer, which is presented as a
877
# non-internal error to the user.
878
transport = MemoryTransport()
879
transport.mkdir('branch')
880
transport = transport.clone('branch')
881
client = FakeClient(transport.base)
883
client.add_error_response('NotStacked')
885
client.add_success_response('ok', 'branch token', 'repo token')
887
client.add_error_response('UnexpectedError')
889
client.add_success_response('ok')
891
branch = self.make_remote_branch(transport, client)
892
# Lock the branch, reset the record of remote calls.
896
err = self.assertRaises(
897
errors.UnknownErrorFromSmartServer,
898
branch.set_last_revision_info, 123, 'revid')
899
self.assertEqual(('UnexpectedError',), err.error_tuple)
902
def test_tip_change_rejected(self):
903
"""TipChangeRejected responses cause a TipChangeRejected exception to
906
transport = MemoryTransport()
907
transport.mkdir('branch')
908
transport = transport.clone('branch')
909
client = FakeClient(transport.base)
911
client.add_error_response('NotStacked')
913
client.add_success_response('ok', 'branch token', 'repo token')
915
client.add_error_response('TipChangeRejected', 'rejection message')
917
client.add_success_response('ok')
919
branch = self.make_remote_branch(transport, client)
920
# Lock the branch, reset the record of remote calls.
922
self.addCleanup(branch.unlock)
925
# The 'TipChangeRejected' error response triggered by calling
926
# set_last_revision_info causes a TipChangeRejected exception.
927
err = self.assertRaises(
928
errors.TipChangeRejected,
929
branch.set_last_revision_info, 123, 'revid')
930
self.assertEqual('rejection message', err.msg)
933
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
934
"""Getting the branch configuration should use an abstract method not vfs.
937
def test_get_branch_conf(self):
938
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
939
## # We should see that branch.get_config() does a single rpc to get the
940
## # remote configuration file, abstracting away where that is stored on
941
## # the server. However at the moment it always falls back to using the
942
## # vfs, and this would need some changes in config.py.
944
## # in an empty branch we decode the response properly
945
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
946
## # we need to make a real branch because the remote_branch.control_files
947
## # will trigger _ensure_real.
948
## branch = self.make_branch('quack')
949
## transport = branch.bzrdir.root_transport
950
## # we do not want bzrdir to make any remote calls
951
## bzrdir = RemoteBzrDir(transport, _client=False)
952
## branch = RemoteBranch(bzrdir, None, _client=client)
953
## config = branch.get_config()
955
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
959
class TestBranchLockWrite(RemoteBranchTestCase):
961
def test_lock_write_unlockable(self):
962
transport = MemoryTransport()
963
client = FakeClient(transport.base)
964
client.add_expected_call(
965
'Branch.get_stacked_on_url', ('quack/',),
966
'error', ('NotStacked',),)
967
client.add_expected_call(
968
'Branch.lock_write', ('quack/', '', ''),
969
'error', ('UnlockableTransport',))
970
transport.mkdir('quack')
971
transport = transport.clone('quack')
972
branch = self.make_remote_branch(transport, client)
973
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
974
client.finished_test()
977
class TestTransportIsReadonly(tests.TestCase):
980
client = FakeClient()
981
client.add_success_response('yes')
982
transport = RemoteTransport('bzr://example.com/', medium=False,
984
self.assertEqual(True, transport.is_readonly())
986
[('call', 'Transport.is_readonly', ())],
989
def test_false(self):
990
client = FakeClient()
991
client.add_success_response('no')
992
transport = RemoteTransport('bzr://example.com/', medium=False,
994
self.assertEqual(False, transport.is_readonly())
996
[('call', 'Transport.is_readonly', ())],
999
def test_error_from_old_server(self):
1000
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1002
Clients should treat it as a "no" response, because is_readonly is only
1003
advisory anyway (a transport could be read-write, but then the
1004
underlying filesystem could be readonly anyway).
1006
client = FakeClient()
1007
client.add_unknown_method_response('Transport.is_readonly')
1008
transport = RemoteTransport('bzr://example.com/', medium=False,
1010
self.assertEqual(False, transport.is_readonly())
1012
[('call', 'Transport.is_readonly', ())],
1016
class TestTransportMkdir(tests.TestCase):
1018
def test_permissiondenied(self):
1019
client = FakeClient()
1020
client.add_error_response('PermissionDenied', 'remote path', 'extra')
1021
transport = RemoteTransport('bzr://example.com/', medium=False,
1023
exc = self.assertRaises(
1024
errors.PermissionDenied, transport.mkdir, 'client path')
1025
expected_error = errors.PermissionDenied('/client path', 'extra')
1026
self.assertEqual(expected_error, exc)
1029
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1031
def test_defaults_to_none(self):
1032
t = RemoteSSHTransport('bzr+ssh://example.com')
1033
self.assertIs(None, t._get_credentials()[0])
1035
def test_uses_authentication_config(self):
1036
conf = config.AuthenticationConfig()
1037
conf._get_config().update(
1038
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1041
t = RemoteSSHTransport('bzr+ssh://example.com')
1042
self.assertEqual('bar', t._get_credentials()[0])
1045
class TestRemoteRepository(tests.TestCase):
1046
"""Base for testing RemoteRepository protocol usage.
1048
These tests contain frozen requests and responses. We want any changes to
1049
what is sent or expected to be require a thoughtful update to these tests
1050
because they might break compatibility with different-versioned servers.
1053
def setup_fake_client_and_repository(self, transport_path):
1054
"""Create the fake client and repository for testing with.
1056
There's no real server here; we just have canned responses sent
1059
:param transport_path: Path below the root of the MemoryTransport
1060
where the repository will be created.
1062
transport = MemoryTransport()
1063
transport.mkdir(transport_path)
1064
client = FakeClient(transport.base)
1065
transport = transport.clone(transport_path)
1066
# we do not want bzrdir to make any remote calls
1067
bzrdir = RemoteBzrDir(transport, _client=False)
1068
repo = RemoteRepository(bzrdir, None, _client=client)
1072
class TestRepositoryGatherStats(TestRemoteRepository):
1074
def test_revid_none(self):
1075
# ('ok',), body with revisions and size
1076
transport_path = 'quack'
1077
repo, client = self.setup_fake_client_and_repository(transport_path)
1078
client.add_success_response_with_body(
1079
'revisions: 2\nsize: 18\n', 'ok')
1080
result = repo.gather_stats(None)
1082
[('call_expecting_body', 'Repository.gather_stats',
1083
('quack/','','no'))],
1085
self.assertEqual({'revisions': 2, 'size': 18}, result)
1087
def test_revid_no_committers(self):
1088
# ('ok',), body without committers
1089
body = ('firstrev: 123456.300 3600\n'
1090
'latestrev: 654231.400 0\n'
1093
transport_path = 'quick'
1094
revid = u'\xc8'.encode('utf8')
1095
repo, client = self.setup_fake_client_and_repository(transport_path)
1096
client.add_success_response_with_body(body, 'ok')
1097
result = repo.gather_stats(revid)
1099
[('call_expecting_body', 'Repository.gather_stats',
1100
('quick/', revid, 'no'))],
1102
self.assertEqual({'revisions': 2, 'size': 18,
1103
'firstrev': (123456.300, 3600),
1104
'latestrev': (654231.400, 0),},
1107
def test_revid_with_committers(self):
1108
# ('ok',), body with committers
1109
body = ('committers: 128\n'
1110
'firstrev: 123456.300 3600\n'
1111
'latestrev: 654231.400 0\n'
1114
transport_path = 'buick'
1115
revid = u'\xc8'.encode('utf8')
1116
repo, client = self.setup_fake_client_and_repository(transport_path)
1117
client.add_success_response_with_body(body, 'ok')
1118
result = repo.gather_stats(revid, True)
1120
[('call_expecting_body', 'Repository.gather_stats',
1121
('buick/', revid, 'yes'))],
1123
self.assertEqual({'revisions': 2, 'size': 18,
1125
'firstrev': (123456.300, 3600),
1126
'latestrev': (654231.400, 0),},
1130
class TestRepositoryGetGraph(TestRemoteRepository):
1132
def test_get_graph(self):
1133
# get_graph returns a graph with a custom parents provider.
1134
transport_path = 'quack'
1135
repo, client = self.setup_fake_client_and_repository(transport_path)
1136
graph = repo.get_graph()
1137
self.assertNotEqual(graph._parents_provider, repo)
1140
class TestRepositoryGetParentMap(TestRemoteRepository):
1142
def test_get_parent_map_caching(self):
1143
# get_parent_map returns from cache until unlock()
1144
# setup a reponse with two revisions
1145
r1 = u'\u0e33'.encode('utf8')
1146
r2 = u'\u0dab'.encode('utf8')
1147
lines = [' '.join([r2, r1]), r1]
1148
encoded_body = bz2.compress('\n'.join(lines))
1150
transport_path = 'quack'
1151
repo, client = self.setup_fake_client_and_repository(transport_path)
1152
client.add_success_response_with_body(encoded_body, 'ok')
1153
client.add_success_response_with_body(encoded_body, 'ok')
1155
graph = repo.get_graph()
1156
parents = graph.get_parent_map([r2])
1157
self.assertEqual({r2: (r1,)}, parents)
1158
# locking and unlocking deeper should not reset
1161
parents = graph.get_parent_map([r1])
1162
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1164
[('call_with_body_bytes_expecting_body',
1165
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1168
# now we call again, and it should use the second response.
1170
graph = repo.get_graph()
1171
parents = graph.get_parent_map([r1])
1172
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1174
[('call_with_body_bytes_expecting_body',
1175
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1176
('call_with_body_bytes_expecting_body',
1177
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1182
def test_get_parent_map_reconnects_if_unknown_method(self):
1183
transport_path = 'quack'
1184
repo, client = self.setup_fake_client_and_repository(transport_path)
1185
client.add_unknown_method_response('Repository,get_parent_map')
1186
client.add_success_response_with_body('', 'ok')
1187
self.assertFalse(client._medium._is_remote_before((1, 2)))
1188
rev_id = 'revision-id'
1189
expected_deprecations = [
1190
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1192
parents = self.callDeprecated(
1193
expected_deprecations, repo.get_parent_map, [rev_id])
1195
[('call_with_body_bytes_expecting_body',
1196
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1197
('disconnect medium',),
1198
('call_expecting_body', 'Repository.get_revision_graph',
1201
# The medium is now marked as being connected to an older server
1202
self.assertTrue(client._medium._is_remote_before((1, 2)))
1204
def test_get_parent_map_fallback_parentless_node(self):
1205
"""get_parent_map falls back to get_revision_graph on old servers. The
1206
results from get_revision_graph are tweaked to match the get_parent_map
1209
Specifically, a {key: ()} result from get_revision_graph means "no
1210
parents" for that key, which in get_parent_map results should be
1211
represented as {key: ('null:',)}.
1213
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1215
rev_id = 'revision-id'
1216
transport_path = 'quack'
1217
repo, client = self.setup_fake_client_and_repository(transport_path)
1218
client.add_success_response_with_body(rev_id, 'ok')
1219
client._medium._remember_remote_is_before((1, 2))
1220
expected_deprecations = [
1221
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1223
parents = self.callDeprecated(
1224
expected_deprecations, repo.get_parent_map, [rev_id])
1226
[('call_expecting_body', 'Repository.get_revision_graph',
1229
self.assertEqual({rev_id: ('null:',)}, parents)
1231
def test_get_parent_map_unexpected_response(self):
1232
repo, client = self.setup_fake_client_and_repository('path')
1233
client.add_success_response('something unexpected!')
1235
errors.UnexpectedSmartServerResponse,
1236
repo.get_parent_map, ['a-revision-id'])
1239
class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
1241
def test_allows_new_revisions(self):
1242
"""get_parent_map's results can be updated by commit."""
1243
smart_server = server.SmartTCPServer_for_testing()
1244
smart_server.setUp()
1245
self.addCleanup(smart_server.tearDown)
1246
self.make_branch('branch')
1247
branch = Branch.open(smart_server.get_url() + '/branch')
1248
tree = branch.create_checkout('tree', lightweight=True)
1250
self.addCleanup(tree.unlock)
1251
graph = tree.branch.repository.get_graph()
1252
# This provides an opportunity for the missing rev-id to be cached.
1253
self.assertEqual({}, graph.get_parent_map(['rev1']))
1254
tree.commit('message', rev_id='rev1')
1255
graph = tree.branch.repository.get_graph()
1256
self.assertEqual({'rev1': ('null:',)}, graph.get_parent_map(['rev1']))
1259
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1261
def test_null_revision(self):
1262
# a null revision has the predictable result {}, we should have no wire
1263
# traffic when calling it with this argument
1264
transport_path = 'empty'
1265
repo, client = self.setup_fake_client_and_repository(transport_path)
1266
client.add_success_response('notused')
1267
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1269
self.assertEqual([], client._calls)
1270
self.assertEqual({}, result)
1272
def test_none_revision(self):
1273
# with none we want the entire graph
1274
r1 = u'\u0e33'.encode('utf8')
1275
r2 = u'\u0dab'.encode('utf8')
1276
lines = [' '.join([r2, r1]), r1]
1277
encoded_body = '\n'.join(lines)
1279
transport_path = 'sinhala'
1280
repo, client = self.setup_fake_client_and_repository(transport_path)
1281
client.add_success_response_with_body(encoded_body, 'ok')
1282
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1284
[('call_expecting_body', 'Repository.get_revision_graph',
1287
self.assertEqual({r1: (), r2: (r1, )}, result)
1289
def test_specific_revision(self):
1290
# with a specific revision we want the graph for that
1291
# with none we want the entire graph
1292
r11 = u'\u0e33'.encode('utf8')
1293
r12 = u'\xc9'.encode('utf8')
1294
r2 = u'\u0dab'.encode('utf8')
1295
lines = [' '.join([r2, r11, r12]), r11, r12]
1296
encoded_body = '\n'.join(lines)
1298
transport_path = 'sinhala'
1299
repo, client = self.setup_fake_client_and_repository(transport_path)
1300
client.add_success_response_with_body(encoded_body, 'ok')
1301
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1303
[('call_expecting_body', 'Repository.get_revision_graph',
1306
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1308
def test_no_such_revision(self):
1310
transport_path = 'sinhala'
1311
repo, client = self.setup_fake_client_and_repository(transport_path)
1312
client.add_error_response('nosuchrevision', revid)
1313
# also check that the right revision is reported in the error
1314
self.assertRaises(errors.NoSuchRevision,
1315
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1317
[('call_expecting_body', 'Repository.get_revision_graph',
1318
('sinhala/', revid))],
1321
def test_unexpected_error(self):
1323
transport_path = 'sinhala'
1324
repo, client = self.setup_fake_client_and_repository(transport_path)
1325
client.add_error_response('AnUnexpectedError')
1326
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1327
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1328
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1331
class TestRepositoryIsShared(TestRemoteRepository):
1333
def test_is_shared(self):
1334
# ('yes', ) for Repository.is_shared -> 'True'.
1335
transport_path = 'quack'
1336
repo, client = self.setup_fake_client_and_repository(transport_path)
1337
client.add_success_response('yes')
1338
result = repo.is_shared()
1340
[('call', 'Repository.is_shared', ('quack/',))],
1342
self.assertEqual(True, result)
1344
def test_is_not_shared(self):
1345
# ('no', ) for Repository.is_shared -> 'False'.
1346
transport_path = 'qwack'
1347
repo, client = self.setup_fake_client_and_repository(transport_path)
1348
client.add_success_response('no')
1349
result = repo.is_shared()
1351
[('call', 'Repository.is_shared', ('qwack/',))],
1353
self.assertEqual(False, result)
1356
class TestRepositoryLockWrite(TestRemoteRepository):
1358
def test_lock_write(self):
1359
transport_path = 'quack'
1360
repo, client = self.setup_fake_client_and_repository(transport_path)
1361
client.add_success_response('ok', 'a token')
1362
result = repo.lock_write()
1364
[('call', 'Repository.lock_write', ('quack/', ''))],
1366
self.assertEqual('a token', result)
1368
def test_lock_write_already_locked(self):
1369
transport_path = 'quack'
1370
repo, client = self.setup_fake_client_and_repository(transport_path)
1371
client.add_error_response('LockContention')
1372
self.assertRaises(errors.LockContention, repo.lock_write)
1374
[('call', 'Repository.lock_write', ('quack/', ''))],
1377
def test_lock_write_unlockable(self):
1378
transport_path = 'quack'
1379
repo, client = self.setup_fake_client_and_repository(transport_path)
1380
client.add_error_response('UnlockableTransport')
1381
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1383
[('call', 'Repository.lock_write', ('quack/', ''))],
1387
class TestRepositoryUnlock(TestRemoteRepository):
1389
def test_unlock(self):
1390
transport_path = 'quack'
1391
repo, client = self.setup_fake_client_and_repository(transport_path)
1392
client.add_success_response('ok', 'a token')
1393
client.add_success_response('ok')
1397
[('call', 'Repository.lock_write', ('quack/', '')),
1398
('call', 'Repository.unlock', ('quack/', 'a token'))],
1401
def test_unlock_wrong_token(self):
1402
# If somehow the token is wrong, unlock will raise TokenMismatch.
1403
transport_path = 'quack'
1404
repo, client = self.setup_fake_client_and_repository(transport_path)
1405
client.add_success_response('ok', 'a token')
1406
client.add_error_response('TokenMismatch')
1408
self.assertRaises(errors.TokenMismatch, repo.unlock)
1411
class TestRepositoryHasRevision(TestRemoteRepository):
1413
def test_none(self):
1414
# repo.has_revision(None) should not cause any traffic.
1415
transport_path = 'quack'
1416
repo, client = self.setup_fake_client_and_repository(transport_path)
1418
# The null revision is always there, so has_revision(None) == True.
1419
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1421
# The remote repo shouldn't be accessed.
1422
self.assertEqual([], client._calls)
1425
class TestRepositoryTarball(TestRemoteRepository):
1427
# This is a canned tarball reponse we can validate against
1429
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1430
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1431
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1432
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1433
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1434
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1435
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1436
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1437
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1438
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1439
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1440
'nWQ7QH/F3JFOFCQ0aSPfA='
1443
def test_repository_tarball(self):
1444
# Test that Repository.tarball generates the right operations
1445
transport_path = 'repo'
1446
expected_calls = [('call_expecting_body', 'Repository.tarball',
1447
('repo/', 'bz2',),),
1449
repo, client = self.setup_fake_client_and_repository(transport_path)
1450
client.add_success_response_with_body(self.tarball_content, 'ok')
1451
# Now actually ask for the tarball
1452
tarball_file = repo._get_tarball('bz2')
1454
self.assertEqual(expected_calls, client._calls)
1455
self.assertEqual(self.tarball_content, tarball_file.read())
1457
tarball_file.close()
1460
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1461
"""RemoteRepository.copy_content_into optimizations"""
1463
def test_copy_content_remote_to_local(self):
1464
self.transport_server = server.SmartTCPServer_for_testing
1465
src_repo = self.make_repository('repo1')
1466
src_repo = repository.Repository.open(self.get_url('repo1'))
1467
# At the moment the tarball-based copy_content_into can't write back
1468
# into a smart server. It would be good if it could upload the
1469
# tarball; once that works we'd have to create repositories of
1470
# different formats. -- mbp 20070410
1471
dest_url = self.get_vfs_only_url('repo2')
1472
dest_bzrdir = BzrDir.create(dest_url)
1473
dest_repo = dest_bzrdir.create_repository()
1474
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1475
self.assertTrue(isinstance(src_repo, RemoteRepository))
1476
src_repo.copy_content_into(dest_repo)
1479
class _StubRealPackRepository(object):
1481
def __init__(self, calls):
1482
self._pack_collection = _StubPackCollection(calls)
1485
class _StubPackCollection(object):
1487
def __init__(self, calls):
1491
self.calls.append(('pack collection autopack',))
1493
def reload_pack_names(self):
1494
self.calls.append(('pack collection reload_pack_names',))
1497
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1498
"""Tests for RemoteRepository.autopack implementation."""
1501
"""When the server returns 'ok' and there's no _real_repository, then
1502
nothing else happens: the autopack method is done.
1504
transport_path = 'quack'
1505
repo, client = self.setup_fake_client_and_repository(transport_path)
1506
client.add_expected_call(
1507
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1509
client.finished_test()
1511
def test_ok_with_real_repo(self):
1512
"""When the server returns 'ok' and there is a _real_repository, then
1513
the _real_repository's reload_pack_name's method will be called.
1515
transport_path = 'quack'
1516
repo, client = self.setup_fake_client_and_repository(transport_path)
1517
client.add_expected_call(
1518
'PackRepository.autopack', ('quack/',),
1520
repo._real_repository = _StubRealPackRepository(client._calls)
1523
[('call', 'PackRepository.autopack', ('quack/',)),
1524
('pack collection reload_pack_names',)],
1527
def test_backwards_compatibility(self):
1528
"""If the server does not recognise the PackRepository.autopack verb,
1529
fallback to the real_repository's implementation.
1531
transport_path = 'quack'
1532
repo, client = self.setup_fake_client_and_repository(transport_path)
1533
client.add_unknown_method_response('PackRepository.autopack')
1534
def stub_ensure_real():
1535
client._calls.append(('_ensure_real',))
1536
repo._real_repository = _StubRealPackRepository(client._calls)
1537
repo._ensure_real = stub_ensure_real
1540
[('call', 'PackRepository.autopack', ('quack/',)),
1542
('pack collection autopack',)],
1546
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1547
"""Base class for unit tests for bzrlib.remote._translate_error."""
1549
def translateTuple(self, error_tuple, **context):
1550
"""Call _translate_error with an ErrorFromSmartServer built from the
1553
:param error_tuple: A tuple of a smart server response, as would be
1554
passed to an ErrorFromSmartServer.
1555
:kwargs context: context items to call _translate_error with.
1557
:returns: The error raised by _translate_error.
1559
# Raise the ErrorFromSmartServer before passing it as an argument,
1560
# because _translate_error may need to re-raise it with a bare 'raise'
1562
server_error = errors.ErrorFromSmartServer(error_tuple)
1563
translated_error = self.translateErrorFromSmartServer(
1564
server_error, **context)
1565
return translated_error
1567
def translateErrorFromSmartServer(self, error_object, **context):
1568
"""Like translateTuple, but takes an already constructed
1569
ErrorFromSmartServer rather than a tuple.
1573
except errors.ErrorFromSmartServer, server_error:
1574
translated_error = self.assertRaises(
1575
errors.BzrError, remote._translate_error, server_error,
1577
return translated_error
1580
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1581
"""Unit tests for bzrlib.remote._translate_error.
1583
Given an ErrorFromSmartServer (which has an error tuple from a smart
1584
server) and some context, _translate_error raises more specific errors from
1587
This test case covers the cases where _translate_error succeeds in
1588
translating an ErrorFromSmartServer to something better. See
1589
TestErrorTranslationRobustness for other cases.
1592
def test_NoSuchRevision(self):
1593
branch = self.make_branch('')
1595
translated_error = self.translateTuple(
1596
('NoSuchRevision', revid), branch=branch)
1597
expected_error = errors.NoSuchRevision(branch, revid)
1598
self.assertEqual(expected_error, translated_error)
1600
def test_nosuchrevision(self):
1601
repository = self.make_repository('')
1603
translated_error = self.translateTuple(
1604
('nosuchrevision', revid), repository=repository)
1605
expected_error = errors.NoSuchRevision(repository, revid)
1606
self.assertEqual(expected_error, translated_error)
1608
def test_nobranch(self):
1609
bzrdir = self.make_bzrdir('')
1610
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1611
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1612
self.assertEqual(expected_error, translated_error)
1614
def test_LockContention(self):
1615
translated_error = self.translateTuple(('LockContention',))
1616
expected_error = errors.LockContention('(remote lock)')
1617
self.assertEqual(expected_error, translated_error)
1619
def test_UnlockableTransport(self):
1620
bzrdir = self.make_bzrdir('')
1621
translated_error = self.translateTuple(
1622
('UnlockableTransport',), bzrdir=bzrdir)
1623
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1624
self.assertEqual(expected_error, translated_error)
1626
def test_LockFailed(self):
1627
lock = 'str() of a server lock'
1628
why = 'str() of why'
1629
translated_error = self.translateTuple(('LockFailed', lock, why))
1630
expected_error = errors.LockFailed(lock, why)
1631
self.assertEqual(expected_error, translated_error)
1633
def test_TokenMismatch(self):
1634
token = 'a lock token'
1635
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1636
expected_error = errors.TokenMismatch(token, '(remote token)')
1637
self.assertEqual(expected_error, translated_error)
1639
def test_Diverged(self):
1640
branch = self.make_branch('a')
1641
other_branch = self.make_branch('b')
1642
translated_error = self.translateTuple(
1643
('Diverged',), branch=branch, other_branch=other_branch)
1644
expected_error = errors.DivergedBranches(branch, other_branch)
1645
self.assertEqual(expected_error, translated_error)
1647
def test_ReadError_no_args(self):
1649
translated_error = self.translateTuple(('ReadError',), path=path)
1650
expected_error = errors.ReadError(path)
1651
self.assertEqual(expected_error, translated_error)
1653
def test_ReadError(self):
1655
translated_error = self.translateTuple(('ReadError', path))
1656
expected_error = errors.ReadError(path)
1657
self.assertEqual(expected_error, translated_error)
1659
def test_PermissionDenied_no_args(self):
1661
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1662
expected_error = errors.PermissionDenied(path)
1663
self.assertEqual(expected_error, translated_error)
1665
def test_PermissionDenied_one_arg(self):
1667
translated_error = self.translateTuple(('PermissionDenied', path))
1668
expected_error = errors.PermissionDenied(path)
1669
self.assertEqual(expected_error, translated_error)
1671
def test_PermissionDenied_one_arg_and_context(self):
1672
"""Given a choice between a path from the local context and a path on
1673
the wire, _translate_error prefers the path from the local context.
1675
local_path = 'local path'
1676
remote_path = 'remote path'
1677
translated_error = self.translateTuple(
1678
('PermissionDenied', remote_path), path=local_path)
1679
expected_error = errors.PermissionDenied(local_path)
1680
self.assertEqual(expected_error, translated_error)
1682
def test_PermissionDenied_two_args(self):
1684
extra = 'a string with extra info'
1685
translated_error = self.translateTuple(
1686
('PermissionDenied', path, extra))
1687
expected_error = errors.PermissionDenied(path, extra)
1688
self.assertEqual(expected_error, translated_error)
1691
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1692
"""Unit tests for bzrlib.remote._translate_error's robustness.
1694
TestErrorTranslationSuccess is for cases where _translate_error can
1695
translate successfully. This class about how _translate_err behaves when
1696
it fails to translate: it re-raises the original error.
1699
def test_unrecognised_server_error(self):
1700
"""If the error code from the server is not recognised, the original
1701
ErrorFromSmartServer is propagated unmodified.
1703
error_tuple = ('An unknown error tuple',)
1704
server_error = errors.ErrorFromSmartServer(error_tuple)
1705
translated_error = self.translateErrorFromSmartServer(server_error)
1706
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1707
self.assertEqual(expected_error, translated_error)
1709
def test_context_missing_a_key(self):
1710
"""In case of a bug in the client, or perhaps an unexpected response
1711
from a server, _translate_error returns the original error tuple from
1712
the server and mutters a warning.
1714
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1715
# in the context dict. So let's give it an empty context dict instead
1716
# to exercise its error recovery.
1718
error_tuple = ('NoSuchRevision', 'revid')
1719
server_error = errors.ErrorFromSmartServer(error_tuple)
1720
translated_error = self.translateErrorFromSmartServer(server_error)
1721
self.assertEqual(server_error, translated_error)
1722
# In addition to re-raising ErrorFromSmartServer, some debug info has
1723
# been muttered to the log file for developer to look at.
1724
self.assertContainsRe(
1725
self._get_log(keep_log_file=True),
1726
"Missing key 'branch' in context")
1728
def test_path_missing(self):
1729
"""Some translations (PermissionDenied, ReadError) can determine the
1730
'path' variable from either the wire or the local context. If neither
1731
has it, then an error is raised.
1733
error_tuple = ('ReadError',)
1734
server_error = errors.ErrorFromSmartServer(error_tuple)
1735
translated_error = self.translateErrorFromSmartServer(server_error)
1736
self.assertEqual(server_error, translated_error)
1737
# In addition to re-raising ErrorFromSmartServer, some debug info has
1738
# been muttered to the log file for developer to look at.
1739
self.assertContainsRe(
1740
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1743
class TestStacking(tests.TestCaseWithTransport):
1744
"""Tests for operations on stacked remote repositories.
1746
The underlying format type must support stacking.
1749
def test_access_stacked_remote(self):
1750
# based on <http://launchpad.net/bugs/261315>
1751
# make a branch stacked on another repository containing an empty
1752
# revision, then open it over hpss - we should be able to see that
1754
base_transport = self.get_transport()
1755
base_builder = self.make_branch_builder('base', format='1.6')
1756
base_builder.start_series()
1757
base_revid = base_builder.build_snapshot('rev-id', None,
1758
[('add', ('', None, 'directory', None))],
1760
base_builder.finish_series()
1761
stacked_branch = self.make_branch('stacked', format='1.6')
1762
stacked_branch.set_stacked_on_url('../base')
1763
# start a server looking at this
1764
smart_server = server.SmartTCPServer_for_testing()
1765
smart_server.setUp()
1766
self.addCleanup(smart_server.tearDown)
1767
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1768
# can get its branch and repository
1769
remote_branch = remote_bzrdir.open_branch()
1770
remote_repo = remote_branch.repository
1771
remote_repo.lock_read()
1773
# it should have an appropriate fallback repository, which should also
1774
# be a RemoteRepository
1775
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1776
self.assertIsInstance(remote_repo._fallback_repositories[0],
1778
# and it has the revision committed to the underlying repository;
1779
# these have varying implementations so we try several of them
1780
self.assertTrue(remote_repo.has_revisions([base_revid]))
1781
self.assertTrue(remote_repo.has_revision(base_revid))
1782
self.assertEqual(remote_repo.get_revision(base_revid).message,
1785
remote_repo.unlock()
1787
def prepare_stacked_remote_branch(self):
1788
smart_server = server.SmartTCPServer_for_testing()
1789
smart_server.setUp()
1790
self.addCleanup(smart_server.tearDown)
1791
tree1 = self.make_branch_and_tree('tree1')
1792
tree1.commit('rev1', rev_id='rev1')
1793
tree2 = self.make_branch_and_tree('tree2', format='1.6')
1794
tree2.branch.set_stacked_on_url(tree1.branch.base)
1795
branch2 = Branch.open(smart_server.get_url() + '/tree2')
1797
self.addCleanup(branch2.unlock)
1800
def test_stacked_get_parent_map(self):
1801
# the public implementation of get_parent_map obeys stacking
1802
branch = self.prepare_stacked_remote_branch()
1803
repo = branch.repository
1804
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
1806
def test_unstacked_get_parent_map(self):
1807
# _unstacked_provider.get_parent_map ignores stacking
1808
branch = self.prepare_stacked_remote_branch()
1809
provider = branch.repository._unstacked_provider
1810
self.assertEqual([], provider.get_parent_map(['rev1']).keys())
1813
class TestRemoteBranchEffort(tests.TestCaseWithTransport):
1816
super(TestRemoteBranchEffort, self).setUp()
1817
# Create a smart server that publishes whatever the backing VFS server
1819
self.smart_server = server.SmartTCPServer_for_testing()
1820
self.smart_server.setUp(self.get_server())
1821
self.addCleanup(self.smart_server.tearDown)
1822
# Log all HPSS calls into self.hpss_calls.
1823
_SmartClient.hooks.install_named_hook(
1824
'call', self.capture_hpss_call, None)
1825
self.hpss_calls = []
1827
def capture_hpss_call(self, params):
1828
self.hpss_calls.append(params.method)
1830
def test_copy_content_into_avoids_revision_history(self):
1831
local = self.make_branch('local')
1832
remote_backing_tree = self.make_branch_and_tree('remote')
1833
remote_backing_tree.commit("Commit.")
1834
remote_branch_url = self.smart_server.get_url() + 'remote'
1835
remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
1836
local.repository.fetch(remote_branch.repository)
1837
self.hpss_calls = []
1838
remote_branch.copy_content_into(local)
1839
self.assertFalse('Branch.revision_history' in self.hpss_calls)