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
38
from bzrlib.branch import Branch
39
from bzrlib.bzrdir import BzrDir, BzrDirFormat
40
from bzrlib.remote import (
46
from bzrlib.revision import NULL_REVISION
47
from bzrlib.smart import server, medium
48
from bzrlib.smart.client import _SmartClient
49
from bzrlib.symbol_versioning import one_four
50
from bzrlib.transport import get_transport, http
51
from bzrlib.transport.memory import MemoryTransport
52
from bzrlib.transport.remote import RemoteTransport, RemoteTCPTransport
55
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
58
self.transport_server = server.SmartTCPServer_for_testing
59
super(BasicRemoteObjectTests, self).setUp()
60
self.transport = self.get_transport()
61
# make a branch that can be opened over the smart transport
62
self.local_wt = BzrDir.create_standalone_workingtree('.')
65
self.transport.disconnect()
66
tests.TestCaseWithTransport.tearDown(self)
68
def test_create_remote_bzrdir(self):
69
b = remote.RemoteBzrDir(self.transport)
70
self.assertIsInstance(b, BzrDir)
72
def test_open_remote_branch(self):
73
# open a standalone branch in the working directory
74
b = remote.RemoteBzrDir(self.transport)
75
branch = b.open_branch()
76
self.assertIsInstance(branch, Branch)
78
def test_remote_repository(self):
79
b = BzrDir.open_from_transport(self.transport)
80
repo = b.open_repository()
81
revid = u'\xc823123123'.encode('utf8')
82
self.assertFalse(repo.has_revision(revid))
83
self.local_wt.commit(message='test commit', rev_id=revid)
84
self.assertTrue(repo.has_revision(revid))
86
def test_remote_branch_revision_history(self):
87
b = BzrDir.open_from_transport(self.transport).open_branch()
88
self.assertEqual([], b.revision_history())
89
r1 = self.local_wt.commit('1st commit')
90
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
91
self.assertEqual([r1, r2], b.revision_history())
93
def test_find_correct_format(self):
94
"""Should open a RemoteBzrDir over a RemoteTransport"""
95
fmt = BzrDirFormat.find_format(self.transport)
96
self.assertTrue(RemoteBzrDirFormat
97
in BzrDirFormat._control_server_formats)
98
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
100
def test_open_detected_smart_format(self):
101
fmt = BzrDirFormat.find_format(self.transport)
102
d = fmt.open(self.transport)
103
self.assertIsInstance(d, BzrDir)
105
def test_remote_branch_repr(self):
106
b = BzrDir.open_from_transport(self.transport).open_branch()
107
self.assertStartsWith(str(b), 'RemoteBranch(')
110
class FakeRemoteTransport(object):
111
"""This class provides the minimum support for use in place of a RemoteTransport.
113
It doesn't actually transmit requests, but rather expects them to be
114
handled by a FakeClient which holds canned responses. It does not allow
115
any vfs access, therefore is not suitable for testing any operation that
116
will fallback to vfs access. Backing the test by an instance of this
117
class guarantees that it's - done using non-vfs operations.
120
_default_url = 'fakeremotetransport://host/path/'
122
def __init__(self, url=None):
124
url = self._default_url
128
return "%r(%r)" % (self.__class__.__name__,
131
def clone(self, relpath):
132
return FakeRemoteTransport(urlutils.join(self.base, relpath))
134
def get(self, relpath):
135
# only get is specifically stubbed out, because it's usually the first
136
# thing we do. anything else will fail with an AttributeError.
137
raise AssertionError("%r doesn't support file access to %r"
142
class FakeProtocol(object):
143
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
145
def __init__(self, body, fake_client):
147
self._body_buffer = None
148
self._fake_client = fake_client
150
def read_body_bytes(self, count=-1):
151
if self._body_buffer is None:
152
self._body_buffer = StringIO(self.body)
153
bytes = self._body_buffer.read(count)
154
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
155
self._fake_client.expecting_body = False
158
def cancel_read_body(self):
159
self._fake_client.expecting_body = False
161
def read_streamed_body(self):
165
class FakeClient(_SmartClient):
166
"""Lookalike for _SmartClient allowing testing."""
168
def __init__(self, fake_medium_base='fake base'):
169
"""Create a FakeClient.
171
:param responses: A list of response-tuple, body-data pairs to be sent
172
back to callers. A special case is if the response-tuple is
173
'unknown verb', then a UnknownSmartMethod will be raised for that
174
call, using the second element of the tuple as the verb in the
179
self.expecting_body = False
180
# if non-None, this is the list of expected calls, with only the
181
# method name and arguments included. the body might be hard to
182
# compute so is not included
183
self._expected_calls = None
184
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
186
def add_expected_call(self, call_name, call_args, response_type,
187
response_args, response_body=None):
188
if self._expected_calls is None:
189
self._expected_calls = []
190
self._expected_calls.append((call_name, call_args))
191
self.responses.append((response_type, response_args, response_body))
193
def add_success_response(self, *args):
194
self.responses.append(('success', args, None))
196
def add_success_response_with_body(self, body, *args):
197
self.responses.append(('success', args, body))
199
def add_error_response(self, *args):
200
self.responses.append(('error', args))
202
def add_unknown_method_response(self, verb):
203
self.responses.append(('unknown', verb))
205
def finished_test(self):
206
if self._expected_calls:
207
raise AssertionError("%r finished but was still expecting %r"
208
% (self, self._expected_calls[0]))
210
def _get_next_response(self):
212
response_tuple = self.responses.pop(0)
213
except IndexError, e:
214
raise AssertionError("%r didn't expect any more calls"
216
if response_tuple[0] == 'unknown':
217
raise errors.UnknownSmartMethod(response_tuple[1])
218
elif response_tuple[0] == 'error':
219
raise errors.ErrorFromSmartServer(response_tuple[1])
220
return response_tuple
222
def _check_call(self, method, args):
223
if self._expected_calls is None:
224
# the test should be updated to say what it expects
227
next_call = self._expected_calls.pop(0)
229
raise AssertionError("%r didn't expect any more calls "
231
% (self, method, args,))
232
if method != next_call[0] or args != next_call[1]:
233
raise AssertionError("%r expected %r%r "
235
% (self, next_call[0], next_call[1], method, args,))
237
def call(self, method, *args):
238
self._check_call(method, args)
239
self._calls.append(('call', method, args))
240
return self._get_next_response()[1]
242
def call_expecting_body(self, method, *args):
243
self._check_call(method, args)
244
self._calls.append(('call_expecting_body', method, args))
245
result = self._get_next_response()
246
self.expecting_body = True
247
return result[1], FakeProtocol(result[2], self)
249
def call_with_body_bytes_expecting_body(self, method, args, body):
250
self._check_call(method, args)
251
self._calls.append(('call_with_body_bytes_expecting_body', method,
253
result = self._get_next_response()
254
self.expecting_body = True
255
return result[1], FakeProtocol(result[2], self)
258
class FakeMedium(medium.SmartClientMedium):
260
def __init__(self, client_calls, base):
261
medium.SmartClientMedium.__init__(self, base)
262
self._client_calls = client_calls
264
def disconnect(self):
265
self._client_calls.append(('disconnect medium',))
268
class TestVfsHas(tests.TestCase):
270
def test_unicode_path(self):
271
client = FakeClient('/')
272
client.add_success_response('yes',)
273
transport = RemoteTransport('bzr://localhost/', _client=client)
274
filename = u'/hell\u00d8'.encode('utf8')
275
result = transport.has(filename)
277
[('call', 'has', (filename,))],
279
self.assertTrue(result)
282
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
283
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
285
def assertRemotePath(self, expected, client_base, transport_base):
286
"""Assert that the result of
287
SmartClientMedium.remote_path_from_transport is the expected value for
288
a given client_base and transport_base.
290
client_medium = medium.SmartClientMedium(client_base)
291
transport = get_transport(transport_base)
292
result = client_medium.remote_path_from_transport(transport)
293
self.assertEqual(expected, result)
295
def test_remote_path_from_transport(self):
296
"""SmartClientMedium.remote_path_from_transport calculates a URL for
297
the given transport relative to the root of the client base URL.
299
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
300
self.assertRemotePath(
301
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
303
def assertRemotePathHTTP(self, expected, transport_base, relpath):
304
"""Assert that the result of
305
HttpTransportBase.remote_path_from_transport is the expected value for
306
a given transport_base and relpath of that transport. (Note that
307
HttpTransportBase is a subclass of SmartClientMedium)
309
base_transport = get_transport(transport_base)
310
client_medium = base_transport.get_smart_medium()
311
cloned_transport = base_transport.clone(relpath)
312
result = client_medium.remote_path_from_transport(cloned_transport)
313
self.assertEqual(expected, result)
315
def test_remote_path_from_transport_http(self):
316
"""Remote paths for HTTP transports are calculated differently to other
317
transports. They are just relative to the client base, not the root
318
directory of the host.
320
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
321
self.assertRemotePathHTTP(
322
'../xyz/', scheme + '//host/path', '../xyz/')
323
self.assertRemotePathHTTP(
324
'xyz/', scheme + '//host/path', 'xyz/')
327
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
328
"""Tests for the behaviour of client_medium.remote_is_at_least."""
330
def test_initially_unlimited(self):
331
"""A fresh medium assumes that the remote side supports all
334
client_medium = medium.SmartClientMedium('dummy base')
335
self.assertFalse(client_medium._is_remote_before((99, 99)))
337
def test__remember_remote_is_before(self):
338
"""Calling _remember_remote_is_before ratchets down the known remote
341
client_medium = medium.SmartClientMedium('dummy base')
342
# Mark the remote side as being less than 1.6. The remote side may
344
client_medium._remember_remote_is_before((1, 6))
345
self.assertTrue(client_medium._is_remote_before((1, 6)))
346
self.assertFalse(client_medium._is_remote_before((1, 5)))
347
# Calling _remember_remote_is_before again with a lower value works.
348
client_medium._remember_remote_is_before((1, 5))
349
self.assertTrue(client_medium._is_remote_before((1, 5)))
350
# You cannot call _remember_remote_is_before with a larger value.
352
AssertionError, client_medium._remember_remote_is_before, (1, 9))
355
class TestBzrDirOpenBranch(tests.TestCase):
357
def test_branch_present(self):
358
transport = MemoryTransport()
359
transport.mkdir('quack')
360
transport = transport.clone('quack')
361
client = FakeClient(transport.base)
362
client.add_expected_call(
363
'BzrDir.open_branch', ('quack/',),
364
'success', ('ok', ''))
365
client.add_expected_call(
366
'BzrDir.find_repositoryV2', ('quack/',),
367
'success', ('ok', '', 'no', 'no', 'no'))
368
client.add_expected_call(
369
'Branch.get_stacked_on_url', ('quack/',),
370
'error', ('NotStacked',))
371
bzrdir = RemoteBzrDir(transport, _client=client)
372
result = bzrdir.open_branch()
373
self.assertIsInstance(result, RemoteBranch)
374
self.assertEqual(bzrdir, result.bzrdir)
375
client.finished_test()
377
def test_branch_missing(self):
378
transport = MemoryTransport()
379
transport.mkdir('quack')
380
transport = transport.clone('quack')
381
client = FakeClient(transport.base)
382
client.add_error_response('nobranch')
383
bzrdir = RemoteBzrDir(transport, _client=client)
384
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
386
[('call', 'BzrDir.open_branch', ('quack/',))],
389
def test__get_tree_branch(self):
390
# _get_tree_branch is a form of open_branch, but it should only ask for
391
# branch opening, not any other network requests.
394
calls.append("Called")
396
transport = MemoryTransport()
397
# no requests on the network - catches other api calls being made.
398
client = FakeClient(transport.base)
399
bzrdir = RemoteBzrDir(transport, _client=client)
400
# patch the open_branch call to record that it was called.
401
bzrdir.open_branch = open_branch
402
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
403
self.assertEqual(["Called"], calls)
404
self.assertEqual([], client._calls)
406
def test_url_quoting_of_path(self):
407
# Relpaths on the wire should not be URL-escaped. So "~" should be
408
# transmitted as "~", not "%7E".
409
transport = RemoteTCPTransport('bzr://localhost/~hello/')
410
client = FakeClient(transport.base)
411
client.add_expected_call(
412
'BzrDir.open_branch', ('~hello/',),
413
'success', ('ok', ''))
414
client.add_expected_call(
415
'BzrDir.find_repositoryV2', ('~hello/',),
416
'success', ('ok', '', 'no', 'no', 'no'))
417
client.add_expected_call(
418
'Branch.get_stacked_on_url', ('~hello/',),
419
'error', ('NotStacked',))
420
bzrdir = RemoteBzrDir(transport, _client=client)
421
result = bzrdir.open_branch()
422
client.finished_test()
424
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
425
transport = MemoryTransport()
426
transport.mkdir('quack')
427
transport = transport.clone('quack')
429
rich_response = 'yes'
433
subtree_response = 'yes'
435
subtree_response = 'no'
436
client = FakeClient(transport.base)
437
client.add_success_response(
438
'ok', '', rich_response, subtree_response, external_lookup)
439
bzrdir = RemoteBzrDir(transport, _client=client)
440
result = bzrdir.open_repository()
442
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
444
self.assertIsInstance(result, RemoteRepository)
445
self.assertEqual(bzrdir, result.bzrdir)
446
self.assertEqual(rich_root, result._format.rich_root_data)
447
self.assertEqual(subtrees, result._format.supports_tree_reference)
449
def test_open_repository_sets_format_attributes(self):
450
self.check_open_repository(True, True)
451
self.check_open_repository(False, True)
452
self.check_open_repository(True, False)
453
self.check_open_repository(False, False)
454
self.check_open_repository(False, False, 'yes')
456
def test_old_server(self):
457
"""RemoteBzrDirFormat should fail to probe if the server version is too
460
self.assertRaises(errors.NotBranchError,
461
RemoteBzrDirFormat.probe_transport, OldServerTransport())
464
class TestBzrDirOpenRepository(tests.TestCase):
466
def test_backwards_compat_1_2(self):
467
transport = MemoryTransport()
468
transport.mkdir('quack')
469
transport = transport.clone('quack')
470
client = FakeClient(transport.base)
471
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
472
client.add_success_response('ok', '', 'no', 'no')
473
bzrdir = RemoteBzrDir(transport, _client=client)
474
repo = bzrdir.open_repository()
476
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
477
('call', 'BzrDir.find_repository', ('quack/',))],
481
class OldSmartClient(object):
482
"""A fake smart client for test_old_version that just returns a version one
483
response to the 'hello' (query version) command.
486
def get_request(self):
487
input_file = StringIO('ok\x011\n')
488
output_file = StringIO()
489
client_medium = medium.SmartSimplePipesClientMedium(
490
input_file, output_file)
491
return medium.SmartClientStreamMediumRequest(client_medium)
493
def protocol_version(self):
497
class OldServerTransport(object):
498
"""A fake transport for test_old_server that reports it's smart server
499
protocol version as version one.
505
def get_smart_client(self):
506
return OldSmartClient()
509
class RemoteBranchTestCase(tests.TestCase):
511
def make_remote_branch(self, transport, client):
512
"""Make a RemoteBranch using 'client' as its _SmartClient.
514
A RemoteBzrDir and RemoteRepository will also be created to fill out
515
the RemoteBranch, albeit with stub values for some of their attributes.
517
# we do not want bzrdir to make any remote calls, so use False as its
518
# _client. If it tries to make a remote call, this will fail
520
bzrdir = RemoteBzrDir(transport, _client=False)
521
repo = RemoteRepository(bzrdir, None, _client=client)
522
return RemoteBranch(bzrdir, repo, _client=client)
525
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
527
def test_empty_branch(self):
528
# in an empty branch we decode the response properly
529
transport = MemoryTransport()
530
client = FakeClient(transport.base)
531
client.add_expected_call(
532
'Branch.get_stacked_on_url', ('quack/',),
533
'error', ('NotStacked',))
534
client.add_expected_call(
535
'Branch.last_revision_info', ('quack/',),
536
'success', ('ok', '0', 'null:'))
537
transport.mkdir('quack')
538
transport = transport.clone('quack')
539
branch = self.make_remote_branch(transport, client)
540
result = branch.last_revision_info()
541
client.finished_test()
542
self.assertEqual((0, NULL_REVISION), result)
544
def test_non_empty_branch(self):
545
# in a non-empty branch we also decode the response properly
546
revid = u'\xc8'.encode('utf8')
547
transport = MemoryTransport()
548
client = FakeClient(transport.base)
549
client.add_expected_call(
550
'Branch.get_stacked_on_url', ('kwaak/',),
551
'error', ('NotStacked',))
552
client.add_expected_call(
553
'Branch.last_revision_info', ('kwaak/',),
554
'success', ('ok', '2', revid))
555
transport.mkdir('kwaak')
556
transport = transport.clone('kwaak')
557
branch = self.make_remote_branch(transport, client)
558
result = branch.last_revision_info()
559
self.assertEqual((2, revid), result)
562
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
563
"""Test Branch._get_stacked_on_url rpc"""
565
def test_get_stacked_on_invalid_url(self):
566
raise tests.KnownFailure('opening a branch requires the server to open the fallback repository')
567
transport = FakeRemoteTransport('fakeremotetransport:///')
568
client = FakeClient(transport.base)
569
client.add_expected_call(
570
'Branch.get_stacked_on_url', ('.',),
571
'success', ('ok', 'file:///stacked/on'))
572
bzrdir = RemoteBzrDir(transport, _client=client)
573
branch = RemoteBranch(bzrdir, None, _client=client)
574
result = branch.get_stacked_on_url()
576
'file:///stacked/on', result)
578
def test_backwards_compatible(self):
579
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
580
base_branch = self.make_branch('base', format='1.6')
581
stacked_branch = self.make_branch('stacked', format='1.6')
582
stacked_branch.set_stacked_on_url('../base')
583
client = FakeClient(self.get_url())
584
client.add_expected_call(
585
'BzrDir.open_branch', ('stacked/',),
586
'success', ('ok', ''))
587
client.add_expected_call(
588
'BzrDir.find_repositoryV2', ('stacked/',),
589
'success', ('ok', '', 'no', 'no', 'no'))
590
# called twice, once from constructor and then again by us
591
client.add_expected_call(
592
'Branch.get_stacked_on_url', ('stacked/',),
593
'unknown', ('Branch.get_stacked_on_url',))
594
client.add_expected_call(
595
'Branch.get_stacked_on_url', ('stacked/',),
596
'unknown', ('Branch.get_stacked_on_url',))
597
# this will also do vfs access, but that goes direct to the transport
598
# and isn't seen by the FakeClient.
599
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
600
branch = bzrdir.open_branch()
601
result = branch.get_stacked_on_url()
602
self.assertEqual('../base', result)
603
client.finished_test()
604
# it's in the fallback list both for the RemoteRepository and its vfs
606
self.assertEqual(1, len(branch.repository._fallback_repositories))
608
len(branch.repository._real_repository._fallback_repositories))
610
def test_get_stacked_on_real_branch(self):
611
base_branch = self.make_branch('base', format='1.6')
612
stacked_branch = self.make_branch('stacked', format='1.6')
613
stacked_branch.set_stacked_on_url('../base')
614
client = FakeClient(self.get_url())
615
client.add_expected_call(
616
'BzrDir.open_branch', ('stacked/',),
617
'success', ('ok', ''))
618
client.add_expected_call(
619
'BzrDir.find_repositoryV2', ('stacked/',),
620
'success', ('ok', '', 'no', 'no', 'no'))
621
# called twice, once from constructor and then again by us
622
client.add_expected_call(
623
'Branch.get_stacked_on_url', ('stacked/',),
624
'success', ('ok', '../base'))
625
client.add_expected_call(
626
'Branch.get_stacked_on_url', ('stacked/',),
627
'success', ('ok', '../base'))
628
bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
629
branch = bzrdir.open_branch()
630
result = branch.get_stacked_on_url()
631
self.assertEqual('../base', result)
632
client.finished_test()
633
# it's in the fallback list both for the RemoteRepository and its vfs
635
self.assertEqual(1, len(branch.repository._fallback_repositories))
637
len(branch.repository._real_repository._fallback_repositories))
640
class TestBranchSetLastRevision(RemoteBranchTestCase):
642
def test_set_empty(self):
643
# set_revision_history([]) is translated to calling
644
# Branch.set_last_revision(path, '') on the wire.
645
transport = MemoryTransport()
646
transport.mkdir('branch')
647
transport = transport.clone('branch')
649
client = FakeClient(transport.base)
650
client.add_expected_call(
651
'Branch.get_stacked_on_url', ('branch/',),
652
'error', ('NotStacked',))
653
client.add_expected_call(
654
'Branch.lock_write', ('branch/', '', ''),
655
'success', ('ok', 'branch token', 'repo token'))
656
client.add_expected_call(
657
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
659
client.add_expected_call(
660
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
662
branch = self.make_remote_branch(transport, client)
663
# This is a hack to work around the problem that RemoteBranch currently
664
# unnecessarily invokes _ensure_real upon a call to lock_write.
665
branch._ensure_real = lambda: None
667
result = branch.set_revision_history([])
669
self.assertEqual(None, result)
670
client.finished_test()
672
def test_set_nonempty(self):
673
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
674
# Branch.set_last_revision(path, rev-idN) on the wire.
675
transport = MemoryTransport()
676
transport.mkdir('branch')
677
transport = transport.clone('branch')
679
client = FakeClient(transport.base)
680
client.add_expected_call(
681
'Branch.get_stacked_on_url', ('branch/',),
682
'error', ('NotStacked',))
683
client.add_expected_call(
684
'Branch.lock_write', ('branch/', '', ''),
685
'success', ('ok', 'branch token', 'repo token'))
686
client.add_expected_call(
687
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
689
client.add_expected_call(
690
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
692
branch = self.make_remote_branch(transport, client)
693
# This is a hack to work around the problem that RemoteBranch currently
694
# unnecessarily invokes _ensure_real upon a call to lock_write.
695
branch._ensure_real = lambda: None
696
# Lock the branch, reset the record of remote calls.
698
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
700
self.assertEqual(None, result)
701
client.finished_test()
703
def test_no_such_revision(self):
704
transport = MemoryTransport()
705
transport.mkdir('branch')
706
transport = transport.clone('branch')
707
# A response of 'NoSuchRevision' is translated into an exception.
708
client = FakeClient(transport.base)
709
client.add_expected_call(
710
'Branch.get_stacked_on_url', ('branch/',),
711
'error', ('NotStacked',))
712
client.add_expected_call(
713
'Branch.lock_write', ('branch/', '', ''),
714
'success', ('ok', 'branch token', 'repo token'))
715
client.add_expected_call(
716
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
717
'error', ('NoSuchRevision', 'rev-id'))
718
client.add_expected_call(
719
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
722
branch = self.make_remote_branch(transport, client)
725
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
727
client.finished_test()
729
def test_tip_change_rejected(self):
730
"""TipChangeRejected responses cause a TipChangeRejected exception to
733
transport = MemoryTransport()
734
transport.mkdir('branch')
735
transport = transport.clone('branch')
736
client = FakeClient(transport.base)
737
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
738
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
739
client.add_expected_call(
740
'Branch.get_stacked_on_url', ('branch/',),
741
'error', ('NotStacked',))
742
client.add_expected_call(
743
'Branch.lock_write', ('branch/', '', ''),
744
'success', ('ok', 'branch token', 'repo token'))
745
client.add_expected_call(
746
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
747
'error', ('TipChangeRejected', rejection_msg_utf8))
748
client.add_expected_call(
749
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
751
branch = self.make_remote_branch(transport, client)
752
branch._ensure_real = lambda: None
754
self.addCleanup(branch.unlock)
755
# The 'TipChangeRejected' error response triggered by calling
756
# set_revision_history causes a TipChangeRejected exception.
757
err = self.assertRaises(
758
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
759
# The UTF-8 message from the response has been decoded into a unicode
761
self.assertIsInstance(err.msg, unicode)
762
self.assertEqual(rejection_msg_unicode, err.msg)
764
client.finished_test()
767
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
769
def test_set_last_revision_info(self):
770
# set_last_revision_info(num, 'rev-id') is translated to calling
771
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
772
transport = MemoryTransport()
773
transport.mkdir('branch')
774
transport = transport.clone('branch')
775
client = FakeClient(transport.base)
777
client.add_error_response('NotStacked')
779
client.add_success_response('ok', 'branch token', 'repo token')
781
client.add_success_response('ok')
783
client.add_success_response('ok')
785
branch = self.make_remote_branch(transport, client)
786
# Lock the branch, reset the record of remote calls.
789
result = branch.set_last_revision_info(1234, 'a-revision-id')
791
[('call', 'Branch.set_last_revision_info',
792
('branch/', 'branch token', 'repo token',
793
'1234', 'a-revision-id'))],
795
self.assertEqual(None, result)
797
def test_no_such_revision(self):
798
# A response of 'NoSuchRevision' is translated into an exception.
799
transport = MemoryTransport()
800
transport.mkdir('branch')
801
transport = transport.clone('branch')
802
client = FakeClient(transport.base)
804
client.add_error_response('NotStacked')
806
client.add_success_response('ok', 'branch token', 'repo token')
808
client.add_error_response('NoSuchRevision', 'revid')
810
client.add_success_response('ok')
812
branch = self.make_remote_branch(transport, client)
813
# Lock the branch, reset the record of remote calls.
818
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
821
def lock_remote_branch(self, branch):
822
"""Trick a RemoteBranch into thinking it is locked."""
823
branch._lock_mode = 'w'
824
branch._lock_count = 2
825
branch._lock_token = 'branch token'
826
branch._repo_lock_token = 'repo token'
827
branch.repository._lock_mode = 'w'
828
branch.repository._lock_count = 2
829
branch.repository._lock_token = 'repo token'
831
def test_backwards_compatibility(self):
832
"""If the server does not support the Branch.set_last_revision_info
833
verb (which is new in 1.4), then the client falls back to VFS methods.
835
# This test is a little messy. Unlike most tests in this file, it
836
# doesn't purely test what a Remote* object sends over the wire, and
837
# how it reacts to responses from the wire. It instead relies partly
838
# on asserting that the RemoteBranch will call
839
# self._real_branch.set_last_revision_info(...).
841
# First, set up our RemoteBranch with a FakeClient that raises
842
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
843
transport = MemoryTransport()
844
transport.mkdir('branch')
845
transport = transport.clone('branch')
846
client = FakeClient(transport.base)
847
client.add_expected_call(
848
'Branch.get_stacked_on_url', ('branch/',),
849
'error', ('NotStacked',))
850
client.add_expected_call(
851
'Branch.set_last_revision_info',
852
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
853
'unknown', 'Branch.set_last_revision_info')
855
branch = self.make_remote_branch(transport, client)
856
class StubRealBranch(object):
859
def set_last_revision_info(self, revno, revision_id):
861
('set_last_revision_info', revno, revision_id))
862
def _clear_cached_state(self):
864
real_branch = StubRealBranch()
865
branch._real_branch = real_branch
866
self.lock_remote_branch(branch)
868
# Call set_last_revision_info, and verify it behaved as expected.
869
result = branch.set_last_revision_info(1234, 'a-revision-id')
871
[('set_last_revision_info', 1234, 'a-revision-id')],
873
client.finished_test()
875
def test_unexpected_error(self):
876
# If the server sends an error the client doesn't understand, it gets
877
# turned into an UnknownErrorFromSmartServer, which is presented as a
878
# non-internal error to the user.
879
transport = MemoryTransport()
880
transport.mkdir('branch')
881
transport = transport.clone('branch')
882
client = FakeClient(transport.base)
884
client.add_error_response('NotStacked')
886
client.add_success_response('ok', 'branch token', 'repo token')
888
client.add_error_response('UnexpectedError')
890
client.add_success_response('ok')
892
branch = self.make_remote_branch(transport, client)
893
# Lock the branch, reset the record of remote calls.
897
err = self.assertRaises(
898
errors.UnknownErrorFromSmartServer,
899
branch.set_last_revision_info, 123, 'revid')
900
self.assertEqual(('UnexpectedError',), err.error_tuple)
903
def test_tip_change_rejected(self):
904
"""TipChangeRejected responses cause a TipChangeRejected exception to
907
transport = MemoryTransport()
908
transport.mkdir('branch')
909
transport = transport.clone('branch')
910
client = FakeClient(transport.base)
912
client.add_error_response('NotStacked')
914
client.add_success_response('ok', 'branch token', 'repo token')
916
client.add_error_response('TipChangeRejected', 'rejection message')
918
client.add_success_response('ok')
920
branch = self.make_remote_branch(transport, client)
921
# Lock the branch, reset the record of remote calls.
923
self.addCleanup(branch.unlock)
926
# The 'TipChangeRejected' error response triggered by calling
927
# set_last_revision_info causes a TipChangeRejected exception.
928
err = self.assertRaises(
929
errors.TipChangeRejected,
930
branch.set_last_revision_info, 123, 'revid')
931
self.assertEqual('rejection message', err.msg)
934
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
935
"""Getting the branch configuration should use an abstract method not vfs.
938
def test_get_branch_conf(self):
939
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
940
## # We should see that branch.get_config() does a single rpc to get the
941
## # remote configuration file, abstracting away where that is stored on
942
## # the server. However at the moment it always falls back to using the
943
## # vfs, and this would need some changes in config.py.
945
## # in an empty branch we decode the response properly
946
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
947
## # we need to make a real branch because the remote_branch.control_files
948
## # will trigger _ensure_real.
949
## branch = self.make_branch('quack')
950
## transport = branch.bzrdir.root_transport
951
## # we do not want bzrdir to make any remote calls
952
## bzrdir = RemoteBzrDir(transport, _client=False)
953
## branch = RemoteBranch(bzrdir, None, _client=client)
954
## config = branch.get_config()
956
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
960
class TestBranchLockWrite(RemoteBranchTestCase):
962
def test_lock_write_unlockable(self):
963
transport = MemoryTransport()
964
client = FakeClient(transport.base)
965
client.add_expected_call(
966
'Branch.get_stacked_on_url', ('quack/',),
967
'error', ('NotStacked',),)
968
client.add_expected_call(
969
'Branch.lock_write', ('quack/', '', ''),
970
'error', ('UnlockableTransport',))
971
transport.mkdir('quack')
972
transport = transport.clone('quack')
973
branch = self.make_remote_branch(transport, client)
974
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
975
client.finished_test()
978
class TestTransportIsReadonly(tests.TestCase):
981
client = FakeClient()
982
client.add_success_response('yes')
983
transport = RemoteTransport('bzr://example.com/', medium=False,
985
self.assertEqual(True, transport.is_readonly())
987
[('call', 'Transport.is_readonly', ())],
990
def test_false(self):
991
client = FakeClient()
992
client.add_success_response('no')
993
transport = RemoteTransport('bzr://example.com/', medium=False,
995
self.assertEqual(False, transport.is_readonly())
997
[('call', 'Transport.is_readonly', ())],
1000
def test_error_from_old_server(self):
1001
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1003
Clients should treat it as a "no" response, because is_readonly is only
1004
advisory anyway (a transport could be read-write, but then the
1005
underlying filesystem could be readonly anyway).
1007
client = FakeClient()
1008
client.add_unknown_method_response('Transport.is_readonly')
1009
transport = RemoteTransport('bzr://example.com/', medium=False,
1011
self.assertEqual(False, transport.is_readonly())
1013
[('call', 'Transport.is_readonly', ())],
1017
class TestRemoteRepository(tests.TestCase):
1018
"""Base for testing RemoteRepository protocol usage.
1020
These tests contain frozen requests and responses. We want any changes to
1021
what is sent or expected to be require a thoughtful update to these tests
1022
because they might break compatibility with different-versioned servers.
1025
def setup_fake_client_and_repository(self, transport_path):
1026
"""Create the fake client and repository for testing with.
1028
There's no real server here; we just have canned responses sent
1031
:param transport_path: Path below the root of the MemoryTransport
1032
where the repository will be created.
1034
transport = MemoryTransport()
1035
transport.mkdir(transport_path)
1036
client = FakeClient(transport.base)
1037
transport = transport.clone(transport_path)
1038
# we do not want bzrdir to make any remote calls
1039
bzrdir = RemoteBzrDir(transport, _client=False)
1040
repo = RemoteRepository(bzrdir, None, _client=client)
1044
class TestRepositoryGatherStats(TestRemoteRepository):
1046
def test_revid_none(self):
1047
# ('ok',), body with revisions and size
1048
transport_path = 'quack'
1049
repo, client = self.setup_fake_client_and_repository(transport_path)
1050
client.add_success_response_with_body(
1051
'revisions: 2\nsize: 18\n', 'ok')
1052
result = repo.gather_stats(None)
1054
[('call_expecting_body', 'Repository.gather_stats',
1055
('quack/','','no'))],
1057
self.assertEqual({'revisions': 2, 'size': 18}, result)
1059
def test_revid_no_committers(self):
1060
# ('ok',), body without committers
1061
body = ('firstrev: 123456.300 3600\n'
1062
'latestrev: 654231.400 0\n'
1065
transport_path = 'quick'
1066
revid = u'\xc8'.encode('utf8')
1067
repo, client = self.setup_fake_client_and_repository(transport_path)
1068
client.add_success_response_with_body(body, 'ok')
1069
result = repo.gather_stats(revid)
1071
[('call_expecting_body', 'Repository.gather_stats',
1072
('quick/', revid, 'no'))],
1074
self.assertEqual({'revisions': 2, 'size': 18,
1075
'firstrev': (123456.300, 3600),
1076
'latestrev': (654231.400, 0),},
1079
def test_revid_with_committers(self):
1080
# ('ok',), body with committers
1081
body = ('committers: 128\n'
1082
'firstrev: 123456.300 3600\n'
1083
'latestrev: 654231.400 0\n'
1086
transport_path = 'buick'
1087
revid = u'\xc8'.encode('utf8')
1088
repo, client = self.setup_fake_client_and_repository(transport_path)
1089
client.add_success_response_with_body(body, 'ok')
1090
result = repo.gather_stats(revid, True)
1092
[('call_expecting_body', 'Repository.gather_stats',
1093
('buick/', revid, 'yes'))],
1095
self.assertEqual({'revisions': 2, 'size': 18,
1097
'firstrev': (123456.300, 3600),
1098
'latestrev': (654231.400, 0),},
1102
class TestRepositoryGetGraph(TestRemoteRepository):
1104
def test_get_graph(self):
1105
# get_graph returns a graph with the repository as the
1107
transport_path = 'quack'
1108
repo, client = self.setup_fake_client_and_repository(transport_path)
1109
graph = repo.get_graph()
1110
self.assertEqual(graph._parents_provider, repo)
1113
class TestRepositoryGetParentMap(TestRemoteRepository):
1115
def test_get_parent_map_caching(self):
1116
# get_parent_map returns from cache until unlock()
1117
# setup a reponse with two revisions
1118
r1 = u'\u0e33'.encode('utf8')
1119
r2 = u'\u0dab'.encode('utf8')
1120
lines = [' '.join([r2, r1]), r1]
1121
encoded_body = bz2.compress('\n'.join(lines))
1123
transport_path = 'quack'
1124
repo, client = self.setup_fake_client_and_repository(transport_path)
1125
client.add_success_response_with_body(encoded_body, 'ok')
1126
client.add_success_response_with_body(encoded_body, 'ok')
1128
graph = repo.get_graph()
1129
parents = graph.get_parent_map([r2])
1130
self.assertEqual({r2: (r1,)}, parents)
1131
# locking and unlocking deeper should not reset
1134
parents = graph.get_parent_map([r1])
1135
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1137
[('call_with_body_bytes_expecting_body',
1138
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1141
# now we call again, and it should use the second response.
1143
graph = repo.get_graph()
1144
parents = graph.get_parent_map([r1])
1145
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1147
[('call_with_body_bytes_expecting_body',
1148
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1149
('call_with_body_bytes_expecting_body',
1150
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1155
def test_get_parent_map_reconnects_if_unknown_method(self):
1156
transport_path = 'quack'
1157
repo, client = self.setup_fake_client_and_repository(transport_path)
1158
client.add_unknown_method_response('Repository,get_parent_map')
1159
client.add_success_response_with_body('', 'ok')
1160
self.assertFalse(client._medium._is_remote_before((1, 2)))
1161
rev_id = 'revision-id'
1162
expected_deprecations = [
1163
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1165
parents = self.callDeprecated(
1166
expected_deprecations, repo.get_parent_map, [rev_id])
1168
[('call_with_body_bytes_expecting_body',
1169
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1170
('disconnect medium',),
1171
('call_expecting_body', 'Repository.get_revision_graph',
1174
# The medium is now marked as being connected to an older server
1175
self.assertTrue(client._medium._is_remote_before((1, 2)))
1177
def test_get_parent_map_fallback_parentless_node(self):
1178
"""get_parent_map falls back to get_revision_graph on old servers. The
1179
results from get_revision_graph are tweaked to match the get_parent_map
1182
Specifically, a {key: ()} result from get_revision_graph means "no
1183
parents" for that key, which in get_parent_map results should be
1184
represented as {key: ('null:',)}.
1186
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1188
rev_id = 'revision-id'
1189
transport_path = 'quack'
1190
repo, client = self.setup_fake_client_and_repository(transport_path)
1191
client.add_success_response_with_body(rev_id, 'ok')
1192
client._medium._remember_remote_is_before((1, 2))
1193
expected_deprecations = [
1194
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1196
parents = self.callDeprecated(
1197
expected_deprecations, repo.get_parent_map, [rev_id])
1199
[('call_expecting_body', 'Repository.get_revision_graph',
1202
self.assertEqual({rev_id: ('null:',)}, parents)
1204
def test_get_parent_map_unexpected_response(self):
1205
repo, client = self.setup_fake_client_and_repository('path')
1206
client.add_success_response('something unexpected!')
1208
errors.UnexpectedSmartServerResponse,
1209
repo.get_parent_map, ['a-revision-id'])
1212
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1214
def test_null_revision(self):
1215
# a null revision has the predictable result {}, we should have no wire
1216
# traffic when calling it with this argument
1217
transport_path = 'empty'
1218
repo, client = self.setup_fake_client_and_repository(transport_path)
1219
client.add_success_response('notused')
1220
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1222
self.assertEqual([], client._calls)
1223
self.assertEqual({}, result)
1225
def test_none_revision(self):
1226
# with none we want the entire graph
1227
r1 = u'\u0e33'.encode('utf8')
1228
r2 = u'\u0dab'.encode('utf8')
1229
lines = [' '.join([r2, r1]), r1]
1230
encoded_body = '\n'.join(lines)
1232
transport_path = 'sinhala'
1233
repo, client = self.setup_fake_client_and_repository(transport_path)
1234
client.add_success_response_with_body(encoded_body, 'ok')
1235
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1237
[('call_expecting_body', 'Repository.get_revision_graph',
1240
self.assertEqual({r1: (), r2: (r1, )}, result)
1242
def test_specific_revision(self):
1243
# with a specific revision we want the graph for that
1244
# with none we want the entire graph
1245
r11 = u'\u0e33'.encode('utf8')
1246
r12 = u'\xc9'.encode('utf8')
1247
r2 = u'\u0dab'.encode('utf8')
1248
lines = [' '.join([r2, r11, r12]), r11, r12]
1249
encoded_body = '\n'.join(lines)
1251
transport_path = 'sinhala'
1252
repo, client = self.setup_fake_client_and_repository(transport_path)
1253
client.add_success_response_with_body(encoded_body, 'ok')
1254
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1256
[('call_expecting_body', 'Repository.get_revision_graph',
1259
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1261
def test_no_such_revision(self):
1263
transport_path = 'sinhala'
1264
repo, client = self.setup_fake_client_and_repository(transport_path)
1265
client.add_error_response('nosuchrevision', revid)
1266
# also check that the right revision is reported in the error
1267
self.assertRaises(errors.NoSuchRevision,
1268
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1270
[('call_expecting_body', 'Repository.get_revision_graph',
1271
('sinhala/', revid))],
1274
def test_unexpected_error(self):
1276
transport_path = 'sinhala'
1277
repo, client = self.setup_fake_client_and_repository(transport_path)
1278
client.add_error_response('AnUnexpectedError')
1279
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1280
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1281
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1284
class TestRepositoryIsShared(TestRemoteRepository):
1286
def test_is_shared(self):
1287
# ('yes', ) for Repository.is_shared -> 'True'.
1288
transport_path = 'quack'
1289
repo, client = self.setup_fake_client_and_repository(transport_path)
1290
client.add_success_response('yes')
1291
result = repo.is_shared()
1293
[('call', 'Repository.is_shared', ('quack/',))],
1295
self.assertEqual(True, result)
1297
def test_is_not_shared(self):
1298
# ('no', ) for Repository.is_shared -> 'False'.
1299
transport_path = 'qwack'
1300
repo, client = self.setup_fake_client_and_repository(transport_path)
1301
client.add_success_response('no')
1302
result = repo.is_shared()
1304
[('call', 'Repository.is_shared', ('qwack/',))],
1306
self.assertEqual(False, result)
1309
class TestRepositoryLockWrite(TestRemoteRepository):
1311
def test_lock_write(self):
1312
transport_path = 'quack'
1313
repo, client = self.setup_fake_client_and_repository(transport_path)
1314
client.add_success_response('ok', 'a token')
1315
result = repo.lock_write()
1317
[('call', 'Repository.lock_write', ('quack/', ''))],
1319
self.assertEqual('a token', result)
1321
def test_lock_write_already_locked(self):
1322
transport_path = 'quack'
1323
repo, client = self.setup_fake_client_and_repository(transport_path)
1324
client.add_error_response('LockContention')
1325
self.assertRaises(errors.LockContention, repo.lock_write)
1327
[('call', 'Repository.lock_write', ('quack/', ''))],
1330
def test_lock_write_unlockable(self):
1331
transport_path = 'quack'
1332
repo, client = self.setup_fake_client_and_repository(transport_path)
1333
client.add_error_response('UnlockableTransport')
1334
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1336
[('call', 'Repository.lock_write', ('quack/', ''))],
1340
class TestRepositoryUnlock(TestRemoteRepository):
1342
def test_unlock(self):
1343
transport_path = 'quack'
1344
repo, client = self.setup_fake_client_and_repository(transport_path)
1345
client.add_success_response('ok', 'a token')
1346
client.add_success_response('ok')
1350
[('call', 'Repository.lock_write', ('quack/', '')),
1351
('call', 'Repository.unlock', ('quack/', 'a token'))],
1354
def test_unlock_wrong_token(self):
1355
# If somehow the token is wrong, unlock will raise TokenMismatch.
1356
transport_path = 'quack'
1357
repo, client = self.setup_fake_client_and_repository(transport_path)
1358
client.add_success_response('ok', 'a token')
1359
client.add_error_response('TokenMismatch')
1361
self.assertRaises(errors.TokenMismatch, repo.unlock)
1364
class TestRepositoryHasRevision(TestRemoteRepository):
1366
def test_none(self):
1367
# repo.has_revision(None) should not cause any traffic.
1368
transport_path = 'quack'
1369
repo, client = self.setup_fake_client_and_repository(transport_path)
1371
# The null revision is always there, so has_revision(None) == True.
1372
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1374
# The remote repo shouldn't be accessed.
1375
self.assertEqual([], client._calls)
1378
class TestRepositoryTarball(TestRemoteRepository):
1380
# This is a canned tarball reponse we can validate against
1382
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1383
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1384
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1385
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1386
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1387
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1388
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1389
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1390
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1391
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1392
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1393
'nWQ7QH/F3JFOFCQ0aSPfA='
1396
def test_repository_tarball(self):
1397
# Test that Repository.tarball generates the right operations
1398
transport_path = 'repo'
1399
expected_calls = [('call_expecting_body', 'Repository.tarball',
1400
('repo/', 'bz2',),),
1402
repo, client = self.setup_fake_client_and_repository(transport_path)
1403
client.add_success_response_with_body(self.tarball_content, 'ok')
1404
# Now actually ask for the tarball
1405
tarball_file = repo._get_tarball('bz2')
1407
self.assertEqual(expected_calls, client._calls)
1408
self.assertEqual(self.tarball_content, tarball_file.read())
1410
tarball_file.close()
1413
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1414
"""RemoteRepository.copy_content_into optimizations"""
1416
def test_copy_content_remote_to_local(self):
1417
self.transport_server = server.SmartTCPServer_for_testing
1418
src_repo = self.make_repository('repo1')
1419
src_repo = repository.Repository.open(self.get_url('repo1'))
1420
# At the moment the tarball-based copy_content_into can't write back
1421
# into a smart server. It would be good if it could upload the
1422
# tarball; once that works we'd have to create repositories of
1423
# different formats. -- mbp 20070410
1424
dest_url = self.get_vfs_only_url('repo2')
1425
dest_bzrdir = BzrDir.create(dest_url)
1426
dest_repo = dest_bzrdir.create_repository()
1427
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1428
self.assertTrue(isinstance(src_repo, RemoteRepository))
1429
src_repo.copy_content_into(dest_repo)
1432
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1433
"""Base class for unit tests for bzrlib.remote._translate_error."""
1435
def translateTuple(self, error_tuple, **context):
1436
"""Call _translate_error with an ErrorFromSmartServer built from the
1439
:param error_tuple: A tuple of a smart server response, as would be
1440
passed to an ErrorFromSmartServer.
1441
:kwargs context: context items to call _translate_error with.
1443
:returns: The error raised by _translate_error.
1445
# Raise the ErrorFromSmartServer before passing it as an argument,
1446
# because _translate_error may need to re-raise it with a bare 'raise'
1448
server_error = errors.ErrorFromSmartServer(error_tuple)
1449
translated_error = self.translateErrorFromSmartServer(
1450
server_error, **context)
1451
return translated_error
1453
def translateErrorFromSmartServer(self, error_object, **context):
1454
"""Like translateTuple, but takes an already constructed
1455
ErrorFromSmartServer rather than a tuple.
1459
except errors.ErrorFromSmartServer, server_error:
1460
translated_error = self.assertRaises(
1461
errors.BzrError, remote._translate_error, server_error,
1463
return translated_error
1466
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1467
"""Unit tests for bzrlib.remote._translate_error.
1469
Given an ErrorFromSmartServer (which has an error tuple from a smart
1470
server) and some context, _translate_error raises more specific errors from
1473
This test case covers the cases where _translate_error succeeds in
1474
translating an ErrorFromSmartServer to something better. See
1475
TestErrorTranslationRobustness for other cases.
1478
def test_NoSuchRevision(self):
1479
branch = self.make_branch('')
1481
translated_error = self.translateTuple(
1482
('NoSuchRevision', revid), branch=branch)
1483
expected_error = errors.NoSuchRevision(branch, revid)
1484
self.assertEqual(expected_error, translated_error)
1486
def test_nosuchrevision(self):
1487
repository = self.make_repository('')
1489
translated_error = self.translateTuple(
1490
('nosuchrevision', revid), repository=repository)
1491
expected_error = errors.NoSuchRevision(repository, revid)
1492
self.assertEqual(expected_error, translated_error)
1494
def test_nobranch(self):
1495
bzrdir = self.make_bzrdir('')
1496
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1497
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1498
self.assertEqual(expected_error, translated_error)
1500
def test_LockContention(self):
1501
translated_error = self.translateTuple(('LockContention',))
1502
expected_error = errors.LockContention('(remote lock)')
1503
self.assertEqual(expected_error, translated_error)
1505
def test_UnlockableTransport(self):
1506
bzrdir = self.make_bzrdir('')
1507
translated_error = self.translateTuple(
1508
('UnlockableTransport',), bzrdir=bzrdir)
1509
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1510
self.assertEqual(expected_error, translated_error)
1512
def test_LockFailed(self):
1513
lock = 'str() of a server lock'
1514
why = 'str() of why'
1515
translated_error = self.translateTuple(('LockFailed', lock, why))
1516
expected_error = errors.LockFailed(lock, why)
1517
self.assertEqual(expected_error, translated_error)
1519
def test_TokenMismatch(self):
1520
token = 'a lock token'
1521
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1522
expected_error = errors.TokenMismatch(token, '(remote token)')
1523
self.assertEqual(expected_error, translated_error)
1525
def test_Diverged(self):
1526
branch = self.make_branch('a')
1527
other_branch = self.make_branch('b')
1528
translated_error = self.translateTuple(
1529
('Diverged',), branch=branch, other_branch=other_branch)
1530
expected_error = errors.DivergedBranches(branch, other_branch)
1531
self.assertEqual(expected_error, translated_error)
1534
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1535
"""Unit tests for bzrlib.remote._translate_error's robustness.
1537
TestErrorTranslationSuccess is for cases where _translate_error can
1538
translate successfully. This class about how _translate_err behaves when
1539
it fails to translate: it re-raises the original error.
1542
def test_unrecognised_server_error(self):
1543
"""If the error code from the server is not recognised, the original
1544
ErrorFromSmartServer is propagated unmodified.
1546
error_tuple = ('An unknown error tuple',)
1547
server_error = errors.ErrorFromSmartServer(error_tuple)
1548
translated_error = self.translateErrorFromSmartServer(server_error)
1549
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1550
self.assertEqual(expected_error, translated_error)
1552
def test_context_missing_a_key(self):
1553
"""In case of a bug in the client, or perhaps an unexpected response
1554
from a server, _translate_error returns the original error tuple from
1555
the server and mutters a warning.
1557
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1558
# in the context dict. So let's give it an empty context dict instead
1559
# to exercise its error recovery.
1561
error_tuple = ('NoSuchRevision', 'revid')
1562
server_error = errors.ErrorFromSmartServer(error_tuple)
1563
translated_error = self.translateErrorFromSmartServer(server_error)
1564
self.assertEqual(server_error, translated_error)
1565
# In addition to re-raising ErrorFromSmartServer, some debug info has
1566
# been muttered to the log file for developer to look at.
1567
self.assertContainsRe(
1568
self._get_log(keep_log_file=True),
1569
"Missing key 'branch' in context")
1572
class TestStacking(tests.TestCaseWithTransport):
1573
"""Tests for operations on stacked remote repositories.
1575
The underlying format type must support stacking.
1578
def test_access_stacked_remote(self):
1579
# based on <http://launchpad.net/bugs/261315>
1580
# make a branch stacked on another repository containing an empty
1581
# revision, then open it over hpss - we should be able to see that
1583
base_transport = self.get_transport()
1584
base_builder = self.make_branch_builder('base', format='1.6')
1585
base_builder.start_series()
1586
base_revid = base_builder.build_snapshot('rev-id', None,
1587
[('add', ('', None, 'directory', None))],
1589
base_builder.finish_series()
1590
stacked_branch = self.make_branch('stacked', format='1.6')
1591
stacked_branch.set_stacked_on_url('../base')
1592
# start a server looking at this
1593
smart_server = server.SmartTCPServer_for_testing()
1594
smart_server.setUp()
1595
self.addCleanup(smart_server.tearDown)
1596
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1597
# can get its branch and repository
1598
remote_branch = remote_bzrdir.open_branch()
1599
remote_repo = remote_branch.repository
1600
remote_repo.lock_read()
1602
# it should have an appropriate fallback repository, which should also
1603
# be a RemoteRepository
1604
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1605
self.assertIsInstance(remote_repo._fallback_repositories[0],
1607
# and it has the revision committed to the underlying repository;
1608
# these have varying implementations so we try several of them
1609
self.assertTrue(remote_repo.has_revisions([base_revid]))
1610
self.assertTrue(remote_repo.has_revision(base_revid))
1611
self.assertEqual(remote_repo.get_revision(base_revid).message,
1614
remote_repo.unlock()