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(self.get_url())
674
client.add_success_response_with_body('# config file body', 'ok')
675
# we need to make a real branch because the remote_branch.control_files
676
# will trigger _ensure_real.
677
branch = self.make_branch('quack')
678
transport = branch.bzrdir.root_transport
679
# we do not want bzrdir to make any remote calls
680
bzrdir = RemoteBzrDir(transport, _client=False)
681
branch = RemoteBranch(bzrdir, None, _client=client)
682
config = branch.get_config()
684
[('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
688
class TestBranchLockWrite(tests.TestCase):
690
def test_lock_write_unlockable(self):
691
transport = MemoryTransport()
692
client = FakeClient(transport.base)
693
client.add_error_response('UnlockableTransport')
694
transport.mkdir('quack')
695
transport = transport.clone('quack')
696
# we do not want bzrdir to make any remote calls
697
bzrdir = RemoteBzrDir(transport, _client=False)
698
branch = RemoteBranch(bzrdir, None, _client=client)
699
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
701
[('call', 'Branch.lock_write', ('quack/', '', ''))],
705
class TestTransportIsReadonly(tests.TestCase):
708
client = FakeClient()
709
client.add_success_response('yes')
710
transport = RemoteTransport('bzr://example.com/', medium=False,
712
self.assertEqual(True, transport.is_readonly())
714
[('call', 'Transport.is_readonly', ())],
717
def test_false(self):
718
client = FakeClient()
719
client.add_success_response('no')
720
transport = RemoteTransport('bzr://example.com/', medium=False,
722
self.assertEqual(False, transport.is_readonly())
724
[('call', 'Transport.is_readonly', ())],
727
def test_error_from_old_server(self):
728
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
730
Clients should treat it as a "no" response, because is_readonly is only
731
advisory anyway (a transport could be read-write, but then the
732
underlying filesystem could be readonly anyway).
734
client = FakeClient()
735
client.add_unknown_method_response('Transport.is_readonly')
736
transport = RemoteTransport('bzr://example.com/', medium=False,
738
self.assertEqual(False, transport.is_readonly())
740
[('call', 'Transport.is_readonly', ())],
744
class TestRemoteRepository(tests.TestCase):
745
"""Base for testing RemoteRepository protocol usage.
747
These tests contain frozen requests and responses. We want any changes to
748
what is sent or expected to be require a thoughtful update to these tests
749
because they might break compatibility with different-versioned servers.
752
def setup_fake_client_and_repository(self, transport_path):
753
"""Create the fake client and repository for testing with.
755
There's no real server here; we just have canned responses sent
758
:param transport_path: Path below the root of the MemoryTransport
759
where the repository will be created.
761
transport = MemoryTransport()
762
transport.mkdir(transport_path)
763
client = FakeClient(transport.base)
764
transport = transport.clone(transport_path)
765
# we do not want bzrdir to make any remote calls
766
bzrdir = RemoteBzrDir(transport, _client=False)
767
repo = RemoteRepository(bzrdir, None, _client=client)
771
class TestRepositoryGatherStats(TestRemoteRepository):
773
def test_revid_none(self):
774
# ('ok',), body with revisions and size
775
transport_path = 'quack'
776
repo, client = self.setup_fake_client_and_repository(transport_path)
777
client.add_success_response_with_body(
778
'revisions: 2\nsize: 18\n', 'ok')
779
result = repo.gather_stats(None)
781
[('call_expecting_body', 'Repository.gather_stats',
782
('quack/','','no'))],
784
self.assertEqual({'revisions': 2, 'size': 18}, result)
786
def test_revid_no_committers(self):
787
# ('ok',), body without committers
788
body = ('firstrev: 123456.300 3600\n'
789
'latestrev: 654231.400 0\n'
792
transport_path = 'quick'
793
revid = u'\xc8'.encode('utf8')
794
repo, client = self.setup_fake_client_and_repository(transport_path)
795
client.add_success_response_with_body(body, 'ok')
796
result = repo.gather_stats(revid)
798
[('call_expecting_body', 'Repository.gather_stats',
799
('quick/', revid, 'no'))],
801
self.assertEqual({'revisions': 2, 'size': 18,
802
'firstrev': (123456.300, 3600),
803
'latestrev': (654231.400, 0),},
806
def test_revid_with_committers(self):
807
# ('ok',), body with committers
808
body = ('committers: 128\n'
809
'firstrev: 123456.300 3600\n'
810
'latestrev: 654231.400 0\n'
813
transport_path = 'buick'
814
revid = u'\xc8'.encode('utf8')
815
repo, client = self.setup_fake_client_and_repository(transport_path)
816
client.add_success_response_with_body(body, 'ok')
817
result = repo.gather_stats(revid, True)
819
[('call_expecting_body', 'Repository.gather_stats',
820
('buick/', revid, 'yes'))],
822
self.assertEqual({'revisions': 2, 'size': 18,
824
'firstrev': (123456.300, 3600),
825
'latestrev': (654231.400, 0),},
829
class TestRepositoryGetGraph(TestRemoteRepository):
831
def test_get_graph(self):
832
# get_graph returns a graph with the repository as the
834
transport_path = 'quack'
835
repo, client = self.setup_fake_client_and_repository(transport_path)
836
graph = repo.get_graph()
837
self.assertEqual(graph._parents_provider, repo)
840
class TestRepositoryGetParentMap(TestRemoteRepository):
842
def test_get_parent_map_caching(self):
843
# get_parent_map returns from cache until unlock()
844
# setup a reponse with two revisions
845
r1 = u'\u0e33'.encode('utf8')
846
r2 = u'\u0dab'.encode('utf8')
847
lines = [' '.join([r2, r1]), r1]
848
encoded_body = bz2.compress('\n'.join(lines))
850
transport_path = 'quack'
851
repo, client = self.setup_fake_client_and_repository(transport_path)
852
client.add_success_response_with_body(encoded_body, 'ok')
853
client.add_success_response_with_body(encoded_body, 'ok')
855
graph = repo.get_graph()
856
parents = graph.get_parent_map([r2])
857
self.assertEqual({r2: (r1,)}, parents)
858
# locking and unlocking deeper should not reset
861
parents = graph.get_parent_map([r1])
862
self.assertEqual({r1: (NULL_REVISION,)}, parents)
864
[('call_with_body_bytes_expecting_body',
865
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
868
# now we call again, and it should use the second response.
870
graph = repo.get_graph()
871
parents = graph.get_parent_map([r1])
872
self.assertEqual({r1: (NULL_REVISION,)}, parents)
874
[('call_with_body_bytes_expecting_body',
875
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
876
('call_with_body_bytes_expecting_body',
877
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
882
def test_get_parent_map_reconnects_if_unknown_method(self):
883
transport_path = 'quack'
884
repo, client = self.setup_fake_client_and_repository(transport_path)
885
client.add_unknown_method_response('Repository,get_parent_map')
886
client.add_success_response_with_body('', 'ok')
887
self.assertTrue(client._medium._remote_is_at_least_1_2)
888
rev_id = 'revision-id'
889
expected_deprecations = [
890
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
892
parents = self.callDeprecated(
893
expected_deprecations, repo.get_parent_map, [rev_id])
895
[('call_with_body_bytes_expecting_body',
896
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
897
('disconnect medium',),
898
('call_expecting_body', 'Repository.get_revision_graph',
901
# The medium is now marked as being connected to an older server
902
self.assertFalse(client._medium._remote_is_at_least_1_2)
904
def test_get_parent_map_fallback_parentless_node(self):
905
"""get_parent_map falls back to get_revision_graph on old servers. The
906
results from get_revision_graph are tweaked to match the get_parent_map
909
Specifically, a {key: ()} result from get_revision_graph means "no
910
parents" for that key, which in get_parent_map results should be
911
represented as {key: ('null:',)}.
913
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
915
rev_id = 'revision-id'
916
transport_path = 'quack'
917
repo, client = self.setup_fake_client_and_repository(transport_path)
918
client.add_success_response_with_body(rev_id, 'ok')
919
client._medium._remote_is_at_least_1_2 = False
920
expected_deprecations = [
921
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
923
parents = self.callDeprecated(
924
expected_deprecations, repo.get_parent_map, [rev_id])
926
[('call_expecting_body', 'Repository.get_revision_graph',
929
self.assertEqual({rev_id: ('null:',)}, parents)
931
def test_get_parent_map_unexpected_response(self):
932
repo, client = self.setup_fake_client_and_repository('path')
933
client.add_success_response('something unexpected!')
935
errors.UnexpectedSmartServerResponse,
936
repo.get_parent_map, ['a-revision-id'])
939
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
941
def test_null_revision(self):
942
# a null revision has the predictable result {}, we should have no wire
943
# traffic when calling it with this argument
944
transport_path = 'empty'
945
repo, client = self.setup_fake_client_and_repository(transport_path)
946
client.add_success_response('notused')
947
result = self.applyDeprecated(one_four, repo.get_revision_graph,
949
self.assertEqual([], client._calls)
950
self.assertEqual({}, result)
952
def test_none_revision(self):
953
# with none we want the entire graph
954
r1 = u'\u0e33'.encode('utf8')
955
r2 = u'\u0dab'.encode('utf8')
956
lines = [' '.join([r2, r1]), r1]
957
encoded_body = '\n'.join(lines)
959
transport_path = 'sinhala'
960
repo, client = self.setup_fake_client_and_repository(transport_path)
961
client.add_success_response_with_body(encoded_body, 'ok')
962
result = self.applyDeprecated(one_four, repo.get_revision_graph)
964
[('call_expecting_body', 'Repository.get_revision_graph',
967
self.assertEqual({r1: (), r2: (r1, )}, result)
969
def test_specific_revision(self):
970
# with a specific revision we want the graph for that
971
# with none we want the entire graph
972
r11 = u'\u0e33'.encode('utf8')
973
r12 = u'\xc9'.encode('utf8')
974
r2 = u'\u0dab'.encode('utf8')
975
lines = [' '.join([r2, r11, r12]), r11, r12]
976
encoded_body = '\n'.join(lines)
978
transport_path = 'sinhala'
979
repo, client = self.setup_fake_client_and_repository(transport_path)
980
client.add_success_response_with_body(encoded_body, 'ok')
981
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
983
[('call_expecting_body', 'Repository.get_revision_graph',
986
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
988
def test_no_such_revision(self):
990
transport_path = 'sinhala'
991
repo, client = self.setup_fake_client_and_repository(transport_path)
992
client.add_error_response('nosuchrevision', revid)
993
# also check that the right revision is reported in the error
994
self.assertRaises(errors.NoSuchRevision,
995
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
997
[('call_expecting_body', 'Repository.get_revision_graph',
998
('sinhala/', revid))],
1001
def test_unexpected_error(self):
1003
transport_path = 'sinhala'
1004
repo, client = self.setup_fake_client_and_repository(transport_path)
1005
client.add_error_response('AnUnexpectedError')
1006
e = self.assertRaises(errors.ErrorFromSmartServer,
1007
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
1008
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1011
class TestRepositoryIsShared(TestRemoteRepository):
1013
def test_is_shared(self):
1014
# ('yes', ) for Repository.is_shared -> 'True'.
1015
transport_path = 'quack'
1016
repo, client = self.setup_fake_client_and_repository(transport_path)
1017
client.add_success_response('yes')
1018
result = repo.is_shared()
1020
[('call', 'Repository.is_shared', ('quack/',))],
1022
self.assertEqual(True, result)
1024
def test_is_not_shared(self):
1025
# ('no', ) for Repository.is_shared -> 'False'.
1026
transport_path = 'qwack'
1027
repo, client = self.setup_fake_client_and_repository(transport_path)
1028
client.add_success_response('no')
1029
result = repo.is_shared()
1031
[('call', 'Repository.is_shared', ('qwack/',))],
1033
self.assertEqual(False, result)
1036
class TestRepositoryLockWrite(TestRemoteRepository):
1038
def test_lock_write(self):
1039
transport_path = 'quack'
1040
repo, client = self.setup_fake_client_and_repository(transport_path)
1041
client.add_success_response('ok', 'a token')
1042
result = repo.lock_write()
1044
[('call', 'Repository.lock_write', ('quack/', ''))],
1046
self.assertEqual('a token', result)
1048
def test_lock_write_already_locked(self):
1049
transport_path = 'quack'
1050
repo, client = self.setup_fake_client_and_repository(transport_path)
1051
client.add_error_response('LockContention')
1052
self.assertRaises(errors.LockContention, repo.lock_write)
1054
[('call', 'Repository.lock_write', ('quack/', ''))],
1057
def test_lock_write_unlockable(self):
1058
transport_path = 'quack'
1059
repo, client = self.setup_fake_client_and_repository(transport_path)
1060
client.add_error_response('UnlockableTransport')
1061
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1063
[('call', 'Repository.lock_write', ('quack/', ''))],
1067
class TestRepositoryUnlock(TestRemoteRepository):
1069
def test_unlock(self):
1070
transport_path = 'quack'
1071
repo, client = self.setup_fake_client_and_repository(transport_path)
1072
client.add_success_response('ok', 'a token')
1073
client.add_success_response('ok')
1077
[('call', 'Repository.lock_write', ('quack/', '')),
1078
('call', 'Repository.unlock', ('quack/', 'a token'))],
1081
def test_unlock_wrong_token(self):
1082
# If somehow the token is wrong, unlock will raise TokenMismatch.
1083
transport_path = 'quack'
1084
repo, client = self.setup_fake_client_and_repository(transport_path)
1085
client.add_success_response('ok', 'a token')
1086
client.add_error_response('TokenMismatch')
1088
self.assertRaises(errors.TokenMismatch, repo.unlock)
1091
class TestRepositoryHasRevision(TestRemoteRepository):
1093
def test_none(self):
1094
# repo.has_revision(None) should not cause any traffic.
1095
transport_path = 'quack'
1096
repo, client = self.setup_fake_client_and_repository(transport_path)
1098
# The null revision is always there, so has_revision(None) == True.
1099
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1101
# The remote repo shouldn't be accessed.
1102
self.assertEqual([], client._calls)
1105
class TestRepositoryTarball(TestRemoteRepository):
1107
# This is a canned tarball reponse we can validate against
1109
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1110
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1111
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1112
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1113
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1114
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1115
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1116
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1117
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1118
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1119
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1120
'nWQ7QH/F3JFOFCQ0aSPfA='
1123
def test_repository_tarball(self):
1124
# Test that Repository.tarball generates the right operations
1125
transport_path = 'repo'
1126
expected_calls = [('call_expecting_body', 'Repository.tarball',
1127
('repo/', 'bz2',),),
1129
repo, client = self.setup_fake_client_and_repository(transport_path)
1130
client.add_success_response_with_body(self.tarball_content, 'ok')
1131
# Now actually ask for the tarball
1132
tarball_file = repo._get_tarball('bz2')
1134
self.assertEqual(expected_calls, client._calls)
1135
self.assertEqual(self.tarball_content, tarball_file.read())
1137
tarball_file.close()
1140
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1141
"""RemoteRepository.copy_content_into optimizations"""
1143
def test_copy_content_remote_to_local(self):
1144
self.transport_server = server.SmartTCPServer_for_testing
1145
src_repo = self.make_repository('repo1')
1146
src_repo = repository.Repository.open(self.get_url('repo1'))
1147
# At the moment the tarball-based copy_content_into can't write back
1148
# into a smart server. It would be good if it could upload the
1149
# tarball; once that works we'd have to create repositories of
1150
# different formats. -- mbp 20070410
1151
dest_url = self.get_vfs_only_url('repo2')
1152
dest_bzrdir = BzrDir.create(dest_url)
1153
dest_repo = dest_bzrdir.create_repository()
1154
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1155
self.assertTrue(isinstance(src_repo, RemoteRepository))
1156
src_repo.copy_content_into(dest_repo)
1159
class TestRepositoryStreamKnitData(TestRemoteRepository):
1161
def make_pack_file(self, records):
1162
pack_file = StringIO()
1163
pack_writer = pack.ContainerWriter(pack_file.write)
1165
for bytes, names in records:
1166
pack_writer.add_bytes_record(bytes, names)
1171
def make_pack_stream(self, records):
1172
pack_serialiser = pack.ContainerSerialiser()
1173
yield pack_serialiser.begin()
1174
for bytes, names in records:
1175
yield pack_serialiser.bytes_record(bytes, names)
1176
yield pack_serialiser.end()
1178
def test_bad_pack_from_server(self):
1179
"""A response with invalid data (e.g. it has a record with multiple
1180
names) triggers an exception.
1182
Not all possible errors will be caught at this stage, but obviously
1183
malformed data should be.
1185
record = ('bytes', [('name1',), ('name2',)])
1186
pack_stream = self.make_pack_stream([record])
1187
transport_path = 'quack'
1188
repo, client = self.setup_fake_client_and_repository(transport_path)
1189
client.add_success_response_with_body(pack_stream, 'ok')
1190
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1191
stream = repo.get_data_stream_for_search(search)
1192
self.assertRaises(errors.SmartProtocolError, list, stream)
1194
def test_backwards_compatibility(self):
1195
"""If the server doesn't recognise this request, fallback to VFS."""
1196
repo, client = self.setup_fake_client_and_repository('path')
1197
client.add_unknown_method_response(
1198
'Repository.stream_revisions_chunked')
1199
self.mock_called = False
1200
repo._real_repository = MockRealRepository(self)
1201
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1202
repo.get_data_stream_for_search(search)
1203
self.assertTrue(self.mock_called)
1204
self.failIf(client.expecting_body,
1205
"The protocol has been left in an unclean state that will cause "
1206
"TooManyConcurrentRequests errors.")
1209
class MockRealRepository(object):
1210
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
1212
def __init__(self, test):
1215
def get_data_stream_for_search(self, search):
1216
self.test.assertEqual(set(['revid']), search.get_keys())
1217
self.test.mock_called = True