1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests for remote bzrdir/branch/repo/etc
19
These are proxy objects which act on remote objects by sending messages
20
through a smart client. The proxies are to be created when attempting to open
21
the object given a transport that supports smartserver rpc operations.
23
These tests correspond to tests.test_smart, which exercises the server side.
27
from cStringIO import StringIO
37
from bzrlib.branch import Branch
38
from bzrlib.bzrdir import BzrDir, BzrDirFormat
39
from bzrlib.remote import (
45
from bzrlib.revision import NULL_REVISION
46
from bzrlib.smart import server, medium
47
from bzrlib.smart.client import _SmartClient
48
from bzrlib.symbol_versioning import one_four
49
from bzrlib.transport import get_transport, http
50
from bzrlib.transport.memory import MemoryTransport
51
from bzrlib.transport.remote import RemoteTransport, RemoteTCPTransport
54
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
57
self.transport_server = server.SmartTCPServer_for_testing
58
super(BasicRemoteObjectTests, self).setUp()
59
self.transport = self.get_transport()
60
# make a branch that can be opened over the smart transport
61
self.local_wt = BzrDir.create_standalone_workingtree('.')
64
self.transport.disconnect()
65
tests.TestCaseWithTransport.tearDown(self)
67
def test_create_remote_bzrdir(self):
68
b = remote.RemoteBzrDir(self.transport)
69
self.assertIsInstance(b, BzrDir)
71
def test_open_remote_branch(self):
72
# open a standalone branch in the working directory
73
b = remote.RemoteBzrDir(self.transport)
74
branch = b.open_branch()
75
self.assertIsInstance(branch, Branch)
77
def test_remote_repository(self):
78
b = BzrDir.open_from_transport(self.transport)
79
repo = b.open_repository()
80
revid = u'\xc823123123'.encode('utf8')
81
self.assertFalse(repo.has_revision(revid))
82
self.local_wt.commit(message='test commit', rev_id=revid)
83
self.assertTrue(repo.has_revision(revid))
85
def test_remote_branch_revision_history(self):
86
b = BzrDir.open_from_transport(self.transport).open_branch()
87
self.assertEqual([], b.revision_history())
88
r1 = self.local_wt.commit('1st commit')
89
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
90
self.assertEqual([r1, r2], b.revision_history())
92
def test_find_correct_format(self):
93
"""Should open a RemoteBzrDir over a RemoteTransport"""
94
fmt = BzrDirFormat.find_format(self.transport)
95
self.assertTrue(RemoteBzrDirFormat
96
in BzrDirFormat._control_server_formats)
97
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
99
def test_open_detected_smart_format(self):
100
fmt = BzrDirFormat.find_format(self.transport)
101
d = fmt.open(self.transport)
102
self.assertIsInstance(d, BzrDir)
104
def test_remote_branch_repr(self):
105
b = BzrDir.open_from_transport(self.transport).open_branch()
106
self.assertStartsWith(str(b), 'RemoteBranch(')
109
class FakeProtocol(object):
110
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
112
def __init__(self, body, fake_client):
114
self._body_buffer = None
115
self._fake_client = fake_client
117
def read_body_bytes(self, count=-1):
118
if self._body_buffer is None:
119
self._body_buffer = StringIO(self.body)
120
bytes = self._body_buffer.read(count)
121
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
122
self._fake_client.expecting_body = False
125
def cancel_read_body(self):
126
self._fake_client.expecting_body = False
128
def read_streamed_body(self):
132
class FakeClient(_SmartClient):
133
"""Lookalike for _SmartClient allowing testing."""
135
def __init__(self, fake_medium_base='fake base'):
136
"""Create a FakeClient.
138
:param responses: A list of response-tuple, body-data pairs to be sent
139
back to callers. A special case is if the response-tuple is
140
'unknown verb', then a UnknownSmartMethod will be raised for that
141
call, using the second element of the tuple as the verb in the
146
self.expecting_body = False
147
_SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
149
def add_success_response(self, *args):
150
self.responses.append(('success', args, None))
152
def add_success_response_with_body(self, body, *args):
153
self.responses.append(('success', args, body))
155
def add_error_response(self, *args):
156
self.responses.append(('error', args))
158
def add_unknown_method_response(self, verb):
159
self.responses.append(('unknown', verb))
161
def _get_next_response(self):
162
response_tuple = self.responses.pop(0)
163
if response_tuple[0] == 'unknown':
164
raise errors.UnknownSmartMethod(response_tuple[1])
165
elif response_tuple[0] == 'error':
166
raise errors.ErrorFromSmartServer(response_tuple[1])
167
return response_tuple
169
def call(self, method, *args):
170
self._calls.append(('call', method, args))
171
return self._get_next_response()[1]
173
def call_expecting_body(self, method, *args):
174
self._calls.append(('call_expecting_body', method, args))
175
result = self._get_next_response()
176
self.expecting_body = True
177
return result[1], FakeProtocol(result[2], self)
179
def call_with_body_bytes_expecting_body(self, method, args, body):
180
self._calls.append(('call_with_body_bytes_expecting_body', method,
182
result = self._get_next_response()
183
self.expecting_body = True
184
return result[1], FakeProtocol(result[2], self)
187
class FakeMedium(medium.SmartClientMedium):
189
def __init__(self, client_calls, base):
190
medium.SmartClientMedium.__init__(self, base)
191
self._client_calls = client_calls
193
def disconnect(self):
194
self._client_calls.append(('disconnect medium',))
197
class TestVfsHas(tests.TestCase):
199
def test_unicode_path(self):
200
client = FakeClient('/')
201
client.add_success_response('yes',)
202
transport = RemoteTransport('bzr://localhost/', _client=client)
203
filename = u'/hell\u00d8'.encode('utf8')
204
result = transport.has(filename)
206
[('call', 'has', (filename,))],
208
self.assertTrue(result)
211
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
212
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
214
def assertRemotePath(self, expected, client_base, transport_base):
215
"""Assert that the result of
216
SmartClientMedium.remote_path_from_transport is the expected value for
217
a given client_base and transport_base.
219
client_medium = medium.SmartClientMedium(client_base)
220
transport = get_transport(transport_base)
221
result = client_medium.remote_path_from_transport(transport)
222
self.assertEqual(expected, result)
224
def test_remote_path_from_transport(self):
225
"""SmartClientMedium.remote_path_from_transport calculates a URL for
226
the given transport relative to the root of the client base URL.
228
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
229
self.assertRemotePath(
230
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
232
def assertRemotePathHTTP(self, expected, transport_base, relpath):
233
"""Assert that the result of
234
HttpTransportBase.remote_path_from_transport is the expected value for
235
a given transport_base and relpath of that transport. (Note that
236
HttpTransportBase is a subclass of SmartClientMedium)
238
base_transport = get_transport(transport_base)
239
client_medium = base_transport.get_smart_medium()
240
cloned_transport = base_transport.clone(relpath)
241
result = client_medium.remote_path_from_transport(cloned_transport)
242
self.assertEqual(expected, result)
244
def test_remote_path_from_transport_http(self):
245
"""Remote paths for HTTP transports are calculated differently to other
246
transports. They are just relative to the client base, not the root
247
directory of the host.
249
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
250
self.assertRemotePathHTTP(
251
'../xyz/', scheme + '//host/path', '../xyz/')
252
self.assertRemotePathHTTP(
253
'xyz/', scheme + '//host/path', 'xyz/')
256
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
257
"""Tests for the behaviour of client_medium.remote_is_at_least."""
259
def test_initially_unlimited(self):
260
"""A fresh medium assumes that the remote side supports all
263
client_medium = medium.SmartClientMedium('dummy base')
264
self.assertFalse(client_medium._is_remote_before((99, 99)))
266
def test__remember_remote_is_before(self):
267
"""Calling _remember_remote_is_before ratchets down the known remote
270
client_medium = medium.SmartClientMedium('dummy base')
271
# Mark the remote side as being less than 1.6. The remote side may
273
client_medium._remember_remote_is_before((1, 6))
274
self.assertTrue(client_medium._is_remote_before((1, 6)))
275
self.assertFalse(client_medium._is_remote_before((1, 5)))
276
# Calling _remember_remote_is_before again with a lower value works.
277
client_medium._remember_remote_is_before((1, 5))
278
self.assertTrue(client_medium._is_remote_before((1, 5)))
279
# You cannot call _remember_remote_is_before with a larger value.
281
AssertionError, client_medium._remember_remote_is_before, (1, 9))
284
class TestBzrDirOpenBranch(tests.TestCase):
286
def test_branch_present(self):
287
transport = MemoryTransport()
288
transport.mkdir('quack')
289
transport = transport.clone('quack')
290
client = FakeClient(transport.base)
291
client.add_success_response('ok', '')
292
client.add_success_response('ok', '', 'no', 'no', 'no')
293
bzrdir = RemoteBzrDir(transport, _client=client)
294
result = bzrdir.open_branch()
296
[('call', 'BzrDir.open_branch', ('quack/',)),
297
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
299
self.assertIsInstance(result, RemoteBranch)
300
self.assertEqual(bzrdir, result.bzrdir)
302
def test_branch_missing(self):
303
transport = MemoryTransport()
304
transport.mkdir('quack')
305
transport = transport.clone('quack')
306
client = FakeClient(transport.base)
307
client.add_error_response('nobranch')
308
bzrdir = RemoteBzrDir(transport, _client=client)
309
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
311
[('call', 'BzrDir.open_branch', ('quack/',))],
314
def test__get_tree_branch(self):
315
# _get_tree_branch is a form of open_branch, but it should only ask for
316
# branch opening, not any other network requests.
319
calls.append("Called")
321
transport = MemoryTransport()
322
# no requests on the network - catches other api calls being made.
323
client = FakeClient(transport.base)
324
bzrdir = RemoteBzrDir(transport, _client=client)
325
# patch the open_branch call to record that it was called.
326
bzrdir.open_branch = open_branch
327
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
328
self.assertEqual(["Called"], calls)
329
self.assertEqual([], client._calls)
331
def test_url_quoting_of_path(self):
332
# Relpaths on the wire should not be URL-escaped. So "~" should be
333
# transmitted as "~", not "%7E".
334
transport = RemoteTCPTransport('bzr://localhost/~hello/')
335
client = FakeClient(transport.base)
336
client.add_success_response('ok', '')
337
client.add_success_response('ok', '', 'no', 'no', 'no')
338
bzrdir = RemoteBzrDir(transport, _client=client)
339
result = bzrdir.open_branch()
341
[('call', 'BzrDir.open_branch', ('~hello/',)),
342
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
345
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
346
transport = MemoryTransport()
347
transport.mkdir('quack')
348
transport = transport.clone('quack')
350
rich_response = 'yes'
354
subtree_response = 'yes'
356
subtree_response = 'no'
357
client = FakeClient(transport.base)
358
client.add_success_response(
359
'ok', '', rich_response, subtree_response, external_lookup)
360
bzrdir = RemoteBzrDir(transport, _client=client)
361
result = bzrdir.open_repository()
363
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
365
self.assertIsInstance(result, RemoteRepository)
366
self.assertEqual(bzrdir, result.bzrdir)
367
self.assertEqual(rich_root, result._format.rich_root_data)
368
self.assertEqual(subtrees, result._format.supports_tree_reference)
370
def test_open_repository_sets_format_attributes(self):
371
self.check_open_repository(True, True)
372
self.check_open_repository(False, True)
373
self.check_open_repository(True, False)
374
self.check_open_repository(False, False)
375
self.check_open_repository(False, False, 'yes')
377
def test_old_server(self):
378
"""RemoteBzrDirFormat should fail to probe if the server version is too
381
self.assertRaises(errors.NotBranchError,
382
RemoteBzrDirFormat.probe_transport, OldServerTransport())
385
class TestBzrDirOpenRepository(tests.TestCase):
387
def test_backwards_compat_1_2(self):
388
transport = MemoryTransport()
389
transport.mkdir('quack')
390
transport = transport.clone('quack')
391
client = FakeClient(transport.base)
392
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
393
client.add_success_response('ok', '', 'no', 'no')
394
bzrdir = RemoteBzrDir(transport, _client=client)
395
repo = bzrdir.open_repository()
397
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
398
('call', 'BzrDir.find_repository', ('quack/',))],
402
class OldSmartClient(object):
403
"""A fake smart client for test_old_version that just returns a version one
404
response to the 'hello' (query version) command.
407
def get_request(self):
408
input_file = StringIO('ok\x011\n')
409
output_file = StringIO()
410
client_medium = medium.SmartSimplePipesClientMedium(
411
input_file, output_file)
412
return medium.SmartClientStreamMediumRequest(client_medium)
414
def protocol_version(self):
418
class OldServerTransport(object):
419
"""A fake transport for test_old_server that reports it's smart server
420
protocol version as version one.
426
def get_smart_client(self):
427
return OldSmartClient()
430
class TestBranchLastRevisionInfo(tests.TestCase):
432
def test_empty_branch(self):
433
# in an empty branch we decode the response properly
434
transport = MemoryTransport()
435
client = FakeClient(transport.base)
436
client.add_success_response('ok', '0', 'null:')
437
transport.mkdir('quack')
438
transport = transport.clone('quack')
439
# we do not want bzrdir to make any remote calls
440
bzrdir = RemoteBzrDir(transport, _client=False)
441
branch = RemoteBranch(bzrdir, None, _client=client)
442
result = branch.last_revision_info()
445
[('call', 'Branch.last_revision_info', ('quack/',))],
447
self.assertEqual((0, NULL_REVISION), result)
449
def test_non_empty_branch(self):
450
# in a non-empty branch we also decode the response properly
451
revid = u'\xc8'.encode('utf8')
452
transport = MemoryTransport()
453
client = FakeClient(transport.base)
454
client.add_success_response('ok', '2', revid)
455
transport.mkdir('kwaak')
456
transport = transport.clone('kwaak')
457
# we do not want bzrdir to make any remote calls
458
bzrdir = RemoteBzrDir(transport, _client=False)
459
branch = RemoteBranch(bzrdir, None, _client=client)
460
result = branch.last_revision_info()
463
[('call', 'Branch.last_revision_info', ('kwaak/',))],
465
self.assertEqual((2, revid), result)
468
class TestBranchSetLastRevision(tests.TestCase):
470
def test_set_empty(self):
471
# set_revision_history([]) is translated to calling
472
# Branch.set_last_revision(path, '') on the wire.
473
transport = MemoryTransport()
474
transport.mkdir('branch')
475
transport = transport.clone('branch')
477
client = FakeClient(transport.base)
479
client.add_success_response('ok', 'branch token', 'repo token')
481
client.add_success_response('ok')
483
client.add_success_response('ok')
484
bzrdir = RemoteBzrDir(transport, _client=False)
485
branch = RemoteBranch(bzrdir, None, _client=client)
486
# This is a hack to work around the problem that RemoteBranch currently
487
# unnecessarily invokes _ensure_real upon a call to lock_write.
488
branch._ensure_real = lambda: None
491
result = branch.set_revision_history([])
493
[('call', 'Branch.set_last_revision',
494
('branch/', 'branch token', 'repo token', 'null:'))],
497
self.assertEqual(None, result)
499
def test_set_nonempty(self):
500
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
501
# Branch.set_last_revision(path, rev-idN) on the wire.
502
transport = MemoryTransport()
503
transport.mkdir('branch')
504
transport = transport.clone('branch')
506
client = FakeClient(transport.base)
508
client.add_success_response('ok', 'branch token', 'repo token')
510
client.add_success_response('ok')
512
client.add_success_response('ok')
513
bzrdir = RemoteBzrDir(transport, _client=False)
514
branch = RemoteBranch(bzrdir, None, _client=client)
515
# This is a hack to work around the problem that RemoteBranch currently
516
# unnecessarily invokes _ensure_real upon a call to lock_write.
517
branch._ensure_real = lambda: None
518
# Lock the branch, reset the record of remote calls.
522
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
524
[('call', 'Branch.set_last_revision',
525
('branch/', 'branch token', 'repo token', 'rev-id2'))],
528
self.assertEqual(None, result)
530
def test_no_such_revision(self):
531
transport = MemoryTransport()
532
transport.mkdir('branch')
533
transport = transport.clone('branch')
534
# A response of 'NoSuchRevision' is translated into an exception.
535
client = FakeClient(transport.base)
537
client.add_success_response('ok', 'branch token', 'repo token')
539
client.add_error_response('NoSuchRevision', 'rev-id')
541
client.add_success_response('ok')
543
bzrdir = RemoteBzrDir(transport, _client=False)
544
branch = RemoteBranch(bzrdir, None, _client=client)
545
branch._ensure_real = lambda: None
550
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
554
class TestBranchSetLastRevisionInfo(tests.TestCase):
556
def test_set_last_revision_info(self):
557
# set_last_revision_info(num, 'rev-id') is translated to calling
558
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
559
transport = MemoryTransport()
560
transport.mkdir('branch')
561
transport = transport.clone('branch')
562
client = FakeClient(transport.base)
564
client.add_success_response('ok', 'branch token', 'repo token')
566
client.add_success_response('ok')
568
client.add_success_response('ok')
570
bzrdir = RemoteBzrDir(transport, _client=False)
571
branch = RemoteBranch(bzrdir, None, _client=client)
572
# This is a hack to work around the problem that RemoteBranch currently
573
# unnecessarily invokes _ensure_real upon a call to lock_write.
574
branch._ensure_real = lambda: None
575
# Lock the branch, reset the record of remote calls.
578
result = branch.set_last_revision_info(1234, 'a-revision-id')
580
[('call', 'Branch.set_last_revision_info',
581
('branch/', 'branch token', 'repo token',
582
'1234', 'a-revision-id'))],
584
self.assertEqual(None, result)
586
def test_no_such_revision(self):
587
# A response of 'NoSuchRevision' is translated into an exception.
588
transport = MemoryTransport()
589
transport.mkdir('branch')
590
transport = transport.clone('branch')
591
client = FakeClient(transport.base)
593
client.add_success_response('ok', 'branch token', 'repo token')
595
client.add_error_response('NoSuchRevision', 'revid')
597
client.add_success_response('ok')
599
bzrdir = RemoteBzrDir(transport, _client=False)
600
branch = RemoteBranch(bzrdir, None, _client=client)
601
# This is a hack to work around the problem that RemoteBranch currently
602
# unnecessarily invokes _ensure_real upon a call to lock_write.
603
branch._ensure_real = lambda: None
604
# Lock the branch, reset the record of remote calls.
609
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
612
def lock_remote_branch(self, branch):
613
"""Trick a RemoteBranch into thinking it is locked."""
614
branch._lock_mode = 'w'
615
branch._lock_count = 2
616
branch._lock_token = 'branch token'
617
branch._repo_lock_token = 'repo token'
619
def test_backwards_compatibility(self):
620
"""If the server does not support the Branch.set_last_revision_info
621
verb (which is new in 1.4), then the client falls back to VFS methods.
623
# This test is a little messy. Unlike most tests in this file, it
624
# doesn't purely test what a Remote* object sends over the wire, and
625
# how it reacts to responses from the wire. It instead relies partly
626
# on asserting that the RemoteBranch will call
627
# self._real_branch.set_last_revision_info(...).
629
# First, set up our RemoteBranch with a FakeClient that raises
630
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
631
transport = MemoryTransport()
632
transport.mkdir('branch')
633
transport = transport.clone('branch')
634
client = FakeClient(transport.base)
635
client.add_unknown_method_response('Branch.set_last_revision_info')
636
bzrdir = RemoteBzrDir(transport, _client=False)
637
branch = RemoteBranch(bzrdir, None, _client=client)
638
class StubRealBranch(object):
641
def set_last_revision_info(self, revno, revision_id):
643
('set_last_revision_info', revno, revision_id))
644
def _clear_cached_state(self):
646
real_branch = StubRealBranch()
647
branch._real_branch = real_branch
648
self.lock_remote_branch(branch)
650
# Call set_last_revision_info, and verify it behaved as expected.
651
result = branch.set_last_revision_info(1234, 'a-revision-id')
653
[('call', 'Branch.set_last_revision_info',
654
('branch/', 'branch token', 'repo token',
655
'1234', 'a-revision-id')),],
658
[('set_last_revision_info', 1234, 'a-revision-id')],
661
def test_unexpected_error(self):
662
# A response of 'NoSuchRevision' is translated into an exception.
663
transport = MemoryTransport()
664
transport.mkdir('branch')
665
transport = transport.clone('branch')
666
client = FakeClient(transport.base)
668
client.add_success_response('ok', 'branch token', 'repo token')
670
client.add_error_response('UnexpectedError')
672
client.add_success_response('ok')
674
bzrdir = RemoteBzrDir(transport, _client=False)
675
branch = RemoteBranch(bzrdir, None, _client=client)
676
# This is a hack to work around the problem that RemoteBranch currently
677
# unnecessarily invokes _ensure_real upon a call to lock_write.
678
branch._ensure_real = lambda: None
679
# Lock the branch, reset the record of remote calls.
683
err = self.assertRaises(
684
errors.ErrorFromSmartServer,
685
branch.set_last_revision_info, 123, 'revid')
686
self.assertEqual(('UnexpectedError',), err.error_tuple)
690
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
691
"""Getting the branch configuration should use an abstract method not vfs.
694
def test_get_branch_conf(self):
695
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
696
## # We should see that branch.get_config() does a single rpc to get the
697
## # remote configuration file, abstracting away where that is stored on
698
## # the server. However at the moment it always falls back to using the
699
## # vfs, and this would need some changes in config.py.
701
## # in an empty branch we decode the response properly
702
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
703
## # we need to make a real branch because the remote_branch.control_files
704
## # will trigger _ensure_real.
705
## branch = self.make_branch('quack')
706
## transport = branch.bzrdir.root_transport
707
## # we do not want bzrdir to make any remote calls
708
## bzrdir = RemoteBzrDir(transport, _client=False)
709
## branch = RemoteBranch(bzrdir, None, _client=client)
710
## config = branch.get_config()
712
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
716
class TestBranchLockWrite(tests.TestCase):
718
def test_lock_write_unlockable(self):
719
transport = MemoryTransport()
720
client = FakeClient(transport.base)
721
client.add_error_response('UnlockableTransport')
722
transport.mkdir('quack')
723
transport = transport.clone('quack')
724
# we do not want bzrdir to make any remote calls
725
bzrdir = RemoteBzrDir(transport, _client=False)
726
branch = RemoteBranch(bzrdir, None, _client=client)
727
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
729
[('call', 'Branch.lock_write', ('quack/', '', ''))],
733
class TestTransportIsReadonly(tests.TestCase):
736
client = FakeClient()
737
client.add_success_response('yes')
738
transport = RemoteTransport('bzr://example.com/', medium=False,
740
self.assertEqual(True, transport.is_readonly())
742
[('call', 'Transport.is_readonly', ())],
745
def test_false(self):
746
client = FakeClient()
747
client.add_success_response('no')
748
transport = RemoteTransport('bzr://example.com/', medium=False,
750
self.assertEqual(False, transport.is_readonly())
752
[('call', 'Transport.is_readonly', ())],
755
def test_error_from_old_server(self):
756
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
758
Clients should treat it as a "no" response, because is_readonly is only
759
advisory anyway (a transport could be read-write, but then the
760
underlying filesystem could be readonly anyway).
762
client = FakeClient()
763
client.add_unknown_method_response('Transport.is_readonly')
764
transport = RemoteTransport('bzr://example.com/', medium=False,
766
self.assertEqual(False, transport.is_readonly())
768
[('call', 'Transport.is_readonly', ())],
772
class TestRemoteRepository(tests.TestCase):
773
"""Base for testing RemoteRepository protocol usage.
775
These tests contain frozen requests and responses. We want any changes to
776
what is sent or expected to be require a thoughtful update to these tests
777
because they might break compatibility with different-versioned servers.
780
def setup_fake_client_and_repository(self, transport_path):
781
"""Create the fake client and repository for testing with.
783
There's no real server here; we just have canned responses sent
786
:param transport_path: Path below the root of the MemoryTransport
787
where the repository will be created.
789
transport = MemoryTransport()
790
transport.mkdir(transport_path)
791
client = FakeClient(transport.base)
792
transport = transport.clone(transport_path)
793
# we do not want bzrdir to make any remote calls
794
bzrdir = RemoteBzrDir(transport, _client=False)
795
repo = RemoteRepository(bzrdir, None, _client=client)
799
class TestRepositoryGatherStats(TestRemoteRepository):
801
def test_revid_none(self):
802
# ('ok',), body with revisions and size
803
transport_path = 'quack'
804
repo, client = self.setup_fake_client_and_repository(transport_path)
805
client.add_success_response_with_body(
806
'revisions: 2\nsize: 18\n', 'ok')
807
result = repo.gather_stats(None)
809
[('call_expecting_body', 'Repository.gather_stats',
810
('quack/','','no'))],
812
self.assertEqual({'revisions': 2, 'size': 18}, result)
814
def test_revid_no_committers(self):
815
# ('ok',), body without committers
816
body = ('firstrev: 123456.300 3600\n'
817
'latestrev: 654231.400 0\n'
820
transport_path = 'quick'
821
revid = u'\xc8'.encode('utf8')
822
repo, client = self.setup_fake_client_and_repository(transport_path)
823
client.add_success_response_with_body(body, 'ok')
824
result = repo.gather_stats(revid)
826
[('call_expecting_body', 'Repository.gather_stats',
827
('quick/', revid, 'no'))],
829
self.assertEqual({'revisions': 2, 'size': 18,
830
'firstrev': (123456.300, 3600),
831
'latestrev': (654231.400, 0),},
834
def test_revid_with_committers(self):
835
# ('ok',), body with committers
836
body = ('committers: 128\n'
837
'firstrev: 123456.300 3600\n'
838
'latestrev: 654231.400 0\n'
841
transport_path = 'buick'
842
revid = u'\xc8'.encode('utf8')
843
repo, client = self.setup_fake_client_and_repository(transport_path)
844
client.add_success_response_with_body(body, 'ok')
845
result = repo.gather_stats(revid, True)
847
[('call_expecting_body', 'Repository.gather_stats',
848
('buick/', revid, 'yes'))],
850
self.assertEqual({'revisions': 2, 'size': 18,
852
'firstrev': (123456.300, 3600),
853
'latestrev': (654231.400, 0),},
857
class TestRepositoryGetGraph(TestRemoteRepository):
859
def test_get_graph(self):
860
# get_graph returns a graph with the repository as the
862
transport_path = 'quack'
863
repo, client = self.setup_fake_client_and_repository(transport_path)
864
graph = repo.get_graph()
865
self.assertEqual(graph._parents_provider, repo)
868
class TestRepositoryGetParentMap(TestRemoteRepository):
870
def test_get_parent_map_caching(self):
871
# get_parent_map returns from cache until unlock()
872
# setup a reponse with two revisions
873
r1 = u'\u0e33'.encode('utf8')
874
r2 = u'\u0dab'.encode('utf8')
875
lines = [' '.join([r2, r1]), r1]
876
encoded_body = bz2.compress('\n'.join(lines))
878
transport_path = 'quack'
879
repo, client = self.setup_fake_client_and_repository(transport_path)
880
client.add_success_response_with_body(encoded_body, 'ok')
881
client.add_success_response_with_body(encoded_body, 'ok')
883
graph = repo.get_graph()
884
parents = graph.get_parent_map([r2])
885
self.assertEqual({r2: (r1,)}, parents)
886
# locking and unlocking deeper should not reset
889
parents = graph.get_parent_map([r1])
890
self.assertEqual({r1: (NULL_REVISION,)}, parents)
892
[('call_with_body_bytes_expecting_body',
893
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
896
# now we call again, and it should use the second response.
898
graph = repo.get_graph()
899
parents = graph.get_parent_map([r1])
900
self.assertEqual({r1: (NULL_REVISION,)}, parents)
902
[('call_with_body_bytes_expecting_body',
903
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
904
('call_with_body_bytes_expecting_body',
905
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
910
def test_get_parent_map_reconnects_if_unknown_method(self):
911
transport_path = 'quack'
912
repo, client = self.setup_fake_client_and_repository(transport_path)
913
client.add_unknown_method_response('Repository,get_parent_map')
914
client.add_success_response_with_body('', 'ok')
915
self.assertFalse(client._medium._is_remote_before((1, 2)))
916
rev_id = 'revision-id'
917
expected_deprecations = [
918
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
920
parents = self.callDeprecated(
921
expected_deprecations, repo.get_parent_map, [rev_id])
923
[('call_with_body_bytes_expecting_body',
924
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
925
('disconnect medium',),
926
('call_expecting_body', 'Repository.get_revision_graph',
929
# The medium is now marked as being connected to an older server
930
self.assertTrue(client._medium._is_remote_before((1, 2)))
932
def test_get_parent_map_fallback_parentless_node(self):
933
"""get_parent_map falls back to get_revision_graph on old servers. The
934
results from get_revision_graph are tweaked to match the get_parent_map
937
Specifically, a {key: ()} result from get_revision_graph means "no
938
parents" for that key, which in get_parent_map results should be
939
represented as {key: ('null:',)}.
941
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
943
rev_id = 'revision-id'
944
transport_path = 'quack'
945
repo, client = self.setup_fake_client_and_repository(transport_path)
946
client.add_success_response_with_body(rev_id, 'ok')
947
client._medium._remember_remote_is_before((1, 2))
948
expected_deprecations = [
949
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
951
parents = self.callDeprecated(
952
expected_deprecations, repo.get_parent_map, [rev_id])
954
[('call_expecting_body', 'Repository.get_revision_graph',
957
self.assertEqual({rev_id: ('null:',)}, parents)
959
def test_get_parent_map_unexpected_response(self):
960
repo, client = self.setup_fake_client_and_repository('path')
961
client.add_success_response('something unexpected!')
963
errors.UnexpectedSmartServerResponse,
964
repo.get_parent_map, ['a-revision-id'])
967
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
969
def test_null_revision(self):
970
# a null revision has the predictable result {}, we should have no wire
971
# traffic when calling it with this argument
972
transport_path = 'empty'
973
repo, client = self.setup_fake_client_and_repository(transport_path)
974
client.add_success_response('notused')
975
result = self.applyDeprecated(one_four, repo.get_revision_graph,
977
self.assertEqual([], client._calls)
978
self.assertEqual({}, result)
980
def test_none_revision(self):
981
# with none we want the entire graph
982
r1 = u'\u0e33'.encode('utf8')
983
r2 = u'\u0dab'.encode('utf8')
984
lines = [' '.join([r2, r1]), r1]
985
encoded_body = '\n'.join(lines)
987
transport_path = 'sinhala'
988
repo, client = self.setup_fake_client_and_repository(transport_path)
989
client.add_success_response_with_body(encoded_body, 'ok')
990
result = self.applyDeprecated(one_four, repo.get_revision_graph)
992
[('call_expecting_body', 'Repository.get_revision_graph',
995
self.assertEqual({r1: (), r2: (r1, )}, result)
997
def test_specific_revision(self):
998
# with a specific revision we want the graph for that
999
# with none we want the entire graph
1000
r11 = u'\u0e33'.encode('utf8')
1001
r12 = u'\xc9'.encode('utf8')
1002
r2 = u'\u0dab'.encode('utf8')
1003
lines = [' '.join([r2, r11, r12]), r11, r12]
1004
encoded_body = '\n'.join(lines)
1006
transport_path = 'sinhala'
1007
repo, client = self.setup_fake_client_and_repository(transport_path)
1008
client.add_success_response_with_body(encoded_body, 'ok')
1009
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1011
[('call_expecting_body', 'Repository.get_revision_graph',
1014
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1016
def test_no_such_revision(self):
1018
transport_path = 'sinhala'
1019
repo, client = self.setup_fake_client_and_repository(transport_path)
1020
client.add_error_response('nosuchrevision', revid)
1021
# also check that the right revision is reported in the error
1022
self.assertRaises(errors.NoSuchRevision,
1023
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1025
[('call_expecting_body', 'Repository.get_revision_graph',
1026
('sinhala/', revid))],
1029
def test_unexpected_error(self):
1031
transport_path = 'sinhala'
1032
repo, client = self.setup_fake_client_and_repository(transport_path)
1033
client.add_error_response('AnUnexpectedError')
1034
e = self.assertRaises(errors.ErrorFromSmartServer,
1035
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1036
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1039
class TestRepositoryIsShared(TestRemoteRepository):
1041
def test_is_shared(self):
1042
# ('yes', ) for Repository.is_shared -> 'True'.
1043
transport_path = 'quack'
1044
repo, client = self.setup_fake_client_and_repository(transport_path)
1045
client.add_success_response('yes')
1046
result = repo.is_shared()
1048
[('call', 'Repository.is_shared', ('quack/',))],
1050
self.assertEqual(True, result)
1052
def test_is_not_shared(self):
1053
# ('no', ) for Repository.is_shared -> 'False'.
1054
transport_path = 'qwack'
1055
repo, client = self.setup_fake_client_and_repository(transport_path)
1056
client.add_success_response('no')
1057
result = repo.is_shared()
1059
[('call', 'Repository.is_shared', ('qwack/',))],
1061
self.assertEqual(False, result)
1064
class TestRepositoryLockWrite(TestRemoteRepository):
1066
def test_lock_write(self):
1067
transport_path = 'quack'
1068
repo, client = self.setup_fake_client_and_repository(transport_path)
1069
client.add_success_response('ok', 'a token')
1070
result = repo.lock_write()
1072
[('call', 'Repository.lock_write', ('quack/', ''))],
1074
self.assertEqual('a token', result)
1076
def test_lock_write_already_locked(self):
1077
transport_path = 'quack'
1078
repo, client = self.setup_fake_client_and_repository(transport_path)
1079
client.add_error_response('LockContention')
1080
self.assertRaises(errors.LockContention, repo.lock_write)
1082
[('call', 'Repository.lock_write', ('quack/', ''))],
1085
def test_lock_write_unlockable(self):
1086
transport_path = 'quack'
1087
repo, client = self.setup_fake_client_and_repository(transport_path)
1088
client.add_error_response('UnlockableTransport')
1089
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1091
[('call', 'Repository.lock_write', ('quack/', ''))],
1095
class TestRepositoryUnlock(TestRemoteRepository):
1097
def test_unlock(self):
1098
transport_path = 'quack'
1099
repo, client = self.setup_fake_client_and_repository(transport_path)
1100
client.add_success_response('ok', 'a token')
1101
client.add_success_response('ok')
1105
[('call', 'Repository.lock_write', ('quack/', '')),
1106
('call', 'Repository.unlock', ('quack/', 'a token'))],
1109
def test_unlock_wrong_token(self):
1110
# If somehow the token is wrong, unlock will raise TokenMismatch.
1111
transport_path = 'quack'
1112
repo, client = self.setup_fake_client_and_repository(transport_path)
1113
client.add_success_response('ok', 'a token')
1114
client.add_error_response('TokenMismatch')
1116
self.assertRaises(errors.TokenMismatch, repo.unlock)
1119
class TestRepositoryHasRevision(TestRemoteRepository):
1121
def test_none(self):
1122
# repo.has_revision(None) should not cause any traffic.
1123
transport_path = 'quack'
1124
repo, client = self.setup_fake_client_and_repository(transport_path)
1126
# The null revision is always there, so has_revision(None) == True.
1127
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1129
# The remote repo shouldn't be accessed.
1130
self.assertEqual([], client._calls)
1133
class TestRepositoryTarball(TestRemoteRepository):
1135
# This is a canned tarball reponse we can validate against
1137
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1138
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1139
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1140
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1141
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1142
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1143
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1144
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1145
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1146
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1147
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1148
'nWQ7QH/F3JFOFCQ0aSPfA='
1151
def test_repository_tarball(self):
1152
# Test that Repository.tarball generates the right operations
1153
transport_path = 'repo'
1154
expected_calls = [('call_expecting_body', 'Repository.tarball',
1155
('repo/', 'bz2',),),
1157
repo, client = self.setup_fake_client_and_repository(transport_path)
1158
client.add_success_response_with_body(self.tarball_content, 'ok')
1159
# Now actually ask for the tarball
1160
tarball_file = repo._get_tarball('bz2')
1162
self.assertEqual(expected_calls, client._calls)
1163
self.assertEqual(self.tarball_content, tarball_file.read())
1165
tarball_file.close()
1168
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1169
"""RemoteRepository.copy_content_into optimizations"""
1171
def test_copy_content_remote_to_local(self):
1172
self.transport_server = server.SmartTCPServer_for_testing
1173
src_repo = self.make_repository('repo1')
1174
src_repo = repository.Repository.open(self.get_url('repo1'))
1175
# At the moment the tarball-based copy_content_into can't write back
1176
# into a smart server. It would be good if it could upload the
1177
# tarball; once that works we'd have to create repositories of
1178
# different formats. -- mbp 20070410
1179
dest_url = self.get_vfs_only_url('repo2')
1180
dest_bzrdir = BzrDir.create(dest_url)
1181
dest_repo = dest_bzrdir.create_repository()
1182
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1183
self.assertTrue(isinstance(src_repo, RemoteRepository))
1184
src_repo.copy_content_into(dest_repo)