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
self._remote_is_at_least_1_2 = True
191
self._client_calls = client_calls
194
def disconnect(self):
195
self._client_calls.append(('disconnect medium',))
198
class TestVfsHas(tests.TestCase):
200
def test_unicode_path(self):
201
client = FakeClient('/')
202
client.add_success_response('yes',)
203
transport = RemoteTransport('bzr://localhost/', _client=client)
204
filename = u'/hell\u00d8'.encode('utf8')
205
result = transport.has(filename)
207
[('call', 'has', (filename,))],
209
self.assertTrue(result)
212
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
213
"""Tests for the behaviour of client_medium.remote_path_from_transport."""
215
def assertRemotePath(self, expected, client_base, transport_base):
216
"""Assert that the result of
217
SmartClientMedium.remote_path_from_transport is the expected value for
218
a given client_base and transport_base.
220
client_medium = medium.SmartClientMedium(client_base)
221
transport = get_transport(transport_base)
222
result = client_medium.remote_path_from_transport(transport)
223
self.assertEqual(expected, result)
225
def test_remote_path_from_transport(self):
226
"""SmartClientMedium.remote_path_from_transport calculates a URL for
227
the given transport relative to the root of the client base URL.
229
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
230
self.assertRemotePath(
231
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
233
def assertRemotePathHTTP(self, expected, transport_base, relpath):
234
"""Assert that the result of
235
HttpTransportBase.remote_path_from_transport is the expected value for
236
a given transport_base and relpath of that transport. (Note that
237
HttpTransportBase is a subclass of SmartClientMedium)
239
base_transport = get_transport(transport_base)
240
client_medium = base_transport.get_smart_medium()
241
cloned_transport = base_transport.clone(relpath)
242
result = client_medium.remote_path_from_transport(cloned_transport)
243
self.assertEqual(expected, result)
245
def test_remote_path_from_transport_http(self):
246
"""Remote paths for HTTP transports are calculated differently to other
247
transports. They are just relative to the client base, not the root
248
directory of the host.
250
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
251
self.assertRemotePathHTTP(
252
'../xyz/', scheme + '//host/path', '../xyz/')
253
self.assertRemotePathHTTP(
254
'xyz/', scheme + '//host/path', 'xyz/')
257
class TestBzrDirOpenBranch(tests.TestCase):
259
def test_branch_present(self):
260
transport = MemoryTransport()
261
transport.mkdir('quack')
262
transport = transport.clone('quack')
263
client = FakeClient(transport.base)
264
client.add_success_response('ok', '')
265
client.add_success_response('ok', '', 'no', 'no', 'no')
266
bzrdir = RemoteBzrDir(transport, _client=client)
267
result = bzrdir.open_branch()
269
[('call', 'BzrDir.open_branch', ('quack/',)),
270
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
272
self.assertIsInstance(result, RemoteBranch)
273
self.assertEqual(bzrdir, result.bzrdir)
275
def test_branch_missing(self):
276
transport = MemoryTransport()
277
transport.mkdir('quack')
278
transport = transport.clone('quack')
279
client = FakeClient(transport.base)
280
client.add_error_response('nobranch')
281
bzrdir = RemoteBzrDir(transport, _client=client)
282
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
284
[('call', 'BzrDir.open_branch', ('quack/',))],
287
def test__get_tree_branch(self):
288
# _get_tree_branch is a form of open_branch, but it should only ask for
289
# branch opening, not any other network requests.
292
calls.append("Called")
294
transport = MemoryTransport()
295
# no requests on the network - catches other api calls being made.
296
client = FakeClient(transport.base)
297
bzrdir = RemoteBzrDir(transport, _client=client)
298
# patch the open_branch call to record that it was called.
299
bzrdir.open_branch = open_branch
300
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
301
self.assertEqual(["Called"], calls)
302
self.assertEqual([], client._calls)
304
def test_url_quoting_of_path(self):
305
# Relpaths on the wire should not be URL-escaped. So "~" should be
306
# transmitted as "~", not "%7E".
307
transport = RemoteTCPTransport('bzr://localhost/~hello/')
308
client = FakeClient(transport.base)
309
client.add_success_response('ok', '')
310
client.add_success_response('ok', '', 'no', 'no', 'no')
311
bzrdir = RemoteBzrDir(transport, _client=client)
312
result = bzrdir.open_branch()
314
[('call', 'BzrDir.open_branch', ('~hello/',)),
315
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
318
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
319
transport = MemoryTransport()
320
transport.mkdir('quack')
321
transport = transport.clone('quack')
323
rich_response = 'yes'
327
subtree_response = 'yes'
329
subtree_response = 'no'
330
client = FakeClient(transport.base)
331
client.add_success_response(
332
'ok', '', rich_response, subtree_response, external_lookup)
333
bzrdir = RemoteBzrDir(transport, _client=client)
334
result = bzrdir.open_repository()
336
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
338
self.assertIsInstance(result, RemoteRepository)
339
self.assertEqual(bzrdir, result.bzrdir)
340
self.assertEqual(rich_root, result._format.rich_root_data)
341
self.assertEqual(subtrees, result._format.supports_tree_reference)
343
def test_open_repository_sets_format_attributes(self):
344
self.check_open_repository(True, True)
345
self.check_open_repository(False, True)
346
self.check_open_repository(True, False)
347
self.check_open_repository(False, False)
348
self.check_open_repository(False, False, 'yes')
350
def test_old_server(self):
351
"""RemoteBzrDirFormat should fail to probe if the server version is too
354
self.assertRaises(errors.NotBranchError,
355
RemoteBzrDirFormat.probe_transport, OldServerTransport())
358
class TestBzrDirOpenRepository(tests.TestCase):
360
def test_backwards_compat_1_2(self):
361
transport = MemoryTransport()
362
transport.mkdir('quack')
363
transport = transport.clone('quack')
364
client = FakeClient(transport.base)
365
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
366
client.add_success_response('ok', '', 'no', 'no')
367
bzrdir = RemoteBzrDir(transport, _client=client)
368
repo = bzrdir.open_repository()
370
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
371
('call', 'BzrDir.find_repository', ('quack/',))],
375
class OldSmartClient(object):
376
"""A fake smart client for test_old_version that just returns a version one
377
response to the 'hello' (query version) command.
380
def get_request(self):
381
input_file = StringIO('ok\x011\n')
382
output_file = StringIO()
383
client_medium = medium.SmartSimplePipesClientMedium(
384
input_file, output_file)
385
return medium.SmartClientStreamMediumRequest(client_medium)
387
def protocol_version(self):
391
class OldServerTransport(object):
392
"""A fake transport for test_old_server that reports it's smart server
393
protocol version as version one.
399
def get_smart_client(self):
400
return OldSmartClient()
403
class TestBranchLastRevisionInfo(tests.TestCase):
405
def test_empty_branch(self):
406
# in an empty branch we decode the response properly
407
transport = MemoryTransport()
408
client = FakeClient(transport.base)
409
client.add_success_response('ok', '0', 'null:')
410
transport.mkdir('quack')
411
transport = transport.clone('quack')
412
# we do not want bzrdir to make any remote calls
413
bzrdir = RemoteBzrDir(transport, _client=False)
414
branch = RemoteBranch(bzrdir, None, _client=client)
415
result = branch.last_revision_info()
418
[('call', 'Branch.last_revision_info', ('quack/',))],
420
self.assertEqual((0, NULL_REVISION), result)
422
def test_non_empty_branch(self):
423
# in a non-empty branch we also decode the response properly
424
revid = u'\xc8'.encode('utf8')
425
transport = MemoryTransport()
426
client = FakeClient(transport.base)
427
client.add_success_response('ok', '2', revid)
428
transport.mkdir('kwaak')
429
transport = transport.clone('kwaak')
430
# we do not want bzrdir to make any remote calls
431
bzrdir = RemoteBzrDir(transport, _client=False)
432
branch = RemoteBranch(bzrdir, None, _client=client)
433
result = branch.last_revision_info()
436
[('call', 'Branch.last_revision_info', ('kwaak/',))],
438
self.assertEqual((2, revid), result)
441
class TestBranchSetLastRevision(tests.TestCase):
443
def test_set_empty(self):
444
# set_revision_history([]) is translated to calling
445
# Branch.set_last_revision(path, '') on the wire.
446
transport = MemoryTransport()
447
transport.mkdir('branch')
448
transport = transport.clone('branch')
450
client = FakeClient(transport.base)
452
client.add_success_response('ok', 'branch token', 'repo token')
454
client.add_success_response('ok')
456
client.add_success_response('ok')
457
bzrdir = RemoteBzrDir(transport, _client=False)
458
branch = RemoteBranch(bzrdir, None, _client=client)
459
# This is a hack to work around the problem that RemoteBranch currently
460
# unnecessarily invokes _ensure_real upon a call to lock_write.
461
branch._ensure_real = lambda: None
464
result = branch.set_revision_history([])
466
[('call', 'Branch.set_last_revision',
467
('branch/', 'branch token', 'repo token', 'null:'))],
470
self.assertEqual(None, result)
472
def test_set_nonempty(self):
473
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
474
# Branch.set_last_revision(path, rev-idN) on the wire.
475
transport = MemoryTransport()
476
transport.mkdir('branch')
477
transport = transport.clone('branch')
479
client = FakeClient(transport.base)
481
client.add_success_response('ok', 'branch token', 'repo token')
483
client.add_success_response('ok')
485
client.add_success_response('ok')
486
bzrdir = RemoteBzrDir(transport, _client=False)
487
branch = RemoteBranch(bzrdir, None, _client=client)
488
# This is a hack to work around the problem that RemoteBranch currently
489
# unnecessarily invokes _ensure_real upon a call to lock_write.
490
branch._ensure_real = lambda: None
491
# Lock the branch, reset the record of remote calls.
495
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
497
[('call', 'Branch.set_last_revision',
498
('branch/', 'branch token', 'repo token', 'rev-id2'))],
501
self.assertEqual(None, result)
503
def test_no_such_revision(self):
504
transport = MemoryTransport()
505
transport.mkdir('branch')
506
transport = transport.clone('branch')
507
# A response of 'NoSuchRevision' is translated into an exception.
508
client = FakeClient(transport.base)
510
client.add_success_response('ok', 'branch token', 'repo token')
512
client.add_error_response('NoSuchRevision', 'rev-id')
514
client.add_success_response('ok')
516
bzrdir = RemoteBzrDir(transport, _client=False)
517
branch = RemoteBranch(bzrdir, None, _client=client)
518
branch._ensure_real = lambda: None
523
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
527
class TestBranchSetLastRevisionInfo(tests.TestCase):
529
def test_set_last_revision_info(self):
530
# set_last_revision_info(num, 'rev-id') is translated to calling
531
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
532
transport = MemoryTransport()
533
transport.mkdir('branch')
534
transport = transport.clone('branch')
535
client = FakeClient(transport.base)
537
client.add_success_response('ok', 'branch token', 'repo token')
539
client.add_success_response('ok')
541
client.add_success_response('ok')
543
bzrdir = RemoteBzrDir(transport, _client=False)
544
branch = RemoteBranch(bzrdir, None, _client=client)
545
# This is a hack to work around the problem that RemoteBranch currently
546
# unnecessarily invokes _ensure_real upon a call to lock_write.
547
branch._ensure_real = lambda: None
548
# Lock the branch, reset the record of remote calls.
551
result = branch.set_last_revision_info(1234, 'a-revision-id')
553
[('call', 'Branch.set_last_revision_info',
554
('branch/', 'branch token', 'repo token',
555
'1234', 'a-revision-id'))],
557
self.assertEqual(None, result)
559
def test_no_such_revision(self):
560
# A response of 'NoSuchRevision' is translated into an exception.
561
transport = MemoryTransport()
562
transport.mkdir('branch')
563
transport = transport.clone('branch')
564
client = FakeClient(transport.base)
566
client.add_success_response('ok', 'branch token', 'repo token')
568
client.add_error_response('NoSuchRevision', 'revid')
570
client.add_success_response('ok')
572
bzrdir = RemoteBzrDir(transport, _client=False)
573
branch = RemoteBranch(bzrdir, None, _client=client)
574
# This is a hack to work around the problem that RemoteBranch currently
575
# unnecessarily invokes _ensure_real upon a call to lock_write.
576
branch._ensure_real = lambda: None
577
# Lock the branch, reset the record of remote calls.
582
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
585
def lock_remote_branch(self, branch):
586
"""Trick a RemoteBranch into thinking it is locked."""
587
branch._lock_mode = 'w'
588
branch._lock_count = 2
589
branch._lock_token = 'branch token'
590
branch._repo_lock_token = 'repo token'
592
def test_backwards_compatibility(self):
593
"""If the server does not support the Branch.set_last_revision_info
594
verb (which is new in 1.4), then the client falls back to VFS methods.
596
# This test is a little messy. Unlike most tests in this file, it
597
# doesn't purely test what a Remote* object sends over the wire, and
598
# how it reacts to responses from the wire. It instead relies partly
599
# on asserting that the RemoteBranch will call
600
# self._real_branch.set_last_revision_info(...).
602
# First, set up our RemoteBranch with a FakeClient that raises
603
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
604
transport = MemoryTransport()
605
transport.mkdir('branch')
606
transport = transport.clone('branch')
607
client = FakeClient(transport.base)
608
client.add_unknown_method_response('Branch.set_last_revision_info')
609
bzrdir = RemoteBzrDir(transport, _client=False)
610
branch = RemoteBranch(bzrdir, None, _client=client)
611
class StubRealBranch(object):
614
def set_last_revision_info(self, revno, revision_id):
616
('set_last_revision_info', revno, revision_id))
617
real_branch = StubRealBranch()
618
branch._real_branch = real_branch
619
self.lock_remote_branch(branch)
621
# Call set_last_revision_info, and verify it behaved as expected.
622
result = branch.set_last_revision_info(1234, 'a-revision-id')
624
[('call', 'Branch.set_last_revision_info',
625
('branch/', 'branch token', 'repo token',
626
'1234', 'a-revision-id')),],
629
[('set_last_revision_info', 1234, 'a-revision-id')],
632
def test_unexpected_error(self):
633
# A response of 'NoSuchRevision' is translated into an exception.
634
transport = MemoryTransport()
635
transport.mkdir('branch')
636
transport = transport.clone('branch')
637
client = FakeClient(transport.base)
639
client.add_success_response('ok', 'branch token', 'repo token')
641
client.add_error_response('UnexpectedError')
643
client.add_success_response('ok')
645
bzrdir = RemoteBzrDir(transport, _client=False)
646
branch = RemoteBranch(bzrdir, None, _client=client)
647
# This is a hack to work around the problem that RemoteBranch currently
648
# unnecessarily invokes _ensure_real upon a call to lock_write.
649
branch._ensure_real = lambda: None
650
# Lock the branch, reset the record of remote calls.
654
err = self.assertRaises(
655
errors.ErrorFromSmartServer,
656
branch.set_last_revision_info, 123, 'revid')
657
self.assertEqual(('UnexpectedError',), err.error_tuple)
661
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
662
"""Getting the branch configuration should use an abstract method not vfs.
665
def test_get_branch_conf(self):
666
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
667
## # We should see that branch.get_config() does a single rpc to get the
668
## # remote configuration file, abstracting away where that is stored on
669
## # the server. However at the moment it always falls back to using the
670
## # vfs, and this would need some changes in config.py.
672
## # in an empty branch we decode the response properly
673
## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
674
## # we need to make a real branch because the remote_branch.control_files
675
## # will trigger _ensure_real.
676
## branch = self.make_branch('quack')
677
## transport = branch.bzrdir.root_transport
678
## # we do not want bzrdir to make any remote calls
679
## bzrdir = RemoteBzrDir(transport, _client=False)
680
## branch = RemoteBranch(bzrdir, None, _client=client)
681
## config = branch.get_config()
683
## [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
687
class TestBranchLockWrite(tests.TestCase):
689
def test_lock_write_unlockable(self):
690
transport = MemoryTransport()
691
client = FakeClient(transport.base)
692
client.add_error_response('UnlockableTransport')
693
transport.mkdir('quack')
694
transport = transport.clone('quack')
695
# we do not want bzrdir to make any remote calls
696
bzrdir = RemoteBzrDir(transport, _client=False)
697
branch = RemoteBranch(bzrdir, None, _client=client)
698
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
700
[('call', 'Branch.lock_write', ('quack/', '', ''))],
704
class TestTransportIsReadonly(tests.TestCase):
707
client = FakeClient()
708
client.add_success_response('yes')
709
transport = RemoteTransport('bzr://example.com/', medium=False,
711
self.assertEqual(True, transport.is_readonly())
713
[('call', 'Transport.is_readonly', ())],
716
def test_false(self):
717
client = FakeClient()
718
client.add_success_response('no')
719
transport = RemoteTransport('bzr://example.com/', medium=False,
721
self.assertEqual(False, transport.is_readonly())
723
[('call', 'Transport.is_readonly', ())],
726
def test_error_from_old_server(self):
727
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
729
Clients should treat it as a "no" response, because is_readonly is only
730
advisory anyway (a transport could be read-write, but then the
731
underlying filesystem could be readonly anyway).
733
client = FakeClient()
734
client.add_unknown_method_response('Transport.is_readonly')
735
transport = RemoteTransport('bzr://example.com/', medium=False,
737
self.assertEqual(False, transport.is_readonly())
739
[('call', 'Transport.is_readonly', ())],
743
class TestRemoteRepository(tests.TestCase):
744
"""Base for testing RemoteRepository protocol usage.
746
These tests contain frozen requests and responses. We want any changes to
747
what is sent or expected to be require a thoughtful update to these tests
748
because they might break compatibility with different-versioned servers.
751
def setup_fake_client_and_repository(self, transport_path):
752
"""Create the fake client and repository for testing with.
754
There's no real server here; we just have canned responses sent
757
:param transport_path: Path below the root of the MemoryTransport
758
where the repository will be created.
760
transport = MemoryTransport()
761
transport.mkdir(transport_path)
762
client = FakeClient(transport.base)
763
transport = transport.clone(transport_path)
764
# we do not want bzrdir to make any remote calls
765
bzrdir = RemoteBzrDir(transport, _client=False)
766
repo = RemoteRepository(bzrdir, None, _client=client)
770
class TestRepositoryGatherStats(TestRemoteRepository):
772
def test_revid_none(self):
773
# ('ok',), body with revisions and size
774
transport_path = 'quack'
775
repo, client = self.setup_fake_client_and_repository(transport_path)
776
client.add_success_response_with_body(
777
'revisions: 2\nsize: 18\n', 'ok')
778
result = repo.gather_stats(None)
780
[('call_expecting_body', 'Repository.gather_stats',
781
('quack/','','no'))],
783
self.assertEqual({'revisions': 2, 'size': 18}, result)
785
def test_revid_no_committers(self):
786
# ('ok',), body without committers
787
body = ('firstrev: 123456.300 3600\n'
788
'latestrev: 654231.400 0\n'
791
transport_path = 'quick'
792
revid = u'\xc8'.encode('utf8')
793
repo, client = self.setup_fake_client_and_repository(transport_path)
794
client.add_success_response_with_body(body, 'ok')
795
result = repo.gather_stats(revid)
797
[('call_expecting_body', 'Repository.gather_stats',
798
('quick/', revid, 'no'))],
800
self.assertEqual({'revisions': 2, 'size': 18,
801
'firstrev': (123456.300, 3600),
802
'latestrev': (654231.400, 0),},
805
def test_revid_with_committers(self):
806
# ('ok',), body with committers
807
body = ('committers: 128\n'
808
'firstrev: 123456.300 3600\n'
809
'latestrev: 654231.400 0\n'
812
transport_path = 'buick'
813
revid = u'\xc8'.encode('utf8')
814
repo, client = self.setup_fake_client_and_repository(transport_path)
815
client.add_success_response_with_body(body, 'ok')
816
result = repo.gather_stats(revid, True)
818
[('call_expecting_body', 'Repository.gather_stats',
819
('buick/', revid, 'yes'))],
821
self.assertEqual({'revisions': 2, 'size': 18,
823
'firstrev': (123456.300, 3600),
824
'latestrev': (654231.400, 0),},
828
class TestRepositoryGetGraph(TestRemoteRepository):
830
def test_get_graph(self):
831
# get_graph returns a graph with the repository as the
833
transport_path = 'quack'
834
repo, client = self.setup_fake_client_and_repository(transport_path)
835
graph = repo.get_graph()
836
self.assertEqual(graph._parents_provider, repo)
839
class TestRepositoryGetParentMap(TestRemoteRepository):
841
def test_get_parent_map_caching(self):
842
# get_parent_map returns from cache until unlock()
843
# setup a reponse with two revisions
844
r1 = u'\u0e33'.encode('utf8')
845
r2 = u'\u0dab'.encode('utf8')
846
lines = [' '.join([r2, r1]), r1]
847
encoded_body = bz2.compress('\n'.join(lines))
849
transport_path = 'quack'
850
repo, client = self.setup_fake_client_and_repository(transport_path)
851
client.add_success_response_with_body(encoded_body, 'ok')
852
client.add_success_response_with_body(encoded_body, 'ok')
854
graph = repo.get_graph()
855
parents = graph.get_parent_map([r2])
856
self.assertEqual({r2: (r1,)}, parents)
857
# locking and unlocking deeper should not reset
860
parents = graph.get_parent_map([r1])
861
self.assertEqual({r1: (NULL_REVISION,)}, parents)
863
[('call_with_body_bytes_expecting_body',
864
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
867
# now we call again, and it should use the second response.
869
graph = repo.get_graph()
870
parents = graph.get_parent_map([r1])
871
self.assertEqual({r1: (NULL_REVISION,)}, parents)
873
[('call_with_body_bytes_expecting_body',
874
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
875
('call_with_body_bytes_expecting_body',
876
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
881
def test_get_parent_map_reconnects_if_unknown_method(self):
882
transport_path = 'quack'
883
repo, client = self.setup_fake_client_and_repository(transport_path)
884
client.add_unknown_method_response('Repository,get_parent_map')
885
client.add_success_response_with_body('', 'ok')
886
self.assertTrue(client._medium._remote_is_at_least_1_2)
887
rev_id = 'revision-id'
888
expected_deprecations = [
889
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
891
parents = self.callDeprecated(
892
expected_deprecations, repo.get_parent_map, [rev_id])
894
[('call_with_body_bytes_expecting_body',
895
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
896
('disconnect medium',),
897
('call_expecting_body', 'Repository.get_revision_graph',
900
# The medium is now marked as being connected to an older server
901
self.assertFalse(client._medium._remote_is_at_least_1_2)
903
def test_get_parent_map_fallback_parentless_node(self):
904
"""get_parent_map falls back to get_revision_graph on old servers. The
905
results from get_revision_graph are tweaked to match the get_parent_map
908
Specifically, a {key: ()} result from get_revision_graph means "no
909
parents" for that key, which in get_parent_map results should be
910
represented as {key: ('null:',)}.
912
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
914
rev_id = 'revision-id'
915
transport_path = 'quack'
916
repo, client = self.setup_fake_client_and_repository(transport_path)
917
client.add_success_response_with_body(rev_id, 'ok')
918
client._medium._remote_is_at_least_1_2 = False
919
expected_deprecations = [
920
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
922
parents = self.callDeprecated(
923
expected_deprecations, repo.get_parent_map, [rev_id])
925
[('call_expecting_body', 'Repository.get_revision_graph',
928
self.assertEqual({rev_id: ('null:',)}, parents)
930
def test_get_parent_map_unexpected_response(self):
931
repo, client = self.setup_fake_client_and_repository('path')
932
client.add_success_response('something unexpected!')
934
errors.UnexpectedSmartServerResponse,
935
repo.get_parent_map, ['a-revision-id'])
938
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
940
def test_null_revision(self):
941
# a null revision has the predictable result {}, we should have no wire
942
# traffic when calling it with this argument
943
transport_path = 'empty'
944
repo, client = self.setup_fake_client_and_repository(transport_path)
945
client.add_success_response('notused')
946
result = self.applyDeprecated(one_four, repo.get_revision_graph,
948
self.assertEqual([], client._calls)
949
self.assertEqual({}, result)
951
def test_none_revision(self):
952
# with none we want the entire graph
953
r1 = u'\u0e33'.encode('utf8')
954
r2 = u'\u0dab'.encode('utf8')
955
lines = [' '.join([r2, r1]), r1]
956
encoded_body = '\n'.join(lines)
958
transport_path = 'sinhala'
959
repo, client = self.setup_fake_client_and_repository(transport_path)
960
client.add_success_response_with_body(encoded_body, 'ok')
961
result = self.applyDeprecated(one_four, repo.get_revision_graph)
963
[('call_expecting_body', 'Repository.get_revision_graph',
966
self.assertEqual({r1: (), r2: (r1, )}, result)
968
def test_specific_revision(self):
969
# with a specific revision we want the graph for that
970
# with none we want the entire graph
971
r11 = u'\u0e33'.encode('utf8')
972
r12 = u'\xc9'.encode('utf8')
973
r2 = u'\u0dab'.encode('utf8')
974
lines = [' '.join([r2, r11, r12]), r11, r12]
975
encoded_body = '\n'.join(lines)
977
transport_path = 'sinhala'
978
repo, client = self.setup_fake_client_and_repository(transport_path)
979
client.add_success_response_with_body(encoded_body, 'ok')
980
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
982
[('call_expecting_body', 'Repository.get_revision_graph',
985
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
987
def test_no_such_revision(self):
989
transport_path = 'sinhala'
990
repo, client = self.setup_fake_client_and_repository(transport_path)
991
client.add_error_response('nosuchrevision', revid)
992
# also check that the right revision is reported in the error
993
self.assertRaises(errors.NoSuchRevision,
994
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
996
[('call_expecting_body', 'Repository.get_revision_graph',
997
('sinhala/', revid))],
1000
def test_unexpected_error(self):
1002
transport_path = 'sinhala'
1003
repo, client = self.setup_fake_client_and_repository(transport_path)
1004
client.add_error_response('AnUnexpectedError')
1005
e = self.assertRaises(errors.ErrorFromSmartServer,
1006
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1007
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1010
class TestRepositoryIsShared(TestRemoteRepository):
1012
def test_is_shared(self):
1013
# ('yes', ) for Repository.is_shared -> 'True'.
1014
transport_path = 'quack'
1015
repo, client = self.setup_fake_client_and_repository(transport_path)
1016
client.add_success_response('yes')
1017
result = repo.is_shared()
1019
[('call', 'Repository.is_shared', ('quack/',))],
1021
self.assertEqual(True, result)
1023
def test_is_not_shared(self):
1024
# ('no', ) for Repository.is_shared -> 'False'.
1025
transport_path = 'qwack'
1026
repo, client = self.setup_fake_client_and_repository(transport_path)
1027
client.add_success_response('no')
1028
result = repo.is_shared()
1030
[('call', 'Repository.is_shared', ('qwack/',))],
1032
self.assertEqual(False, result)
1035
class TestRepositoryLockWrite(TestRemoteRepository):
1037
def test_lock_write(self):
1038
transport_path = 'quack'
1039
repo, client = self.setup_fake_client_and_repository(transport_path)
1040
client.add_success_response('ok', 'a token')
1041
result = repo.lock_write()
1043
[('call', 'Repository.lock_write', ('quack/', ''))],
1045
self.assertEqual('a token', result)
1047
def test_lock_write_already_locked(self):
1048
transport_path = 'quack'
1049
repo, client = self.setup_fake_client_and_repository(transport_path)
1050
client.add_error_response('LockContention')
1051
self.assertRaises(errors.LockContention, repo.lock_write)
1053
[('call', 'Repository.lock_write', ('quack/', ''))],
1056
def test_lock_write_unlockable(self):
1057
transport_path = 'quack'
1058
repo, client = self.setup_fake_client_and_repository(transport_path)
1059
client.add_error_response('UnlockableTransport')
1060
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1062
[('call', 'Repository.lock_write', ('quack/', ''))],
1066
class TestRepositoryUnlock(TestRemoteRepository):
1068
def test_unlock(self):
1069
transport_path = 'quack'
1070
repo, client = self.setup_fake_client_and_repository(transport_path)
1071
client.add_success_response('ok', 'a token')
1072
client.add_success_response('ok')
1076
[('call', 'Repository.lock_write', ('quack/', '')),
1077
('call', 'Repository.unlock', ('quack/', 'a token'))],
1080
def test_unlock_wrong_token(self):
1081
# If somehow the token is wrong, unlock will raise TokenMismatch.
1082
transport_path = 'quack'
1083
repo, client = self.setup_fake_client_and_repository(transport_path)
1084
client.add_success_response('ok', 'a token')
1085
client.add_error_response('TokenMismatch')
1087
self.assertRaises(errors.TokenMismatch, repo.unlock)
1090
class TestRepositoryHasRevision(TestRemoteRepository):
1092
def test_none(self):
1093
# repo.has_revision(None) should not cause any traffic.
1094
transport_path = 'quack'
1095
repo, client = self.setup_fake_client_and_repository(transport_path)
1097
# The null revision is always there, so has_revision(None) == True.
1098
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1100
# The remote repo shouldn't be accessed.
1101
self.assertEqual([], client._calls)
1104
class TestRepositoryTarball(TestRemoteRepository):
1106
# This is a canned tarball reponse we can validate against
1108
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1109
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1110
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1111
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1112
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1113
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1114
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1115
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1116
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1117
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1118
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1119
'nWQ7QH/F3JFOFCQ0aSPfA='
1122
def test_repository_tarball(self):
1123
# Test that Repository.tarball generates the right operations
1124
transport_path = 'repo'
1125
expected_calls = [('call_expecting_body', 'Repository.tarball',
1126
('repo/', 'bz2',),),
1128
repo, client = self.setup_fake_client_and_repository(transport_path)
1129
client.add_success_response_with_body(self.tarball_content, 'ok')
1130
# Now actually ask for the tarball
1131
tarball_file = repo._get_tarball('bz2')
1133
self.assertEqual(expected_calls, client._calls)
1134
self.assertEqual(self.tarball_content, tarball_file.read())
1136
tarball_file.close()
1139
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1140
"""RemoteRepository.copy_content_into optimizations"""
1142
def test_copy_content_remote_to_local(self):
1143
self.transport_server = server.SmartTCPServer_for_testing
1144
src_repo = self.make_repository('repo1')
1145
src_repo = repository.Repository.open(self.get_url('repo1'))
1146
# At the moment the tarball-based copy_content_into can't write back
1147
# into a smart server. It would be good if it could upload the
1148
# tarball; once that works we'd have to create repositories of
1149
# different formats. -- mbp 20070410
1150
dest_url = self.get_vfs_only_url('repo2')
1151
dest_bzrdir = BzrDir.create(dest_url)
1152
dest_repo = dest_bzrdir.create_repository()
1153
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1154
self.assertTrue(isinstance(src_repo, RemoteRepository))
1155
src_repo.copy_content_into(dest_repo)
1158
class TestRepositoryStreamKnitData(TestRemoteRepository):
1160
def make_pack_file(self, records):
1161
pack_file = StringIO()
1162
pack_writer = pack.ContainerWriter(pack_file.write)
1164
for bytes, names in records:
1165
pack_writer.add_bytes_record(bytes, names)
1170
def make_pack_stream(self, records):
1171
pack_serialiser = pack.ContainerSerialiser()
1172
yield pack_serialiser.begin()
1173
for bytes, names in records:
1174
yield pack_serialiser.bytes_record(bytes, names)
1175
yield pack_serialiser.end()
1177
def test_bad_pack_from_server(self):
1178
"""A response with invalid data (e.g. it has a record with multiple
1179
names) triggers an exception.
1181
Not all possible errors will be caught at this stage, but obviously
1182
malformed data should be.
1184
record = ('bytes', [('name1',), ('name2',)])
1185
pack_stream = self.make_pack_stream([record])
1186
transport_path = 'quack'
1187
repo, client = self.setup_fake_client_and_repository(transport_path)
1188
client.add_success_response_with_body(pack_stream, 'ok')
1189
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1190
stream = repo.get_data_stream_for_search(search)
1191
self.assertRaises(errors.SmartProtocolError, list, stream)
1193
def test_backwards_compatibility(self):
1194
"""If the server doesn't recognise this request, fallback to VFS."""
1195
repo, client = self.setup_fake_client_and_repository('path')
1196
client.add_unknown_method_response(
1197
'Repository.stream_revisions_chunked')
1198
self.mock_called = False
1199
repo._real_repository = MockRealRepository(self)
1200
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1201
repo.get_data_stream_for_search(search)
1202
self.assertTrue(self.mock_called)
1203
self.failIf(client.expecting_body,
1204
"The protocol has been left in an unclean state that will cause "
1205
"TooManyConcurrentRequests errors.")
1208
class MockRealRepository(object):
1209
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
1211
def __init__(self, test):
1214
def get_data_stream_for_search(self, search):
1215
self.test.assertEqual(set(['revid']), search.get_keys())
1216
self.test.mock_called = True