1
# Copyright (C) 2006, 2007, 2008, 2009 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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
42
from bzrlib.branch import Branch
43
from bzrlib.bzrdir import BzrDir, BzrDirFormat
44
from bzrlib.remote import (
50
RemoteRepositoryFormat,
52
from bzrlib.repofmt import pack_repo
53
from bzrlib.revision import NULL_REVISION
54
from bzrlib.smart import server, medium
55
from bzrlib.smart.client import _SmartClient
56
from bzrlib.smart.repository import SmartServerRepositoryGetParentMap
57
from bzrlib.tests import (
59
split_suite_by_condition,
62
from bzrlib.transport import get_transport, http
63
from bzrlib.transport.memory import MemoryTransport
64
from bzrlib.transport.remote import (
70
def load_tests(standard_tests, module, loader):
71
to_adapt, result = split_suite_by_condition(
72
standard_tests, condition_isinstance(BasicRemoteObjectTests))
73
smart_server_version_scenarios = [
75
{'transport_server': server.SmartTCPServer_for_testing_v2_only}),
77
{'transport_server': server.SmartTCPServer_for_testing})]
78
return multiply_tests(to_adapt, smart_server_version_scenarios, result)
81
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
84
super(BasicRemoteObjectTests, self).setUp()
85
self.transport = self.get_transport()
86
# make a branch that can be opened over the smart transport
87
self.local_wt = BzrDir.create_standalone_workingtree('.')
90
self.transport.disconnect()
91
tests.TestCaseWithTransport.tearDown(self)
93
def test_create_remote_bzrdir(self):
94
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
95
self.assertIsInstance(b, BzrDir)
97
def test_open_remote_branch(self):
98
# open a standalone branch in the working directory
99
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
100
branch = b.open_branch()
101
self.assertIsInstance(branch, Branch)
103
def test_remote_repository(self):
104
b = BzrDir.open_from_transport(self.transport)
105
repo = b.open_repository()
106
revid = u'\xc823123123'.encode('utf8')
107
self.assertFalse(repo.has_revision(revid))
108
self.local_wt.commit(message='test commit', rev_id=revid)
109
self.assertTrue(repo.has_revision(revid))
111
def test_remote_branch_revision_history(self):
112
b = BzrDir.open_from_transport(self.transport).open_branch()
113
self.assertEqual([], b.revision_history())
114
r1 = self.local_wt.commit('1st commit')
115
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
116
self.assertEqual([r1, r2], b.revision_history())
118
def test_find_correct_format(self):
119
"""Should open a RemoteBzrDir over a RemoteTransport"""
120
fmt = BzrDirFormat.find_format(self.transport)
121
self.assertTrue(RemoteBzrDirFormat
122
in BzrDirFormat._control_server_formats)
123
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
125
def test_open_detected_smart_format(self):
126
fmt = BzrDirFormat.find_format(self.transport)
127
d = fmt.open(self.transport)
128
self.assertIsInstance(d, BzrDir)
130
def test_remote_branch_repr(self):
131
b = BzrDir.open_from_transport(self.transport).open_branch()
132
self.assertStartsWith(str(b), 'RemoteBranch(')
134
def test_remote_branch_format_supports_stacking(self):
136
self.make_branch('unstackable', format='pack-0.92')
137
b = BzrDir.open_from_transport(t.clone('unstackable')).open_branch()
138
self.assertFalse(b._format.supports_stacking())
139
self.make_branch('stackable', format='1.9')
140
b = BzrDir.open_from_transport(t.clone('stackable')).open_branch()
141
self.assertTrue(b._format.supports_stacking())
143
def test_remote_repo_format_supports_external_references(self):
145
bd = self.make_bzrdir('unstackable', format='pack-0.92')
146
r = bd.create_repository()
147
self.assertFalse(r._format.supports_external_lookups)
148
r = BzrDir.open_from_transport(t.clone('unstackable')).open_repository()
149
self.assertFalse(r._format.supports_external_lookups)
150
bd = self.make_bzrdir('stackable', format='1.9')
151
r = bd.create_repository()
152
self.assertTrue(r._format.supports_external_lookups)
153
r = BzrDir.open_from_transport(t.clone('stackable')).open_repository()
154
self.assertTrue(r._format.supports_external_lookups)
157
class FakeProtocol(object):
158
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
160
def __init__(self, body, fake_client):
162
self._body_buffer = None
163
self._fake_client = fake_client
165
def read_body_bytes(self, count=-1):
166
if self._body_buffer is None:
167
self._body_buffer = StringIO(self.body)
168
bytes = self._body_buffer.read(count)
169
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
170
self._fake_client.expecting_body = False
173
def cancel_read_body(self):
174
self._fake_client.expecting_body = False
176
def read_streamed_body(self):
180
class FakeClient(_SmartClient):
181
"""Lookalike for _SmartClient allowing testing."""
183
def __init__(self, fake_medium_base='fake base'):
184
"""Create a FakeClient."""
187
self.expecting_body = False
188
# if non-None, this is the list of expected calls, with only the
189
# method name and arguments included. the body might be hard to
190
# compute so is not included. If a call is None, that call can
192
self._expected_calls = None
193
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
195
def add_expected_call(self, call_name, call_args, response_type,
196
response_args, response_body=None):
197
if self._expected_calls is None:
198
self._expected_calls = []
199
self._expected_calls.append((call_name, call_args))
200
self.responses.append((response_type, response_args, response_body))
202
def add_success_response(self, *args):
203
self.responses.append(('success', args, None))
205
def add_success_response_with_body(self, body, *args):
206
self.responses.append(('success', args, body))
207
if self._expected_calls is not None:
208
self._expected_calls.append(None)
210
def add_error_response(self, *args):
211
self.responses.append(('error', args))
213
def add_unknown_method_response(self, verb):
214
self.responses.append(('unknown', verb))
216
def finished_test(self):
217
if self._expected_calls:
218
raise AssertionError("%r finished but was still expecting %r"
219
% (self, self._expected_calls[0]))
221
def _get_next_response(self):
223
response_tuple = self.responses.pop(0)
224
except IndexError, e:
225
raise AssertionError("%r didn't expect any more calls"
227
if response_tuple[0] == 'unknown':
228
raise errors.UnknownSmartMethod(response_tuple[1])
229
elif response_tuple[0] == 'error':
230
raise errors.ErrorFromSmartServer(response_tuple[1])
231
return response_tuple
233
def _check_call(self, method, args):
234
if self._expected_calls is None:
235
# the test should be updated to say what it expects
238
next_call = self._expected_calls.pop(0)
240
raise AssertionError("%r didn't expect any more calls "
242
% (self, method, args,))
243
if next_call is None:
245
if method != next_call[0] or args != next_call[1]:
246
raise AssertionError("%r expected %r%r "
248
% (self, next_call[0], next_call[1], method, args,))
250
def call(self, method, *args):
251
self._check_call(method, args)
252
self._calls.append(('call', method, args))
253
return self._get_next_response()[1]
255
def call_expecting_body(self, method, *args):
256
self._check_call(method, args)
257
self._calls.append(('call_expecting_body', method, args))
258
result = self._get_next_response()
259
self.expecting_body = True
260
return result[1], FakeProtocol(result[2], self)
262
def call_with_body_bytes_expecting_body(self, method, args, body):
263
self._check_call(method, args)
264
self._calls.append(('call_with_body_bytes_expecting_body', method,
266
result = self._get_next_response()
267
self.expecting_body = True
268
return result[1], FakeProtocol(result[2], self)
270
def call_with_body_stream(self, args, stream):
271
# Explicitly consume the stream before checking for an error, because
272
# that's what happens a real medium.
273
stream = list(stream)
274
self._check_call(args[0], args[1:])
275
self._calls.append(('call_with_body_stream', args[0], args[1:], stream))
276
result = self._get_next_response()
277
# The second value returned from call_with_body_stream is supposed to
278
# be a response_handler object, but so far no tests depend on that.
279
response_handler = None
280
return result[1], response_handler
283
class FakeMedium(medium.SmartClientMedium):
285
def __init__(self, client_calls, base):
286
medium.SmartClientMedium.__init__(self, base)
287
self._client_calls = client_calls
289
def disconnect(self):
290
self._client_calls.append(('disconnect medium',))
293
class TestVfsHas(tests.TestCase):
295
def test_unicode_path(self):
296
client = FakeClient('/')
297
client.add_success_response('yes',)
298
transport = RemoteTransport('bzr://localhost/', _client=client)
299
filename = u'/hell\u00d8'.encode('utf8')
300
result = transport.has(filename)
302
[('call', 'has', (filename,))],
304
self.assertTrue(result)
307
class TestRemote(tests.TestCaseWithMemoryTransport):
309
def get_branch_format(self):
310
reference_bzrdir_format = bzrdir.format_registry.get('default')()
311
return reference_bzrdir_format.get_branch_format()
313
def get_repo_format(self):
314
reference_bzrdir_format = bzrdir.format_registry.get('default')()
315
return reference_bzrdir_format.repository_format
317
def disable_verb(self, verb):
318
"""Disable a verb for one test."""
319
request_handlers = smart.request.request_handlers
320
orig_method = request_handlers.get(verb)
321
request_handlers.remove(verb)
323
request_handlers.register(verb, orig_method)
324
self.addCleanup(restoreVerb)
327
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
328
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
330
def assertRemotePath(self, expected, client_base, transport_base):
331
"""Assert that the result of
332
SmartClientMedium.remote_path_from_transport is the expected value for
333
a given client_base and transport_base.
335
client_medium = medium.SmartClientMedium(client_base)
336
transport = get_transport(transport_base)
337
result = client_medium.remote_path_from_transport(transport)
338
self.assertEqual(expected, result)
340
def test_remote_path_from_transport(self):
341
"""SmartClientMedium.remote_path_from_transport calculates a URL for
342
the given transport relative to the root of the client base URL.
344
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
345
self.assertRemotePath(
346
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
348
def assertRemotePathHTTP(self, expected, transport_base, relpath):
349
"""Assert that the result of
350
HttpTransportBase.remote_path_from_transport is the expected value for
351
a given transport_base and relpath of that transport. (Note that
352
HttpTransportBase is a subclass of SmartClientMedium)
354
base_transport = get_transport(transport_base)
355
client_medium = base_transport.get_smart_medium()
356
cloned_transport = base_transport.clone(relpath)
357
result = client_medium.remote_path_from_transport(cloned_transport)
358
self.assertEqual(expected, result)
360
def test_remote_path_from_transport_http(self):
361
"""Remote paths for HTTP transports are calculated differently to other
362
transports. They are just relative to the client base, not the root
363
directory of the host.
365
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
366
self.assertRemotePathHTTP(
367
'../xyz/', scheme + '//host/path', '../xyz/')
368
self.assertRemotePathHTTP(
369
'xyz/', scheme + '//host/path', 'xyz/')
372
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
373
"""Tests for the behaviour of client_medium.remote_is_at_least."""
375
def test_initially_unlimited(self):
376
"""A fresh medium assumes that the remote side supports all
379
client_medium = medium.SmartClientMedium('dummy base')
380
self.assertFalse(client_medium._is_remote_before((99, 99)))
382
def test__remember_remote_is_before(self):
383
"""Calling _remember_remote_is_before ratchets down the known remote
386
client_medium = medium.SmartClientMedium('dummy base')
387
# Mark the remote side as being less than 1.6. The remote side may
389
client_medium._remember_remote_is_before((1, 6))
390
self.assertTrue(client_medium._is_remote_before((1, 6)))
391
self.assertFalse(client_medium._is_remote_before((1, 5)))
392
# Calling _remember_remote_is_before again with a lower value works.
393
client_medium._remember_remote_is_before((1, 5))
394
self.assertTrue(client_medium._is_remote_before((1, 5)))
395
# You cannot call _remember_remote_is_before with a larger value.
397
AssertionError, client_medium._remember_remote_is_before, (1, 9))
400
class TestBzrDirCloningMetaDir(TestRemote):
402
def test_backwards_compat(self):
403
self.setup_smart_server_with_call_log()
404
a_dir = self.make_bzrdir('.')
405
self.reset_smart_call_log()
406
verb = 'BzrDir.cloning_metadir'
407
self.disable_verb(verb)
408
format = a_dir.cloning_metadir()
409
call_count = len([call for call in self.hpss_calls if
410
call.call.method == verb])
411
self.assertEqual(1, call_count)
413
def test_branch_reference(self):
414
transport = self.get_transport('quack')
415
referenced = self.make_branch('referenced')
416
expected = referenced.bzrdir.cloning_metadir()
417
client = FakeClient(transport.base)
418
client.add_expected_call(
419
'BzrDir.cloning_metadir', ('quack/', 'False'),
420
'error', ('BranchReference',)),
421
client.add_expected_call(
422
'BzrDir.open_branchV2', ('quack/',),
423
'success', ('ref', self.get_url('referenced'))),
424
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
426
result = a_bzrdir.cloning_metadir()
427
# We should have got a control dir matching the referenced branch.
428
self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
429
self.assertEqual(expected._repository_format, result._repository_format)
430
self.assertEqual(expected._branch_format, result._branch_format)
431
client.finished_test()
433
def test_current_server(self):
434
transport = self.get_transport('.')
435
transport = transport.clone('quack')
436
self.make_bzrdir('quack')
437
client = FakeClient(transport.base)
438
reference_bzrdir_format = bzrdir.format_registry.get('default')()
439
control_name = reference_bzrdir_format.network_name()
440
client.add_expected_call(
441
'BzrDir.cloning_metadir', ('quack/', 'False'),
442
'success', (control_name, '', ('branch', ''))),
443
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
445
result = a_bzrdir.cloning_metadir()
446
# We should have got a reference control dir with default branch and
447
# repository formats.
448
# This pokes a little, just to be sure.
449
self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
450
self.assertEqual(None, result._repository_format)
451
self.assertEqual(None, result._branch_format)
452
client.finished_test()
455
class TestBzrDirOpenBranch(TestRemote):
457
def test_backwards_compat(self):
458
self.setup_smart_server_with_call_log()
459
self.make_branch('.')
460
a_dir = BzrDir.open(self.get_url('.'))
461
self.reset_smart_call_log()
462
verb = 'BzrDir.open_branchV2'
463
self.disable_verb(verb)
464
format = a_dir.open_branch()
465
call_count = len([call for call in self.hpss_calls if
466
call.call.method == verb])
467
self.assertEqual(1, call_count)
469
def test_branch_present(self):
470
reference_format = self.get_repo_format()
471
network_name = reference_format.network_name()
472
branch_network_name = self.get_branch_format().network_name()
473
transport = MemoryTransport()
474
transport.mkdir('quack')
475
transport = transport.clone('quack')
476
client = FakeClient(transport.base)
477
client.add_expected_call(
478
'BzrDir.open_branchV2', ('quack/',),
479
'success', ('branch', branch_network_name))
480
client.add_expected_call(
481
'BzrDir.find_repositoryV3', ('quack/',),
482
'success', ('ok', '', 'no', 'no', 'no', network_name))
483
client.add_expected_call(
484
'Branch.get_stacked_on_url', ('quack/',),
485
'error', ('NotStacked',))
486
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
488
result = bzrdir.open_branch()
489
self.assertIsInstance(result, RemoteBranch)
490
self.assertEqual(bzrdir, result.bzrdir)
491
client.finished_test()
493
def test_branch_missing(self):
494
transport = MemoryTransport()
495
transport.mkdir('quack')
496
transport = transport.clone('quack')
497
client = FakeClient(transport.base)
498
client.add_error_response('nobranch')
499
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
501
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
503
[('call', 'BzrDir.open_branchV2', ('quack/',))],
506
def test__get_tree_branch(self):
507
# _get_tree_branch is a form of open_branch, but it should only ask for
508
# branch opening, not any other network requests.
511
calls.append("Called")
513
transport = MemoryTransport()
514
# no requests on the network - catches other api calls being made.
515
client = FakeClient(transport.base)
516
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
518
# patch the open_branch call to record that it was called.
519
bzrdir.open_branch = open_branch
520
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
521
self.assertEqual(["Called"], calls)
522
self.assertEqual([], client._calls)
524
def test_url_quoting_of_path(self):
525
# Relpaths on the wire should not be URL-escaped. So "~" should be
526
# transmitted as "~", not "%7E".
527
transport = RemoteTCPTransport('bzr://localhost/~hello/')
528
client = FakeClient(transport.base)
529
reference_format = self.get_repo_format()
530
network_name = reference_format.network_name()
531
branch_network_name = self.get_branch_format().network_name()
532
client.add_expected_call(
533
'BzrDir.open_branchV2', ('~hello/',),
534
'success', ('branch', branch_network_name))
535
client.add_expected_call(
536
'BzrDir.find_repositoryV3', ('~hello/',),
537
'success', ('ok', '', 'no', 'no', 'no', network_name))
538
client.add_expected_call(
539
'Branch.get_stacked_on_url', ('~hello/',),
540
'error', ('NotStacked',))
541
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
543
result = bzrdir.open_branch()
544
client.finished_test()
546
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
547
reference_format = self.get_repo_format()
548
network_name = reference_format.network_name()
549
transport = MemoryTransport()
550
transport.mkdir('quack')
551
transport = transport.clone('quack')
553
rich_response = 'yes'
557
subtree_response = 'yes'
559
subtree_response = 'no'
560
client = FakeClient(transport.base)
561
client.add_success_response(
562
'ok', '', rich_response, subtree_response, external_lookup,
564
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
566
result = bzrdir.open_repository()
568
[('call', 'BzrDir.find_repositoryV3', ('quack/',))],
570
self.assertIsInstance(result, RemoteRepository)
571
self.assertEqual(bzrdir, result.bzrdir)
572
self.assertEqual(rich_root, result._format.rich_root_data)
573
self.assertEqual(subtrees, result._format.supports_tree_reference)
575
def test_open_repository_sets_format_attributes(self):
576
self.check_open_repository(True, True)
577
self.check_open_repository(False, True)
578
self.check_open_repository(True, False)
579
self.check_open_repository(False, False)
580
self.check_open_repository(False, False, 'yes')
582
def test_old_server(self):
583
"""RemoteBzrDirFormat should fail to probe if the server version is too
586
self.assertRaises(errors.NotBranchError,
587
RemoteBzrDirFormat.probe_transport, OldServerTransport())
590
class TestBzrDirCreateBranch(TestRemote):
592
def test_backwards_compat(self):
593
self.setup_smart_server_with_call_log()
594
repo = self.make_repository('.')
595
self.reset_smart_call_log()
596
self.disable_verb('BzrDir.create_branch')
597
branch = repo.bzrdir.create_branch()
598
create_branch_call_count = len([call for call in self.hpss_calls if
599
call.call.method == 'BzrDir.create_branch'])
600
self.assertEqual(1, create_branch_call_count)
602
def test_current_server(self):
603
transport = self.get_transport('.')
604
transport = transport.clone('quack')
605
self.make_repository('quack')
606
client = FakeClient(transport.base)
607
reference_bzrdir_format = bzrdir.format_registry.get('default')()
608
reference_format = reference_bzrdir_format.get_branch_format()
609
network_name = reference_format.network_name()
610
reference_repo_fmt = reference_bzrdir_format.repository_format
611
reference_repo_name = reference_repo_fmt.network_name()
612
client.add_expected_call(
613
'BzrDir.create_branch', ('quack/', network_name),
614
'success', ('ok', network_name, '', 'no', 'no', 'yes',
615
reference_repo_name))
616
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
618
branch = a_bzrdir.create_branch()
619
# We should have got a remote branch
620
self.assertIsInstance(branch, remote.RemoteBranch)
621
# its format should have the settings from the response
622
format = branch._format
623
self.assertEqual(network_name, format.network_name())
626
class TestBzrDirCreateRepository(TestRemote):
628
def test_backwards_compat(self):
629
self.setup_smart_server_with_call_log()
630
bzrdir = self.make_bzrdir('.')
631
self.reset_smart_call_log()
632
self.disable_verb('BzrDir.create_repository')
633
repo = bzrdir.create_repository()
634
create_repo_call_count = len([call for call in self.hpss_calls if
635
call.call.method == 'BzrDir.create_repository'])
636
self.assertEqual(1, create_repo_call_count)
638
def test_current_server(self):
639
transport = self.get_transport('.')
640
transport = transport.clone('quack')
641
self.make_bzrdir('quack')
642
client = FakeClient(transport.base)
643
reference_bzrdir_format = bzrdir.format_registry.get('default')()
644
reference_format = reference_bzrdir_format.repository_format
645
network_name = reference_format.network_name()
646
client.add_expected_call(
647
'BzrDir.create_repository', ('quack/',
648
'Bazaar pack repository format 1 (needs bzr 0.92)\n', 'False'),
649
'success', ('ok', 'no', 'no', 'no', network_name))
650
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
652
repo = a_bzrdir.create_repository()
653
# We should have got a remote repository
654
self.assertIsInstance(repo, remote.RemoteRepository)
655
# its format should have the settings from the response
656
format = repo._format
657
self.assertFalse(format.rich_root_data)
658
self.assertFalse(format.supports_tree_reference)
659
self.assertFalse(format.supports_external_lookups)
660
self.assertEqual(network_name, format.network_name())
663
class TestBzrDirOpenRepository(TestRemote):
665
def test_backwards_compat_1_2_3(self):
666
# fallback all the way to the first version.
667
reference_format = self.get_repo_format()
668
network_name = reference_format.network_name()
669
client = FakeClient('bzr://example.com/')
670
client.add_unknown_method_response('BzrDir.find_repositoryV3')
671
client.add_unknown_method_response('BzrDir.find_repositoryV2')
672
client.add_success_response('ok', '', 'no', 'no')
673
# A real repository instance will be created to determine the network
675
client.add_success_response_with_body(
676
"Bazaar-NG meta directory, format 1\n", 'ok')
677
client.add_success_response_with_body(
678
reference_format.get_format_string(), 'ok')
679
# PackRepository wants to do a stat
680
client.add_success_response('stat', '0', '65535')
681
remote_transport = RemoteTransport('bzr://example.com/quack/', medium=False,
683
bzrdir = RemoteBzrDir(remote_transport, remote.RemoteBzrDirFormat(),
685
repo = bzrdir.open_repository()
687
[('call', 'BzrDir.find_repositoryV3', ('quack/',)),
688
('call', 'BzrDir.find_repositoryV2', ('quack/',)),
689
('call', 'BzrDir.find_repository', ('quack/',)),
690
('call_expecting_body', 'get', ('/quack/.bzr/branch-format',)),
691
('call_expecting_body', 'get', ('/quack/.bzr/repository/format',)),
692
('call', 'stat', ('/quack/.bzr/repository',)),
695
self.assertEqual(network_name, repo._format.network_name())
697
def test_backwards_compat_2(self):
698
# fallback to find_repositoryV2
699
reference_format = self.get_repo_format()
700
network_name = reference_format.network_name()
701
client = FakeClient('bzr://example.com/')
702
client.add_unknown_method_response('BzrDir.find_repositoryV3')
703
client.add_success_response('ok', '', 'no', 'no', 'no')
704
# A real repository instance will be created to determine the network
706
client.add_success_response_with_body(
707
"Bazaar-NG meta directory, format 1\n", 'ok')
708
client.add_success_response_with_body(
709
reference_format.get_format_string(), 'ok')
710
# PackRepository wants to do a stat
711
client.add_success_response('stat', '0', '65535')
712
remote_transport = RemoteTransport('bzr://example.com/quack/', medium=False,
714
bzrdir = RemoteBzrDir(remote_transport, remote.RemoteBzrDirFormat(),
716
repo = bzrdir.open_repository()
718
[('call', 'BzrDir.find_repositoryV3', ('quack/',)),
719
('call', 'BzrDir.find_repositoryV2', ('quack/',)),
720
('call_expecting_body', 'get', ('/quack/.bzr/branch-format',)),
721
('call_expecting_body', 'get', ('/quack/.bzr/repository/format',)),
722
('call', 'stat', ('/quack/.bzr/repository',)),
725
self.assertEqual(network_name, repo._format.network_name())
727
def test_current_server(self):
728
reference_format = self.get_repo_format()
729
network_name = reference_format.network_name()
730
transport = MemoryTransport()
731
transport.mkdir('quack')
732
transport = transport.clone('quack')
733
client = FakeClient(transport.base)
734
client.add_success_response('ok', '', 'no', 'no', 'no', network_name)
735
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
737
repo = bzrdir.open_repository()
739
[('call', 'BzrDir.find_repositoryV3', ('quack/',))],
741
self.assertEqual(network_name, repo._format.network_name())
744
class OldSmartClient(object):
745
"""A fake smart client for test_old_version that just returns a version one
746
response to the 'hello' (query version) command.
749
def get_request(self):
750
input_file = StringIO('ok\x011\n')
751
output_file = StringIO()
752
client_medium = medium.SmartSimplePipesClientMedium(
753
input_file, output_file)
754
return medium.SmartClientStreamMediumRequest(client_medium)
756
def protocol_version(self):
760
class OldServerTransport(object):
761
"""A fake transport for test_old_server that reports it's smart server
762
protocol version as version one.
768
def get_smart_client(self):
769
return OldSmartClient()
772
class RemoteBranchTestCase(TestRemote):
774
def make_remote_branch(self, transport, client):
775
"""Make a RemoteBranch using 'client' as its _SmartClient.
777
A RemoteBzrDir and RemoteRepository will also be created to fill out
778
the RemoteBranch, albeit with stub values for some of their attributes.
780
# we do not want bzrdir to make any remote calls, so use False as its
781
# _client. If it tries to make a remote call, this will fail
783
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
785
repo = RemoteRepository(bzrdir, None, _client=client)
786
branch_format = self.get_branch_format()
787
format = RemoteBranchFormat(network_name=branch_format.network_name())
788
return RemoteBranch(bzrdir, repo, _client=client, format=format)
791
class TestBranchGetParent(RemoteBranchTestCase):
793
def test_no_parent(self):
794
# in an empty branch we decode the response properly
795
transport = MemoryTransport()
796
client = FakeClient(transport.base)
797
client.add_expected_call(
798
'Branch.get_stacked_on_url', ('quack/',),
799
'error', ('NotStacked',))
800
client.add_expected_call(
801
'Branch.get_parent', ('quack/',),
803
transport.mkdir('quack')
804
transport = transport.clone('quack')
805
branch = self.make_remote_branch(transport, client)
806
result = branch.get_parent()
807
client.finished_test()
808
self.assertEqual(None, result)
810
def test_parent_relative(self):
811
transport = MemoryTransport()
812
client = FakeClient(transport.base)
813
client.add_expected_call(
814
'Branch.get_stacked_on_url', ('kwaak/',),
815
'error', ('NotStacked',))
816
client.add_expected_call(
817
'Branch.get_parent', ('kwaak/',),
818
'success', ('../foo/',))
819
transport.mkdir('kwaak')
820
transport = transport.clone('kwaak')
821
branch = self.make_remote_branch(transport, client)
822
result = branch.get_parent()
823
self.assertEqual(transport.clone('../foo').base, result)
825
def test_parent_absolute(self):
826
transport = MemoryTransport()
827
client = FakeClient(transport.base)
828
client.add_expected_call(
829
'Branch.get_stacked_on_url', ('kwaak/',),
830
'error', ('NotStacked',))
831
client.add_expected_call(
832
'Branch.get_parent', ('kwaak/',),
833
'success', ('http://foo/',))
834
transport.mkdir('kwaak')
835
transport = transport.clone('kwaak')
836
branch = self.make_remote_branch(transport, client)
837
result = branch.get_parent()
838
self.assertEqual('http://foo/', result)
841
class TestBranchGetTagsBytes(RemoteBranchTestCase):
843
def test_backwards_compat(self):
844
self.setup_smart_server_with_call_log()
845
branch = self.make_branch('.')
846
self.reset_smart_call_log()
847
verb = 'Branch.get_tags_bytes'
848
self.disable_verb(verb)
849
branch.tags.get_tag_dict()
850
call_count = len([call for call in self.hpss_calls if
851
call.call.method == verb])
852
self.assertEqual(1, call_count)
854
def test_trivial(self):
855
transport = MemoryTransport()
856
client = FakeClient(transport.base)
857
client.add_expected_call(
858
'Branch.get_stacked_on_url', ('quack/',),
859
'error', ('NotStacked',))
860
client.add_expected_call(
861
'Branch.get_tags_bytes', ('quack/',),
863
transport.mkdir('quack')
864
transport = transport.clone('quack')
865
branch = self.make_remote_branch(transport, client)
866
result = branch.tags.get_tag_dict()
867
client.finished_test()
868
self.assertEqual({}, result)
871
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
873
def test_empty_branch(self):
874
# in an empty branch we decode the response properly
875
transport = MemoryTransport()
876
client = FakeClient(transport.base)
877
client.add_expected_call(
878
'Branch.get_stacked_on_url', ('quack/',),
879
'error', ('NotStacked',))
880
client.add_expected_call(
881
'Branch.last_revision_info', ('quack/',),
882
'success', ('ok', '0', 'null:'))
883
transport.mkdir('quack')
884
transport = transport.clone('quack')
885
branch = self.make_remote_branch(transport, client)
886
result = branch.last_revision_info()
887
client.finished_test()
888
self.assertEqual((0, NULL_REVISION), result)
890
def test_non_empty_branch(self):
891
# in a non-empty branch we also decode the response properly
892
revid = u'\xc8'.encode('utf8')
893
transport = MemoryTransport()
894
client = FakeClient(transport.base)
895
client.add_expected_call(
896
'Branch.get_stacked_on_url', ('kwaak/',),
897
'error', ('NotStacked',))
898
client.add_expected_call(
899
'Branch.last_revision_info', ('kwaak/',),
900
'success', ('ok', '2', revid))
901
transport.mkdir('kwaak')
902
transport = transport.clone('kwaak')
903
branch = self.make_remote_branch(transport, client)
904
result = branch.last_revision_info()
905
self.assertEqual((2, revid), result)
908
class TestBranch_get_stacked_on_url(TestRemote):
909
"""Test Branch._get_stacked_on_url rpc"""
911
def test_get_stacked_on_invalid_url(self):
912
# test that asking for a stacked on url the server can't access works.
913
# This isn't perfect, but then as we're in the same process there
914
# really isn't anything we can do to be 100% sure that the server
915
# doesn't just open in - this test probably needs to be rewritten using
916
# a spawn()ed server.
917
stacked_branch = self.make_branch('stacked', format='1.9')
918
memory_branch = self.make_branch('base', format='1.9')
919
vfs_url = self.get_vfs_only_url('base')
920
stacked_branch.set_stacked_on_url(vfs_url)
921
transport = stacked_branch.bzrdir.root_transport
922
client = FakeClient(transport.base)
923
client.add_expected_call(
924
'Branch.get_stacked_on_url', ('stacked/',),
925
'success', ('ok', vfs_url))
926
# XXX: Multiple calls are bad, this second call documents what is
928
client.add_expected_call(
929
'Branch.get_stacked_on_url', ('stacked/',),
930
'success', ('ok', vfs_url))
931
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
933
repo_fmt = remote.RemoteRepositoryFormat()
934
repo_fmt._custom_format = stacked_branch.repository._format
935
branch = RemoteBranch(bzrdir, RemoteRepository(bzrdir, repo_fmt),
937
result = branch.get_stacked_on_url()
938
self.assertEqual(vfs_url, result)
940
def test_backwards_compatible(self):
941
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
942
base_branch = self.make_branch('base', format='1.6')
943
stacked_branch = self.make_branch('stacked', format='1.6')
944
stacked_branch.set_stacked_on_url('../base')
945
client = FakeClient(self.get_url())
946
branch_network_name = self.get_branch_format().network_name()
947
client.add_expected_call(
948
'BzrDir.open_branchV2', ('stacked/',),
949
'success', ('branch', branch_network_name))
950
client.add_expected_call(
951
'BzrDir.find_repositoryV3', ('stacked/',),
952
'success', ('ok', '', 'no', 'no', 'yes',
953
stacked_branch.repository._format.network_name()))
954
# called twice, once from constructor and then again by us
955
client.add_expected_call(
956
'Branch.get_stacked_on_url', ('stacked/',),
957
'unknown', ('Branch.get_stacked_on_url',))
958
client.add_expected_call(
959
'Branch.get_stacked_on_url', ('stacked/',),
960
'unknown', ('Branch.get_stacked_on_url',))
961
# this will also do vfs access, but that goes direct to the transport
962
# and isn't seen by the FakeClient.
963
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
964
remote.RemoteBzrDirFormat(), _client=client)
965
branch = bzrdir.open_branch()
966
result = branch.get_stacked_on_url()
967
self.assertEqual('../base', result)
968
client.finished_test()
969
# it's in the fallback list both for the RemoteRepository and its vfs
971
self.assertEqual(1, len(branch.repository._fallback_repositories))
973
len(branch.repository._real_repository._fallback_repositories))
975
def test_get_stacked_on_real_branch(self):
976
base_branch = self.make_branch('base', format='1.6')
977
stacked_branch = self.make_branch('stacked', format='1.6')
978
stacked_branch.set_stacked_on_url('../base')
979
reference_format = self.get_repo_format()
980
network_name = reference_format.network_name()
981
client = FakeClient(self.get_url())
982
branch_network_name = self.get_branch_format().network_name()
983
client.add_expected_call(
984
'BzrDir.open_branchV2', ('stacked/',),
985
'success', ('branch', branch_network_name))
986
client.add_expected_call(
987
'BzrDir.find_repositoryV3', ('stacked/',),
988
'success', ('ok', '', 'no', 'no', 'yes', network_name))
989
# called twice, once from constructor and then again by us
990
client.add_expected_call(
991
'Branch.get_stacked_on_url', ('stacked/',),
992
'success', ('ok', '../base'))
993
client.add_expected_call(
994
'Branch.get_stacked_on_url', ('stacked/',),
995
'success', ('ok', '../base'))
996
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
997
remote.RemoteBzrDirFormat(), _client=client)
998
branch = bzrdir.open_branch()
999
result = branch.get_stacked_on_url()
1000
self.assertEqual('../base', result)
1001
client.finished_test()
1002
# it's in the fallback list both for the RemoteRepository.
1003
self.assertEqual(1, len(branch.repository._fallback_repositories))
1004
# And we haven't had to construct a real repository.
1005
self.assertEqual(None, branch.repository._real_repository)
1008
class TestBranchSetLastRevision(RemoteBranchTestCase):
1010
def test_set_empty(self):
1011
# set_revision_history([]) is translated to calling
1012
# Branch.set_last_revision(path, '') on the wire.
1013
transport = MemoryTransport()
1014
transport.mkdir('branch')
1015
transport = transport.clone('branch')
1017
client = FakeClient(transport.base)
1018
client.add_expected_call(
1019
'Branch.get_stacked_on_url', ('branch/',),
1020
'error', ('NotStacked',))
1021
client.add_expected_call(
1022
'Branch.lock_write', ('branch/', '', ''),
1023
'success', ('ok', 'branch token', 'repo token'))
1024
client.add_expected_call(
1025
'Branch.last_revision_info',
1027
'success', ('ok', '0', 'null:'))
1028
client.add_expected_call(
1029
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
1031
client.add_expected_call(
1032
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1034
branch = self.make_remote_branch(transport, client)
1035
# This is a hack to work around the problem that RemoteBranch currently
1036
# unnecessarily invokes _ensure_real upon a call to lock_write.
1037
branch._ensure_real = lambda: None
1039
result = branch.set_revision_history([])
1041
self.assertEqual(None, result)
1042
client.finished_test()
1044
def test_set_nonempty(self):
1045
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
1046
# Branch.set_last_revision(path, rev-idN) on the wire.
1047
transport = MemoryTransport()
1048
transport.mkdir('branch')
1049
transport = transport.clone('branch')
1051
client = FakeClient(transport.base)
1052
client.add_expected_call(
1053
'Branch.get_stacked_on_url', ('branch/',),
1054
'error', ('NotStacked',))
1055
client.add_expected_call(
1056
'Branch.lock_write', ('branch/', '', ''),
1057
'success', ('ok', 'branch token', 'repo token'))
1058
client.add_expected_call(
1059
'Branch.last_revision_info',
1061
'success', ('ok', '0', 'null:'))
1063
encoded_body = bz2.compress('\n'.join(lines))
1064
client.add_success_response_with_body(encoded_body, 'ok')
1065
client.add_expected_call(
1066
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
1068
client.add_expected_call(
1069
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1071
branch = self.make_remote_branch(transport, client)
1072
# This is a hack to work around the problem that RemoteBranch currently
1073
# unnecessarily invokes _ensure_real upon a call to lock_write.
1074
branch._ensure_real = lambda: None
1075
# Lock the branch, reset the record of remote calls.
1077
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
1079
self.assertEqual(None, result)
1080
client.finished_test()
1082
def test_no_such_revision(self):
1083
transport = MemoryTransport()
1084
transport.mkdir('branch')
1085
transport = transport.clone('branch')
1086
# A response of 'NoSuchRevision' is translated into an exception.
1087
client = FakeClient(transport.base)
1088
client.add_expected_call(
1089
'Branch.get_stacked_on_url', ('branch/',),
1090
'error', ('NotStacked',))
1091
client.add_expected_call(
1092
'Branch.lock_write', ('branch/', '', ''),
1093
'success', ('ok', 'branch token', 'repo token'))
1094
client.add_expected_call(
1095
'Branch.last_revision_info',
1097
'success', ('ok', '0', 'null:'))
1098
# get_graph calls to construct the revision history, for the set_rh
1101
encoded_body = bz2.compress('\n'.join(lines))
1102
client.add_success_response_with_body(encoded_body, 'ok')
1103
client.add_expected_call(
1104
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
1105
'error', ('NoSuchRevision', 'rev-id'))
1106
client.add_expected_call(
1107
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1110
branch = self.make_remote_branch(transport, client)
1113
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
1115
client.finished_test()
1117
def test_tip_change_rejected(self):
1118
"""TipChangeRejected responses cause a TipChangeRejected exception to
1121
transport = MemoryTransport()
1122
transport.mkdir('branch')
1123
transport = transport.clone('branch')
1124
client = FakeClient(transport.base)
1125
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
1126
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
1127
client.add_expected_call(
1128
'Branch.get_stacked_on_url', ('branch/',),
1129
'error', ('NotStacked',))
1130
client.add_expected_call(
1131
'Branch.lock_write', ('branch/', '', ''),
1132
'success', ('ok', 'branch token', 'repo token'))
1133
client.add_expected_call(
1134
'Branch.last_revision_info',
1136
'success', ('ok', '0', 'null:'))
1138
encoded_body = bz2.compress('\n'.join(lines))
1139
client.add_success_response_with_body(encoded_body, 'ok')
1140
client.add_expected_call(
1141
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
1142
'error', ('TipChangeRejected', rejection_msg_utf8))
1143
client.add_expected_call(
1144
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1146
branch = self.make_remote_branch(transport, client)
1147
branch._ensure_real = lambda: None
1149
# The 'TipChangeRejected' error response triggered by calling
1150
# set_revision_history causes a TipChangeRejected exception.
1151
err = self.assertRaises(
1152
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
1153
# The UTF-8 message from the response has been decoded into a unicode
1155
self.assertIsInstance(err.msg, unicode)
1156
self.assertEqual(rejection_msg_unicode, err.msg)
1158
client.finished_test()
1161
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
1163
def test_set_last_revision_info(self):
1164
# set_last_revision_info(num, 'rev-id') is translated to calling
1165
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
1166
transport = MemoryTransport()
1167
transport.mkdir('branch')
1168
transport = transport.clone('branch')
1169
client = FakeClient(transport.base)
1170
# get_stacked_on_url
1171
client.add_error_response('NotStacked')
1173
client.add_success_response('ok', 'branch token', 'repo token')
1174
# query the current revision
1175
client.add_success_response('ok', '0', 'null:')
1177
client.add_success_response('ok')
1179
client.add_success_response('ok')
1181
branch = self.make_remote_branch(transport, client)
1182
# Lock the branch, reset the record of remote calls.
1185
result = branch.set_last_revision_info(1234, 'a-revision-id')
1187
[('call', 'Branch.last_revision_info', ('branch/',)),
1188
('call', 'Branch.set_last_revision_info',
1189
('branch/', 'branch token', 'repo token',
1190
'1234', 'a-revision-id'))],
1192
self.assertEqual(None, result)
1194
def test_no_such_revision(self):
1195
# A response of 'NoSuchRevision' is translated into an exception.
1196
transport = MemoryTransport()
1197
transport.mkdir('branch')
1198
transport = transport.clone('branch')
1199
client = FakeClient(transport.base)
1200
# get_stacked_on_url
1201
client.add_error_response('NotStacked')
1203
client.add_success_response('ok', 'branch token', 'repo token')
1205
client.add_error_response('NoSuchRevision', 'revid')
1207
client.add_success_response('ok')
1209
branch = self.make_remote_branch(transport, client)
1210
# Lock the branch, reset the record of remote calls.
1215
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
1218
def lock_remote_branch(self, branch):
1219
"""Trick a RemoteBranch into thinking it is locked."""
1220
branch._lock_mode = 'w'
1221
branch._lock_count = 2
1222
branch._lock_token = 'branch token'
1223
branch._repo_lock_token = 'repo token'
1224
branch.repository._lock_mode = 'w'
1225
branch.repository._lock_count = 2
1226
branch.repository._lock_token = 'repo token'
1228
def test_backwards_compatibility(self):
1229
"""If the server does not support the Branch.set_last_revision_info
1230
verb (which is new in 1.4), then the client falls back to VFS methods.
1232
# This test is a little messy. Unlike most tests in this file, it
1233
# doesn't purely test what a Remote* object sends over the wire, and
1234
# how it reacts to responses from the wire. It instead relies partly
1235
# on asserting that the RemoteBranch will call
1236
# self._real_branch.set_last_revision_info(...).
1238
# First, set up our RemoteBranch with a FakeClient that raises
1239
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
1240
transport = MemoryTransport()
1241
transport.mkdir('branch')
1242
transport = transport.clone('branch')
1243
client = FakeClient(transport.base)
1244
client.add_expected_call(
1245
'Branch.get_stacked_on_url', ('branch/',),
1246
'error', ('NotStacked',))
1247
client.add_expected_call(
1248
'Branch.last_revision_info',
1250
'success', ('ok', '0', 'null:'))
1251
client.add_expected_call(
1252
'Branch.set_last_revision_info',
1253
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
1254
'unknown', 'Branch.set_last_revision_info')
1256
branch = self.make_remote_branch(transport, client)
1257
class StubRealBranch(object):
1260
def set_last_revision_info(self, revno, revision_id):
1262
('set_last_revision_info', revno, revision_id))
1263
def _clear_cached_state(self):
1265
real_branch = StubRealBranch()
1266
branch._real_branch = real_branch
1267
self.lock_remote_branch(branch)
1269
# Call set_last_revision_info, and verify it behaved as expected.
1270
result = branch.set_last_revision_info(1234, 'a-revision-id')
1272
[('set_last_revision_info', 1234, 'a-revision-id')],
1274
client.finished_test()
1276
def test_unexpected_error(self):
1277
# If the server sends an error the client doesn't understand, it gets
1278
# turned into an UnknownErrorFromSmartServer, which is presented as a
1279
# non-internal error to the user.
1280
transport = MemoryTransport()
1281
transport.mkdir('branch')
1282
transport = transport.clone('branch')
1283
client = FakeClient(transport.base)
1284
# get_stacked_on_url
1285
client.add_error_response('NotStacked')
1287
client.add_success_response('ok', 'branch token', 'repo token')
1289
client.add_error_response('UnexpectedError')
1291
client.add_success_response('ok')
1293
branch = self.make_remote_branch(transport, client)
1294
# Lock the branch, reset the record of remote calls.
1298
err = self.assertRaises(
1299
errors.UnknownErrorFromSmartServer,
1300
branch.set_last_revision_info, 123, 'revid')
1301
self.assertEqual(('UnexpectedError',), err.error_tuple)
1304
def test_tip_change_rejected(self):
1305
"""TipChangeRejected responses cause a TipChangeRejected exception to
1308
transport = MemoryTransport()
1309
transport.mkdir('branch')
1310
transport = transport.clone('branch')
1311
client = FakeClient(transport.base)
1312
# get_stacked_on_url
1313
client.add_error_response('NotStacked')
1315
client.add_success_response('ok', 'branch token', 'repo token')
1317
client.add_error_response('TipChangeRejected', 'rejection message')
1319
client.add_success_response('ok')
1321
branch = self.make_remote_branch(transport, client)
1322
# Lock the branch, reset the record of remote calls.
1324
self.addCleanup(branch.unlock)
1327
# The 'TipChangeRejected' error response triggered by calling
1328
# set_last_revision_info causes a TipChangeRejected exception.
1329
err = self.assertRaises(
1330
errors.TipChangeRejected,
1331
branch.set_last_revision_info, 123, 'revid')
1332
self.assertEqual('rejection message', err.msg)
1335
class TestBranchGetSetConfig(RemoteBranchTestCase):
1337
def test_get_branch_conf(self):
1338
# in an empty branch we decode the response properly
1339
client = FakeClient()
1340
client.add_expected_call(
1341
'Branch.get_stacked_on_url', ('memory:///',),
1342
'error', ('NotStacked',),)
1343
client.add_success_response_with_body('# config file body', 'ok')
1344
transport = MemoryTransport()
1345
branch = self.make_remote_branch(transport, client)
1346
config = branch.get_config()
1347
config.has_explicit_nickname()
1349
[('call', 'Branch.get_stacked_on_url', ('memory:///',)),
1350
('call_expecting_body', 'Branch.get_config_file', ('memory:///',))],
1353
def test_set_option(self):
1354
client = FakeClient()
1355
client.add_expected_call(
1356
'Branch.get_stacked_on_url', ('memory:///',),
1357
'error', ('NotStacked',),)
1358
client.add_expected_call(
1359
'Branch.lock_write', ('memory:///', '', ''),
1360
'success', ('ok', 'branch token', 'repo token'))
1361
client.add_expected_call(
1362
'Branch.set_config_option', ('memory:///', 'branch token',
1363
'repo token', 'foo', 'bar', ''),
1365
client.add_expected_call(
1366
'Branch.unlock', ('memory:///', 'branch token', 'repo token'),
1368
transport = MemoryTransport()
1369
branch = self.make_remote_branch(transport, client)
1371
config = branch._get_config()
1372
config.set_option('foo', 'bar')
1374
client.finished_test()
1376
def test_backwards_compat_set_option(self):
1377
self.setup_smart_server_with_call_log()
1378
branch = self.make_branch('.')
1379
verb = 'Branch.set_config_option'
1380
self.disable_verb(verb)
1382
self.addCleanup(branch.unlock)
1383
self.reset_smart_call_log()
1384
branch._get_config().set_option('value', 'name')
1385
self.assertLength(10, self.hpss_calls)
1386
self.assertEqual('value', branch._get_config().get_option('name'))
1389
class TestBranchLockWrite(RemoteBranchTestCase):
1391
def test_lock_write_unlockable(self):
1392
transport = MemoryTransport()
1393
client = FakeClient(transport.base)
1394
client.add_expected_call(
1395
'Branch.get_stacked_on_url', ('quack/',),
1396
'error', ('NotStacked',),)
1397
client.add_expected_call(
1398
'Branch.lock_write', ('quack/', '', ''),
1399
'error', ('UnlockableTransport',))
1400
transport.mkdir('quack')
1401
transport = transport.clone('quack')
1402
branch = self.make_remote_branch(transport, client)
1403
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
1404
client.finished_test()
1407
class TestTransportIsReadonly(tests.TestCase):
1409
def test_true(self):
1410
client = FakeClient()
1411
client.add_success_response('yes')
1412
transport = RemoteTransport('bzr://example.com/', medium=False,
1414
self.assertEqual(True, transport.is_readonly())
1416
[('call', 'Transport.is_readonly', ())],
1419
def test_false(self):
1420
client = FakeClient()
1421
client.add_success_response('no')
1422
transport = RemoteTransport('bzr://example.com/', medium=False,
1424
self.assertEqual(False, transport.is_readonly())
1426
[('call', 'Transport.is_readonly', ())],
1429
def test_error_from_old_server(self):
1430
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1432
Clients should treat it as a "no" response, because is_readonly is only
1433
advisory anyway (a transport could be read-write, but then the
1434
underlying filesystem could be readonly anyway).
1436
client = FakeClient()
1437
client.add_unknown_method_response('Transport.is_readonly')
1438
transport = RemoteTransport('bzr://example.com/', medium=False,
1440
self.assertEqual(False, transport.is_readonly())
1442
[('call', 'Transport.is_readonly', ())],
1446
class TestTransportMkdir(tests.TestCase):
1448
def test_permissiondenied(self):
1449
client = FakeClient()
1450
client.add_error_response('PermissionDenied', 'remote path', 'extra')
1451
transport = RemoteTransport('bzr://example.com/', medium=False,
1453
exc = self.assertRaises(
1454
errors.PermissionDenied, transport.mkdir, 'client path')
1455
expected_error = errors.PermissionDenied('/client path', 'extra')
1456
self.assertEqual(expected_error, exc)
1459
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1461
def test_defaults_to_none(self):
1462
t = RemoteSSHTransport('bzr+ssh://example.com')
1463
self.assertIs(None, t._get_credentials()[0])
1465
def test_uses_authentication_config(self):
1466
conf = config.AuthenticationConfig()
1467
conf._get_config().update(
1468
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1471
t = RemoteSSHTransport('bzr+ssh://example.com')
1472
self.assertEqual('bar', t._get_credentials()[0])
1475
class TestRemoteRepository(TestRemote):
1476
"""Base for testing RemoteRepository protocol usage.
1478
These tests contain frozen requests and responses. We want any changes to
1479
what is sent or expected to be require a thoughtful update to these tests
1480
because they might break compatibility with different-versioned servers.
1483
def setup_fake_client_and_repository(self, transport_path):
1484
"""Create the fake client and repository for testing with.
1486
There's no real server here; we just have canned responses sent
1489
:param transport_path: Path below the root of the MemoryTransport
1490
where the repository will be created.
1492
transport = MemoryTransport()
1493
transport.mkdir(transport_path)
1494
client = FakeClient(transport.base)
1495
transport = transport.clone(transport_path)
1496
# we do not want bzrdir to make any remote calls
1497
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
1499
repo = RemoteRepository(bzrdir, None, _client=client)
1503
class TestRepositoryFormat(TestRemoteRepository):
1505
def test_fast_delta(self):
1506
true_name = pack_repo.RepositoryFormatPackDevelopment2().network_name()
1507
true_format = RemoteRepositoryFormat()
1508
true_format._network_name = true_name
1509
self.assertEqual(True, true_format.fast_deltas)
1510
false_name = pack_repo.RepositoryFormatKnitPack1().network_name()
1511
false_format = RemoteRepositoryFormat()
1512
false_format._network_name = false_name
1513
self.assertEqual(False, false_format.fast_deltas)
1516
class TestRepositoryGatherStats(TestRemoteRepository):
1518
def test_revid_none(self):
1519
# ('ok',), body with revisions and size
1520
transport_path = 'quack'
1521
repo, client = self.setup_fake_client_and_repository(transport_path)
1522
client.add_success_response_with_body(
1523
'revisions: 2\nsize: 18\n', 'ok')
1524
result = repo.gather_stats(None)
1526
[('call_expecting_body', 'Repository.gather_stats',
1527
('quack/','','no'))],
1529
self.assertEqual({'revisions': 2, 'size': 18}, result)
1531
def test_revid_no_committers(self):
1532
# ('ok',), body without committers
1533
body = ('firstrev: 123456.300 3600\n'
1534
'latestrev: 654231.400 0\n'
1537
transport_path = 'quick'
1538
revid = u'\xc8'.encode('utf8')
1539
repo, client = self.setup_fake_client_and_repository(transport_path)
1540
client.add_success_response_with_body(body, 'ok')
1541
result = repo.gather_stats(revid)
1543
[('call_expecting_body', 'Repository.gather_stats',
1544
('quick/', revid, 'no'))],
1546
self.assertEqual({'revisions': 2, 'size': 18,
1547
'firstrev': (123456.300, 3600),
1548
'latestrev': (654231.400, 0),},
1551
def test_revid_with_committers(self):
1552
# ('ok',), body with committers
1553
body = ('committers: 128\n'
1554
'firstrev: 123456.300 3600\n'
1555
'latestrev: 654231.400 0\n'
1558
transport_path = 'buick'
1559
revid = u'\xc8'.encode('utf8')
1560
repo, client = self.setup_fake_client_and_repository(transport_path)
1561
client.add_success_response_with_body(body, 'ok')
1562
result = repo.gather_stats(revid, True)
1564
[('call_expecting_body', 'Repository.gather_stats',
1565
('buick/', revid, 'yes'))],
1567
self.assertEqual({'revisions': 2, 'size': 18,
1569
'firstrev': (123456.300, 3600),
1570
'latestrev': (654231.400, 0),},
1574
class TestRepositoryGetGraph(TestRemoteRepository):
1576
def test_get_graph(self):
1577
# get_graph returns a graph with a custom parents provider.
1578
transport_path = 'quack'
1579
repo, client = self.setup_fake_client_and_repository(transport_path)
1580
graph = repo.get_graph()
1581
self.assertNotEqual(graph._parents_provider, repo)
1584
class TestRepositoryGetParentMap(TestRemoteRepository):
1586
def test_get_parent_map_caching(self):
1587
# get_parent_map returns from cache until unlock()
1588
# setup a reponse with two revisions
1589
r1 = u'\u0e33'.encode('utf8')
1590
r2 = u'\u0dab'.encode('utf8')
1591
lines = [' '.join([r2, r1]), r1]
1592
encoded_body = bz2.compress('\n'.join(lines))
1594
transport_path = 'quack'
1595
repo, client = self.setup_fake_client_and_repository(transport_path)
1596
client.add_success_response_with_body(encoded_body, 'ok')
1597
client.add_success_response_with_body(encoded_body, 'ok')
1599
graph = repo.get_graph()
1600
parents = graph.get_parent_map([r2])
1601
self.assertEqual({r2: (r1,)}, parents)
1602
# locking and unlocking deeper should not reset
1605
parents = graph.get_parent_map([r1])
1606
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1608
[('call_with_body_bytes_expecting_body',
1609
'Repository.get_parent_map', ('quack/', 'include-missing:', r2),
1613
# now we call again, and it should use the second response.
1615
graph = repo.get_graph()
1616
parents = graph.get_parent_map([r1])
1617
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1619
[('call_with_body_bytes_expecting_body',
1620
'Repository.get_parent_map', ('quack/', 'include-missing:', r2),
1622
('call_with_body_bytes_expecting_body',
1623
'Repository.get_parent_map', ('quack/', 'include-missing:', r1),
1629
def test_get_parent_map_reconnects_if_unknown_method(self):
1630
transport_path = 'quack'
1631
rev_id = 'revision-id'
1632
repo, client = self.setup_fake_client_and_repository(transport_path)
1633
client.add_unknown_method_response('Repository.get_parent_map')
1634
client.add_success_response_with_body(rev_id, 'ok')
1635
self.assertFalse(client._medium._is_remote_before((1, 2)))
1636
parents = repo.get_parent_map([rev_id])
1638
[('call_with_body_bytes_expecting_body',
1639
'Repository.get_parent_map', ('quack/', 'include-missing:',
1641
('disconnect medium',),
1642
('call_expecting_body', 'Repository.get_revision_graph',
1645
# The medium is now marked as being connected to an older server
1646
self.assertTrue(client._medium._is_remote_before((1, 2)))
1647
self.assertEqual({rev_id: ('null:',)}, parents)
1649
def test_get_parent_map_fallback_parentless_node(self):
1650
"""get_parent_map falls back to get_revision_graph on old servers. The
1651
results from get_revision_graph are tweaked to match the get_parent_map
1654
Specifically, a {key: ()} result from get_revision_graph means "no
1655
parents" for that key, which in get_parent_map results should be
1656
represented as {key: ('null:',)}.
1658
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1660
rev_id = 'revision-id'
1661
transport_path = 'quack'
1662
repo, client = self.setup_fake_client_and_repository(transport_path)
1663
client.add_success_response_with_body(rev_id, 'ok')
1664
client._medium._remember_remote_is_before((1, 2))
1665
parents = repo.get_parent_map([rev_id])
1667
[('call_expecting_body', 'Repository.get_revision_graph',
1670
self.assertEqual({rev_id: ('null:',)}, parents)
1672
def test_get_parent_map_unexpected_response(self):
1673
repo, client = self.setup_fake_client_and_repository('path')
1674
client.add_success_response('something unexpected!')
1676
errors.UnexpectedSmartServerResponse,
1677
repo.get_parent_map, ['a-revision-id'])
1679
def test_get_parent_map_negative_caches_missing_keys(self):
1680
self.setup_smart_server_with_call_log()
1681
repo = self.make_repository('foo')
1682
self.assertIsInstance(repo, RemoteRepository)
1684
self.addCleanup(repo.unlock)
1685
self.reset_smart_call_log()
1686
graph = repo.get_graph()
1687
self.assertEqual({},
1688
graph.get_parent_map(['some-missing', 'other-missing']))
1689
self.assertLength(1, self.hpss_calls)
1690
# No call if we repeat this
1691
self.reset_smart_call_log()
1692
graph = repo.get_graph()
1693
self.assertEqual({},
1694
graph.get_parent_map(['some-missing', 'other-missing']))
1695
self.assertLength(0, self.hpss_calls)
1696
# Asking for more unknown keys makes a request.
1697
self.reset_smart_call_log()
1698
graph = repo.get_graph()
1699
self.assertEqual({},
1700
graph.get_parent_map(['some-missing', 'other-missing',
1702
self.assertLength(1, self.hpss_calls)
1704
def disableExtraResults(self):
1705
old_flag = SmartServerRepositoryGetParentMap.no_extra_results
1706
SmartServerRepositoryGetParentMap.no_extra_results = True
1708
SmartServerRepositoryGetParentMap.no_extra_results = old_flag
1709
self.addCleanup(reset_values)
1711
def test_null_cached_missing_and_stop_key(self):
1712
self.setup_smart_server_with_call_log()
1713
# Make a branch with a single revision.
1714
builder = self.make_branch_builder('foo')
1715
builder.start_series()
1716
builder.build_snapshot('first', None, [
1717
('add', ('', 'root-id', 'directory', ''))])
1718
builder.finish_series()
1719
branch = builder.get_branch()
1720
repo = branch.repository
1721
self.assertIsInstance(repo, RemoteRepository)
1722
# Stop the server from sending extra results.
1723
self.disableExtraResults()
1725
self.addCleanup(repo.unlock)
1726
self.reset_smart_call_log()
1727
graph = repo.get_graph()
1728
# Query for 'first' and 'null:'. Because 'null:' is a parent of
1729
# 'first' it will be a candidate for the stop_keys of subsequent
1730
# requests, and because 'null:' was queried but not returned it will be
1731
# cached as missing.
1732
self.assertEqual({'first': ('null:',)},
1733
graph.get_parent_map(['first', 'null:']))
1734
# Now query for another key. This request will pass along a recipe of
1735
# start and stop keys describing the already cached results, and this
1736
# recipe's revision count must be correct (or else it will trigger an
1737
# error from the server).
1738
self.assertEqual({}, graph.get_parent_map(['another-key']))
1739
# This assertion guards against disableExtraResults silently failing to
1740
# work, thus invalidating the test.
1741
self.assertLength(2, self.hpss_calls)
1743
def test_get_parent_map_gets_ghosts_from_result(self):
1744
# asking for a revision should negatively cache close ghosts in its
1746
self.setup_smart_server_with_call_log()
1747
tree = self.make_branch_and_memory_tree('foo')
1750
builder = treebuilder.TreeBuilder()
1751
builder.start_tree(tree)
1753
builder.finish_tree()
1754
tree.set_parent_ids(['non-existant'], allow_leftmost_as_ghost=True)
1755
rev_id = tree.commit('')
1759
self.addCleanup(tree.unlock)
1760
repo = tree.branch.repository
1761
self.assertIsInstance(repo, RemoteRepository)
1763
repo.get_parent_map([rev_id])
1764
self.reset_smart_call_log()
1765
# Now asking for rev_id's ghost parent should not make calls
1766
self.assertEqual({}, repo.get_parent_map(['non-existant']))
1767
self.assertLength(0, self.hpss_calls)
1770
class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
1772
def test_allows_new_revisions(self):
1773
"""get_parent_map's results can be updated by commit."""
1774
smart_server = server.SmartTCPServer_for_testing()
1775
smart_server.setUp()
1776
self.addCleanup(smart_server.tearDown)
1777
self.make_branch('branch')
1778
branch = Branch.open(smart_server.get_url() + '/branch')
1779
tree = branch.create_checkout('tree', lightweight=True)
1781
self.addCleanup(tree.unlock)
1782
graph = tree.branch.repository.get_graph()
1783
# This provides an opportunity for the missing rev-id to be cached.
1784
self.assertEqual({}, graph.get_parent_map(['rev1']))
1785
tree.commit('message', rev_id='rev1')
1786
graph = tree.branch.repository.get_graph()
1787
self.assertEqual({'rev1': ('null:',)}, graph.get_parent_map(['rev1']))
1790
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1792
def test_null_revision(self):
1793
# a null revision has the predictable result {}, we should have no wire
1794
# traffic when calling it with this argument
1795
transport_path = 'empty'
1796
repo, client = self.setup_fake_client_and_repository(transport_path)
1797
client.add_success_response('notused')
1798
# actual RemoteRepository.get_revision_graph is gone, but there's an
1799
# equivalent private method for testing
1800
result = repo._get_revision_graph(NULL_REVISION)
1801
self.assertEqual([], client._calls)
1802
self.assertEqual({}, result)
1804
def test_none_revision(self):
1805
# with none we want the entire graph
1806
r1 = u'\u0e33'.encode('utf8')
1807
r2 = u'\u0dab'.encode('utf8')
1808
lines = [' '.join([r2, r1]), r1]
1809
encoded_body = '\n'.join(lines)
1811
transport_path = 'sinhala'
1812
repo, client = self.setup_fake_client_and_repository(transport_path)
1813
client.add_success_response_with_body(encoded_body, 'ok')
1814
# actual RemoteRepository.get_revision_graph is gone, but there's an
1815
# equivalent private method for testing
1816
result = repo._get_revision_graph(None)
1818
[('call_expecting_body', 'Repository.get_revision_graph',
1821
self.assertEqual({r1: (), r2: (r1, )}, result)
1823
def test_specific_revision(self):
1824
# with a specific revision we want the graph for that
1825
# with none we want the entire graph
1826
r11 = u'\u0e33'.encode('utf8')
1827
r12 = u'\xc9'.encode('utf8')
1828
r2 = u'\u0dab'.encode('utf8')
1829
lines = [' '.join([r2, r11, r12]), r11, r12]
1830
encoded_body = '\n'.join(lines)
1832
transport_path = 'sinhala'
1833
repo, client = self.setup_fake_client_and_repository(transport_path)
1834
client.add_success_response_with_body(encoded_body, 'ok')
1835
result = repo._get_revision_graph(r2)
1837
[('call_expecting_body', 'Repository.get_revision_graph',
1840
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1842
def test_no_such_revision(self):
1844
transport_path = 'sinhala'
1845
repo, client = self.setup_fake_client_and_repository(transport_path)
1846
client.add_error_response('nosuchrevision', revid)
1847
# also check that the right revision is reported in the error
1848
self.assertRaises(errors.NoSuchRevision,
1849
repo._get_revision_graph, revid)
1851
[('call_expecting_body', 'Repository.get_revision_graph',
1852
('sinhala/', revid))],
1855
def test_unexpected_error(self):
1857
transport_path = 'sinhala'
1858
repo, client = self.setup_fake_client_and_repository(transport_path)
1859
client.add_error_response('AnUnexpectedError')
1860
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1861
repo._get_revision_graph, revid)
1862
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1865
class TestRepositoryIsShared(TestRemoteRepository):
1867
def test_is_shared(self):
1868
# ('yes', ) for Repository.is_shared -> 'True'.
1869
transport_path = 'quack'
1870
repo, client = self.setup_fake_client_and_repository(transport_path)
1871
client.add_success_response('yes')
1872
result = repo.is_shared()
1874
[('call', 'Repository.is_shared', ('quack/',))],
1876
self.assertEqual(True, result)
1878
def test_is_not_shared(self):
1879
# ('no', ) for Repository.is_shared -> 'False'.
1880
transport_path = 'qwack'
1881
repo, client = self.setup_fake_client_and_repository(transport_path)
1882
client.add_success_response('no')
1883
result = repo.is_shared()
1885
[('call', 'Repository.is_shared', ('qwack/',))],
1887
self.assertEqual(False, result)
1890
class TestRepositoryLockWrite(TestRemoteRepository):
1892
def test_lock_write(self):
1893
transport_path = 'quack'
1894
repo, client = self.setup_fake_client_and_repository(transport_path)
1895
client.add_success_response('ok', 'a token')
1896
result = repo.lock_write()
1898
[('call', 'Repository.lock_write', ('quack/', ''))],
1900
self.assertEqual('a token', result)
1902
def test_lock_write_already_locked(self):
1903
transport_path = 'quack'
1904
repo, client = self.setup_fake_client_and_repository(transport_path)
1905
client.add_error_response('LockContention')
1906
self.assertRaises(errors.LockContention, repo.lock_write)
1908
[('call', 'Repository.lock_write', ('quack/', ''))],
1911
def test_lock_write_unlockable(self):
1912
transport_path = 'quack'
1913
repo, client = self.setup_fake_client_and_repository(transport_path)
1914
client.add_error_response('UnlockableTransport')
1915
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1917
[('call', 'Repository.lock_write', ('quack/', ''))],
1921
class TestRepositorySetMakeWorkingTrees(TestRemoteRepository):
1923
def test_backwards_compat(self):
1924
self.setup_smart_server_with_call_log()
1925
repo = self.make_repository('.')
1926
self.reset_smart_call_log()
1927
verb = 'Repository.set_make_working_trees'
1928
self.disable_verb(verb)
1929
repo.set_make_working_trees(True)
1930
call_count = len([call for call in self.hpss_calls if
1931
call.call.method == verb])
1932
self.assertEqual(1, call_count)
1934
def test_current(self):
1935
transport_path = 'quack'
1936
repo, client = self.setup_fake_client_and_repository(transport_path)
1937
client.add_expected_call(
1938
'Repository.set_make_working_trees', ('quack/', 'True'),
1940
client.add_expected_call(
1941
'Repository.set_make_working_trees', ('quack/', 'False'),
1943
repo.set_make_working_trees(True)
1944
repo.set_make_working_trees(False)
1947
class TestRepositoryUnlock(TestRemoteRepository):
1949
def test_unlock(self):
1950
transport_path = 'quack'
1951
repo, client = self.setup_fake_client_and_repository(transport_path)
1952
client.add_success_response('ok', 'a token')
1953
client.add_success_response('ok')
1957
[('call', 'Repository.lock_write', ('quack/', '')),
1958
('call', 'Repository.unlock', ('quack/', 'a token'))],
1961
def test_unlock_wrong_token(self):
1962
# If somehow the token is wrong, unlock will raise TokenMismatch.
1963
transport_path = 'quack'
1964
repo, client = self.setup_fake_client_and_repository(transport_path)
1965
client.add_success_response('ok', 'a token')
1966
client.add_error_response('TokenMismatch')
1968
self.assertRaises(errors.TokenMismatch, repo.unlock)
1971
class TestRepositoryHasRevision(TestRemoteRepository):
1973
def test_none(self):
1974
# repo.has_revision(None) should not cause any traffic.
1975
transport_path = 'quack'
1976
repo, client = self.setup_fake_client_and_repository(transport_path)
1978
# The null revision is always there, so has_revision(None) == True.
1979
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1981
# The remote repo shouldn't be accessed.
1982
self.assertEqual([], client._calls)
1985
class TestRepositoryInsertStream(TestRemoteRepository):
1987
def test_unlocked_repo(self):
1988
transport_path = 'quack'
1989
repo, client = self.setup_fake_client_and_repository(transport_path)
1990
client.add_expected_call(
1991
'Repository.insert_stream', ('quack/', ''),
1993
client.add_expected_call(
1994
'Repository.insert_stream', ('quack/', ''),
1996
sink = repo._get_sink()
1997
fmt = repository.RepositoryFormat.get_default_format()
1998
resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
1999
self.assertEqual([], resume_tokens)
2000
self.assertEqual(set(), missing_keys)
2001
client.finished_test()
2003
def test_locked_repo_with_no_lock_token(self):
2004
transport_path = 'quack'
2005
repo, client = self.setup_fake_client_and_repository(transport_path)
2006
client.add_expected_call(
2007
'Repository.lock_write', ('quack/', ''),
2008
'success', ('ok', ''))
2009
client.add_expected_call(
2010
'Repository.insert_stream', ('quack/', ''),
2012
client.add_expected_call(
2013
'Repository.insert_stream', ('quack/', ''),
2016
sink = repo._get_sink()
2017
fmt = repository.RepositoryFormat.get_default_format()
2018
resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
2019
self.assertEqual([], resume_tokens)
2020
self.assertEqual(set(), missing_keys)
2021
client.finished_test()
2023
def test_locked_repo_with_lock_token(self):
2024
transport_path = 'quack'
2025
repo, client = self.setup_fake_client_and_repository(transport_path)
2026
client.add_expected_call(
2027
'Repository.lock_write', ('quack/', ''),
2028
'success', ('ok', 'a token'))
2029
client.add_expected_call(
2030
'Repository.insert_stream_locked', ('quack/', '', 'a token'),
2032
client.add_expected_call(
2033
'Repository.insert_stream_locked', ('quack/', '', 'a token'),
2036
sink = repo._get_sink()
2037
fmt = repository.RepositoryFormat.get_default_format()
2038
resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
2039
self.assertEqual([], resume_tokens)
2040
self.assertEqual(set(), missing_keys)
2041
client.finished_test()
2044
class TestRepositoryTarball(TestRemoteRepository):
2046
# This is a canned tarball reponse we can validate against
2048
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
2049
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
2050
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
2051
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
2052
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
2053
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
2054
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
2055
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
2056
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
2057
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
2058
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
2059
'nWQ7QH/F3JFOFCQ0aSPfA='
2062
def test_repository_tarball(self):
2063
# Test that Repository.tarball generates the right operations
2064
transport_path = 'repo'
2065
expected_calls = [('call_expecting_body', 'Repository.tarball',
2066
('repo/', 'bz2',),),
2068
repo, client = self.setup_fake_client_and_repository(transport_path)
2069
client.add_success_response_with_body(self.tarball_content, 'ok')
2070
# Now actually ask for the tarball
2071
tarball_file = repo._get_tarball('bz2')
2073
self.assertEqual(expected_calls, client._calls)
2074
self.assertEqual(self.tarball_content, tarball_file.read())
2076
tarball_file.close()
2079
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
2080
"""RemoteRepository.copy_content_into optimizations"""
2082
def test_copy_content_remote_to_local(self):
2083
self.transport_server = server.SmartTCPServer_for_testing
2084
src_repo = self.make_repository('repo1')
2085
src_repo = repository.Repository.open(self.get_url('repo1'))
2086
# At the moment the tarball-based copy_content_into can't write back
2087
# into a smart server. It would be good if it could upload the
2088
# tarball; once that works we'd have to create repositories of
2089
# different formats. -- mbp 20070410
2090
dest_url = self.get_vfs_only_url('repo2')
2091
dest_bzrdir = BzrDir.create(dest_url)
2092
dest_repo = dest_bzrdir.create_repository()
2093
self.assertFalse(isinstance(dest_repo, RemoteRepository))
2094
self.assertTrue(isinstance(src_repo, RemoteRepository))
2095
src_repo.copy_content_into(dest_repo)
2098
class _StubRealPackRepository(object):
2100
def __init__(self, calls):
2102
self._pack_collection = _StubPackCollection(calls)
2104
def is_in_write_group(self):
2107
def refresh_data(self):
2108
self.calls.append(('pack collection reload_pack_names',))
2111
class _StubPackCollection(object):
2113
def __init__(self, calls):
2117
self.calls.append(('pack collection autopack',))
2120
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
2121
"""Tests for RemoteRepository.autopack implementation."""
2124
"""When the server returns 'ok' and there's no _real_repository, then
2125
nothing else happens: the autopack method is done.
2127
transport_path = 'quack'
2128
repo, client = self.setup_fake_client_and_repository(transport_path)
2129
client.add_expected_call(
2130
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
2132
client.finished_test()
2134
def test_ok_with_real_repo(self):
2135
"""When the server returns 'ok' and there is a _real_repository, then
2136
the _real_repository's reload_pack_name's method will be called.
2138
transport_path = 'quack'
2139
repo, client = self.setup_fake_client_and_repository(transport_path)
2140
client.add_expected_call(
2141
'PackRepository.autopack', ('quack/',),
2143
repo._real_repository = _StubRealPackRepository(client._calls)
2146
[('call', 'PackRepository.autopack', ('quack/',)),
2147
('pack collection reload_pack_names',)],
2150
def test_backwards_compatibility(self):
2151
"""If the server does not recognise the PackRepository.autopack verb,
2152
fallback to the real_repository's implementation.
2154
transport_path = 'quack'
2155
repo, client = self.setup_fake_client_and_repository(transport_path)
2156
client.add_unknown_method_response('PackRepository.autopack')
2157
def stub_ensure_real():
2158
client._calls.append(('_ensure_real',))
2159
repo._real_repository = _StubRealPackRepository(client._calls)
2160
repo._ensure_real = stub_ensure_real
2163
[('call', 'PackRepository.autopack', ('quack/',)),
2165
('pack collection autopack',)],
2169
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
2170
"""Base class for unit tests for bzrlib.remote._translate_error."""
2172
def translateTuple(self, error_tuple, **context):
2173
"""Call _translate_error with an ErrorFromSmartServer built from the
2176
:param error_tuple: A tuple of a smart server response, as would be
2177
passed to an ErrorFromSmartServer.
2178
:kwargs context: context items to call _translate_error with.
2180
:returns: The error raised by _translate_error.
2182
# Raise the ErrorFromSmartServer before passing it as an argument,
2183
# because _translate_error may need to re-raise it with a bare 'raise'
2185
server_error = errors.ErrorFromSmartServer(error_tuple)
2186
translated_error = self.translateErrorFromSmartServer(
2187
server_error, **context)
2188
return translated_error
2190
def translateErrorFromSmartServer(self, error_object, **context):
2191
"""Like translateTuple, but takes an already constructed
2192
ErrorFromSmartServer rather than a tuple.
2196
except errors.ErrorFromSmartServer, server_error:
2197
translated_error = self.assertRaises(
2198
errors.BzrError, remote._translate_error, server_error,
2200
return translated_error
2203
class TestErrorTranslationSuccess(TestErrorTranslationBase):
2204
"""Unit tests for bzrlib.remote._translate_error.
2206
Given an ErrorFromSmartServer (which has an error tuple from a smart
2207
server) and some context, _translate_error raises more specific errors from
2210
This test case covers the cases where _translate_error succeeds in
2211
translating an ErrorFromSmartServer to something better. See
2212
TestErrorTranslationRobustness for other cases.
2215
def test_NoSuchRevision(self):
2216
branch = self.make_branch('')
2218
translated_error = self.translateTuple(
2219
('NoSuchRevision', revid), branch=branch)
2220
expected_error = errors.NoSuchRevision(branch, revid)
2221
self.assertEqual(expected_error, translated_error)
2223
def test_nosuchrevision(self):
2224
repository = self.make_repository('')
2226
translated_error = self.translateTuple(
2227
('nosuchrevision', revid), repository=repository)
2228
expected_error = errors.NoSuchRevision(repository, revid)
2229
self.assertEqual(expected_error, translated_error)
2231
def test_nobranch(self):
2232
bzrdir = self.make_bzrdir('')
2233
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
2234
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
2235
self.assertEqual(expected_error, translated_error)
2237
def test_LockContention(self):
2238
translated_error = self.translateTuple(('LockContention',))
2239
expected_error = errors.LockContention('(remote lock)')
2240
self.assertEqual(expected_error, translated_error)
2242
def test_UnlockableTransport(self):
2243
bzrdir = self.make_bzrdir('')
2244
translated_error = self.translateTuple(
2245
('UnlockableTransport',), bzrdir=bzrdir)
2246
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
2247
self.assertEqual(expected_error, translated_error)
2249
def test_LockFailed(self):
2250
lock = 'str() of a server lock'
2251
why = 'str() of why'
2252
translated_error = self.translateTuple(('LockFailed', lock, why))
2253
expected_error = errors.LockFailed(lock, why)
2254
self.assertEqual(expected_error, translated_error)
2256
def test_TokenMismatch(self):
2257
token = 'a lock token'
2258
translated_error = self.translateTuple(('TokenMismatch',), token=token)
2259
expected_error = errors.TokenMismatch(token, '(remote token)')
2260
self.assertEqual(expected_error, translated_error)
2262
def test_Diverged(self):
2263
branch = self.make_branch('a')
2264
other_branch = self.make_branch('b')
2265
translated_error = self.translateTuple(
2266
('Diverged',), branch=branch, other_branch=other_branch)
2267
expected_error = errors.DivergedBranches(branch, other_branch)
2268
self.assertEqual(expected_error, translated_error)
2270
def test_ReadError_no_args(self):
2272
translated_error = self.translateTuple(('ReadError',), path=path)
2273
expected_error = errors.ReadError(path)
2274
self.assertEqual(expected_error, translated_error)
2276
def test_ReadError(self):
2278
translated_error = self.translateTuple(('ReadError', path))
2279
expected_error = errors.ReadError(path)
2280
self.assertEqual(expected_error, translated_error)
2282
def test_PermissionDenied_no_args(self):
2284
translated_error = self.translateTuple(('PermissionDenied',), path=path)
2285
expected_error = errors.PermissionDenied(path)
2286
self.assertEqual(expected_error, translated_error)
2288
def test_PermissionDenied_one_arg(self):
2290
translated_error = self.translateTuple(('PermissionDenied', path))
2291
expected_error = errors.PermissionDenied(path)
2292
self.assertEqual(expected_error, translated_error)
2294
def test_PermissionDenied_one_arg_and_context(self):
2295
"""Given a choice between a path from the local context and a path on
2296
the wire, _translate_error prefers the path from the local context.
2298
local_path = 'local path'
2299
remote_path = 'remote path'
2300
translated_error = self.translateTuple(
2301
('PermissionDenied', remote_path), path=local_path)
2302
expected_error = errors.PermissionDenied(local_path)
2303
self.assertEqual(expected_error, translated_error)
2305
def test_PermissionDenied_two_args(self):
2307
extra = 'a string with extra info'
2308
translated_error = self.translateTuple(
2309
('PermissionDenied', path, extra))
2310
expected_error = errors.PermissionDenied(path, extra)
2311
self.assertEqual(expected_error, translated_error)
2314
class TestErrorTranslationRobustness(TestErrorTranslationBase):
2315
"""Unit tests for bzrlib.remote._translate_error's robustness.
2317
TestErrorTranslationSuccess is for cases where _translate_error can
2318
translate successfully. This class about how _translate_err behaves when
2319
it fails to translate: it re-raises the original error.
2322
def test_unrecognised_server_error(self):
2323
"""If the error code from the server is not recognised, the original
2324
ErrorFromSmartServer is propagated unmodified.
2326
error_tuple = ('An unknown error tuple',)
2327
server_error = errors.ErrorFromSmartServer(error_tuple)
2328
translated_error = self.translateErrorFromSmartServer(server_error)
2329
expected_error = errors.UnknownErrorFromSmartServer(server_error)
2330
self.assertEqual(expected_error, translated_error)
2332
def test_context_missing_a_key(self):
2333
"""In case of a bug in the client, or perhaps an unexpected response
2334
from a server, _translate_error returns the original error tuple from
2335
the server and mutters a warning.
2337
# To translate a NoSuchRevision error _translate_error needs a 'branch'
2338
# in the context dict. So let's give it an empty context dict instead
2339
# to exercise its error recovery.
2341
error_tuple = ('NoSuchRevision', 'revid')
2342
server_error = errors.ErrorFromSmartServer(error_tuple)
2343
translated_error = self.translateErrorFromSmartServer(server_error)
2344
self.assertEqual(server_error, translated_error)
2345
# In addition to re-raising ErrorFromSmartServer, some debug info has
2346
# been muttered to the log file for developer to look at.
2347
self.assertContainsRe(
2348
self._get_log(keep_log_file=True),
2349
"Missing key 'branch' in context")
2351
def test_path_missing(self):
2352
"""Some translations (PermissionDenied, ReadError) can determine the
2353
'path' variable from either the wire or the local context. If neither
2354
has it, then an error is raised.
2356
error_tuple = ('ReadError',)
2357
server_error = errors.ErrorFromSmartServer(error_tuple)
2358
translated_error = self.translateErrorFromSmartServer(server_error)
2359
self.assertEqual(server_error, translated_error)
2360
# In addition to re-raising ErrorFromSmartServer, some debug info has
2361
# been muttered to the log file for developer to look at.
2362
self.assertContainsRe(
2363
self._get_log(keep_log_file=True), "Missing key 'path' in context")
2366
class TestStacking(tests.TestCaseWithTransport):
2367
"""Tests for operations on stacked remote repositories.
2369
The underlying format type must support stacking.
2372
def test_access_stacked_remote(self):
2373
# based on <http://launchpad.net/bugs/261315>
2374
# make a branch stacked on another repository containing an empty
2375
# revision, then open it over hpss - we should be able to see that
2377
base_transport = self.get_transport()
2378
base_builder = self.make_branch_builder('base', format='1.9')
2379
base_builder.start_series()
2380
base_revid = base_builder.build_snapshot('rev-id', None,
2381
[('add', ('', None, 'directory', None))],
2383
base_builder.finish_series()
2384
stacked_branch = self.make_branch('stacked', format='1.9')
2385
stacked_branch.set_stacked_on_url('../base')
2386
# start a server looking at this
2387
smart_server = server.SmartTCPServer_for_testing()
2388
smart_server.setUp()
2389
self.addCleanup(smart_server.tearDown)
2390
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
2391
# can get its branch and repository
2392
remote_branch = remote_bzrdir.open_branch()
2393
remote_repo = remote_branch.repository
2394
remote_repo.lock_read()
2396
# it should have an appropriate fallback repository, which should also
2397
# be a RemoteRepository
2398
self.assertEquals(len(remote_repo._fallback_repositories), 1)
2399
self.assertIsInstance(remote_repo._fallback_repositories[0],
2401
# and it has the revision committed to the underlying repository;
2402
# these have varying implementations so we try several of them
2403
self.assertTrue(remote_repo.has_revisions([base_revid]))
2404
self.assertTrue(remote_repo.has_revision(base_revid))
2405
self.assertEqual(remote_repo.get_revision(base_revid).message,
2408
remote_repo.unlock()
2410
def prepare_stacked_remote_branch(self):
2411
"""Get stacked_upon and stacked branches with content in each."""
2412
self.setup_smart_server_with_call_log()
2413
tree1 = self.make_branch_and_tree('tree1', format='1.9')
2414
tree1.commit('rev1', rev_id='rev1')
2415
tree2 = tree1.branch.bzrdir.sprout('tree2', stacked=True
2416
).open_workingtree()
2417
tree2.commit('local changes make me feel good.')
2418
branch2 = Branch.open(self.get_url('tree2'))
2420
self.addCleanup(branch2.unlock)
2421
return tree1.branch, branch2
2423
def test_stacked_get_parent_map(self):
2424
# the public implementation of get_parent_map obeys stacking
2425
_, branch = self.prepare_stacked_remote_branch()
2426
repo = branch.repository
2427
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
2429
def test_unstacked_get_parent_map(self):
2430
# _unstacked_provider.get_parent_map ignores stacking
2431
_, branch = self.prepare_stacked_remote_branch()
2432
provider = branch.repository._unstacked_provider
2433
self.assertEqual([], provider.get_parent_map(['rev1']).keys())
2435
def fetch_stream_to_rev_order(self, stream):
2437
for kind, substream in stream:
2438
if not kind == 'revisions':
2441
for content in substream:
2442
result.append(content.key[-1])
2445
def get_ordered_revs(self, format, order):
2446
"""Get a list of the revisions in a stream to format format.
2448
:param format: The format of the target.
2449
:param order: the order that target should have requested.
2450
:result: The revision ids in the stream, in the order seen,
2451
the topological order of revisions in the source.
2453
unordered_format = bzrdir.format_registry.get(format)()
2454
target_repository_format = unordered_format.repository_format
2456
self.assertEqual(order, target_repository_format._fetch_order)
2457
trunk, stacked = self.prepare_stacked_remote_branch()
2458
source = stacked.repository._get_source(target_repository_format)
2459
tip = stacked.last_revision()
2460
revs = stacked.repository.get_ancestry(tip)
2461
search = graph.PendingAncestryResult([tip], stacked.repository)
2462
self.reset_smart_call_log()
2463
stream = source.get_stream(search)
2466
# We trust that if a revision is in the stream the rest of the new
2467
# content for it is too, as per our main fetch tests; here we are
2468
# checking that the revisions are actually included at all, and their
2470
return self.fetch_stream_to_rev_order(stream), revs
2472
def test_stacked_get_stream_unordered(self):
2473
# Repository._get_source.get_stream() from a stacked repository with
2474
# unordered yields the full data from both stacked and stacked upon
2476
rev_ord, expected_revs = self.get_ordered_revs('1.9', 'unordered')
2477
self.assertEqual(set(expected_revs), set(rev_ord))
2478
# Getting unordered results should have made a streaming data request
2479
# from the server, then one from the backing branch.
2480
self.assertLength(2, self.hpss_calls)
2482
def test_stacked_get_stream_topological(self):
2483
# Repository._get_source.get_stream() from a stacked repository with
2484
# topological sorting yields the full data from both stacked and
2485
# stacked upon sources in topological order.
2486
rev_ord, expected_revs = self.get_ordered_revs('knit', 'topological')
2487
self.assertEqual(expected_revs, rev_ord)
2488
# Getting topological sort requires VFS calls still
2489
self.assertLength(12, self.hpss_calls)
2491
def test_stacked_get_stream_groupcompress(self):
2492
# Repository._get_source.get_stream() from a stacked repository with
2493
# groupcompress sorting yields the full data from both stacked and
2494
# stacked upon sources in groupcompress order.
2495
raise tests.TestSkipped('No groupcompress ordered format available')
2496
rev_ord, expected_revs = self.get_ordered_revs('dev5', 'groupcompress')
2497
self.assertEqual(expected_revs, reversed(rev_ord))
2498
# Getting unordered results should have made a streaming data request
2499
# from the backing branch, and one from the stacked on branch.
2500
self.assertLength(2, self.hpss_calls)
2503
class TestRemoteBranchEffort(tests.TestCaseWithTransport):
2506
super(TestRemoteBranchEffort, self).setUp()
2507
# Create a smart server that publishes whatever the backing VFS server
2509
self.smart_server = server.SmartTCPServer_for_testing()
2510
self.smart_server.setUp(self.get_server())
2511
self.addCleanup(self.smart_server.tearDown)
2512
# Log all HPSS calls into self.hpss_calls.
2513
_SmartClient.hooks.install_named_hook(
2514
'call', self.capture_hpss_call, None)
2515
self.hpss_calls = []
2517
def capture_hpss_call(self, params):
2518
self.hpss_calls.append(params.method)
2520
def test_copy_content_into_avoids_revision_history(self):
2521
local = self.make_branch('local')
2522
remote_backing_tree = self.make_branch_and_tree('remote')
2523
remote_backing_tree.commit("Commit.")
2524
remote_branch_url = self.smart_server.get_url() + 'remote'
2525
remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
2526
local.repository.fetch(remote_branch.repository)
2527
self.hpss_calls = []
2528
remote_branch.copy_content_into(local)
2529
self.assertFalse('Branch.revision_history' in self.hpss_calls)