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 TestRepositoryGetRevisionGraph(TestRemoteRepository):
1241
def test_null_revision(self):
1242
# a null revision has the predictable result {}, we should have no wire
1243
# traffic when calling it with this argument
1244
transport_path = 'empty'
1245
repo, client = self.setup_fake_client_and_repository(transport_path)
1246
client.add_success_response('notused')
1247
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1249
self.assertEqual([], client._calls)
1250
self.assertEqual({}, result)
1252
def test_none_revision(self):
1253
# with none we want the entire graph
1254
r1 = u'\u0e33'.encode('utf8')
1255
r2 = u'\u0dab'.encode('utf8')
1256
lines = [' '.join([r2, r1]), r1]
1257
encoded_body = '\n'.join(lines)
1259
transport_path = 'sinhala'
1260
repo, client = self.setup_fake_client_and_repository(transport_path)
1261
client.add_success_response_with_body(encoded_body, 'ok')
1262
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1264
[('call_expecting_body', 'Repository.get_revision_graph',
1267
self.assertEqual({r1: (), r2: (r1, )}, result)
1269
def test_specific_revision(self):
1270
# with a specific revision we want the graph for that
1271
# with none we want the entire graph
1272
r11 = u'\u0e33'.encode('utf8')
1273
r12 = u'\xc9'.encode('utf8')
1274
r2 = u'\u0dab'.encode('utf8')
1275
lines = [' '.join([r2, r11, r12]), r11, r12]
1276
encoded_body = '\n'.join(lines)
1278
transport_path = 'sinhala'
1279
repo, client = self.setup_fake_client_and_repository(transport_path)
1280
client.add_success_response_with_body(encoded_body, 'ok')
1281
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1283
[('call_expecting_body', 'Repository.get_revision_graph',
1286
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1288
def test_no_such_revision(self):
1290
transport_path = 'sinhala'
1291
repo, client = self.setup_fake_client_and_repository(transport_path)
1292
client.add_error_response('nosuchrevision', revid)
1293
# also check that the right revision is reported in the error
1294
self.assertRaises(errors.NoSuchRevision,
1295
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1297
[('call_expecting_body', 'Repository.get_revision_graph',
1298
('sinhala/', revid))],
1301
def test_unexpected_error(self):
1303
transport_path = 'sinhala'
1304
repo, client = self.setup_fake_client_and_repository(transport_path)
1305
client.add_error_response('AnUnexpectedError')
1306
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1307
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1308
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1311
class TestRepositoryIsShared(TestRemoteRepository):
1313
def test_is_shared(self):
1314
# ('yes', ) for Repository.is_shared -> 'True'.
1315
transport_path = 'quack'
1316
repo, client = self.setup_fake_client_and_repository(transport_path)
1317
client.add_success_response('yes')
1318
result = repo.is_shared()
1320
[('call', 'Repository.is_shared', ('quack/',))],
1322
self.assertEqual(True, result)
1324
def test_is_not_shared(self):
1325
# ('no', ) for Repository.is_shared -> 'False'.
1326
transport_path = 'qwack'
1327
repo, client = self.setup_fake_client_and_repository(transport_path)
1328
client.add_success_response('no')
1329
result = repo.is_shared()
1331
[('call', 'Repository.is_shared', ('qwack/',))],
1333
self.assertEqual(False, result)
1336
class TestRepositoryLockWrite(TestRemoteRepository):
1338
def test_lock_write(self):
1339
transport_path = 'quack'
1340
repo, client = self.setup_fake_client_and_repository(transport_path)
1341
client.add_success_response('ok', 'a token')
1342
result = repo.lock_write()
1344
[('call', 'Repository.lock_write', ('quack/', ''))],
1346
self.assertEqual('a token', result)
1348
def test_lock_write_already_locked(self):
1349
transport_path = 'quack'
1350
repo, client = self.setup_fake_client_and_repository(transport_path)
1351
client.add_error_response('LockContention')
1352
self.assertRaises(errors.LockContention, repo.lock_write)
1354
[('call', 'Repository.lock_write', ('quack/', ''))],
1357
def test_lock_write_unlockable(self):
1358
transport_path = 'quack'
1359
repo, client = self.setup_fake_client_and_repository(transport_path)
1360
client.add_error_response('UnlockableTransport')
1361
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1363
[('call', 'Repository.lock_write', ('quack/', ''))],
1367
class TestRepositoryUnlock(TestRemoteRepository):
1369
def test_unlock(self):
1370
transport_path = 'quack'
1371
repo, client = self.setup_fake_client_and_repository(transport_path)
1372
client.add_success_response('ok', 'a token')
1373
client.add_success_response('ok')
1377
[('call', 'Repository.lock_write', ('quack/', '')),
1378
('call', 'Repository.unlock', ('quack/', 'a token'))],
1381
def test_unlock_wrong_token(self):
1382
# If somehow the token is wrong, unlock will raise TokenMismatch.
1383
transport_path = 'quack'
1384
repo, client = self.setup_fake_client_and_repository(transport_path)
1385
client.add_success_response('ok', 'a token')
1386
client.add_error_response('TokenMismatch')
1388
self.assertRaises(errors.TokenMismatch, repo.unlock)
1391
class TestRepositoryHasRevision(TestRemoteRepository):
1393
def test_none(self):
1394
# repo.has_revision(None) should not cause any traffic.
1395
transport_path = 'quack'
1396
repo, client = self.setup_fake_client_and_repository(transport_path)
1398
# The null revision is always there, so has_revision(None) == True.
1399
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1401
# The remote repo shouldn't be accessed.
1402
self.assertEqual([], client._calls)
1405
class TestRepositoryTarball(TestRemoteRepository):
1407
# This is a canned tarball reponse we can validate against
1409
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1410
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1411
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1412
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1413
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1414
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1415
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1416
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1417
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1418
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1419
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1420
'nWQ7QH/F3JFOFCQ0aSPfA='
1423
def test_repository_tarball(self):
1424
# Test that Repository.tarball generates the right operations
1425
transport_path = 'repo'
1426
expected_calls = [('call_expecting_body', 'Repository.tarball',
1427
('repo/', 'bz2',),),
1429
repo, client = self.setup_fake_client_and_repository(transport_path)
1430
client.add_success_response_with_body(self.tarball_content, 'ok')
1431
# Now actually ask for the tarball
1432
tarball_file = repo._get_tarball('bz2')
1434
self.assertEqual(expected_calls, client._calls)
1435
self.assertEqual(self.tarball_content, tarball_file.read())
1437
tarball_file.close()
1440
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1441
"""RemoteRepository.copy_content_into optimizations"""
1443
def test_copy_content_remote_to_local(self):
1444
self.transport_server = server.SmartTCPServer_for_testing
1445
src_repo = self.make_repository('repo1')
1446
src_repo = repository.Repository.open(self.get_url('repo1'))
1447
# At the moment the tarball-based copy_content_into can't write back
1448
# into a smart server. It would be good if it could upload the
1449
# tarball; once that works we'd have to create repositories of
1450
# different formats. -- mbp 20070410
1451
dest_url = self.get_vfs_only_url('repo2')
1452
dest_bzrdir = BzrDir.create(dest_url)
1453
dest_repo = dest_bzrdir.create_repository()
1454
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1455
self.assertTrue(isinstance(src_repo, RemoteRepository))
1456
src_repo.copy_content_into(dest_repo)
1459
class _StubRealPackRepository(object):
1461
def __init__(self, calls):
1462
self._pack_collection = _StubPackCollection(calls)
1465
class _StubPackCollection(object):
1467
def __init__(self, calls):
1471
self.calls.append(('pack collection autopack',))
1473
def reload_pack_names(self):
1474
self.calls.append(('pack collection reload_pack_names',))
1477
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1478
"""Tests for RemoteRepository.autopack implementation."""
1481
"""When the server returns 'ok' and there's no _real_repository, then
1482
nothing else happens: the autopack method is done.
1484
transport_path = 'quack'
1485
repo, client = self.setup_fake_client_and_repository(transport_path)
1486
client.add_expected_call(
1487
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1489
client.finished_test()
1491
def test_ok_with_real_repo(self):
1492
"""When the server returns 'ok' and there is a _real_repository, then
1493
the _real_repository's reload_pack_name's method will be called.
1495
transport_path = 'quack'
1496
repo, client = self.setup_fake_client_and_repository(transport_path)
1497
client.add_expected_call(
1498
'PackRepository.autopack', ('quack/',),
1500
repo._real_repository = _StubRealPackRepository(client._calls)
1503
[('call', 'PackRepository.autopack', ('quack/',)),
1504
('pack collection reload_pack_names',)],
1507
def test_backwards_compatibility(self):
1508
"""If the server does not recognise the PackRepository.autopack verb,
1509
fallback to the real_repository's implementation.
1511
transport_path = 'quack'
1512
repo, client = self.setup_fake_client_and_repository(transport_path)
1513
client.add_unknown_method_response('PackRepository.autopack')
1514
def stub_ensure_real():
1515
client._calls.append(('_ensure_real',))
1516
repo._real_repository = _StubRealPackRepository(client._calls)
1517
repo._ensure_real = stub_ensure_real
1520
[('call', 'PackRepository.autopack', ('quack/',)),
1522
('pack collection autopack',)],
1526
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1527
"""Base class for unit tests for bzrlib.remote._translate_error."""
1529
def translateTuple(self, error_tuple, **context):
1530
"""Call _translate_error with an ErrorFromSmartServer built from the
1533
:param error_tuple: A tuple of a smart server response, as would be
1534
passed to an ErrorFromSmartServer.
1535
:kwargs context: context items to call _translate_error with.
1537
:returns: The error raised by _translate_error.
1539
# Raise the ErrorFromSmartServer before passing it as an argument,
1540
# because _translate_error may need to re-raise it with a bare 'raise'
1542
server_error = errors.ErrorFromSmartServer(error_tuple)
1543
translated_error = self.translateErrorFromSmartServer(
1544
server_error, **context)
1545
return translated_error
1547
def translateErrorFromSmartServer(self, error_object, **context):
1548
"""Like translateTuple, but takes an already constructed
1549
ErrorFromSmartServer rather than a tuple.
1553
except errors.ErrorFromSmartServer, server_error:
1554
translated_error = self.assertRaises(
1555
errors.BzrError, remote._translate_error, server_error,
1557
return translated_error
1560
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1561
"""Unit tests for bzrlib.remote._translate_error.
1563
Given an ErrorFromSmartServer (which has an error tuple from a smart
1564
server) and some context, _translate_error raises more specific errors from
1567
This test case covers the cases where _translate_error succeeds in
1568
translating an ErrorFromSmartServer to something better. See
1569
TestErrorTranslationRobustness for other cases.
1572
def test_NoSuchRevision(self):
1573
branch = self.make_branch('')
1575
translated_error = self.translateTuple(
1576
('NoSuchRevision', revid), branch=branch)
1577
expected_error = errors.NoSuchRevision(branch, revid)
1578
self.assertEqual(expected_error, translated_error)
1580
def test_nosuchrevision(self):
1581
repository = self.make_repository('')
1583
translated_error = self.translateTuple(
1584
('nosuchrevision', revid), repository=repository)
1585
expected_error = errors.NoSuchRevision(repository, revid)
1586
self.assertEqual(expected_error, translated_error)
1588
def test_nobranch(self):
1589
bzrdir = self.make_bzrdir('')
1590
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1591
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1592
self.assertEqual(expected_error, translated_error)
1594
def test_LockContention(self):
1595
translated_error = self.translateTuple(('LockContention',))
1596
expected_error = errors.LockContention('(remote lock)')
1597
self.assertEqual(expected_error, translated_error)
1599
def test_UnlockableTransport(self):
1600
bzrdir = self.make_bzrdir('')
1601
translated_error = self.translateTuple(
1602
('UnlockableTransport',), bzrdir=bzrdir)
1603
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1604
self.assertEqual(expected_error, translated_error)
1606
def test_LockFailed(self):
1607
lock = 'str() of a server lock'
1608
why = 'str() of why'
1609
translated_error = self.translateTuple(('LockFailed', lock, why))
1610
expected_error = errors.LockFailed(lock, why)
1611
self.assertEqual(expected_error, translated_error)
1613
def test_TokenMismatch(self):
1614
token = 'a lock token'
1615
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1616
expected_error = errors.TokenMismatch(token, '(remote token)')
1617
self.assertEqual(expected_error, translated_error)
1619
def test_Diverged(self):
1620
branch = self.make_branch('a')
1621
other_branch = self.make_branch('b')
1622
translated_error = self.translateTuple(
1623
('Diverged',), branch=branch, other_branch=other_branch)
1624
expected_error = errors.DivergedBranches(branch, other_branch)
1625
self.assertEqual(expected_error, translated_error)
1627
def test_ReadError_no_args(self):
1629
translated_error = self.translateTuple(('ReadError',), path=path)
1630
expected_error = errors.ReadError(path)
1631
self.assertEqual(expected_error, translated_error)
1633
def test_ReadError(self):
1635
translated_error = self.translateTuple(('ReadError', path))
1636
expected_error = errors.ReadError(path)
1637
self.assertEqual(expected_error, translated_error)
1639
def test_PermissionDenied_no_args(self):
1641
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1642
expected_error = errors.PermissionDenied(path)
1643
self.assertEqual(expected_error, translated_error)
1645
def test_PermissionDenied_one_arg(self):
1647
translated_error = self.translateTuple(('PermissionDenied', path))
1648
expected_error = errors.PermissionDenied(path)
1649
self.assertEqual(expected_error, translated_error)
1651
def test_PermissionDenied_one_arg_and_context(self):
1652
"""Given a choice between a path from the local context and a path on
1653
the wire, _translate_error prefers the path from the local context.
1655
local_path = 'local path'
1656
remote_path = 'remote path'
1657
translated_error = self.translateTuple(
1658
('PermissionDenied', remote_path), path=local_path)
1659
expected_error = errors.PermissionDenied(local_path)
1660
self.assertEqual(expected_error, translated_error)
1662
def test_PermissionDenied_two_args(self):
1664
extra = 'a string with extra info'
1665
translated_error = self.translateTuple(
1666
('PermissionDenied', path, extra))
1667
expected_error = errors.PermissionDenied(path, extra)
1668
self.assertEqual(expected_error, translated_error)
1671
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1672
"""Unit tests for bzrlib.remote._translate_error's robustness.
1674
TestErrorTranslationSuccess is for cases where _translate_error can
1675
translate successfully. This class about how _translate_err behaves when
1676
it fails to translate: it re-raises the original error.
1679
def test_unrecognised_server_error(self):
1680
"""If the error code from the server is not recognised, the original
1681
ErrorFromSmartServer is propagated unmodified.
1683
error_tuple = ('An unknown error tuple',)
1684
server_error = errors.ErrorFromSmartServer(error_tuple)
1685
translated_error = self.translateErrorFromSmartServer(server_error)
1686
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1687
self.assertEqual(expected_error, translated_error)
1689
def test_context_missing_a_key(self):
1690
"""In case of a bug in the client, or perhaps an unexpected response
1691
from a server, _translate_error returns the original error tuple from
1692
the server and mutters a warning.
1694
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1695
# in the context dict. So let's give it an empty context dict instead
1696
# to exercise its error recovery.
1698
error_tuple = ('NoSuchRevision', 'revid')
1699
server_error = errors.ErrorFromSmartServer(error_tuple)
1700
translated_error = self.translateErrorFromSmartServer(server_error)
1701
self.assertEqual(server_error, translated_error)
1702
# In addition to re-raising ErrorFromSmartServer, some debug info has
1703
# been muttered to the log file for developer to look at.
1704
self.assertContainsRe(
1705
self._get_log(keep_log_file=True),
1706
"Missing key 'branch' in context")
1708
def test_path_missing(self):
1709
"""Some translations (PermissionDenied, ReadError) can determine the
1710
'path' variable from either the wire or the local context. If neither
1711
has it, then an error is raised.
1713
error_tuple = ('ReadError',)
1714
server_error = errors.ErrorFromSmartServer(error_tuple)
1715
translated_error = self.translateErrorFromSmartServer(server_error)
1716
self.assertEqual(server_error, translated_error)
1717
# In addition to re-raising ErrorFromSmartServer, some debug info has
1718
# been muttered to the log file for developer to look at.
1719
self.assertContainsRe(
1720
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1723
class TestStacking(tests.TestCaseWithTransport):
1724
"""Tests for operations on stacked remote repositories.
1726
The underlying format type must support stacking.
1729
def test_access_stacked_remote(self):
1730
# based on <http://launchpad.net/bugs/261315>
1731
# make a branch stacked on another repository containing an empty
1732
# revision, then open it over hpss - we should be able to see that
1734
base_transport = self.get_transport()
1735
base_builder = self.make_branch_builder('base', format='1.6')
1736
base_builder.start_series()
1737
base_revid = base_builder.build_snapshot('rev-id', None,
1738
[('add', ('', None, 'directory', None))],
1740
base_builder.finish_series()
1741
stacked_branch = self.make_branch('stacked', format='1.6')
1742
stacked_branch.set_stacked_on_url('../base')
1743
# start a server looking at this
1744
smart_server = server.SmartTCPServer_for_testing()
1745
smart_server.setUp()
1746
self.addCleanup(smart_server.tearDown)
1747
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1748
# can get its branch and repository
1749
remote_branch = remote_bzrdir.open_branch()
1750
remote_repo = remote_branch.repository
1751
remote_repo.lock_read()
1753
# it should have an appropriate fallback repository, which should also
1754
# be a RemoteRepository
1755
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1756
self.assertIsInstance(remote_repo._fallback_repositories[0],
1758
# and it has the revision committed to the underlying repository;
1759
# these have varying implementations so we try several of them
1760
self.assertTrue(remote_repo.has_revisions([base_revid]))
1761
self.assertTrue(remote_repo.has_revision(base_revid))
1762
self.assertEqual(remote_repo.get_revision(base_revid).message,
1765
remote_repo.unlock()
1767
def prepare_stacked_remote_branch(self):
1768
smart_server = server.SmartTCPServer_for_testing()
1769
smart_server.setUp()
1770
self.addCleanup(smart_server.tearDown)
1771
tree1 = self.make_branch_and_tree('tree1')
1772
tree1.commit('rev1', rev_id='rev1')
1773
tree2 = self.make_branch_and_tree('tree2', format='1.6')
1774
tree2.branch.set_stacked_on_url(tree1.branch.base)
1775
branch2 = Branch.open(smart_server.get_url() + '/tree2')
1777
self.addCleanup(branch2.unlock)
1780
def test_stacked_get_parent_map(self):
1781
# the public implementation of get_parent_map obeys stacking
1782
branch = self.prepare_stacked_remote_branch()
1783
repo = branch.repository
1784
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
1786
def test_stacked__get_parent_map(self):
1787
# the private variant of _get_parent_map ignores stacking
1788
branch = self.prepare_stacked_remote_branch()
1789
repo = branch.repository
1790
self.assertEqual([], repo._get_parent_map(['rev1']).keys())
1793
class TestRemoteBranchEffort(tests.TestCaseWithTransport):
1796
super(TestRemoteBranchEffort, self).setUp()
1797
# Create a smart server that publishes whatever the backing VFS server
1799
self.smart_server = server.SmartTCPServer_for_testing()
1800
self.smart_server.setUp(self.get_server())
1801
self.addCleanup(self.smart_server.tearDown)
1802
# Log all HPSS calls into self.hpss_calls.
1803
_SmartClient.hooks.install_named_hook(
1804
'call', self.capture_hpss_call, None)
1805
self.hpss_calls = []
1807
def capture_hpss_call(self, params):
1808
self.hpss_calls.append(params.method)
1810
def test_copy_content_into_avoids_revision_history(self):
1811
local = self.make_branch('local')
1812
remote_backing_tree = self.make_branch_and_tree('remote')
1813
remote_backing_tree.commit("Commit.")
1814
remote_branch_url = self.smart_server.get_url() + 'remote'
1815
remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
1816
local.repository.fetch(remote_branch.repository)
1817
self.hpss_calls = []
1818
remote_branch.copy_content_into(local)
1819
self.assertFalse('Branch.revision_history' in self.hpss_calls)