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 a custom parents provider.
1120
transport_path = 'quack'
1121
repo, client = self.setup_fake_client_and_repository(transport_path)
1122
graph = repo.get_graph()
1123
self.assertNotEqual(graph._parents_provider, repo)
1126
class TestRepositoryGetParentMap(TestRemoteRepository):
1128
def test_get_parent_map_caching(self):
1129
# get_parent_map returns from cache until unlock()
1130
# setup a reponse with two revisions
1131
r1 = u'\u0e33'.encode('utf8')
1132
r2 = u'\u0dab'.encode('utf8')
1133
lines = [' '.join([r2, r1]), r1]
1134
encoded_body = bz2.compress('\n'.join(lines))
1136
transport_path = 'quack'
1137
repo, client = self.setup_fake_client_and_repository(transport_path)
1138
client.add_success_response_with_body(encoded_body, 'ok')
1139
client.add_success_response_with_body(encoded_body, 'ok')
1141
graph = repo.get_graph()
1142
parents = graph.get_parent_map([r2])
1143
self.assertEqual({r2: (r1,)}, parents)
1144
# locking and unlocking deeper should not reset
1147
parents = graph.get_parent_map([r1])
1148
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1150
[('call_with_body_bytes_expecting_body',
1151
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1154
# now we call again, and it should use the second response.
1156
graph = repo.get_graph()
1157
parents = graph.get_parent_map([r1])
1158
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1160
[('call_with_body_bytes_expecting_body',
1161
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1162
('call_with_body_bytes_expecting_body',
1163
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1168
def test_get_parent_map_reconnects_if_unknown_method(self):
1169
transport_path = 'quack'
1170
repo, client = self.setup_fake_client_and_repository(transport_path)
1171
client.add_unknown_method_response('Repository,get_parent_map')
1172
client.add_success_response_with_body('', 'ok')
1173
self.assertFalse(client._medium._is_remote_before((1, 2)))
1174
rev_id = 'revision-id'
1175
expected_deprecations = [
1176
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1178
parents = self.callDeprecated(
1179
expected_deprecations, repo.get_parent_map, [rev_id])
1181
[('call_with_body_bytes_expecting_body',
1182
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1183
('disconnect medium',),
1184
('call_expecting_body', 'Repository.get_revision_graph',
1187
# The medium is now marked as being connected to an older server
1188
self.assertTrue(client._medium._is_remote_before((1, 2)))
1190
def test_get_parent_map_fallback_parentless_node(self):
1191
"""get_parent_map falls back to get_revision_graph on old servers. The
1192
results from get_revision_graph are tweaked to match the get_parent_map
1195
Specifically, a {key: ()} result from get_revision_graph means "no
1196
parents" for that key, which in get_parent_map results should be
1197
represented as {key: ('null:',)}.
1199
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1201
rev_id = 'revision-id'
1202
transport_path = 'quack'
1203
repo, client = self.setup_fake_client_and_repository(transport_path)
1204
client.add_success_response_with_body(rev_id, 'ok')
1205
client._medium._remember_remote_is_before((1, 2))
1206
expected_deprecations = [
1207
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1209
parents = self.callDeprecated(
1210
expected_deprecations, repo.get_parent_map, [rev_id])
1212
[('call_expecting_body', 'Repository.get_revision_graph',
1215
self.assertEqual({rev_id: ('null:',)}, parents)
1217
def test_get_parent_map_unexpected_response(self):
1218
repo, client = self.setup_fake_client_and_repository('path')
1219
client.add_success_response('something unexpected!')
1221
errors.UnexpectedSmartServerResponse,
1222
repo.get_parent_map, ['a-revision-id'])
1225
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1227
def test_null_revision(self):
1228
# a null revision has the predictable result {}, we should have no wire
1229
# traffic when calling it with this argument
1230
transport_path = 'empty'
1231
repo, client = self.setup_fake_client_and_repository(transport_path)
1232
client.add_success_response('notused')
1233
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1235
self.assertEqual([], client._calls)
1236
self.assertEqual({}, result)
1238
def test_none_revision(self):
1239
# with none we want the entire graph
1240
r1 = u'\u0e33'.encode('utf8')
1241
r2 = u'\u0dab'.encode('utf8')
1242
lines = [' '.join([r2, r1]), r1]
1243
encoded_body = '\n'.join(lines)
1245
transport_path = 'sinhala'
1246
repo, client = self.setup_fake_client_and_repository(transport_path)
1247
client.add_success_response_with_body(encoded_body, 'ok')
1248
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1250
[('call_expecting_body', 'Repository.get_revision_graph',
1253
self.assertEqual({r1: (), r2: (r1, )}, result)
1255
def test_specific_revision(self):
1256
# with a specific revision we want the graph for that
1257
# with none we want the entire graph
1258
r11 = u'\u0e33'.encode('utf8')
1259
r12 = u'\xc9'.encode('utf8')
1260
r2 = u'\u0dab'.encode('utf8')
1261
lines = [' '.join([r2, r11, r12]), r11, r12]
1262
encoded_body = '\n'.join(lines)
1264
transport_path = 'sinhala'
1265
repo, client = self.setup_fake_client_and_repository(transport_path)
1266
client.add_success_response_with_body(encoded_body, 'ok')
1267
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1269
[('call_expecting_body', 'Repository.get_revision_graph',
1272
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1274
def test_no_such_revision(self):
1276
transport_path = 'sinhala'
1277
repo, client = self.setup_fake_client_and_repository(transport_path)
1278
client.add_error_response('nosuchrevision', revid)
1279
# also check that the right revision is reported in the error
1280
self.assertRaises(errors.NoSuchRevision,
1281
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1283
[('call_expecting_body', 'Repository.get_revision_graph',
1284
('sinhala/', revid))],
1287
def test_unexpected_error(self):
1289
transport_path = 'sinhala'
1290
repo, client = self.setup_fake_client_and_repository(transport_path)
1291
client.add_error_response('AnUnexpectedError')
1292
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1293
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1294
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1297
class TestRepositoryIsShared(TestRemoteRepository):
1299
def test_is_shared(self):
1300
# ('yes', ) for Repository.is_shared -> 'True'.
1301
transport_path = 'quack'
1302
repo, client = self.setup_fake_client_and_repository(transport_path)
1303
client.add_success_response('yes')
1304
result = repo.is_shared()
1306
[('call', 'Repository.is_shared', ('quack/',))],
1308
self.assertEqual(True, result)
1310
def test_is_not_shared(self):
1311
# ('no', ) for Repository.is_shared -> 'False'.
1312
transport_path = 'qwack'
1313
repo, client = self.setup_fake_client_and_repository(transport_path)
1314
client.add_success_response('no')
1315
result = repo.is_shared()
1317
[('call', 'Repository.is_shared', ('qwack/',))],
1319
self.assertEqual(False, result)
1322
class TestRepositoryLockWrite(TestRemoteRepository):
1324
def test_lock_write(self):
1325
transport_path = 'quack'
1326
repo, client = self.setup_fake_client_and_repository(transport_path)
1327
client.add_success_response('ok', 'a token')
1328
result = repo.lock_write()
1330
[('call', 'Repository.lock_write', ('quack/', ''))],
1332
self.assertEqual('a token', result)
1334
def test_lock_write_already_locked(self):
1335
transport_path = 'quack'
1336
repo, client = self.setup_fake_client_and_repository(transport_path)
1337
client.add_error_response('LockContention')
1338
self.assertRaises(errors.LockContention, repo.lock_write)
1340
[('call', 'Repository.lock_write', ('quack/', ''))],
1343
def test_lock_write_unlockable(self):
1344
transport_path = 'quack'
1345
repo, client = self.setup_fake_client_and_repository(transport_path)
1346
client.add_error_response('UnlockableTransport')
1347
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1349
[('call', 'Repository.lock_write', ('quack/', ''))],
1353
class TestRepositoryUnlock(TestRemoteRepository):
1355
def test_unlock(self):
1356
transport_path = 'quack'
1357
repo, client = self.setup_fake_client_and_repository(transport_path)
1358
client.add_success_response('ok', 'a token')
1359
client.add_success_response('ok')
1363
[('call', 'Repository.lock_write', ('quack/', '')),
1364
('call', 'Repository.unlock', ('quack/', 'a token'))],
1367
def test_unlock_wrong_token(self):
1368
# If somehow the token is wrong, unlock will raise TokenMismatch.
1369
transport_path = 'quack'
1370
repo, client = self.setup_fake_client_and_repository(transport_path)
1371
client.add_success_response('ok', 'a token')
1372
client.add_error_response('TokenMismatch')
1374
self.assertRaises(errors.TokenMismatch, repo.unlock)
1377
class TestRepositoryHasRevision(TestRemoteRepository):
1379
def test_none(self):
1380
# repo.has_revision(None) should not cause any traffic.
1381
transport_path = 'quack'
1382
repo, client = self.setup_fake_client_and_repository(transport_path)
1384
# The null revision is always there, so has_revision(None) == True.
1385
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1387
# The remote repo shouldn't be accessed.
1388
self.assertEqual([], client._calls)
1391
class TestRepositoryTarball(TestRemoteRepository):
1393
# This is a canned tarball reponse we can validate against
1395
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1396
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1397
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1398
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1399
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1400
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1401
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1402
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1403
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1404
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1405
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1406
'nWQ7QH/F3JFOFCQ0aSPfA='
1409
def test_repository_tarball(self):
1410
# Test that Repository.tarball generates the right operations
1411
transport_path = 'repo'
1412
expected_calls = [('call_expecting_body', 'Repository.tarball',
1413
('repo/', 'bz2',),),
1415
repo, client = self.setup_fake_client_and_repository(transport_path)
1416
client.add_success_response_with_body(self.tarball_content, 'ok')
1417
# Now actually ask for the tarball
1418
tarball_file = repo._get_tarball('bz2')
1420
self.assertEqual(expected_calls, client._calls)
1421
self.assertEqual(self.tarball_content, tarball_file.read())
1423
tarball_file.close()
1426
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1427
"""RemoteRepository.copy_content_into optimizations"""
1429
def test_copy_content_remote_to_local(self):
1430
self.transport_server = server.SmartTCPServer_for_testing
1431
src_repo = self.make_repository('repo1')
1432
src_repo = repository.Repository.open(self.get_url('repo1'))
1433
# At the moment the tarball-based copy_content_into can't write back
1434
# into a smart server. It would be good if it could upload the
1435
# tarball; once that works we'd have to create repositories of
1436
# different formats. -- mbp 20070410
1437
dest_url = self.get_vfs_only_url('repo2')
1438
dest_bzrdir = BzrDir.create(dest_url)
1439
dest_repo = dest_bzrdir.create_repository()
1440
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1441
self.assertTrue(isinstance(src_repo, RemoteRepository))
1442
src_repo.copy_content_into(dest_repo)
1445
class _StubRealPackRepository(object):
1447
def __init__(self, calls):
1448
self._pack_collection = _StubPackCollection(calls)
1451
class _StubPackCollection(object):
1453
def __init__(self, calls):
1457
self.calls.append(('pack collection autopack',))
1459
def reload_pack_names(self):
1460
self.calls.append(('pack collection reload_pack_names',))
1463
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1464
"""Tests for RemoteRepository.autopack implementation."""
1467
"""When the server returns 'ok' and there's no _real_repository, then
1468
nothing else happens: the autopack method is done.
1470
transport_path = 'quack'
1471
repo, client = self.setup_fake_client_and_repository(transport_path)
1472
client.add_expected_call(
1473
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1475
client.finished_test()
1477
def test_ok_with_real_repo(self):
1478
"""When the server returns 'ok' and there is a _real_repository, then
1479
the _real_repository's reload_pack_name's method will be called.
1481
transport_path = 'quack'
1482
repo, client = self.setup_fake_client_and_repository(transport_path)
1483
client.add_expected_call(
1484
'PackRepository.autopack', ('quack/',),
1486
repo._real_repository = _StubRealPackRepository(client._calls)
1489
[('call', 'PackRepository.autopack', ('quack/',)),
1490
('pack collection reload_pack_names',)],
1493
def test_backwards_compatibility(self):
1494
"""If the server does not recognise the PackRepository.autopack verb,
1495
fallback to the real_repository's implementation.
1497
transport_path = 'quack'
1498
repo, client = self.setup_fake_client_and_repository(transport_path)
1499
client.add_unknown_method_response('PackRepository.autopack')
1500
def stub_ensure_real():
1501
client._calls.append(('_ensure_real',))
1502
repo._real_repository = _StubRealPackRepository(client._calls)
1503
repo._ensure_real = stub_ensure_real
1506
[('call', 'PackRepository.autopack', ('quack/',)),
1508
('pack collection autopack',)],
1512
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1513
"""Base class for unit tests for bzrlib.remote._translate_error."""
1515
def translateTuple(self, error_tuple, **context):
1516
"""Call _translate_error with an ErrorFromSmartServer built from the
1519
:param error_tuple: A tuple of a smart server response, as would be
1520
passed to an ErrorFromSmartServer.
1521
:kwargs context: context items to call _translate_error with.
1523
:returns: The error raised by _translate_error.
1525
# Raise the ErrorFromSmartServer before passing it as an argument,
1526
# because _translate_error may need to re-raise it with a bare 'raise'
1528
server_error = errors.ErrorFromSmartServer(error_tuple)
1529
translated_error = self.translateErrorFromSmartServer(
1530
server_error, **context)
1531
return translated_error
1533
def translateErrorFromSmartServer(self, error_object, **context):
1534
"""Like translateTuple, but takes an already constructed
1535
ErrorFromSmartServer rather than a tuple.
1539
except errors.ErrorFromSmartServer, server_error:
1540
translated_error = self.assertRaises(
1541
errors.BzrError, remote._translate_error, server_error,
1543
return translated_error
1546
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1547
"""Unit tests for bzrlib.remote._translate_error.
1549
Given an ErrorFromSmartServer (which has an error tuple from a smart
1550
server) and some context, _translate_error raises more specific errors from
1553
This test case covers the cases where _translate_error succeeds in
1554
translating an ErrorFromSmartServer to something better. See
1555
TestErrorTranslationRobustness for other cases.
1558
def test_NoSuchRevision(self):
1559
branch = self.make_branch('')
1561
translated_error = self.translateTuple(
1562
('NoSuchRevision', revid), branch=branch)
1563
expected_error = errors.NoSuchRevision(branch, revid)
1564
self.assertEqual(expected_error, translated_error)
1566
def test_nosuchrevision(self):
1567
repository = self.make_repository('')
1569
translated_error = self.translateTuple(
1570
('nosuchrevision', revid), repository=repository)
1571
expected_error = errors.NoSuchRevision(repository, revid)
1572
self.assertEqual(expected_error, translated_error)
1574
def test_nobranch(self):
1575
bzrdir = self.make_bzrdir('')
1576
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1577
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1578
self.assertEqual(expected_error, translated_error)
1580
def test_LockContention(self):
1581
translated_error = self.translateTuple(('LockContention',))
1582
expected_error = errors.LockContention('(remote lock)')
1583
self.assertEqual(expected_error, translated_error)
1585
def test_UnlockableTransport(self):
1586
bzrdir = self.make_bzrdir('')
1587
translated_error = self.translateTuple(
1588
('UnlockableTransport',), bzrdir=bzrdir)
1589
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1590
self.assertEqual(expected_error, translated_error)
1592
def test_LockFailed(self):
1593
lock = 'str() of a server lock'
1594
why = 'str() of why'
1595
translated_error = self.translateTuple(('LockFailed', lock, why))
1596
expected_error = errors.LockFailed(lock, why)
1597
self.assertEqual(expected_error, translated_error)
1599
def test_TokenMismatch(self):
1600
token = 'a lock token'
1601
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1602
expected_error = errors.TokenMismatch(token, '(remote token)')
1603
self.assertEqual(expected_error, translated_error)
1605
def test_Diverged(self):
1606
branch = self.make_branch('a')
1607
other_branch = self.make_branch('b')
1608
translated_error = self.translateTuple(
1609
('Diverged',), branch=branch, other_branch=other_branch)
1610
expected_error = errors.DivergedBranches(branch, other_branch)
1611
self.assertEqual(expected_error, translated_error)
1613
def test_ReadError_no_args(self):
1615
translated_error = self.translateTuple(('ReadError',), path=path)
1616
expected_error = errors.ReadError(path)
1617
self.assertEqual(expected_error, translated_error)
1619
def test_ReadError(self):
1621
translated_error = self.translateTuple(('ReadError', path))
1622
expected_error = errors.ReadError(path)
1623
self.assertEqual(expected_error, translated_error)
1625
def test_PermissionDenied_no_args(self):
1627
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1628
expected_error = errors.PermissionDenied(path)
1629
self.assertEqual(expected_error, translated_error)
1631
def test_PermissionDenied_one_arg(self):
1633
translated_error = self.translateTuple(('PermissionDenied', path))
1634
expected_error = errors.PermissionDenied(path)
1635
self.assertEqual(expected_error, translated_error)
1637
def test_PermissionDenied_one_arg_and_context(self):
1638
"""Given a choice between a path from the local context and a path on
1639
the wire, _translate_error prefers the path from the local context.
1641
local_path = 'local path'
1642
remote_path = 'remote path'
1643
translated_error = self.translateTuple(
1644
('PermissionDenied', remote_path), path=local_path)
1645
expected_error = errors.PermissionDenied(local_path)
1646
self.assertEqual(expected_error, translated_error)
1648
def test_PermissionDenied_two_args(self):
1650
extra = 'a string with extra info'
1651
translated_error = self.translateTuple(
1652
('PermissionDenied', path, extra))
1653
expected_error = errors.PermissionDenied(path, extra)
1654
self.assertEqual(expected_error, translated_error)
1657
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1658
"""Unit tests for bzrlib.remote._translate_error's robustness.
1660
TestErrorTranslationSuccess is for cases where _translate_error can
1661
translate successfully. This class about how _translate_err behaves when
1662
it fails to translate: it re-raises the original error.
1665
def test_unrecognised_server_error(self):
1666
"""If the error code from the server is not recognised, the original
1667
ErrorFromSmartServer is propagated unmodified.
1669
error_tuple = ('An unknown error tuple',)
1670
server_error = errors.ErrorFromSmartServer(error_tuple)
1671
translated_error = self.translateErrorFromSmartServer(server_error)
1672
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1673
self.assertEqual(expected_error, translated_error)
1675
def test_context_missing_a_key(self):
1676
"""In case of a bug in the client, or perhaps an unexpected response
1677
from a server, _translate_error returns the original error tuple from
1678
the server and mutters a warning.
1680
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1681
# in the context dict. So let's give it an empty context dict instead
1682
# to exercise its error recovery.
1684
error_tuple = ('NoSuchRevision', 'revid')
1685
server_error = errors.ErrorFromSmartServer(error_tuple)
1686
translated_error = self.translateErrorFromSmartServer(server_error)
1687
self.assertEqual(server_error, translated_error)
1688
# In addition to re-raising ErrorFromSmartServer, some debug info has
1689
# been muttered to the log file for developer to look at.
1690
self.assertContainsRe(
1691
self._get_log(keep_log_file=True),
1692
"Missing key 'branch' in context")
1694
def test_path_missing(self):
1695
"""Some translations (PermissionDenied, ReadError) can determine the
1696
'path' variable from either the wire or the local context. If neither
1697
has it, then an error is raised.
1699
error_tuple = ('ReadError',)
1700
server_error = errors.ErrorFromSmartServer(error_tuple)
1701
translated_error = self.translateErrorFromSmartServer(server_error)
1702
self.assertEqual(server_error, translated_error)
1703
# In addition to re-raising ErrorFromSmartServer, some debug info has
1704
# been muttered to the log file for developer to look at.
1705
self.assertContainsRe(
1706
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1709
class TestStacking(tests.TestCaseWithTransport):
1710
"""Tests for operations on stacked remote repositories.
1712
The underlying format type must support stacking.
1715
def test_access_stacked_remote(self):
1716
# based on <http://launchpad.net/bugs/261315>
1717
# make a branch stacked on another repository containing an empty
1718
# revision, then open it over hpss - we should be able to see that
1720
base_transport = self.get_transport()
1721
base_builder = self.make_branch_builder('base', format='1.6')
1722
base_builder.start_series()
1723
base_revid = base_builder.build_snapshot('rev-id', None,
1724
[('add', ('', None, 'directory', None))],
1726
base_builder.finish_series()
1727
stacked_branch = self.make_branch('stacked', format='1.6')
1728
stacked_branch.set_stacked_on_url('../base')
1729
# start a server looking at this
1730
smart_server = server.SmartTCPServer_for_testing()
1731
smart_server.setUp()
1732
self.addCleanup(smart_server.tearDown)
1733
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1734
# can get its branch and repository
1735
remote_branch = remote_bzrdir.open_branch()
1736
remote_repo = remote_branch.repository
1737
remote_repo.lock_read()
1739
# it should have an appropriate fallback repository, which should also
1740
# be a RemoteRepository
1741
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1742
self.assertIsInstance(remote_repo._fallback_repositories[0],
1744
# and it has the revision committed to the underlying repository;
1745
# these have varying implementations so we try several of them
1746
self.assertTrue(remote_repo.has_revisions([base_revid]))
1747
self.assertTrue(remote_repo.has_revision(base_revid))
1748
self.assertEqual(remote_repo.get_revision(base_revid).message,
1751
remote_repo.unlock()
1753
def prepare_stacked_remote_branch(self):
1754
smart_server = server.SmartTCPServer_for_testing()
1755
smart_server.setUp()
1756
self.addCleanup(smart_server.tearDown)
1757
tree1 = self.make_branch_and_tree('tree1')
1758
tree1.commit('rev1', rev_id='rev1')
1759
tree2 = self.make_branch_and_tree('tree2', format='1.6')
1760
tree2.branch.set_stacked_on_url(tree1.branch.base)
1761
branch2 = Branch.open(smart_server.get_url() + '/tree2')
1763
self.addCleanup(branch2.unlock)
1766
def test_stacked_get_parent_map(self):
1767
# the public implementation of get_parent_map obeys stacking
1768
branch = self.prepare_stacked_remote_branch()
1769
repo = branch.repository
1770
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
1772
def test_stacked__get_parent_map(self):
1773
# the private variant of _get_parent_map ignores stacking
1774
branch = self.prepare_stacked_remote_branch()
1775
repo = branch.repository
1776
self.assertEqual([], repo._get_parent_map(['rev1']).keys())