1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests for remote bzrdir/branch/repo/etc
19
These are proxy objects which act on remote objects by sending messages
20
through a smart client. The proxies are to be created when attempting to open
21
the object given a transport that supports smartserver rpc operations.
23
These tests correspond to tests.test_smart, which exercises the server side.
27
from cStringIO import StringIO
40
from bzrlib.branch import Branch
41
from bzrlib.bzrdir import BzrDir, BzrDirFormat
42
from bzrlib.remote import (
48
from bzrlib.revision import NULL_REVISION
49
from bzrlib.smart import server, medium
50
from bzrlib.smart.client import _SmartClient
51
from bzrlib.symbol_versioning import one_four
52
from bzrlib.transport import get_transport, http
53
from bzrlib.transport.memory import MemoryTransport
54
from bzrlib.transport.remote import (
61
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
64
self.transport_server = server.SmartTCPServer_for_testing
65
super(BasicRemoteObjectTests, self).setUp()
66
self.transport = self.get_transport()
67
# make a branch that can be opened over the smart transport
68
self.local_wt = BzrDir.create_standalone_workingtree('.')
71
self.transport.disconnect()
72
tests.TestCaseWithTransport.tearDown(self)
74
def test_create_remote_bzrdir(self):
75
b = remote.RemoteBzrDir(self.transport)
76
self.assertIsInstance(b, BzrDir)
78
def test_open_remote_branch(self):
79
# open a standalone branch in the working directory
80
b = remote.RemoteBzrDir(self.transport)
81
branch = b.open_branch()
82
self.assertIsInstance(branch, Branch)
84
def test_remote_repository(self):
85
b = BzrDir.open_from_transport(self.transport)
86
repo = b.open_repository()
87
revid = u'\xc823123123'.encode('utf8')
88
self.assertFalse(repo.has_revision(revid))
89
self.local_wt.commit(message='test commit', rev_id=revid)
90
self.assertTrue(repo.has_revision(revid))
92
def test_remote_branch_revision_history(self):
93
b = BzrDir.open_from_transport(self.transport).open_branch()
94
self.assertEqual([], b.revision_history())
95
r1 = self.local_wt.commit('1st commit')
96
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
97
self.assertEqual([r1, r2], b.revision_history())
99
def test_find_correct_format(self):
100
"""Should open a RemoteBzrDir over a RemoteTransport"""
101
fmt = BzrDirFormat.find_format(self.transport)
102
self.assertTrue(RemoteBzrDirFormat
103
in BzrDirFormat._control_server_formats)
104
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
106
def test_open_detected_smart_format(self):
107
fmt = BzrDirFormat.find_format(self.transport)
108
d = fmt.open(self.transport)
109
self.assertIsInstance(d, BzrDir)
111
def test_remote_branch_repr(self):
112
b = BzrDir.open_from_transport(self.transport).open_branch()
113
self.assertStartsWith(str(b), 'RemoteBranch(')
116
class FakeRemoteTransport(object):
117
"""This class provides the minimum support for use in place of a RemoteTransport.
119
It doesn't actually transmit requests, but rather expects them to be
120
handled by a FakeClient which holds canned responses. It does not allow
121
any vfs access, therefore is not suitable for testing any operation that
122
will fallback to vfs access. Backing the test by an instance of this
123
class guarantees that it's - done using non-vfs operations.
126
_default_url = 'fakeremotetransport://host/path/'
128
def __init__(self, url=None):
130
url = self._default_url
134
return "%r(%r)" % (self.__class__.__name__,
137
def clone(self, relpath):
138
return FakeRemoteTransport(urlutils.join(self.base, relpath))
140
def get(self, relpath):
141
# only get is specifically stubbed out, because it's usually the first
142
# thing we do. anything else will fail with an AttributeError.
143
raise AssertionError("%r doesn't support file access to %r"
148
class FakeProtocol(object):
149
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
151
def __init__(self, body, fake_client):
153
self._body_buffer = None
154
self._fake_client = fake_client
156
def read_body_bytes(self, count=-1):
157
if self._body_buffer is None:
158
self._body_buffer = StringIO(self.body)
159
bytes = self._body_buffer.read(count)
160
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
161
self._fake_client.expecting_body = False
164
def cancel_read_body(self):
165
self._fake_client.expecting_body = False
167
def read_streamed_body(self):
171
class FakeClient(_SmartClient):
172
"""Lookalike for _SmartClient allowing testing."""
174
def __init__(self, fake_medium_base='fake base'):
175
"""Create a FakeClient."""
178
self.expecting_body = False
179
# if non-None, this is the list of expected calls, with only the
180
# method name and arguments included. the body might be hard to
181
# compute so is not included
182
self._expected_calls = None
183
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
185
def add_expected_call(self, call_name, call_args, response_type,
186
response_args, response_body=None):
187
if self._expected_calls is None:
188
self._expected_calls = []
189
self._expected_calls.append((call_name, call_args))
190
self.responses.append((response_type, response_args, response_body))
192
def add_success_response(self, *args):
193
self.responses.append(('success', args, None))
195
def add_success_response_with_body(self, body, *args):
196
self.responses.append(('success', args, body))
198
def add_error_response(self, *args):
199
self.responses.append(('error', args))
201
def add_unknown_method_response(self, verb):
202
self.responses.append(('unknown', verb))
204
def finished_test(self):
205
if self._expected_calls:
206
raise AssertionError("%r finished but was still expecting %r"
207
% (self, self._expected_calls[0]))
209
def _get_next_response(self):
211
response_tuple = self.responses.pop(0)
212
except IndexError, e:
213
raise AssertionError("%r didn't expect any more calls"
215
if response_tuple[0] == 'unknown':
216
raise errors.UnknownSmartMethod(response_tuple[1])
217
elif response_tuple[0] == 'error':
218
raise errors.ErrorFromSmartServer(response_tuple[1])
219
return response_tuple
221
def _check_call(self, method, args):
222
if self._expected_calls is None:
223
# the test should be updated to say what it expects
226
next_call = self._expected_calls.pop(0)
228
raise AssertionError("%r didn't expect any more calls "
230
% (self, method, args,))
231
if method != next_call[0] or args != next_call[1]:
232
raise AssertionError("%r expected %r%r "
234
% (self, next_call[0], next_call[1], method, args,))
236
def call(self, method, *args):
237
self._check_call(method, args)
238
self._calls.append(('call', method, args))
239
return self._get_next_response()[1]
241
def call_expecting_body(self, method, *args):
242
self._check_call(method, args)
243
self._calls.append(('call_expecting_body', method, args))
244
result = self._get_next_response()
245
self.expecting_body = True
246
return result[1], FakeProtocol(result[2], self)
248
def call_with_body_bytes_expecting_body(self, method, args, body):
249
self._check_call(method, args)
250
self._calls.append(('call_with_body_bytes_expecting_body', method,
252
result = self._get_next_response()
253
self.expecting_body = True
254
return result[1], FakeProtocol(result[2], self)
256
def call_with_body_stream(self, args, stream):
257
# Explicitly consume the stream before checking for an error, because
258
# that's what happens a real medium.
259
stream = list(stream)
260
self._check_call(args[0], args[1:])
261
self._calls.append(('call_with_body_stream', args[0], args[1:], stream))
262
return self._get_next_response()[1]
265
class FakeMedium(medium.SmartClientMedium):
267
def __init__(self, client_calls, base):
268
medium.SmartClientMedium.__init__(self, base)
269
self._client_calls = client_calls
271
def disconnect(self):
272
self._client_calls.append(('disconnect medium',))
275
class TestVfsHas(tests.TestCase):
277
def test_unicode_path(self):
278
client = FakeClient('/')
279
client.add_success_response('yes',)
280
transport = RemoteTransport('bzr://localhost/', _client=client)
281
filename = u'/hell\u00d8'.encode('utf8')
282
result = transport.has(filename)
284
[('call', 'has', (filename,))],
286
self.assertTrue(result)
289
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
290
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
292
def assertRemotePath(self, expected, client_base, transport_base):
293
"""Assert that the result of
294
SmartClientMedium.remote_path_from_transport is the expected value for
295
a given client_base and transport_base.
297
client_medium = medium.SmartClientMedium(client_base)
298
transport = get_transport(transport_base)
299
result = client_medium.remote_path_from_transport(transport)
300
self.assertEqual(expected, result)
302
def test_remote_path_from_transport(self):
303
"""SmartClientMedium.remote_path_from_transport calculates a URL for
304
the given transport relative to the root of the client base URL.
306
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
307
self.assertRemotePath(
308
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
310
def assertRemotePathHTTP(self, expected, transport_base, relpath):
311
"""Assert that the result of
312
HttpTransportBase.remote_path_from_transport is the expected value for
313
a given transport_base and relpath of that transport. (Note that
314
HttpTransportBase is a subclass of SmartClientMedium)
316
base_transport = get_transport(transport_base)
317
client_medium = base_transport.get_smart_medium()
318
cloned_transport = base_transport.clone(relpath)
319
result = client_medium.remote_path_from_transport(cloned_transport)
320
self.assertEqual(expected, result)
322
def test_remote_path_from_transport_http(self):
323
"""Remote paths for HTTP transports are calculated differently to other
324
transports. They are just relative to the client base, not the root
325
directory of the host.
327
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
328
self.assertRemotePathHTTP(
329
'../xyz/', scheme + '//host/path', '../xyz/')
330
self.assertRemotePathHTTP(
331
'xyz/', scheme + '//host/path', 'xyz/')
334
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
335
"""Tests for the behaviour of client_medium.remote_is_at_least."""
337
def test_initially_unlimited(self):
338
"""A fresh medium assumes that the remote side supports all
341
client_medium = medium.SmartClientMedium('dummy base')
342
self.assertFalse(client_medium._is_remote_before((99, 99)))
344
def test__remember_remote_is_before(self):
345
"""Calling _remember_remote_is_before ratchets down the known remote
348
client_medium = medium.SmartClientMedium('dummy base')
349
# Mark the remote side as being less than 1.6. The remote side may
351
client_medium._remember_remote_is_before((1, 6))
352
self.assertTrue(client_medium._is_remote_before((1, 6)))
353
self.assertFalse(client_medium._is_remote_before((1, 5)))
354
# Calling _remember_remote_is_before again with a lower value works.
355
client_medium._remember_remote_is_before((1, 5))
356
self.assertTrue(client_medium._is_remote_before((1, 5)))
357
# You cannot call _remember_remote_is_before with a larger value.
359
AssertionError, client_medium._remember_remote_is_before, (1, 9))
362
class TestBzrDirOpenBranch(tests.TestCase):
364
def test_branch_present(self):
365
transport = MemoryTransport()
366
transport.mkdir('quack')
367
transport = transport.clone('quack')
368
client = FakeClient(transport.base)
369
client.add_expected_call(
370
'BzrDir.open_branch', ('quack/',),
371
'success', ('ok', ''))
372
client.add_expected_call(
373
'BzrDir.find_repositoryV2', ('quack/',),
374
'success', ('ok', '', 'no', 'no', 'no'))
375
client.add_expected_call(
376
'Branch.get_stacked_on_url', ('quack/',),
377
'error', ('NotStacked',))
378
bzrdir = RemoteBzrDir(transport, _client=client)
379
result = bzrdir.open_branch()
380
self.assertIsInstance(result, RemoteBranch)
381
self.assertEqual(bzrdir, result.bzrdir)
382
client.finished_test()
384
def test_branch_missing(self):
385
transport = MemoryTransport()
386
transport.mkdir('quack')
387
transport = transport.clone('quack')
388
client = FakeClient(transport.base)
389
client.add_error_response('nobranch')
390
bzrdir = RemoteBzrDir(transport, _client=client)
391
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
393
[('call', 'BzrDir.open_branch', ('quack/',))],
396
def test__get_tree_branch(self):
397
# _get_tree_branch is a form of open_branch, but it should only ask for
398
# branch opening, not any other network requests.
401
calls.append("Called")
403
transport = MemoryTransport()
404
# no requests on the network - catches other api calls being made.
405
client = FakeClient(transport.base)
406
bzrdir = RemoteBzrDir(transport, _client=client)
407
# patch the open_branch call to record that it was called.
408
bzrdir.open_branch = open_branch
409
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
410
self.assertEqual(["Called"], calls)
411
self.assertEqual([], client._calls)
413
def test_url_quoting_of_path(self):
414
# Relpaths on the wire should not be URL-escaped. So "~" should be
415
# transmitted as "~", not "%7E".
416
transport = RemoteTCPTransport('bzr://localhost/~hello/')
417
client = FakeClient(transport.base)
418
client.add_expected_call(
419
'BzrDir.open_branch', ('~hello/',),
420
'success', ('ok', ''))
421
client.add_expected_call(
422
'BzrDir.find_repositoryV2', ('~hello/',),
423
'success', ('ok', '', 'no', 'no', 'no'))
424
client.add_expected_call(
425
'Branch.get_stacked_on_url', ('~hello/',),
426
'error', ('NotStacked',))
427
bzrdir = RemoteBzrDir(transport, _client=client)
428
result = bzrdir.open_branch()
429
client.finished_test()
431
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
432
transport = MemoryTransport()
433
transport.mkdir('quack')
434
transport = transport.clone('quack')
436
rich_response = 'yes'
440
subtree_response = 'yes'
442
subtree_response = 'no'
443
client = FakeClient(transport.base)
444
client.add_success_response(
445
'ok', '', rich_response, subtree_response, external_lookup)
446
bzrdir = RemoteBzrDir(transport, _client=client)
447
result = bzrdir.open_repository()
449
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
451
self.assertIsInstance(result, RemoteRepository)
452
self.assertEqual(bzrdir, result.bzrdir)
453
self.assertEqual(rich_root, result._format.rich_root_data)
454
self.assertEqual(subtrees, result._format.supports_tree_reference)
456
def test_open_repository_sets_format_attributes(self):
457
self.check_open_repository(True, True)
458
self.check_open_repository(False, True)
459
self.check_open_repository(True, False)
460
self.check_open_repository(False, False)
461
self.check_open_repository(False, False, 'yes')
463
def test_old_server(self):
464
"""RemoteBzrDirFormat should fail to probe if the server version is too
467
self.assertRaises(errors.NotBranchError,
468
RemoteBzrDirFormat.probe_transport, OldServerTransport())
471
class TestBzrDirOpenRepository(tests.TestCase):
473
def test_backwards_compat_1_2(self):
474
transport = MemoryTransport()
475
transport.mkdir('quack')
476
transport = transport.clone('quack')
477
client = FakeClient(transport.base)
478
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
479
client.add_success_response('ok', '', 'no', 'no')
480
bzrdir = RemoteBzrDir(transport, _client=client)
481
repo = bzrdir.open_repository()
483
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
484
('call', 'BzrDir.find_repository', ('quack/',))],
488
class OldSmartClient(object):
489
"""A fake smart client for test_old_version that just returns a version one
490
response to the 'hello' (query version) command.
493
def get_request(self):
494
input_file = StringIO('ok\x011\n')
495
output_file = StringIO()
496
client_medium = medium.SmartSimplePipesClientMedium(
497
input_file, output_file)
498
return medium.SmartClientStreamMediumRequest(client_medium)
500
def protocol_version(self):
504
class OldServerTransport(object):
505
"""A fake transport for test_old_server that reports it's smart server
506
protocol version as version one.
512
def get_smart_client(self):
513
return OldSmartClient()
516
class RemoteBranchTestCase(tests.TestCase):
518
def make_remote_branch(self, transport, client):
519
"""Make a RemoteBranch using 'client' as its _SmartClient.
521
A RemoteBzrDir and RemoteRepository will also be created to fill out
522
the RemoteBranch, albeit with stub values for some of their attributes.
524
# we do not want bzrdir to make any remote calls, so use False as its
525
# _client. If it tries to make a remote call, this will fail
527
bzrdir = RemoteBzrDir(transport, _client=False)
528
repo = RemoteRepository(bzrdir, None, _client=client)
529
return RemoteBranch(bzrdir, repo, _client=client)
532
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
534
def test_empty_branch(self):
535
# in an empty branch we decode the response properly
536
transport = MemoryTransport()
537
client = FakeClient(transport.base)
538
client.add_expected_call(
539
'Branch.get_stacked_on_url', ('quack/',),
540
'error', ('NotStacked',))
541
client.add_expected_call(
542
'Branch.last_revision_info', ('quack/',),
543
'success', ('ok', '0', 'null:'))
544
transport.mkdir('quack')
545
transport = transport.clone('quack')
546
branch = self.make_remote_branch(transport, client)
547
result = branch.last_revision_info()
548
client.finished_test()
549
self.assertEqual((0, NULL_REVISION), result)
551
def test_non_empty_branch(self):
552
# in a non-empty branch we also decode the response properly
553
revid = u'\xc8'.encode('utf8')
554
transport = MemoryTransport()
555
client = FakeClient(transport.base)
556
client.add_expected_call(
557
'Branch.get_stacked_on_url', ('kwaak/',),
558
'error', ('NotStacked',))
559
client.add_expected_call(
560
'Branch.last_revision_info', ('kwaak/',),
561
'success', ('ok', '2', revid))
562
transport.mkdir('kwaak')
563
transport = transport.clone('kwaak')
564
branch = self.make_remote_branch(transport, client)
565
result = branch.last_revision_info()
566
self.assertEqual((2, revid), result)
569
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
570
"""Test Branch._get_stacked_on_url rpc"""
572
def test_get_stacked_on_invalid_url(self):
573
raise tests.KnownFailure('opening a branch requires the server to open the fallback repository')
574
transport = FakeRemoteTransport('fakeremotetransport:///')
575
client = FakeClient(transport.base)
576
client.add_expected_call(
577
'Branch.get_stacked_on_url', ('.',),
578
'success', ('ok', 'file:///stacked/on'))
579
bzrdir = RemoteBzrDir(transport, _client=client)
580
branch = RemoteBranch(bzrdir, None, _client=client)
581
result = branch.get_stacked_on_url()
583
'file:///stacked/on', result)
585
def test_backwards_compatible(self):
586
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
587
base_branch = self.make_branch('base', format='1.6')
588
stacked_branch = self.make_branch('stacked', format='1.6')
589
stacked_branch.set_stacked_on_url('../base')
590
client = FakeClient(self.get_url())
591
client.add_expected_call(
592
'BzrDir.open_branch', ('stacked/',),
593
'success', ('ok', ''))
594
client.add_expected_call(
595
'BzrDir.find_repositoryV2', ('stacked/',),
596
'success', ('ok', '', 'no', 'no', 'no'))
597
# called twice, once from constructor and then again by us
598
client.add_expected_call(
599
'Branch.get_stacked_on_url', ('stacked/',),
600
'unknown', ('Branch.get_stacked_on_url',))
601
client.add_expected_call(
602
'Branch.get_stacked_on_url', ('stacked/',),
603
'unknown', ('Branch.get_stacked_on_url',))
604
# this will also do vfs access, but that goes direct to the transport
605
# and isn't seen by the FakeClient.
606
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
607
branch = bzrdir.open_branch()
608
result = branch.get_stacked_on_url()
609
self.assertEqual('../base', result)
610
client.finished_test()
611
# it's in the fallback list both for the RemoteRepository and its vfs
613
self.assertEqual(1, len(branch.repository._fallback_repositories))
615
len(branch.repository._real_repository._fallback_repositories))
617
def test_get_stacked_on_real_branch(self):
618
base_branch = self.make_branch('base', format='1.6')
619
stacked_branch = self.make_branch('stacked', format='1.6')
620
stacked_branch.set_stacked_on_url('../base')
621
client = FakeClient(self.get_url())
622
client.add_expected_call(
623
'BzrDir.open_branch', ('stacked/',),
624
'success', ('ok', ''))
625
client.add_expected_call(
626
'BzrDir.find_repositoryV2', ('stacked/',),
627
'success', ('ok', '', 'no', 'no', 'no'))
628
# called twice, once from constructor and then again by us
629
client.add_expected_call(
630
'Branch.get_stacked_on_url', ('stacked/',),
631
'success', ('ok', '../base'))
632
client.add_expected_call(
633
'Branch.get_stacked_on_url', ('stacked/',),
634
'success', ('ok', '../base'))
635
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
636
branch = bzrdir.open_branch()
637
result = branch.get_stacked_on_url()
638
self.assertEqual('../base', result)
639
client.finished_test()
640
# it's in the fallback list both for the RemoteRepository and its vfs
642
self.assertEqual(1, len(branch.repository._fallback_repositories))
644
len(branch.repository._real_repository._fallback_repositories))
647
class TestBranchSetLastRevision(RemoteBranchTestCase):
649
def test_set_empty(self):
650
# set_revision_history([]) is translated to calling
651
# Branch.set_last_revision(path, '') on the wire.
652
transport = MemoryTransport()
653
transport.mkdir('branch')
654
transport = transport.clone('branch')
656
client = FakeClient(transport.base)
657
client.add_expected_call(
658
'Branch.get_stacked_on_url', ('branch/',),
659
'error', ('NotStacked',))
660
client.add_expected_call(
661
'Branch.lock_write', ('branch/', '', ''),
662
'success', ('ok', 'branch token', 'repo token'))
663
client.add_expected_call(
664
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
666
client.add_expected_call(
667
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
669
branch = self.make_remote_branch(transport, client)
670
# This is a hack to work around the problem that RemoteBranch currently
671
# unnecessarily invokes _ensure_real upon a call to lock_write.
672
branch._ensure_real = lambda: None
674
result = branch.set_revision_history([])
676
self.assertEqual(None, result)
677
client.finished_test()
679
def test_set_nonempty(self):
680
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
681
# Branch.set_last_revision(path, rev-idN) on the wire.
682
transport = MemoryTransport()
683
transport.mkdir('branch')
684
transport = transport.clone('branch')
686
client = FakeClient(transport.base)
687
client.add_expected_call(
688
'Branch.get_stacked_on_url', ('branch/',),
689
'error', ('NotStacked',))
690
client.add_expected_call(
691
'Branch.lock_write', ('branch/', '', ''),
692
'success', ('ok', 'branch token', 'repo token'))
693
client.add_expected_call(
694
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
696
client.add_expected_call(
697
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
699
branch = self.make_remote_branch(transport, client)
700
# This is a hack to work around the problem that RemoteBranch currently
701
# unnecessarily invokes _ensure_real upon a call to lock_write.
702
branch._ensure_real = lambda: None
703
# Lock the branch, reset the record of remote calls.
705
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
707
self.assertEqual(None, result)
708
client.finished_test()
710
def test_no_such_revision(self):
711
transport = MemoryTransport()
712
transport.mkdir('branch')
713
transport = transport.clone('branch')
714
# A response of 'NoSuchRevision' is translated into an exception.
715
client = FakeClient(transport.base)
716
client.add_expected_call(
717
'Branch.get_stacked_on_url', ('branch/',),
718
'error', ('NotStacked',))
719
client.add_expected_call(
720
'Branch.lock_write', ('branch/', '', ''),
721
'success', ('ok', 'branch token', 'repo token'))
722
client.add_expected_call(
723
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
724
'error', ('NoSuchRevision', 'rev-id'))
725
client.add_expected_call(
726
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
729
branch = self.make_remote_branch(transport, client)
732
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
734
client.finished_test()
736
def test_tip_change_rejected(self):
737
"""TipChangeRejected responses cause a TipChangeRejected exception to
740
transport = MemoryTransport()
741
transport.mkdir('branch')
742
transport = transport.clone('branch')
743
client = FakeClient(transport.base)
744
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
745
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
746
client.add_expected_call(
747
'Branch.get_stacked_on_url', ('branch/',),
748
'error', ('NotStacked',))
749
client.add_expected_call(
750
'Branch.lock_write', ('branch/', '', ''),
751
'success', ('ok', 'branch token', 'repo token'))
752
client.add_expected_call(
753
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
754
'error', ('TipChangeRejected', rejection_msg_utf8))
755
client.add_expected_call(
756
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
758
branch = self.make_remote_branch(transport, client)
759
branch._ensure_real = lambda: None
761
self.addCleanup(branch.unlock)
762
# The 'TipChangeRejected' error response triggered by calling
763
# set_revision_history causes a TipChangeRejected exception.
764
err = self.assertRaises(
765
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
766
# The UTF-8 message from the response has been decoded into a unicode
768
self.assertIsInstance(err.msg, unicode)
769
self.assertEqual(rejection_msg_unicode, err.msg)
771
client.finished_test()
774
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
776
def test_set_last_revision_info(self):
777
# set_last_revision_info(num, 'rev-id') is translated to calling
778
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
779
transport = MemoryTransport()
780
transport.mkdir('branch')
781
transport = transport.clone('branch')
782
client = FakeClient(transport.base)
784
client.add_error_response('NotStacked')
786
client.add_success_response('ok', 'branch token', 'repo token')
788
client.add_success_response('ok')
790
client.add_success_response('ok')
792
branch = self.make_remote_branch(transport, client)
793
# Lock the branch, reset the record of remote calls.
796
result = branch.set_last_revision_info(1234, 'a-revision-id')
798
[('call', 'Branch.set_last_revision_info',
799
('branch/', 'branch token', 'repo token',
800
'1234', 'a-revision-id'))],
802
self.assertEqual(None, result)
804
def test_no_such_revision(self):
805
# A response of 'NoSuchRevision' is translated into an exception.
806
transport = MemoryTransport()
807
transport.mkdir('branch')
808
transport = transport.clone('branch')
809
client = FakeClient(transport.base)
811
client.add_error_response('NotStacked')
813
client.add_success_response('ok', 'branch token', 'repo token')
815
client.add_error_response('NoSuchRevision', 'revid')
817
client.add_success_response('ok')
819
branch = self.make_remote_branch(transport, client)
820
# Lock the branch, reset the record of remote calls.
825
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
828
def lock_remote_branch(self, branch):
829
"""Trick a RemoteBranch into thinking it is locked."""
830
branch._lock_mode = 'w'
831
branch._lock_count = 2
832
branch._lock_token = 'branch token'
833
branch._repo_lock_token = 'repo token'
834
branch.repository._lock_mode = 'w'
835
branch.repository._lock_count = 2
836
branch.repository._lock_token = 'repo token'
838
def test_backwards_compatibility(self):
839
"""If the server does not support the Branch.set_last_revision_info
840
verb (which is new in 1.4), then the client falls back to VFS methods.
842
# This test is a little messy. Unlike most tests in this file, it
843
# doesn't purely test what a Remote* object sends over the wire, and
844
# how it reacts to responses from the wire. It instead relies partly
845
# on asserting that the RemoteBranch will call
846
# self._real_branch.set_last_revision_info(...).
848
# First, set up our RemoteBranch with a FakeClient that raises
849
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
850
transport = MemoryTransport()
851
transport.mkdir('branch')
852
transport = transport.clone('branch')
853
client = FakeClient(transport.base)
854
client.add_expected_call(
855
'Branch.get_stacked_on_url', ('branch/',),
856
'error', ('NotStacked',))
857
client.add_expected_call(
858
'Branch.set_last_revision_info',
859
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
860
'unknown', 'Branch.set_last_revision_info')
862
branch = self.make_remote_branch(transport, client)
863
class StubRealBranch(object):
866
def set_last_revision_info(self, revno, revision_id):
868
('set_last_revision_info', revno, revision_id))
869
def _clear_cached_state(self):
871
real_branch = StubRealBranch()
872
branch._real_branch = real_branch
873
self.lock_remote_branch(branch)
875
# Call set_last_revision_info, and verify it behaved as expected.
876
result = branch.set_last_revision_info(1234, 'a-revision-id')
878
[('set_last_revision_info', 1234, 'a-revision-id')],
880
client.finished_test()
882
def test_unexpected_error(self):
883
# If the server sends an error the client doesn't understand, it gets
884
# turned into an UnknownErrorFromSmartServer, which is presented as a
885
# non-internal error to the user.
886
transport = MemoryTransport()
887
transport.mkdir('branch')
888
transport = transport.clone('branch')
889
client = FakeClient(transport.base)
891
client.add_error_response('NotStacked')
893
client.add_success_response('ok', 'branch token', 'repo token')
895
client.add_error_response('UnexpectedError')
897
client.add_success_response('ok')
899
branch = self.make_remote_branch(transport, client)
900
# Lock the branch, reset the record of remote calls.
904
err = self.assertRaises(
905
errors.UnknownErrorFromSmartServer,
906
branch.set_last_revision_info, 123, 'revid')
907
self.assertEqual(('UnexpectedError',), err.error_tuple)
910
def test_tip_change_rejected(self):
911
"""TipChangeRejected responses cause a TipChangeRejected exception to
914
transport = MemoryTransport()
915
transport.mkdir('branch')
916
transport = transport.clone('branch')
917
client = FakeClient(transport.base)
919
client.add_error_response('NotStacked')
921
client.add_success_response('ok', 'branch token', 'repo token')
923
client.add_error_response('TipChangeRejected', 'rejection message')
925
client.add_success_response('ok')
927
branch = self.make_remote_branch(transport, client)
928
# Lock the branch, reset the record of remote calls.
930
self.addCleanup(branch.unlock)
933
# The 'TipChangeRejected' error response triggered by calling
934
# set_last_revision_info causes a TipChangeRejected exception.
935
err = self.assertRaises(
936
errors.TipChangeRejected,
937
branch.set_last_revision_info, 123, 'revid')
938
self.assertEqual('rejection message', err.msg)
941
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
942
"""Getting the branch configuration should use an abstract method not vfs.
945
def test_get_branch_conf(self):
946
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
947
## # We should see that branch.get_config() does a single rpc to get the
948
## # remote configuration file, abstracting away where that is stored on
949
## # the server. However at the moment it always falls back to using the
950
## # vfs, and this would need some changes in config.py.
952
## # in an empty branch we decode the response properly
953
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
954
## # we need to make a real branch because the remote_branch.control_files
955
## # will trigger _ensure_real.
956
## branch = self.make_branch('quack')
957
## transport = branch.bzrdir.root_transport
958
## # we do not want bzrdir to make any remote calls
959
## bzrdir = RemoteBzrDir(transport, _client=False)
960
## branch = RemoteBranch(bzrdir, None, _client=client)
961
## config = branch.get_config()
963
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
967
class TestBranchLockWrite(RemoteBranchTestCase):
969
def test_lock_write_unlockable(self):
970
transport = MemoryTransport()
971
client = FakeClient(transport.base)
972
client.add_expected_call(
973
'Branch.get_stacked_on_url', ('quack/',),
974
'error', ('NotStacked',),)
975
client.add_expected_call(
976
'Branch.lock_write', ('quack/', '', ''),
977
'error', ('UnlockableTransport',))
978
transport.mkdir('quack')
979
transport = transport.clone('quack')
980
branch = self.make_remote_branch(transport, client)
981
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
982
client.finished_test()
985
class TestTransportIsReadonly(tests.TestCase):
988
client = FakeClient()
989
client.add_success_response('yes')
990
transport = RemoteTransport('bzr://example.com/', medium=False,
992
self.assertEqual(True, transport.is_readonly())
994
[('call', 'Transport.is_readonly', ())],
997
def test_false(self):
998
client = FakeClient()
999
client.add_success_response('no')
1000
transport = RemoteTransport('bzr://example.com/', medium=False,
1002
self.assertEqual(False, transport.is_readonly())
1004
[('call', 'Transport.is_readonly', ())],
1007
def test_error_from_old_server(self):
1008
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1010
Clients should treat it as a "no" response, because is_readonly is only
1011
advisory anyway (a transport could be read-write, but then the
1012
underlying filesystem could be readonly anyway).
1014
client = FakeClient()
1015
client.add_unknown_method_response('Transport.is_readonly')
1016
transport = RemoteTransport('bzr://example.com/', medium=False,
1018
self.assertEqual(False, transport.is_readonly())
1020
[('call', 'Transport.is_readonly', ())],
1024
class TestTransportMkdir(tests.TestCase):
1026
def test_permissiondenied(self):
1027
client = FakeClient()
1028
client.add_error_response('PermissionDenied', 'remote path', 'extra')
1029
transport = RemoteTransport('bzr://example.com/', medium=False,
1031
exc = self.assertRaises(
1032
errors.PermissionDenied, transport.mkdir, 'client path')
1033
expected_error = errors.PermissionDenied('/client path', 'extra')
1034
self.assertEqual(expected_error, exc)
1037
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1039
def test_defaults_to_none(self):
1040
t = RemoteSSHTransport('bzr+ssh://example.com')
1041
self.assertIs(None, t._get_credentials()[0])
1043
def test_uses_authentication_config(self):
1044
conf = config.AuthenticationConfig()
1045
conf._get_config().update(
1046
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1049
t = RemoteSSHTransport('bzr+ssh://example.com')
1050
self.assertEqual('bar', t._get_credentials()[0])
1053
class TestRemoteRepository(tests.TestCase):
1054
"""Base for testing RemoteRepository protocol usage.
1056
These tests contain frozen requests and responses. We want any changes to
1057
what is sent or expected to be require a thoughtful update to these tests
1058
because they might break compatibility with different-versioned servers.
1061
def setup_fake_client_and_repository(self, transport_path):
1062
"""Create the fake client and repository for testing with.
1064
There's no real server here; we just have canned responses sent
1067
:param transport_path: Path below the root of the MemoryTransport
1068
where the repository will be created.
1070
transport = MemoryTransport()
1071
transport.mkdir(transport_path)
1072
client = FakeClient(transport.base)
1073
transport = transport.clone(transport_path)
1074
# we do not want bzrdir to make any remote calls
1075
bzrdir = RemoteBzrDir(transport, _client=False)
1076
repo = RemoteRepository(bzrdir, None, _client=client)
1080
class TestRepositoryGatherStats(TestRemoteRepository):
1082
def test_revid_none(self):
1083
# ('ok',), body with revisions and size
1084
transport_path = 'quack'
1085
repo, client = self.setup_fake_client_and_repository(transport_path)
1086
client.add_success_response_with_body(
1087
'revisions: 2\nsize: 18\n', 'ok')
1088
result = repo.gather_stats(None)
1090
[('call_expecting_body', 'Repository.gather_stats',
1091
('quack/','','no'))],
1093
self.assertEqual({'revisions': 2, 'size': 18}, result)
1095
def test_revid_no_committers(self):
1096
# ('ok',), body without committers
1097
body = ('firstrev: 123456.300 3600\n'
1098
'latestrev: 654231.400 0\n'
1101
transport_path = 'quick'
1102
revid = u'\xc8'.encode('utf8')
1103
repo, client = self.setup_fake_client_and_repository(transport_path)
1104
client.add_success_response_with_body(body, 'ok')
1105
result = repo.gather_stats(revid)
1107
[('call_expecting_body', 'Repository.gather_stats',
1108
('quick/', revid, 'no'))],
1110
self.assertEqual({'revisions': 2, 'size': 18,
1111
'firstrev': (123456.300, 3600),
1112
'latestrev': (654231.400, 0),},
1115
def test_revid_with_committers(self):
1116
# ('ok',), body with committers
1117
body = ('committers: 128\n'
1118
'firstrev: 123456.300 3600\n'
1119
'latestrev: 654231.400 0\n'
1122
transport_path = 'buick'
1123
revid = u'\xc8'.encode('utf8')
1124
repo, client = self.setup_fake_client_and_repository(transport_path)
1125
client.add_success_response_with_body(body, 'ok')
1126
result = repo.gather_stats(revid, True)
1128
[('call_expecting_body', 'Repository.gather_stats',
1129
('buick/', revid, 'yes'))],
1131
self.assertEqual({'revisions': 2, 'size': 18,
1133
'firstrev': (123456.300, 3600),
1134
'latestrev': (654231.400, 0),},
1138
class TestRepositoryGetGraph(TestRemoteRepository):
1140
def test_get_graph(self):
1141
# get_graph returns a graph with a custom parents provider.
1142
transport_path = 'quack'
1143
repo, client = self.setup_fake_client_and_repository(transport_path)
1144
graph = repo.get_graph()
1145
self.assertNotEqual(graph._parents_provider, repo)
1148
class TestRepositoryGetParentMap(TestRemoteRepository):
1150
def test_get_parent_map_caching(self):
1151
# get_parent_map returns from cache until unlock()
1152
# setup a reponse with two revisions
1153
r1 = u'\u0e33'.encode('utf8')
1154
r2 = u'\u0dab'.encode('utf8')
1155
lines = [' '.join([r2, r1]), r1]
1156
encoded_body = bz2.compress('\n'.join(lines))
1158
transport_path = 'quack'
1159
repo, client = self.setup_fake_client_and_repository(transport_path)
1160
client.add_success_response_with_body(encoded_body, 'ok')
1161
client.add_success_response_with_body(encoded_body, 'ok')
1163
graph = repo.get_graph()
1164
parents = graph.get_parent_map([r2])
1165
self.assertEqual({r2: (r1,)}, parents)
1166
# locking and unlocking deeper should not reset
1169
parents = graph.get_parent_map([r1])
1170
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1172
[('call_with_body_bytes_expecting_body',
1173
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1176
# now we call again, and it should use the second response.
1178
graph = repo.get_graph()
1179
parents = graph.get_parent_map([r1])
1180
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1182
[('call_with_body_bytes_expecting_body',
1183
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1184
('call_with_body_bytes_expecting_body',
1185
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1190
def test_get_parent_map_reconnects_if_unknown_method(self):
1191
transport_path = 'quack'
1192
repo, client = self.setup_fake_client_and_repository(transport_path)
1193
client.add_unknown_method_response('Repository,get_parent_map')
1194
client.add_success_response_with_body('', 'ok')
1195
self.assertFalse(client._medium._is_remote_before((1, 2)))
1196
rev_id = 'revision-id'
1197
expected_deprecations = [
1198
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1200
parents = self.callDeprecated(
1201
expected_deprecations, repo.get_parent_map, [rev_id])
1203
[('call_with_body_bytes_expecting_body',
1204
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1205
('disconnect medium',),
1206
('call_expecting_body', 'Repository.get_revision_graph',
1209
# The medium is now marked as being connected to an older server
1210
self.assertTrue(client._medium._is_remote_before((1, 2)))
1212
def test_get_parent_map_fallback_parentless_node(self):
1213
"""get_parent_map falls back to get_revision_graph on old servers. The
1214
results from get_revision_graph are tweaked to match the get_parent_map
1217
Specifically, a {key: ()} result from get_revision_graph means "no
1218
parents" for that key, which in get_parent_map results should be
1219
represented as {key: ('null:',)}.
1221
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1223
rev_id = 'revision-id'
1224
transport_path = 'quack'
1225
repo, client = self.setup_fake_client_and_repository(transport_path)
1226
client.add_success_response_with_body(rev_id, 'ok')
1227
client._medium._remember_remote_is_before((1, 2))
1228
expected_deprecations = [
1229
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1231
parents = self.callDeprecated(
1232
expected_deprecations, repo.get_parent_map, [rev_id])
1234
[('call_expecting_body', 'Repository.get_revision_graph',
1237
self.assertEqual({rev_id: ('null:',)}, parents)
1239
def test_get_parent_map_unexpected_response(self):
1240
repo, client = self.setup_fake_client_and_repository('path')
1241
client.add_success_response('something unexpected!')
1243
errors.UnexpectedSmartServerResponse,
1244
repo.get_parent_map, ['a-revision-id'])
1247
class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
1249
def test_allows_new_revisions(self):
1250
"""get_parent_map's results can be updated by commit."""
1251
smart_server = server.SmartTCPServer_for_testing()
1252
smart_server.setUp()
1253
self.addCleanup(smart_server.tearDown)
1254
self.make_branch('branch')
1255
branch = Branch.open(smart_server.get_url() + '/branch')
1256
tree = branch.create_checkout('tree', lightweight=True)
1258
self.addCleanup(tree.unlock)
1259
graph = tree.branch.repository.get_graph()
1260
# This provides an opportunity for the missing rev-id to be cached.
1261
self.assertEqual({}, graph.get_parent_map(['rev1']))
1262
tree.commit('message', rev_id='rev1')
1263
graph = tree.branch.repository.get_graph()
1264
self.assertEqual({'rev1': ('null:',)}, graph.get_parent_map(['rev1']))
1267
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1269
def test_null_revision(self):
1270
# a null revision has the predictable result {}, we should have no wire
1271
# traffic when calling it with this argument
1272
transport_path = 'empty'
1273
repo, client = self.setup_fake_client_and_repository(transport_path)
1274
client.add_success_response('notused')
1275
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1277
self.assertEqual([], client._calls)
1278
self.assertEqual({}, result)
1280
def test_none_revision(self):
1281
# with none we want the entire graph
1282
r1 = u'\u0e33'.encode('utf8')
1283
r2 = u'\u0dab'.encode('utf8')
1284
lines = [' '.join([r2, r1]), r1]
1285
encoded_body = '\n'.join(lines)
1287
transport_path = 'sinhala'
1288
repo, client = self.setup_fake_client_and_repository(transport_path)
1289
client.add_success_response_with_body(encoded_body, 'ok')
1290
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1292
[('call_expecting_body', 'Repository.get_revision_graph',
1295
self.assertEqual({r1: (), r2: (r1, )}, result)
1297
def test_specific_revision(self):
1298
# with a specific revision we want the graph for that
1299
# with none we want the entire graph
1300
r11 = u'\u0e33'.encode('utf8')
1301
r12 = u'\xc9'.encode('utf8')
1302
r2 = u'\u0dab'.encode('utf8')
1303
lines = [' '.join([r2, r11, r12]), r11, r12]
1304
encoded_body = '\n'.join(lines)
1306
transport_path = 'sinhala'
1307
repo, client = self.setup_fake_client_and_repository(transport_path)
1308
client.add_success_response_with_body(encoded_body, 'ok')
1309
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1311
[('call_expecting_body', 'Repository.get_revision_graph',
1314
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1316
def test_no_such_revision(self):
1318
transport_path = 'sinhala'
1319
repo, client = self.setup_fake_client_and_repository(transport_path)
1320
client.add_error_response('nosuchrevision', revid)
1321
# also check that the right revision is reported in the error
1322
self.assertRaises(errors.NoSuchRevision,
1323
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1325
[('call_expecting_body', 'Repository.get_revision_graph',
1326
('sinhala/', revid))],
1329
def test_unexpected_error(self):
1331
transport_path = 'sinhala'
1332
repo, client = self.setup_fake_client_and_repository(transport_path)
1333
client.add_error_response('AnUnexpectedError')
1334
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1335
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1336
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1339
class TestRepositoryIsShared(TestRemoteRepository):
1341
def test_is_shared(self):
1342
# ('yes', ) for Repository.is_shared -> 'True'.
1343
transport_path = 'quack'
1344
repo, client = self.setup_fake_client_and_repository(transport_path)
1345
client.add_success_response('yes')
1346
result = repo.is_shared()
1348
[('call', 'Repository.is_shared', ('quack/',))],
1350
self.assertEqual(True, result)
1352
def test_is_not_shared(self):
1353
# ('no', ) for Repository.is_shared -> 'False'.
1354
transport_path = 'qwack'
1355
repo, client = self.setup_fake_client_and_repository(transport_path)
1356
client.add_success_response('no')
1357
result = repo.is_shared()
1359
[('call', 'Repository.is_shared', ('qwack/',))],
1361
self.assertEqual(False, result)
1364
class TestRepositoryLockWrite(TestRemoteRepository):
1366
def test_lock_write(self):
1367
transport_path = 'quack'
1368
repo, client = self.setup_fake_client_and_repository(transport_path)
1369
client.add_success_response('ok', 'a token')
1370
result = repo.lock_write()
1372
[('call', 'Repository.lock_write', ('quack/', ''))],
1374
self.assertEqual('a token', result)
1376
def test_lock_write_already_locked(self):
1377
transport_path = 'quack'
1378
repo, client = self.setup_fake_client_and_repository(transport_path)
1379
client.add_error_response('LockContention')
1380
self.assertRaises(errors.LockContention, repo.lock_write)
1382
[('call', 'Repository.lock_write', ('quack/', ''))],
1385
def test_lock_write_unlockable(self):
1386
transport_path = 'quack'
1387
repo, client = self.setup_fake_client_and_repository(transport_path)
1388
client.add_error_response('UnlockableTransport')
1389
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1391
[('call', 'Repository.lock_write', ('quack/', ''))],
1395
class TestRepositoryUnlock(TestRemoteRepository):
1397
def test_unlock(self):
1398
transport_path = 'quack'
1399
repo, client = self.setup_fake_client_and_repository(transport_path)
1400
client.add_success_response('ok', 'a token')
1401
client.add_success_response('ok')
1405
[('call', 'Repository.lock_write', ('quack/', '')),
1406
('call', 'Repository.unlock', ('quack/', 'a token'))],
1409
def test_unlock_wrong_token(self):
1410
# If somehow the token is wrong, unlock will raise TokenMismatch.
1411
transport_path = 'quack'
1412
repo, client = self.setup_fake_client_and_repository(transport_path)
1413
client.add_success_response('ok', 'a token')
1414
client.add_error_response('TokenMismatch')
1416
self.assertRaises(errors.TokenMismatch, repo.unlock)
1419
class TestRepositoryHasRevision(TestRemoteRepository):
1421
def test_none(self):
1422
# repo.has_revision(None) should not cause any traffic.
1423
transport_path = 'quack'
1424
repo, client = self.setup_fake_client_and_repository(transport_path)
1426
# The null revision is always there, so has_revision(None) == True.
1427
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1429
# The remote repo shouldn't be accessed.
1430
self.assertEqual([], client._calls)
1433
class TestRepositoryTarball(TestRemoteRepository):
1435
# This is a canned tarball reponse we can validate against
1437
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1438
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1439
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1440
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1441
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1442
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1443
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1444
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1445
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1446
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1447
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1448
'nWQ7QH/F3JFOFCQ0aSPfA='
1451
def test_repository_tarball(self):
1452
# Test that Repository.tarball generates the right operations
1453
transport_path = 'repo'
1454
expected_calls = [('call_expecting_body', 'Repository.tarball',
1455
('repo/', 'bz2',),),
1457
repo, client = self.setup_fake_client_and_repository(transport_path)
1458
client.add_success_response_with_body(self.tarball_content, 'ok')
1459
# Now actually ask for the tarball
1460
tarball_file = repo._get_tarball('bz2')
1462
self.assertEqual(expected_calls, client._calls)
1463
self.assertEqual(self.tarball_content, tarball_file.read())
1465
tarball_file.close()
1468
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1469
"""RemoteRepository.copy_content_into optimizations"""
1471
def test_copy_content_remote_to_local(self):
1472
self.transport_server = server.SmartTCPServer_for_testing
1473
src_repo = self.make_repository('repo1')
1474
src_repo = repository.Repository.open(self.get_url('repo1'))
1475
# At the moment the tarball-based copy_content_into can't write back
1476
# into a smart server. It would be good if it could upload the
1477
# tarball; once that works we'd have to create repositories of
1478
# different formats. -- mbp 20070410
1479
dest_url = self.get_vfs_only_url('repo2')
1480
dest_bzrdir = BzrDir.create(dest_url)
1481
dest_repo = dest_bzrdir.create_repository()
1482
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1483
self.assertTrue(isinstance(src_repo, RemoteRepository))
1484
src_repo.copy_content_into(dest_repo)
1487
class _StubRealPackRepository(object):
1489
def __init__(self, calls):
1490
self._pack_collection = _StubPackCollection(calls)
1493
class _StubPackCollection(object):
1495
def __init__(self, calls):
1499
self.calls.append(('pack collection autopack',))
1501
def reload_pack_names(self):
1502
self.calls.append(('pack collection reload_pack_names',))
1505
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1506
"""Tests for RemoteRepository.autopack implementation."""
1509
"""When the server returns 'ok' and there's no _real_repository, then
1510
nothing else happens: the autopack method is done.
1512
transport_path = 'quack'
1513
repo, client = self.setup_fake_client_and_repository(transport_path)
1514
client.add_expected_call(
1515
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1517
client.finished_test()
1519
def test_ok_with_real_repo(self):
1520
"""When the server returns 'ok' and there is a _real_repository, then
1521
the _real_repository's reload_pack_name's method will be called.
1523
transport_path = 'quack'
1524
repo, client = self.setup_fake_client_and_repository(transport_path)
1525
client.add_expected_call(
1526
'PackRepository.autopack', ('quack/',),
1528
repo._real_repository = _StubRealPackRepository(client._calls)
1531
[('call', 'PackRepository.autopack', ('quack/',)),
1532
('pack collection reload_pack_names',)],
1535
def test_backwards_compatibility(self):
1536
"""If the server does not recognise the PackRepository.autopack verb,
1537
fallback to the real_repository's implementation.
1539
transport_path = 'quack'
1540
repo, client = self.setup_fake_client_and_repository(transport_path)
1541
client.add_unknown_method_response('PackRepository.autopack')
1542
def stub_ensure_real():
1543
client._calls.append(('_ensure_real',))
1544
repo._real_repository = _StubRealPackRepository(client._calls)
1545
repo._ensure_real = stub_ensure_real
1548
[('call', 'PackRepository.autopack', ('quack/',)),
1550
('pack collection autopack',)],
1554
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1555
"""Base class for unit tests for bzrlib.remote._translate_error."""
1557
def translateTuple(self, error_tuple, **context):
1558
"""Call _translate_error with an ErrorFromSmartServer built from the
1561
:param error_tuple: A tuple of a smart server response, as would be
1562
passed to an ErrorFromSmartServer.
1563
:kwargs context: context items to call _translate_error with.
1565
:returns: The error raised by _translate_error.
1567
# Raise the ErrorFromSmartServer before passing it as an argument,
1568
# because _translate_error may need to re-raise it with a bare 'raise'
1570
server_error = errors.ErrorFromSmartServer(error_tuple)
1571
translated_error = self.translateErrorFromSmartServer(
1572
server_error, **context)
1573
return translated_error
1575
def translateErrorFromSmartServer(self, error_object, **context):
1576
"""Like translateTuple, but takes an already constructed
1577
ErrorFromSmartServer rather than a tuple.
1581
except errors.ErrorFromSmartServer, server_error:
1582
translated_error = self.assertRaises(
1583
errors.BzrError, remote._translate_error, server_error,
1585
return translated_error
1588
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1589
"""Unit tests for bzrlib.remote._translate_error.
1591
Given an ErrorFromSmartServer (which has an error tuple from a smart
1592
server) and some context, _translate_error raises more specific errors from
1595
This test case covers the cases where _translate_error succeeds in
1596
translating an ErrorFromSmartServer to something better. See
1597
TestErrorTranslationRobustness for other cases.
1600
def test_NoSuchRevision(self):
1601
branch = self.make_branch('')
1603
translated_error = self.translateTuple(
1604
('NoSuchRevision', revid), branch=branch)
1605
expected_error = errors.NoSuchRevision(branch, revid)
1606
self.assertEqual(expected_error, translated_error)
1608
def test_nosuchrevision(self):
1609
repository = self.make_repository('')
1611
translated_error = self.translateTuple(
1612
('nosuchrevision', revid), repository=repository)
1613
expected_error = errors.NoSuchRevision(repository, revid)
1614
self.assertEqual(expected_error, translated_error)
1616
def test_nobranch(self):
1617
bzrdir = self.make_bzrdir('')
1618
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1619
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1620
self.assertEqual(expected_error, translated_error)
1622
def test_LockContention(self):
1623
translated_error = self.translateTuple(('LockContention',))
1624
expected_error = errors.LockContention('(remote lock)')
1625
self.assertEqual(expected_error, translated_error)
1627
def test_UnlockableTransport(self):
1628
bzrdir = self.make_bzrdir('')
1629
translated_error = self.translateTuple(
1630
('UnlockableTransport',), bzrdir=bzrdir)
1631
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1632
self.assertEqual(expected_error, translated_error)
1634
def test_LockFailed(self):
1635
lock = 'str() of a server lock'
1636
why = 'str() of why'
1637
translated_error = self.translateTuple(('LockFailed', lock, why))
1638
expected_error = errors.LockFailed(lock, why)
1639
self.assertEqual(expected_error, translated_error)
1641
def test_TokenMismatch(self):
1642
token = 'a lock token'
1643
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1644
expected_error = errors.TokenMismatch(token, '(remote token)')
1645
self.assertEqual(expected_error, translated_error)
1647
def test_Diverged(self):
1648
branch = self.make_branch('a')
1649
other_branch = self.make_branch('b')
1650
translated_error = self.translateTuple(
1651
('Diverged',), branch=branch, other_branch=other_branch)
1652
expected_error = errors.DivergedBranches(branch, other_branch)
1653
self.assertEqual(expected_error, translated_error)
1655
def test_ReadError_no_args(self):
1657
translated_error = self.translateTuple(('ReadError',), path=path)
1658
expected_error = errors.ReadError(path)
1659
self.assertEqual(expected_error, translated_error)
1661
def test_ReadError(self):
1663
translated_error = self.translateTuple(('ReadError', path))
1664
expected_error = errors.ReadError(path)
1665
self.assertEqual(expected_error, translated_error)
1667
def test_PermissionDenied_no_args(self):
1669
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1670
expected_error = errors.PermissionDenied(path)
1671
self.assertEqual(expected_error, translated_error)
1673
def test_PermissionDenied_one_arg(self):
1675
translated_error = self.translateTuple(('PermissionDenied', path))
1676
expected_error = errors.PermissionDenied(path)
1677
self.assertEqual(expected_error, translated_error)
1679
def test_PermissionDenied_one_arg_and_context(self):
1680
"""Given a choice between a path from the local context and a path on
1681
the wire, _translate_error prefers the path from the local context.
1683
local_path = 'local path'
1684
remote_path = 'remote path'
1685
translated_error = self.translateTuple(
1686
('PermissionDenied', remote_path), path=local_path)
1687
expected_error = errors.PermissionDenied(local_path)
1688
self.assertEqual(expected_error, translated_error)
1690
def test_PermissionDenied_two_args(self):
1692
extra = 'a string with extra info'
1693
translated_error = self.translateTuple(
1694
('PermissionDenied', path, extra))
1695
expected_error = errors.PermissionDenied(path, extra)
1696
self.assertEqual(expected_error, translated_error)
1699
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1700
"""Unit tests for bzrlib.remote._translate_error's robustness.
1702
TestErrorTranslationSuccess is for cases where _translate_error can
1703
translate successfully. This class about how _translate_err behaves when
1704
it fails to translate: it re-raises the original error.
1707
def test_unrecognised_server_error(self):
1708
"""If the error code from the server is not recognised, the original
1709
ErrorFromSmartServer is propagated unmodified.
1711
error_tuple = ('An unknown error tuple',)
1712
server_error = errors.ErrorFromSmartServer(error_tuple)
1713
translated_error = self.translateErrorFromSmartServer(server_error)
1714
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1715
self.assertEqual(expected_error, translated_error)
1717
def test_context_missing_a_key(self):
1718
"""In case of a bug in the client, or perhaps an unexpected response
1719
from a server, _translate_error returns the original error tuple from
1720
the server and mutters a warning.
1722
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1723
# in the context dict. So let's give it an empty context dict instead
1724
# to exercise its error recovery.
1726
error_tuple = ('NoSuchRevision', 'revid')
1727
server_error = errors.ErrorFromSmartServer(error_tuple)
1728
translated_error = self.translateErrorFromSmartServer(server_error)
1729
self.assertEqual(server_error, translated_error)
1730
# In addition to re-raising ErrorFromSmartServer, some debug info has
1731
# been muttered to the log file for developer to look at.
1732
self.assertContainsRe(
1733
self._get_log(keep_log_file=True),
1734
"Missing key 'branch' in context")
1736
def test_path_missing(self):
1737
"""Some translations (PermissionDenied, ReadError) can determine the
1738
'path' variable from either the wire or the local context. If neither
1739
has it, then an error is raised.
1741
error_tuple = ('ReadError',)
1742
server_error = errors.ErrorFromSmartServer(error_tuple)
1743
translated_error = self.translateErrorFromSmartServer(server_error)
1744
self.assertEqual(server_error, translated_error)
1745
# In addition to re-raising ErrorFromSmartServer, some debug info has
1746
# been muttered to the log file for developer to look at.
1747
self.assertContainsRe(
1748
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1751
class TestStacking(tests.TestCaseWithTransport):
1752
"""Tests for operations on stacked remote repositories.
1754
The underlying format type must support stacking.
1757
def test_access_stacked_remote(self):
1758
# based on <http://launchpad.net/bugs/261315>
1759
# make a branch stacked on another repository containing an empty
1760
# revision, then open it over hpss - we should be able to see that
1762
base_transport = self.get_transport()
1763
base_builder = self.make_branch_builder('base', format='1.6')
1764
base_builder.start_series()
1765
base_revid = base_builder.build_snapshot('rev-id', None,
1766
[('add', ('', None, 'directory', None))],
1768
base_builder.finish_series()
1769
stacked_branch = self.make_branch('stacked', format='1.6')
1770
stacked_branch.set_stacked_on_url('../base')
1771
# start a server looking at this
1772
smart_server = server.SmartTCPServer_for_testing()
1773
smart_server.setUp()
1774
self.addCleanup(smart_server.tearDown)
1775
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1776
# can get its branch and repository
1777
remote_branch = remote_bzrdir.open_branch()
1778
remote_repo = remote_branch.repository
1779
remote_repo.lock_read()
1781
# it should have an appropriate fallback repository, which should also
1782
# be a RemoteRepository
1783
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1784
self.assertIsInstance(remote_repo._fallback_repositories[0],
1786
# and it has the revision committed to the underlying repository;
1787
# these have varying implementations so we try several of them
1788
self.assertTrue(remote_repo.has_revisions([base_revid]))
1789
self.assertTrue(remote_repo.has_revision(base_revid))
1790
self.assertEqual(remote_repo.get_revision(base_revid).message,
1793
remote_repo.unlock()
1795
def prepare_stacked_remote_branch(self):
1796
smart_server = server.SmartTCPServer_for_testing()
1797
smart_server.setUp()
1798
self.addCleanup(smart_server.tearDown)
1799
tree1 = self.make_branch_and_tree('tree1')
1800
tree1.commit('rev1', rev_id='rev1')
1801
tree2 = self.make_branch_and_tree('tree2', format='1.6')
1802
tree2.branch.set_stacked_on_url(tree1.branch.base)
1803
branch2 = Branch.open(smart_server.get_url() + '/tree2')
1805
self.addCleanup(branch2.unlock)
1808
def test_stacked_get_parent_map(self):
1809
# the public implementation of get_parent_map obeys stacking
1810
branch = self.prepare_stacked_remote_branch()
1811
repo = branch.repository
1812
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
1814
def test_unstacked_get_parent_map(self):
1815
# _unstacked_provider.get_parent_map ignores stacking
1816
branch = self.prepare_stacked_remote_branch()
1817
provider = branch.repository._unstacked_provider
1818
self.assertEqual([], provider.get_parent_map(['rev1']).keys())
1821
class TestRemoteBranchEffort(tests.TestCaseWithTransport):
1824
super(TestRemoteBranchEffort, self).setUp()
1825
# Create a smart server that publishes whatever the backing VFS server
1827
self.smart_server = server.SmartTCPServer_for_testing()
1828
self.smart_server.setUp(self.get_server())
1829
self.addCleanup(self.smart_server.tearDown)
1830
# Log all HPSS calls into self.hpss_calls.
1831
_SmartClient.hooks.install_named_hook(
1832
'call', self.capture_hpss_call, None)
1833
self.hpss_calls = []
1835
def capture_hpss_call(self, params):
1836
self.hpss_calls.append(params.method)
1838
def test_copy_content_into_avoids_revision_history(self):
1839
local = self.make_branch('local')
1840
remote_backing_tree = self.make_branch_and_tree('remote')
1841
remote_backing_tree.commit("Commit.")
1842
remote_branch_url = self.smart_server.get_url() + 'remote'
1843
remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
1844
local.repository.fetch(remote_branch.repository)
1845
self.hpss_calls = []
1846
remote_branch.copy_content_into(local)
1847
self.assertFalse('Branch.revision_history' in self.hpss_calls)