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
39
from bzrlib.branch import Branch
40
from bzrlib.bzrdir import BzrDir, BzrDirFormat
41
from bzrlib.remote import (
47
from bzrlib.revision import NULL_REVISION
48
from bzrlib.smart import server, medium
49
from bzrlib.smart.client import _SmartClient
50
from bzrlib.symbol_versioning import one_four
51
from bzrlib.transport import get_transport, http
52
from bzrlib.transport.memory import MemoryTransport
53
from bzrlib.transport.remote import (
60
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
63
self.transport_server = server.SmartTCPServer_for_testing
64
super(BasicRemoteObjectTests, self).setUp()
65
self.transport = self.get_transport()
66
# make a branch that can be opened over the smart transport
67
self.local_wt = BzrDir.create_standalone_workingtree('.')
70
self.transport.disconnect()
71
tests.TestCaseWithTransport.tearDown(self)
73
def test_create_remote_bzrdir(self):
74
b = remote.RemoteBzrDir(self.transport)
75
self.assertIsInstance(b, BzrDir)
77
def test_open_remote_branch(self):
78
# open a standalone branch in the working directory
79
b = remote.RemoteBzrDir(self.transport)
80
branch = b.open_branch()
81
self.assertIsInstance(branch, Branch)
83
def test_remote_repository(self):
84
b = BzrDir.open_from_transport(self.transport)
85
repo = b.open_repository()
86
revid = u'\xc823123123'.encode('utf8')
87
self.assertFalse(repo.has_revision(revid))
88
self.local_wt.commit(message='test commit', rev_id=revid)
89
self.assertTrue(repo.has_revision(revid))
91
def test_remote_branch_revision_history(self):
92
b = BzrDir.open_from_transport(self.transport).open_branch()
93
self.assertEqual([], b.revision_history())
94
r1 = self.local_wt.commit('1st commit')
95
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
96
self.assertEqual([r1, r2], b.revision_history())
98
def test_find_correct_format(self):
99
"""Should open a RemoteBzrDir over a RemoteTransport"""
100
fmt = BzrDirFormat.find_format(self.transport)
101
self.assertTrue(RemoteBzrDirFormat
102
in BzrDirFormat._control_server_formats)
103
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
105
def test_open_detected_smart_format(self):
106
fmt = BzrDirFormat.find_format(self.transport)
107
d = fmt.open(self.transport)
108
self.assertIsInstance(d, BzrDir)
110
def test_remote_branch_repr(self):
111
b = BzrDir.open_from_transport(self.transport).open_branch()
112
self.assertStartsWith(str(b), 'RemoteBranch(')
115
class FakeRemoteTransport(object):
116
"""This class provides the minimum support for use in place of a RemoteTransport.
118
It doesn't actually transmit requests, but rather expects them to be
119
handled by a FakeClient which holds canned responses. It does not allow
120
any vfs access, therefore is not suitable for testing any operation that
121
will fallback to vfs access. Backing the test by an instance of this
122
class guarantees that it's - done using non-vfs operations.
125
_default_url = 'fakeremotetransport://host/path/'
127
def __init__(self, url=None):
129
url = self._default_url
133
return "%r(%r)" % (self.__class__.__name__,
136
def clone(self, relpath):
137
return FakeRemoteTransport(urlutils.join(self.base, relpath))
139
def get(self, relpath):
140
# only get is specifically stubbed out, because it's usually the first
141
# thing we do. anything else will fail with an AttributeError.
142
raise AssertionError("%r doesn't support file access to %r"
147
class FakeProtocol(object):
148
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
150
def __init__(self, body, fake_client):
152
self._body_buffer = None
153
self._fake_client = fake_client
155
def read_body_bytes(self, count=-1):
156
if self._body_buffer is None:
157
self._body_buffer = StringIO(self.body)
158
bytes = self._body_buffer.read(count)
159
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
160
self._fake_client.expecting_body = False
163
def cancel_read_body(self):
164
self._fake_client.expecting_body = False
166
def read_streamed_body(self):
170
class FakeClient(_SmartClient):
171
"""Lookalike for _SmartClient allowing testing."""
173
def __init__(self, fake_medium_base='fake base'):
174
"""Create a FakeClient.
176
:param responses: A list of response-tuple, body-data pairs to be sent
177
back to callers. A special case is if the response-tuple is
178
'unknown verb', then a UnknownSmartMethod will be raised for that
179
call, using the second element of the tuple as the verb in the
184
self.expecting_body = False
185
# if non-None, this is the list of expected calls, with only the
186
# method name and arguments included. the body might be hard to
187
# compute so is not included
188
self._expected_calls = None
189
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
191
def add_expected_call(self, call_name, call_args, response_type,
192
response_args, response_body=None):
193
if self._expected_calls is None:
194
self._expected_calls = []
195
self._expected_calls.append((call_name, call_args))
196
self.responses.append((response_type, response_args, response_body))
198
def add_success_response(self, *args):
199
self.responses.append(('success', args, None))
201
def add_success_response_with_body(self, body, *args):
202
self.responses.append(('success', args, body))
204
def add_error_response(self, *args):
205
self.responses.append(('error', args))
207
def add_unknown_method_response(self, verb):
208
self.responses.append(('unknown', verb))
210
def finished_test(self):
211
if self._expected_calls:
212
raise AssertionError("%r finished but was still expecting %r"
213
% (self, self._expected_calls[0]))
215
def _get_next_response(self):
217
response_tuple = self.responses.pop(0)
218
except IndexError, e:
219
raise AssertionError("%r didn't expect any more calls"
221
if response_tuple[0] == 'unknown':
222
raise errors.UnknownSmartMethod(response_tuple[1])
223
elif response_tuple[0] == 'error':
224
raise errors.ErrorFromSmartServer(response_tuple[1])
225
return response_tuple
227
def _check_call(self, method, args):
228
if self._expected_calls is None:
229
# the test should be updated to say what it expects
232
next_call = self._expected_calls.pop(0)
234
raise AssertionError("%r didn't expect any more calls "
236
% (self, method, args,))
237
if method != next_call[0] or args != next_call[1]:
238
raise AssertionError("%r expected %r%r "
240
% (self, next_call[0], next_call[1], method, args,))
242
def call(self, method, *args):
243
self._check_call(method, args)
244
self._calls.append(('call', method, args))
245
return self._get_next_response()[1]
247
def call_expecting_body(self, method, *args):
248
self._check_call(method, args)
249
self._calls.append(('call_expecting_body', method, args))
250
result = self._get_next_response()
251
self.expecting_body = True
252
return result[1], FakeProtocol(result[2], self)
254
def call_with_body_bytes_expecting_body(self, method, args, body):
255
self._check_call(method, args)
256
self._calls.append(('call_with_body_bytes_expecting_body', method,
258
result = self._get_next_response()
259
self.expecting_body = True
260
return result[1], FakeProtocol(result[2], self)
263
class FakeMedium(medium.SmartClientMedium):
265
def __init__(self, client_calls, base):
266
medium.SmartClientMedium.__init__(self, base)
267
self._client_calls = client_calls
269
def disconnect(self):
270
self._client_calls.append(('disconnect medium',))
273
class TestVfsHas(tests.TestCase):
275
def test_unicode_path(self):
276
client = FakeClient('/')
277
client.add_success_response('yes',)
278
transport = RemoteTransport('bzr://localhost/', _client=client)
279
filename = u'/hell\u00d8'.encode('utf8')
280
result = transport.has(filename)
282
[('call', 'has', (filename,))],
284
self.assertTrue(result)
287
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
288
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
290
def assertRemotePath(self, expected, client_base, transport_base):
291
"""Assert that the result of
292
SmartClientMedium.remote_path_from_transport is the expected value for
293
a given client_base and transport_base.
295
client_medium = medium.SmartClientMedium(client_base)
296
transport = get_transport(transport_base)
297
result = client_medium.remote_path_from_transport(transport)
298
self.assertEqual(expected, result)
300
def test_remote_path_from_transport(self):
301
"""SmartClientMedium.remote_path_from_transport calculates a URL for
302
the given transport relative to the root of the client base URL.
304
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
305
self.assertRemotePath(
306
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
308
def assertRemotePathHTTP(self, expected, transport_base, relpath):
309
"""Assert that the result of
310
HttpTransportBase.remote_path_from_transport is the expected value for
311
a given transport_base and relpath of that transport. (Note that
312
HttpTransportBase is a subclass of SmartClientMedium)
314
base_transport = get_transport(transport_base)
315
client_medium = base_transport.get_smart_medium()
316
cloned_transport = base_transport.clone(relpath)
317
result = client_medium.remote_path_from_transport(cloned_transport)
318
self.assertEqual(expected, result)
320
def test_remote_path_from_transport_http(self):
321
"""Remote paths for HTTP transports are calculated differently to other
322
transports. They are just relative to the client base, not the root
323
directory of the host.
325
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
326
self.assertRemotePathHTTP(
327
'../xyz/', scheme + '//host/path', '../xyz/')
328
self.assertRemotePathHTTP(
329
'xyz/', scheme + '//host/path', 'xyz/')
332
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
333
"""Tests for the behaviour of client_medium.remote_is_at_least."""
335
def test_initially_unlimited(self):
336
"""A fresh medium assumes that the remote side supports all
339
client_medium = medium.SmartClientMedium('dummy base')
340
self.assertFalse(client_medium._is_remote_before((99, 99)))
342
def test__remember_remote_is_before(self):
343
"""Calling _remember_remote_is_before ratchets down the known remote
346
client_medium = medium.SmartClientMedium('dummy base')
347
# Mark the remote side as being less than 1.6. The remote side may
349
client_medium._remember_remote_is_before((1, 6))
350
self.assertTrue(client_medium._is_remote_before((1, 6)))
351
self.assertFalse(client_medium._is_remote_before((1, 5)))
352
# Calling _remember_remote_is_before again with a lower value works.
353
client_medium._remember_remote_is_before((1, 5))
354
self.assertTrue(client_medium._is_remote_before((1, 5)))
355
# You cannot call _remember_remote_is_before with a larger value.
357
AssertionError, client_medium._remember_remote_is_before, (1, 9))
360
class TestBzrDirOpenBranch(tests.TestCase):
362
def test_branch_present(self):
363
transport = MemoryTransport()
364
transport.mkdir('quack')
365
transport = transport.clone('quack')
366
client = FakeClient(transport.base)
367
client.add_expected_call(
368
'BzrDir.open_branch', ('quack/',),
369
'success', ('ok', ''))
370
client.add_expected_call(
371
'BzrDir.find_repositoryV2', ('quack/',),
372
'success', ('ok', '', 'no', 'no', 'no'))
373
client.add_expected_call(
374
'Branch.get_stacked_on_url', ('quack/',),
375
'error', ('NotStacked',))
376
bzrdir = RemoteBzrDir(transport, _client=client)
377
result = bzrdir.open_branch()
378
self.assertIsInstance(result, RemoteBranch)
379
self.assertEqual(bzrdir, result.bzrdir)
380
client.finished_test()
382
def test_branch_missing(self):
383
transport = MemoryTransport()
384
transport.mkdir('quack')
385
transport = transport.clone('quack')
386
client = FakeClient(transport.base)
387
client.add_error_response('nobranch')
388
bzrdir = RemoteBzrDir(transport, _client=client)
389
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
391
[('call', 'BzrDir.open_branch', ('quack/',))],
394
def test__get_tree_branch(self):
395
# _get_tree_branch is a form of open_branch, but it should only ask for
396
# branch opening, not any other network requests.
399
calls.append("Called")
401
transport = MemoryTransport()
402
# no requests on the network - catches other api calls being made.
403
client = FakeClient(transport.base)
404
bzrdir = RemoteBzrDir(transport, _client=client)
405
# patch the open_branch call to record that it was called.
406
bzrdir.open_branch = open_branch
407
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
408
self.assertEqual(["Called"], calls)
409
self.assertEqual([], client._calls)
411
def test_url_quoting_of_path(self):
412
# Relpaths on the wire should not be URL-escaped. So "~" should be
413
# transmitted as "~", not "%7E".
414
transport = RemoteTCPTransport('bzr://localhost/~hello/')
415
client = FakeClient(transport.base)
416
client.add_expected_call(
417
'BzrDir.open_branch', ('~hello/',),
418
'success', ('ok', ''))
419
client.add_expected_call(
420
'BzrDir.find_repositoryV2', ('~hello/',),
421
'success', ('ok', '', 'no', 'no', 'no'))
422
client.add_expected_call(
423
'Branch.get_stacked_on_url', ('~hello/',),
424
'error', ('NotStacked',))
425
bzrdir = RemoteBzrDir(transport, _client=client)
426
result = bzrdir.open_branch()
427
client.finished_test()
429
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
430
transport = MemoryTransport()
431
transport.mkdir('quack')
432
transport = transport.clone('quack')
434
rich_response = 'yes'
438
subtree_response = 'yes'
440
subtree_response = 'no'
441
client = FakeClient(transport.base)
442
client.add_success_response(
443
'ok', '', rich_response, subtree_response, external_lookup)
444
bzrdir = RemoteBzrDir(transport, _client=client)
445
result = bzrdir.open_repository()
447
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
449
self.assertIsInstance(result, RemoteRepository)
450
self.assertEqual(bzrdir, result.bzrdir)
451
self.assertEqual(rich_root, result._format.rich_root_data)
452
self.assertEqual(subtrees, result._format.supports_tree_reference)
454
def test_open_repository_sets_format_attributes(self):
455
self.check_open_repository(True, True)
456
self.check_open_repository(False, True)
457
self.check_open_repository(True, False)
458
self.check_open_repository(False, False)
459
self.check_open_repository(False, False, 'yes')
461
def test_old_server(self):
462
"""RemoteBzrDirFormat should fail to probe if the server version is too
465
self.assertRaises(errors.NotBranchError,
466
RemoteBzrDirFormat.probe_transport, OldServerTransport())
469
class TestBzrDirOpenRepository(tests.TestCase):
471
def test_backwards_compat_1_2(self):
472
transport = MemoryTransport()
473
transport.mkdir('quack')
474
transport = transport.clone('quack')
475
client = FakeClient(transport.base)
476
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
477
client.add_success_response('ok', '', 'no', 'no')
478
bzrdir = RemoteBzrDir(transport, _client=client)
479
repo = bzrdir.open_repository()
481
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
482
('call', 'BzrDir.find_repository', ('quack/',))],
486
class OldSmartClient(object):
487
"""A fake smart client for test_old_version that just returns a version one
488
response to the 'hello' (query version) command.
491
def get_request(self):
492
input_file = StringIO('ok\x011\n')
493
output_file = StringIO()
494
client_medium = medium.SmartSimplePipesClientMedium(
495
input_file, output_file)
496
return medium.SmartClientStreamMediumRequest(client_medium)
498
def protocol_version(self):
502
class OldServerTransport(object):
503
"""A fake transport for test_old_server that reports it's smart server
504
protocol version as version one.
510
def get_smart_client(self):
511
return OldSmartClient()
514
class RemoteBranchTestCase(tests.TestCase):
516
def make_remote_branch(self, transport, client):
517
"""Make a RemoteBranch using 'client' as its _SmartClient.
519
A RemoteBzrDir and RemoteRepository will also be created to fill out
520
the RemoteBranch, albeit with stub values for some of their attributes.
522
# we do not want bzrdir to make any remote calls, so use False as its
523
# _client. If it tries to make a remote call, this will fail
525
bzrdir = RemoteBzrDir(transport, _client=False)
526
repo = RemoteRepository(bzrdir, None, _client=client)
527
return RemoteBranch(bzrdir, repo, _client=client)
530
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
532
def test_empty_branch(self):
533
# in an empty branch we decode the response properly
534
transport = MemoryTransport()
535
client = FakeClient(transport.base)
536
client.add_expected_call(
537
'Branch.get_stacked_on_url', ('quack/',),
538
'error', ('NotStacked',))
539
client.add_expected_call(
540
'Branch.last_revision_info', ('quack/',),
541
'success', ('ok', '0', 'null:'))
542
transport.mkdir('quack')
543
transport = transport.clone('quack')
544
branch = self.make_remote_branch(transport, client)
545
result = branch.last_revision_info()
546
client.finished_test()
547
self.assertEqual((0, NULL_REVISION), result)
549
def test_non_empty_branch(self):
550
# in a non-empty branch we also decode the response properly
551
revid = u'\xc8'.encode('utf8')
552
transport = MemoryTransport()
553
client = FakeClient(transport.base)
554
client.add_expected_call(
555
'Branch.get_stacked_on_url', ('kwaak/',),
556
'error', ('NotStacked',))
557
client.add_expected_call(
558
'Branch.last_revision_info', ('kwaak/',),
559
'success', ('ok', '2', revid))
560
transport.mkdir('kwaak')
561
transport = transport.clone('kwaak')
562
branch = self.make_remote_branch(transport, client)
563
result = branch.last_revision_info()
564
self.assertEqual((2, revid), result)
567
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
568
"""Test Branch._get_stacked_on_url rpc"""
570
def test_get_stacked_on_invalid_url(self):
571
raise tests.KnownFailure('opening a branch requires the server to open the fallback repository')
572
transport = FakeRemoteTransport('fakeremotetransport:///')
573
client = FakeClient(transport.base)
574
client.add_expected_call(
575
'Branch.get_stacked_on_url', ('.',),
576
'success', ('ok', 'file:///stacked/on'))
577
bzrdir = RemoteBzrDir(transport, _client=client)
578
branch = RemoteBranch(bzrdir, None, _client=client)
579
result = branch.get_stacked_on_url()
581
'file:///stacked/on', result)
583
def test_backwards_compatible(self):
584
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
585
base_branch = self.make_branch('base', format='1.6')
586
stacked_branch = self.make_branch('stacked', format='1.6')
587
stacked_branch.set_stacked_on_url('../base')
588
client = FakeClient(self.get_url())
589
client.add_expected_call(
590
'BzrDir.open_branch', ('stacked/',),
591
'success', ('ok', ''))
592
client.add_expected_call(
593
'BzrDir.find_repositoryV2', ('stacked/',),
594
'success', ('ok', '', 'no', 'no', 'no'))
595
# called twice, once from constructor and then again by us
596
client.add_expected_call(
597
'Branch.get_stacked_on_url', ('stacked/',),
598
'unknown', ('Branch.get_stacked_on_url',))
599
client.add_expected_call(
600
'Branch.get_stacked_on_url', ('stacked/',),
601
'unknown', ('Branch.get_stacked_on_url',))
602
# this will also do vfs access, but that goes direct to the transport
603
# and isn't seen by the FakeClient.
604
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
605
branch = bzrdir.open_branch()
606
result = branch.get_stacked_on_url()
607
self.assertEqual('../base', result)
608
client.finished_test()
609
# it's in the fallback list both for the RemoteRepository and its vfs
611
self.assertEqual(1, len(branch.repository._fallback_repositories))
613
len(branch.repository._real_repository._fallback_repositories))
615
def test_get_stacked_on_real_branch(self):
616
base_branch = self.make_branch('base', format='1.6')
617
stacked_branch = self.make_branch('stacked', format='1.6')
618
stacked_branch.set_stacked_on_url('../base')
619
client = FakeClient(self.get_url())
620
client.add_expected_call(
621
'BzrDir.open_branch', ('stacked/',),
622
'success', ('ok', ''))
623
client.add_expected_call(
624
'BzrDir.find_repositoryV2', ('stacked/',),
625
'success', ('ok', '', 'no', 'no', 'no'))
626
# called twice, once from constructor and then again by us
627
client.add_expected_call(
628
'Branch.get_stacked_on_url', ('stacked/',),
629
'success', ('ok', '../base'))
630
client.add_expected_call(
631
'Branch.get_stacked_on_url', ('stacked/',),
632
'success', ('ok', '../base'))
633
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
634
branch = bzrdir.open_branch()
635
result = branch.get_stacked_on_url()
636
self.assertEqual('../base', result)
637
client.finished_test()
638
# it's in the fallback list both for the RemoteRepository and its vfs
640
self.assertEqual(1, len(branch.repository._fallback_repositories))
642
len(branch.repository._real_repository._fallback_repositories))
645
class TestBranchSetLastRevision(RemoteBranchTestCase):
647
def test_set_empty(self):
648
# set_revision_history([]) is translated to calling
649
# Branch.set_last_revision(path, '') on the wire.
650
transport = MemoryTransport()
651
transport.mkdir('branch')
652
transport = transport.clone('branch')
654
client = FakeClient(transport.base)
655
client.add_expected_call(
656
'Branch.get_stacked_on_url', ('branch/',),
657
'error', ('NotStacked',))
658
client.add_expected_call(
659
'Branch.lock_write', ('branch/', '', ''),
660
'success', ('ok', 'branch token', 'repo token'))
661
client.add_expected_call(
662
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
664
client.add_expected_call(
665
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
667
branch = self.make_remote_branch(transport, client)
668
# This is a hack to work around the problem that RemoteBranch currently
669
# unnecessarily invokes _ensure_real upon a call to lock_write.
670
branch._ensure_real = lambda: None
672
result = branch.set_revision_history([])
674
self.assertEqual(None, result)
675
client.finished_test()
677
def test_set_nonempty(self):
678
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
679
# Branch.set_last_revision(path, rev-idN) on the wire.
680
transport = MemoryTransport()
681
transport.mkdir('branch')
682
transport = transport.clone('branch')
684
client = FakeClient(transport.base)
685
client.add_expected_call(
686
'Branch.get_stacked_on_url', ('branch/',),
687
'error', ('NotStacked',))
688
client.add_expected_call(
689
'Branch.lock_write', ('branch/', '', ''),
690
'success', ('ok', 'branch token', 'repo token'))
691
client.add_expected_call(
692
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
694
client.add_expected_call(
695
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
697
branch = self.make_remote_branch(transport, client)
698
# This is a hack to work around the problem that RemoteBranch currently
699
# unnecessarily invokes _ensure_real upon a call to lock_write.
700
branch._ensure_real = lambda: None
701
# Lock the branch, reset the record of remote calls.
703
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
705
self.assertEqual(None, result)
706
client.finished_test()
708
def test_no_such_revision(self):
709
transport = MemoryTransport()
710
transport.mkdir('branch')
711
transport = transport.clone('branch')
712
# A response of 'NoSuchRevision' is translated into an exception.
713
client = FakeClient(transport.base)
714
client.add_expected_call(
715
'Branch.get_stacked_on_url', ('branch/',),
716
'error', ('NotStacked',))
717
client.add_expected_call(
718
'Branch.lock_write', ('branch/', '', ''),
719
'success', ('ok', 'branch token', 'repo token'))
720
client.add_expected_call(
721
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
722
'error', ('NoSuchRevision', 'rev-id'))
723
client.add_expected_call(
724
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
727
branch = self.make_remote_branch(transport, client)
730
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
732
client.finished_test()
734
def test_tip_change_rejected(self):
735
"""TipChangeRejected responses cause a TipChangeRejected exception to
738
transport = MemoryTransport()
739
transport.mkdir('branch')
740
transport = transport.clone('branch')
741
client = FakeClient(transport.base)
742
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
743
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
744
client.add_expected_call(
745
'Branch.get_stacked_on_url', ('branch/',),
746
'error', ('NotStacked',))
747
client.add_expected_call(
748
'Branch.lock_write', ('branch/', '', ''),
749
'success', ('ok', 'branch token', 'repo token'))
750
client.add_expected_call(
751
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
752
'error', ('TipChangeRejected', rejection_msg_utf8))
753
client.add_expected_call(
754
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
756
branch = self.make_remote_branch(transport, client)
757
branch._ensure_real = lambda: None
759
self.addCleanup(branch.unlock)
760
# The 'TipChangeRejected' error response triggered by calling
761
# set_revision_history causes a TipChangeRejected exception.
762
err = self.assertRaises(
763
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
764
# The UTF-8 message from the response has been decoded into a unicode
766
self.assertIsInstance(err.msg, unicode)
767
self.assertEqual(rejection_msg_unicode, err.msg)
769
client.finished_test()
772
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
774
def test_set_last_revision_info(self):
775
# set_last_revision_info(num, 'rev-id') is translated to calling
776
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
777
transport = MemoryTransport()
778
transport.mkdir('branch')
779
transport = transport.clone('branch')
780
client = FakeClient(transport.base)
782
client.add_error_response('NotStacked')
784
client.add_success_response('ok', 'branch token', 'repo token')
786
client.add_success_response('ok')
788
client.add_success_response('ok')
790
branch = self.make_remote_branch(transport, client)
791
# Lock the branch, reset the record of remote calls.
794
result = branch.set_last_revision_info(1234, 'a-revision-id')
796
[('call', 'Branch.set_last_revision_info',
797
('branch/', 'branch token', 'repo token',
798
'1234', 'a-revision-id'))],
800
self.assertEqual(None, result)
802
def test_no_such_revision(self):
803
# A response of 'NoSuchRevision' is translated into an exception.
804
transport = MemoryTransport()
805
transport.mkdir('branch')
806
transport = transport.clone('branch')
807
client = FakeClient(transport.base)
809
client.add_error_response('NotStacked')
811
client.add_success_response('ok', 'branch token', 'repo token')
813
client.add_error_response('NoSuchRevision', 'revid')
815
client.add_success_response('ok')
817
branch = self.make_remote_branch(transport, client)
818
# Lock the branch, reset the record of remote calls.
823
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
826
def lock_remote_branch(self, branch):
827
"""Trick a RemoteBranch into thinking it is locked."""
828
branch._lock_mode = 'w'
829
branch._lock_count = 2
830
branch._lock_token = 'branch token'
831
branch._repo_lock_token = 'repo token'
832
branch.repository._lock_mode = 'w'
833
branch.repository._lock_count = 2
834
branch.repository._lock_token = 'repo token'
836
def test_backwards_compatibility(self):
837
"""If the server does not support the Branch.set_last_revision_info
838
verb (which is new in 1.4), then the client falls back to VFS methods.
840
# This test is a little messy. Unlike most tests in this file, it
841
# doesn't purely test what a Remote* object sends over the wire, and
842
# how it reacts to responses from the wire. It instead relies partly
843
# on asserting that the RemoteBranch will call
844
# self._real_branch.set_last_revision_info(...).
846
# First, set up our RemoteBranch with a FakeClient that raises
847
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
848
transport = MemoryTransport()
849
transport.mkdir('branch')
850
transport = transport.clone('branch')
851
client = FakeClient(transport.base)
852
client.add_expected_call(
853
'Branch.get_stacked_on_url', ('branch/',),
854
'error', ('NotStacked',))
855
client.add_expected_call(
856
'Branch.set_last_revision_info',
857
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
858
'unknown', 'Branch.set_last_revision_info')
860
branch = self.make_remote_branch(transport, client)
861
class StubRealBranch(object):
864
def set_last_revision_info(self, revno, revision_id):
866
('set_last_revision_info', revno, revision_id))
867
def _clear_cached_state(self):
869
real_branch = StubRealBranch()
870
branch._real_branch = real_branch
871
self.lock_remote_branch(branch)
873
# Call set_last_revision_info, and verify it behaved as expected.
874
result = branch.set_last_revision_info(1234, 'a-revision-id')
876
[('set_last_revision_info', 1234, 'a-revision-id')],
878
client.finished_test()
880
def test_unexpected_error(self):
881
# If the server sends an error the client doesn't understand, it gets
882
# turned into an UnknownErrorFromSmartServer, which is presented as a
883
# non-internal error to the user.
884
transport = MemoryTransport()
885
transport.mkdir('branch')
886
transport = transport.clone('branch')
887
client = FakeClient(transport.base)
889
client.add_error_response('NotStacked')
891
client.add_success_response('ok', 'branch token', 'repo token')
893
client.add_error_response('UnexpectedError')
895
client.add_success_response('ok')
897
branch = self.make_remote_branch(transport, client)
898
# Lock the branch, reset the record of remote calls.
902
err = self.assertRaises(
903
errors.UnknownErrorFromSmartServer,
904
branch.set_last_revision_info, 123, 'revid')
905
self.assertEqual(('UnexpectedError',), err.error_tuple)
908
def test_tip_change_rejected(self):
909
"""TipChangeRejected responses cause a TipChangeRejected exception to
912
transport = MemoryTransport()
913
transport.mkdir('branch')
914
transport = transport.clone('branch')
915
client = FakeClient(transport.base)
917
client.add_error_response('NotStacked')
919
client.add_success_response('ok', 'branch token', 'repo token')
921
client.add_error_response('TipChangeRejected', 'rejection message')
923
client.add_success_response('ok')
925
branch = self.make_remote_branch(transport, client)
926
# Lock the branch, reset the record of remote calls.
928
self.addCleanup(branch.unlock)
931
# The 'TipChangeRejected' error response triggered by calling
932
# set_last_revision_info causes a TipChangeRejected exception.
933
err = self.assertRaises(
934
errors.TipChangeRejected,
935
branch.set_last_revision_info, 123, 'revid')
936
self.assertEqual('rejection message', err.msg)
939
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
940
"""Getting the branch configuration should use an abstract method not vfs.
943
def test_get_branch_conf(self):
944
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
945
## # We should see that branch.get_config() does a single rpc to get the
946
## # remote configuration file, abstracting away where that is stored on
947
## # the server. However at the moment it always falls back to using the
948
## # vfs, and this would need some changes in config.py.
950
## # in an empty branch we decode the response properly
951
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
952
## # we need to make a real branch because the remote_branch.control_files
953
## # will trigger _ensure_real.
954
## branch = self.make_branch('quack')
955
## transport = branch.bzrdir.root_transport
956
## # we do not want bzrdir to make any remote calls
957
## bzrdir = RemoteBzrDir(transport, _client=False)
958
## branch = RemoteBranch(bzrdir, None, _client=client)
959
## config = branch.get_config()
961
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
965
class TestBranchLockWrite(RemoteBranchTestCase):
967
def test_lock_write_unlockable(self):
968
transport = MemoryTransport()
969
client = FakeClient(transport.base)
970
client.add_expected_call(
971
'Branch.get_stacked_on_url', ('quack/',),
972
'error', ('NotStacked',),)
973
client.add_expected_call(
974
'Branch.lock_write', ('quack/', '', ''),
975
'error', ('UnlockableTransport',))
976
transport.mkdir('quack')
977
transport = transport.clone('quack')
978
branch = self.make_remote_branch(transport, client)
979
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
980
client.finished_test()
983
class TestTransportIsReadonly(tests.TestCase):
986
client = FakeClient()
987
client.add_success_response('yes')
988
transport = RemoteTransport('bzr://example.com/', medium=False,
990
self.assertEqual(True, transport.is_readonly())
992
[('call', 'Transport.is_readonly', ())],
995
def test_false(self):
996
client = FakeClient()
997
client.add_success_response('no')
998
transport = RemoteTransport('bzr://example.com/', medium=False,
1000
self.assertEqual(False, transport.is_readonly())
1002
[('call', 'Transport.is_readonly', ())],
1005
def test_error_from_old_server(self):
1006
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1008
Clients should treat it as a "no" response, because is_readonly is only
1009
advisory anyway (a transport could be read-write, but then the
1010
underlying filesystem could be readonly anyway).
1012
client = FakeClient()
1013
client.add_unknown_method_response('Transport.is_readonly')
1014
transport = RemoteTransport('bzr://example.com/', medium=False,
1016
self.assertEqual(False, transport.is_readonly())
1018
[('call', 'Transport.is_readonly', ())],
1022
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1024
def test_defaults_to_none(self):
1025
t = RemoteSSHTransport('bzr+ssh://example.com')
1026
self.assertIs(None, t._get_credentials()[0])
1028
def test_uses_authentication_config(self):
1029
conf = config.AuthenticationConfig()
1030
conf._get_config().update(
1031
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1034
t = RemoteSSHTransport('bzr+ssh://example.com')
1035
self.assertEqual('bar', t._get_credentials()[0])
1038
class TestRemoteRepository(tests.TestCase):
1039
"""Base for testing RemoteRepository protocol usage.
1041
These tests contain frozen requests and responses. We want any changes to
1042
what is sent or expected to be require a thoughtful update to these tests
1043
because they might break compatibility with different-versioned servers.
1046
def setup_fake_client_and_repository(self, transport_path):
1047
"""Create the fake client and repository for testing with.
1049
There's no real server here; we just have canned responses sent
1052
:param transport_path: Path below the root of the MemoryTransport
1053
where the repository will be created.
1055
transport = MemoryTransport()
1056
transport.mkdir(transport_path)
1057
client = FakeClient(transport.base)
1058
transport = transport.clone(transport_path)
1059
# we do not want bzrdir to make any remote calls
1060
bzrdir = RemoteBzrDir(transport, _client=False)
1061
repo = RemoteRepository(bzrdir, None, _client=client)
1065
class TestRepositoryGatherStats(TestRemoteRepository):
1067
def test_revid_none(self):
1068
# ('ok',), body with revisions and size
1069
transport_path = 'quack'
1070
repo, client = self.setup_fake_client_and_repository(transport_path)
1071
client.add_success_response_with_body(
1072
'revisions: 2\nsize: 18\n', 'ok')
1073
result = repo.gather_stats(None)
1075
[('call_expecting_body', 'Repository.gather_stats',
1076
('quack/','','no'))],
1078
self.assertEqual({'revisions': 2, 'size': 18}, result)
1080
def test_revid_no_committers(self):
1081
# ('ok',), body without committers
1082
body = ('firstrev: 123456.300 3600\n'
1083
'latestrev: 654231.400 0\n'
1086
transport_path = 'quick'
1087
revid = u'\xc8'.encode('utf8')
1088
repo, client = self.setup_fake_client_and_repository(transport_path)
1089
client.add_success_response_with_body(body, 'ok')
1090
result = repo.gather_stats(revid)
1092
[('call_expecting_body', 'Repository.gather_stats',
1093
('quick/', revid, 'no'))],
1095
self.assertEqual({'revisions': 2, 'size': 18,
1096
'firstrev': (123456.300, 3600),
1097
'latestrev': (654231.400, 0),},
1100
def test_revid_with_committers(self):
1101
# ('ok',), body with committers
1102
body = ('committers: 128\n'
1103
'firstrev: 123456.300 3600\n'
1104
'latestrev: 654231.400 0\n'
1107
transport_path = 'buick'
1108
revid = u'\xc8'.encode('utf8')
1109
repo, client = self.setup_fake_client_and_repository(transport_path)
1110
client.add_success_response_with_body(body, 'ok')
1111
result = repo.gather_stats(revid, True)
1113
[('call_expecting_body', 'Repository.gather_stats',
1114
('buick/', revid, 'yes'))],
1116
self.assertEqual({'revisions': 2, 'size': 18,
1118
'firstrev': (123456.300, 3600),
1119
'latestrev': (654231.400, 0),},
1123
class TestRepositoryGetGraph(TestRemoteRepository):
1125
def test_get_graph(self):
1126
# get_graph returns a graph with the repository as the
1128
transport_path = 'quack'
1129
repo, client = self.setup_fake_client_and_repository(transport_path)
1130
graph = repo.get_graph()
1131
self.assertEqual(graph._parents_provider, repo)
1134
class TestRepositoryGetParentMap(TestRemoteRepository):
1136
def test_get_parent_map_caching(self):
1137
# get_parent_map returns from cache until unlock()
1138
# setup a reponse with two revisions
1139
r1 = u'\u0e33'.encode('utf8')
1140
r2 = u'\u0dab'.encode('utf8')
1141
lines = [' '.join([r2, r1]), r1]
1142
encoded_body = bz2.compress('\n'.join(lines))
1144
transport_path = 'quack'
1145
repo, client = self.setup_fake_client_and_repository(transport_path)
1146
client.add_success_response_with_body(encoded_body, 'ok')
1147
client.add_success_response_with_body(encoded_body, 'ok')
1149
graph = repo.get_graph()
1150
parents = graph.get_parent_map([r2])
1151
self.assertEqual({r2: (r1,)}, parents)
1152
# locking and unlocking deeper should not reset
1155
parents = graph.get_parent_map([r1])
1156
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1158
[('call_with_body_bytes_expecting_body',
1159
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1162
# now we call again, and it should use the second response.
1164
graph = repo.get_graph()
1165
parents = graph.get_parent_map([r1])
1166
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1168
[('call_with_body_bytes_expecting_body',
1169
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1170
('call_with_body_bytes_expecting_body',
1171
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1176
def test_get_parent_map_reconnects_if_unknown_method(self):
1177
transport_path = 'quack'
1178
repo, client = self.setup_fake_client_and_repository(transport_path)
1179
client.add_unknown_method_response('Repository,get_parent_map')
1180
client.add_success_response_with_body('', 'ok')
1181
self.assertFalse(client._medium._is_remote_before((1, 2)))
1182
rev_id = 'revision-id'
1183
expected_deprecations = [
1184
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1186
parents = self.callDeprecated(
1187
expected_deprecations, repo.get_parent_map, [rev_id])
1189
[('call_with_body_bytes_expecting_body',
1190
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1191
('disconnect medium',),
1192
('call_expecting_body', 'Repository.get_revision_graph',
1195
# The medium is now marked as being connected to an older server
1196
self.assertTrue(client._medium._is_remote_before((1, 2)))
1198
def test_get_parent_map_fallback_parentless_node(self):
1199
"""get_parent_map falls back to get_revision_graph on old servers. The
1200
results from get_revision_graph are tweaked to match the get_parent_map
1203
Specifically, a {key: ()} result from get_revision_graph means "no
1204
parents" for that key, which in get_parent_map results should be
1205
represented as {key: ('null:',)}.
1207
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1209
rev_id = 'revision-id'
1210
transport_path = 'quack'
1211
repo, client = self.setup_fake_client_and_repository(transport_path)
1212
client.add_success_response_with_body(rev_id, 'ok')
1213
client._medium._remember_remote_is_before((1, 2))
1214
expected_deprecations = [
1215
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1217
parents = self.callDeprecated(
1218
expected_deprecations, repo.get_parent_map, [rev_id])
1220
[('call_expecting_body', 'Repository.get_revision_graph',
1223
self.assertEqual({rev_id: ('null:',)}, parents)
1225
def test_get_parent_map_unexpected_response(self):
1226
repo, client = self.setup_fake_client_and_repository('path')
1227
client.add_success_response('something unexpected!')
1229
errors.UnexpectedSmartServerResponse,
1230
repo.get_parent_map, ['a-revision-id'])
1233
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1235
def test_null_revision(self):
1236
# a null revision has the predictable result {}, we should have no wire
1237
# traffic when calling it with this argument
1238
transport_path = 'empty'
1239
repo, client = self.setup_fake_client_and_repository(transport_path)
1240
client.add_success_response('notused')
1241
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1243
self.assertEqual([], client._calls)
1244
self.assertEqual({}, result)
1246
def test_none_revision(self):
1247
# with none we want the entire graph
1248
r1 = u'\u0e33'.encode('utf8')
1249
r2 = u'\u0dab'.encode('utf8')
1250
lines = [' '.join([r2, r1]), r1]
1251
encoded_body = '\n'.join(lines)
1253
transport_path = 'sinhala'
1254
repo, client = self.setup_fake_client_and_repository(transport_path)
1255
client.add_success_response_with_body(encoded_body, 'ok')
1256
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1258
[('call_expecting_body', 'Repository.get_revision_graph',
1261
self.assertEqual({r1: (), r2: (r1, )}, result)
1263
def test_specific_revision(self):
1264
# with a specific revision we want the graph for that
1265
# with none we want the entire graph
1266
r11 = u'\u0e33'.encode('utf8')
1267
r12 = u'\xc9'.encode('utf8')
1268
r2 = u'\u0dab'.encode('utf8')
1269
lines = [' '.join([r2, r11, r12]), r11, r12]
1270
encoded_body = '\n'.join(lines)
1272
transport_path = 'sinhala'
1273
repo, client = self.setup_fake_client_and_repository(transport_path)
1274
client.add_success_response_with_body(encoded_body, 'ok')
1275
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1277
[('call_expecting_body', 'Repository.get_revision_graph',
1280
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1282
def test_no_such_revision(self):
1284
transport_path = 'sinhala'
1285
repo, client = self.setup_fake_client_and_repository(transport_path)
1286
client.add_error_response('nosuchrevision', revid)
1287
# also check that the right revision is reported in the error
1288
self.assertRaises(errors.NoSuchRevision,
1289
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1291
[('call_expecting_body', 'Repository.get_revision_graph',
1292
('sinhala/', revid))],
1295
def test_unexpected_error(self):
1297
transport_path = 'sinhala'
1298
repo, client = self.setup_fake_client_and_repository(transport_path)
1299
client.add_error_response('AnUnexpectedError')
1300
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1301
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1302
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1305
class TestRepositoryIsShared(TestRemoteRepository):
1307
def test_is_shared(self):
1308
# ('yes', ) for Repository.is_shared -> 'True'.
1309
transport_path = 'quack'
1310
repo, client = self.setup_fake_client_and_repository(transport_path)
1311
client.add_success_response('yes')
1312
result = repo.is_shared()
1314
[('call', 'Repository.is_shared', ('quack/',))],
1316
self.assertEqual(True, result)
1318
def test_is_not_shared(self):
1319
# ('no', ) for Repository.is_shared -> 'False'.
1320
transport_path = 'qwack'
1321
repo, client = self.setup_fake_client_and_repository(transport_path)
1322
client.add_success_response('no')
1323
result = repo.is_shared()
1325
[('call', 'Repository.is_shared', ('qwack/',))],
1327
self.assertEqual(False, result)
1330
class TestRepositoryLockWrite(TestRemoteRepository):
1332
def test_lock_write(self):
1333
transport_path = 'quack'
1334
repo, client = self.setup_fake_client_and_repository(transport_path)
1335
client.add_success_response('ok', 'a token')
1336
result = repo.lock_write()
1338
[('call', 'Repository.lock_write', ('quack/', ''))],
1340
self.assertEqual('a token', result)
1342
def test_lock_write_already_locked(self):
1343
transport_path = 'quack'
1344
repo, client = self.setup_fake_client_and_repository(transport_path)
1345
client.add_error_response('LockContention')
1346
self.assertRaises(errors.LockContention, repo.lock_write)
1348
[('call', 'Repository.lock_write', ('quack/', ''))],
1351
def test_lock_write_unlockable(self):
1352
transport_path = 'quack'
1353
repo, client = self.setup_fake_client_and_repository(transport_path)
1354
client.add_error_response('UnlockableTransport')
1355
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1357
[('call', 'Repository.lock_write', ('quack/', ''))],
1361
class TestRepositoryUnlock(TestRemoteRepository):
1363
def test_unlock(self):
1364
transport_path = 'quack'
1365
repo, client = self.setup_fake_client_and_repository(transport_path)
1366
client.add_success_response('ok', 'a token')
1367
client.add_success_response('ok')
1371
[('call', 'Repository.lock_write', ('quack/', '')),
1372
('call', 'Repository.unlock', ('quack/', 'a token'))],
1375
def test_unlock_wrong_token(self):
1376
# If somehow the token is wrong, unlock will raise TokenMismatch.
1377
transport_path = 'quack'
1378
repo, client = self.setup_fake_client_and_repository(transport_path)
1379
client.add_success_response('ok', 'a token')
1380
client.add_error_response('TokenMismatch')
1382
self.assertRaises(errors.TokenMismatch, repo.unlock)
1385
class TestRepositoryHasRevision(TestRemoteRepository):
1387
def test_none(self):
1388
# repo.has_revision(None) should not cause any traffic.
1389
transport_path = 'quack'
1390
repo, client = self.setup_fake_client_and_repository(transport_path)
1392
# The null revision is always there, so has_revision(None) == True.
1393
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1395
# The remote repo shouldn't be accessed.
1396
self.assertEqual([], client._calls)
1399
class TestRepositoryTarball(TestRemoteRepository):
1401
# This is a canned tarball reponse we can validate against
1403
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1404
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1405
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1406
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1407
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1408
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1409
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1410
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1411
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1412
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1413
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1414
'nWQ7QH/F3JFOFCQ0aSPfA='
1417
def test_repository_tarball(self):
1418
# Test that Repository.tarball generates the right operations
1419
transport_path = 'repo'
1420
expected_calls = [('call_expecting_body', 'Repository.tarball',
1421
('repo/', 'bz2',),),
1423
repo, client = self.setup_fake_client_and_repository(transport_path)
1424
client.add_success_response_with_body(self.tarball_content, 'ok')
1425
# Now actually ask for the tarball
1426
tarball_file = repo._get_tarball('bz2')
1428
self.assertEqual(expected_calls, client._calls)
1429
self.assertEqual(self.tarball_content, tarball_file.read())
1431
tarball_file.close()
1434
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1435
"""RemoteRepository.copy_content_into optimizations"""
1437
def test_copy_content_remote_to_local(self):
1438
self.transport_server = server.SmartTCPServer_for_testing
1439
src_repo = self.make_repository('repo1')
1440
src_repo = repository.Repository.open(self.get_url('repo1'))
1441
# At the moment the tarball-based copy_content_into can't write back
1442
# into a smart server. It would be good if it could upload the
1443
# tarball; once that works we'd have to create repositories of
1444
# different formats. -- mbp 20070410
1445
dest_url = self.get_vfs_only_url('repo2')
1446
dest_bzrdir = BzrDir.create(dest_url)
1447
dest_repo = dest_bzrdir.create_repository()
1448
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1449
self.assertTrue(isinstance(src_repo, RemoteRepository))
1450
src_repo.copy_content_into(dest_repo)
1453
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1454
"""Base class for unit tests for bzrlib.remote._translate_error."""
1456
def translateTuple(self, error_tuple, **context):
1457
"""Call _translate_error with an ErrorFromSmartServer built from the
1460
:param error_tuple: A tuple of a smart server response, as would be
1461
passed to an ErrorFromSmartServer.
1462
:kwargs context: context items to call _translate_error with.
1464
:returns: The error raised by _translate_error.
1466
# Raise the ErrorFromSmartServer before passing it as an argument,
1467
# because _translate_error may need to re-raise it with a bare 'raise'
1469
server_error = errors.ErrorFromSmartServer(error_tuple)
1470
translated_error = self.translateErrorFromSmartServer(
1471
server_error, **context)
1472
return translated_error
1474
def translateErrorFromSmartServer(self, error_object, **context):
1475
"""Like translateTuple, but takes an already constructed
1476
ErrorFromSmartServer rather than a tuple.
1480
except errors.ErrorFromSmartServer, server_error:
1481
translated_error = self.assertRaises(
1482
errors.BzrError, remote._translate_error, server_error,
1484
return translated_error
1487
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1488
"""Unit tests for bzrlib.remote._translate_error.
1490
Given an ErrorFromSmartServer (which has an error tuple from a smart
1491
server) and some context, _translate_error raises more specific errors from
1494
This test case covers the cases where _translate_error succeeds in
1495
translating an ErrorFromSmartServer to something better. See
1496
TestErrorTranslationRobustness for other cases.
1499
def test_NoSuchRevision(self):
1500
branch = self.make_branch('')
1502
translated_error = self.translateTuple(
1503
('NoSuchRevision', revid), branch=branch)
1504
expected_error = errors.NoSuchRevision(branch, revid)
1505
self.assertEqual(expected_error, translated_error)
1507
def test_nosuchrevision(self):
1508
repository = self.make_repository('')
1510
translated_error = self.translateTuple(
1511
('nosuchrevision', revid), repository=repository)
1512
expected_error = errors.NoSuchRevision(repository, revid)
1513
self.assertEqual(expected_error, translated_error)
1515
def test_nobranch(self):
1516
bzrdir = self.make_bzrdir('')
1517
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1518
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1519
self.assertEqual(expected_error, translated_error)
1521
def test_LockContention(self):
1522
translated_error = self.translateTuple(('LockContention',))
1523
expected_error = errors.LockContention('(remote lock)')
1524
self.assertEqual(expected_error, translated_error)
1526
def test_UnlockableTransport(self):
1527
bzrdir = self.make_bzrdir('')
1528
translated_error = self.translateTuple(
1529
('UnlockableTransport',), bzrdir=bzrdir)
1530
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1531
self.assertEqual(expected_error, translated_error)
1533
def test_LockFailed(self):
1534
lock = 'str() of a server lock'
1535
why = 'str() of why'
1536
translated_error = self.translateTuple(('LockFailed', lock, why))
1537
expected_error = errors.LockFailed(lock, why)
1538
self.assertEqual(expected_error, translated_error)
1540
def test_TokenMismatch(self):
1541
token = 'a lock token'
1542
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1543
expected_error = errors.TokenMismatch(token, '(remote token)')
1544
self.assertEqual(expected_error, translated_error)
1546
def test_Diverged(self):
1547
branch = self.make_branch('a')
1548
other_branch = self.make_branch('b')
1549
translated_error = self.translateTuple(
1550
('Diverged',), branch=branch, other_branch=other_branch)
1551
expected_error = errors.DivergedBranches(branch, other_branch)
1552
self.assertEqual(expected_error, translated_error)
1555
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1556
"""Unit tests for bzrlib.remote._translate_error's robustness.
1558
TestErrorTranslationSuccess is for cases where _translate_error can
1559
translate successfully. This class about how _translate_err behaves when
1560
it fails to translate: it re-raises the original error.
1563
def test_unrecognised_server_error(self):
1564
"""If the error code from the server is not recognised, the original
1565
ErrorFromSmartServer is propagated unmodified.
1567
error_tuple = ('An unknown error tuple',)
1568
server_error = errors.ErrorFromSmartServer(error_tuple)
1569
translated_error = self.translateErrorFromSmartServer(server_error)
1570
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1571
self.assertEqual(expected_error, translated_error)
1573
def test_context_missing_a_key(self):
1574
"""In case of a bug in the client, or perhaps an unexpected response
1575
from a server, _translate_error returns the original error tuple from
1576
the server and mutters a warning.
1578
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1579
# in the context dict. So let's give it an empty context dict instead
1580
# to exercise its error recovery.
1582
error_tuple = ('NoSuchRevision', 'revid')
1583
server_error = errors.ErrorFromSmartServer(error_tuple)
1584
translated_error = self.translateErrorFromSmartServer(server_error)
1585
self.assertEqual(server_error, translated_error)
1586
# In addition to re-raising ErrorFromSmartServer, some debug info has
1587
# been muttered to the log file for developer to look at.
1588
self.assertContainsRe(
1589
self._get_log(keep_log_file=True),
1590
"Missing key 'branch' in context")
1593
class TestStacking(tests.TestCaseWithTransport):
1594
"""Tests for operations on stacked remote repositories.
1596
The underlying format type must support stacking.
1599
def test_access_stacked_remote(self):
1600
# based on <http://launchpad.net/bugs/261315>
1601
# make a branch stacked on another repository containing an empty
1602
# revision, then open it over hpss - we should be able to see that
1604
base_transport = self.get_transport()
1605
base_builder = self.make_branch_builder('base', format='1.6')
1606
base_builder.start_series()
1607
base_revid = base_builder.build_snapshot('rev-id', None,
1608
[('add', ('', None, 'directory', None))],
1610
base_builder.finish_series()
1611
stacked_branch = self.make_branch('stacked', format='1.6')
1612
stacked_branch.set_stacked_on_url('../base')
1613
# start a server looking at this
1614
smart_server = server.SmartTCPServer_for_testing()
1615
smart_server.setUp()
1616
self.addCleanup(smart_server.tearDown)
1617
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1618
# can get its branch and repository
1619
remote_branch = remote_bzrdir.open_branch()
1620
remote_repo = remote_branch.repository
1621
remote_repo.lock_read()
1623
# it should have an appropriate fallback repository, which should also
1624
# be a RemoteRepository
1625
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1626
self.assertIsInstance(remote_repo._fallback_repositories[0],
1628
# and it has the revision committed to the underlying repository;
1629
# these have varying implementations so we try several of them
1630
self.assertTrue(remote_repo.has_revisions([base_revid]))
1631
self.assertTrue(remote_repo.has_revision(base_revid))
1632
self.assertEqual(remote_repo.get_revision(base_revid).message,
1635
remote_repo.unlock()