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 TestTransportMkdir(tests.TestCase):
1017
def test_permissiondenied(self):
1018
client = FakeClient()
1019
client.add_error_response('PermissionDenied', 'remote path', 'extra')
1020
transport = RemoteTransport('bzr://example.com/', medium=False,
1022
exc = self.assertRaises(
1023
errors.PermissionDenied, transport.mkdir, 'client path')
1024
expected_error = errors.PermissionDenied('/client path', 'extra')
1025
self.assertEqual(expected_error, exc)
1028
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1030
def test_defaults_to_none(self):
1031
t = RemoteSSHTransport('bzr+ssh://example.com')
1032
self.assertIs(None, t._get_credentials()[0])
1034
def test_uses_authentication_config(self):
1035
conf = config.AuthenticationConfig()
1036
conf._get_config().update(
1037
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1040
t = RemoteSSHTransport('bzr+ssh://example.com')
1041
self.assertEqual('bar', t._get_credentials()[0])
1044
class TestRemoteRepository(tests.TestCase):
1045
"""Base for testing RemoteRepository protocol usage.
1047
These tests contain frozen requests and responses. We want any changes to
1048
what is sent or expected to be require a thoughtful update to these tests
1049
because they might break compatibility with different-versioned servers.
1052
def setup_fake_client_and_repository(self, transport_path):
1053
"""Create the fake client and repository for testing with.
1055
There's no real server here; we just have canned responses sent
1058
:param transport_path: Path below the root of the MemoryTransport
1059
where the repository will be created.
1061
transport = MemoryTransport()
1062
transport.mkdir(transport_path)
1063
client = FakeClient(transport.base)
1064
transport = transport.clone(transport_path)
1065
# we do not want bzrdir to make any remote calls
1066
bzrdir = RemoteBzrDir(transport, _client=False)
1067
repo = RemoteRepository(bzrdir, None, _client=client)
1071
class TestRepositoryGatherStats(TestRemoteRepository):
1073
def test_revid_none(self):
1074
# ('ok',), body with revisions and size
1075
transport_path = 'quack'
1076
repo, client = self.setup_fake_client_and_repository(transport_path)
1077
client.add_success_response_with_body(
1078
'revisions: 2\nsize: 18\n', 'ok')
1079
result = repo.gather_stats(None)
1081
[('call_expecting_body', 'Repository.gather_stats',
1082
('quack/','','no'))],
1084
self.assertEqual({'revisions': 2, 'size': 18}, result)
1086
def test_revid_no_committers(self):
1087
# ('ok',), body without committers
1088
body = ('firstrev: 123456.300 3600\n'
1089
'latestrev: 654231.400 0\n'
1092
transport_path = 'quick'
1093
revid = u'\xc8'.encode('utf8')
1094
repo, client = self.setup_fake_client_and_repository(transport_path)
1095
client.add_success_response_with_body(body, 'ok')
1096
result = repo.gather_stats(revid)
1098
[('call_expecting_body', 'Repository.gather_stats',
1099
('quick/', revid, 'no'))],
1101
self.assertEqual({'revisions': 2, 'size': 18,
1102
'firstrev': (123456.300, 3600),
1103
'latestrev': (654231.400, 0),},
1106
def test_revid_with_committers(self):
1107
# ('ok',), body with committers
1108
body = ('committers: 128\n'
1109
'firstrev: 123456.300 3600\n'
1110
'latestrev: 654231.400 0\n'
1113
transport_path = 'buick'
1114
revid = u'\xc8'.encode('utf8')
1115
repo, client = self.setup_fake_client_and_repository(transport_path)
1116
client.add_success_response_with_body(body, 'ok')
1117
result = repo.gather_stats(revid, True)
1119
[('call_expecting_body', 'Repository.gather_stats',
1120
('buick/', revid, 'yes'))],
1122
self.assertEqual({'revisions': 2, 'size': 18,
1124
'firstrev': (123456.300, 3600),
1125
'latestrev': (654231.400, 0),},
1129
class TestRepositoryGetGraph(TestRemoteRepository):
1131
def test_get_graph(self):
1132
# get_graph returns a graph with a custom parents provider.
1133
transport_path = 'quack'
1134
repo, client = self.setup_fake_client_and_repository(transport_path)
1135
graph = repo.get_graph()
1136
self.assertNotEqual(graph._parents_provider, repo)
1139
class TestRepositoryGetParentMap(TestRemoteRepository):
1141
def test_get_parent_map_caching(self):
1142
# get_parent_map returns from cache until unlock()
1143
# setup a reponse with two revisions
1144
r1 = u'\u0e33'.encode('utf8')
1145
r2 = u'\u0dab'.encode('utf8')
1146
lines = [' '.join([r2, r1]), r1]
1147
encoded_body = bz2.compress('\n'.join(lines))
1149
transport_path = 'quack'
1150
repo, client = self.setup_fake_client_and_repository(transport_path)
1151
client.add_success_response_with_body(encoded_body, 'ok')
1152
client.add_success_response_with_body(encoded_body, 'ok')
1154
graph = repo.get_graph()
1155
parents = graph.get_parent_map([r2])
1156
self.assertEqual({r2: (r1,)}, parents)
1157
# locking and unlocking deeper should not reset
1160
parents = graph.get_parent_map([r1])
1161
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1163
[('call_with_body_bytes_expecting_body',
1164
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1167
# now we call again, and it should use the second response.
1169
graph = repo.get_graph()
1170
parents = graph.get_parent_map([r1])
1171
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1173
[('call_with_body_bytes_expecting_body',
1174
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1175
('call_with_body_bytes_expecting_body',
1176
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1181
def test_get_parent_map_reconnects_if_unknown_method(self):
1182
transport_path = 'quack'
1183
repo, client = self.setup_fake_client_and_repository(transport_path)
1184
client.add_unknown_method_response('Repository,get_parent_map')
1185
client.add_success_response_with_body('', 'ok')
1186
self.assertFalse(client._medium._is_remote_before((1, 2)))
1187
rev_id = 'revision-id'
1188
expected_deprecations = [
1189
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1191
parents = self.callDeprecated(
1192
expected_deprecations, repo.get_parent_map, [rev_id])
1194
[('call_with_body_bytes_expecting_body',
1195
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1196
('disconnect medium',),
1197
('call_expecting_body', 'Repository.get_revision_graph',
1200
# The medium is now marked as being connected to an older server
1201
self.assertTrue(client._medium._is_remote_before((1, 2)))
1203
def test_get_parent_map_fallback_parentless_node(self):
1204
"""get_parent_map falls back to get_revision_graph on old servers. The
1205
results from get_revision_graph are tweaked to match the get_parent_map
1208
Specifically, a {key: ()} result from get_revision_graph means "no
1209
parents" for that key, which in get_parent_map results should be
1210
represented as {key: ('null:',)}.
1212
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1214
rev_id = 'revision-id'
1215
transport_path = 'quack'
1216
repo, client = self.setup_fake_client_and_repository(transport_path)
1217
client.add_success_response_with_body(rev_id, 'ok')
1218
client._medium._remember_remote_is_before((1, 2))
1219
expected_deprecations = [
1220
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1222
parents = self.callDeprecated(
1223
expected_deprecations, repo.get_parent_map, [rev_id])
1225
[('call_expecting_body', 'Repository.get_revision_graph',
1228
self.assertEqual({rev_id: ('null:',)}, parents)
1230
def test_get_parent_map_unexpected_response(self):
1231
repo, client = self.setup_fake_client_and_repository('path')
1232
client.add_success_response('something unexpected!')
1234
errors.UnexpectedSmartServerResponse,
1235
repo.get_parent_map, ['a-revision-id'])
1238
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1240
def test_null_revision(self):
1241
# a null revision has the predictable result {}, we should have no wire
1242
# traffic when calling it with this argument
1243
transport_path = 'empty'
1244
repo, client = self.setup_fake_client_and_repository(transport_path)
1245
client.add_success_response('notused')
1246
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1248
self.assertEqual([], client._calls)
1249
self.assertEqual({}, result)
1251
def test_none_revision(self):
1252
# with none we want the entire graph
1253
r1 = u'\u0e33'.encode('utf8')
1254
r2 = u'\u0dab'.encode('utf8')
1255
lines = [' '.join([r2, r1]), r1]
1256
encoded_body = '\n'.join(lines)
1258
transport_path = 'sinhala'
1259
repo, client = self.setup_fake_client_and_repository(transport_path)
1260
client.add_success_response_with_body(encoded_body, 'ok')
1261
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1263
[('call_expecting_body', 'Repository.get_revision_graph',
1266
self.assertEqual({r1: (), r2: (r1, )}, result)
1268
def test_specific_revision(self):
1269
# with a specific revision we want the graph for that
1270
# with none we want the entire graph
1271
r11 = u'\u0e33'.encode('utf8')
1272
r12 = u'\xc9'.encode('utf8')
1273
r2 = u'\u0dab'.encode('utf8')
1274
lines = [' '.join([r2, r11, r12]), r11, r12]
1275
encoded_body = '\n'.join(lines)
1277
transport_path = 'sinhala'
1278
repo, client = self.setup_fake_client_and_repository(transport_path)
1279
client.add_success_response_with_body(encoded_body, 'ok')
1280
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1282
[('call_expecting_body', 'Repository.get_revision_graph',
1285
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1287
def test_no_such_revision(self):
1289
transport_path = 'sinhala'
1290
repo, client = self.setup_fake_client_and_repository(transport_path)
1291
client.add_error_response('nosuchrevision', revid)
1292
# also check that the right revision is reported in the error
1293
self.assertRaises(errors.NoSuchRevision,
1294
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1296
[('call_expecting_body', 'Repository.get_revision_graph',
1297
('sinhala/', revid))],
1300
def test_unexpected_error(self):
1302
transport_path = 'sinhala'
1303
repo, client = self.setup_fake_client_and_repository(transport_path)
1304
client.add_error_response('AnUnexpectedError')
1305
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1306
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1307
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1310
class TestRepositoryIsShared(TestRemoteRepository):
1312
def test_is_shared(self):
1313
# ('yes', ) for Repository.is_shared -> 'True'.
1314
transport_path = 'quack'
1315
repo, client = self.setup_fake_client_and_repository(transport_path)
1316
client.add_success_response('yes')
1317
result = repo.is_shared()
1319
[('call', 'Repository.is_shared', ('quack/',))],
1321
self.assertEqual(True, result)
1323
def test_is_not_shared(self):
1324
# ('no', ) for Repository.is_shared -> 'False'.
1325
transport_path = 'qwack'
1326
repo, client = self.setup_fake_client_and_repository(transport_path)
1327
client.add_success_response('no')
1328
result = repo.is_shared()
1330
[('call', 'Repository.is_shared', ('qwack/',))],
1332
self.assertEqual(False, result)
1335
class TestRepositoryLockWrite(TestRemoteRepository):
1337
def test_lock_write(self):
1338
transport_path = 'quack'
1339
repo, client = self.setup_fake_client_and_repository(transport_path)
1340
client.add_success_response('ok', 'a token')
1341
result = repo.lock_write()
1343
[('call', 'Repository.lock_write', ('quack/', ''))],
1345
self.assertEqual('a token', result)
1347
def test_lock_write_already_locked(self):
1348
transport_path = 'quack'
1349
repo, client = self.setup_fake_client_and_repository(transport_path)
1350
client.add_error_response('LockContention')
1351
self.assertRaises(errors.LockContention, repo.lock_write)
1353
[('call', 'Repository.lock_write', ('quack/', ''))],
1356
def test_lock_write_unlockable(self):
1357
transport_path = 'quack'
1358
repo, client = self.setup_fake_client_and_repository(transport_path)
1359
client.add_error_response('UnlockableTransport')
1360
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1362
[('call', 'Repository.lock_write', ('quack/', ''))],
1366
class TestRepositoryUnlock(TestRemoteRepository):
1368
def test_unlock(self):
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_success_response('ok')
1376
[('call', 'Repository.lock_write', ('quack/', '')),
1377
('call', 'Repository.unlock', ('quack/', 'a token'))],
1380
def test_unlock_wrong_token(self):
1381
# If somehow the token is wrong, unlock will raise TokenMismatch.
1382
transport_path = 'quack'
1383
repo, client = self.setup_fake_client_and_repository(transport_path)
1384
client.add_success_response('ok', 'a token')
1385
client.add_error_response('TokenMismatch')
1387
self.assertRaises(errors.TokenMismatch, repo.unlock)
1390
class TestRepositoryHasRevision(TestRemoteRepository):
1392
def test_none(self):
1393
# repo.has_revision(None) should not cause any traffic.
1394
transport_path = 'quack'
1395
repo, client = self.setup_fake_client_and_repository(transport_path)
1397
# The null revision is always there, so has_revision(None) == True.
1398
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1400
# The remote repo shouldn't be accessed.
1401
self.assertEqual([], client._calls)
1404
class TestRepositoryTarball(TestRemoteRepository):
1406
# This is a canned tarball reponse we can validate against
1408
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1409
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1410
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1411
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1412
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1413
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1414
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1415
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1416
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1417
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1418
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1419
'nWQ7QH/F3JFOFCQ0aSPfA='
1422
def test_repository_tarball(self):
1423
# Test that Repository.tarball generates the right operations
1424
transport_path = 'repo'
1425
expected_calls = [('call_expecting_body', 'Repository.tarball',
1426
('repo/', 'bz2',),),
1428
repo, client = self.setup_fake_client_and_repository(transport_path)
1429
client.add_success_response_with_body(self.tarball_content, 'ok')
1430
# Now actually ask for the tarball
1431
tarball_file = repo._get_tarball('bz2')
1433
self.assertEqual(expected_calls, client._calls)
1434
self.assertEqual(self.tarball_content, tarball_file.read())
1436
tarball_file.close()
1439
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1440
"""RemoteRepository.copy_content_into optimizations"""
1442
def test_copy_content_remote_to_local(self):
1443
self.transport_server = server.SmartTCPServer_for_testing
1444
src_repo = self.make_repository('repo1')
1445
src_repo = repository.Repository.open(self.get_url('repo1'))
1446
# At the moment the tarball-based copy_content_into can't write back
1447
# into a smart server. It would be good if it could upload the
1448
# tarball; once that works we'd have to create repositories of
1449
# different formats. -- mbp 20070410
1450
dest_url = self.get_vfs_only_url('repo2')
1451
dest_bzrdir = BzrDir.create(dest_url)
1452
dest_repo = dest_bzrdir.create_repository()
1453
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1454
self.assertTrue(isinstance(src_repo, RemoteRepository))
1455
src_repo.copy_content_into(dest_repo)
1458
class _StubRealPackRepository(object):
1460
def __init__(self, calls):
1461
self._pack_collection = _StubPackCollection(calls)
1464
class _StubPackCollection(object):
1466
def __init__(self, calls):
1470
self.calls.append(('pack collection autopack',))
1472
def reload_pack_names(self):
1473
self.calls.append(('pack collection reload_pack_names',))
1476
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1477
"""Tests for RemoteRepository.autopack implementation."""
1480
"""When the server returns 'ok' and there's no _real_repository, then
1481
nothing else happens: the autopack method is done.
1483
transport_path = 'quack'
1484
repo, client = self.setup_fake_client_and_repository(transport_path)
1485
client.add_expected_call(
1486
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1488
client.finished_test()
1490
def test_ok_with_real_repo(self):
1491
"""When the server returns 'ok' and there is a _real_repository, then
1492
the _real_repository's reload_pack_name's method will be called.
1494
transport_path = 'quack'
1495
repo, client = self.setup_fake_client_and_repository(transport_path)
1496
client.add_expected_call(
1497
'PackRepository.autopack', ('quack/',),
1499
repo._real_repository = _StubRealPackRepository(client._calls)
1502
[('call', 'PackRepository.autopack', ('quack/',)),
1503
('pack collection reload_pack_names',)],
1506
def test_backwards_compatibility(self):
1507
"""If the server does not recognise the PackRepository.autopack verb,
1508
fallback to the real_repository's implementation.
1510
transport_path = 'quack'
1511
repo, client = self.setup_fake_client_and_repository(transport_path)
1512
client.add_unknown_method_response('PackRepository.autopack')
1513
def stub_ensure_real():
1514
client._calls.append(('_ensure_real',))
1515
repo._real_repository = _StubRealPackRepository(client._calls)
1516
repo._ensure_real = stub_ensure_real
1519
[('call', 'PackRepository.autopack', ('quack/',)),
1521
('pack collection autopack',)],
1525
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1526
"""Base class for unit tests for bzrlib.remote._translate_error."""
1528
def translateTuple(self, error_tuple, **context):
1529
"""Call _translate_error with an ErrorFromSmartServer built from the
1532
:param error_tuple: A tuple of a smart server response, as would be
1533
passed to an ErrorFromSmartServer.
1534
:kwargs context: context items to call _translate_error with.
1536
:returns: The error raised by _translate_error.
1538
# Raise the ErrorFromSmartServer before passing it as an argument,
1539
# because _translate_error may need to re-raise it with a bare 'raise'
1541
server_error = errors.ErrorFromSmartServer(error_tuple)
1542
translated_error = self.translateErrorFromSmartServer(
1543
server_error, **context)
1544
return translated_error
1546
def translateErrorFromSmartServer(self, error_object, **context):
1547
"""Like translateTuple, but takes an already constructed
1548
ErrorFromSmartServer rather than a tuple.
1552
except errors.ErrorFromSmartServer, server_error:
1553
translated_error = self.assertRaises(
1554
errors.BzrError, remote._translate_error, server_error,
1556
return translated_error
1559
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1560
"""Unit tests for bzrlib.remote._translate_error.
1562
Given an ErrorFromSmartServer (which has an error tuple from a smart
1563
server) and some context, _translate_error raises more specific errors from
1566
This test case covers the cases where _translate_error succeeds in
1567
translating an ErrorFromSmartServer to something better. See
1568
TestErrorTranslationRobustness for other cases.
1571
def test_NoSuchRevision(self):
1572
branch = self.make_branch('')
1574
translated_error = self.translateTuple(
1575
('NoSuchRevision', revid), branch=branch)
1576
expected_error = errors.NoSuchRevision(branch, revid)
1577
self.assertEqual(expected_error, translated_error)
1579
def test_nosuchrevision(self):
1580
repository = self.make_repository('')
1582
translated_error = self.translateTuple(
1583
('nosuchrevision', revid), repository=repository)
1584
expected_error = errors.NoSuchRevision(repository, revid)
1585
self.assertEqual(expected_error, translated_error)
1587
def test_nobranch(self):
1588
bzrdir = self.make_bzrdir('')
1589
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1590
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1591
self.assertEqual(expected_error, translated_error)
1593
def test_LockContention(self):
1594
translated_error = self.translateTuple(('LockContention',))
1595
expected_error = errors.LockContention('(remote lock)')
1596
self.assertEqual(expected_error, translated_error)
1598
def test_UnlockableTransport(self):
1599
bzrdir = self.make_bzrdir('')
1600
translated_error = self.translateTuple(
1601
('UnlockableTransport',), bzrdir=bzrdir)
1602
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1603
self.assertEqual(expected_error, translated_error)
1605
def test_LockFailed(self):
1606
lock = 'str() of a server lock'
1607
why = 'str() of why'
1608
translated_error = self.translateTuple(('LockFailed', lock, why))
1609
expected_error = errors.LockFailed(lock, why)
1610
self.assertEqual(expected_error, translated_error)
1612
def test_TokenMismatch(self):
1613
token = 'a lock token'
1614
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1615
expected_error = errors.TokenMismatch(token, '(remote token)')
1616
self.assertEqual(expected_error, translated_error)
1618
def test_Diverged(self):
1619
branch = self.make_branch('a')
1620
other_branch = self.make_branch('b')
1621
translated_error = self.translateTuple(
1622
('Diverged',), branch=branch, other_branch=other_branch)
1623
expected_error = errors.DivergedBranches(branch, other_branch)
1624
self.assertEqual(expected_error, translated_error)
1626
def test_ReadError_no_args(self):
1628
translated_error = self.translateTuple(('ReadError',), path=path)
1629
expected_error = errors.ReadError(path)
1630
self.assertEqual(expected_error, translated_error)
1632
def test_ReadError(self):
1634
translated_error = self.translateTuple(('ReadError', path))
1635
expected_error = errors.ReadError(path)
1636
self.assertEqual(expected_error, translated_error)
1638
def test_PermissionDenied_no_args(self):
1640
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1641
expected_error = errors.PermissionDenied(path)
1642
self.assertEqual(expected_error, translated_error)
1644
def test_PermissionDenied_one_arg(self):
1646
translated_error = self.translateTuple(('PermissionDenied', path))
1647
expected_error = errors.PermissionDenied(path)
1648
self.assertEqual(expected_error, translated_error)
1650
def test_PermissionDenied_one_arg_and_context(self):
1651
"""Given a choice between a path from the local context and a path on
1652
the wire, _translate_error prefers the path from the local context.
1654
local_path = 'local path'
1655
remote_path = 'remote path'
1656
translated_error = self.translateTuple(
1657
('PermissionDenied', remote_path), path=local_path)
1658
expected_error = errors.PermissionDenied(local_path)
1659
self.assertEqual(expected_error, translated_error)
1661
def test_PermissionDenied_two_args(self):
1663
extra = 'a string with extra info'
1664
translated_error = self.translateTuple(
1665
('PermissionDenied', path, extra))
1666
expected_error = errors.PermissionDenied(path, extra)
1667
self.assertEqual(expected_error, translated_error)
1670
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1671
"""Unit tests for bzrlib.remote._translate_error's robustness.
1673
TestErrorTranslationSuccess is for cases where _translate_error can
1674
translate successfully. This class about how _translate_err behaves when
1675
it fails to translate: it re-raises the original error.
1678
def test_unrecognised_server_error(self):
1679
"""If the error code from the server is not recognised, the original
1680
ErrorFromSmartServer is propagated unmodified.
1682
error_tuple = ('An unknown error tuple',)
1683
server_error = errors.ErrorFromSmartServer(error_tuple)
1684
translated_error = self.translateErrorFromSmartServer(server_error)
1685
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1686
self.assertEqual(expected_error, translated_error)
1688
def test_context_missing_a_key(self):
1689
"""In case of a bug in the client, or perhaps an unexpected response
1690
from a server, _translate_error returns the original error tuple from
1691
the server and mutters a warning.
1693
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1694
# in the context dict. So let's give it an empty context dict instead
1695
# to exercise its error recovery.
1697
error_tuple = ('NoSuchRevision', 'revid')
1698
server_error = errors.ErrorFromSmartServer(error_tuple)
1699
translated_error = self.translateErrorFromSmartServer(server_error)
1700
self.assertEqual(server_error, translated_error)
1701
# In addition to re-raising ErrorFromSmartServer, some debug info has
1702
# been muttered to the log file for developer to look at.
1703
self.assertContainsRe(
1704
self._get_log(keep_log_file=True),
1705
"Missing key 'branch' in context")
1707
def test_path_missing(self):
1708
"""Some translations (PermissionDenied, ReadError) can determine the
1709
'path' variable from either the wire or the local context. If neither
1710
has it, then an error is raised.
1712
error_tuple = ('ReadError',)
1713
server_error = errors.ErrorFromSmartServer(error_tuple)
1714
translated_error = self.translateErrorFromSmartServer(server_error)
1715
self.assertEqual(server_error, translated_error)
1716
# In addition to re-raising ErrorFromSmartServer, some debug info has
1717
# been muttered to the log file for developer to look at.
1718
self.assertContainsRe(
1719
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1722
class TestStacking(tests.TestCaseWithTransport):
1723
"""Tests for operations on stacked remote repositories.
1725
The underlying format type must support stacking.
1728
def test_access_stacked_remote(self):
1729
# based on <http://launchpad.net/bugs/261315>
1730
# make a branch stacked on another repository containing an empty
1731
# revision, then open it over hpss - we should be able to see that
1733
base_transport = self.get_transport()
1734
base_builder = self.make_branch_builder('base', format='1.6')
1735
base_builder.start_series()
1736
base_revid = base_builder.build_snapshot('rev-id', None,
1737
[('add', ('', None, 'directory', None))],
1739
base_builder.finish_series()
1740
stacked_branch = self.make_branch('stacked', format='1.6')
1741
stacked_branch.set_stacked_on_url('../base')
1742
# start a server looking at this
1743
smart_server = server.SmartTCPServer_for_testing()
1744
smart_server.setUp()
1745
self.addCleanup(smart_server.tearDown)
1746
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1747
# can get its branch and repository
1748
remote_branch = remote_bzrdir.open_branch()
1749
remote_repo = remote_branch.repository
1750
remote_repo.lock_read()
1752
# it should have an appropriate fallback repository, which should also
1753
# be a RemoteRepository
1754
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1755
self.assertIsInstance(remote_repo._fallback_repositories[0],
1757
# and it has the revision committed to the underlying repository;
1758
# these have varying implementations so we try several of them
1759
self.assertTrue(remote_repo.has_revisions([base_revid]))
1760
self.assertTrue(remote_repo.has_revision(base_revid))
1761
self.assertEqual(remote_repo.get_revision(base_revid).message,
1764
remote_repo.unlock()
1766
def prepare_stacked_remote_branch(self):
1767
smart_server = server.SmartTCPServer_for_testing()
1768
smart_server.setUp()
1769
self.addCleanup(smart_server.tearDown)
1770
tree1 = self.make_branch_and_tree('tree1')
1771
tree1.commit('rev1', rev_id='rev1')
1772
tree2 = self.make_branch_and_tree('tree2', format='1.6')
1773
tree2.branch.set_stacked_on_url(tree1.branch.base)
1774
branch2 = Branch.open(smart_server.get_url() + '/tree2')
1776
self.addCleanup(branch2.unlock)
1779
def test_stacked_get_parent_map(self):
1780
# the public implementation of get_parent_map obeys stacking
1781
branch = self.prepare_stacked_remote_branch()
1782
repo = branch.repository
1783
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
1785
def test_stacked__get_parent_map(self):
1786
# the private variant of _get_parent_map ignores stacking
1787
branch = self.prepare_stacked_remote_branch()
1788
repo = branch.repository
1789
self.assertEqual([], repo._get_parent_map(['rev1']).keys())