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
41
from bzrlib.branch import Branch
42
from bzrlib.bzrdir import BzrDir, BzrDirFormat
43
from bzrlib.remote import (
49
from bzrlib.revision import NULL_REVISION
50
from bzrlib.smart import server, medium
51
from bzrlib.smart.client import _SmartClient
52
from bzrlib.symbol_versioning import one_four
53
from bzrlib.transport import get_transport, http
54
from bzrlib.transport.memory import MemoryTransport
55
from bzrlib.transport.remote import (
62
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
65
self.transport_server = server.SmartTCPServer_for_testing
66
super(BasicRemoteObjectTests, self).setUp()
67
self.transport = self.get_transport()
68
# make a branch that can be opened over the smart transport
69
self.local_wt = BzrDir.create_standalone_workingtree('.')
72
self.transport.disconnect()
73
tests.TestCaseWithTransport.tearDown(self)
75
def test_create_remote_bzrdir(self):
76
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
77
self.assertIsInstance(b, BzrDir)
79
def test_open_remote_branch(self):
80
# open a standalone branch in the working directory
81
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
82
branch = b.open_branch()
83
self.assertIsInstance(branch, Branch)
85
def test_remote_repository(self):
86
b = BzrDir.open_from_transport(self.transport)
87
repo = b.open_repository()
88
revid = u'\xc823123123'.encode('utf8')
89
self.assertFalse(repo.has_revision(revid))
90
self.local_wt.commit(message='test commit', rev_id=revid)
91
self.assertTrue(repo.has_revision(revid))
93
def test_remote_branch_revision_history(self):
94
b = BzrDir.open_from_transport(self.transport).open_branch()
95
self.assertEqual([], b.revision_history())
96
r1 = self.local_wt.commit('1st commit')
97
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
98
self.assertEqual([r1, r2], b.revision_history())
100
def test_find_correct_format(self):
101
"""Should open a RemoteBzrDir over a RemoteTransport"""
102
fmt = BzrDirFormat.find_format(self.transport)
103
self.assertTrue(RemoteBzrDirFormat
104
in BzrDirFormat._control_server_formats)
105
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
107
def test_open_detected_smart_format(self):
108
fmt = BzrDirFormat.find_format(self.transport)
109
d = fmt.open(self.transport)
110
self.assertIsInstance(d, BzrDir)
112
def test_remote_branch_repr(self):
113
b = BzrDir.open_from_transport(self.transport).open_branch()
114
self.assertStartsWith(str(b), 'RemoteBranch(')
117
class FakeProtocol(object):
118
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
120
def __init__(self, body, fake_client):
122
self._body_buffer = None
123
self._fake_client = fake_client
125
def read_body_bytes(self, count=-1):
126
if self._body_buffer is None:
127
self._body_buffer = StringIO(self.body)
128
bytes = self._body_buffer.read(count)
129
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
130
self._fake_client.expecting_body = False
133
def cancel_read_body(self):
134
self._fake_client.expecting_body = False
136
def read_streamed_body(self):
140
class FakeClient(_SmartClient):
141
"""Lookalike for _SmartClient allowing testing."""
143
def __init__(self, fake_medium_base='fake base'):
144
"""Create a FakeClient."""
147
self.expecting_body = False
148
# if non-None, this is the list of expected calls, with only the
149
# method name and arguments included. the body might be hard to
150
# compute so is not included. If a call is None, that call can
152
self._expected_calls = None
153
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
155
def add_expected_call(self, call_name, call_args, response_type,
156
response_args, response_body=None):
157
if self._expected_calls is None:
158
self._expected_calls = []
159
self._expected_calls.append((call_name, call_args))
160
self.responses.append((response_type, response_args, response_body))
162
def add_success_response(self, *args):
163
self.responses.append(('success', args, None))
165
def add_success_response_with_body(self, body, *args):
166
self.responses.append(('success', args, body))
167
if self._expected_calls is not None:
168
self._expected_calls.append(None)
170
def add_error_response(self, *args):
171
self.responses.append(('error', args))
173
def add_unknown_method_response(self, verb):
174
self.responses.append(('unknown', verb))
176
def finished_test(self):
177
if self._expected_calls:
178
raise AssertionError("%r finished but was still expecting %r"
179
% (self, self._expected_calls[0]))
181
def _get_next_response(self):
183
response_tuple = self.responses.pop(0)
184
except IndexError, e:
185
raise AssertionError("%r didn't expect any more calls"
187
if response_tuple[0] == 'unknown':
188
raise errors.UnknownSmartMethod(response_tuple[1])
189
elif response_tuple[0] == 'error':
190
raise errors.ErrorFromSmartServer(response_tuple[1])
191
return response_tuple
193
def _check_call(self, method, args):
194
if self._expected_calls is None:
195
# the test should be updated to say what it expects
198
next_call = self._expected_calls.pop(0)
200
raise AssertionError("%r didn't expect any more calls "
202
% (self, method, args,))
203
if next_call is None:
205
if method != next_call[0] or args != next_call[1]:
206
raise AssertionError("%r expected %r%r "
208
% (self, next_call[0], next_call[1], method, args,))
210
def call(self, method, *args):
211
self._check_call(method, args)
212
self._calls.append(('call', method, args))
213
return self._get_next_response()[1]
215
def call_expecting_body(self, method, *args):
216
self._check_call(method, args)
217
self._calls.append(('call_expecting_body', method, args))
218
result = self._get_next_response()
219
self.expecting_body = True
220
return result[1], FakeProtocol(result[2], self)
222
def call_with_body_bytes_expecting_body(self, method, args, body):
223
self._check_call(method, args)
224
self._calls.append(('call_with_body_bytes_expecting_body', method,
226
result = self._get_next_response()
227
self.expecting_body = True
228
return result[1], FakeProtocol(result[2], self)
230
def call_with_body_stream(self, args, stream):
231
# Explicitly consume the stream before checking for an error, because
232
# that's what happens a real medium.
233
stream = list(stream)
234
self._check_call(args[0], args[1:])
235
self._calls.append(('call_with_body_stream', args[0], args[1:], stream))
236
return self._get_next_response()[1]
239
class FakeMedium(medium.SmartClientMedium):
241
def __init__(self, client_calls, base):
242
medium.SmartClientMedium.__init__(self, base)
243
self._client_calls = client_calls
245
def disconnect(self):
246
self._client_calls.append(('disconnect medium',))
249
class TestVfsHas(tests.TestCase):
251
def test_unicode_path(self):
252
client = FakeClient('/')
253
client.add_success_response('yes',)
254
transport = RemoteTransport('bzr://localhost/', _client=client)
255
filename = u'/hell\u00d8'.encode('utf8')
256
result = transport.has(filename)
258
[('call', 'has', (filename,))],
260
self.assertTrue(result)
263
class TestRemote(tests.TestCaseWithMemoryTransport):
265
def disable_verb(self, verb):
266
"""Disable a verb for one test."""
267
request_handlers = smart.request.request_handlers
268
orig_method = request_handlers.get(verb)
269
request_handlers.remove(verb)
271
request_handlers.register(verb, orig_method)
272
self.addCleanup(restoreVerb)
275
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
276
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
278
def assertRemotePath(self, expected, client_base, transport_base):
279
"""Assert that the result of
280
SmartClientMedium.remote_path_from_transport is the expected value for
281
a given client_base and transport_base.
283
client_medium = medium.SmartClientMedium(client_base)
284
transport = get_transport(transport_base)
285
result = client_medium.remote_path_from_transport(transport)
286
self.assertEqual(expected, result)
288
def test_remote_path_from_transport(self):
289
"""SmartClientMedium.remote_path_from_transport calculates a URL for
290
the given transport relative to the root of the client base URL.
292
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
293
self.assertRemotePath(
294
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
296
def assertRemotePathHTTP(self, expected, transport_base, relpath):
297
"""Assert that the result of
298
HttpTransportBase.remote_path_from_transport is the expected value for
299
a given transport_base and relpath of that transport. (Note that
300
HttpTransportBase is a subclass of SmartClientMedium)
302
base_transport = get_transport(transport_base)
303
client_medium = base_transport.get_smart_medium()
304
cloned_transport = base_transport.clone(relpath)
305
result = client_medium.remote_path_from_transport(cloned_transport)
306
self.assertEqual(expected, result)
308
def test_remote_path_from_transport_http(self):
309
"""Remote paths for HTTP transports are calculated differently to other
310
transports. They are just relative to the client base, not the root
311
directory of the host.
313
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
314
self.assertRemotePathHTTP(
315
'../xyz/', scheme + '//host/path', '../xyz/')
316
self.assertRemotePathHTTP(
317
'xyz/', scheme + '//host/path', 'xyz/')
320
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
321
"""Tests for the behaviour of client_medium.remote_is_at_least."""
323
def test_initially_unlimited(self):
324
"""A fresh medium assumes that the remote side supports all
327
client_medium = medium.SmartClientMedium('dummy base')
328
self.assertFalse(client_medium._is_remote_before((99, 99)))
330
def test__remember_remote_is_before(self):
331
"""Calling _remember_remote_is_before ratchets down the known remote
334
client_medium = medium.SmartClientMedium('dummy base')
335
# Mark the remote side as being less than 1.6. The remote side may
337
client_medium._remember_remote_is_before((1, 6))
338
self.assertTrue(client_medium._is_remote_before((1, 6)))
339
self.assertFalse(client_medium._is_remote_before((1, 5)))
340
# Calling _remember_remote_is_before again with a lower value works.
341
client_medium._remember_remote_is_before((1, 5))
342
self.assertTrue(client_medium._is_remote_before((1, 5)))
343
# You cannot call _remember_remote_is_before with a larger value.
345
AssertionError, client_medium._remember_remote_is_before, (1, 9))
348
class TestBzrDirOpenBranch(tests.TestCase):
350
def test_branch_present(self):
351
transport = MemoryTransport()
352
transport.mkdir('quack')
353
transport = transport.clone('quack')
354
client = FakeClient(transport.base)
355
client.add_expected_call(
356
'BzrDir.open_branch', ('quack/',),
357
'success', ('ok', ''))
358
client.add_expected_call(
359
'BzrDir.find_repositoryV2', ('quack/',),
360
'success', ('ok', '', 'no', 'no', 'no'))
361
client.add_expected_call(
362
'Branch.get_stacked_on_url', ('quack/',),
363
'error', ('NotStacked',))
364
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
366
result = bzrdir.open_branch()
367
self.assertIsInstance(result, RemoteBranch)
368
self.assertEqual(bzrdir, result.bzrdir)
369
client.finished_test()
371
def test_branch_missing(self):
372
transport = MemoryTransport()
373
transport.mkdir('quack')
374
transport = transport.clone('quack')
375
client = FakeClient(transport.base)
376
client.add_error_response('nobranch')
377
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
379
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
381
[('call', 'BzrDir.open_branch', ('quack/',))],
384
def test__get_tree_branch(self):
385
# _get_tree_branch is a form of open_branch, but it should only ask for
386
# branch opening, not any other network requests.
389
calls.append("Called")
391
transport = MemoryTransport()
392
# no requests on the network - catches other api calls being made.
393
client = FakeClient(transport.base)
394
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
396
# patch the open_branch call to record that it was called.
397
bzrdir.open_branch = open_branch
398
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
399
self.assertEqual(["Called"], calls)
400
self.assertEqual([], client._calls)
402
def test_url_quoting_of_path(self):
403
# Relpaths on the wire should not be URL-escaped. So "~" should be
404
# transmitted as "~", not "%7E".
405
transport = RemoteTCPTransport('bzr://localhost/~hello/')
406
client = FakeClient(transport.base)
407
client.add_expected_call(
408
'BzrDir.open_branch', ('~hello/',),
409
'success', ('ok', ''))
410
client.add_expected_call(
411
'BzrDir.find_repositoryV2', ('~hello/',),
412
'success', ('ok', '', 'no', 'no', 'no'))
413
client.add_expected_call(
414
'Branch.get_stacked_on_url', ('~hello/',),
415
'error', ('NotStacked',))
416
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
418
result = bzrdir.open_branch()
419
client.finished_test()
421
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
422
transport = MemoryTransport()
423
transport.mkdir('quack')
424
transport = transport.clone('quack')
426
rich_response = 'yes'
430
subtree_response = 'yes'
432
subtree_response = 'no'
433
client = FakeClient(transport.base)
434
client.add_success_response(
435
'ok', '', rich_response, subtree_response, external_lookup)
436
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
438
result = bzrdir.open_repository()
440
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
442
self.assertIsInstance(result, RemoteRepository)
443
self.assertEqual(bzrdir, result.bzrdir)
444
self.assertEqual(rich_root, result._format.rich_root_data)
445
self.assertEqual(subtrees, result._format.supports_tree_reference)
447
def test_open_repository_sets_format_attributes(self):
448
self.check_open_repository(True, True)
449
self.check_open_repository(False, True)
450
self.check_open_repository(True, False)
451
self.check_open_repository(False, False)
452
self.check_open_repository(False, False, 'yes')
454
def test_old_server(self):
455
"""RemoteBzrDirFormat should fail to probe if the server version is too
458
self.assertRaises(errors.NotBranchError,
459
RemoteBzrDirFormat.probe_transport, OldServerTransport())
462
class TestBzrDirCreateRepository(TestRemote):
464
def test_backwards_compat(self):
465
self.setup_smart_server_with_call_log()
466
bzrdir = self.make_bzrdir('.')
467
self.reset_smart_call_log()
468
self.disable_verb('BzrDir.create_repository')
469
repo = bzrdir.create_repository()
470
create_repo_call_count = len([call for call in self.hpss_calls if
471
call[0].method == 'BzrDir.create_repository'])
472
self.assertEqual(1, create_repo_call_count)
474
def test_current_server(self):
475
transport = self.get_transport('.')
476
transport = transport.clone('quack')
477
self.make_bzrdir('quack')
478
client = FakeClient(transport.base)
479
reference_bzrdir_format = bzrdir.format_registry.get('default')()
480
reference_format = reference_bzrdir_format.repository_format
481
network_name = reference_format.network_name()
482
client.add_expected_call(
483
'BzrDir.create_repository', ('quack/',
484
'Bazaar pack repository format 1 (needs bzr 0.92)\n', 'False'),
485
'success', ('ok', 'no', 'no', 'no', network_name))
486
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
488
repo = a_bzrdir.create_repository()
489
# We should have got a remote repository
490
self.assertIsInstance(repo, remote.RemoteRepository)
491
# its format should have the settings from the response
492
format = repo._format
493
self.assertFalse(format.rich_root_data)
494
self.assertFalse(format.supports_tree_reference)
495
self.assertFalse(format.supports_external_lookups)
496
self.assertEqual(network_name, format.network_name())
499
class TestBzrDirOpenRepository(tests.TestCase):
501
def test_backwards_compat_1_2(self):
502
transport = MemoryTransport()
503
transport.mkdir('quack')
504
transport = transport.clone('quack')
505
client = FakeClient(transport.base)
506
client.add_unknown_method_response('BzrDir.find_repositoryV2')
507
client.add_success_response('ok', '', 'no', 'no')
508
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
510
repo = bzrdir.open_repository()
512
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
513
('call', 'BzrDir.find_repository', ('quack/',))],
517
class OldSmartClient(object):
518
"""A fake smart client for test_old_version that just returns a version one
519
response to the 'hello' (query version) command.
522
def get_request(self):
523
input_file = StringIO('ok\x011\n')
524
output_file = StringIO()
525
client_medium = medium.SmartSimplePipesClientMedium(
526
input_file, output_file)
527
return medium.SmartClientStreamMediumRequest(client_medium)
529
def protocol_version(self):
533
class OldServerTransport(object):
534
"""A fake transport for test_old_server that reports it's smart server
535
protocol version as version one.
541
def get_smart_client(self):
542
return OldSmartClient()
545
class RemoteBranchTestCase(tests.TestCase):
547
def make_remote_branch(self, transport, client):
548
"""Make a RemoteBranch using 'client' as its _SmartClient.
550
A RemoteBzrDir and RemoteRepository will also be created to fill out
551
the RemoteBranch, albeit with stub values for some of their attributes.
553
# we do not want bzrdir to make any remote calls, so use False as its
554
# _client. If it tries to make a remote call, this will fail
556
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
558
repo = RemoteRepository(bzrdir, None, _client=client)
559
return RemoteBranch(bzrdir, repo, _client=client)
562
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
564
def test_empty_branch(self):
565
# in an empty branch we decode the response properly
566
transport = MemoryTransport()
567
client = FakeClient(transport.base)
568
client.add_expected_call(
569
'Branch.get_stacked_on_url', ('quack/',),
570
'error', ('NotStacked',))
571
client.add_expected_call(
572
'Branch.last_revision_info', ('quack/',),
573
'success', ('ok', '0', 'null:'))
574
transport.mkdir('quack')
575
transport = transport.clone('quack')
576
branch = self.make_remote_branch(transport, client)
577
result = branch.last_revision_info()
578
client.finished_test()
579
self.assertEqual((0, NULL_REVISION), result)
581
def test_non_empty_branch(self):
582
# in a non-empty branch we also decode the response properly
583
revid = u'\xc8'.encode('utf8')
584
transport = MemoryTransport()
585
client = FakeClient(transport.base)
586
client.add_expected_call(
587
'Branch.get_stacked_on_url', ('kwaak/',),
588
'error', ('NotStacked',))
589
client.add_expected_call(
590
'Branch.last_revision_info', ('kwaak/',),
591
'success', ('ok', '2', revid))
592
transport.mkdir('kwaak')
593
transport = transport.clone('kwaak')
594
branch = self.make_remote_branch(transport, client)
595
result = branch.last_revision_info()
596
self.assertEqual((2, revid), result)
599
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
600
"""Test Branch._get_stacked_on_url rpc"""
602
def test_get_stacked_on_invalid_url(self):
603
# test that asking for a stacked on url the server can't access works.
604
# This isn't perfect, but then as we're in the same process there
605
# really isn't anything we can do to be 100% sure that the server
606
# doesn't just open in - this test probably needs to be rewritten using
607
# a spawn()ed server.
608
stacked_branch = self.make_branch('stacked', format='1.9')
609
memory_branch = self.make_branch('base', format='1.9')
610
vfs_url = self.get_vfs_only_url('base')
611
stacked_branch.set_stacked_on_url(vfs_url)
612
transport = stacked_branch.bzrdir.root_transport
613
client = FakeClient(transport.base)
614
client.add_expected_call(
615
'Branch.get_stacked_on_url', ('stacked/',),
616
'success', ('ok', vfs_url))
617
# XXX: Multiple calls are bad, this second call documents what is
619
client.add_expected_call(
620
'Branch.get_stacked_on_url', ('stacked/',),
621
'success', ('ok', vfs_url))
622
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
624
branch = RemoteBranch(bzrdir, RemoteRepository(bzrdir, None),
626
result = branch.get_stacked_on_url()
627
self.assertEqual(vfs_url, result)
629
def test_backwards_compatible(self):
630
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
631
base_branch = self.make_branch('base', format='1.6')
632
stacked_branch = self.make_branch('stacked', format='1.6')
633
stacked_branch.set_stacked_on_url('../base')
634
client = FakeClient(self.get_url())
635
client.add_expected_call(
636
'BzrDir.open_branch', ('stacked/',),
637
'success', ('ok', ''))
638
client.add_expected_call(
639
'BzrDir.find_repositoryV2', ('stacked/',),
640
'success', ('ok', '', 'no', 'no', 'no'))
641
# called twice, once from constructor and then again by us
642
client.add_expected_call(
643
'Branch.get_stacked_on_url', ('stacked/',),
644
'unknown', ('Branch.get_stacked_on_url',))
645
client.add_expected_call(
646
'Branch.get_stacked_on_url', ('stacked/',),
647
'unknown', ('Branch.get_stacked_on_url',))
648
# this will also do vfs access, but that goes direct to the transport
649
# and isn't seen by the FakeClient.
650
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
651
remote.RemoteBzrDirFormat(), _client=client)
652
branch = bzrdir.open_branch()
653
result = branch.get_stacked_on_url()
654
self.assertEqual('../base', result)
655
client.finished_test()
656
# it's in the fallback list both for the RemoteRepository and its vfs
658
self.assertEqual(1, len(branch.repository._fallback_repositories))
660
len(branch.repository._real_repository._fallback_repositories))
662
def test_get_stacked_on_real_branch(self):
663
base_branch = self.make_branch('base', format='1.6')
664
stacked_branch = self.make_branch('stacked', format='1.6')
665
stacked_branch.set_stacked_on_url('../base')
666
client = FakeClient(self.get_url())
667
client.add_expected_call(
668
'BzrDir.open_branch', ('stacked/',),
669
'success', ('ok', ''))
670
client.add_expected_call(
671
'BzrDir.find_repositoryV2', ('stacked/',),
672
'success', ('ok', '', 'no', 'no', 'no'))
673
# called twice, once from constructor and then again by us
674
client.add_expected_call(
675
'Branch.get_stacked_on_url', ('stacked/',),
676
'success', ('ok', '../base'))
677
client.add_expected_call(
678
'Branch.get_stacked_on_url', ('stacked/',),
679
'success', ('ok', '../base'))
680
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
681
remote.RemoteBzrDirFormat(), _client=client)
682
branch = bzrdir.open_branch()
683
result = branch.get_stacked_on_url()
684
self.assertEqual('../base', result)
685
client.finished_test()
686
# it's in the fallback list both for the RemoteRepository and its vfs
688
self.assertEqual(1, len(branch.repository._fallback_repositories))
690
len(branch.repository._real_repository._fallback_repositories))
693
class TestBranchSetLastRevision(RemoteBranchTestCase):
695
def test_set_empty(self):
696
# set_revision_history([]) is translated to calling
697
# Branch.set_last_revision(path, '') on the wire.
698
transport = MemoryTransport()
699
transport.mkdir('branch')
700
transport = transport.clone('branch')
702
client = FakeClient(transport.base)
703
client.add_expected_call(
704
'Branch.get_stacked_on_url', ('branch/',),
705
'error', ('NotStacked',))
706
client.add_expected_call(
707
'Branch.lock_write', ('branch/', '', ''),
708
'success', ('ok', 'branch token', 'repo token'))
709
client.add_expected_call(
710
'Branch.last_revision_info',
712
'success', ('ok', '0', 'null:'))
713
client.add_expected_call(
714
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
716
client.add_expected_call(
717
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
719
branch = self.make_remote_branch(transport, client)
720
# This is a hack to work around the problem that RemoteBranch currently
721
# unnecessarily invokes _ensure_real upon a call to lock_write.
722
branch._ensure_real = lambda: None
724
result = branch.set_revision_history([])
726
self.assertEqual(None, result)
727
client.finished_test()
729
def test_set_nonempty(self):
730
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
731
# Branch.set_last_revision(path, rev-idN) on the wire.
732
transport = MemoryTransport()
733
transport.mkdir('branch')
734
transport = transport.clone('branch')
736
client = FakeClient(transport.base)
737
client.add_expected_call(
738
'Branch.get_stacked_on_url', ('branch/',),
739
'error', ('NotStacked',))
740
client.add_expected_call(
741
'Branch.lock_write', ('branch/', '', ''),
742
'success', ('ok', 'branch token', 'repo token'))
743
client.add_expected_call(
744
'Branch.last_revision_info',
746
'success', ('ok', '0', 'null:'))
748
encoded_body = bz2.compress('\n'.join(lines))
749
client.add_success_response_with_body(encoded_body, 'ok')
750
client.add_expected_call(
751
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
753
client.add_expected_call(
754
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
756
branch = self.make_remote_branch(transport, client)
757
# This is a hack to work around the problem that RemoteBranch currently
758
# unnecessarily invokes _ensure_real upon a call to lock_write.
759
branch._ensure_real = lambda: None
760
# Lock the branch, reset the record of remote calls.
762
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
764
self.assertEqual(None, result)
765
client.finished_test()
767
def test_no_such_revision(self):
768
transport = MemoryTransport()
769
transport.mkdir('branch')
770
transport = transport.clone('branch')
771
# A response of 'NoSuchRevision' is translated into an exception.
772
client = FakeClient(transport.base)
773
client.add_expected_call(
774
'Branch.get_stacked_on_url', ('branch/',),
775
'error', ('NotStacked',))
776
client.add_expected_call(
777
'Branch.lock_write', ('branch/', '', ''),
778
'success', ('ok', 'branch token', 'repo token'))
779
client.add_expected_call(
780
'Branch.last_revision_info',
782
'success', ('ok', '0', 'null:'))
783
# get_graph calls to construct the revision history, for the set_rh
786
encoded_body = bz2.compress('\n'.join(lines))
787
client.add_success_response_with_body(encoded_body, 'ok')
788
client.add_expected_call(
789
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
790
'error', ('NoSuchRevision', 'rev-id'))
791
client.add_expected_call(
792
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
795
branch = self.make_remote_branch(transport, client)
798
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
800
client.finished_test()
802
def test_tip_change_rejected(self):
803
"""TipChangeRejected responses cause a TipChangeRejected exception to
806
transport = MemoryTransport()
807
transport.mkdir('branch')
808
transport = transport.clone('branch')
809
client = FakeClient(transport.base)
810
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
811
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
812
client.add_expected_call(
813
'Branch.get_stacked_on_url', ('branch/',),
814
'error', ('NotStacked',))
815
client.add_expected_call(
816
'Branch.lock_write', ('branch/', '', ''),
817
'success', ('ok', 'branch token', 'repo token'))
818
client.add_expected_call(
819
'Branch.last_revision_info',
821
'success', ('ok', '0', 'null:'))
823
encoded_body = bz2.compress('\n'.join(lines))
824
client.add_success_response_with_body(encoded_body, 'ok')
825
client.add_expected_call(
826
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
827
'error', ('TipChangeRejected', rejection_msg_utf8))
828
client.add_expected_call(
829
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
831
branch = self.make_remote_branch(transport, client)
832
branch._ensure_real = lambda: None
834
self.addCleanup(branch.unlock)
835
# The 'TipChangeRejected' error response triggered by calling
836
# set_revision_history causes a TipChangeRejected exception.
837
err = self.assertRaises(
838
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
839
# The UTF-8 message from the response has been decoded into a unicode
841
self.assertIsInstance(err.msg, unicode)
842
self.assertEqual(rejection_msg_unicode, err.msg)
844
client.finished_test()
847
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
849
def test_set_last_revision_info(self):
850
# set_last_revision_info(num, 'rev-id') is translated to calling
851
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
852
transport = MemoryTransport()
853
transport.mkdir('branch')
854
transport = transport.clone('branch')
855
client = FakeClient(transport.base)
857
client.add_error_response('NotStacked')
859
client.add_success_response('ok', 'branch token', 'repo token')
860
# query the current revision
861
client.add_success_response('ok', '0', 'null:')
863
client.add_success_response('ok')
865
client.add_success_response('ok')
867
branch = self.make_remote_branch(transport, client)
868
# Lock the branch, reset the record of remote calls.
871
result = branch.set_last_revision_info(1234, 'a-revision-id')
873
[('call', 'Branch.last_revision_info', ('branch/',)),
874
('call', 'Branch.set_last_revision_info',
875
('branch/', 'branch token', 'repo token',
876
'1234', 'a-revision-id'))],
878
self.assertEqual(None, result)
880
def test_no_such_revision(self):
881
# A response of 'NoSuchRevision' is translated into an exception.
882
transport = MemoryTransport()
883
transport.mkdir('branch')
884
transport = transport.clone('branch')
885
client = FakeClient(transport.base)
887
client.add_error_response('NotStacked')
889
client.add_success_response('ok', 'branch token', 'repo token')
891
client.add_error_response('NoSuchRevision', 'revid')
893
client.add_success_response('ok')
895
branch = self.make_remote_branch(transport, client)
896
# Lock the branch, reset the record of remote calls.
901
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
904
def lock_remote_branch(self, branch):
905
"""Trick a RemoteBranch into thinking it is locked."""
906
branch._lock_mode = 'w'
907
branch._lock_count = 2
908
branch._lock_token = 'branch token'
909
branch._repo_lock_token = 'repo token'
910
branch.repository._lock_mode = 'w'
911
branch.repository._lock_count = 2
912
branch.repository._lock_token = 'repo token'
914
def test_backwards_compatibility(self):
915
"""If the server does not support the Branch.set_last_revision_info
916
verb (which is new in 1.4), then the client falls back to VFS methods.
918
# This test is a little messy. Unlike most tests in this file, it
919
# doesn't purely test what a Remote* object sends over the wire, and
920
# how it reacts to responses from the wire. It instead relies partly
921
# on asserting that the RemoteBranch will call
922
# self._real_branch.set_last_revision_info(...).
924
# First, set up our RemoteBranch with a FakeClient that raises
925
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
926
transport = MemoryTransport()
927
transport.mkdir('branch')
928
transport = transport.clone('branch')
929
client = FakeClient(transport.base)
930
client.add_expected_call(
931
'Branch.get_stacked_on_url', ('branch/',),
932
'error', ('NotStacked',))
933
client.add_expected_call(
934
'Branch.last_revision_info',
936
'success', ('ok', '0', 'null:'))
937
client.add_expected_call(
938
'Branch.set_last_revision_info',
939
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
940
'unknown', 'Branch.set_last_revision_info')
942
branch = self.make_remote_branch(transport, client)
943
class StubRealBranch(object):
946
def set_last_revision_info(self, revno, revision_id):
948
('set_last_revision_info', revno, revision_id))
949
def _clear_cached_state(self):
951
real_branch = StubRealBranch()
952
branch._real_branch = real_branch
953
self.lock_remote_branch(branch)
955
# Call set_last_revision_info, and verify it behaved as expected.
956
result = branch.set_last_revision_info(1234, 'a-revision-id')
958
[('set_last_revision_info', 1234, 'a-revision-id')],
960
client.finished_test()
962
def test_unexpected_error(self):
963
# If the server sends an error the client doesn't understand, it gets
964
# turned into an UnknownErrorFromSmartServer, which is presented as a
965
# non-internal error to the user.
966
transport = MemoryTransport()
967
transport.mkdir('branch')
968
transport = transport.clone('branch')
969
client = FakeClient(transport.base)
971
client.add_error_response('NotStacked')
973
client.add_success_response('ok', 'branch token', 'repo token')
975
client.add_error_response('UnexpectedError')
977
client.add_success_response('ok')
979
branch = self.make_remote_branch(transport, client)
980
# Lock the branch, reset the record of remote calls.
984
err = self.assertRaises(
985
errors.UnknownErrorFromSmartServer,
986
branch.set_last_revision_info, 123, 'revid')
987
self.assertEqual(('UnexpectedError',), err.error_tuple)
990
def test_tip_change_rejected(self):
991
"""TipChangeRejected responses cause a TipChangeRejected exception to
994
transport = MemoryTransport()
995
transport.mkdir('branch')
996
transport = transport.clone('branch')
997
client = FakeClient(transport.base)
999
client.add_error_response('NotStacked')
1001
client.add_success_response('ok', 'branch token', 'repo token')
1003
client.add_error_response('TipChangeRejected', 'rejection message')
1005
client.add_success_response('ok')
1007
branch = self.make_remote_branch(transport, client)
1008
# Lock the branch, reset the record of remote calls.
1010
self.addCleanup(branch.unlock)
1013
# The 'TipChangeRejected' error response triggered by calling
1014
# set_last_revision_info causes a TipChangeRejected exception.
1015
err = self.assertRaises(
1016
errors.TipChangeRejected,
1017
branch.set_last_revision_info, 123, 'revid')
1018
self.assertEqual('rejection message', err.msg)
1021
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
1022
"""Getting the branch configuration should use an abstract method not vfs.
1025
def test_get_branch_conf(self):
1026
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
1027
## # We should see that branch.get_config() does a single rpc to get the
1028
## # remote configuration file, abstracting away where that is stored on
1029
## # the server. However at the moment it always falls back to using the
1030
## # vfs, and this would need some changes in config.py.
1032
## # in an empty branch we decode the response properly
1033
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
1034
## # we need to make a real branch because the remote_branch.control_files
1035
## # will trigger _ensure_real.
1036
## branch = self.make_branch('quack')
1037
## transport = branch.bzrdir.root_transport
1038
## # we do not want bzrdir to make any remote calls
1039
## bzrdir = RemoteBzrDir(transport, _client=False)
1040
## branch = RemoteBranch(bzrdir, None, _client=client)
1041
## config = branch.get_config()
1042
## self.assertEqual(
1043
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
1047
class TestBranchLockWrite(RemoteBranchTestCase):
1049
def test_lock_write_unlockable(self):
1050
transport = MemoryTransport()
1051
client = FakeClient(transport.base)
1052
client.add_expected_call(
1053
'Branch.get_stacked_on_url', ('quack/',),
1054
'error', ('NotStacked',),)
1055
client.add_expected_call(
1056
'Branch.lock_write', ('quack/', '', ''),
1057
'error', ('UnlockableTransport',))
1058
transport.mkdir('quack')
1059
transport = transport.clone('quack')
1060
branch = self.make_remote_branch(transport, client)
1061
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
1062
client.finished_test()
1065
class TestTransportIsReadonly(tests.TestCase):
1067
def test_true(self):
1068
client = FakeClient()
1069
client.add_success_response('yes')
1070
transport = RemoteTransport('bzr://example.com/', medium=False,
1072
self.assertEqual(True, transport.is_readonly())
1074
[('call', 'Transport.is_readonly', ())],
1077
def test_false(self):
1078
client = FakeClient()
1079
client.add_success_response('no')
1080
transport = RemoteTransport('bzr://example.com/', medium=False,
1082
self.assertEqual(False, transport.is_readonly())
1084
[('call', 'Transport.is_readonly', ())],
1087
def test_error_from_old_server(self):
1088
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1090
Clients should treat it as a "no" response, because is_readonly is only
1091
advisory anyway (a transport could be read-write, but then the
1092
underlying filesystem could be readonly anyway).
1094
client = FakeClient()
1095
client.add_unknown_method_response('Transport.is_readonly')
1096
transport = RemoteTransport('bzr://example.com/', medium=False,
1098
self.assertEqual(False, transport.is_readonly())
1100
[('call', 'Transport.is_readonly', ())],
1104
class TestTransportMkdir(tests.TestCase):
1106
def test_permissiondenied(self):
1107
client = FakeClient()
1108
client.add_error_response('PermissionDenied', 'remote path', 'extra')
1109
transport = RemoteTransport('bzr://example.com/', medium=False,
1111
exc = self.assertRaises(
1112
errors.PermissionDenied, transport.mkdir, 'client path')
1113
expected_error = errors.PermissionDenied('/client path', 'extra')
1114
self.assertEqual(expected_error, exc)
1117
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1119
def test_defaults_to_none(self):
1120
t = RemoteSSHTransport('bzr+ssh://example.com')
1121
self.assertIs(None, t._get_credentials()[0])
1123
def test_uses_authentication_config(self):
1124
conf = config.AuthenticationConfig()
1125
conf._get_config().update(
1126
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1129
t = RemoteSSHTransport('bzr+ssh://example.com')
1130
self.assertEqual('bar', t._get_credentials()[0])
1133
class TestRemoteRepository(TestRemote):
1134
"""Base for testing RemoteRepository protocol usage.
1136
These tests contain frozen requests and responses. We want any changes to
1137
what is sent or expected to be require a thoughtful update to these tests
1138
because they might break compatibility with different-versioned servers.
1141
def setup_fake_client_and_repository(self, transport_path):
1142
"""Create the fake client and repository for testing with.
1144
There's no real server here; we just have canned responses sent
1147
:param transport_path: Path below the root of the MemoryTransport
1148
where the repository will be created.
1150
transport = MemoryTransport()
1151
transport.mkdir(transport_path)
1152
client = FakeClient(transport.base)
1153
transport = transport.clone(transport_path)
1154
# we do not want bzrdir to make any remote calls
1155
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
1157
repo = RemoteRepository(bzrdir, None, _client=client)
1161
class TestRepositoryGatherStats(TestRemoteRepository):
1163
def test_revid_none(self):
1164
# ('ok',), body with revisions and size
1165
transport_path = 'quack'
1166
repo, client = self.setup_fake_client_and_repository(transport_path)
1167
client.add_success_response_with_body(
1168
'revisions: 2\nsize: 18\n', 'ok')
1169
result = repo.gather_stats(None)
1171
[('call_expecting_body', 'Repository.gather_stats',
1172
('quack/','','no'))],
1174
self.assertEqual({'revisions': 2, 'size': 18}, result)
1176
def test_revid_no_committers(self):
1177
# ('ok',), body without committers
1178
body = ('firstrev: 123456.300 3600\n'
1179
'latestrev: 654231.400 0\n'
1182
transport_path = 'quick'
1183
revid = u'\xc8'.encode('utf8')
1184
repo, client = self.setup_fake_client_and_repository(transport_path)
1185
client.add_success_response_with_body(body, 'ok')
1186
result = repo.gather_stats(revid)
1188
[('call_expecting_body', 'Repository.gather_stats',
1189
('quick/', revid, 'no'))],
1191
self.assertEqual({'revisions': 2, 'size': 18,
1192
'firstrev': (123456.300, 3600),
1193
'latestrev': (654231.400, 0),},
1196
def test_revid_with_committers(self):
1197
# ('ok',), body with committers
1198
body = ('committers: 128\n'
1199
'firstrev: 123456.300 3600\n'
1200
'latestrev: 654231.400 0\n'
1203
transport_path = 'buick'
1204
revid = u'\xc8'.encode('utf8')
1205
repo, client = self.setup_fake_client_and_repository(transport_path)
1206
client.add_success_response_with_body(body, 'ok')
1207
result = repo.gather_stats(revid, True)
1209
[('call_expecting_body', 'Repository.gather_stats',
1210
('buick/', revid, 'yes'))],
1212
self.assertEqual({'revisions': 2, 'size': 18,
1214
'firstrev': (123456.300, 3600),
1215
'latestrev': (654231.400, 0),},
1219
class TestRepositoryGetGraph(TestRemoteRepository):
1221
def test_get_graph(self):
1222
# get_graph returns a graph with a custom parents provider.
1223
transport_path = 'quack'
1224
repo, client = self.setup_fake_client_and_repository(transport_path)
1225
graph = repo.get_graph()
1226
self.assertNotEqual(graph._parents_provider, repo)
1229
class TestRepositoryGetParentMap(TestRemoteRepository):
1231
def test_get_parent_map_caching(self):
1232
# get_parent_map returns from cache until unlock()
1233
# setup a reponse with two revisions
1234
r1 = u'\u0e33'.encode('utf8')
1235
r2 = u'\u0dab'.encode('utf8')
1236
lines = [' '.join([r2, r1]), r1]
1237
encoded_body = bz2.compress('\n'.join(lines))
1239
transport_path = 'quack'
1240
repo, client = self.setup_fake_client_and_repository(transport_path)
1241
client.add_success_response_with_body(encoded_body, 'ok')
1242
client.add_success_response_with_body(encoded_body, 'ok')
1244
graph = repo.get_graph()
1245
parents = graph.get_parent_map([r2])
1246
self.assertEqual({r2: (r1,)}, parents)
1247
# locking and unlocking deeper should not reset
1250
parents = graph.get_parent_map([r1])
1251
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1253
[('call_with_body_bytes_expecting_body',
1254
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
1257
# now we call again, and it should use the second response.
1259
graph = repo.get_graph()
1260
parents = graph.get_parent_map([r1])
1261
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1263
[('call_with_body_bytes_expecting_body',
1264
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
1265
('call_with_body_bytes_expecting_body',
1266
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
1271
def test_get_parent_map_reconnects_if_unknown_method(self):
1272
transport_path = 'quack'
1273
repo, client = self.setup_fake_client_and_repository(transport_path)
1274
client.add_unknown_method_response('Repository,get_parent_map')
1275
client.add_success_response_with_body('', 'ok')
1276
self.assertFalse(client._medium._is_remote_before((1, 2)))
1277
rev_id = 'revision-id'
1278
expected_deprecations = [
1279
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1281
parents = self.callDeprecated(
1282
expected_deprecations, repo.get_parent_map, [rev_id])
1284
[('call_with_body_bytes_expecting_body',
1285
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
1286
('disconnect medium',),
1287
('call_expecting_body', 'Repository.get_revision_graph',
1290
# The medium is now marked as being connected to an older server
1291
self.assertTrue(client._medium._is_remote_before((1, 2)))
1293
def test_get_parent_map_fallback_parentless_node(self):
1294
"""get_parent_map falls back to get_revision_graph on old servers. The
1295
results from get_revision_graph are tweaked to match the get_parent_map
1298
Specifically, a {key: ()} result from get_revision_graph means "no
1299
parents" for that key, which in get_parent_map results should be
1300
represented as {key: ('null:',)}.
1302
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1304
rev_id = 'revision-id'
1305
transport_path = 'quack'
1306
repo, client = self.setup_fake_client_and_repository(transport_path)
1307
client.add_success_response_with_body(rev_id, 'ok')
1308
client._medium._remember_remote_is_before((1, 2))
1309
expected_deprecations = [
1310
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
1312
parents = self.callDeprecated(
1313
expected_deprecations, repo.get_parent_map, [rev_id])
1315
[('call_expecting_body', 'Repository.get_revision_graph',
1318
self.assertEqual({rev_id: ('null:',)}, parents)
1320
def test_get_parent_map_unexpected_response(self):
1321
repo, client = self.setup_fake_client_and_repository('path')
1322
client.add_success_response('something unexpected!')
1324
errors.UnexpectedSmartServerResponse,
1325
repo.get_parent_map, ['a-revision-id'])
1328
class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
1330
def test_allows_new_revisions(self):
1331
"""get_parent_map's results can be updated by commit."""
1332
smart_server = server.SmartTCPServer_for_testing()
1333
smart_server.setUp()
1334
self.addCleanup(smart_server.tearDown)
1335
self.make_branch('branch')
1336
branch = Branch.open(smart_server.get_url() + '/branch')
1337
tree = branch.create_checkout('tree', lightweight=True)
1339
self.addCleanup(tree.unlock)
1340
graph = tree.branch.repository.get_graph()
1341
# This provides an opportunity for the missing rev-id to be cached.
1342
self.assertEqual({}, graph.get_parent_map(['rev1']))
1343
tree.commit('message', rev_id='rev1')
1344
graph = tree.branch.repository.get_graph()
1345
self.assertEqual({'rev1': ('null:',)}, graph.get_parent_map(['rev1']))
1348
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1350
def test_null_revision(self):
1351
# a null revision has the predictable result {}, we should have no wire
1352
# traffic when calling it with this argument
1353
transport_path = 'empty'
1354
repo, client = self.setup_fake_client_and_repository(transport_path)
1355
client.add_success_response('notused')
1356
result = self.applyDeprecated(one_four, repo.get_revision_graph,
1358
self.assertEqual([], client._calls)
1359
self.assertEqual({}, result)
1361
def test_none_revision(self):
1362
# with none we want the entire graph
1363
r1 = u'\u0e33'.encode('utf8')
1364
r2 = u'\u0dab'.encode('utf8')
1365
lines = [' '.join([r2, r1]), r1]
1366
encoded_body = '\n'.join(lines)
1368
transport_path = 'sinhala'
1369
repo, client = self.setup_fake_client_and_repository(transport_path)
1370
client.add_success_response_with_body(encoded_body, 'ok')
1371
result = self.applyDeprecated(one_four, repo.get_revision_graph)
1373
[('call_expecting_body', 'Repository.get_revision_graph',
1376
self.assertEqual({r1: (), r2: (r1, )}, result)
1378
def test_specific_revision(self):
1379
# with a specific revision we want the graph for that
1380
# with none we want the entire graph
1381
r11 = u'\u0e33'.encode('utf8')
1382
r12 = u'\xc9'.encode('utf8')
1383
r2 = u'\u0dab'.encode('utf8')
1384
lines = [' '.join([r2, r11, r12]), r11, r12]
1385
encoded_body = '\n'.join(lines)
1387
transport_path = 'sinhala'
1388
repo, client = self.setup_fake_client_and_repository(transport_path)
1389
client.add_success_response_with_body(encoded_body, 'ok')
1390
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1392
[('call_expecting_body', 'Repository.get_revision_graph',
1395
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1397
def test_no_such_revision(self):
1399
transport_path = 'sinhala'
1400
repo, client = self.setup_fake_client_and_repository(transport_path)
1401
client.add_error_response('nosuchrevision', revid)
1402
# also check that the right revision is reported in the error
1403
self.assertRaises(errors.NoSuchRevision,
1404
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1406
[('call_expecting_body', 'Repository.get_revision_graph',
1407
('sinhala/', revid))],
1410
def test_unexpected_error(self):
1412
transport_path = 'sinhala'
1413
repo, client = self.setup_fake_client_and_repository(transport_path)
1414
client.add_error_response('AnUnexpectedError')
1415
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1416
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1417
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1420
class TestRepositoryIsShared(TestRemoteRepository):
1422
def test_is_shared(self):
1423
# ('yes', ) for Repository.is_shared -> 'True'.
1424
transport_path = 'quack'
1425
repo, client = self.setup_fake_client_and_repository(transport_path)
1426
client.add_success_response('yes')
1427
result = repo.is_shared()
1429
[('call', 'Repository.is_shared', ('quack/',))],
1431
self.assertEqual(True, result)
1433
def test_is_not_shared(self):
1434
# ('no', ) for Repository.is_shared -> 'False'.
1435
transport_path = 'qwack'
1436
repo, client = self.setup_fake_client_and_repository(transport_path)
1437
client.add_success_response('no')
1438
result = repo.is_shared()
1440
[('call', 'Repository.is_shared', ('qwack/',))],
1442
self.assertEqual(False, result)
1445
class TestRepositoryLockWrite(TestRemoteRepository):
1447
def test_lock_write(self):
1448
transport_path = 'quack'
1449
repo, client = self.setup_fake_client_and_repository(transport_path)
1450
client.add_success_response('ok', 'a token')
1451
result = repo.lock_write()
1453
[('call', 'Repository.lock_write', ('quack/', ''))],
1455
self.assertEqual('a token', result)
1457
def test_lock_write_already_locked(self):
1458
transport_path = 'quack'
1459
repo, client = self.setup_fake_client_and_repository(transport_path)
1460
client.add_error_response('LockContention')
1461
self.assertRaises(errors.LockContention, repo.lock_write)
1463
[('call', 'Repository.lock_write', ('quack/', ''))],
1466
def test_lock_write_unlockable(self):
1467
transport_path = 'quack'
1468
repo, client = self.setup_fake_client_and_repository(transport_path)
1469
client.add_error_response('UnlockableTransport')
1470
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1472
[('call', 'Repository.lock_write', ('quack/', ''))],
1476
class TestRepositorySetMakeWorkingTrees(TestRemoteRepository):
1478
def test_backwards_compat(self):
1479
self.setup_smart_server_with_call_log()
1480
repo = self.make_repository('.')
1481
self.reset_smart_call_log()
1482
verb = 'Repository.set_make_working_trees'
1483
self.disable_verb(verb)
1484
repo.set_make_working_trees(True)
1485
call_count = len([call for call in self.hpss_calls if
1486
call[0].method == verb])
1487
self.assertEqual(1, call_count)
1489
def test_current(self):
1490
transport_path = 'quack'
1491
repo, client = self.setup_fake_client_and_repository(transport_path)
1492
client.add_expected_call(
1493
'Repository.set_make_working_trees', ('quack/', 'True'),
1495
client.add_expected_call(
1496
'Repository.set_make_working_trees', ('quack/', 'False'),
1498
repo.set_make_working_trees(True)
1499
repo.set_make_working_trees(False)
1502
class TestRepositoryUnlock(TestRemoteRepository):
1504
def test_unlock(self):
1505
transport_path = 'quack'
1506
repo, client = self.setup_fake_client_and_repository(transport_path)
1507
client.add_success_response('ok', 'a token')
1508
client.add_success_response('ok')
1512
[('call', 'Repository.lock_write', ('quack/', '')),
1513
('call', 'Repository.unlock', ('quack/', 'a token'))],
1516
def test_unlock_wrong_token(self):
1517
# If somehow the token is wrong, unlock will raise TokenMismatch.
1518
transport_path = 'quack'
1519
repo, client = self.setup_fake_client_and_repository(transport_path)
1520
client.add_success_response('ok', 'a token')
1521
client.add_error_response('TokenMismatch')
1523
self.assertRaises(errors.TokenMismatch, repo.unlock)
1526
class TestRepositoryHasRevision(TestRemoteRepository):
1528
def test_none(self):
1529
# repo.has_revision(None) should not cause any traffic.
1530
transport_path = 'quack'
1531
repo, client = self.setup_fake_client_and_repository(transport_path)
1533
# The null revision is always there, so has_revision(None) == True.
1534
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1536
# The remote repo shouldn't be accessed.
1537
self.assertEqual([], client._calls)
1540
class TestRepositoryTarball(TestRemoteRepository):
1542
# This is a canned tarball reponse we can validate against
1544
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1545
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1546
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1547
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1548
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1549
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1550
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1551
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1552
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1553
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1554
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1555
'nWQ7QH/F3JFOFCQ0aSPfA='
1558
def test_repository_tarball(self):
1559
# Test that Repository.tarball generates the right operations
1560
transport_path = 'repo'
1561
expected_calls = [('call_expecting_body', 'Repository.tarball',
1562
('repo/', 'bz2',),),
1564
repo, client = self.setup_fake_client_and_repository(transport_path)
1565
client.add_success_response_with_body(self.tarball_content, 'ok')
1566
# Now actually ask for the tarball
1567
tarball_file = repo._get_tarball('bz2')
1569
self.assertEqual(expected_calls, client._calls)
1570
self.assertEqual(self.tarball_content, tarball_file.read())
1572
tarball_file.close()
1575
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1576
"""RemoteRepository.copy_content_into optimizations"""
1578
def test_copy_content_remote_to_local(self):
1579
self.transport_server = server.SmartTCPServer_for_testing
1580
src_repo = self.make_repository('repo1')
1581
src_repo = repository.Repository.open(self.get_url('repo1'))
1582
# At the moment the tarball-based copy_content_into can't write back
1583
# into a smart server. It would be good if it could upload the
1584
# tarball; once that works we'd have to create repositories of
1585
# different formats. -- mbp 20070410
1586
dest_url = self.get_vfs_only_url('repo2')
1587
dest_bzrdir = BzrDir.create(dest_url)
1588
dest_repo = dest_bzrdir.create_repository()
1589
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1590
self.assertTrue(isinstance(src_repo, RemoteRepository))
1591
src_repo.copy_content_into(dest_repo)
1594
class _StubRealPackRepository(object):
1596
def __init__(self, calls):
1597
self._pack_collection = _StubPackCollection(calls)
1600
class _StubPackCollection(object):
1602
def __init__(self, calls):
1606
self.calls.append(('pack collection autopack',))
1608
def reload_pack_names(self):
1609
self.calls.append(('pack collection reload_pack_names',))
1612
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
1613
"""Tests for RemoteRepository.autopack implementation."""
1616
"""When the server returns 'ok' and there's no _real_repository, then
1617
nothing else happens: the autopack method is done.
1619
transport_path = 'quack'
1620
repo, client = self.setup_fake_client_and_repository(transport_path)
1621
client.add_expected_call(
1622
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
1624
client.finished_test()
1626
def test_ok_with_real_repo(self):
1627
"""When the server returns 'ok' and there is a _real_repository, then
1628
the _real_repository's reload_pack_name's method will be called.
1630
transport_path = 'quack'
1631
repo, client = self.setup_fake_client_and_repository(transport_path)
1632
client.add_expected_call(
1633
'PackRepository.autopack', ('quack/',),
1635
repo._real_repository = _StubRealPackRepository(client._calls)
1638
[('call', 'PackRepository.autopack', ('quack/',)),
1639
('pack collection reload_pack_names',)],
1642
def test_backwards_compatibility(self):
1643
"""If the server does not recognise the PackRepository.autopack verb,
1644
fallback to the real_repository's implementation.
1646
transport_path = 'quack'
1647
repo, client = self.setup_fake_client_and_repository(transport_path)
1648
client.add_unknown_method_response('PackRepository.autopack')
1649
def stub_ensure_real():
1650
client._calls.append(('_ensure_real',))
1651
repo._real_repository = _StubRealPackRepository(client._calls)
1652
repo._ensure_real = stub_ensure_real
1655
[('call', 'PackRepository.autopack', ('quack/',)),
1657
('pack collection autopack',)],
1661
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
1662
"""Base class for unit tests for bzrlib.remote._translate_error."""
1664
def translateTuple(self, error_tuple, **context):
1665
"""Call _translate_error with an ErrorFromSmartServer built from the
1668
:param error_tuple: A tuple of a smart server response, as would be
1669
passed to an ErrorFromSmartServer.
1670
:kwargs context: context items to call _translate_error with.
1672
:returns: The error raised by _translate_error.
1674
# Raise the ErrorFromSmartServer before passing it as an argument,
1675
# because _translate_error may need to re-raise it with a bare 'raise'
1677
server_error = errors.ErrorFromSmartServer(error_tuple)
1678
translated_error = self.translateErrorFromSmartServer(
1679
server_error, **context)
1680
return translated_error
1682
def translateErrorFromSmartServer(self, error_object, **context):
1683
"""Like translateTuple, but takes an already constructed
1684
ErrorFromSmartServer rather than a tuple.
1688
except errors.ErrorFromSmartServer, server_error:
1689
translated_error = self.assertRaises(
1690
errors.BzrError, remote._translate_error, server_error,
1692
return translated_error
1695
class TestErrorTranslationSuccess(TestErrorTranslationBase):
1696
"""Unit tests for bzrlib.remote._translate_error.
1698
Given an ErrorFromSmartServer (which has an error tuple from a smart
1699
server) and some context, _translate_error raises more specific errors from
1702
This test case covers the cases where _translate_error succeeds in
1703
translating an ErrorFromSmartServer to something better. See
1704
TestErrorTranslationRobustness for other cases.
1707
def test_NoSuchRevision(self):
1708
branch = self.make_branch('')
1710
translated_error = self.translateTuple(
1711
('NoSuchRevision', revid), branch=branch)
1712
expected_error = errors.NoSuchRevision(branch, revid)
1713
self.assertEqual(expected_error, translated_error)
1715
def test_nosuchrevision(self):
1716
repository = self.make_repository('')
1718
translated_error = self.translateTuple(
1719
('nosuchrevision', revid), repository=repository)
1720
expected_error = errors.NoSuchRevision(repository, revid)
1721
self.assertEqual(expected_error, translated_error)
1723
def test_nobranch(self):
1724
bzrdir = self.make_bzrdir('')
1725
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
1726
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
1727
self.assertEqual(expected_error, translated_error)
1729
def test_LockContention(self):
1730
translated_error = self.translateTuple(('LockContention',))
1731
expected_error = errors.LockContention('(remote lock)')
1732
self.assertEqual(expected_error, translated_error)
1734
def test_UnlockableTransport(self):
1735
bzrdir = self.make_bzrdir('')
1736
translated_error = self.translateTuple(
1737
('UnlockableTransport',), bzrdir=bzrdir)
1738
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
1739
self.assertEqual(expected_error, translated_error)
1741
def test_LockFailed(self):
1742
lock = 'str() of a server lock'
1743
why = 'str() of why'
1744
translated_error = self.translateTuple(('LockFailed', lock, why))
1745
expected_error = errors.LockFailed(lock, why)
1746
self.assertEqual(expected_error, translated_error)
1748
def test_TokenMismatch(self):
1749
token = 'a lock token'
1750
translated_error = self.translateTuple(('TokenMismatch',), token=token)
1751
expected_error = errors.TokenMismatch(token, '(remote token)')
1752
self.assertEqual(expected_error, translated_error)
1754
def test_Diverged(self):
1755
branch = self.make_branch('a')
1756
other_branch = self.make_branch('b')
1757
translated_error = self.translateTuple(
1758
('Diverged',), branch=branch, other_branch=other_branch)
1759
expected_error = errors.DivergedBranches(branch, other_branch)
1760
self.assertEqual(expected_error, translated_error)
1762
def test_ReadError_no_args(self):
1764
translated_error = self.translateTuple(('ReadError',), path=path)
1765
expected_error = errors.ReadError(path)
1766
self.assertEqual(expected_error, translated_error)
1768
def test_ReadError(self):
1770
translated_error = self.translateTuple(('ReadError', path))
1771
expected_error = errors.ReadError(path)
1772
self.assertEqual(expected_error, translated_error)
1774
def test_PermissionDenied_no_args(self):
1776
translated_error = self.translateTuple(('PermissionDenied',), path=path)
1777
expected_error = errors.PermissionDenied(path)
1778
self.assertEqual(expected_error, translated_error)
1780
def test_PermissionDenied_one_arg(self):
1782
translated_error = self.translateTuple(('PermissionDenied', path))
1783
expected_error = errors.PermissionDenied(path)
1784
self.assertEqual(expected_error, translated_error)
1786
def test_PermissionDenied_one_arg_and_context(self):
1787
"""Given a choice between a path from the local context and a path on
1788
the wire, _translate_error prefers the path from the local context.
1790
local_path = 'local path'
1791
remote_path = 'remote path'
1792
translated_error = self.translateTuple(
1793
('PermissionDenied', remote_path), path=local_path)
1794
expected_error = errors.PermissionDenied(local_path)
1795
self.assertEqual(expected_error, translated_error)
1797
def test_PermissionDenied_two_args(self):
1799
extra = 'a string with extra info'
1800
translated_error = self.translateTuple(
1801
('PermissionDenied', path, extra))
1802
expected_error = errors.PermissionDenied(path, extra)
1803
self.assertEqual(expected_error, translated_error)
1806
class TestErrorTranslationRobustness(TestErrorTranslationBase):
1807
"""Unit tests for bzrlib.remote._translate_error's robustness.
1809
TestErrorTranslationSuccess is for cases where _translate_error can
1810
translate successfully. This class about how _translate_err behaves when
1811
it fails to translate: it re-raises the original error.
1814
def test_unrecognised_server_error(self):
1815
"""If the error code from the server is not recognised, the original
1816
ErrorFromSmartServer is propagated unmodified.
1818
error_tuple = ('An unknown error tuple',)
1819
server_error = errors.ErrorFromSmartServer(error_tuple)
1820
translated_error = self.translateErrorFromSmartServer(server_error)
1821
expected_error = errors.UnknownErrorFromSmartServer(server_error)
1822
self.assertEqual(expected_error, translated_error)
1824
def test_context_missing_a_key(self):
1825
"""In case of a bug in the client, or perhaps an unexpected response
1826
from a server, _translate_error returns the original error tuple from
1827
the server and mutters a warning.
1829
# To translate a NoSuchRevision error _translate_error needs a 'branch'
1830
# in the context dict. So let's give it an empty context dict instead
1831
# to exercise its error recovery.
1833
error_tuple = ('NoSuchRevision', 'revid')
1834
server_error = errors.ErrorFromSmartServer(error_tuple)
1835
translated_error = self.translateErrorFromSmartServer(server_error)
1836
self.assertEqual(server_error, translated_error)
1837
# In addition to re-raising ErrorFromSmartServer, some debug info has
1838
# been muttered to the log file for developer to look at.
1839
self.assertContainsRe(
1840
self._get_log(keep_log_file=True),
1841
"Missing key 'branch' in context")
1843
def test_path_missing(self):
1844
"""Some translations (PermissionDenied, ReadError) can determine the
1845
'path' variable from either the wire or the local context. If neither
1846
has it, then an error is raised.
1848
error_tuple = ('ReadError',)
1849
server_error = errors.ErrorFromSmartServer(error_tuple)
1850
translated_error = self.translateErrorFromSmartServer(server_error)
1851
self.assertEqual(server_error, translated_error)
1852
# In addition to re-raising ErrorFromSmartServer, some debug info has
1853
# been muttered to the log file for developer to look at.
1854
self.assertContainsRe(
1855
self._get_log(keep_log_file=True), "Missing key 'path' in context")
1858
class TestStacking(tests.TestCaseWithTransport):
1859
"""Tests for operations on stacked remote repositories.
1861
The underlying format type must support stacking.
1864
def test_access_stacked_remote(self):
1865
# based on <http://launchpad.net/bugs/261315>
1866
# make a branch stacked on another repository containing an empty
1867
# revision, then open it over hpss - we should be able to see that
1869
base_transport = self.get_transport()
1870
base_builder = self.make_branch_builder('base', format='1.6')
1871
base_builder.start_series()
1872
base_revid = base_builder.build_snapshot('rev-id', None,
1873
[('add', ('', None, 'directory', None))],
1875
base_builder.finish_series()
1876
stacked_branch = self.make_branch('stacked', format='1.6')
1877
stacked_branch.set_stacked_on_url('../base')
1878
# start a server looking at this
1879
smart_server = server.SmartTCPServer_for_testing()
1880
smart_server.setUp()
1881
self.addCleanup(smart_server.tearDown)
1882
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
1883
# can get its branch and repository
1884
remote_branch = remote_bzrdir.open_branch()
1885
remote_repo = remote_branch.repository
1886
remote_repo.lock_read()
1888
# it should have an appropriate fallback repository, which should also
1889
# be a RemoteRepository
1890
self.assertEquals(len(remote_repo._fallback_repositories), 1)
1891
self.assertIsInstance(remote_repo._fallback_repositories[0],
1893
# and it has the revision committed to the underlying repository;
1894
# these have varying implementations so we try several of them
1895
self.assertTrue(remote_repo.has_revisions([base_revid]))
1896
self.assertTrue(remote_repo.has_revision(base_revid))
1897
self.assertEqual(remote_repo.get_revision(base_revid).message,
1900
remote_repo.unlock()
1902
def prepare_stacked_remote_branch(self):
1903
smart_server = server.SmartTCPServer_for_testing()
1904
smart_server.setUp()
1905
self.addCleanup(smart_server.tearDown)
1906
tree1 = self.make_branch_and_tree('tree1')
1907
tree1.commit('rev1', rev_id='rev1')
1908
tree2 = self.make_branch_and_tree('tree2', format='1.6')
1909
tree2.branch.set_stacked_on_url(tree1.branch.base)
1910
branch2 = Branch.open(smart_server.get_url() + '/tree2')
1912
self.addCleanup(branch2.unlock)
1915
def test_stacked_get_parent_map(self):
1916
# the public implementation of get_parent_map obeys stacking
1917
branch = self.prepare_stacked_remote_branch()
1918
repo = branch.repository
1919
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
1921
def test_unstacked_get_parent_map(self):
1922
# _unstacked_provider.get_parent_map ignores stacking
1923
branch = self.prepare_stacked_remote_branch()
1924
provider = branch.repository._unstacked_provider
1925
self.assertEqual([], provider.get_parent_map(['rev1']).keys())
1928
class TestRemoteBranchEffort(tests.TestCaseWithTransport):
1931
super(TestRemoteBranchEffort, self).setUp()
1932
# Create a smart server that publishes whatever the backing VFS server
1934
self.smart_server = server.SmartTCPServer_for_testing()
1935
self.smart_server.setUp(self.get_server())
1936
self.addCleanup(self.smart_server.tearDown)
1937
# Log all HPSS calls into self.hpss_calls.
1938
_SmartClient.hooks.install_named_hook(
1939
'call', self.capture_hpss_call, None)
1940
self.hpss_calls = []
1942
def capture_hpss_call(self, params):
1943
self.hpss_calls.append(params.method)
1945
def test_copy_content_into_avoids_revision_history(self):
1946
local = self.make_branch('local')
1947
remote_backing_tree = self.make_branch_and_tree('remote')
1948
remote_backing_tree.commit("Commit.")
1949
remote_branch_url = self.smart_server.get_url() + 'remote'
1950
remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
1951
local.repository.fetch(remote_branch.repository)
1952
self.hpss_calls = []
1953
remote_branch.copy_content_into(local)
1954
self.assertFalse('Branch.revision_history' in self.hpss_calls)