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."""
177
self.expecting_body = False
178
# if non-None, this is the list of expected calls, with only the
179
# method name and arguments included. the body might be hard to
180
# compute so is not included
181
self._expected_calls = None
182
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
184
def add_expected_call(self, call_name, call_args, response_type,
185
response_args, response_body=None):
186
if self._expected_calls is None:
187
self._expected_calls = []
188
self._expected_calls.append((call_name, call_args))
189
self.responses.append((response_type, response_args, response_body))
191
def add_success_response(self, *args):
192
self.responses.append(('success', args, None))
194
def add_success_response_with_body(self, body, *args):
195
self.responses.append(('success', args, body))
197
def add_error_response(self, *args):
198
self.responses.append(('error', args))
200
def add_unknown_method_response(self, verb):
201
self.responses.append(('unknown', verb))
203
def finished_test(self):
204
if self._expected_calls:
205
raise AssertionError("%r finished but was still expecting %r"
206
% (self, self._expected_calls[0]))
208
def _get_next_response(self):
210
response_tuple = self.responses.pop(0)
211
except IndexError, e:
212
raise AssertionError("%r didn't expect any more calls"
214
if response_tuple[0] == 'unknown':
215
raise errors.UnknownSmartMethod(response_tuple[1])
216
elif response_tuple[0] == 'error':
217
raise errors.ErrorFromSmartServer(response_tuple[1])
218
return response_tuple
220
def _check_call(self, method, args):
221
if self._expected_calls is None:
222
# the test should be updated to say what it expects
225
next_call = self._expected_calls.pop(0)
227
raise AssertionError("%r didn't expect any more calls "
229
% (self, method, args,))
230
if method != next_call[0] or args != next_call[1]:
231
raise AssertionError("%r expected %r%r "
233
% (self, next_call[0], next_call[1], method, args,))
235
def call(self, method, *args):
236
self._check_call(method, args)
237
self._calls.append(('call', method, args))
238
return self._get_next_response()[1]
240
def call_expecting_body(self, method, *args):
241
self._check_call(method, args)
242
self._calls.append(('call_expecting_body', method, args))
243
result = self._get_next_response()
244
self.expecting_body = True
245
return result[1], FakeProtocol(result[2], self)
247
def call_with_body_bytes_expecting_body(self, method, args, body):
248
self._check_call(method, args)
249
self._calls.append(('call_with_body_bytes_expecting_body', method,
251
result = self._get_next_response()
252
self.expecting_body = True
253
return result[1], FakeProtocol(result[2], self)
256
class FakeMedium(medium.SmartClientMedium):
258
def __init__(self, client_calls, base):
259
medium.SmartClientMedium.__init__(self, base)
260
self._client_calls = client_calls
262
def disconnect(self):
263
self._client_calls.append(('disconnect medium',))
266
class TestVfsHas(tests.TestCase):
268
def test_unicode_path(self):
269
client = FakeClient('/')
270
client.add_success_response('yes',)
271
transport = RemoteTransport('bzr://localhost/', _client=client)
272
filename = u'/hell\u00d8'.encode('utf8')
273
result = transport.has(filename)
275
[('call', 'has', (filename,))],
277
self.assertTrue(result)
280
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
281
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
283
def assertRemotePath(self, expected, client_base, transport_base):
284
"""Assert that the result of
285
SmartClientMedium.remote_path_from_transport is the expected value for
286
a given client_base and transport_base.
288
client_medium = medium.SmartClientMedium(client_base)
289
transport = get_transport(transport_base)
290
result = client_medium.remote_path_from_transport(transport)
291
self.assertEqual(expected, result)
293
def test_remote_path_from_transport(self):
294
"""SmartClientMedium.remote_path_from_transport calculates a URL for
295
the given transport relative to the root of the client base URL.
297
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
298
self.assertRemotePath(
299
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
301
def assertRemotePathHTTP(self, expected, transport_base, relpath):
302
"""Assert that the result of
303
HttpTransportBase.remote_path_from_transport is the expected value for
304
a given transport_base and relpath of that transport. (Note that
305
HttpTransportBase is a subclass of SmartClientMedium)
307
base_transport = get_transport(transport_base)
308
client_medium = base_transport.get_smart_medium()
309
cloned_transport = base_transport.clone(relpath)
310
result = client_medium.remote_path_from_transport(cloned_transport)
311
self.assertEqual(expected, result)
313
def test_remote_path_from_transport_http(self):
314
"""Remote paths for HTTP transports are calculated differently to other
315
transports. They are just relative to the client base, not the root
316
directory of the host.
318
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
319
self.assertRemotePathHTTP(
320
'../xyz/', scheme + '//host/path', '../xyz/')
321
self.assertRemotePathHTTP(
322
'xyz/', scheme + '//host/path', 'xyz/')
325
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
326
"""Tests for the behaviour of client_medium.remote_is_at_least."""
328
def test_initially_unlimited(self):
329
"""A fresh medium assumes that the remote side supports all
332
client_medium = medium.SmartClientMedium('dummy base')
333
self.assertFalse(client_medium._is_remote_before((99, 99)))
335
def test__remember_remote_is_before(self):
336
"""Calling _remember_remote_is_before ratchets down the known remote
339
client_medium = medium.SmartClientMedium('dummy base')
340
# Mark the remote side as being less than 1.6. The remote side may
342
client_medium._remember_remote_is_before((1, 6))
343
self.assertTrue(client_medium._is_remote_before((1, 6)))
344
self.assertFalse(client_medium._is_remote_before((1, 5)))
345
# Calling _remember_remote_is_before again with a lower value works.
346
client_medium._remember_remote_is_before((1, 5))
347
self.assertTrue(client_medium._is_remote_before((1, 5)))
348
# You cannot call _remember_remote_is_before with a larger value.
350
AssertionError, client_medium._remember_remote_is_before, (1, 9))
353
class TestBzrDirOpenBranch(tests.TestCase):
355
def test_branch_present(self):
356
transport = MemoryTransport()
357
transport.mkdir('quack')
358
transport = transport.clone('quack')
359
client = FakeClient(transport.base)
360
client.add_expected_call(
361
'BzrDir.open_branch', ('quack/',),
362
'success', ('ok', ''))
363
client.add_expected_call(
364
'BzrDir.find_repositoryV2', ('quack/',),
365
'success', ('ok', '', 'no', 'no', 'no'))
366
client.add_expected_call(
367
'Branch.get_stacked_on_url', ('quack/',),
368
'error', ('NotStacked',))
369
bzrdir = RemoteBzrDir(transport, _client=client)
370
result = bzrdir.open_branch()
371
self.assertIsInstance(result, RemoteBranch)
372
self.assertEqual(bzrdir, result.bzrdir)
373
client.finished_test()
375
def test_branch_missing(self):
376
transport = MemoryTransport()
377
transport.mkdir('quack')
378
transport = transport.clone('quack')
379
client = FakeClient(transport.base)
380
client.add_error_response('nobranch')
381
bzrdir = RemoteBzrDir(transport, _client=client)
382
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
384
[('call', 'BzrDir.open_branch', ('quack/',))],
387
def test__get_tree_branch(self):
388
# _get_tree_branch is a form of open_branch, but it should only ask for
389
# branch opening, not any other network requests.
392
calls.append("Called")
394
transport = MemoryTransport()
395
# no requests on the network - catches other api calls being made.
396
client = FakeClient(transport.base)
397
bzrdir = RemoteBzrDir(transport, _client=client)
398
# patch the open_branch call to record that it was called.
399
bzrdir.open_branch = open_branch
400
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
401
self.assertEqual(["Called"], calls)
402
self.assertEqual([], client._calls)
404
def test_url_quoting_of_path(self):
405
# Relpaths on the wire should not be URL-escaped. So "~" should be
406
# transmitted as "~", not "%7E".
407
transport = RemoteTCPTransport('bzr://localhost/~hello/')
408
client = FakeClient(transport.base)
409
client.add_expected_call(
410
'BzrDir.open_branch', ('~hello/',),
411
'success', ('ok', ''))
412
client.add_expected_call(
413
'BzrDir.find_repositoryV2', ('~hello/',),
414
'success', ('ok', '', 'no', 'no', 'no'))
415
client.add_expected_call(
416
'Branch.get_stacked_on_url', ('~hello/',),
417
'error', ('NotStacked',))
418
bzrdir = RemoteBzrDir(transport, _client=client)
419
result = bzrdir.open_branch()
420
client.finished_test()
422
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
423
transport = MemoryTransport()
424
transport.mkdir('quack')
425
transport = transport.clone('quack')
427
rich_response = 'yes'
431
subtree_response = 'yes'
433
subtree_response = 'no'
434
client = FakeClient(transport.base)
435
client.add_success_response(
436
'ok', '', rich_response, subtree_response, external_lookup)
437
bzrdir = RemoteBzrDir(transport, _client=client)
438
result = bzrdir.open_repository()
440
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
442
self.assertIsInstance(result, RemoteRepository)
443
self.assertEqual(bzrdir, result.bzrdir)
444
self.assertEqual(rich_root, result._format.rich_root_data)
445
self.assertEqual(subtrees, result._format.supports_tree_reference)
447
def test_open_repository_sets_format_attributes(self):
448
self.check_open_repository(True, True)
449
self.check_open_repository(False, True)
450
self.check_open_repository(True, False)
451
self.check_open_repository(False, False)
452
self.check_open_repository(False, False, 'yes')
454
def test_old_server(self):
455
"""RemoteBzrDirFormat should fail to probe if the server version is too
458
self.assertRaises(errors.NotBranchError,
459
RemoteBzrDirFormat.probe_transport, OldServerTransport())
462
class TestBzrDirOpenRepository(tests.TestCase):
464
def test_backwards_compat_1_2(self):
465
transport = MemoryTransport()
466
transport.mkdir('quack')
467
transport = transport.clone('quack')
468
client = FakeClient(transport.base)
469
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
470
client.add_success_response('ok', '', 'no', 'no')
471
bzrdir = RemoteBzrDir(transport, _client=client)
472
repo = bzrdir.open_repository()
474
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
475
('call', 'BzrDir.find_repository', ('quack/',))],
479
class OldSmartClient(object):
480
"""A fake smart client for test_old_version that just returns a version one
481
response to the 'hello' (query version) command.
484
def get_request(self):
485
input_file = StringIO('ok\x011\n')
486
output_file = StringIO()
487
client_medium = medium.SmartSimplePipesClientMedium(
488
input_file, output_file)
489
return medium.SmartClientStreamMediumRequest(client_medium)
491
def protocol_version(self):
495
class OldServerTransport(object):
496
"""A fake transport for test_old_server that reports it's smart server
497
protocol version as version one.
503
def get_smart_client(self):
504
return OldSmartClient()
507
class RemoteBranchTestCase(tests.TestCase):
509
def make_remote_branch(self, transport, client):
510
"""Make a RemoteBranch using 'client' as its _SmartClient.
512
A RemoteBzrDir and RemoteRepository will also be created to fill out
513
the RemoteBranch, albeit with stub values for some of their attributes.
515
# we do not want bzrdir to make any remote calls, so use False as its
516
# _client. If it tries to make a remote call, this will fail
518
bzrdir = RemoteBzrDir(transport, _client=False)
519
repo = RemoteRepository(bzrdir, None, _client=client)
520
return RemoteBranch(bzrdir, repo, _client=client)
523
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
525
def test_empty_branch(self):
526
# in an empty branch we decode the response properly
527
transport = MemoryTransport()
528
client = FakeClient(transport.base)
529
client.add_expected_call(
530
'Branch.get_stacked_on_url', ('quack/',),
531
'error', ('NotStacked',))
532
client.add_expected_call(
533
'Branch.last_revision_info', ('quack/',),
534
'success', ('ok', '0', 'null:'))
535
transport.mkdir('quack')
536
transport = transport.clone('quack')
537
branch = self.make_remote_branch(transport, client)
538
result = branch.last_revision_info()
539
client.finished_test()
540
self.assertEqual((0, NULL_REVISION), result)
542
def test_non_empty_branch(self):
543
# in a non-empty branch we also decode the response properly
544
revid = u'\xc8'.encode('utf8')
545
transport = MemoryTransport()
546
client = FakeClient(transport.base)
547
client.add_expected_call(
548
'Branch.get_stacked_on_url', ('kwaak/',),
549
'error', ('NotStacked',))
550
client.add_expected_call(
551
'Branch.last_revision_info', ('kwaak/',),
552
'success', ('ok', '2', revid))
553
transport.mkdir('kwaak')
554
transport = transport.clone('kwaak')
555
branch = self.make_remote_branch(transport, client)
556
result = branch.last_revision_info()
557
self.assertEqual((2, revid), result)
560
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
561
"""Test Branch._get_stacked_on_url rpc"""
563
def test_get_stacked_on_invalid_url(self):
564
raise tests.KnownFailure('opening a branch requires the server to open the fallback repository')
565
transport = FakeRemoteTransport('fakeremotetransport:///')
566
client = FakeClient(transport.base)
567
client.add_expected_call(
568
'Branch.get_stacked_on_url', ('.',),
569
'success', ('ok', 'file:///stacked/on'))
570
bzrdir = RemoteBzrDir(transport, _client=client)
571
branch = RemoteBranch(bzrdir, None, _client=client)
572
result = branch.get_stacked_on_url()
574
'file:///stacked/on', result)
576
def test_backwards_compatible(self):
577
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
578
base_branch = self.make_branch('base', format='1.6')
579
stacked_branch = self.make_branch('stacked', format='1.6')
580
stacked_branch.set_stacked_on_url('../base')
581
client = FakeClient(self.get_url())
582
client.add_expected_call(
583
'BzrDir.open_branch', ('stacked/',),
584
'success', ('ok', ''))
585
client.add_expected_call(
586
'BzrDir.find_repositoryV2', ('stacked/',),
587
'success', ('ok', '', 'no', 'no', 'no'))
588
# called twice, once from constructor and then again by us
589
client.add_expected_call(
590
'Branch.get_stacked_on_url', ('stacked/',),
591
'unknown', ('Branch.get_stacked_on_url',))
592
client.add_expected_call(
593
'Branch.get_stacked_on_url', ('stacked/',),
594
'unknown', ('Branch.get_stacked_on_url',))
595
# this will also do vfs access, but that goes direct to the transport
596
# and isn't seen by the FakeClient.
597
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
598
branch = bzrdir.open_branch()
599
result = branch.get_stacked_on_url()
600
self.assertEqual('../base', result)
601
client.finished_test()
602
# it's in the fallback list both for the RemoteRepository and its vfs
604
self.assertEqual(1, len(branch.repository._fallback_repositories))
606
len(branch.repository._real_repository._fallback_repositories))
608
def test_get_stacked_on_real_branch(self):
609
base_branch = self.make_branch('base', format='1.6')
610
stacked_branch = self.make_branch('stacked', format='1.6')
611
stacked_branch.set_stacked_on_url('../base')
612
client = FakeClient(self.get_url())
613
client.add_expected_call(
614
'BzrDir.open_branch', ('stacked/',),
615
'success', ('ok', ''))
616
client.add_expected_call(
617
'BzrDir.find_repositoryV2', ('stacked/',),
618
'success', ('ok', '', 'no', 'no', 'no'))
619
# called twice, once from constructor and then again by us
620
client.add_expected_call(
621
'Branch.get_stacked_on_url', ('stacked/',),
622
'success', ('ok', '../base'))
623
client.add_expected_call(
624
'Branch.get_stacked_on_url', ('stacked/',),
625
'success', ('ok', '../base'))
626
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
627
branch = bzrdir.open_branch()
628
result = branch.get_stacked_on_url()
629
self.assertEqual('../base', result)
630
client.finished_test()
631
# it's in the fallback list both for the RemoteRepository and its vfs
633
self.assertEqual(1, len(branch.repository._fallback_repositories))
635
len(branch.repository._real_repository._fallback_repositories))
638
class TestBranchSetLastRevision(RemoteBranchTestCase):
640
def test_set_empty(self):
641
# set_revision_history([]) is translated to calling
642
# Branch.set_last_revision(path, '') on the wire.
643
transport = MemoryTransport()
644
transport.mkdir('branch')
645
transport = transport.clone('branch')
647
client = FakeClient(transport.base)
648
client.add_expected_call(
649
'Branch.get_stacked_on_url', ('branch/',),
650
'error', ('NotStacked',))
651
client.add_expected_call(
652
'Branch.lock_write', ('branch/', '', ''),
653
'success', ('ok', 'branch token', 'repo token'))
654
client.add_expected_call(
655
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
657
client.add_expected_call(
658
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
660
branch = self.make_remote_branch(transport, client)
661
# This is a hack to work around the problem that RemoteBranch currently
662
# unnecessarily invokes _ensure_real upon a call to lock_write.
663
branch._ensure_real = lambda: None
665
result = branch.set_revision_history([])
667
self.assertEqual(None, result)
668
client.finished_test()
670
def test_set_nonempty(self):
671
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
672
# Branch.set_last_revision(path, rev-idN) on the wire.
673
transport = MemoryTransport()
674
transport.mkdir('branch')
675
transport = transport.clone('branch')
677
client = FakeClient(transport.base)
678
client.add_expected_call(
679
'Branch.get_stacked_on_url', ('branch/',),
680
'error', ('NotStacked',))
681
client.add_expected_call(
682
'Branch.lock_write', ('branch/', '', ''),
683
'success', ('ok', 'branch token', 'repo token'))
684
client.add_expected_call(
685
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
687
client.add_expected_call(
688
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
690
branch = self.make_remote_branch(transport, client)
691
# This is a hack to work around the problem that RemoteBranch currently
692
# unnecessarily invokes _ensure_real upon a call to lock_write.
693
branch._ensure_real = lambda: None
694
# Lock the branch, reset the record of remote calls.
696
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
698
self.assertEqual(None, result)
699
client.finished_test()
701
def test_no_such_revision(self):
702
transport = MemoryTransport()
703
transport.mkdir('branch')
704
transport = transport.clone('branch')
705
# A response of 'NoSuchRevision' is translated into an exception.
706
client = FakeClient(transport.base)
707
client.add_expected_call(
708
'Branch.get_stacked_on_url', ('branch/',),
709
'error', ('NotStacked',))
710
client.add_expected_call(
711
'Branch.lock_write', ('branch/', '', ''),
712
'success', ('ok', 'branch token', 'repo token'))
713
client.add_expected_call(
714
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
715
'error', ('NoSuchRevision', 'rev-id'))
716
client.add_expected_call(
717
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
720
branch = self.make_remote_branch(transport, client)
723
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
725
client.finished_test()
727
def test_tip_change_rejected(self):
728
"""TipChangeRejected responses cause a TipChangeRejected exception to
731
transport = MemoryTransport()
732
transport.mkdir('branch')
733
transport = transport.clone('branch')
734
client = FakeClient(transport.base)
735
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
736
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
737
client.add_expected_call(
738
'Branch.get_stacked_on_url', ('branch/',),
739
'error', ('NotStacked',))
740
client.add_expected_call(
741
'Branch.lock_write', ('branch/', '', ''),
742
'success', ('ok', 'branch token', 'repo token'))
743
client.add_expected_call(
744
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
745
'error', ('TipChangeRejected', rejection_msg_utf8))
746
client.add_expected_call(
747
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
749
branch = self.make_remote_branch(transport, client)
750
branch._ensure_real = lambda: None
752
self.addCleanup(branch.unlock)
753
# The 'TipChangeRejected' error response triggered by calling
754
# set_revision_history causes a TipChangeRejected exception.
755
err = self.assertRaises(
756
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
757
# The UTF-8 message from the response has been decoded into a unicode
759
self.assertIsInstance(err.msg, unicode)
760
self.assertEqual(rejection_msg_unicode, err.msg)
762
client.finished_test()
765
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
767
def test_set_last_revision_info(self):
768
# set_last_revision_info(num, 'rev-id') is translated to calling
769
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
770
transport = MemoryTransport()
771
transport.mkdir('branch')
772
transport = transport.clone('branch')
773
client = FakeClient(transport.base)
775
client.add_error_response('NotStacked')
777
client.add_success_response('ok', 'branch token', 'repo token')
779
client.add_success_response('ok')
781
client.add_success_response('ok')
783
branch = self.make_remote_branch(transport, client)
784
# Lock the branch, reset the record of remote calls.
787
result = branch.set_last_revision_info(1234, 'a-revision-id')
789
[('call', 'Branch.set_last_revision_info',
790
('branch/', 'branch token', 'repo token',
791
'1234', 'a-revision-id'))],
793
self.assertEqual(None, result)
795
def test_no_such_revision(self):
796
# A response of 'NoSuchRevision' is translated into an exception.
797
transport = MemoryTransport()
798
transport.mkdir('branch')
799
transport = transport.clone('branch')
800
client = FakeClient(transport.base)
802
client.add_error_response('NotStacked')
804
client.add_success_response('ok', 'branch token', 'repo token')
806
client.add_error_response('NoSuchRevision', 'revid')
808
client.add_success_response('ok')
810
branch = self.make_remote_branch(transport, client)
811
# Lock the branch, reset the record of remote calls.
816
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
819
def lock_remote_branch(self, branch):
820
"""Trick a RemoteBranch into thinking it is locked."""
821
branch._lock_mode = 'w'
822
branch._lock_count = 2
823
branch._lock_token = 'branch token'
824
branch._repo_lock_token = 'repo token'
825
branch.repository._lock_mode = 'w'
826
branch.repository._lock_count = 2
827
branch.repository._lock_token = 'repo token'
829
def test_backwards_compatibility(self):
830
"""If the server does not support the Branch.set_last_revision_info
831
verb (which is new in 1.4), then the client falls back to VFS methods.
833
# This test is a little messy. Unlike most tests in this file, it
834
# doesn't purely test what a Remote* object sends over the wire, and
835
# how it reacts to responses from the wire. It instead relies partly
836
# on asserting that the RemoteBranch will call
837
# self._real_branch.set_last_revision_info(...).
839
# First, set up our RemoteBranch with a FakeClient that raises
840
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
841
transport = MemoryTransport()
842
transport.mkdir('branch')
843
transport = transport.clone('branch')
844
client = FakeClient(transport.base)
845
client.add_expected_call(
846
'Branch.get_stacked_on_url', ('branch/',),
847
'error', ('NotStacked',))
848
client.add_expected_call(
849
'Branch.set_last_revision_info',
850
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
851
'unknown', 'Branch.set_last_revision_info')
853
branch = self.make_remote_branch(transport, client)
854
class StubRealBranch(object):
857
def set_last_revision_info(self, revno, revision_id):
859
('set_last_revision_info', revno, revision_id))
860
def _clear_cached_state(self):
862
real_branch = StubRealBranch()
863
branch._real_branch = real_branch
864
self.lock_remote_branch(branch)
866
# Call set_last_revision_info, and verify it behaved as expected.
867
result = branch.set_last_revision_info(1234, 'a-revision-id')
869
[('set_last_revision_info', 1234, 'a-revision-id')],
871
client.finished_test()
873
def test_unexpected_error(self):
874
# If the server sends an error the client doesn't understand, it gets
875
# turned into an UnknownErrorFromSmartServer, which is presented as a
876
# non-internal error to the user.
877
transport = MemoryTransport()
878
transport.mkdir('branch')
879
transport = transport.clone('branch')
880
client = FakeClient(transport.base)
882
client.add_error_response('NotStacked')
884
client.add_success_response('ok', 'branch token', 'repo token')
886
client.add_error_response('UnexpectedError')
888
client.add_success_response('ok')
890
branch = self.make_remote_branch(transport, client)
891
# Lock the branch, reset the record of remote calls.
895
err = self.assertRaises(
896
errors.UnknownErrorFromSmartServer,
897
branch.set_last_revision_info, 123, 'revid')
898
self.assertEqual(('UnexpectedError',), err.error_tuple)
901
def test_tip_change_rejected(self):
902
"""TipChangeRejected responses cause a TipChangeRejected exception to
905
transport = MemoryTransport()
906
transport.mkdir('branch')
907
transport = transport.clone('branch')
908
client = FakeClient(transport.base)
910
client.add_error_response('NotStacked')
912
client.add_success_response('ok', 'branch token', 'repo token')
914
client.add_error_response('TipChangeRejected', 'rejection message')
916
client.add_success_response('ok')
918
branch = self.make_remote_branch(transport, client)
919
# Lock the branch, reset the record of remote calls.
921
self.addCleanup(branch.unlock)
924
# The 'TipChangeRejected' error response triggered by calling
925
# set_last_revision_info causes a TipChangeRejected exception.
926
err = self.assertRaises(
927
errors.TipChangeRejected,
928
branch.set_last_revision_info, 123, 'revid')
929
self.assertEqual('rejection message', err.msg)
932
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
933
"""Getting the branch configuration should use an abstract method not vfs.
936
def test_get_branch_conf(self):
937
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
938
## # We should see that branch.get_config() does a single rpc to get the
939
## # remote configuration file, abstracting away where that is stored on
940
## # the server. However at the moment it always falls back to using the
941
## # vfs, and this would need some changes in config.py.
943
## # in an empty branch we decode the response properly
944
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
945
## # we need to make a real branch because the remote_branch.control_files
946
## # will trigger _ensure_real.
947
## branch = self.make_branch('quack')
948
## transport = branch.bzrdir.root_transport
949
## # we do not want bzrdir to make any remote calls
950
## bzrdir = RemoteBzrDir(transport, _client=False)
951
## branch = RemoteBranch(bzrdir, None, _client=client)
952
## config = branch.get_config()
954
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
958
class TestBranchLockWrite(RemoteBranchTestCase):
960
def test_lock_write_unlockable(self):
961
transport = MemoryTransport()
962
client = FakeClient(transport.base)
963
client.add_expected_call(
964
'Branch.get_stacked_on_url', ('quack/',),
965
'error', ('NotStacked',),)
966
client.add_expected_call(
967
'Branch.lock_write', ('quack/', '', ''),
968
'error', ('UnlockableTransport',))
969
transport.mkdir('quack')
970
transport = transport.clone('quack')
971
branch = self.make_remote_branch(transport, client)
972
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
973
client.finished_test()
976
class TestTransportIsReadonly(tests.TestCase):
979
client = FakeClient()
980
client.add_success_response('yes')
981
transport = RemoteTransport('bzr://example.com/', medium=False,
983
self.assertEqual(True, transport.is_readonly())
985
[('call', 'Transport.is_readonly', ())],
988
def test_false(self):
989
client = FakeClient()
990
client.add_success_response('no')
991
transport = RemoteTransport('bzr://example.com/', medium=False,
993
self.assertEqual(False, transport.is_readonly())
995
[('call', 'Transport.is_readonly', ())],
998
def test_error_from_old_server(self):
999
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1001
Clients should treat it as a "no" response, because is_readonly is only
1002
advisory anyway (a transport could be read-write, but then the
1003
underlying filesystem could be readonly anyway).
1005
client = FakeClient()
1006
client.add_unknown_method_response('Transport.is_readonly')
1007
transport = RemoteTransport('bzr://example.com/', medium=False,
1009
self.assertEqual(False, transport.is_readonly())
1011
[('call', 'Transport.is_readonly', ())],
1015
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1017
def test_defaults_to_none(self):
1018
t = RemoteSSHTransport('bzr+ssh://example.com')
1019
self.assertIs(None, t._get_credentials()[0])
1021
def test_uses_authentication_config(self):
1022
conf = config.AuthenticationConfig()
1023
conf._get_config().update(
1024
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1027
t = RemoteSSHTransport('bzr+ssh://example.com')
1028
self.assertEqual('bar', t._get_credentials()[0])
1031
class TestRemoteRepository(tests.TestCase):
1032
"""Base for testing RemoteRepository protocol usage.
1034
These tests contain frozen requests and responses. We want any changes to
1035
what is sent or expected to be require a thoughtful update to these tests
1036
because they might break compatibility with different-versioned servers.
1039
def setup_fake_client_and_repository(self, transport_path):
1040
"""Create the fake client and repository for testing with.
1042
There's no real server here; we just have canned responses sent
1045
:param transport_path: Path below the root of the MemoryTransport
1046
where the repository will be created.
1048
transport = MemoryTransport()
1049
transport.mkdir(transport_path)
1050
client = FakeClient(transport.base)
1051
transport = transport.clone(transport_path)
1052
# we do not want bzrdir to make any remote calls
1053
bzrdir = RemoteBzrDir(transport, _client=False)
1054
repo = RemoteRepository(bzrdir, None, _client=client)
1058
class TestRepositoryGatherStats(TestRemoteRepository):
1060
def test_revid_none(self):
1061
# ('ok',), body with revisions and size
1062
transport_path = 'quack'
1063
repo, client = self.setup_fake_client_and_repository(transport_path)
1064
client.add_success_response_with_body(
1065
'revisions: 2\nsize: 18\n', 'ok')
1066
result = repo.gather_stats(None)
1068
[('call_expecting_body', 'Repository.gather_stats',
1069
('quack/','','no'))],
1071
self.assertEqual({'revisions': 2, 'size': 18}, result)
1073
def test_revid_no_committers(self):
1074
# ('ok',), body without committers
1075
body = ('firstrev: 123456.300 3600\n'
1076
'latestrev: 654231.400 0\n'
1079
transport_path = 'quick'
1080
revid = u'\xc8'.encode('utf8')
1081
repo, client = self.setup_fake_client_and_repository(transport_path)
1082
client.add_success_response_with_body(body, 'ok')
1083
result = repo.gather_stats(revid)
1085
[('call_expecting_body', 'Repository.gather_stats',
1086
('quick/', revid, 'no'))],
1088
self.assertEqual({'revisions': 2, 'size': 18,
1089
'firstrev': (123456.300, 3600),
1090
'latestrev': (654231.400, 0),},
1093
def test_revid_with_committers(self):
1094
# ('ok',), body with committers
1095
body = ('committers: 128\n'
1096
'firstrev: 123456.300 3600\n'
1097
'latestrev: 654231.400 0\n'
1100
transport_path = 'buick'
1101
revid = u'\xc8'.encode('utf8')
1102
repo, client = self.setup_fake_client_and_repository(transport_path)
1103
client.add_success_response_with_body(body, 'ok')
1104
result = repo.gather_stats(revid, True)
1106
[('call_expecting_body', 'Repository.gather_stats',
1107
('buick/', revid, 'yes'))],
1109
self.assertEqual({'revisions': 2, 'size': 18,
1111
'firstrev': (123456.300, 3600),
1112
'latestrev': (654231.400, 0),},
1116
class TestRepositoryGetGraph(TestRemoteRepository):
1118
def test_get_graph(self):
1119
# get_graph returns a graph with the repository as the
1121
transport_path = 'quack'
1122
repo, client = self.setup_fake_client_and_repository(transport_path)
1123
graph = repo.get_graph()
1124
self.assertEqual(graph._parents_provider, repo)
1127
class TestRepositoryGetParentMap(TestRemoteRepository):
1129
def test_get_parent_map_caching(self):
1130
# get_parent_map returns from cache until unlock()
1131
# setup a reponse with two revisions
1132
r1 = u'\u0e33'.encode('utf8')
1133
r2 = u'\u0dab'.encode('utf8')
1134
lines = [' '.join([r2, r1]), r1]
1135
encoded_body = bz2.compress('\n'.join(lines))
1137
transport_path = 'quack'
1138
repo, client = self.setup_fake_client_and_repository(transport_path)
1139
client.add_success_response_with_body(encoded_body, 'ok')
1140
client.add_success_response_with_body(encoded_body, 'ok')
1142
graph = repo.get_graph()
1143
parents = graph.get_parent_map([r2])
1144
self.assertEqual({r2: (r1,)}, parents)
1145
# locking and unlocking deeper should not reset
1148
parents = graph.get_parent_map([r1])
1149
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1151
[('call_with_body_bytes_expecting_body',
1152
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1155
# now we call again, and it should use the second response.
1157
graph = repo.get_graph()
1158
parents = graph.get_parent_map([r1])
1159
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1161
[('call_with_body_bytes_expecting_body',
1162
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1163
('call_with_body_bytes_expecting_body',
1164
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1169
def test_get_parent_map_reconnects_if_unknown_method(self):
1170
transport_path = 'quack'
1171
repo, client = self.setup_fake_client_and_repository(transport_path)
1172
client.add_unknown_method_response('Repository,get_parent_map')
1173
client.add_success_response_with_body('', 'ok')
1174
self.assertFalse(client._medium._is_remote_before((1, 2)))
1175
rev_id = 'revision-id'
1176
expected_deprecations = [
1177
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1179
parents = self.callDeprecated(
1180
expected_deprecations, repo.get_parent_map, [rev_id])
1182
[('call_with_body_bytes_expecting_body',
1183
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1184
('disconnect medium',),
1185
('call_expecting_body', 'Repository.get_revision_graph',
1188
# The medium is now marked as being connected to an older server
1189
self.assertTrue(client._medium._is_remote_before((1, 2)))
1191
def test_get_parent_map_fallback_parentless_node(self):
1192
"""get_parent_map falls back to get_revision_graph on old servers. The
1193
results from get_revision_graph are tweaked to match the get_parent_map
1196
Specifically, a {key: ()} result from get_revision_graph means "no
1197
parents" for that key, which in get_parent_map results should be
1198
represented as {key: ('null:',)}.
1200
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1202
rev_id = 'revision-id'
1203
transport_path = 'quack'
1204
repo, client = self.setup_fake_client_and_repository(transport_path)
1205
client.add_success_response_with_body(rev_id, 'ok')
1206
client._medium._remember_remote_is_before((1, 2))
1207
expected_deprecations = [
1208
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1210
parents = self.callDeprecated(
1211
expected_deprecations, repo.get_parent_map, [rev_id])
1213
[('call_expecting_body', 'Repository.get_revision_graph',
1216
self.assertEqual({rev_id: ('null:',)}, parents)
1218
def test_get_parent_map_unexpected_response(self):
1219
repo, client = self.setup_fake_client_and_repository('path')
1220
client.add_success_response('something unexpected!')
1222
errors.UnexpectedSmartServerResponse,
1223
repo.get_parent_map, ['a-revision-id'])
1226
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1228
def test_null_revision(self):
1229
# a null revision has the predictable result {}, we should have no wire
1230
# traffic when calling it with this argument
1231
transport_path = 'empty'
1232
repo, client = self.setup_fake_client_and_repository(transport_path)
1233
client.add_success_response('notused')
1234
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1236
self.assertEqual([], client._calls)
1237
self.assertEqual({}, result)
1239
def test_none_revision(self):
1240
# with none we want the entire graph
1241
r1 = u'\u0e33'.encode('utf8')
1242
r2 = u'\u0dab'.encode('utf8')
1243
lines = [' '.join([r2, r1]), r1]
1244
encoded_body = '\n'.join(lines)
1246
transport_path = 'sinhala'
1247
repo, client = self.setup_fake_client_and_repository(transport_path)
1248
client.add_success_response_with_body(encoded_body, 'ok')
1249
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1251
[('call_expecting_body', 'Repository.get_revision_graph',
1254
self.assertEqual({r1: (), r2: (r1, )}, result)
1256
def test_specific_revision(self):
1257
# with a specific revision we want the graph for that
1258
# with none we want the entire graph
1259
r11 = u'\u0e33'.encode('utf8')
1260
r12 = u'\xc9'.encode('utf8')
1261
r2 = u'\u0dab'.encode('utf8')
1262
lines = [' '.join([r2, r11, r12]), r11, r12]
1263
encoded_body = '\n'.join(lines)
1265
transport_path = 'sinhala'
1266
repo, client = self.setup_fake_client_and_repository(transport_path)
1267
client.add_success_response_with_body(encoded_body, 'ok')
1268
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1270
[('call_expecting_body', 'Repository.get_revision_graph',
1273
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1275
def test_no_such_revision(self):
1277
transport_path = 'sinhala'
1278
repo, client = self.setup_fake_client_and_repository(transport_path)
1279
client.add_error_response('nosuchrevision', revid)
1280
# also check that the right revision is reported in the error
1281
self.assertRaises(errors.NoSuchRevision,
1282
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1284
[('call_expecting_body', 'Repository.get_revision_graph',
1285
('sinhala/', revid))],
1288
def test_unexpected_error(self):
1290
transport_path = 'sinhala'
1291
repo, client = self.setup_fake_client_and_repository(transport_path)
1292
client.add_error_response('AnUnexpectedError')
1293
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1294
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1295
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1298
class TestRepositoryIsShared(TestRemoteRepository):
1300
def test_is_shared(self):
1301
# ('yes', ) for Repository.is_shared -> 'True'.
1302
transport_path = 'quack'
1303
repo, client = self.setup_fake_client_and_repository(transport_path)
1304
client.add_success_response('yes')
1305
result = repo.is_shared()
1307
[('call', 'Repository.is_shared', ('quack/',))],
1309
self.assertEqual(True, result)
1311
def test_is_not_shared(self):
1312
# ('no', ) for Repository.is_shared -> 'False'.
1313
transport_path = 'qwack'
1314
repo, client = self.setup_fake_client_and_repository(transport_path)
1315
client.add_success_response('no')
1316
result = repo.is_shared()
1318
[('call', 'Repository.is_shared', ('qwack/',))],
1320
self.assertEqual(False, result)
1323
class TestRepositoryLockWrite(TestRemoteRepository):
1325
def test_lock_write(self):
1326
transport_path = 'quack'
1327
repo, client = self.setup_fake_client_and_repository(transport_path)
1328
client.add_success_response('ok', 'a token')
1329
result = repo.lock_write()
1331
[('call', 'Repository.lock_write', ('quack/', ''))],
1333
self.assertEqual('a token', result)
1335
def test_lock_write_already_locked(self):
1336
transport_path = 'quack'
1337
repo, client = self.setup_fake_client_and_repository(transport_path)
1338
client.add_error_response('LockContention')
1339
self.assertRaises(errors.LockContention, repo.lock_write)
1341
[('call', 'Repository.lock_write', ('quack/', ''))],
1344
def test_lock_write_unlockable(self):
1345
transport_path = 'quack'
1346
repo, client = self.setup_fake_client_and_repository(transport_path)
1347
client.add_error_response('UnlockableTransport')
1348
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1350
[('call', 'Repository.lock_write', ('quack/', ''))],
1354
class TestRepositoryUnlock(TestRemoteRepository):
1356
def test_unlock(self):
1357
transport_path = 'quack'
1358
repo, client = self.setup_fake_client_and_repository(transport_path)
1359
client.add_success_response('ok', 'a token')
1360
client.add_success_response('ok')
1364
[('call', 'Repository.lock_write', ('quack/', '')),
1365
('call', 'Repository.unlock', ('quack/', 'a token'))],
1368
def test_unlock_wrong_token(self):
1369
# If somehow the token is wrong, unlock will raise TokenMismatch.
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_error_response('TokenMismatch')
1375
self.assertRaises(errors.TokenMismatch, repo.unlock)
1378
class TestRepositoryHasRevision(TestRemoteRepository):
1380
def test_none(self):
1381
# repo.has_revision(None) should not cause any traffic.
1382
transport_path = 'quack'
1383
repo, client = self.setup_fake_client_and_repository(transport_path)
1385
# The null revision is always there, so has_revision(None) == True.
1386
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1388
# The remote repo shouldn't be accessed.
1389
self.assertEqual([], client._calls)
1392
class TestRepositoryTarball(TestRemoteRepository):
1394
# This is a canned tarball reponse we can validate against
1396
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1397
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1398
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1399
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1400
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1401
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1402
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1403
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1404
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1405
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1406
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1407
'nWQ7QH/F3JFOFCQ0aSPfA='
1410
def test_repository_tarball(self):
1411
# Test that Repository.tarball generates the right operations
1412
transport_path = 'repo'
1413
expected_calls = [('call_expecting_body', 'Repository.tarball',
1414
('repo/', 'bz2',),),
1416
repo, client = self.setup_fake_client_and_repository(transport_path)
1417
client.add_success_response_with_body(self.tarball_content, 'ok')
1418
# Now actually ask for the tarball
1419
tarball_file = repo._get_tarball('bz2')
1421
self.assertEqual(expected_calls, client._calls)
1422
self.assertEqual(self.tarball_content, tarball_file.read())
1424
tarball_file.close()
1427
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1428
"""RemoteRepository.copy_content_into optimizations"""
1430
def test_copy_content_remote_to_local(self):
1431
self.transport_server = server.SmartTCPServer_for_testing
1432
src_repo = self.make_repository('repo1')
1433
src_repo = repository.Repository.open(self.get_url('repo1'))
1434
# At the moment the tarball-based copy_content_into can't write back
1435
# into a smart server. It would be good if it could upload the
1436
# tarball; once that works we'd have to create repositories of
1437
# different formats. -- mbp 20070410
1438
dest_url = self.get_vfs_only_url('repo2')
1439
dest_bzrdir = BzrDir.create(dest_url)
1440
dest_repo = dest_bzrdir.create_repository()
1441
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1442
self.assertTrue(isinstance(src_repo, RemoteRepository))
1443
src_repo.copy_content_into(dest_repo)
1446
class _StubRealPackRepository(object):
1448
def __init__(self, calls):
1449
self._pack_collection = _StubPackCollection(calls)
1452
class _StubPackCollection(object):
1454
def __init__(self, calls):
1458
self.calls.append(('pack collection autopack',))
1460
def reload_pack_names(self):
1461
self.calls.append(('pack collection reload_pack_names',))
1464
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1465
"""Tests for RemoteRepository.autopack implementation."""
1468
"""When the server returns 'ok' and there's no _real_repository, then
1469
nothing else happens: the autopack method is done.
1471
transport_path = 'quack'
1472
repo, client = self.setup_fake_client_and_repository(transport_path)
1473
client.add_expected_call(
1474
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1476
client.finished_test()
1478
def test_ok_with_real_repo(self):
1479
"""When the server returns 'ok' and there is a _real_repository, then
1480
the _real_repository's reload_pack_name's method will be called.
1482
transport_path = 'quack'
1483
repo, client = self.setup_fake_client_and_repository(transport_path)
1484
client.add_expected_call(
1485
'PackRepository.autopack', ('quack/',),
1487
repo._real_repository = _StubRealPackRepository(client._calls)
1490
[('call', 'PackRepository.autopack', ('quack/',)),
1491
('pack collection reload_pack_names',)],
1494
def test_backwards_compatibility(self):
1495
"""If the server does not recognise the PackRepository.autopack verb,
1496
fallback to the real_repository's implementation.
1498
transport_path = 'quack'
1499
repo, client = self.setup_fake_client_and_repository(transport_path)
1500
client.add_unknown_method_response('PackRepository.autopack')
1501
def stub_ensure_real():
1502
client._calls.append(('_ensure_real',))
1503
repo._real_repository = _StubRealPackRepository(client._calls)
1504
repo._ensure_real = stub_ensure_real
1507
[('call', 'PackRepository.autopack', ('quack/',)),
1509
('pack collection autopack',)],
1513
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1514
"""Base class for unit tests for bzrlib.remote._translate_error."""
1516
def translateTuple(self, error_tuple, **context):
1517
"""Call _translate_error with an ErrorFromSmartServer built from the
1520
:param error_tuple: A tuple of a smart server response, as would be
1521
passed to an ErrorFromSmartServer.
1522
:kwargs context: context items to call _translate_error with.
1524
:returns: The error raised by _translate_error.
1526
# Raise the ErrorFromSmartServer before passing it as an argument,
1527
# because _translate_error may need to re-raise it with a bare 'raise'
1529
server_error = errors.ErrorFromSmartServer(error_tuple)
1530
translated_error = self.translateErrorFromSmartServer(
1531
server_error, **context)
1532
return translated_error
1534
def translateErrorFromSmartServer(self, error_object, **context):
1535
"""Like translateTuple, but takes an already constructed
1536
ErrorFromSmartServer rather than a tuple.
1540
except errors.ErrorFromSmartServer, server_error:
1541
translated_error = self.assertRaises(
1542
errors.BzrError, remote._translate_error, server_error,
1544
return translated_error
1547
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1548
"""Unit tests for bzrlib.remote._translate_error.
1550
Given an ErrorFromSmartServer (which has an error tuple from a smart
1551
server) and some context, _translate_error raises more specific errors from
1554
This test case covers the cases where _translate_error succeeds in
1555
translating an ErrorFromSmartServer to something better. See
1556
TestErrorTranslationRobustness for other cases.
1559
def test_NoSuchRevision(self):
1560
branch = self.make_branch('')
1562
translated_error = self.translateTuple(
1563
('NoSuchRevision', revid), branch=branch)
1564
expected_error = errors.NoSuchRevision(branch, revid)
1565
self.assertEqual(expected_error, translated_error)
1567
def test_nosuchrevision(self):
1568
repository = self.make_repository('')
1570
translated_error = self.translateTuple(
1571
('nosuchrevision', revid), repository=repository)
1572
expected_error = errors.NoSuchRevision(repository, revid)
1573
self.assertEqual(expected_error, translated_error)
1575
def test_nobranch(self):
1576
bzrdir = self.make_bzrdir('')
1577
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1578
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1579
self.assertEqual(expected_error, translated_error)
1581
def test_LockContention(self):
1582
translated_error = self.translateTuple(('LockContention',))
1583
expected_error = errors.LockContention('(remote lock)')
1584
self.assertEqual(expected_error, translated_error)
1586
def test_UnlockableTransport(self):
1587
bzrdir = self.make_bzrdir('')
1588
translated_error = self.translateTuple(
1589
('UnlockableTransport',), bzrdir=bzrdir)
1590
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1591
self.assertEqual(expected_error, translated_error)
1593
def test_LockFailed(self):
1594
lock = 'str() of a server lock'
1595
why = 'str() of why'
1596
translated_error = self.translateTuple(('LockFailed', lock, why))
1597
expected_error = errors.LockFailed(lock, why)
1598
self.assertEqual(expected_error, translated_error)
1600
def test_TokenMismatch(self):
1601
token = 'a lock token'
1602
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1603
expected_error = errors.TokenMismatch(token, '(remote token)')
1604
self.assertEqual(expected_error, translated_error)
1606
def test_Diverged(self):
1607
branch = self.make_branch('a')
1608
other_branch = self.make_branch('b')
1609
translated_error = self.translateTuple(
1610
('Diverged',), branch=branch, other_branch=other_branch)
1611
expected_error = errors.DivergedBranches(branch, other_branch)
1612
self.assertEqual(expected_error, translated_error)
1614
def test_ReadError_no_args(self):
1616
translated_error = self.translateTuple(('ReadError',), path=path)
1617
expected_error = errors.ReadError(path)
1618
self.assertEqual(expected_error, translated_error)
1620
def test_ReadError(self):
1622
translated_error = self.translateTuple(('ReadError', path))
1623
expected_error = errors.ReadError(path)
1624
self.assertEqual(expected_error, translated_error)
1626
def test_PermissionDenied_no_args(self):
1628
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1629
expected_error = errors.PermissionDenied(path)
1630
self.assertEqual(expected_error, translated_error)
1632
def test_PermissionDenied_one_arg(self):
1634
translated_error = self.translateTuple(('PermissionDenied', path))
1635
expected_error = errors.PermissionDenied(path)
1636
self.assertEqual(expected_error, translated_error)
1638
def test_PermissionDenied_one_arg_and_context(self):
1639
"""Given a choice between a path from the local context and a path on
1640
the wire, _translate_error prefers the path from the local context.
1642
local_path = 'local path'
1643
remote_path = 'remote path'
1644
translated_error = self.translateTuple(
1645
('PermissionDenied', remote_path), path=local_path)
1646
expected_error = errors.PermissionDenied(local_path)
1647
self.assertEqual(expected_error, translated_error)
1649
def test_PermissionDenied_two_args(self):
1651
extra = 'a string with extra info'
1652
translated_error = self.translateTuple(
1653
('PermissionDenied', path, extra))
1654
expected_error = errors.PermissionDenied(path, extra)
1655
self.assertEqual(expected_error, translated_error)
1658
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1659
"""Unit tests for bzrlib.remote._translate_error's robustness.
1661
TestErrorTranslationSuccess is for cases where _translate_error can
1662
translate successfully. This class about how _translate_err behaves when
1663
it fails to translate: it re-raises the original error.
1666
def test_unrecognised_server_error(self):
1667
"""If the error code from the server is not recognised, the original
1668
ErrorFromSmartServer is propagated unmodified.
1670
error_tuple = ('An unknown error tuple',)
1671
server_error = errors.ErrorFromSmartServer(error_tuple)
1672
translated_error = self.translateErrorFromSmartServer(server_error)
1673
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1674
self.assertEqual(expected_error, translated_error)
1676
def test_context_missing_a_key(self):
1677
"""In case of a bug in the client, or perhaps an unexpected response
1678
from a server, _translate_error returns the original error tuple from
1679
the server and mutters a warning.
1681
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1682
# in the context dict. So let's give it an empty context dict instead
1683
# to exercise its error recovery.
1685
error_tuple = ('NoSuchRevision', 'revid')
1686
server_error = errors.ErrorFromSmartServer(error_tuple)
1687
translated_error = self.translateErrorFromSmartServer(server_error)
1688
self.assertEqual(server_error, translated_error)
1689
# In addition to re-raising ErrorFromSmartServer, some debug info has
1690
# been muttered to the log file for developer to look at.
1691
self.assertContainsRe(
1692
self._get_log(keep_log_file=True),
1693
"Missing key 'branch' in context")
1695
def test_path_missing(self):
1696
"""Some translations (PermissionDenied, ReadError) can determine the
1697
'path' variable from either the wire or the local context. If neither
1698
has it, then an error is raised.
1700
error_tuple = ('ReadError',)
1701
server_error = errors.ErrorFromSmartServer(error_tuple)
1702
translated_error = self.translateErrorFromSmartServer(server_error)
1703
self.assertEqual(server_error, translated_error)
1704
# In addition to re-raising ErrorFromSmartServer, some debug info has
1705
# been muttered to the log file for developer to look at.
1706
self.assertContainsRe(
1707
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1710
class TestStacking(tests.TestCaseWithTransport):
1711
"""Tests for operations on stacked remote repositories.
1713
The underlying format type must support stacking.
1716
def test_access_stacked_remote(self):
1717
# based on <http://launchpad.net/bugs/261315>
1718
# make a branch stacked on another repository containing an empty
1719
# revision, then open it over hpss - we should be able to see that
1721
base_transport = self.get_transport()
1722
base_builder = self.make_branch_builder('base', format='1.6')
1723
base_builder.start_series()
1724
base_revid = base_builder.build_snapshot('rev-id', None,
1725
[('add', ('', None, 'directory', None))],
1727
base_builder.finish_series()
1728
stacked_branch = self.make_branch('stacked', format='1.6')
1729
stacked_branch.set_stacked_on_url('../base')
1730
# start a server looking at this
1731
smart_server = server.SmartTCPServer_for_testing()
1732
smart_server.setUp()
1733
self.addCleanup(smart_server.tearDown)
1734
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1735
# can get its branch and repository
1736
remote_branch = remote_bzrdir.open_branch()
1737
remote_repo = remote_branch.repository
1738
remote_repo.lock_read()
1740
# it should have an appropriate fallback repository, which should also
1741
# be a RemoteRepository
1742
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1743
self.assertIsInstance(remote_repo._fallback_repositories[0],
1745
# and it has the revision committed to the underlying repository;
1746
# these have varying implementations so we try several of them
1747
self.assertTrue(remote_repo.has_revisions([base_revid]))
1748
self.assertTrue(remote_repo.has_revision(base_revid))
1749
self.assertEqual(remote_repo.get_revision(base_revid).message,
1752
remote_repo.unlock()