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
43
from bzrlib.branch import Branch
44
from bzrlib.bzrdir import BzrDir, BzrDirFormat
45
from bzrlib.remote import (
51
RemoteRepositoryFormat,
53
from bzrlib.repofmt import groupcompress_repo, pack_repo
54
from bzrlib.revision import NULL_REVISION
55
from bzrlib.smart import server, medium
56
from bzrlib.smart.client import _SmartClient
57
from bzrlib.smart.repository import SmartServerRepositoryGetParentMap
58
from bzrlib.tests import (
60
split_suite_by_condition,
63
from bzrlib.transport import get_transport, http
64
from bzrlib.transport.memory import MemoryTransport
65
from bzrlib.transport.remote import (
71
def load_tests(standard_tests, module, loader):
72
to_adapt, result = split_suite_by_condition(
73
standard_tests, condition_isinstance(BasicRemoteObjectTests))
74
smart_server_version_scenarios = [
76
{'transport_server': server.SmartTCPServer_for_testing_v2_only}),
78
{'transport_server': server.SmartTCPServer_for_testing})]
79
return multiply_tests(to_adapt, smart_server_version_scenarios, result)
82
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
85
super(BasicRemoteObjectTests, self).setUp()
86
self.transport = self.get_transport()
87
# make a branch that can be opened over the smart transport
88
self.local_wt = BzrDir.create_standalone_workingtree('.')
91
self.transport.disconnect()
92
tests.TestCaseWithTransport.tearDown(self)
94
def test_create_remote_bzrdir(self):
95
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
96
self.assertIsInstance(b, BzrDir)
98
def test_open_remote_branch(self):
99
# open a standalone branch in the working directory
100
b = remote.RemoteBzrDir(self.transport, remote.RemoteBzrDirFormat())
101
branch = b.open_branch()
102
self.assertIsInstance(branch, Branch)
104
def test_remote_repository(self):
105
b = BzrDir.open_from_transport(self.transport)
106
repo = b.open_repository()
107
revid = u'\xc823123123'.encode('utf8')
108
self.assertFalse(repo.has_revision(revid))
109
self.local_wt.commit(message='test commit', rev_id=revid)
110
self.assertTrue(repo.has_revision(revid))
112
def test_remote_branch_revision_history(self):
113
b = BzrDir.open_from_transport(self.transport).open_branch()
114
self.assertEqual([], b.revision_history())
115
r1 = self.local_wt.commit('1st commit')
116
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
117
self.assertEqual([r1, r2], b.revision_history())
119
def test_find_correct_format(self):
120
"""Should open a RemoteBzrDir over a RemoteTransport"""
121
fmt = BzrDirFormat.find_format(self.transport)
122
self.assertTrue(RemoteBzrDirFormat
123
in BzrDirFormat._control_server_formats)
124
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
126
def test_open_detected_smart_format(self):
127
fmt = BzrDirFormat.find_format(self.transport)
128
d = fmt.open(self.transport)
129
self.assertIsInstance(d, BzrDir)
131
def test_remote_branch_repr(self):
132
b = BzrDir.open_from_transport(self.transport).open_branch()
133
self.assertStartsWith(str(b), 'RemoteBranch(')
135
def test_remote_branch_format_supports_stacking(self):
137
self.make_branch('unstackable', format='pack-0.92')
138
b = BzrDir.open_from_transport(t.clone('unstackable')).open_branch()
139
self.assertFalse(b._format.supports_stacking())
140
self.make_branch('stackable', format='1.9')
141
b = BzrDir.open_from_transport(t.clone('stackable')).open_branch()
142
self.assertTrue(b._format.supports_stacking())
144
def test_remote_repo_format_supports_external_references(self):
146
bd = self.make_bzrdir('unstackable', format='pack-0.92')
147
r = bd.create_repository()
148
self.assertFalse(r._format.supports_external_lookups)
149
r = BzrDir.open_from_transport(t.clone('unstackable')).open_repository()
150
self.assertFalse(r._format.supports_external_lookups)
151
bd = self.make_bzrdir('stackable', format='1.9')
152
r = bd.create_repository()
153
self.assertTrue(r._format.supports_external_lookups)
154
r = BzrDir.open_from_transport(t.clone('stackable')).open_repository()
155
self.assertTrue(r._format.supports_external_lookups)
158
class FakeProtocol(object):
159
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
161
def __init__(self, body, fake_client):
163
self._body_buffer = None
164
self._fake_client = fake_client
166
def read_body_bytes(self, count=-1):
167
if self._body_buffer is None:
168
self._body_buffer = StringIO(self.body)
169
bytes = self._body_buffer.read(count)
170
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
171
self._fake_client.expecting_body = False
174
def cancel_read_body(self):
175
self._fake_client.expecting_body = False
177
def read_streamed_body(self):
181
class FakeClient(_SmartClient):
182
"""Lookalike for _SmartClient allowing testing."""
184
def __init__(self, fake_medium_base='fake base'):
185
"""Create a FakeClient."""
188
self.expecting_body = False
189
# if non-None, this is the list of expected calls, with only the
190
# method name and arguments included. the body might be hard to
191
# compute so is not included. If a call is None, that call can
193
self._expected_calls = None
194
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
196
def add_expected_call(self, call_name, call_args, response_type,
197
response_args, response_body=None):
198
if self._expected_calls is None:
199
self._expected_calls = []
200
self._expected_calls.append((call_name, call_args))
201
self.responses.append((response_type, response_args, response_body))
203
def add_success_response(self, *args):
204
self.responses.append(('success', args, None))
206
def add_success_response_with_body(self, body, *args):
207
self.responses.append(('success', args, body))
208
if self._expected_calls is not None:
209
self._expected_calls.append(None)
211
def add_error_response(self, *args):
212
self.responses.append(('error', args))
214
def add_unknown_method_response(self, verb):
215
self.responses.append(('unknown', verb))
217
def finished_test(self):
218
if self._expected_calls:
219
raise AssertionError("%r finished but was still expecting %r"
220
% (self, self._expected_calls[0]))
222
def _get_next_response(self):
224
response_tuple = self.responses.pop(0)
225
except IndexError, e:
226
raise AssertionError("%r didn't expect any more calls"
228
if response_tuple[0] == 'unknown':
229
raise errors.UnknownSmartMethod(response_tuple[1])
230
elif response_tuple[0] == 'error':
231
raise errors.ErrorFromSmartServer(response_tuple[1])
232
return response_tuple
234
def _check_call(self, method, args):
235
if self._expected_calls is None:
236
# the test should be updated to say what it expects
239
next_call = self._expected_calls.pop(0)
241
raise AssertionError("%r didn't expect any more calls "
243
% (self, method, args,))
244
if next_call is None:
246
if method != next_call[0] or args != next_call[1]:
247
raise AssertionError("%r expected %r%r "
249
% (self, next_call[0], next_call[1], method, args,))
251
def call(self, method, *args):
252
self._check_call(method, args)
253
self._calls.append(('call', method, args))
254
return self._get_next_response()[1]
256
def call_expecting_body(self, method, *args):
257
self._check_call(method, args)
258
self._calls.append(('call_expecting_body', method, args))
259
result = self._get_next_response()
260
self.expecting_body = True
261
return result[1], FakeProtocol(result[2], self)
263
def call_with_body_bytes_expecting_body(self, method, args, body):
264
self._check_call(method, args)
265
self._calls.append(('call_with_body_bytes_expecting_body', method,
267
result = self._get_next_response()
268
self.expecting_body = True
269
return result[1], FakeProtocol(result[2], self)
271
def call_with_body_stream(self, args, stream):
272
# Explicitly consume the stream before checking for an error, because
273
# that's what happens a real medium.
274
stream = list(stream)
275
self._check_call(args[0], args[1:])
276
self._calls.append(('call_with_body_stream', args[0], args[1:], stream))
277
result = self._get_next_response()
278
# The second value returned from call_with_body_stream is supposed to
279
# be a response_handler object, but so far no tests depend on that.
280
response_handler = None
281
return result[1], response_handler
284
class FakeMedium(medium.SmartClientMedium):
286
def __init__(self, client_calls, base):
287
medium.SmartClientMedium.__init__(self, base)
288
self._client_calls = client_calls
290
def disconnect(self):
291
self._client_calls.append(('disconnect medium',))
294
class TestVfsHas(tests.TestCase):
296
def test_unicode_path(self):
297
client = FakeClient('/')
298
client.add_success_response('yes',)
299
transport = RemoteTransport('bzr://localhost/', _client=client)
300
filename = u'/hell\u00d8'.encode('utf8')
301
result = transport.has(filename)
303
[('call', 'has', (filename,))],
305
self.assertTrue(result)
308
class TestRemote(tests.TestCaseWithMemoryTransport):
310
def get_branch_format(self):
311
reference_bzrdir_format = bzrdir.format_registry.get('default')()
312
return reference_bzrdir_format.get_branch_format()
314
def get_repo_format(self):
315
reference_bzrdir_format = bzrdir.format_registry.get('default')()
316
return reference_bzrdir_format.repository_format
318
def disable_verb(self, verb):
319
"""Disable a verb for one test."""
320
request_handlers = smart.request.request_handlers
321
orig_method = request_handlers.get(verb)
322
request_handlers.remove(verb)
324
request_handlers.register(verb, orig_method)
325
self.addCleanup(restoreVerb)
328
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
329
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
331
def assertRemotePath(self, expected, client_base, transport_base):
332
"""Assert that the result of
333
SmartClientMedium.remote_path_from_transport is the expected value for
334
a given client_base and transport_base.
336
client_medium = medium.SmartClientMedium(client_base)
337
transport = get_transport(transport_base)
338
result = client_medium.remote_path_from_transport(transport)
339
self.assertEqual(expected, result)
341
def test_remote_path_from_transport(self):
342
"""SmartClientMedium.remote_path_from_transport calculates a URL for
343
the given transport relative to the root of the client base URL.
345
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
346
self.assertRemotePath(
347
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
349
def assertRemotePathHTTP(self, expected, transport_base, relpath):
350
"""Assert that the result of
351
HttpTransportBase.remote_path_from_transport is the expected value for
352
a given transport_base and relpath of that transport. (Note that
353
HttpTransportBase is a subclass of SmartClientMedium)
355
base_transport = get_transport(transport_base)
356
client_medium = base_transport.get_smart_medium()
357
cloned_transport = base_transport.clone(relpath)
358
result = client_medium.remote_path_from_transport(cloned_transport)
359
self.assertEqual(expected, result)
361
def test_remote_path_from_transport_http(self):
362
"""Remote paths for HTTP transports are calculated differently to other
363
transports. They are just relative to the client base, not the root
364
directory of the host.
366
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
367
self.assertRemotePathHTTP(
368
'../xyz/', scheme + '//host/path', '../xyz/')
369
self.assertRemotePathHTTP(
370
'xyz/', scheme + '//host/path', 'xyz/')
373
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
374
"""Tests for the behaviour of client_medium.remote_is_at_least."""
376
def test_initially_unlimited(self):
377
"""A fresh medium assumes that the remote side supports all
380
client_medium = medium.SmartClientMedium('dummy base')
381
self.assertFalse(client_medium._is_remote_before((99, 99)))
383
def test__remember_remote_is_before(self):
384
"""Calling _remember_remote_is_before ratchets down the known remote
387
client_medium = medium.SmartClientMedium('dummy base')
388
# Mark the remote side as being less than 1.6. The remote side may
390
client_medium._remember_remote_is_before((1, 6))
391
self.assertTrue(client_medium._is_remote_before((1, 6)))
392
self.assertFalse(client_medium._is_remote_before((1, 5)))
393
# Calling _remember_remote_is_before again with a lower value works.
394
client_medium._remember_remote_is_before((1, 5))
395
self.assertTrue(client_medium._is_remote_before((1, 5)))
396
# You cannot call _remember_remote_is_before with a larger value.
398
AssertionError, client_medium._remember_remote_is_before, (1, 9))
401
class TestBzrDirCloningMetaDir(TestRemote):
403
def test_backwards_compat(self):
404
self.setup_smart_server_with_call_log()
405
a_dir = self.make_bzrdir('.')
406
self.reset_smart_call_log()
407
verb = 'BzrDir.cloning_metadir'
408
self.disable_verb(verb)
409
format = a_dir.cloning_metadir()
410
call_count = len([call for call in self.hpss_calls if
411
call.call.method == verb])
412
self.assertEqual(1, call_count)
414
def test_branch_reference(self):
415
transport = self.get_transport('quack')
416
referenced = self.make_branch('referenced')
417
expected = referenced.bzrdir.cloning_metadir()
418
client = FakeClient(transport.base)
419
client.add_expected_call(
420
'BzrDir.cloning_metadir', ('quack/', 'False'),
421
'error', ('BranchReference',)),
422
client.add_expected_call(
423
'BzrDir.open_branchV2', ('quack/',),
424
'success', ('ref', self.get_url('referenced'))),
425
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
427
result = a_bzrdir.cloning_metadir()
428
# We should have got a control dir matching the referenced branch.
429
self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
430
self.assertEqual(expected._repository_format, result._repository_format)
431
self.assertEqual(expected._branch_format, result._branch_format)
432
client.finished_test()
434
def test_current_server(self):
435
transport = self.get_transport('.')
436
transport = transport.clone('quack')
437
self.make_bzrdir('quack')
438
client = FakeClient(transport.base)
439
reference_bzrdir_format = bzrdir.format_registry.get('default')()
440
control_name = reference_bzrdir_format.network_name()
441
client.add_expected_call(
442
'BzrDir.cloning_metadir', ('quack/', 'False'),
443
'success', (control_name, '', ('branch', ''))),
444
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
446
result = a_bzrdir.cloning_metadir()
447
# We should have got a reference control dir with default branch and
448
# repository formats.
449
# This pokes a little, just to be sure.
450
self.assertEqual(bzrdir.BzrDirMetaFormat1, type(result))
451
self.assertEqual(None, result._repository_format)
452
self.assertEqual(None, result._branch_format)
453
client.finished_test()
456
class TestBzrDirOpenBranch(TestRemote):
458
def test_backwards_compat(self):
459
self.setup_smart_server_with_call_log()
460
self.make_branch('.')
461
a_dir = BzrDir.open(self.get_url('.'))
462
self.reset_smart_call_log()
463
verb = 'BzrDir.open_branchV2'
464
self.disable_verb(verb)
465
format = a_dir.open_branch()
466
call_count = len([call for call in self.hpss_calls if
467
call.call.method == verb])
468
self.assertEqual(1, call_count)
470
def test_branch_present(self):
471
reference_format = self.get_repo_format()
472
network_name = reference_format.network_name()
473
branch_network_name = self.get_branch_format().network_name()
474
transport = MemoryTransport()
475
transport.mkdir('quack')
476
transport = transport.clone('quack')
477
client = FakeClient(transport.base)
478
client.add_expected_call(
479
'BzrDir.open_branchV2', ('quack/',),
480
'success', ('branch', branch_network_name))
481
client.add_expected_call(
482
'BzrDir.find_repositoryV3', ('quack/',),
483
'success', ('ok', '', 'no', 'no', 'no', network_name))
484
client.add_expected_call(
485
'Branch.get_stacked_on_url', ('quack/',),
486
'error', ('NotStacked',))
487
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
489
result = bzrdir.open_branch()
490
self.assertIsInstance(result, RemoteBranch)
491
self.assertEqual(bzrdir, result.bzrdir)
492
client.finished_test()
494
def test_branch_missing(self):
495
transport = MemoryTransport()
496
transport.mkdir('quack')
497
transport = transport.clone('quack')
498
client = FakeClient(transport.base)
499
client.add_error_response('nobranch')
500
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
502
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
504
[('call', 'BzrDir.open_branchV2', ('quack/',))],
507
def test__get_tree_branch(self):
508
# _get_tree_branch is a form of open_branch, but it should only ask for
509
# branch opening, not any other network requests.
512
calls.append("Called")
514
transport = MemoryTransport()
515
# no requests on the network - catches other api calls being made.
516
client = FakeClient(transport.base)
517
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
519
# patch the open_branch call to record that it was called.
520
bzrdir.open_branch = open_branch
521
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
522
self.assertEqual(["Called"], calls)
523
self.assertEqual([], client._calls)
525
def test_url_quoting_of_path(self):
526
# Relpaths on the wire should not be URL-escaped. So "~" should be
527
# transmitted as "~", not "%7E".
528
transport = RemoteTCPTransport('bzr://localhost/~hello/')
529
client = FakeClient(transport.base)
530
reference_format = self.get_repo_format()
531
network_name = reference_format.network_name()
532
branch_network_name = self.get_branch_format().network_name()
533
client.add_expected_call(
534
'BzrDir.open_branchV2', ('~hello/',),
535
'success', ('branch', branch_network_name))
536
client.add_expected_call(
537
'BzrDir.find_repositoryV3', ('~hello/',),
538
'success', ('ok', '', 'no', 'no', 'no', network_name))
539
client.add_expected_call(
540
'Branch.get_stacked_on_url', ('~hello/',),
541
'error', ('NotStacked',))
542
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
544
result = bzrdir.open_branch()
545
client.finished_test()
547
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
548
reference_format = self.get_repo_format()
549
network_name = reference_format.network_name()
550
transport = MemoryTransport()
551
transport.mkdir('quack')
552
transport = transport.clone('quack')
554
rich_response = 'yes'
558
subtree_response = 'yes'
560
subtree_response = 'no'
561
client = FakeClient(transport.base)
562
client.add_success_response(
563
'ok', '', rich_response, subtree_response, external_lookup,
565
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
567
result = bzrdir.open_repository()
569
[('call', 'BzrDir.find_repositoryV3', ('quack/',))],
571
self.assertIsInstance(result, RemoteRepository)
572
self.assertEqual(bzrdir, result.bzrdir)
573
self.assertEqual(rich_root, result._format.rich_root_data)
574
self.assertEqual(subtrees, result._format.supports_tree_reference)
576
def test_open_repository_sets_format_attributes(self):
577
self.check_open_repository(True, True)
578
self.check_open_repository(False, True)
579
self.check_open_repository(True, False)
580
self.check_open_repository(False, False)
581
self.check_open_repository(False, False, 'yes')
583
def test_old_server(self):
584
"""RemoteBzrDirFormat should fail to probe if the server version is too
587
self.assertRaises(errors.NotBranchError,
588
RemoteBzrDirFormat.probe_transport, OldServerTransport())
591
class TestBzrDirCreateBranch(TestRemote):
593
def test_backwards_compat(self):
594
self.setup_smart_server_with_call_log()
595
repo = self.make_repository('.')
596
self.reset_smart_call_log()
597
self.disable_verb('BzrDir.create_branch')
598
branch = repo.bzrdir.create_branch()
599
create_branch_call_count = len([call for call in self.hpss_calls if
600
call.call.method == 'BzrDir.create_branch'])
601
self.assertEqual(1, create_branch_call_count)
603
def test_current_server(self):
604
transport = self.get_transport('.')
605
transport = transport.clone('quack')
606
self.make_repository('quack')
607
client = FakeClient(transport.base)
608
reference_bzrdir_format = bzrdir.format_registry.get('default')()
609
reference_format = reference_bzrdir_format.get_branch_format()
610
network_name = reference_format.network_name()
611
reference_repo_fmt = reference_bzrdir_format.repository_format
612
reference_repo_name = reference_repo_fmt.network_name()
613
client.add_expected_call(
614
'BzrDir.create_branch', ('quack/', network_name),
615
'success', ('ok', network_name, '', 'no', 'no', 'yes',
616
reference_repo_name))
617
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
619
branch = a_bzrdir.create_branch()
620
# We should have got a remote branch
621
self.assertIsInstance(branch, remote.RemoteBranch)
622
# its format should have the settings from the response
623
format = branch._format
624
self.assertEqual(network_name, format.network_name())
627
class TestBzrDirCreateRepository(TestRemote):
629
def test_backwards_compat(self):
630
self.setup_smart_server_with_call_log()
631
bzrdir = self.make_bzrdir('.')
632
self.reset_smart_call_log()
633
self.disable_verb('BzrDir.create_repository')
634
repo = bzrdir.create_repository()
635
create_repo_call_count = len([call for call in self.hpss_calls if
636
call.call.method == 'BzrDir.create_repository'])
637
self.assertEqual(1, create_repo_call_count)
639
def test_current_server(self):
640
transport = self.get_transport('.')
641
transport = transport.clone('quack')
642
self.make_bzrdir('quack')
643
client = FakeClient(transport.base)
644
reference_bzrdir_format = bzrdir.format_registry.get('default')()
645
reference_format = reference_bzrdir_format.repository_format
646
network_name = reference_format.network_name()
647
client.add_expected_call(
648
'BzrDir.create_repository', ('quack/',
649
'Bazaar pack repository format 1 (needs bzr 0.92)\n', 'False'),
650
'success', ('ok', 'no', 'no', 'no', network_name))
651
a_bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
653
repo = a_bzrdir.create_repository()
654
# We should have got a remote repository
655
self.assertIsInstance(repo, remote.RemoteRepository)
656
# its format should have the settings from the response
657
format = repo._format
658
self.assertFalse(format.rich_root_data)
659
self.assertFalse(format.supports_tree_reference)
660
self.assertFalse(format.supports_external_lookups)
661
self.assertEqual(network_name, format.network_name())
664
class TestBzrDirOpenRepository(TestRemote):
666
def test_backwards_compat_1_2_3(self):
667
# fallback all the way to the first version.
668
reference_format = self.get_repo_format()
669
network_name = reference_format.network_name()
670
client = FakeClient('bzr://example.com/')
671
client.add_unknown_method_response('BzrDir.find_repositoryV3')
672
client.add_unknown_method_response('BzrDir.find_repositoryV2')
673
client.add_success_response('ok', '', 'no', 'no')
674
# A real repository instance will be created to determine the network
676
client.add_success_response_with_body(
677
"Bazaar-NG meta directory, format 1\n", 'ok')
678
client.add_success_response_with_body(
679
reference_format.get_format_string(), 'ok')
680
# PackRepository wants to do a stat
681
client.add_success_response('stat', '0', '65535')
682
remote_transport = RemoteTransport('bzr://example.com/quack/', medium=False,
684
bzrdir = RemoteBzrDir(remote_transport, remote.RemoteBzrDirFormat(),
686
repo = bzrdir.open_repository()
688
[('call', 'BzrDir.find_repositoryV3', ('quack/',)),
689
('call', 'BzrDir.find_repositoryV2', ('quack/',)),
690
('call', 'BzrDir.find_repository', ('quack/',)),
691
('call_expecting_body', 'get', ('/quack/.bzr/branch-format',)),
692
('call_expecting_body', 'get', ('/quack/.bzr/repository/format',)),
693
('call', 'stat', ('/quack/.bzr/repository',)),
696
self.assertEqual(network_name, repo._format.network_name())
698
def test_backwards_compat_2(self):
699
# fallback to find_repositoryV2
700
reference_format = self.get_repo_format()
701
network_name = reference_format.network_name()
702
client = FakeClient('bzr://example.com/')
703
client.add_unknown_method_response('BzrDir.find_repositoryV3')
704
client.add_success_response('ok', '', 'no', 'no', 'no')
705
# A real repository instance will be created to determine the network
707
client.add_success_response_with_body(
708
"Bazaar-NG meta directory, format 1\n", 'ok')
709
client.add_success_response_with_body(
710
reference_format.get_format_string(), 'ok')
711
# PackRepository wants to do a stat
712
client.add_success_response('stat', '0', '65535')
713
remote_transport = RemoteTransport('bzr://example.com/quack/', medium=False,
715
bzrdir = RemoteBzrDir(remote_transport, remote.RemoteBzrDirFormat(),
717
repo = bzrdir.open_repository()
719
[('call', 'BzrDir.find_repositoryV3', ('quack/',)),
720
('call', 'BzrDir.find_repositoryV2', ('quack/',)),
721
('call_expecting_body', 'get', ('/quack/.bzr/branch-format',)),
722
('call_expecting_body', 'get', ('/quack/.bzr/repository/format',)),
723
('call', 'stat', ('/quack/.bzr/repository',)),
726
self.assertEqual(network_name, repo._format.network_name())
728
def test_current_server(self):
729
reference_format = self.get_repo_format()
730
network_name = reference_format.network_name()
731
transport = MemoryTransport()
732
transport.mkdir('quack')
733
transport = transport.clone('quack')
734
client = FakeClient(transport.base)
735
client.add_success_response('ok', '', 'no', 'no', 'no', network_name)
736
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
738
repo = bzrdir.open_repository()
740
[('call', 'BzrDir.find_repositoryV3', ('quack/',))],
742
self.assertEqual(network_name, repo._format.network_name())
745
class OldSmartClient(object):
746
"""A fake smart client for test_old_version that just returns a version one
747
response to the 'hello' (query version) command.
750
def get_request(self):
751
input_file = StringIO('ok\x011\n')
752
output_file = StringIO()
753
client_medium = medium.SmartSimplePipesClientMedium(
754
input_file, output_file)
755
return medium.SmartClientStreamMediumRequest(client_medium)
757
def protocol_version(self):
761
class OldServerTransport(object):
762
"""A fake transport for test_old_server that reports it's smart server
763
protocol version as version one.
769
def get_smart_client(self):
770
return OldSmartClient()
773
class RemoteBzrDirTestCase(TestRemote):
775
def make_remote_bzrdir(self, transport, client):
776
"""Make a RemotebzrDir using 'client' as the _client."""
777
return RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
781
class RemoteBranchTestCase(RemoteBzrDirTestCase):
783
def make_remote_branch(self, transport, client):
784
"""Make a RemoteBranch using 'client' as its _SmartClient.
786
A RemoteBzrDir and RemoteRepository will also be created to fill out
787
the RemoteBranch, albeit with stub values for some of their attributes.
789
# we do not want bzrdir to make any remote calls, so use False as its
790
# _client. If it tries to make a remote call, this will fail
792
bzrdir = self.make_remote_bzrdir(transport, False)
793
repo = RemoteRepository(bzrdir, None, _client=client)
794
branch_format = self.get_branch_format()
795
format = RemoteBranchFormat(network_name=branch_format.network_name())
796
return RemoteBranch(bzrdir, repo, _client=client, format=format)
799
class TestBranchGetParent(RemoteBranchTestCase):
801
def test_no_parent(self):
802
# in an empty branch we decode the response properly
803
transport = MemoryTransport()
804
client = FakeClient(transport.base)
805
client.add_expected_call(
806
'Branch.get_stacked_on_url', ('quack/',),
807
'error', ('NotStacked',))
808
client.add_expected_call(
809
'Branch.get_parent', ('quack/',),
811
transport.mkdir('quack')
812
transport = transport.clone('quack')
813
branch = self.make_remote_branch(transport, client)
814
result = branch.get_parent()
815
client.finished_test()
816
self.assertEqual(None, result)
818
def test_parent_relative(self):
819
transport = MemoryTransport()
820
client = FakeClient(transport.base)
821
client.add_expected_call(
822
'Branch.get_stacked_on_url', ('kwaak/',),
823
'error', ('NotStacked',))
824
client.add_expected_call(
825
'Branch.get_parent', ('kwaak/',),
826
'success', ('../foo/',))
827
transport.mkdir('kwaak')
828
transport = transport.clone('kwaak')
829
branch = self.make_remote_branch(transport, client)
830
result = branch.get_parent()
831
self.assertEqual(transport.clone('../foo').base, result)
833
def test_parent_absolute(self):
834
transport = MemoryTransport()
835
client = FakeClient(transport.base)
836
client.add_expected_call(
837
'Branch.get_stacked_on_url', ('kwaak/',),
838
'error', ('NotStacked',))
839
client.add_expected_call(
840
'Branch.get_parent', ('kwaak/',),
841
'success', ('http://foo/',))
842
transport.mkdir('kwaak')
843
transport = transport.clone('kwaak')
844
branch = self.make_remote_branch(transport, client)
845
result = branch.get_parent()
846
self.assertEqual('http://foo/', result)
849
class TestBranchGetTagsBytes(RemoteBranchTestCase):
851
def test_backwards_compat(self):
852
self.setup_smart_server_with_call_log()
853
branch = self.make_branch('.')
854
self.reset_smart_call_log()
855
verb = 'Branch.get_tags_bytes'
856
self.disable_verb(verb)
857
branch.tags.get_tag_dict()
858
call_count = len([call for call in self.hpss_calls if
859
call.call.method == verb])
860
self.assertEqual(1, call_count)
862
def test_trivial(self):
863
transport = MemoryTransport()
864
client = FakeClient(transport.base)
865
client.add_expected_call(
866
'Branch.get_stacked_on_url', ('quack/',),
867
'error', ('NotStacked',))
868
client.add_expected_call(
869
'Branch.get_tags_bytes', ('quack/',),
871
transport.mkdir('quack')
872
transport = transport.clone('quack')
873
branch = self.make_remote_branch(transport, client)
874
result = branch.tags.get_tag_dict()
875
client.finished_test()
876
self.assertEqual({}, result)
879
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
881
def test_empty_branch(self):
882
# in an empty branch we decode the response properly
883
transport = MemoryTransport()
884
client = FakeClient(transport.base)
885
client.add_expected_call(
886
'Branch.get_stacked_on_url', ('quack/',),
887
'error', ('NotStacked',))
888
client.add_expected_call(
889
'Branch.last_revision_info', ('quack/',),
890
'success', ('ok', '0', 'null:'))
891
transport.mkdir('quack')
892
transport = transport.clone('quack')
893
branch = self.make_remote_branch(transport, client)
894
result = branch.last_revision_info()
895
client.finished_test()
896
self.assertEqual((0, NULL_REVISION), result)
898
def test_non_empty_branch(self):
899
# in a non-empty branch we also decode the response properly
900
revid = u'\xc8'.encode('utf8')
901
transport = MemoryTransport()
902
client = FakeClient(transport.base)
903
client.add_expected_call(
904
'Branch.get_stacked_on_url', ('kwaak/',),
905
'error', ('NotStacked',))
906
client.add_expected_call(
907
'Branch.last_revision_info', ('kwaak/',),
908
'success', ('ok', '2', revid))
909
transport.mkdir('kwaak')
910
transport = transport.clone('kwaak')
911
branch = self.make_remote_branch(transport, client)
912
result = branch.last_revision_info()
913
self.assertEqual((2, revid), result)
916
class TestBranch_get_stacked_on_url(TestRemote):
917
"""Test Branch._get_stacked_on_url rpc"""
919
def test_get_stacked_on_invalid_url(self):
920
# test that asking for a stacked on url the server can't access works.
921
# This isn't perfect, but then as we're in the same process there
922
# really isn't anything we can do to be 100% sure that the server
923
# doesn't just open in - this test probably needs to be rewritten using
924
# a spawn()ed server.
925
stacked_branch = self.make_branch('stacked', format='1.9')
926
memory_branch = self.make_branch('base', format='1.9')
927
vfs_url = self.get_vfs_only_url('base')
928
stacked_branch.set_stacked_on_url(vfs_url)
929
transport = stacked_branch.bzrdir.root_transport
930
client = FakeClient(transport.base)
931
client.add_expected_call(
932
'Branch.get_stacked_on_url', ('stacked/',),
933
'success', ('ok', vfs_url))
934
# XXX: Multiple calls are bad, this second call documents what is
936
client.add_expected_call(
937
'Branch.get_stacked_on_url', ('stacked/',),
938
'success', ('ok', vfs_url))
939
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
941
repo_fmt = remote.RemoteRepositoryFormat()
942
repo_fmt._custom_format = stacked_branch.repository._format
943
branch = RemoteBranch(bzrdir, RemoteRepository(bzrdir, repo_fmt),
945
result = branch.get_stacked_on_url()
946
self.assertEqual(vfs_url, result)
948
def test_backwards_compatible(self):
949
# like with bzr1.6 with no Branch.get_stacked_on_url rpc
950
base_branch = self.make_branch('base', format='1.6')
951
stacked_branch = self.make_branch('stacked', format='1.6')
952
stacked_branch.set_stacked_on_url('../base')
953
client = FakeClient(self.get_url())
954
branch_network_name = self.get_branch_format().network_name()
955
client.add_expected_call(
956
'BzrDir.open_branchV2', ('stacked/',),
957
'success', ('branch', branch_network_name))
958
client.add_expected_call(
959
'BzrDir.find_repositoryV3', ('stacked/',),
960
'success', ('ok', '', 'no', 'no', 'yes',
961
stacked_branch.repository._format.network_name()))
962
# called twice, once from constructor and then again by us
963
client.add_expected_call(
964
'Branch.get_stacked_on_url', ('stacked/',),
965
'unknown', ('Branch.get_stacked_on_url',))
966
client.add_expected_call(
967
'Branch.get_stacked_on_url', ('stacked/',),
968
'unknown', ('Branch.get_stacked_on_url',))
969
# this will also do vfs access, but that goes direct to the transport
970
# and isn't seen by the FakeClient.
971
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
972
remote.RemoteBzrDirFormat(), _client=client)
973
branch = bzrdir.open_branch()
974
result = branch.get_stacked_on_url()
975
self.assertEqual('../base', result)
976
client.finished_test()
977
# it's in the fallback list both for the RemoteRepository and its vfs
979
self.assertEqual(1, len(branch.repository._fallback_repositories))
981
len(branch.repository._real_repository._fallback_repositories))
983
def test_get_stacked_on_real_branch(self):
984
base_branch = self.make_branch('base', format='1.6')
985
stacked_branch = self.make_branch('stacked', format='1.6')
986
stacked_branch.set_stacked_on_url('../base')
987
reference_format = self.get_repo_format()
988
network_name = reference_format.network_name()
989
client = FakeClient(self.get_url())
990
branch_network_name = self.get_branch_format().network_name()
991
client.add_expected_call(
992
'BzrDir.open_branchV2', ('stacked/',),
993
'success', ('branch', branch_network_name))
994
client.add_expected_call(
995
'BzrDir.find_repositoryV3', ('stacked/',),
996
'success', ('ok', '', 'no', 'no', 'yes', network_name))
997
# called twice, once from constructor and then again by us
998
client.add_expected_call(
999
'Branch.get_stacked_on_url', ('stacked/',),
1000
'success', ('ok', '../base'))
1001
client.add_expected_call(
1002
'Branch.get_stacked_on_url', ('stacked/',),
1003
'success', ('ok', '../base'))
1004
bzrdir = RemoteBzrDir(self.get_transport('stacked'),
1005
remote.RemoteBzrDirFormat(), _client=client)
1006
branch = bzrdir.open_branch()
1007
result = branch.get_stacked_on_url()
1008
self.assertEqual('../base', result)
1009
client.finished_test()
1010
# it's in the fallback list both for the RemoteRepository.
1011
self.assertEqual(1, len(branch.repository._fallback_repositories))
1012
# And we haven't had to construct a real repository.
1013
self.assertEqual(None, branch.repository._real_repository)
1016
class TestBranchSetLastRevision(RemoteBranchTestCase):
1018
def test_set_empty(self):
1019
# set_revision_history([]) is translated to calling
1020
# Branch.set_last_revision(path, '') on the wire.
1021
transport = MemoryTransport()
1022
transport.mkdir('branch')
1023
transport = transport.clone('branch')
1025
client = FakeClient(transport.base)
1026
client.add_expected_call(
1027
'Branch.get_stacked_on_url', ('branch/',),
1028
'error', ('NotStacked',))
1029
client.add_expected_call(
1030
'Branch.lock_write', ('branch/', '', ''),
1031
'success', ('ok', 'branch token', 'repo token'))
1032
client.add_expected_call(
1033
'Branch.last_revision_info',
1035
'success', ('ok', '0', 'null:'))
1036
client.add_expected_call(
1037
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
1039
client.add_expected_call(
1040
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1042
branch = self.make_remote_branch(transport, client)
1043
# This is a hack to work around the problem that RemoteBranch currently
1044
# unnecessarily invokes _ensure_real upon a call to lock_write.
1045
branch._ensure_real = lambda: None
1047
result = branch.set_revision_history([])
1049
self.assertEqual(None, result)
1050
client.finished_test()
1052
def test_set_nonempty(self):
1053
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
1054
# Branch.set_last_revision(path, rev-idN) on the wire.
1055
transport = MemoryTransport()
1056
transport.mkdir('branch')
1057
transport = transport.clone('branch')
1059
client = FakeClient(transport.base)
1060
client.add_expected_call(
1061
'Branch.get_stacked_on_url', ('branch/',),
1062
'error', ('NotStacked',))
1063
client.add_expected_call(
1064
'Branch.lock_write', ('branch/', '', ''),
1065
'success', ('ok', 'branch token', 'repo token'))
1066
client.add_expected_call(
1067
'Branch.last_revision_info',
1069
'success', ('ok', '0', 'null:'))
1071
encoded_body = bz2.compress('\n'.join(lines))
1072
client.add_success_response_with_body(encoded_body, 'ok')
1073
client.add_expected_call(
1074
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
1076
client.add_expected_call(
1077
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1079
branch = self.make_remote_branch(transport, client)
1080
# This is a hack to work around the problem that RemoteBranch currently
1081
# unnecessarily invokes _ensure_real upon a call to lock_write.
1082
branch._ensure_real = lambda: None
1083
# Lock the branch, reset the record of remote calls.
1085
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
1087
self.assertEqual(None, result)
1088
client.finished_test()
1090
def test_no_such_revision(self):
1091
transport = MemoryTransport()
1092
transport.mkdir('branch')
1093
transport = transport.clone('branch')
1094
# A response of 'NoSuchRevision' is translated into an exception.
1095
client = FakeClient(transport.base)
1096
client.add_expected_call(
1097
'Branch.get_stacked_on_url', ('branch/',),
1098
'error', ('NotStacked',))
1099
client.add_expected_call(
1100
'Branch.lock_write', ('branch/', '', ''),
1101
'success', ('ok', 'branch token', 'repo token'))
1102
client.add_expected_call(
1103
'Branch.last_revision_info',
1105
'success', ('ok', '0', 'null:'))
1106
# get_graph calls to construct the revision history, for the set_rh
1109
encoded_body = bz2.compress('\n'.join(lines))
1110
client.add_success_response_with_body(encoded_body, 'ok')
1111
client.add_expected_call(
1112
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
1113
'error', ('NoSuchRevision', 'rev-id'))
1114
client.add_expected_call(
1115
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1118
branch = self.make_remote_branch(transport, client)
1121
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
1123
client.finished_test()
1125
def test_tip_change_rejected(self):
1126
"""TipChangeRejected responses cause a TipChangeRejected exception to
1129
transport = MemoryTransport()
1130
transport.mkdir('branch')
1131
transport = transport.clone('branch')
1132
client = FakeClient(transport.base)
1133
rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
1134
rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
1135
client.add_expected_call(
1136
'Branch.get_stacked_on_url', ('branch/',),
1137
'error', ('NotStacked',))
1138
client.add_expected_call(
1139
'Branch.lock_write', ('branch/', '', ''),
1140
'success', ('ok', 'branch token', 'repo token'))
1141
client.add_expected_call(
1142
'Branch.last_revision_info',
1144
'success', ('ok', '0', 'null:'))
1146
encoded_body = bz2.compress('\n'.join(lines))
1147
client.add_success_response_with_body(encoded_body, 'ok')
1148
client.add_expected_call(
1149
'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
1150
'error', ('TipChangeRejected', rejection_msg_utf8))
1151
client.add_expected_call(
1152
'Branch.unlock', ('branch/', 'branch token', 'repo token'),
1154
branch = self.make_remote_branch(transport, client)
1155
branch._ensure_real = lambda: None
1157
# The 'TipChangeRejected' error response triggered by calling
1158
# set_revision_history causes a TipChangeRejected exception.
1159
err = self.assertRaises(
1160
errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
1161
# The UTF-8 message from the response has been decoded into a unicode
1163
self.assertIsInstance(err.msg, unicode)
1164
self.assertEqual(rejection_msg_unicode, err.msg)
1166
client.finished_test()
1169
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
1171
def test_set_last_revision_info(self):
1172
# set_last_revision_info(num, 'rev-id') is translated to calling
1173
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
1174
transport = MemoryTransport()
1175
transport.mkdir('branch')
1176
transport = transport.clone('branch')
1177
client = FakeClient(transport.base)
1178
# get_stacked_on_url
1179
client.add_error_response('NotStacked')
1181
client.add_success_response('ok', 'branch token', 'repo token')
1182
# query the current revision
1183
client.add_success_response('ok', '0', 'null:')
1185
client.add_success_response('ok')
1187
client.add_success_response('ok')
1189
branch = self.make_remote_branch(transport, client)
1190
# Lock the branch, reset the record of remote calls.
1193
result = branch.set_last_revision_info(1234, 'a-revision-id')
1195
[('call', 'Branch.last_revision_info', ('branch/',)),
1196
('call', 'Branch.set_last_revision_info',
1197
('branch/', 'branch token', 'repo token',
1198
'1234', 'a-revision-id'))],
1200
self.assertEqual(None, result)
1202
def test_no_such_revision(self):
1203
# A response of 'NoSuchRevision' is translated into an exception.
1204
transport = MemoryTransport()
1205
transport.mkdir('branch')
1206
transport = transport.clone('branch')
1207
client = FakeClient(transport.base)
1208
# get_stacked_on_url
1209
client.add_error_response('NotStacked')
1211
client.add_success_response('ok', 'branch token', 'repo token')
1213
client.add_error_response('NoSuchRevision', 'revid')
1215
client.add_success_response('ok')
1217
branch = self.make_remote_branch(transport, client)
1218
# Lock the branch, reset the record of remote calls.
1223
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
1226
def lock_remote_branch(self, branch):
1227
"""Trick a RemoteBranch into thinking it is locked."""
1228
branch._lock_mode = 'w'
1229
branch._lock_count = 2
1230
branch._lock_token = 'branch token'
1231
branch._repo_lock_token = 'repo token'
1232
branch.repository._lock_mode = 'w'
1233
branch.repository._lock_count = 2
1234
branch.repository._lock_token = 'repo token'
1236
def test_backwards_compatibility(self):
1237
"""If the server does not support the Branch.set_last_revision_info
1238
verb (which is new in 1.4), then the client falls back to VFS methods.
1240
# This test is a little messy. Unlike most tests in this file, it
1241
# doesn't purely test what a Remote* object sends over the wire, and
1242
# how it reacts to responses from the wire. It instead relies partly
1243
# on asserting that the RemoteBranch will call
1244
# self._real_branch.set_last_revision_info(...).
1246
# First, set up our RemoteBranch with a FakeClient that raises
1247
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
1248
transport = MemoryTransport()
1249
transport.mkdir('branch')
1250
transport = transport.clone('branch')
1251
client = FakeClient(transport.base)
1252
client.add_expected_call(
1253
'Branch.get_stacked_on_url', ('branch/',),
1254
'error', ('NotStacked',))
1255
client.add_expected_call(
1256
'Branch.last_revision_info',
1258
'success', ('ok', '0', 'null:'))
1259
client.add_expected_call(
1260
'Branch.set_last_revision_info',
1261
('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
1262
'unknown', 'Branch.set_last_revision_info')
1264
branch = self.make_remote_branch(transport, client)
1265
class StubRealBranch(object):
1268
def set_last_revision_info(self, revno, revision_id):
1270
('set_last_revision_info', revno, revision_id))
1271
def _clear_cached_state(self):
1273
real_branch = StubRealBranch()
1274
branch._real_branch = real_branch
1275
self.lock_remote_branch(branch)
1277
# Call set_last_revision_info, and verify it behaved as expected.
1278
result = branch.set_last_revision_info(1234, 'a-revision-id')
1280
[('set_last_revision_info', 1234, 'a-revision-id')],
1282
client.finished_test()
1284
def test_unexpected_error(self):
1285
# If the server sends an error the client doesn't understand, it gets
1286
# turned into an UnknownErrorFromSmartServer, which is presented as a
1287
# non-internal error to the user.
1288
transport = MemoryTransport()
1289
transport.mkdir('branch')
1290
transport = transport.clone('branch')
1291
client = FakeClient(transport.base)
1292
# get_stacked_on_url
1293
client.add_error_response('NotStacked')
1295
client.add_success_response('ok', 'branch token', 'repo token')
1297
client.add_error_response('UnexpectedError')
1299
client.add_success_response('ok')
1301
branch = self.make_remote_branch(transport, client)
1302
# Lock the branch, reset the record of remote calls.
1306
err = self.assertRaises(
1307
errors.UnknownErrorFromSmartServer,
1308
branch.set_last_revision_info, 123, 'revid')
1309
self.assertEqual(('UnexpectedError',), err.error_tuple)
1312
def test_tip_change_rejected(self):
1313
"""TipChangeRejected responses cause a TipChangeRejected exception to
1316
transport = MemoryTransport()
1317
transport.mkdir('branch')
1318
transport = transport.clone('branch')
1319
client = FakeClient(transport.base)
1320
# get_stacked_on_url
1321
client.add_error_response('NotStacked')
1323
client.add_success_response('ok', 'branch token', 'repo token')
1325
client.add_error_response('TipChangeRejected', 'rejection message')
1327
client.add_success_response('ok')
1329
branch = self.make_remote_branch(transport, client)
1330
# Lock the branch, reset the record of remote calls.
1332
self.addCleanup(branch.unlock)
1335
# The 'TipChangeRejected' error response triggered by calling
1336
# set_last_revision_info causes a TipChangeRejected exception.
1337
err = self.assertRaises(
1338
errors.TipChangeRejected,
1339
branch.set_last_revision_info, 123, 'revid')
1340
self.assertEqual('rejection message', err.msg)
1343
class TestBranchGetSetConfig(RemoteBranchTestCase):
1345
def test_get_branch_conf(self):
1346
# in an empty branch we decode the response properly
1347
client = FakeClient()
1348
client.add_expected_call(
1349
'Branch.get_stacked_on_url', ('memory:///',),
1350
'error', ('NotStacked',),)
1351
client.add_success_response_with_body('# config file body', 'ok')
1352
transport = MemoryTransport()
1353
branch = self.make_remote_branch(transport, client)
1354
config = branch.get_config()
1355
config.has_explicit_nickname()
1357
[('call', 'Branch.get_stacked_on_url', ('memory:///',)),
1358
('call_expecting_body', 'Branch.get_config_file', ('memory:///',))],
1361
def test_get_multi_line_branch_conf(self):
1362
# Make sure that multiple-line branch.conf files are supported
1364
# https://bugs.edge.launchpad.net/bzr/+bug/354075
1365
client = FakeClient()
1366
client.add_expected_call(
1367
'Branch.get_stacked_on_url', ('memory:///',),
1368
'error', ('NotStacked',),)
1369
client.add_success_response_with_body('a = 1\nb = 2\nc = 3\n', 'ok')
1370
transport = MemoryTransport()
1371
branch = self.make_remote_branch(transport, client)
1372
config = branch.get_config()
1373
self.assertEqual(u'2', config.get_user_option('b'))
1375
def test_set_option(self):
1376
client = FakeClient()
1377
client.add_expected_call(
1378
'Branch.get_stacked_on_url', ('memory:///',),
1379
'error', ('NotStacked',),)
1380
client.add_expected_call(
1381
'Branch.lock_write', ('memory:///', '', ''),
1382
'success', ('ok', 'branch token', 'repo token'))
1383
client.add_expected_call(
1384
'Branch.set_config_option', ('memory:///', 'branch token',
1385
'repo token', 'foo', 'bar', ''),
1387
client.add_expected_call(
1388
'Branch.unlock', ('memory:///', 'branch token', 'repo token'),
1390
transport = MemoryTransport()
1391
branch = self.make_remote_branch(transport, client)
1393
config = branch._get_config()
1394
config.set_option('foo', 'bar')
1396
client.finished_test()
1398
def test_backwards_compat_set_option(self):
1399
self.setup_smart_server_with_call_log()
1400
branch = self.make_branch('.')
1401
verb = 'Branch.set_config_option'
1402
self.disable_verb(verb)
1404
self.addCleanup(branch.unlock)
1405
self.reset_smart_call_log()
1406
branch._get_config().set_option('value', 'name')
1407
self.assertLength(10, self.hpss_calls)
1408
self.assertEqual('value', branch._get_config().get_option('name'))
1411
class TestBranchLockWrite(RemoteBranchTestCase):
1413
def test_lock_write_unlockable(self):
1414
transport = MemoryTransport()
1415
client = FakeClient(transport.base)
1416
client.add_expected_call(
1417
'Branch.get_stacked_on_url', ('quack/',),
1418
'error', ('NotStacked',),)
1419
client.add_expected_call(
1420
'Branch.lock_write', ('quack/', '', ''),
1421
'error', ('UnlockableTransport',))
1422
transport.mkdir('quack')
1423
transport = transport.clone('quack')
1424
branch = self.make_remote_branch(transport, client)
1425
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
1426
client.finished_test()
1429
class TestBzrDirGetSetConfig(RemoteBzrDirTestCase):
1431
def test__get_config(self):
1432
client = FakeClient()
1433
client.add_success_response_with_body('default_stack_on = /\n', 'ok')
1434
transport = MemoryTransport()
1435
bzrdir = self.make_remote_bzrdir(transport, client)
1436
config = bzrdir.get_config()
1437
self.assertEqual('/', config.get_default_stack_on())
1439
[('call_expecting_body', 'BzrDir.get_config_file', ('memory:///',))],
1442
def test_set_option_uses_vfs(self):
1443
self.setup_smart_server_with_call_log()
1444
bzrdir = self.make_bzrdir('.')
1445
self.reset_smart_call_log()
1446
config = bzrdir.get_config()
1447
config.set_default_stack_on('/')
1448
self.assertLength(3, self.hpss_calls)
1450
def test_backwards_compat_get_option(self):
1451
self.setup_smart_server_with_call_log()
1452
bzrdir = self.make_bzrdir('.')
1453
verb = 'BzrDir.get_config_file'
1454
self.disable_verb(verb)
1455
self.reset_smart_call_log()
1456
self.assertEqual(None,
1457
bzrdir._get_config().get_option('default_stack_on'))
1458
self.assertLength(3, self.hpss_calls)
1461
class TestTransportIsReadonly(tests.TestCase):
1463
def test_true(self):
1464
client = FakeClient()
1465
client.add_success_response('yes')
1466
transport = RemoteTransport('bzr://example.com/', medium=False,
1468
self.assertEqual(True, transport.is_readonly())
1470
[('call', 'Transport.is_readonly', ())],
1473
def test_false(self):
1474
client = FakeClient()
1475
client.add_success_response('no')
1476
transport = RemoteTransport('bzr://example.com/', medium=False,
1478
self.assertEqual(False, transport.is_readonly())
1480
[('call', 'Transport.is_readonly', ())],
1483
def test_error_from_old_server(self):
1484
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
1486
Clients should treat it as a "no" response, because is_readonly is only
1487
advisory anyway (a transport could be read-write, but then the
1488
underlying filesystem could be readonly anyway).
1490
client = FakeClient()
1491
client.add_unknown_method_response('Transport.is_readonly')
1492
transport = RemoteTransport('bzr://example.com/', medium=False,
1494
self.assertEqual(False, transport.is_readonly())
1496
[('call', 'Transport.is_readonly', ())],
1500
class TestTransportMkdir(tests.TestCase):
1502
def test_permissiondenied(self):
1503
client = FakeClient()
1504
client.add_error_response('PermissionDenied', 'remote path', 'extra')
1505
transport = RemoteTransport('bzr://example.com/', medium=False,
1507
exc = self.assertRaises(
1508
errors.PermissionDenied, transport.mkdir, 'client path')
1509
expected_error = errors.PermissionDenied('/client path', 'extra')
1510
self.assertEqual(expected_error, exc)
1513
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
1515
def test_defaults_to_getuser(self):
1516
t = RemoteSSHTransport('bzr+ssh://example.com')
1517
self.assertIs(getpass.getuser(), t._get_credentials()[0])
1519
def test_uses_authentication_config(self):
1520
conf = config.AuthenticationConfig()
1521
conf._get_config().update(
1522
{'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
1525
t = RemoteSSHTransport('bzr+ssh://example.com')
1526
self.assertEqual('bar', t._get_credentials()[0])
1529
class TestRemoteRepository(TestRemote):
1530
"""Base for testing RemoteRepository protocol usage.
1532
These tests contain frozen requests and responses. We want any changes to
1533
what is sent or expected to be require a thoughtful update to these tests
1534
because they might break compatibility with different-versioned servers.
1537
def setup_fake_client_and_repository(self, transport_path):
1538
"""Create the fake client and repository for testing with.
1540
There's no real server here; we just have canned responses sent
1543
:param transport_path: Path below the root of the MemoryTransport
1544
where the repository will be created.
1546
transport = MemoryTransport()
1547
transport.mkdir(transport_path)
1548
client = FakeClient(transport.base)
1549
transport = transport.clone(transport_path)
1550
# we do not want bzrdir to make any remote calls
1551
bzrdir = RemoteBzrDir(transport, remote.RemoteBzrDirFormat(),
1553
repo = RemoteRepository(bzrdir, None, _client=client)
1557
class TestRepositoryFormat(TestRemoteRepository):
1559
def test_fast_delta(self):
1560
true_name = groupcompress_repo.RepositoryFormatCHK1().network_name()
1561
true_format = RemoteRepositoryFormat()
1562
true_format._network_name = true_name
1563
self.assertEqual(True, true_format.fast_deltas)
1564
false_name = pack_repo.RepositoryFormatKnitPack1().network_name()
1565
false_format = RemoteRepositoryFormat()
1566
false_format._network_name = false_name
1567
self.assertEqual(False, false_format.fast_deltas)
1570
class TestRepositoryGatherStats(TestRemoteRepository):
1572
def test_revid_none(self):
1573
# ('ok',), body with revisions and size
1574
transport_path = 'quack'
1575
repo, client = self.setup_fake_client_and_repository(transport_path)
1576
client.add_success_response_with_body(
1577
'revisions: 2\nsize: 18\n', 'ok')
1578
result = repo.gather_stats(None)
1580
[('call_expecting_body', 'Repository.gather_stats',
1581
('quack/','','no'))],
1583
self.assertEqual({'revisions': 2, 'size': 18}, result)
1585
def test_revid_no_committers(self):
1586
# ('ok',), body without committers
1587
body = ('firstrev: 123456.300 3600\n'
1588
'latestrev: 654231.400 0\n'
1591
transport_path = 'quick'
1592
revid = u'\xc8'.encode('utf8')
1593
repo, client = self.setup_fake_client_and_repository(transport_path)
1594
client.add_success_response_with_body(body, 'ok')
1595
result = repo.gather_stats(revid)
1597
[('call_expecting_body', 'Repository.gather_stats',
1598
('quick/', revid, 'no'))],
1600
self.assertEqual({'revisions': 2, 'size': 18,
1601
'firstrev': (123456.300, 3600),
1602
'latestrev': (654231.400, 0),},
1605
def test_revid_with_committers(self):
1606
# ('ok',), body with committers
1607
body = ('committers: 128\n'
1608
'firstrev: 123456.300 3600\n'
1609
'latestrev: 654231.400 0\n'
1612
transport_path = 'buick'
1613
revid = u'\xc8'.encode('utf8')
1614
repo, client = self.setup_fake_client_and_repository(transport_path)
1615
client.add_success_response_with_body(body, 'ok')
1616
result = repo.gather_stats(revid, True)
1618
[('call_expecting_body', 'Repository.gather_stats',
1619
('buick/', revid, 'yes'))],
1621
self.assertEqual({'revisions': 2, 'size': 18,
1623
'firstrev': (123456.300, 3600),
1624
'latestrev': (654231.400, 0),},
1628
class TestRepositoryGetGraph(TestRemoteRepository):
1630
def test_get_graph(self):
1631
# get_graph returns a graph with a custom parents provider.
1632
transport_path = 'quack'
1633
repo, client = self.setup_fake_client_and_repository(transport_path)
1634
graph = repo.get_graph()
1635
self.assertNotEqual(graph._parents_provider, repo)
1638
class TestRepositoryGetParentMap(TestRemoteRepository):
1640
def test_get_parent_map_caching(self):
1641
# get_parent_map returns from cache until unlock()
1642
# setup a reponse with two revisions
1643
r1 = u'\u0e33'.encode('utf8')
1644
r2 = u'\u0dab'.encode('utf8')
1645
lines = [' '.join([r2, r1]), r1]
1646
encoded_body = bz2.compress('\n'.join(lines))
1648
transport_path = 'quack'
1649
repo, client = self.setup_fake_client_and_repository(transport_path)
1650
client.add_success_response_with_body(encoded_body, 'ok')
1651
client.add_success_response_with_body(encoded_body, 'ok')
1653
graph = repo.get_graph()
1654
parents = graph.get_parent_map([r2])
1655
self.assertEqual({r2: (r1,)}, parents)
1656
# locking and unlocking deeper should not reset
1659
parents = graph.get_parent_map([r1])
1660
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1662
[('call_with_body_bytes_expecting_body',
1663
'Repository.get_parent_map', ('quack/', 'include-missing:', r2),
1667
# now we call again, and it should use the second response.
1669
graph = repo.get_graph()
1670
parents = graph.get_parent_map([r1])
1671
self.assertEqual({r1: (NULL_REVISION,)}, parents)
1673
[('call_with_body_bytes_expecting_body',
1674
'Repository.get_parent_map', ('quack/', 'include-missing:', r2),
1676
('call_with_body_bytes_expecting_body',
1677
'Repository.get_parent_map', ('quack/', 'include-missing:', r1),
1683
def test_get_parent_map_reconnects_if_unknown_method(self):
1684
transport_path = 'quack'
1685
rev_id = 'revision-id'
1686
repo, client = self.setup_fake_client_and_repository(transport_path)
1687
client.add_unknown_method_response('Repository.get_parent_map')
1688
client.add_success_response_with_body(rev_id, 'ok')
1689
self.assertFalse(client._medium._is_remote_before((1, 2)))
1690
parents = repo.get_parent_map([rev_id])
1692
[('call_with_body_bytes_expecting_body',
1693
'Repository.get_parent_map', ('quack/', 'include-missing:',
1695
('disconnect medium',),
1696
('call_expecting_body', 'Repository.get_revision_graph',
1699
# The medium is now marked as being connected to an older server
1700
self.assertTrue(client._medium._is_remote_before((1, 2)))
1701
self.assertEqual({rev_id: ('null:',)}, parents)
1703
def test_get_parent_map_fallback_parentless_node(self):
1704
"""get_parent_map falls back to get_revision_graph on old servers. The
1705
results from get_revision_graph are tweaked to match the get_parent_map
1708
Specifically, a {key: ()} result from get_revision_graph means "no
1709
parents" for that key, which in get_parent_map results should be
1710
represented as {key: ('null:',)}.
1712
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
1714
rev_id = 'revision-id'
1715
transport_path = 'quack'
1716
repo, client = self.setup_fake_client_and_repository(transport_path)
1717
client.add_success_response_with_body(rev_id, 'ok')
1718
client._medium._remember_remote_is_before((1, 2))
1719
parents = repo.get_parent_map([rev_id])
1721
[('call_expecting_body', 'Repository.get_revision_graph',
1724
self.assertEqual({rev_id: ('null:',)}, parents)
1726
def test_get_parent_map_unexpected_response(self):
1727
repo, client = self.setup_fake_client_and_repository('path')
1728
client.add_success_response('something unexpected!')
1730
errors.UnexpectedSmartServerResponse,
1731
repo.get_parent_map, ['a-revision-id'])
1733
def test_get_parent_map_negative_caches_missing_keys(self):
1734
self.setup_smart_server_with_call_log()
1735
repo = self.make_repository('foo')
1736
self.assertIsInstance(repo, RemoteRepository)
1738
self.addCleanup(repo.unlock)
1739
self.reset_smart_call_log()
1740
graph = repo.get_graph()
1741
self.assertEqual({},
1742
graph.get_parent_map(['some-missing', 'other-missing']))
1743
self.assertLength(1, self.hpss_calls)
1744
# No call if we repeat this
1745
self.reset_smart_call_log()
1746
graph = repo.get_graph()
1747
self.assertEqual({},
1748
graph.get_parent_map(['some-missing', 'other-missing']))
1749
self.assertLength(0, self.hpss_calls)
1750
# Asking for more unknown keys makes a request.
1751
self.reset_smart_call_log()
1752
graph = repo.get_graph()
1753
self.assertEqual({},
1754
graph.get_parent_map(['some-missing', 'other-missing',
1756
self.assertLength(1, self.hpss_calls)
1758
def disableExtraResults(self):
1759
old_flag = SmartServerRepositoryGetParentMap.no_extra_results
1760
SmartServerRepositoryGetParentMap.no_extra_results = True
1762
SmartServerRepositoryGetParentMap.no_extra_results = old_flag
1763
self.addCleanup(reset_values)
1765
def test_null_cached_missing_and_stop_key(self):
1766
self.setup_smart_server_with_call_log()
1767
# Make a branch with a single revision.
1768
builder = self.make_branch_builder('foo')
1769
builder.start_series()
1770
builder.build_snapshot('first', None, [
1771
('add', ('', 'root-id', 'directory', ''))])
1772
builder.finish_series()
1773
branch = builder.get_branch()
1774
repo = branch.repository
1775
self.assertIsInstance(repo, RemoteRepository)
1776
# Stop the server from sending extra results.
1777
self.disableExtraResults()
1779
self.addCleanup(repo.unlock)
1780
self.reset_smart_call_log()
1781
graph = repo.get_graph()
1782
# Query for 'first' and 'null:'. Because 'null:' is a parent of
1783
# 'first' it will be a candidate for the stop_keys of subsequent
1784
# requests, and because 'null:' was queried but not returned it will be
1785
# cached as missing.
1786
self.assertEqual({'first': ('null:',)},
1787
graph.get_parent_map(['first', 'null:']))
1788
# Now query for another key. This request will pass along a recipe of
1789
# start and stop keys describing the already cached results, and this
1790
# recipe's revision count must be correct (or else it will trigger an
1791
# error from the server).
1792
self.assertEqual({}, graph.get_parent_map(['another-key']))
1793
# This assertion guards against disableExtraResults silently failing to
1794
# work, thus invalidating the test.
1795
self.assertLength(2, self.hpss_calls)
1797
def test_get_parent_map_gets_ghosts_from_result(self):
1798
# asking for a revision should negatively cache close ghosts in its
1800
self.setup_smart_server_with_call_log()
1801
tree = self.make_branch_and_memory_tree('foo')
1804
builder = treebuilder.TreeBuilder()
1805
builder.start_tree(tree)
1807
builder.finish_tree()
1808
tree.set_parent_ids(['non-existant'], allow_leftmost_as_ghost=True)
1809
rev_id = tree.commit('')
1813
self.addCleanup(tree.unlock)
1814
repo = tree.branch.repository
1815
self.assertIsInstance(repo, RemoteRepository)
1817
repo.get_parent_map([rev_id])
1818
self.reset_smart_call_log()
1819
# Now asking for rev_id's ghost parent should not make calls
1820
self.assertEqual({}, repo.get_parent_map(['non-existant']))
1821
self.assertLength(0, self.hpss_calls)
1824
class TestGetParentMapAllowsNew(tests.TestCaseWithTransport):
1826
def test_allows_new_revisions(self):
1827
"""get_parent_map's results can be updated by commit."""
1828
smart_server = server.SmartTCPServer_for_testing()
1829
smart_server.setUp()
1830
self.addCleanup(smart_server.tearDown)
1831
self.make_branch('branch')
1832
branch = Branch.open(smart_server.get_url() + '/branch')
1833
tree = branch.create_checkout('tree', lightweight=True)
1835
self.addCleanup(tree.unlock)
1836
graph = tree.branch.repository.get_graph()
1837
# This provides an opportunity for the missing rev-id to be cached.
1838
self.assertEqual({}, graph.get_parent_map(['rev1']))
1839
tree.commit('message', rev_id='rev1')
1840
graph = tree.branch.repository.get_graph()
1841
self.assertEqual({'rev1': ('null:',)}, graph.get_parent_map(['rev1']))
1844
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
1846
def test_null_revision(self):
1847
# a null revision has the predictable result {}, we should have no wire
1848
# traffic when calling it with this argument
1849
transport_path = 'empty'
1850
repo, client = self.setup_fake_client_and_repository(transport_path)
1851
client.add_success_response('notused')
1852
# actual RemoteRepository.get_revision_graph is gone, but there's an
1853
# equivalent private method for testing
1854
result = repo._get_revision_graph(NULL_REVISION)
1855
self.assertEqual([], client._calls)
1856
self.assertEqual({}, result)
1858
def test_none_revision(self):
1859
# with none we want the entire graph
1860
r1 = u'\u0e33'.encode('utf8')
1861
r2 = u'\u0dab'.encode('utf8')
1862
lines = [' '.join([r2, r1]), r1]
1863
encoded_body = '\n'.join(lines)
1865
transport_path = 'sinhala'
1866
repo, client = self.setup_fake_client_and_repository(transport_path)
1867
client.add_success_response_with_body(encoded_body, 'ok')
1868
# actual RemoteRepository.get_revision_graph is gone, but there's an
1869
# equivalent private method for testing
1870
result = repo._get_revision_graph(None)
1872
[('call_expecting_body', 'Repository.get_revision_graph',
1875
self.assertEqual({r1: (), r2: (r1, )}, result)
1877
def test_specific_revision(self):
1878
# with a specific revision we want the graph for that
1879
# with none we want the entire graph
1880
r11 = u'\u0e33'.encode('utf8')
1881
r12 = u'\xc9'.encode('utf8')
1882
r2 = u'\u0dab'.encode('utf8')
1883
lines = [' '.join([r2, r11, r12]), r11, r12]
1884
encoded_body = '\n'.join(lines)
1886
transport_path = 'sinhala'
1887
repo, client = self.setup_fake_client_and_repository(transport_path)
1888
client.add_success_response_with_body(encoded_body, 'ok')
1889
result = repo._get_revision_graph(r2)
1891
[('call_expecting_body', 'Repository.get_revision_graph',
1894
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1896
def test_no_such_revision(self):
1898
transport_path = 'sinhala'
1899
repo, client = self.setup_fake_client_and_repository(transport_path)
1900
client.add_error_response('nosuchrevision', revid)
1901
# also check that the right revision is reported in the error
1902
self.assertRaises(errors.NoSuchRevision,
1903
repo._get_revision_graph, revid)
1905
[('call_expecting_body', 'Repository.get_revision_graph',
1906
('sinhala/', revid))],
1909
def test_unexpected_error(self):
1911
transport_path = 'sinhala'
1912
repo, client = self.setup_fake_client_and_repository(transport_path)
1913
client.add_error_response('AnUnexpectedError')
1914
e = self.assertRaises(errors.UnknownErrorFromSmartServer,
1915
repo._get_revision_graph, revid)
1916
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1919
class TestRepositoryIsShared(TestRemoteRepository):
1921
def test_is_shared(self):
1922
# ('yes', ) for Repository.is_shared -> 'True'.
1923
transport_path = 'quack'
1924
repo, client = self.setup_fake_client_and_repository(transport_path)
1925
client.add_success_response('yes')
1926
result = repo.is_shared()
1928
[('call', 'Repository.is_shared', ('quack/',))],
1930
self.assertEqual(True, result)
1932
def test_is_not_shared(self):
1933
# ('no', ) for Repository.is_shared -> 'False'.
1934
transport_path = 'qwack'
1935
repo, client = self.setup_fake_client_and_repository(transport_path)
1936
client.add_success_response('no')
1937
result = repo.is_shared()
1939
[('call', 'Repository.is_shared', ('qwack/',))],
1941
self.assertEqual(False, result)
1944
class TestRepositoryLockWrite(TestRemoteRepository):
1946
def test_lock_write(self):
1947
transport_path = 'quack'
1948
repo, client = self.setup_fake_client_and_repository(transport_path)
1949
client.add_success_response('ok', 'a token')
1950
result = repo.lock_write()
1952
[('call', 'Repository.lock_write', ('quack/', ''))],
1954
self.assertEqual('a token', result)
1956
def test_lock_write_already_locked(self):
1957
transport_path = 'quack'
1958
repo, client = self.setup_fake_client_and_repository(transport_path)
1959
client.add_error_response('LockContention')
1960
self.assertRaises(errors.LockContention, repo.lock_write)
1962
[('call', 'Repository.lock_write', ('quack/', ''))],
1965
def test_lock_write_unlockable(self):
1966
transport_path = 'quack'
1967
repo, client = self.setup_fake_client_and_repository(transport_path)
1968
client.add_error_response('UnlockableTransport')
1969
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1971
[('call', 'Repository.lock_write', ('quack/', ''))],
1975
class TestRepositorySetMakeWorkingTrees(TestRemoteRepository):
1977
def test_backwards_compat(self):
1978
self.setup_smart_server_with_call_log()
1979
repo = self.make_repository('.')
1980
self.reset_smart_call_log()
1981
verb = 'Repository.set_make_working_trees'
1982
self.disable_verb(verb)
1983
repo.set_make_working_trees(True)
1984
call_count = len([call for call in self.hpss_calls if
1985
call.call.method == verb])
1986
self.assertEqual(1, call_count)
1988
def test_current(self):
1989
transport_path = 'quack'
1990
repo, client = self.setup_fake_client_and_repository(transport_path)
1991
client.add_expected_call(
1992
'Repository.set_make_working_trees', ('quack/', 'True'),
1994
client.add_expected_call(
1995
'Repository.set_make_working_trees', ('quack/', 'False'),
1997
repo.set_make_working_trees(True)
1998
repo.set_make_working_trees(False)
2001
class TestRepositoryUnlock(TestRemoteRepository):
2003
def test_unlock(self):
2004
transport_path = 'quack'
2005
repo, client = self.setup_fake_client_and_repository(transport_path)
2006
client.add_success_response('ok', 'a token')
2007
client.add_success_response('ok')
2011
[('call', 'Repository.lock_write', ('quack/', '')),
2012
('call', 'Repository.unlock', ('quack/', 'a token'))],
2015
def test_unlock_wrong_token(self):
2016
# If somehow the token is wrong, unlock will raise TokenMismatch.
2017
transport_path = 'quack'
2018
repo, client = self.setup_fake_client_and_repository(transport_path)
2019
client.add_success_response('ok', 'a token')
2020
client.add_error_response('TokenMismatch')
2022
self.assertRaises(errors.TokenMismatch, repo.unlock)
2025
class TestRepositoryHasRevision(TestRemoteRepository):
2027
def test_none(self):
2028
# repo.has_revision(None) should not cause any traffic.
2029
transport_path = 'quack'
2030
repo, client = self.setup_fake_client_and_repository(transport_path)
2032
# The null revision is always there, so has_revision(None) == True.
2033
self.assertEqual(True, repo.has_revision(NULL_REVISION))
2035
# The remote repo shouldn't be accessed.
2036
self.assertEqual([], client._calls)
2039
class TestRepositoryInsertStream(TestRemoteRepository):
2041
def test_unlocked_repo(self):
2042
transport_path = 'quack'
2043
repo, client = self.setup_fake_client_and_repository(transport_path)
2044
client.add_expected_call(
2045
'Repository.insert_stream', ('quack/', ''),
2047
client.add_expected_call(
2048
'Repository.insert_stream', ('quack/', ''),
2050
sink = repo._get_sink()
2051
fmt = repository.RepositoryFormat.get_default_format()
2052
resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
2053
self.assertEqual([], resume_tokens)
2054
self.assertEqual(set(), missing_keys)
2055
client.finished_test()
2057
def test_locked_repo_with_no_lock_token(self):
2058
transport_path = 'quack'
2059
repo, client = self.setup_fake_client_and_repository(transport_path)
2060
client.add_expected_call(
2061
'Repository.lock_write', ('quack/', ''),
2062
'success', ('ok', ''))
2063
client.add_expected_call(
2064
'Repository.insert_stream', ('quack/', ''),
2066
client.add_expected_call(
2067
'Repository.insert_stream', ('quack/', ''),
2070
sink = repo._get_sink()
2071
fmt = repository.RepositoryFormat.get_default_format()
2072
resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
2073
self.assertEqual([], resume_tokens)
2074
self.assertEqual(set(), missing_keys)
2075
client.finished_test()
2077
def test_locked_repo_with_lock_token(self):
2078
transport_path = 'quack'
2079
repo, client = self.setup_fake_client_and_repository(transport_path)
2080
client.add_expected_call(
2081
'Repository.lock_write', ('quack/', ''),
2082
'success', ('ok', 'a token'))
2083
client.add_expected_call(
2084
'Repository.insert_stream_locked', ('quack/', '', 'a token'),
2086
client.add_expected_call(
2087
'Repository.insert_stream_locked', ('quack/', '', 'a token'),
2090
sink = repo._get_sink()
2091
fmt = repository.RepositoryFormat.get_default_format()
2092
resume_tokens, missing_keys = sink.insert_stream([], fmt, [])
2093
self.assertEqual([], resume_tokens)
2094
self.assertEqual(set(), missing_keys)
2095
client.finished_test()
2098
class TestRepositoryTarball(TestRemoteRepository):
2100
# This is a canned tarball reponse we can validate against
2102
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
2103
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
2104
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
2105
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
2106
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
2107
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
2108
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
2109
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
2110
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
2111
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
2112
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
2113
'nWQ7QH/F3JFOFCQ0aSPfA='
2116
def test_repository_tarball(self):
2117
# Test that Repository.tarball generates the right operations
2118
transport_path = 'repo'
2119
expected_calls = [('call_expecting_body', 'Repository.tarball',
2120
('repo/', 'bz2',),),
2122
repo, client = self.setup_fake_client_and_repository(transport_path)
2123
client.add_success_response_with_body(self.tarball_content, 'ok')
2124
# Now actually ask for the tarball
2125
tarball_file = repo._get_tarball('bz2')
2127
self.assertEqual(expected_calls, client._calls)
2128
self.assertEqual(self.tarball_content, tarball_file.read())
2130
tarball_file.close()
2133
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
2134
"""RemoteRepository.copy_content_into optimizations"""
2136
def test_copy_content_remote_to_local(self):
2137
self.transport_server = server.SmartTCPServer_for_testing
2138
src_repo = self.make_repository('repo1')
2139
src_repo = repository.Repository.open(self.get_url('repo1'))
2140
# At the moment the tarball-based copy_content_into can't write back
2141
# into a smart server. It would be good if it could upload the
2142
# tarball; once that works we'd have to create repositories of
2143
# different formats. -- mbp 20070410
2144
dest_url = self.get_vfs_only_url('repo2')
2145
dest_bzrdir = BzrDir.create(dest_url)
2146
dest_repo = dest_bzrdir.create_repository()
2147
self.assertFalse(isinstance(dest_repo, RemoteRepository))
2148
self.assertTrue(isinstance(src_repo, RemoteRepository))
2149
src_repo.copy_content_into(dest_repo)
2152
class _StubRealPackRepository(object):
2154
def __init__(self, calls):
2156
self._pack_collection = _StubPackCollection(calls)
2158
def is_in_write_group(self):
2161
def refresh_data(self):
2162
self.calls.append(('pack collection reload_pack_names',))
2165
class _StubPackCollection(object):
2167
def __init__(self, calls):
2171
self.calls.append(('pack collection autopack',))
2174
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
2175
"""Tests for RemoteRepository.autopack implementation."""
2178
"""When the server returns 'ok' and there's no _real_repository, then
2179
nothing else happens: the autopack method is done.
2181
transport_path = 'quack'
2182
repo, client = self.setup_fake_client_and_repository(transport_path)
2183
client.add_expected_call(
2184
'PackRepository.autopack', ('quack/',), 'success', ('ok',))
2186
client.finished_test()
2188
def test_ok_with_real_repo(self):
2189
"""When the server returns 'ok' and there is a _real_repository, then
2190
the _real_repository's reload_pack_name's method will be called.
2192
transport_path = 'quack'
2193
repo, client = self.setup_fake_client_and_repository(transport_path)
2194
client.add_expected_call(
2195
'PackRepository.autopack', ('quack/',),
2197
repo._real_repository = _StubRealPackRepository(client._calls)
2200
[('call', 'PackRepository.autopack', ('quack/',)),
2201
('pack collection reload_pack_names',)],
2204
def test_backwards_compatibility(self):
2205
"""If the server does not recognise the PackRepository.autopack verb,
2206
fallback to the real_repository's implementation.
2208
transport_path = 'quack'
2209
repo, client = self.setup_fake_client_and_repository(transport_path)
2210
client.add_unknown_method_response('PackRepository.autopack')
2211
def stub_ensure_real():
2212
client._calls.append(('_ensure_real',))
2213
repo._real_repository = _StubRealPackRepository(client._calls)
2214
repo._ensure_real = stub_ensure_real
2217
[('call', 'PackRepository.autopack', ('quack/',)),
2219
('pack collection autopack',)],
2223
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
2224
"""Base class for unit tests for bzrlib.remote._translate_error."""
2226
def translateTuple(self, error_tuple, **context):
2227
"""Call _translate_error with an ErrorFromSmartServer built from the
2230
:param error_tuple: A tuple of a smart server response, as would be
2231
passed to an ErrorFromSmartServer.
2232
:kwargs context: context items to call _translate_error with.
2234
:returns: The error raised by _translate_error.
2236
# Raise the ErrorFromSmartServer before passing it as an argument,
2237
# because _translate_error may need to re-raise it with a bare 'raise'
2239
server_error = errors.ErrorFromSmartServer(error_tuple)
2240
translated_error = self.translateErrorFromSmartServer(
2241
server_error, **context)
2242
return translated_error
2244
def translateErrorFromSmartServer(self, error_object, **context):
2245
"""Like translateTuple, but takes an already constructed
2246
ErrorFromSmartServer rather than a tuple.
2250
except errors.ErrorFromSmartServer, server_error:
2251
translated_error = self.assertRaises(
2252
errors.BzrError, remote._translate_error, server_error,
2254
return translated_error
2257
class TestErrorTranslationSuccess(TestErrorTranslationBase):
2258
"""Unit tests for bzrlib.remote._translate_error.
2260
Given an ErrorFromSmartServer (which has an error tuple from a smart
2261
server) and some context, _translate_error raises more specific errors from
2264
This test case covers the cases where _translate_error succeeds in
2265
translating an ErrorFromSmartServer to something better. See
2266
TestErrorTranslationRobustness for other cases.
2269
def test_NoSuchRevision(self):
2270
branch = self.make_branch('')
2272
translated_error = self.translateTuple(
2273
('NoSuchRevision', revid), branch=branch)
2274
expected_error = errors.NoSuchRevision(branch, revid)
2275
self.assertEqual(expected_error, translated_error)
2277
def test_nosuchrevision(self):
2278
repository = self.make_repository('')
2280
translated_error = self.translateTuple(
2281
('nosuchrevision', revid), repository=repository)
2282
expected_error = errors.NoSuchRevision(repository, revid)
2283
self.assertEqual(expected_error, translated_error)
2285
def test_nobranch(self):
2286
bzrdir = self.make_bzrdir('')
2287
translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
2288
expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
2289
self.assertEqual(expected_error, translated_error)
2291
def test_LockContention(self):
2292
translated_error = self.translateTuple(('LockContention',))
2293
expected_error = errors.LockContention('(remote lock)')
2294
self.assertEqual(expected_error, translated_error)
2296
def test_UnlockableTransport(self):
2297
bzrdir = self.make_bzrdir('')
2298
translated_error = self.translateTuple(
2299
('UnlockableTransport',), bzrdir=bzrdir)
2300
expected_error = errors.UnlockableTransport(bzrdir.root_transport)
2301
self.assertEqual(expected_error, translated_error)
2303
def test_LockFailed(self):
2304
lock = 'str() of a server lock'
2305
why = 'str() of why'
2306
translated_error = self.translateTuple(('LockFailed', lock, why))
2307
expected_error = errors.LockFailed(lock, why)
2308
self.assertEqual(expected_error, translated_error)
2310
def test_TokenMismatch(self):
2311
token = 'a lock token'
2312
translated_error = self.translateTuple(('TokenMismatch',), token=token)
2313
expected_error = errors.TokenMismatch(token, '(remote token)')
2314
self.assertEqual(expected_error, translated_error)
2316
def test_Diverged(self):
2317
branch = self.make_branch('a')
2318
other_branch = self.make_branch('b')
2319
translated_error = self.translateTuple(
2320
('Diverged',), branch=branch, other_branch=other_branch)
2321
expected_error = errors.DivergedBranches(branch, other_branch)
2322
self.assertEqual(expected_error, translated_error)
2324
def test_ReadError_no_args(self):
2326
translated_error = self.translateTuple(('ReadError',), path=path)
2327
expected_error = errors.ReadError(path)
2328
self.assertEqual(expected_error, translated_error)
2330
def test_ReadError(self):
2332
translated_error = self.translateTuple(('ReadError', path))
2333
expected_error = errors.ReadError(path)
2334
self.assertEqual(expected_error, translated_error)
2336
def test_PermissionDenied_no_args(self):
2338
translated_error = self.translateTuple(('PermissionDenied',), path=path)
2339
expected_error = errors.PermissionDenied(path)
2340
self.assertEqual(expected_error, translated_error)
2342
def test_PermissionDenied_one_arg(self):
2344
translated_error = self.translateTuple(('PermissionDenied', path))
2345
expected_error = errors.PermissionDenied(path)
2346
self.assertEqual(expected_error, translated_error)
2348
def test_PermissionDenied_one_arg_and_context(self):
2349
"""Given a choice between a path from the local context and a path on
2350
the wire, _translate_error prefers the path from the local context.
2352
local_path = 'local path'
2353
remote_path = 'remote path'
2354
translated_error = self.translateTuple(
2355
('PermissionDenied', remote_path), path=local_path)
2356
expected_error = errors.PermissionDenied(local_path)
2357
self.assertEqual(expected_error, translated_error)
2359
def test_PermissionDenied_two_args(self):
2361
extra = 'a string with extra info'
2362
translated_error = self.translateTuple(
2363
('PermissionDenied', path, extra))
2364
expected_error = errors.PermissionDenied(path, extra)
2365
self.assertEqual(expected_error, translated_error)
2368
class TestErrorTranslationRobustness(TestErrorTranslationBase):
2369
"""Unit tests for bzrlib.remote._translate_error's robustness.
2371
TestErrorTranslationSuccess is for cases where _translate_error can
2372
translate successfully. This class about how _translate_err behaves when
2373
it fails to translate: it re-raises the original error.
2376
def test_unrecognised_server_error(self):
2377
"""If the error code from the server is not recognised, the original
2378
ErrorFromSmartServer is propagated unmodified.
2380
error_tuple = ('An unknown error tuple',)
2381
server_error = errors.ErrorFromSmartServer(error_tuple)
2382
translated_error = self.translateErrorFromSmartServer(server_error)
2383
expected_error = errors.UnknownErrorFromSmartServer(server_error)
2384
self.assertEqual(expected_error, translated_error)
2386
def test_context_missing_a_key(self):
2387
"""In case of a bug in the client, or perhaps an unexpected response
2388
from a server, _translate_error returns the original error tuple from
2389
the server and mutters a warning.
2391
# To translate a NoSuchRevision error _translate_error needs a 'branch'
2392
# in the context dict. So let's give it an empty context dict instead
2393
# to exercise its error recovery.
2395
error_tuple = ('NoSuchRevision', 'revid')
2396
server_error = errors.ErrorFromSmartServer(error_tuple)
2397
translated_error = self.translateErrorFromSmartServer(server_error)
2398
self.assertEqual(server_error, translated_error)
2399
# In addition to re-raising ErrorFromSmartServer, some debug info has
2400
# been muttered to the log file for developer to look at.
2401
self.assertContainsRe(
2402
self._get_log(keep_log_file=True),
2403
"Missing key 'branch' in context")
2405
def test_path_missing(self):
2406
"""Some translations (PermissionDenied, ReadError) can determine the
2407
'path' variable from either the wire or the local context. If neither
2408
has it, then an error is raised.
2410
error_tuple = ('ReadError',)
2411
server_error = errors.ErrorFromSmartServer(error_tuple)
2412
translated_error = self.translateErrorFromSmartServer(server_error)
2413
self.assertEqual(server_error, translated_error)
2414
# In addition to re-raising ErrorFromSmartServer, some debug info has
2415
# been muttered to the log file for developer to look at.
2416
self.assertContainsRe(
2417
self._get_log(keep_log_file=True), "Missing key 'path' in context")
2420
class TestStacking(tests.TestCaseWithTransport):
2421
"""Tests for operations on stacked remote repositories.
2423
The underlying format type must support stacking.
2426
def test_access_stacked_remote(self):
2427
# based on <http://launchpad.net/bugs/261315>
2428
# make a branch stacked on another repository containing an empty
2429
# revision, then open it over hpss - we should be able to see that
2431
base_transport = self.get_transport()
2432
base_builder = self.make_branch_builder('base', format='1.9')
2433
base_builder.start_series()
2434
base_revid = base_builder.build_snapshot('rev-id', None,
2435
[('add', ('', None, 'directory', None))],
2437
base_builder.finish_series()
2438
stacked_branch = self.make_branch('stacked', format='1.9')
2439
stacked_branch.set_stacked_on_url('../base')
2440
# start a server looking at this
2441
smart_server = server.SmartTCPServer_for_testing()
2442
smart_server.setUp()
2443
self.addCleanup(smart_server.tearDown)
2444
remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
2445
# can get its branch and repository
2446
remote_branch = remote_bzrdir.open_branch()
2447
remote_repo = remote_branch.repository
2448
remote_repo.lock_read()
2450
# it should have an appropriate fallback repository, which should also
2451
# be a RemoteRepository
2452
self.assertLength(1, remote_repo._fallback_repositories)
2453
self.assertIsInstance(remote_repo._fallback_repositories[0],
2455
# and it has the revision committed to the underlying repository;
2456
# these have varying implementations so we try several of them
2457
self.assertTrue(remote_repo.has_revisions([base_revid]))
2458
self.assertTrue(remote_repo.has_revision(base_revid))
2459
self.assertEqual(remote_repo.get_revision(base_revid).message,
2462
remote_repo.unlock()
2464
def prepare_stacked_remote_branch(self):
2465
"""Get stacked_upon and stacked branches with content in each."""
2466
self.setup_smart_server_with_call_log()
2467
tree1 = self.make_branch_and_tree('tree1', format='1.9')
2468
tree1.commit('rev1', rev_id='rev1')
2469
tree2 = tree1.branch.bzrdir.sprout('tree2', stacked=True
2470
).open_workingtree()
2471
tree2.commit('local changes make me feel good.')
2472
branch2 = Branch.open(self.get_url('tree2'))
2474
self.addCleanup(branch2.unlock)
2475
return tree1.branch, branch2
2477
def test_stacked_get_parent_map(self):
2478
# the public implementation of get_parent_map obeys stacking
2479
_, branch = self.prepare_stacked_remote_branch()
2480
repo = branch.repository
2481
self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
2483
def test_unstacked_get_parent_map(self):
2484
# _unstacked_provider.get_parent_map ignores stacking
2485
_, branch = self.prepare_stacked_remote_branch()
2486
provider = branch.repository._unstacked_provider
2487
self.assertEqual([], provider.get_parent_map(['rev1']).keys())
2489
def fetch_stream_to_rev_order(self, stream):
2491
for kind, substream in stream:
2492
if not kind == 'revisions':
2495
for content in substream:
2496
result.append(content.key[-1])
2499
def get_ordered_revs(self, format, order):
2500
"""Get a list of the revisions in a stream to format format.
2502
:param format: The format of the target.
2503
:param order: the order that target should have requested.
2504
:result: The revision ids in the stream, in the order seen,
2505
the topological order of revisions in the source.
2507
unordered_format = bzrdir.format_registry.get(format)()
2508
target_repository_format = unordered_format.repository_format
2510
self.assertEqual(order, target_repository_format._fetch_order)
2511
trunk, stacked = self.prepare_stacked_remote_branch()
2512
source = stacked.repository._get_source(target_repository_format)
2513
tip = stacked.last_revision()
2514
revs = stacked.repository.get_ancestry(tip)
2515
search = graph.PendingAncestryResult([tip], stacked.repository)
2516
self.reset_smart_call_log()
2517
stream = source.get_stream(search)
2520
# We trust that if a revision is in the stream the rest of the new
2521
# content for it is too, as per our main fetch tests; here we are
2522
# checking that the revisions are actually included at all, and their
2524
return self.fetch_stream_to_rev_order(stream), revs
2526
def test_stacked_get_stream_unordered(self):
2527
# Repository._get_source.get_stream() from a stacked repository with
2528
# unordered yields the full data from both stacked and stacked upon
2530
rev_ord, expected_revs = self.get_ordered_revs('1.9', 'unordered')
2531
self.assertEqual(set(expected_revs), set(rev_ord))
2532
# Getting unordered results should have made a streaming data request
2533
# from the server, then one from the backing branch.
2534
self.assertLength(2, self.hpss_calls)
2536
def test_stacked_get_stream_topological(self):
2537
# Repository._get_source.get_stream() from a stacked repository with
2538
# topological sorting yields the full data from both stacked and
2539
# stacked upon sources in topological order.
2540
rev_ord, expected_revs = self.get_ordered_revs('knit', 'topological')
2541
self.assertEqual(expected_revs, rev_ord)
2542
# Getting topological sort requires VFS calls still
2543
self.assertLength(12, self.hpss_calls)
2545
def test_stacked_get_stream_groupcompress(self):
2546
# Repository._get_source.get_stream() from a stacked repository with
2547
# groupcompress sorting yields the full data from both stacked and
2548
# stacked upon sources in groupcompress order.
2549
raise tests.TestSkipped('No groupcompress ordered format available')
2550
rev_ord, expected_revs = self.get_ordered_revs('dev5', 'groupcompress')
2551
self.assertEqual(expected_revs, reversed(rev_ord))
2552
# Getting unordered results should have made a streaming data request
2553
# from the backing branch, and one from the stacked on branch.
2554
self.assertLength(2, self.hpss_calls)
2557
class TestRemoteBranchEffort(tests.TestCaseWithTransport):
2560
super(TestRemoteBranchEffort, self).setUp()
2561
# Create a smart server that publishes whatever the backing VFS server
2563
self.smart_server = server.SmartTCPServer_for_testing()
2564
self.smart_server.setUp(self.get_server())
2565
self.addCleanup(self.smart_server.tearDown)
2566
# Log all HPSS calls into self.hpss_calls.
2567
_SmartClient.hooks.install_named_hook(
2568
'call', self.capture_hpss_call, None)
2569
self.hpss_calls = []
2571
def capture_hpss_call(self, params):
2572
self.hpss_calls.append(params.method)
2574
def test_copy_content_into_avoids_revision_history(self):
2575
local = self.make_branch('local')
2576
remote_backing_tree = self.make_branch_and_tree('remote')
2577
remote_backing_tree.commit("Commit.")
2578
remote_branch_url = self.smart_server.get_url() + 'remote'
2579
remote_branch = bzrdir.BzrDir.open(remote_branch_url).open_branch()
2580
local.repository.fetch(remote_branch.repository)
2581
self.hpss_calls = []
2582
remote_branch.copy_content_into(local)
2583
self.assertFalse('Branch.revision_history' in self.hpss_calls)