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.assertTrue(client_medium._is_remote_at_least((99, 99)))
266
def test__remote_is_not(self):
267
"""Calling _remote_is_not ratchets down the known remote version."""
268
client_medium = medium.SmartClientMedium('dummy base')
269
# Mark the remote side as being less than 1.6. The remote side may
271
client_medium._remote_is_not((1, 6))
272
self.assertFalse(client_medium._is_remote_at_least((1, 6)))
273
self.assertTrue(client_medium._is_remote_at_least((1, 5)))
274
# Calling _remote_is_not again with a lower value works.
275
client_medium._remote_is_not((1, 5))
276
self.assertFalse(client_medium._is_remote_at_least((1, 5)))
277
# You cannot call _remote_is_not with a larger value.
279
AssertionError, client_medium._remote_is_not, (1, 9))
282
class TestBzrDirOpenBranch(tests.TestCase):
284
def test_branch_present(self):
285
transport = MemoryTransport()
286
transport.mkdir('quack')
287
transport = transport.clone('quack')
288
client = FakeClient(transport.base)
289
client.add_success_response('ok', '')
290
client.add_success_response('ok', '', 'no', 'no', 'no')
291
bzrdir = RemoteBzrDir(transport, _client=client)
292
result = bzrdir.open_branch()
294
[('call', 'BzrDir.open_branch', ('quack/',)),
295
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
297
self.assertIsInstance(result, RemoteBranch)
298
self.assertEqual(bzrdir, result.bzrdir)
300
def test_branch_missing(self):
301
transport = MemoryTransport()
302
transport.mkdir('quack')
303
transport = transport.clone('quack')
304
client = FakeClient(transport.base)
305
client.add_error_response('nobranch')
306
bzrdir = RemoteBzrDir(transport, _client=client)
307
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
309
[('call', 'BzrDir.open_branch', ('quack/',))],
312
def test__get_tree_branch(self):
313
# _get_tree_branch is a form of open_branch, but it should only ask for
314
# branch opening, not any other network requests.
317
calls.append("Called")
319
transport = MemoryTransport()
320
# no requests on the network - catches other api calls being made.
321
client = FakeClient(transport.base)
322
bzrdir = RemoteBzrDir(transport, _client=client)
323
# patch the open_branch call to record that it was called.
324
bzrdir.open_branch = open_branch
325
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
326
self.assertEqual(["Called"], calls)
327
self.assertEqual([], client._calls)
329
def test_url_quoting_of_path(self):
330
# Relpaths on the wire should not be URL-escaped. So "~" should be
331
# transmitted as "~", not "%7E".
332
transport = RemoteTCPTransport('bzr://localhost/~hello/')
333
client = FakeClient(transport.base)
334
client.add_success_response('ok', '')
335
client.add_success_response('ok', '', 'no', 'no', 'no')
336
bzrdir = RemoteBzrDir(transport, _client=client)
337
result = bzrdir.open_branch()
339
[('call', 'BzrDir.open_branch', ('~hello/',)),
340
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
343
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
344
transport = MemoryTransport()
345
transport.mkdir('quack')
346
transport = transport.clone('quack')
348
rich_response = 'yes'
352
subtree_response = 'yes'
354
subtree_response = 'no'
355
client = FakeClient(transport.base)
356
client.add_success_response(
357
'ok', '', rich_response, subtree_response, external_lookup)
358
bzrdir = RemoteBzrDir(transport, _client=client)
359
result = bzrdir.open_repository()
361
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
363
self.assertIsInstance(result, RemoteRepository)
364
self.assertEqual(bzrdir, result.bzrdir)
365
self.assertEqual(rich_root, result._format.rich_root_data)
366
self.assertEqual(subtrees, result._format.supports_tree_reference)
368
def test_open_repository_sets_format_attributes(self):
369
self.check_open_repository(True, True)
370
self.check_open_repository(False, True)
371
self.check_open_repository(True, False)
372
self.check_open_repository(False, False)
373
self.check_open_repository(False, False, 'yes')
375
def test_old_server(self):
376
"""RemoteBzrDirFormat should fail to probe if the server version is too
379
self.assertRaises(errors.NotBranchError,
380
RemoteBzrDirFormat.probe_transport, OldServerTransport())
383
class TestBzrDirOpenRepository(tests.TestCase):
385
def test_backwards_compat_1_2(self):
386
transport = MemoryTransport()
387
transport.mkdir('quack')
388
transport = transport.clone('quack')
389
client = FakeClient(transport.base)
390
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
391
client.add_success_response('ok', '', 'no', 'no')
392
bzrdir = RemoteBzrDir(transport, _client=client)
393
repo = bzrdir.open_repository()
395
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
396
('call', 'BzrDir.find_repository', ('quack/',))],
400
class OldSmartClient(object):
401
"""A fake smart client for test_old_version that just returns a version one
402
response to the 'hello' (query version) command.
405
def get_request(self):
406
input_file = StringIO('ok\x011\n')
407
output_file = StringIO()
408
client_medium = medium.SmartSimplePipesClientMedium(
409
input_file, output_file)
410
return medium.SmartClientStreamMediumRequest(client_medium)
412
def protocol_version(self):
416
class OldServerTransport(object):
417
"""A fake transport for test_old_server that reports it's smart server
418
protocol version as version one.
424
def get_smart_client(self):
425
return OldSmartClient()
428
class TestBranchLastRevisionInfo(tests.TestCase):
430
def test_empty_branch(self):
431
# in an empty branch we decode the response properly
432
transport = MemoryTransport()
433
client = FakeClient(transport.base)
434
client.add_success_response('ok', '0', 'null:')
435
transport.mkdir('quack')
436
transport = transport.clone('quack')
437
# we do not want bzrdir to make any remote calls
438
bzrdir = RemoteBzrDir(transport, _client=False)
439
branch = RemoteBranch(bzrdir, None, _client=client)
440
result = branch.last_revision_info()
443
[('call', 'Branch.last_revision_info', ('quack/',))],
445
self.assertEqual((0, NULL_REVISION), result)
447
def test_non_empty_branch(self):
448
# in a non-empty branch we also decode the response properly
449
revid = u'\xc8'.encode('utf8')
450
transport = MemoryTransport()
451
client = FakeClient(transport.base)
452
client.add_success_response('ok', '2', revid)
453
transport.mkdir('kwaak')
454
transport = transport.clone('kwaak')
455
# we do not want bzrdir to make any remote calls
456
bzrdir = RemoteBzrDir(transport, _client=False)
457
branch = RemoteBranch(bzrdir, None, _client=client)
458
result = branch.last_revision_info()
461
[('call', 'Branch.last_revision_info', ('kwaak/',))],
463
self.assertEqual((2, revid), result)
466
class TestBranchSetLastRevision(tests.TestCase):
468
def test_set_empty(self):
469
# set_revision_history([]) is translated to calling
470
# Branch.set_last_revision(path, '') on the wire.
471
transport = MemoryTransport()
472
transport.mkdir('branch')
473
transport = transport.clone('branch')
475
client = FakeClient(transport.base)
477
client.add_success_response('ok', 'branch token', 'repo token')
479
client.add_success_response('ok')
481
client.add_success_response('ok')
482
bzrdir = RemoteBzrDir(transport, _client=False)
483
branch = RemoteBranch(bzrdir, None, _client=client)
484
# This is a hack to work around the problem that RemoteBranch currently
485
# unnecessarily invokes _ensure_real upon a call to lock_write.
486
branch._ensure_real = lambda: None
489
result = branch.set_revision_history([])
491
[('call', 'Branch.set_last_revision',
492
('branch/', 'branch token', 'repo token', 'null:'))],
495
self.assertEqual(None, result)
497
def test_set_nonempty(self):
498
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
499
# Branch.set_last_revision(path, rev-idN) on the wire.
500
transport = MemoryTransport()
501
transport.mkdir('branch')
502
transport = transport.clone('branch')
504
client = FakeClient(transport.base)
506
client.add_success_response('ok', 'branch token', 'repo token')
508
client.add_success_response('ok')
510
client.add_success_response('ok')
511
bzrdir = RemoteBzrDir(transport, _client=False)
512
branch = RemoteBranch(bzrdir, None, _client=client)
513
# This is a hack to work around the problem that RemoteBranch currently
514
# unnecessarily invokes _ensure_real upon a call to lock_write.
515
branch._ensure_real = lambda: None
516
# Lock the branch, reset the record of remote calls.
520
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
522
[('call', 'Branch.set_last_revision',
523
('branch/', 'branch token', 'repo token', 'rev-id2'))],
526
self.assertEqual(None, result)
528
def test_no_such_revision(self):
529
transport = MemoryTransport()
530
transport.mkdir('branch')
531
transport = transport.clone('branch')
532
# A response of 'NoSuchRevision' is translated into an exception.
533
client = FakeClient(transport.base)
535
client.add_success_response('ok', 'branch token', 'repo token')
537
client.add_error_response('NoSuchRevision', 'rev-id')
539
client.add_success_response('ok')
541
bzrdir = RemoteBzrDir(transport, _client=False)
542
branch = RemoteBranch(bzrdir, None, _client=client)
543
branch._ensure_real = lambda: None
548
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
552
class TestBranchSetLastRevisionInfo(tests.TestCase):
554
def test_set_last_revision_info(self):
555
# set_last_revision_info(num, 'rev-id') is translated to calling
556
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
557
transport = MemoryTransport()
558
transport.mkdir('branch')
559
transport = transport.clone('branch')
560
client = FakeClient(transport.base)
562
client.add_success_response('ok', 'branch token', 'repo token')
564
client.add_success_response('ok')
566
client.add_success_response('ok')
568
bzrdir = RemoteBzrDir(transport, _client=False)
569
branch = RemoteBranch(bzrdir, None, _client=client)
570
# This is a hack to work around the problem that RemoteBranch currently
571
# unnecessarily invokes _ensure_real upon a call to lock_write.
572
branch._ensure_real = lambda: None
573
# Lock the branch, reset the record of remote calls.
576
result = branch.set_last_revision_info(1234, 'a-revision-id')
578
[('call', 'Branch.set_last_revision_info',
579
('branch/', 'branch token', 'repo token',
580
'1234', 'a-revision-id'))],
582
self.assertEqual(None, result)
584
def test_no_such_revision(self):
585
# A response of 'NoSuchRevision' is translated into an exception.
586
transport = MemoryTransport()
587
transport.mkdir('branch')
588
transport = transport.clone('branch')
589
client = FakeClient(transport.base)
591
client.add_success_response('ok', 'branch token', 'repo token')
593
client.add_error_response('NoSuchRevision', 'revid')
595
client.add_success_response('ok')
597
bzrdir = RemoteBzrDir(transport, _client=False)
598
branch = RemoteBranch(bzrdir, None, _client=client)
599
# This is a hack to work around the problem that RemoteBranch currently
600
# unnecessarily invokes _ensure_real upon a call to lock_write.
601
branch._ensure_real = lambda: None
602
# Lock the branch, reset the record of remote calls.
607
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
610
def lock_remote_branch(self, branch):
611
"""Trick a RemoteBranch into thinking it is locked."""
612
branch._lock_mode = 'w'
613
branch._lock_count = 2
614
branch._lock_token = 'branch token'
615
branch._repo_lock_token = 'repo token'
617
def test_backwards_compatibility(self):
618
"""If the server does not support the Branch.set_last_revision_info
619
verb (which is new in 1.4), then the client falls back to VFS methods.
621
# This test is a little messy. Unlike most tests in this file, it
622
# doesn't purely test what a Remote* object sends over the wire, and
623
# how it reacts to responses from the wire. It instead relies partly
624
# on asserting that the RemoteBranch will call
625
# self._real_branch.set_last_revision_info(...).
627
# First, set up our RemoteBranch with a FakeClient that raises
628
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
629
transport = MemoryTransport()
630
transport.mkdir('branch')
631
transport = transport.clone('branch')
632
client = FakeClient(transport.base)
633
client.add_unknown_method_response('Branch.set_last_revision_info')
634
bzrdir = RemoteBzrDir(transport, _client=False)
635
branch = RemoteBranch(bzrdir, None, _client=client)
636
class StubRealBranch(object):
639
def set_last_revision_info(self, revno, revision_id):
641
('set_last_revision_info', revno, revision_id))
642
real_branch = StubRealBranch()
643
branch._real_branch = real_branch
644
self.lock_remote_branch(branch)
646
# Call set_last_revision_info, and verify it behaved as expected.
647
result = branch.set_last_revision_info(1234, 'a-revision-id')
649
[('call', 'Branch.set_last_revision_info',
650
('branch/', 'branch token', 'repo token',
651
'1234', 'a-revision-id')),],
654
[('set_last_revision_info', 1234, 'a-revision-id')],
657
def test_unexpected_error(self):
658
# A response of 'NoSuchRevision' is translated into an exception.
659
transport = MemoryTransport()
660
transport.mkdir('branch')
661
transport = transport.clone('branch')
662
client = FakeClient(transport.base)
664
client.add_success_response('ok', 'branch token', 'repo token')
666
client.add_error_response('UnexpectedError')
668
client.add_success_response('ok')
670
bzrdir = RemoteBzrDir(transport, _client=False)
671
branch = RemoteBranch(bzrdir, None, _client=client)
672
# This is a hack to work around the problem that RemoteBranch currently
673
# unnecessarily invokes _ensure_real upon a call to lock_write.
674
branch._ensure_real = lambda: None
675
# Lock the branch, reset the record of remote calls.
679
err = self.assertRaises(
680
errors.ErrorFromSmartServer,
681
branch.set_last_revision_info, 123, 'revid')
682
self.assertEqual(('UnexpectedError',), err.error_tuple)
686
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
687
"""Getting the branch configuration should use an abstract method not vfs.
690
def test_get_branch_conf(self):
691
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
692
## # We should see that branch.get_config() does a single rpc to get the
693
## # remote configuration file, abstracting away where that is stored on
694
## # the server. However at the moment it always falls back to using the
695
## # vfs, and this would need some changes in config.py.
697
## # in an empty branch we decode the response properly
698
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
699
## # we need to make a real branch because the remote_branch.control_files
700
## # will trigger _ensure_real.
701
## branch = self.make_branch('quack')
702
## transport = branch.bzrdir.root_transport
703
## # we do not want bzrdir to make any remote calls
704
## bzrdir = RemoteBzrDir(transport, _client=False)
705
## branch = RemoteBranch(bzrdir, None, _client=client)
706
## config = branch.get_config()
708
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
712
class TestBranchLockWrite(tests.TestCase):
714
def test_lock_write_unlockable(self):
715
transport = MemoryTransport()
716
client = FakeClient(transport.base)
717
client.add_error_response('UnlockableTransport')
718
transport.mkdir('quack')
719
transport = transport.clone('quack')
720
# we do not want bzrdir to make any remote calls
721
bzrdir = RemoteBzrDir(transport, _client=False)
722
branch = RemoteBranch(bzrdir, None, _client=client)
723
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
725
[('call', 'Branch.lock_write', ('quack/', '', ''))],
729
class TestTransportIsReadonly(tests.TestCase):
732
client = FakeClient()
733
client.add_success_response('yes')
734
transport = RemoteTransport('bzr://example.com/', medium=False,
736
self.assertEqual(True, transport.is_readonly())
738
[('call', 'Transport.is_readonly', ())],
741
def test_false(self):
742
client = FakeClient()
743
client.add_success_response('no')
744
transport = RemoteTransport('bzr://example.com/', medium=False,
746
self.assertEqual(False, transport.is_readonly())
748
[('call', 'Transport.is_readonly', ())],
751
def test_error_from_old_server(self):
752
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
754
Clients should treat it as a "no" response, because is_readonly is only
755
advisory anyway (a transport could be read-write, but then the
756
underlying filesystem could be readonly anyway).
758
client = FakeClient()
759
client.add_unknown_method_response('Transport.is_readonly')
760
transport = RemoteTransport('bzr://example.com/', medium=False,
762
self.assertEqual(False, transport.is_readonly())
764
[('call', 'Transport.is_readonly', ())],
768
class TestRemoteRepository(tests.TestCase):
769
"""Base for testing RemoteRepository protocol usage.
771
These tests contain frozen requests and responses. We want any changes to
772
what is sent or expected to be require a thoughtful update to these tests
773
because they might break compatibility with different-versioned servers.
776
def setup_fake_client_and_repository(self, transport_path):
777
"""Create the fake client and repository for testing with.
779
There's no real server here; we just have canned responses sent
782
:param transport_path: Path below the root of the MemoryTransport
783
where the repository will be created.
785
transport = MemoryTransport()
786
transport.mkdir(transport_path)
787
client = FakeClient(transport.base)
788
transport = transport.clone(transport_path)
789
# we do not want bzrdir to make any remote calls
790
bzrdir = RemoteBzrDir(transport, _client=False)
791
repo = RemoteRepository(bzrdir, None, _client=client)
795
class TestRepositoryGatherStats(TestRemoteRepository):
797
def test_revid_none(self):
798
# ('ok',), body with revisions and size
799
transport_path = 'quack'
800
repo, client = self.setup_fake_client_and_repository(transport_path)
801
client.add_success_response_with_body(
802
'revisions: 2\nsize: 18\n', 'ok')
803
result = repo.gather_stats(None)
805
[('call_expecting_body', 'Repository.gather_stats',
806
('quack/','','no'))],
808
self.assertEqual({'revisions': 2, 'size': 18}, result)
810
def test_revid_no_committers(self):
811
# ('ok',), body without committers
812
body = ('firstrev: 123456.300 3600\n'
813
'latestrev: 654231.400 0\n'
816
transport_path = 'quick'
817
revid = u'\xc8'.encode('utf8')
818
repo, client = self.setup_fake_client_and_repository(transport_path)
819
client.add_success_response_with_body(body, 'ok')
820
result = repo.gather_stats(revid)
822
[('call_expecting_body', 'Repository.gather_stats',
823
('quick/', revid, 'no'))],
825
self.assertEqual({'revisions': 2, 'size': 18,
826
'firstrev': (123456.300, 3600),
827
'latestrev': (654231.400, 0),},
830
def test_revid_with_committers(self):
831
# ('ok',), body with committers
832
body = ('committers: 128\n'
833
'firstrev: 123456.300 3600\n'
834
'latestrev: 654231.400 0\n'
837
transport_path = 'buick'
838
revid = u'\xc8'.encode('utf8')
839
repo, client = self.setup_fake_client_and_repository(transport_path)
840
client.add_success_response_with_body(body, 'ok')
841
result = repo.gather_stats(revid, True)
843
[('call_expecting_body', 'Repository.gather_stats',
844
('buick/', revid, 'yes'))],
846
self.assertEqual({'revisions': 2, 'size': 18,
848
'firstrev': (123456.300, 3600),
849
'latestrev': (654231.400, 0),},
853
class TestRepositoryGetGraph(TestRemoteRepository):
855
def test_get_graph(self):
856
# get_graph returns a graph with the repository as the
858
transport_path = 'quack'
859
repo, client = self.setup_fake_client_and_repository(transport_path)
860
graph = repo.get_graph()
861
self.assertEqual(graph._parents_provider, repo)
864
class TestRepositoryGetParentMap(TestRemoteRepository):
866
def test_get_parent_map_caching(self):
867
# get_parent_map returns from cache until unlock()
868
# setup a reponse with two revisions
869
r1 = u'\u0e33'.encode('utf8')
870
r2 = u'\u0dab'.encode('utf8')
871
lines = [' '.join([r2, r1]), r1]
872
encoded_body = bz2.compress('\n'.join(lines))
874
transport_path = 'quack'
875
repo, client = self.setup_fake_client_and_repository(transport_path)
876
client.add_success_response_with_body(encoded_body, 'ok')
877
client.add_success_response_with_body(encoded_body, 'ok')
879
graph = repo.get_graph()
880
parents = graph.get_parent_map([r2])
881
self.assertEqual({r2: (r1,)}, parents)
882
# locking and unlocking deeper should not reset
885
parents = graph.get_parent_map([r1])
886
self.assertEqual({r1: (NULL_REVISION,)}, parents)
888
[('call_with_body_bytes_expecting_body',
889
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
892
# now we call again, and it should use the second response.
894
graph = repo.get_graph()
895
parents = graph.get_parent_map([r1])
896
self.assertEqual({r1: (NULL_REVISION,)}, parents)
898
[('call_with_body_bytes_expecting_body',
899
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
900
('call_with_body_bytes_expecting_body',
901
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
906
def test_get_parent_map_reconnects_if_unknown_method(self):
907
transport_path = 'quack'
908
repo, client = self.setup_fake_client_and_repository(transport_path)
909
client.add_unknown_method_response('Repository,get_parent_map')
910
client.add_success_response_with_body('', 'ok')
911
self.assertTrue(client._medium._is_remote_at_least((1, 2)))
912
rev_id = 'revision-id'
913
expected_deprecations = [
914
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
916
parents = self.callDeprecated(
917
expected_deprecations, repo.get_parent_map, [rev_id])
919
[('call_with_body_bytes_expecting_body',
920
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
921
('disconnect medium',),
922
('call_expecting_body', 'Repository.get_revision_graph',
925
# The medium is now marked as being connected to an older server
926
self.assertFalse(client._medium._is_remote_at_least((1, 2)))
928
def test_get_parent_map_fallback_parentless_node(self):
929
"""get_parent_map falls back to get_revision_graph on old servers. The
930
results from get_revision_graph are tweaked to match the get_parent_map
933
Specifically, a {key: ()} result from get_revision_graph means "no
934
parents" for that key, which in get_parent_map results should be
935
represented as {key: ('null:',)}.
937
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
939
rev_id = 'revision-id'
940
transport_path = 'quack'
941
repo, client = self.setup_fake_client_and_repository(transport_path)
942
client.add_success_response_with_body(rev_id, 'ok')
943
client._medium._remote_is_not((1, 2))
944
expected_deprecations = [
945
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
947
parents = self.callDeprecated(
948
expected_deprecations, repo.get_parent_map, [rev_id])
950
[('call_expecting_body', 'Repository.get_revision_graph',
953
self.assertEqual({rev_id: ('null:',)}, parents)
955
def test_get_parent_map_unexpected_response(self):
956
repo, client = self.setup_fake_client_and_repository('path')
957
client.add_success_response('something unexpected!')
959
errors.UnexpectedSmartServerResponse,
960
repo.get_parent_map, ['a-revision-id'])
963
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
965
def test_null_revision(self):
966
# a null revision has the predictable result {}, we should have no wire
967
# traffic when calling it with this argument
968
transport_path = 'empty'
969
repo, client = self.setup_fake_client_and_repository(transport_path)
970
client.add_success_response('notused')
971
result = self.applyDeprecated(one_four, repo.get_revision_graph,
973
self.assertEqual([], client._calls)
974
self.assertEqual({}, result)
976
def test_none_revision(self):
977
# with none we want the entire graph
978
r1 = u'\u0e33'.encode('utf8')
979
r2 = u'\u0dab'.encode('utf8')
980
lines = [' '.join([r2, r1]), r1]
981
encoded_body = '\n'.join(lines)
983
transport_path = 'sinhala'
984
repo, client = self.setup_fake_client_and_repository(transport_path)
985
client.add_success_response_with_body(encoded_body, 'ok')
986
result = self.applyDeprecated(one_four, repo.get_revision_graph)
988
[('call_expecting_body', 'Repository.get_revision_graph',
991
self.assertEqual({r1: (), r2: (r1, )}, result)
993
def test_specific_revision(self):
994
# with a specific revision we want the graph for that
995
# with none we want the entire graph
996
r11 = u'\u0e33'.encode('utf8')
997
r12 = u'\xc9'.encode('utf8')
998
r2 = u'\u0dab'.encode('utf8')
999
lines = [' '.join([r2, r11, r12]), r11, r12]
1000
encoded_body = '\n'.join(lines)
1002
transport_path = 'sinhala'
1003
repo, client = self.setup_fake_client_and_repository(transport_path)
1004
client.add_success_response_with_body(encoded_body, 'ok')
1005
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
1007
[('call_expecting_body', 'Repository.get_revision_graph',
1010
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
1012
def test_no_such_revision(self):
1014
transport_path = 'sinhala'
1015
repo, client = self.setup_fake_client_and_repository(transport_path)
1016
client.add_error_response('nosuchrevision', revid)
1017
# also check that the right revision is reported in the error
1018
self.assertRaises(errors.NoSuchRevision,
1019
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1021
[('call_expecting_body', 'Repository.get_revision_graph',
1022
('sinhala/', revid))],
1025
def test_unexpected_error(self):
1027
transport_path = 'sinhala'
1028
repo, client = self.setup_fake_client_and_repository(transport_path)
1029
client.add_error_response('AnUnexpectedError')
1030
e = self.assertRaises(errors.ErrorFromSmartServer,
1031
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1032
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1035
class TestRepositoryIsShared(TestRemoteRepository):
1037
def test_is_shared(self):
1038
# ('yes', ) for Repository.is_shared -> 'True'.
1039
transport_path = 'quack'
1040
repo, client = self.setup_fake_client_and_repository(transport_path)
1041
client.add_success_response('yes')
1042
result = repo.is_shared()
1044
[('call', 'Repository.is_shared', ('quack/',))],
1046
self.assertEqual(True, result)
1048
def test_is_not_shared(self):
1049
# ('no', ) for Repository.is_shared -> 'False'.
1050
transport_path = 'qwack'
1051
repo, client = self.setup_fake_client_and_repository(transport_path)
1052
client.add_success_response('no')
1053
result = repo.is_shared()
1055
[('call', 'Repository.is_shared', ('qwack/',))],
1057
self.assertEqual(False, result)
1060
class TestRepositoryLockWrite(TestRemoteRepository):
1062
def test_lock_write(self):
1063
transport_path = 'quack'
1064
repo, client = self.setup_fake_client_and_repository(transport_path)
1065
client.add_success_response('ok', 'a token')
1066
result = repo.lock_write()
1068
[('call', 'Repository.lock_write', ('quack/', ''))],
1070
self.assertEqual('a token', result)
1072
def test_lock_write_already_locked(self):
1073
transport_path = 'quack'
1074
repo, client = self.setup_fake_client_and_repository(transport_path)
1075
client.add_error_response('LockContention')
1076
self.assertRaises(errors.LockContention, repo.lock_write)
1078
[('call', 'Repository.lock_write', ('quack/', ''))],
1081
def test_lock_write_unlockable(self):
1082
transport_path = 'quack'
1083
repo, client = self.setup_fake_client_and_repository(transport_path)
1084
client.add_error_response('UnlockableTransport')
1085
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1087
[('call', 'Repository.lock_write', ('quack/', ''))],
1091
class TestRepositoryUnlock(TestRemoteRepository):
1093
def test_unlock(self):
1094
transport_path = 'quack'
1095
repo, client = self.setup_fake_client_and_repository(transport_path)
1096
client.add_success_response('ok', 'a token')
1097
client.add_success_response('ok')
1101
[('call', 'Repository.lock_write', ('quack/', '')),
1102
('call', 'Repository.unlock', ('quack/', 'a token'))],
1105
def test_unlock_wrong_token(self):
1106
# If somehow the token is wrong, unlock will raise TokenMismatch.
1107
transport_path = 'quack'
1108
repo, client = self.setup_fake_client_and_repository(transport_path)
1109
client.add_success_response('ok', 'a token')
1110
client.add_error_response('TokenMismatch')
1112
self.assertRaises(errors.TokenMismatch, repo.unlock)
1115
class TestRepositoryHasRevision(TestRemoteRepository):
1117
def test_none(self):
1118
# repo.has_revision(None) should not cause any traffic.
1119
transport_path = 'quack'
1120
repo, client = self.setup_fake_client_and_repository(transport_path)
1122
# The null revision is always there, so has_revision(None) == True.
1123
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1125
# The remote repo shouldn't be accessed.
1126
self.assertEqual([], client._calls)
1129
class TestRepositoryTarball(TestRemoteRepository):
1131
# This is a canned tarball reponse we can validate against
1133
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1134
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1135
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1136
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1137
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1138
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1139
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1140
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1141
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1142
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1143
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1144
'nWQ7QH/F3JFOFCQ0aSPfA='
1147
def test_repository_tarball(self):
1148
# Test that Repository.tarball generates the right operations
1149
transport_path = 'repo'
1150
expected_calls = [('call_expecting_body', 'Repository.tarball',
1151
('repo/', 'bz2',),),
1153
repo, client = self.setup_fake_client_and_repository(transport_path)
1154
client.add_success_response_with_body(self.tarball_content, 'ok')
1155
# Now actually ask for the tarball
1156
tarball_file = repo._get_tarball('bz2')
1158
self.assertEqual(expected_calls, client._calls)
1159
self.assertEqual(self.tarball_content, tarball_file.read())
1161
tarball_file.close()
1164
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1165
"""RemoteRepository.copy_content_into optimizations"""
1167
def test_copy_content_remote_to_local(self):
1168
self.transport_server = server.SmartTCPServer_for_testing
1169
src_repo = self.make_repository('repo1')
1170
src_repo = repository.Repository.open(self.get_url('repo1'))
1171
# At the moment the tarball-based copy_content_into can't write back
1172
# into a smart server. It would be good if it could upload the
1173
# tarball; once that works we'd have to create repositories of
1174
# different formats. -- mbp 20070410
1175
dest_url = self.get_vfs_only_url('repo2')
1176
dest_bzrdir = BzrDir.create(dest_url)
1177
dest_repo = dest_bzrdir.create_repository()
1178
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1179
self.assertTrue(isinstance(src_repo, RemoteRepository))
1180
src_repo.copy_content_into(dest_repo)
1183
class TestRepositoryStreamKnitData(TestRemoteRepository):
1185
def make_pack_file(self, records):
1186
pack_file = StringIO()
1187
pack_writer = pack.ContainerWriter(pack_file.write)
1189
for bytes, names in records:
1190
pack_writer.add_bytes_record(bytes, names)
1195
def make_pack_stream(self, records):
1196
pack_serialiser = pack.ContainerSerialiser()
1197
yield pack_serialiser.begin()
1198
for bytes, names in records:
1199
yield pack_serialiser.bytes_record(bytes, names)
1200
yield pack_serialiser.end()
1202
def test_bad_pack_from_server(self):
1203
"""A response with invalid data (e.g. it has a record with multiple
1204
names) triggers an exception.
1206
Not all possible errors will be caught at this stage, but obviously
1207
malformed data should be.
1209
record = ('bytes', [('name1',), ('name2',)])
1210
pack_stream = self.make_pack_stream([record])
1211
transport_path = 'quack'
1212
repo, client = self.setup_fake_client_and_repository(transport_path)
1213
client.add_success_response_with_body(pack_stream, 'ok')
1214
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1215
stream = repo.get_data_stream_for_search(search)
1216
self.assertRaises(errors.SmartProtocolError, list, stream)
1218
def test_backwards_compatibility(self):
1219
"""If the server doesn't recognise this request, fallback to VFS."""
1220
repo, client = self.setup_fake_client_and_repository('path')
1221
client.add_unknown_method_response(
1222
'Repository.stream_revisions_chunked')
1223
self.mock_called = False
1224
repo._real_repository = MockRealRepository(self)
1225
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1226
repo.get_data_stream_for_search(search)
1227
self.assertTrue(self.mock_called)
1228
self.failIf(client.expecting_body,
1229
"The protocol has been left in an unclean state that will cause "
1230
"TooManyConcurrentRequests errors.")
1233
class MockRealRepository(object):
1234
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
1236
def __init__(self, test):
1239
def get_data_stream_for_search(self, search):
1240
self.test.assertEqual(set(['revid']), search.get_keys())
1241
self.test.mock_called = True