1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests for remote bzrdir/branch/repo/etc
19
These are proxy objects which act on remote objects by sending messages
20
through a smart client. The proxies are to be created when attempting to open
21
the object given a transport that supports smartserver rpc operations.
23
These tests correspond to tests.test_smart, which exercises the server side.
27
from cStringIO import StringIO
40
from bzrlib.branch import Branch
41
from bzrlib.bzrdir import BzrDir, BzrDirFormat
42
from bzrlib.remote import (
48
from bzrlib.revision import NULL_REVISION
49
from bzrlib.smart import server, medium
50
from bzrlib.smart.client import _SmartClient
51
from bzrlib.symbol_versioning import one_four
52
from bzrlib.transport import get_transport, http
53
from bzrlib.transport.memory import MemoryTransport
54
from bzrlib.transport.remote import (
61
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
64
self.transport_server = server.SmartTCPServer_for_testing
65
super(BasicRemoteObjectTests, self).setUp()
66
self.transport = self.get_transport()
67
# make a branch that can be opened over the smart transport
68
self.local_wt = BzrDir.create_standalone_workingtree('.')
71
self.transport.disconnect()
72
tests.TestCaseWithTransport.tearDown(self)
74
def test_create_remote_bzrdir(self):
75
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
76
self.assertIsInstance(b, BzrDir)
78
def test_open_remote_branch(self):
79
# open a standalone branch in the working directory
80
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
81
branch = b.open_branch()
82
self.assertIsInstance(branch, Branch)
84
def test_remote_repository(self):
85
b = BzrDir.open_from_transport(self.transport)
86
repo = b.open_repository()
87
revid = u'\xc823123123'.encode('utf8')
88
self.assertFalse(repo.has_revision(revid))
89
self.local_wt.commit(message='test commit', rev_id=revid)
90
self.assertTrue(repo.has_revision(revid))
92
def test_remote_branch_revision_history(self):
93
b = BzrDir.open_from_transport(self.transport).open_branch()
94
self.assertEqual([], b.revision_history())
95
r1 = self.local_wt.commit('1st commit')
96
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
97
self.assertEqual([r1, r2], b.revision_history())
99
def test_find_correct_format(self):
100
"""Should open a RemoteBzrDir over a RemoteTransport"""
101
fmt = BzrDirFormat.find_format(self.transport)
102
self.assertTrue(RemoteBzrDirFormat
103
in BzrDirFormat._control_server_formats)
104
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
106
def test_open_detected_smart_format(self):
107
fmt = BzrDirFormat.find_format(self.transport)
108
d = fmt.open(self.transport)
109
self.assertIsInstance(d, BzrDir)
111
def test_remote_branch_repr(self):
112
b = BzrDir.open_from_transport(self.transport).open_branch()
113
self.assertStartsWith(str(b), 'RemoteBranch(')
116
class FakeProtocol(object):
117
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
119
def __init__(self, body, fake_client):
121
self._body_buffer = None
122
self._fake_client = fake_client
124
def read_body_bytes(self, count=-1):
125
if self._body_buffer is None:
126
self._body_buffer = StringIO(self.body)
127
bytes = self._body_buffer.read(count)
128
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
129
self._fake_client.expecting_body = False
132
def cancel_read_body(self):
133
self._fake_client.expecting_body = False
135
def read_streamed_body(self):
139
class FakeClient(_SmartClient):
140
"""Lookalike for _SmartClient allowing testing."""
142
def __init__(self, fake_medium_base='fake base'):
143
"""Create a FakeClient."""
146
self.expecting_body = False
147
# if non-None, this is the list of expected calls, with only the
148
# method name and arguments included. the body might be hard to
149
# compute so is not included. If a call is None, that call can
151
self._expected_calls = None
152
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
154
def add_expected_call(self, call_name, call_args, response_type,
155
response_args, response_body=None):
156
if self._expected_calls is None:
157
self._expected_calls = []
158
self._expected_calls.append((call_name, call_args))
159
self.responses.append((response_type, response_args, response_body))
161
def add_success_response(self, *args):
162
self.responses.append(('success', args, None))
164
def add_success_response_with_body(self, body, *args):
165
self.responses.append(('success', args, body))
166
if self._expected_calls is not None:
167
self._expected_calls.append(None)
169
def add_error_response(self, *args):
170
self.responses.append(('error', args))
172
def add_unknown_method_response(self, verb):
173
self.responses.append(('unknown', verb))
175
def finished_test(self):
176
if self._expected_calls:
177
raise AssertionError("%r finished but was still expecting %r"
178
% (self, self._expected_calls[0]))
180
def _get_next_response(self):
182
response_tuple = self.responses.pop(0)
183
except IndexError, e:
184
raise AssertionError("%r didn't expect any more calls"
186
if response_tuple[0] == 'unknown':
187
raise errors.UnknownSmartMethod(response_tuple[1])
188
elif response_tuple[0] == 'error':
189
raise errors.ErrorFromSmartServer(response_tuple[1])
190
return response_tuple
192
def _check_call(self, method, args):
193
if self._expected_calls is None:
194
# the test should be updated to say what it expects
197
next_call = self._expected_calls.pop(0)
199
raise AssertionError("%r didn't expect any more calls "
201
% (self, method, args,))
202
if next_call is None:
204
if method != next_call[0] or args != next_call[1]:
205
raise AssertionError("%r expected %r%r "
207
% (self, next_call[0], next_call[1], method, args,))
209
def call(self, method, *args):
210
self._check_call(method, args)
211
self._calls.append(('call', method, args))
212
return self._get_next_response()[1]
214
def call_expecting_body(self, method, *args):
215
self._check_call(method, args)
216
self._calls.append(('call_expecting_body', method, args))
217
result = self._get_next_response()
218
self.expecting_body = True
219
return result[1], FakeProtocol(result[2], self)
221
def call_with_body_bytes_expecting_body(self, method, args, body):
222
self._check_call(method, args)
223
self._calls.append(('call_with_body_bytes_expecting_body', method,
225
result = self._get_next_response()
226
self.expecting_body = True
227
return result[1], FakeProtocol(result[2], self)
229
def call_with_body_stream(self, args, stream):
230
# Explicitly consume the stream before checking for an error, because
231
# that's what happens a real medium.
232
stream = list(stream)
233
self._check_call(args[0], args[1:])
234
self._calls.append(('call_with_body_stream', args[0], args[1:], stream))
235
return self._get_next_response()[1]
238
class FakeMedium(medium.SmartClientMedium):
240
def __init__(self, client_calls, base):
241
medium.SmartClientMedium.__init__(self, base)
242
self._client_calls = client_calls
244
def disconnect(self):
245
self._client_calls.append(('disconnect medium',))
248
class TestVfsHas(tests.TestCase):
250
def test_unicode_path(self):
251
client = FakeClient('/')
252
client.add_success_response('yes',)
253
transport = RemoteTransport('bzr://localhost/', _client=client)
254
filename = u'/hell\u00d8'.encode('utf8')
255
result = transport.has(filename)
257
[('call', 'has', (filename,))],
259
self.assertTrue(result)
262
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
263
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
265
def assertRemotePath(self, expected, client_base, transport_base):
266
"""Assert that the result of
267
SmartClientMedium.remote_path_from_transport is the expected value for
268
a given client_base and transport_base.
270
client_medium = medium.SmartClientMedium(client_base)
271
transport = get_transport(transport_base)
272
result = client_medium.remote_path_from_transport(transport)
273
self.assertEqual(expected, result)
275
def test_remote_path_from_transport(self):
276
"""SmartClientMedium.remote_path_from_transport calculates a URL for
277
the given transport relative to the root of the client base URL.
279
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
280
self.assertRemotePath(
281
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
283
def assertRemotePathHTTP(self, expected, transport_base, relpath):
284
"""Assert that the result of
285
HttpTransportBase.remote_path_from_transport is the expected value for
286
a given transport_base and relpath of that transport. (Note that
287
HttpTransportBase is a subclass of SmartClientMedium)
289
base_transport = get_transport(transport_base)
290
client_medium = base_transport.get_smart_medium()
291
cloned_transport = base_transport.clone(relpath)
292
result = client_medium.remote_path_from_transport(cloned_transport)
293
self.assertEqual(expected, result)
295
def test_remote_path_from_transport_http(self):
296
"""Remote paths for HTTP transports are calculated differently to other
297
transports. They are just relative to the client base, not the root
298
directory of the host.
300
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
301
self.assertRemotePathHTTP(
302
'../xyz/', scheme + '//host/path', '../xyz/')
303
self.assertRemotePathHTTP(
304
'xyz/', scheme + '//host/path', 'xyz/')
307
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
308
"""Tests for the behaviour of client_medium.remote_is_at_least."""
310
def test_initially_unlimited(self):
311
"""A fresh medium assumes that the remote side supports all
314
client_medium = medium.SmartClientMedium('dummy base')
315
self.assertFalse(client_medium._is_remote_before((99, 99)))
317
def test__remember_remote_is_before(self):
318
"""Calling _remember_remote_is_before ratchets down the known remote
321
client_medium = medium.SmartClientMedium('dummy base')
322
# Mark the remote side as being less than 1.6. The remote side may
324
client_medium._remember_remote_is_before((1, 6))
325
self.assertTrue(client_medium._is_remote_before((1, 6)))
326
self.assertFalse(client_medium._is_remote_before((1, 5)))
327
# Calling _remember_remote_is_before again with a lower value works.
328
client_medium._remember_remote_is_before((1, 5))
329
self.assertTrue(client_medium._is_remote_before((1, 5)))
330
# You cannot call _remember_remote_is_before with a larger value.
332
AssertionError, client_medium._remember_remote_is_before, (1, 9))
335
class TestBzrDirOpenBranch(tests.TestCase):
337
def test_branch_present(self):
338
transport = MemoryTransport()
339
transport.mkdir('quack')
340
transport = transport.clone('quack')
341
client = FakeClient(transport.base)
342
client.add_expected_call(
343
'BzrDir.open_branch', ('quack/',),
344
'success', ('ok', ''))
345
client.add_expected_call(
346
'BzrDir.find_repositoryV2', ('quack/',),
347
'success', ('ok', '', 'no', 'no', 'no'))
348
client.add_expected_call(
349
'Branch.get_stacked_on_url', ('quack/',),
350
'error', ('NotStacked',))
351
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
353
result = bzrdir.open_branch()
354
self.assertIsInstance(result, RemoteBranch)
355
self.assertEqual(bzrdir, result.bzrdir)
356
client.finished_test()
358
def test_branch_missing(self):
359
transport = MemoryTransport()
360
transport.mkdir('quack')
361
transport = transport.clone('quack')
362
client = FakeClient(transport.base)
363
client.add_error_response('nobranch')
364
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
366
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
368
[('call', 'BzrDir.open_branch', ('quack/',))],
371
def test__get_tree_branch(self):
372
# _get_tree_branch is a form of open_branch, but it should only ask for
373
# branch opening, not any other network requests.
376
calls.append("Called")
378
transport = MemoryTransport()
379
# no requests on the network - catches other api calls being made.
380
client = FakeClient(transport.base)
381
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
383
# patch the open_branch call to record that it was called.
384
bzrdir.open_branch = open_branch
385
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
386
self.assertEqual(["Called"], calls)
387
self.assertEqual([], client._calls)
389
def test_url_quoting_of_path(self):
390
# Relpaths on the wire should not be URL-escaped. So "~" should be
391
# transmitted as "~", not "%7E".
392
transport = RemoteTCPTransport('bzr://localhost/~hello/')
393
client = FakeClient(transport.base)
394
client.add_expected_call(
395
'BzrDir.open_branch', ('~hello/',),
396
'success', ('ok', ''))
397
client.add_expected_call(
398
'BzrDir.find_repositoryV2', ('~hello/',),
399
'success', ('ok', '', 'no', 'no', 'no'))
400
client.add_expected_call(
401
'Branch.get_stacked_on_url', ('~hello/',),
402
'error', ('NotStacked',))
403
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
405
result = bzrdir.open_branch()
406
client.finished_test()
408
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
409
transport = MemoryTransport()
410
transport.mkdir('quack')
411
transport = transport.clone('quack')
413
rich_response = 'yes'
417
subtree_response = 'yes'
419
subtree_response = 'no'
420
client = FakeClient(transport.base)
421
client.add_success_response(
422
'ok', '', rich_response, subtree_response, external_lookup)
423
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
425
result = bzrdir.open_repository()
427
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
429
self.assertIsInstance(result, RemoteRepository)
430
self.assertEqual(bzrdir, result.bzrdir)
431
self.assertEqual(rich_root, result._format.rich_root_data)
432
self.assertEqual(subtrees, result._format.supports_tree_reference)
434
def test_open_repository_sets_format_attributes(self):
435
self.check_open_repository(True, True)
436
self.check_open_repository(False, True)
437
self.check_open_repository(True, False)
438
self.check_open_repository(False, False)
439
self.check_open_repository(False, False, 'yes')
441
def test_old_server(self):
442
"""RemoteBzrDirFormat should fail to probe if the server version is too
445
self.assertRaises(errors.NotBranchError,
446
RemoteBzrDirFormat.probe_transport, OldServerTransport())
449
class TestBzrDirOpenRepository(tests.TestCase):
451
def test_backwards_compat_1_2(self):
452
transport = MemoryTransport()
453
transport.mkdir('quack')
454
transport = transport.clone('quack')
455
client = FakeClient(transport.base)
456
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
457
client.add_success_response('ok', '', 'no', 'no')
458
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
460
repo = bzrdir.open_repository()
462
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
463
('call', 'BzrDir.find_repository', ('quack/',))],
467
class OldSmartClient(object):
468
"""A fake smart client for test_old_version that just returns a version one
469
response to the 'hello' (query version) command.
472
def get_request(self):
473
input_file = StringIO('ok\x011\n')
474
output_file = StringIO()
475
client_medium = medium.SmartSimplePipesClientMedium(
476
input_file, output_file)
477
return medium.SmartClientStreamMediumRequest(client_medium)
479
def protocol_version(self):
483
class OldServerTransport(object):
484
"""A fake transport for test_old_server that reports it's smart server
485
protocol version as version one.
491
def get_smart_client(self):
492
return OldSmartClient()
495
class RemoteBranchTestCase(tests.TestCase):
497
def make_remote_branch(self, transport, client):
498
"""Make a RemoteBranch using 'client' as its _SmartClient.
500
A RemoteBzrDir and RemoteRepository will also be created to fill out
501
the RemoteBranch, albeit with stub values for some of their attributes.
503
# we do not want bzrdir to make any remote calls, so use False as its
504
# _client. If it tries to make a remote call, this will fail
506
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
508
repo = RemoteRepository(bzrdir, None, _client=client)
509
return RemoteBranch(bzrdir, repo, _client=client)
512
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
514
def test_empty_branch(self):
515
# in an empty branch we decode the response properly
516
transport = MemoryTransport()
517
client = FakeClient(transport.base)
518
client.add_expected_call(
519
'Branch.get_stacked_on_url', ('quack/',),
520
'error', ('NotStacked',))
521
client.add_expected_call(
522
'Branch.last_revision_info', ('quack/',),
523
'success', ('ok', '0', 'null:'))
524
transport.mkdir('quack')
525
transport = transport.clone('quack')
526
branch = self.make_remote_branch(transport, client)
527
result = branch.last_revision_info()
528
client.finished_test()
529
self.assertEqual((0, NULL_REVISION), result)
531
def test_non_empty_branch(self):
532
# in a non-empty branch we also decode the response properly
533
revid = u'\xc8'.encode('utf8')
534
transport = MemoryTransport()
535
client = FakeClient(transport.base)
536
client.add_expected_call(
537
'Branch.get_stacked_on_url', ('kwaak/',),
538
'error', ('NotStacked',))
539
client.add_expected_call(
540
'Branch.last_revision_info', ('kwaak/',),
541
'success', ('ok', '2', revid))
542
transport.mkdir('kwaak')
543
transport = transport.clone('kwaak')
544
branch = self.make_remote_branch(transport, client)
545
result = branch.last_revision_info()
546
self.assertEqual((2, revid), result)
549
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
550
"""Test Branch._get_stacked_on_url rpc"""
552
def test_get_stacked_on_invalid_url(self):
553
# test that asking for a stacked on url the server can't access works.
554
# This isn't perfect, but then as we're in the same process there
555
# really isn't anything we can do to be 100% sure that the server
556
# doesn't just open in - this test probably needs to be rewritten using
557
# a spawn()ed server.
558
stacked_branch = self.make_branch('stacked', format='1.9')
559
memory_branch = self.make_branch('base', format='1.9')
560
vfs_url = self.get_vfs_only_url('base')
561
stacked_branch.set_stacked_on_url(vfs_url)
562
transport = stacked_branch.bzrdir.root_transport
563
client = FakeClient(transport.base)
564
client.add_expected_call(
565
'Branch.get_stacked_on_url', ('stacked/',),
566
'success', ('ok', vfs_url))
567
# XXX: Multiple calls are bad, this second call documents what is
569
client.add_expected_call(
570
'Branch.get_stacked_on_url', ('stacked/',),
571
'success', ('ok', vfs_url))
572
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
574
branch = RemoteBranch(bzrdir, RemoteRepository(bzrdir, None),
576
result = branch.get_stacked_on_url()
577
self.assertEqual(vfs_url, result)
579
def test_backwards_compatible(self):
580
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
581
base_branch = self.make_branch('base', format='1.6')
582
stacked_branch = self.make_branch('stacked', format='1.6')
583
stacked_branch.set_stacked_on_url('../base')
584
client = FakeClient(self.get_url())
585
client.add_expected_call(
586
'BzrDir.open_branch', ('stacked/',),
587
'success', ('ok', ''))
588
client.add_expected_call(
589
'BzrDir.find_repositoryV2', ('stacked/',),
590
'success', ('ok', '', 'no', 'no', 'no'))
591
# called twice, once from constructor and then again by us
592
client.add_expected_call(
593
'Branch.get_stacked_on_url', ('stacked/',),
594
'unknown', ('Branch.get_stacked_on_url',))
595
client.add_expected_call(
596
'Branch.get_stacked_on_url', ('stacked/',),
597
'unknown', ('Branch.get_stacked_on_url',))
598
# this will also do vfs access, but that goes direct to the transport
599
# and isn't seen by the FakeClient.
600
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
601
remote.RemoteBzrDirFormat(), _client=client)
602
branch = bzrdir.open_branch()
603
result = branch.get_stacked_on_url()
604
self.assertEqual('../base', result)
605
client.finished_test()
606
# it's in the fallback list both for the RemoteRepository and its vfs
608
self.assertEqual(1, len(branch.repository._fallback_repositories))
610
len(branch.repository._real_repository._fallback_repositories))
612
def test_get_stacked_on_real_branch(self):
613
base_branch = self.make_branch('base', format='1.6')
614
stacked_branch = self.make_branch('stacked', format='1.6')
615
stacked_branch.set_stacked_on_url('../base')
616
client = FakeClient(self.get_url())
617
client.add_expected_call(
618
'BzrDir.open_branch', ('stacked/',),
619
'success', ('ok', ''))
620
client.add_expected_call(
621
'BzrDir.find_repositoryV2', ('stacked/',),
622
'success', ('ok', '', 'no', 'no', 'no'))
623
# called twice, once from constructor and then again by us
624
client.add_expected_call(
625
'Branch.get_stacked_on_url', ('stacked/',),
626
'success', ('ok', '../base'))
627
client.add_expected_call(
628
'Branch.get_stacked_on_url', ('stacked/',),
629
'success', ('ok', '../base'))
630
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
631
remote.RemoteBzrDirFormat(), _client=client)
632
branch = bzrdir.open_branch()
633
result = branch.get_stacked_on_url()
634
self.assertEqual('../base', result)
635
client.finished_test()
636
# it's in the fallback list both for the RemoteRepository and its vfs
638
self.assertEqual(1, len(branch.repository._fallback_repositories))
640
len(branch.repository._real_repository._fallback_repositories))
643
class TestBranchSetLastRevision(RemoteBranchTestCase):
645
def test_set_empty(self):
646
# set_revision_history([]) is translated to calling
647
# Branch.set_last_revision(path, '') on the wire.
648
transport = MemoryTransport()
649
transport.mkdir('branch')
650
transport = transport.clone('branch')
652
client = FakeClient(transport.base)
653
client.add_expected_call(
654
'Branch.get_stacked_on_url', ('branch/',),
655
'error', ('NotStacked',))
656
client.add_expected_call(
657
'Branch.lock_write', ('branch/', '', ''),
658
'success', ('ok', 'branch token', 'repo token'))
659
client.add_expected_call(
660
'Branch.last_revision_info',
662
'success', ('ok', '0', 'null:'))
663
client.add_expected_call(
664
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
666
client.add_expected_call(
667
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
669
branch = self.make_remote_branch(transport, client)
670
# This is a hack to work around the problem that RemoteBranch currently
671
# unnecessarily invokes _ensure_real upon a call to lock_write.
672
branch._ensure_real = lambda: None
674
result = branch.set_revision_history([])
676
self.assertEqual(None, result)
677
client.finished_test()
679
def test_set_nonempty(self):
680
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
681
# Branch.set_last_revision(path, rev-idN) on the wire.
682
transport = MemoryTransport()
683
transport.mkdir('branch')
684
transport = transport.clone('branch')
686
client = FakeClient(transport.base)
687
client.add_expected_call(
688
'Branch.get_stacked_on_url', ('branch/',),
689
'error', ('NotStacked',))
690
client.add_expected_call(
691
'Branch.lock_write', ('branch/', '', ''),
692
'success', ('ok', 'branch token', 'repo token'))
693
client.add_expected_call(
694
'Branch.last_revision_info',
696
'success', ('ok', '0', 'null:'))
698
encoded_body = bz2.compress('\n'.join(lines))
699
client.add_success_response_with_body(encoded_body, 'ok')
700
client.add_expected_call(
701
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
703
client.add_expected_call(
704
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
706
branch = self.make_remote_branch(transport, client)
707
# This is a hack to work around the problem that RemoteBranch currently
708
# unnecessarily invokes _ensure_real upon a call to lock_write.
709
branch._ensure_real = lambda: None
710
# Lock the branch, reset the record of remote calls.
712
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
714
self.assertEqual(None, result)
715
client.finished_test()
717
def test_no_such_revision(self):
718
transport = MemoryTransport()
719
transport.mkdir('branch')
720
transport = transport.clone('branch')
721
# A response of 'NoSuchRevision' is translated into an exception.
722
client = FakeClient(transport.base)
723
client.add_expected_call(
724
'Branch.get_stacked_on_url', ('branch/',),
725
'error', ('NotStacked',))
726
client.add_expected_call(
727
'Branch.lock_write', ('branch/', '', ''),
728
'success', ('ok', 'branch token', 'repo token'))
729
client.add_expected_call(
730
'Branch.last_revision_info',
732
'success', ('ok', '0', 'null:'))
733
# get_graph calls to construct the revision history, for the set_rh
736
encoded_body = bz2.compress('\n'.join(lines))
737
client.add_success_response_with_body(encoded_body, 'ok')
738
client.add_expected_call(
739
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
740
'error', ('NoSuchRevision', 'rev-id'))
741
client.add_expected_call(
742
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
745
branch = self.make_remote_branch(transport, client)
748
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
750
client.finished_test()
752
def test_tip_change_rejected(self):
753
"""TipChangeRejected responses cause a TipChangeRejected exception to
756
transport = MemoryTransport()
757
transport.mkdir('branch')
758
transport = transport.clone('branch')
759
client = FakeClient(transport.base)
760
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
761
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
762
client.add_expected_call(
763
'Branch.get_stacked_on_url', ('branch/',),
764
'error', ('NotStacked',))
765
client.add_expected_call(
766
'Branch.lock_write', ('branch/', '', ''),
767
'success', ('ok', 'branch token', 'repo token'))
768
client.add_expected_call(
769
'Branch.last_revision_info',
771
'success', ('ok', '0', 'null:'))
773
encoded_body = bz2.compress('\n'.join(lines))
774
client.add_success_response_with_body(encoded_body, 'ok')
775
client.add_expected_call(
776
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
777
'error', ('TipChangeRejected', rejection_msg_utf8))
778
client.add_expected_call(
779
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
781
branch = self.make_remote_branch(transport, client)
782
branch._ensure_real = lambda: None
784
self.addCleanup(branch.unlock)
785
# The 'TipChangeRejected' error response triggered by calling
786
# set_revision_history causes a TipChangeRejected exception.
787
err = self.assertRaises(
788
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
789
# The UTF-8 message from the response has been decoded into a unicode
791
self.assertIsInstance(err.msg, unicode)
792
self.assertEqual(rejection_msg_unicode, err.msg)
794
client.finished_test()
797
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
799
def test_set_last_revision_info(self):
800
# set_last_revision_info(num, 'rev-id') is translated to calling
801
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
802
transport = MemoryTransport()
803
transport.mkdir('branch')
804
transport = transport.clone('branch')
805
client = FakeClient(transport.base)
807
client.add_error_response('NotStacked')
809
client.add_success_response('ok', 'branch token', 'repo token')
810
# query the current revision
811
client.add_success_response('ok', '0', 'null:')
813
client.add_success_response('ok')
815
client.add_success_response('ok')
817
branch = self.make_remote_branch(transport, client)
818
# Lock the branch, reset the record of remote calls.
821
result = branch.set_last_revision_info(1234, 'a-revision-id')
823
[('call', 'Branch.last_revision_info', ('branch/',)),
824
('call', 'Branch.set_last_revision_info',
825
('branch/', 'branch token', 'repo token',
826
'1234', 'a-revision-id'))],
828
self.assertEqual(None, result)
830
def test_no_such_revision(self):
831
# A response of 'NoSuchRevision' is translated into an exception.
832
transport = MemoryTransport()
833
transport.mkdir('branch')
834
transport = transport.clone('branch')
835
client = FakeClient(transport.base)
837
client.add_error_response('NotStacked')
839
client.add_success_response('ok', 'branch token', 'repo token')
841
client.add_error_response('NoSuchRevision', 'revid')
843
client.add_success_response('ok')
845
branch = self.make_remote_branch(transport, client)
846
# Lock the branch, reset the record of remote calls.
851
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
854
def lock_remote_branch(self, branch):
855
"""Trick a RemoteBranch into thinking it is locked."""
856
branch._lock_mode = 'w'
857
branch._lock_count = 2
858
branch._lock_token = 'branch token'
859
branch._repo_lock_token = 'repo token'
860
branch.repository._lock_mode = 'w'
861
branch.repository._lock_count = 2
862
branch.repository._lock_token = 'repo token'
864
def test_backwards_compatibility(self):
865
"""If the server does not support the Branch.set_last_revision_info
866
verb (which is new in 1.4), then the client falls back to VFS methods.
868
# This test is a little messy. Unlike most tests in this file, it
869
# doesn't purely test what a Remote* object sends over the wire, and
870
# how it reacts to responses from the wire. It instead relies partly
871
# on asserting that the RemoteBranch will call
872
# self._real_branch.set_last_revision_info(...).
874
# First, set up our RemoteBranch with a FakeClient that raises
875
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
876
transport = MemoryTransport()
877
transport.mkdir('branch')
878
transport = transport.clone('branch')
879
client = FakeClient(transport.base)
880
client.add_expected_call(
881
'Branch.get_stacked_on_url', ('branch/',),
882
'error', ('NotStacked',))
883
client.add_expected_call(
884
'Branch.last_revision_info',
886
'success', ('ok', '0', 'null:'))
887
client.add_expected_call(
888
'Branch.set_last_revision_info',
889
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
890
'unknown', 'Branch.set_last_revision_info')
892
branch = self.make_remote_branch(transport, client)
893
class StubRealBranch(object):
896
def set_last_revision_info(self, revno, revision_id):
898
('set_last_revision_info', revno, revision_id))
899
def _clear_cached_state(self):
901
real_branch = StubRealBranch()
902
branch._real_branch = real_branch
903
self.lock_remote_branch(branch)
905
# Call set_last_revision_info, and verify it behaved as expected.
906
result = branch.set_last_revision_info(1234, 'a-revision-id')
908
[('set_last_revision_info', 1234, 'a-revision-id')],
910
client.finished_test()
912
def test_unexpected_error(self):
913
# If the server sends an error the client doesn't understand, it gets
914
# turned into an UnknownErrorFromSmartServer, which is presented as a
915
# non-internal error to the user.
916
transport = MemoryTransport()
917
transport.mkdir('branch')
918
transport = transport.clone('branch')
919
client = FakeClient(transport.base)
921
client.add_error_response('NotStacked')
923
client.add_success_response('ok', 'branch token', 'repo token')
925
client.add_error_response('UnexpectedError')
927
client.add_success_response('ok')
929
branch = self.make_remote_branch(transport, client)
930
# Lock the branch, reset the record of remote calls.
934
err = self.assertRaises(
935
errors.UnknownErrorFromSmartServer,
936
branch.set_last_revision_info, 123, 'revid')
937
self.assertEqual(('UnexpectedError',), err.error_tuple)
940
def test_tip_change_rejected(self):
941
"""TipChangeRejected responses cause a TipChangeRejected exception to
944
transport = MemoryTransport()
945
transport.mkdir('branch')
946
transport = transport.clone('branch')
947
client = FakeClient(transport.base)
949
client.add_error_response('NotStacked')
951
client.add_success_response('ok', 'branch token', 'repo token')
953
client.add_error_response('TipChangeRejected', 'rejection message')
955
client.add_success_response('ok')
957
branch = self.make_remote_branch(transport, client)
958
# Lock the branch, reset the record of remote calls.
960
self.addCleanup(branch.unlock)
963
# The 'TipChangeRejected' error response triggered by calling
964
# set_last_revision_info causes a TipChangeRejected exception.
965
err = self.assertRaises(
966
errors.TipChangeRejected,
967
branch.set_last_revision_info, 123, 'revid')
968
self.assertEqual('rejection message', err.msg)
971
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
972
"""Getting the branch configuration should use an abstract method not vfs.
975
def test_get_branch_conf(self):
976
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
977
## # We should see that branch.get_config() does a single rpc to get the
978
## # remote configuration file, abstracting away where that is stored on
979
## # the server. However at the moment it always falls back to using the
980
## # vfs, and this would need some changes in config.py.
982
## # in an empty branch we decode the response properly
983
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
984
## # we need to make a real branch because the remote_branch.control_files
985
## # will trigger _ensure_real.
986
## branch = self.make_branch('quack')
987
## transport = branch.bzrdir.root_transport
988
## # we do not want bzrdir to make any remote calls
989
## bzrdir = RemoteBzrDir(transport, _client=False)
990
## branch = RemoteBranch(bzrdir, None, _client=client)
991
## config = branch.get_config()
993
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
997
class TestBranchLockWrite(RemoteBranchTestCase):
999
def test_lock_write_unlockable(self):
1000
transport = MemoryTransport()
1001
client = FakeClient(transport.base)
1002
client.add_expected_call(
1003
'Branch.get_stacked_on_url', ('quack/',),
1004
'error', ('NotStacked',),)
1005
client.add_expected_call(
1006
'Branch.lock_write', ('quack/', '', ''),
1007
'error', ('UnlockableTransport',))
1008
transport.mkdir('quack')
1009
transport = transport.clone('quack')
1010
branch = self.make_remote_branch(transport, client)
1011
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
1012
client.finished_test()
1015
class TestTransportIsReadonly(tests.TestCase):
1017
def test_true(self):
1018
client = FakeClient()
1019
client.add_success_response('yes')
1020
transport = RemoteTransport('bzr://example.com/', medium=False,
1022
self.assertEqual(True, transport.is_readonly())
1024
[('call', 'Transport.is_readonly', ())],
1027
def test_false(self):
1028
client = FakeClient()
1029
client.add_success_response('no')
1030
transport = RemoteTransport('bzr://example.com/', medium=False,
1032
self.assertEqual(False, transport.is_readonly())
1034
[('call', 'Transport.is_readonly', ())],
1037
def test_error_from_old_server(self):
1038
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1040
Clients should treat it as a "no" response, because is_readonly is only
1041
advisory anyway (a transport could be read-write, but then the
1042
underlying filesystem could be readonly anyway).
1044
client = FakeClient()
1045
client.add_unknown_method_response('Transport.is_readonly')
1046
transport = RemoteTransport('bzr://example.com/', medium=False,
1048
self.assertEqual(False, transport.is_readonly())
1050
[('call', 'Transport.is_readonly', ())],
1054
class TestTransportMkdir(tests.TestCase):
1056
def test_permissiondenied(self):
1057
client = FakeClient()
1058
client.add_error_response('PermissionDenied', 'remote path', 'extra')
1059
transport = RemoteTransport('bzr://example.com/', medium=False,
1061
exc = self.assertRaises(
1062
errors.PermissionDenied, transport.mkdir, 'client path')
1063
expected_error = errors.PermissionDenied('/client path', 'extra')
1064
self.assertEqual(expected_error, exc)
1067
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1069
def test_defaults_to_none(self):
1070
t = RemoteSSHTransport('bzr+ssh://example.com')
1071
self.assertIs(None, t._get_credentials()[0])
1073
def test_uses_authentication_config(self):
1074
conf = config.AuthenticationConfig()
1075
conf._get_config().update(
1076
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1079
t = RemoteSSHTransport('bzr+ssh://example.com')
1080
self.assertEqual('bar', t._get_credentials()[0])
1083
class TestRemoteRepository(tests.TestCase):
1084
"""Base for testing RemoteRepository protocol usage.
1086
These tests contain frozen requests and responses. We want any changes to
1087
what is sent or expected to be require a thoughtful update to these tests
1088
because they might break compatibility with different-versioned servers.
1091
def setup_fake_client_and_repository(self, transport_path):
1092
"""Create the fake client and repository for testing with.
1094
There's no real server here; we just have canned responses sent
1097
:param transport_path: Path below the root of the MemoryTransport
1098
where the repository will be created.
1100
transport = MemoryTransport()
1101
transport.mkdir(transport_path)
1102
client = FakeClient(transport.base)
1103
transport = transport.clone(transport_path)
1104
# we do not want bzrdir to make any remote calls
1105
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
1107
repo = RemoteRepository(bzrdir, None, _client=client)
1111
class TestRepositoryGatherStats(TestRemoteRepository):
1113
def test_revid_none(self):
1114
# ('ok',), body with revisions and size
1115
transport_path = 'quack'
1116
repo, client = self.setup_fake_client_and_repository(transport_path)
1117
client.add_success_response_with_body(
1118
'revisions: 2\nsize: 18\n', 'ok')
1119
result = repo.gather_stats(None)
1121
[('call_expecting_body', 'Repository.gather_stats',
1122
('quack/','','no'))],
1124
self.assertEqual({'revisions': 2, 'size': 18}, result)
1126
def test_revid_no_committers(self):
1127
# ('ok',), body without committers
1128
body = ('firstrev: 123456.300 3600\n'
1129
'latestrev: 654231.400 0\n'
1132
transport_path = 'quick'
1133
revid = u'\xc8'.encode('utf8')
1134
repo, client = self.setup_fake_client_and_repository(transport_path)
1135
client.add_success_response_with_body(body, 'ok')
1136
result = repo.gather_stats(revid)
1138
[('call_expecting_body', 'Repository.gather_stats',
1139
('quick/', revid, 'no'))],
1141
self.assertEqual({'revisions': 2, 'size': 18,
1142
'firstrev': (123456.300, 3600),
1143
'latestrev': (654231.400, 0),},
1146
def test_revid_with_committers(self):
1147
# ('ok',), body with committers
1148
body = ('committers: 128\n'
1149
'firstrev: 123456.300 3600\n'
1150
'latestrev: 654231.400 0\n'
1153
transport_path = 'buick'
1154
revid = u'\xc8'.encode('utf8')
1155
repo, client = self.setup_fake_client_and_repository(transport_path)
1156
client.add_success_response_with_body(body, 'ok')
1157
result = repo.gather_stats(revid, True)
1159
[('call_expecting_body', 'Repository.gather_stats',
1160
('buick/', revid, 'yes'))],
1162
self.assertEqual({'revisions': 2, 'size': 18,
1164
'firstrev': (123456.300, 3600),
1165
'latestrev': (654231.400, 0),},
1169
class TestRepositoryGetGraph(TestRemoteRepository):
1171
def test_get_graph(self):
1172
# get_graph returns a graph with a custom parents provider.
1173
transport_path = 'quack'
1174
repo, client = self.setup_fake_client_and_repository(transport_path)
1175
graph = repo.get_graph()
1176
self.assertNotEqual(graph._parents_provider, repo)
1179
class TestRepositoryGetParentMap(TestRemoteRepository):
1181
def test_get_parent_map_caching(self):
1182
# get_parent_map returns from cache until unlock()
1183
# setup a reponse with two revisions
1184
r1 = u'\u0e33'.encode('utf8')
1185
r2 = u'\u0dab'.encode('utf8')
1186
lines = [' '.join([r2, r1]), r1]
1187
encoded_body = bz2.compress('\n'.join(lines))
1189
transport_path = 'quack'
1190
repo, client = self.setup_fake_client_and_repository(transport_path)
1191
client.add_success_response_with_body(encoded_body, 'ok')
1192
client.add_success_response_with_body(encoded_body, 'ok')
1194
graph = repo.get_graph()
1195
parents = graph.get_parent_map([r2])
1196
self.assertEqual({r2: (r1,)}, parents)
1197
# locking and unlocking deeper should not reset
1200
parents = graph.get_parent_map([r1])
1201
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1203
[('call_with_body_bytes_expecting_body',
1204
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1207
# now we call again, and it should use the second response.
1209
graph = repo.get_graph()
1210
parents = graph.get_parent_map([r1])
1211
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1213
[('call_with_body_bytes_expecting_body',
1214
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1215
('call_with_body_bytes_expecting_body',
1216
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1221
def test_get_parent_map_reconnects_if_unknown_method(self):
1222
transport_path = 'quack'
1223
repo, client = self.setup_fake_client_and_repository(transport_path)
1224
client.add_unknown_method_response('Repository,get_parent_map')
1225
client.add_success_response_with_body('', 'ok')
1226
self.assertFalse(client._medium._is_remote_before((1, 2)))
1227
rev_id = 'revision-id'
1228
expected_deprecations = [
1229
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1231
parents = self.callDeprecated(
1232
expected_deprecations, repo.get_parent_map, [rev_id])
1234
[('call_with_body_bytes_expecting_body',
1235
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1236
('disconnect medium',),
1237
('call_expecting_body', 'Repository.get_revision_graph',
1240
# The medium is now marked as being connected to an older server
1241
self.assertTrue(client._medium._is_remote_before((1, 2)))
1243
def test_get_parent_map_fallback_parentless_node(self):
1244
"""get_parent_map falls back to get_revision_graph on old servers. The
1245
results from get_revision_graph are tweaked to match the get_parent_map
1248
Specifically, a {key: ()} result from get_revision_graph means "no
1249
parents" for that key, which in get_parent_map results should be
1250
represented as {key: ('null:',)}.
1252
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1254
rev_id = 'revision-id'
1255
transport_path = 'quack'
1256
repo, client = self.setup_fake_client_and_repository(transport_path)
1257
client.add_success_response_with_body(rev_id, 'ok')
1258
client._medium._remember_remote_is_before((1, 2))
1259
expected_deprecations = [
1260
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1262
parents = self.callDeprecated(
1263
expected_deprecations, repo.get_parent_map, [rev_id])
1265
[('call_expecting_body', 'Repository.get_revision_graph',
1268
self.assertEqual({rev_id: ('null:',)}, parents)
1270
def test_get_parent_map_unexpected_response(self):
1271
repo, client = self.setup_fake_client_and_repository('path')
1272
client.add_success_response('something unexpected!')
1274
errors.UnexpectedSmartServerResponse,
1275
repo.get_parent_map, ['a-revision-id'])
1278
class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
1280
def test_allows_new_revisions(self):
1281
"""get_parent_map's results can be updated by commit."""
1282
smart_server = server.SmartTCPServer_for_testing()
1283
smart_server.setUp()
1284
self.addCleanup(smart_server.tearDown)
1285
self.make_branch('branch')
1286
branch = Branch.open(smart_server.get_url() + '/branch')
1287
tree = branch.create_checkout('tree', lightweight=True)
1289
self.addCleanup(tree.unlock)
1290
graph = tree.branch.repository.get_graph()
1291
# This provides an opportunity for the missing rev-id to be cached.
1292
self.assertEqual({}, graph.get_parent_map(['rev1']))
1293
tree.commit('message', rev_id='rev1')
1294
graph = tree.branch.repository.get_graph()
1295
self.assertEqual({'rev1': ('null:',)}, graph.get_parent_map(['rev1']))
1298
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1300
def test_null_revision(self):
1301
# a null revision has the predictable result {}, we should have no wire
1302
# traffic when calling it with this argument
1303
transport_path = 'empty'
1304
repo, client = self.setup_fake_client_and_repository(transport_path)
1305
client.add_success_response('notused')
1306
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1308
self.assertEqual([], client._calls)
1309
self.assertEqual({}, result)
1311
def test_none_revision(self):
1312
# with none we want the entire graph
1313
r1 = u'\u0e33'.encode('utf8')
1314
r2 = u'\u0dab'.encode('utf8')
1315
lines = [' '.join([r2, r1]), r1]
1316
encoded_body = '\n'.join(lines)
1318
transport_path = 'sinhala'
1319
repo, client = self.setup_fake_client_and_repository(transport_path)
1320
client.add_success_response_with_body(encoded_body, 'ok')
1321
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1323
[('call_expecting_body', 'Repository.get_revision_graph',
1326
self.assertEqual({r1: (), r2: (r1, )}, result)
1328
def test_specific_revision(self):
1329
# with a specific revision we want the graph for that
1330
# with none we want the entire graph
1331
r11 = u'\u0e33'.encode('utf8')
1332
r12 = u'\xc9'.encode('utf8')
1333
r2 = u'\u0dab'.encode('utf8')
1334
lines = [' '.join([r2, r11, r12]), r11, r12]
1335
encoded_body = '\n'.join(lines)
1337
transport_path = 'sinhala'
1338
repo, client = self.setup_fake_client_and_repository(transport_path)
1339
client.add_success_response_with_body(encoded_body, 'ok')
1340
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1342
[('call_expecting_body', 'Repository.get_revision_graph',
1345
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1347
def test_no_such_revision(self):
1349
transport_path = 'sinhala'
1350
repo, client = self.setup_fake_client_and_repository(transport_path)
1351
client.add_error_response('nosuchrevision', revid)
1352
# also check that the right revision is reported in the error
1353
self.assertRaises(errors.NoSuchRevision,
1354
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1356
[('call_expecting_body', 'Repository.get_revision_graph',
1357
('sinhala/', revid))],
1360
def test_unexpected_error(self):
1362
transport_path = 'sinhala'
1363
repo, client = self.setup_fake_client_and_repository(transport_path)
1364
client.add_error_response('AnUnexpectedError')
1365
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1366
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1367
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1370
class TestRepositoryIsShared(TestRemoteRepository):
1372
def test_is_shared(self):
1373
# ('yes', ) for Repository.is_shared -> 'True'.
1374
transport_path = 'quack'
1375
repo, client = self.setup_fake_client_and_repository(transport_path)
1376
client.add_success_response('yes')
1377
result = repo.is_shared()
1379
[('call', 'Repository.is_shared', ('quack/',))],
1381
self.assertEqual(True, result)
1383
def test_is_not_shared(self):
1384
# ('no', ) for Repository.is_shared -> 'False'.
1385
transport_path = 'qwack'
1386
repo, client = self.setup_fake_client_and_repository(transport_path)
1387
client.add_success_response('no')
1388
result = repo.is_shared()
1390
[('call', 'Repository.is_shared', ('qwack/',))],
1392
self.assertEqual(False, result)
1395
class TestRepositoryLockWrite(TestRemoteRepository):
1397
def test_lock_write(self):
1398
transport_path = 'quack'
1399
repo, client = self.setup_fake_client_and_repository(transport_path)
1400
client.add_success_response('ok', 'a token')
1401
result = repo.lock_write()
1403
[('call', 'Repository.lock_write', ('quack/', ''))],
1405
self.assertEqual('a token', result)
1407
def test_lock_write_already_locked(self):
1408
transport_path = 'quack'
1409
repo, client = self.setup_fake_client_and_repository(transport_path)
1410
client.add_error_response('LockContention')
1411
self.assertRaises(errors.LockContention, repo.lock_write)
1413
[('call', 'Repository.lock_write', ('quack/', ''))],
1416
def test_lock_write_unlockable(self):
1417
transport_path = 'quack'
1418
repo, client = self.setup_fake_client_and_repository(transport_path)
1419
client.add_error_response('UnlockableTransport')
1420
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1422
[('call', 'Repository.lock_write', ('quack/', ''))],
1426
class TestRepositoryUnlock(TestRemoteRepository):
1428
def test_unlock(self):
1429
transport_path = 'quack'
1430
repo, client = self.setup_fake_client_and_repository(transport_path)
1431
client.add_success_response('ok', 'a token')
1432
client.add_success_response('ok')
1436
[('call', 'Repository.lock_write', ('quack/', '')),
1437
('call', 'Repository.unlock', ('quack/', 'a token'))],
1440
def test_unlock_wrong_token(self):
1441
# If somehow the token is wrong, unlock will raise TokenMismatch.
1442
transport_path = 'quack'
1443
repo, client = self.setup_fake_client_and_repository(transport_path)
1444
client.add_success_response('ok', 'a token')
1445
client.add_error_response('TokenMismatch')
1447
self.assertRaises(errors.TokenMismatch, repo.unlock)
1450
class TestRepositoryHasRevision(TestRemoteRepository):
1452
def test_none(self):
1453
# repo.has_revision(None) should not cause any traffic.
1454
transport_path = 'quack'
1455
repo, client = self.setup_fake_client_and_repository(transport_path)
1457
# The null revision is always there, so has_revision(None) == True.
1458
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1460
# The remote repo shouldn't be accessed.
1461
self.assertEqual([], client._calls)
1464
class TestRepositoryTarball(TestRemoteRepository):
1466
# This is a canned tarball reponse we can validate against
1468
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1469
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1470
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1471
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1472
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1473
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1474
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1475
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1476
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1477
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1478
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1479
'nWQ7QH/F3JFOFCQ0aSPfA='
1482
def test_repository_tarball(self):
1483
# Test that Repository.tarball generates the right operations
1484
transport_path = 'repo'
1485
expected_calls = [('call_expecting_body', 'Repository.tarball',
1486
('repo/', 'bz2',),),
1488
repo, client = self.setup_fake_client_and_repository(transport_path)
1489
client.add_success_response_with_body(self.tarball_content, 'ok')
1490
# Now actually ask for the tarball
1491
tarball_file = repo._get_tarball('bz2')
1493
self.assertEqual(expected_calls, client._calls)
1494
self.assertEqual(self.tarball_content, tarball_file.read())
1496
tarball_file.close()
1499
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1500
"""RemoteRepository.copy_content_into optimizations"""
1502
def test_copy_content_remote_to_local(self):
1503
self.transport_server = server.SmartTCPServer_for_testing
1504
src_repo = self.make_repository('repo1')
1505
src_repo = repository.Repository.open(self.get_url('repo1'))
1506
# At the moment the tarball-based copy_content_into can't write back
1507
# into a smart server. It would be good if it could upload the
1508
# tarball; once that works we'd have to create repositories of
1509
# different formats. -- mbp 20070410
1510
dest_url = self.get_vfs_only_url('repo2')
1511
dest_bzrdir = BzrDir.create(dest_url)
1512
dest_repo = dest_bzrdir.create_repository()
1513
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1514
self.assertTrue(isinstance(src_repo, RemoteRepository))
1515
src_repo.copy_content_into(dest_repo)
1518
class _StubRealPackRepository(object):
1520
def __init__(self, calls):
1521
self._pack_collection = _StubPackCollection(calls)
1524
class _StubPackCollection(object):
1526
def __init__(self, calls):
1530
self.calls.append(('pack collection autopack',))
1532
def reload_pack_names(self):
1533
self.calls.append(('pack collection reload_pack_names',))
1536
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1537
"""Tests for RemoteRepository.autopack implementation."""
1540
"""When the server returns 'ok' and there's no _real_repository, then
1541
nothing else happens: the autopack method is done.
1543
transport_path = 'quack'
1544
repo, client = self.setup_fake_client_and_repository(transport_path)
1545
client.add_expected_call(
1546
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1548
client.finished_test()
1550
def test_ok_with_real_repo(self):
1551
"""When the server returns 'ok' and there is a _real_repository, then
1552
the _real_repository's reload_pack_name's method will be called.
1554
transport_path = 'quack'
1555
repo, client = self.setup_fake_client_and_repository(transport_path)
1556
client.add_expected_call(
1557
'PackRepository.autopack', ('quack/',),
1559
repo._real_repository = _StubRealPackRepository(client._calls)
1562
[('call', 'PackRepository.autopack', ('quack/',)),
1563
('pack collection reload_pack_names',)],
1566
def test_backwards_compatibility(self):
1567
"""If the server does not recognise the PackRepository.autopack verb,
1568
fallback to the real_repository's implementation.
1570
transport_path = 'quack'
1571
repo, client = self.setup_fake_client_and_repository(transport_path)
1572
client.add_unknown_method_response('PackRepository.autopack')
1573
def stub_ensure_real():
1574
client._calls.append(('_ensure_real',))
1575
repo._real_repository = _StubRealPackRepository(client._calls)
1576
repo._ensure_real = stub_ensure_real
1579
[('call', 'PackRepository.autopack', ('quack/',)),
1581
('pack collection autopack',)],
1585
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1586
"""Base class for unit tests for bzrlib.remote._translate_error."""
1588
def translateTuple(self, error_tuple, **context):
1589
"""Call _translate_error with an ErrorFromSmartServer built from the
1592
:param error_tuple: A tuple of a smart server response, as would be
1593
passed to an ErrorFromSmartServer.
1594
:kwargs context: context items to call _translate_error with.
1596
:returns: The error raised by _translate_error.
1598
# Raise the ErrorFromSmartServer before passing it as an argument,
1599
# because _translate_error may need to re-raise it with a bare 'raise'
1601
server_error = errors.ErrorFromSmartServer(error_tuple)
1602
translated_error = self.translateErrorFromSmartServer(
1603
server_error, **context)
1604
return translated_error
1606
def translateErrorFromSmartServer(self, error_object, **context):
1607
"""Like translateTuple, but takes an already constructed
1608
ErrorFromSmartServer rather than a tuple.
1612
except errors.ErrorFromSmartServer, server_error:
1613
translated_error = self.assertRaises(
1614
errors.BzrError, remote._translate_error, server_error,
1616
return translated_error
1619
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1620
"""Unit tests for bzrlib.remote._translate_error.
1622
Given an ErrorFromSmartServer (which has an error tuple from a smart
1623
server) and some context, _translate_error raises more specific errors from
1626
This test case covers the cases where _translate_error succeeds in
1627
translating an ErrorFromSmartServer to something better. See
1628
TestErrorTranslationRobustness for other cases.
1631
def test_NoSuchRevision(self):
1632
branch = self.make_branch('')
1634
translated_error = self.translateTuple(
1635
('NoSuchRevision', revid), branch=branch)
1636
expected_error = errors.NoSuchRevision(branch, revid)
1637
self.assertEqual(expected_error, translated_error)
1639
def test_nosuchrevision(self):
1640
repository = self.make_repository('')
1642
translated_error = self.translateTuple(
1643
('nosuchrevision', revid), repository=repository)
1644
expected_error = errors.NoSuchRevision(repository, revid)
1645
self.assertEqual(expected_error, translated_error)
1647
def test_nobranch(self):
1648
bzrdir = self.make_bzrdir('')
1649
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1650
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1651
self.assertEqual(expected_error, translated_error)
1653
def test_LockContention(self):
1654
translated_error = self.translateTuple(('LockContention',))
1655
expected_error = errors.LockContention('(remote lock)')
1656
self.assertEqual(expected_error, translated_error)
1658
def test_UnlockableTransport(self):
1659
bzrdir = self.make_bzrdir('')
1660
translated_error = self.translateTuple(
1661
('UnlockableTransport',), bzrdir=bzrdir)
1662
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1663
self.assertEqual(expected_error, translated_error)
1665
def test_LockFailed(self):
1666
lock = 'str() of a server lock'
1667
why = 'str() of why'
1668
translated_error = self.translateTuple(('LockFailed', lock, why))
1669
expected_error = errors.LockFailed(lock, why)
1670
self.assertEqual(expected_error, translated_error)
1672
def test_TokenMismatch(self):
1673
token = 'a lock token'
1674
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1675
expected_error = errors.TokenMismatch(token, '(remote token)')
1676
self.assertEqual(expected_error, translated_error)
1678
def test_Diverged(self):
1679
branch = self.make_branch('a')
1680
other_branch = self.make_branch('b')
1681
translated_error = self.translateTuple(
1682
('Diverged',), branch=branch, other_branch=other_branch)
1683
expected_error = errors.DivergedBranches(branch, other_branch)
1684
self.assertEqual(expected_error, translated_error)
1686
def test_ReadError_no_args(self):
1688
translated_error = self.translateTuple(('ReadError',), path=path)
1689
expected_error = errors.ReadError(path)
1690
self.assertEqual(expected_error, translated_error)
1692
def test_ReadError(self):
1694
translated_error = self.translateTuple(('ReadError', path))
1695
expected_error = errors.ReadError(path)
1696
self.assertEqual(expected_error, translated_error)
1698
def test_PermissionDenied_no_args(self):
1700
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1701
expected_error = errors.PermissionDenied(path)
1702
self.assertEqual(expected_error, translated_error)
1704
def test_PermissionDenied_one_arg(self):
1706
translated_error = self.translateTuple(('PermissionDenied', path))
1707
expected_error = errors.PermissionDenied(path)
1708
self.assertEqual(expected_error, translated_error)
1710
def test_PermissionDenied_one_arg_and_context(self):
1711
"""Given a choice between a path from the local context and a path on
1712
the wire, _translate_error prefers the path from the local context.
1714
local_path = 'local path'
1715
remote_path = 'remote path'
1716
translated_error = self.translateTuple(
1717
('PermissionDenied', remote_path), path=local_path)
1718
expected_error = errors.PermissionDenied(local_path)
1719
self.assertEqual(expected_error, translated_error)
1721
def test_PermissionDenied_two_args(self):
1723
extra = 'a string with extra info'
1724
translated_error = self.translateTuple(
1725
('PermissionDenied', path, extra))
1726
expected_error = errors.PermissionDenied(path, extra)
1727
self.assertEqual(expected_error, translated_error)
1730
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1731
"""Unit tests for bzrlib.remote._translate_error's robustness.
1733
TestErrorTranslationSuccess is for cases where _translate_error can
1734
translate successfully. This class about how _translate_err behaves when
1735
it fails to translate: it re-raises the original error.
1738
def test_unrecognised_server_error(self):
1739
"""If the error code from the server is not recognised, the original
1740
ErrorFromSmartServer is propagated unmodified.
1742
error_tuple = ('An unknown error tuple',)
1743
server_error = errors.ErrorFromSmartServer(error_tuple)
1744
translated_error = self.translateErrorFromSmartServer(server_error)
1745
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1746
self.assertEqual(expected_error, translated_error)
1748
def test_context_missing_a_key(self):
1749
"""In case of a bug in the client, or perhaps an unexpected response
1750
from a server, _translate_error returns the original error tuple from
1751
the server and mutters a warning.
1753
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1754
# in the context dict. So let's give it an empty context dict instead
1755
# to exercise its error recovery.
1757
error_tuple = ('NoSuchRevision', 'revid')
1758
server_error = errors.ErrorFromSmartServer(error_tuple)
1759
translated_error = self.translateErrorFromSmartServer(server_error)
1760
self.assertEqual(server_error, translated_error)
1761
# In addition to re-raising ErrorFromSmartServer, some debug info has
1762
# been muttered to the log file for developer to look at.
1763
self.assertContainsRe(
1764
self._get_log(keep_log_file=True),
1765
"Missing key 'branch' in context")
1767
def test_path_missing(self):
1768
"""Some translations (PermissionDenied, ReadError) can determine the
1769
'path' variable from either the wire or the local context. If neither
1770
has it, then an error is raised.
1772
error_tuple = ('ReadError',)
1773
server_error = errors.ErrorFromSmartServer(error_tuple)
1774
translated_error = self.translateErrorFromSmartServer(server_error)
1775
self.assertEqual(server_error, translated_error)
1776
# In addition to re-raising ErrorFromSmartServer, some debug info has
1777
# been muttered to the log file for developer to look at.
1778
self.assertContainsRe(
1779
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1782
class TestStacking(tests.TestCaseWithTransport):
1783
"""Tests for operations on stacked remote repositories.
1785
The underlying format type must support stacking.
1788
def test_access_stacked_remote(self):
1789
# based on <http://launchpad.net/bugs/261315>
1790
# make a branch stacked on another repository containing an empty
1791
# revision, then open it over hpss - we should be able to see that
1793
base_transport = self.get_transport()
1794
base_builder = self.make_branch_builder('base', format='1.6')
1795
base_builder.start_series()
1796
base_revid = base_builder.build_snapshot('rev-id', None,
1797
[('add', ('', None, 'directory', None))],
1799
base_builder.finish_series()
1800
stacked_branch = self.make_branch('stacked', format='1.6')
1801
stacked_branch.set_stacked_on_url('../base')
1802
# start a server looking at this
1803
smart_server = server.SmartTCPServer_for_testing()
1804
smart_server.setUp()
1805
self.addCleanup(smart_server.tearDown)
1806
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1807
# can get its branch and repository
1808
remote_branch = remote_bzrdir.open_branch()
1809
remote_repo = remote_branch.repository
1810
remote_repo.lock_read()
1812
# it should have an appropriate fallback repository, which should also
1813
# be a RemoteRepository
1814
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1815
self.assertIsInstance(remote_repo._fallback_repositories[0],
1817
# and it has the revision committed to the underlying repository;
1818
# these have varying implementations so we try several of them
1819
self.assertTrue(remote_repo.has_revisions([base_revid]))
1820
self.assertTrue(remote_repo.has_revision(base_revid))
1821
self.assertEqual(remote_repo.get_revision(base_revid).message,
1824
remote_repo.unlock()
1826
def prepare_stacked_remote_branch(self):
1827
smart_server = server.SmartTCPServer_for_testing()
1828
smart_server.setUp()
1829
self.addCleanup(smart_server.tearDown)
1830
tree1 = self.make_branch_and_tree('tree1')
1831
tree1.commit('rev1', rev_id='rev1')
1832
tree2 = self.make_branch_and_tree('tree2', format='1.6')
1833
tree2.branch.set_stacked_on_url(tree1.branch.base)
1834
branch2 = Branch.open(smart_server.get_url() + '/tree2')
1836
self.addCleanup(branch2.unlock)
1839
def test_stacked_get_parent_map(self):
1840
# the public implementation of get_parent_map obeys stacking
1841
branch = self.prepare_stacked_remote_branch()
1842
repo = branch.repository
1843
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
1845
def test_unstacked_get_parent_map(self):
1846
# _unstacked_provider.get_parent_map ignores stacking
1847
branch = self.prepare_stacked_remote_branch()
1848
provider = branch.repository._unstacked_provider
1849
self.assertEqual([], provider.get_parent_map(['rev1']).keys())
1852
class TestRemoteBranchEffort(tests.TestCaseWithTransport):
1855
super(TestRemoteBranchEffort, self).setUp()
1856
# Create a smart server that publishes whatever the backing VFS server
1858
self.smart_server = server.SmartTCPServer_for_testing()
1859
self.smart_server.setUp(self.get_server())
1860
self.addCleanup(self.smart_server.tearDown)
1861
# Log all HPSS calls into self.hpss_calls.
1862
_SmartClient.hooks.install_named_hook(
1863
'call', self.capture_hpss_call, None)
1864
self.hpss_calls = []
1866
def capture_hpss_call(self, params):
1867
self.hpss_calls.append(params.method)
1869
def test_copy_content_into_avoids_revision_history(self):
1870
local = self.make_branch('local')
1871
remote_backing_tree = self.make_branch_and_tree('remote')
1872
remote_backing_tree.commit("Commit.")
1873
remote_branch_url = self.smart_server.get_url() + 'remote'
1874
remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
1875
local.repository.fetch(remote_branch.repository)
1876
self.hpss_calls = []
1877
remote_branch.copy_content_into(local)
1878
self.assertFalse('Branch.revision_history' in self.hpss_calls)