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