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
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(object):
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_SmartClient_remote_path_from_transport(tests.TestCase):
213
"""Tests for the behaviour of _SmartClient.remote_path_from_transport."""
215
def assertRemotePath(self, expected, client_base, transport_base):
216
"""Assert that the result of _SmartClient.remote_path_from_transport
217
is the expected value for a given client_base and transport_base.
219
class DummyMedium(object):
221
client = _SmartClient(DummyMedium())
222
transport = get_transport(transport_base)
223
result = client.remote_path_from_transport(transport)
224
self.assertEqual(expected, result)
226
def test_remote_path_from_transport(self):
227
"""_SmartClient.remote_path_from_transport calculates a URL for the
228
given transport relative to the root of the client base URL.
230
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
231
self.assertRemotePath(
232
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
234
def test_remote_path_from_transport_http(self):
235
"""Remote paths for HTTP transports are calculated differently to other
236
transports. They are just relative to the client base, not the root
237
directory of the host.
239
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
240
self.assertRemotePath(
241
'../xyz/', scheme + '//host/path', scheme + '//host/xyz')
242
self.assertRemotePath(
243
'xyz/', scheme + '//host/path', scheme + '//host/path/xyz')
246
class TestBzrDirOpenBranch(tests.TestCase):
248
def test_branch_present(self):
249
transport = MemoryTransport()
250
transport.mkdir('quack')
251
transport = transport.clone('quack')
252
client = FakeClient(transport.base)
253
client.add_success_response('ok', '')
254
client.add_success_response('ok', '', 'no', 'no', 'no')
255
bzrdir = RemoteBzrDir(transport, _client=client)
256
result = bzrdir.open_branch()
258
[('call', 'BzrDir.open_branch', ('quack/',)),
259
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
261
self.assertIsInstance(result, RemoteBranch)
262
self.assertEqual(bzrdir, result.bzrdir)
264
def test_branch_missing(self):
265
transport = MemoryTransport()
266
transport.mkdir('quack')
267
transport = transport.clone('quack')
268
client = FakeClient(transport.base)
269
client.add_error_response('nobranch')
270
bzrdir = RemoteBzrDir(transport, _client=client)
271
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
273
[('call', 'BzrDir.open_branch', ('quack/',))],
276
def test__get_tree_branch(self):
277
# _get_tree_branch is a form of open_branch, but it should only ask for
278
# branch opening, not any other network requests.
281
calls.append("Called")
283
transport = MemoryTransport()
284
# no requests on the network - catches other api calls being made.
285
client = FakeClient(transport.base)
286
bzrdir = RemoteBzrDir(transport, _client=client)
287
# patch the open_branch call to record that it was called.
288
bzrdir.open_branch = open_branch
289
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
290
self.assertEqual(["Called"], calls)
291
self.assertEqual([], client._calls)
293
def test_url_quoting_of_path(self):
294
# Relpaths on the wire should not be URL-escaped. So "~" should be
295
# transmitted as "~", not "%7E".
296
transport = RemoteTCPTransport('bzr://localhost/~hello/')
297
client = FakeClient(transport.base)
298
client.add_success_response('ok', '')
299
client.add_success_response('ok', '', 'no', 'no', 'no')
300
bzrdir = RemoteBzrDir(transport, _client=client)
301
result = bzrdir.open_branch()
303
[('call', 'BzrDir.open_branch', ('~hello/',)),
304
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
307
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
308
transport = MemoryTransport()
309
transport.mkdir('quack')
310
transport = transport.clone('quack')
312
rich_response = 'yes'
316
subtree_response = 'yes'
318
subtree_response = 'no'
319
client = FakeClient(transport.base)
320
client.add_success_response(
321
'ok', '', rich_response, subtree_response, external_lookup)
322
bzrdir = RemoteBzrDir(transport, _client=client)
323
result = bzrdir.open_repository()
325
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
327
self.assertIsInstance(result, RemoteRepository)
328
self.assertEqual(bzrdir, result.bzrdir)
329
self.assertEqual(rich_root, result._format.rich_root_data)
330
self.assertEqual(subtrees, result._format.supports_tree_reference)
332
def test_open_repository_sets_format_attributes(self):
333
self.check_open_repository(True, True)
334
self.check_open_repository(False, True)
335
self.check_open_repository(True, False)
336
self.check_open_repository(False, False)
337
self.check_open_repository(False, False, 'yes')
339
def test_old_server(self):
340
"""RemoteBzrDirFormat should fail to probe if the server version is too
343
self.assertRaises(errors.NotBranchError,
344
RemoteBzrDirFormat.probe_transport, OldServerTransport())
347
class TestBzrDirOpenRepository(tests.TestCase):
349
def test_backwards_compat_1_2(self):
350
transport = MemoryTransport()
351
transport.mkdir('quack')
352
transport = transport.clone('quack')
353
client = FakeClient(transport.base)
354
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
355
client.add_success_response('ok', '', 'no', 'no')
356
bzrdir = RemoteBzrDir(transport, _client=client)
357
repo = bzrdir.open_repository()
359
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
360
('call', 'BzrDir.find_repository', ('quack/',))],
364
class OldSmartClient(object):
365
"""A fake smart client for test_old_version that just returns a version one
366
response to the 'hello' (query version) command.
369
def get_request(self):
370
input_file = StringIO('ok\x011\n')
371
output_file = StringIO()
372
client_medium = medium.SmartSimplePipesClientMedium(
373
input_file, output_file)
374
return medium.SmartClientStreamMediumRequest(client_medium)
376
def protocol_version(self):
380
class OldServerTransport(object):
381
"""A fake transport for test_old_server that reports it's smart server
382
protocol version as version one.
388
def get_smart_client(self):
389
return OldSmartClient()
392
class TestBranchLastRevisionInfo(tests.TestCase):
394
def test_empty_branch(self):
395
# in an empty branch we decode the response properly
396
transport = MemoryTransport()
397
client = FakeClient(transport.base)
398
client.add_success_response('ok', '0', 'null:')
399
transport.mkdir('quack')
400
transport = transport.clone('quack')
401
# we do not want bzrdir to make any remote calls
402
bzrdir = RemoteBzrDir(transport, _client=False)
403
branch = RemoteBranch(bzrdir, None, _client=client)
404
result = branch.last_revision_info()
407
[('call', 'Branch.last_revision_info', ('quack/',))],
409
self.assertEqual((0, NULL_REVISION), result)
411
def test_non_empty_branch(self):
412
# in a non-empty branch we also decode the response properly
413
revid = u'\xc8'.encode('utf8')
414
transport = MemoryTransport()
415
client = FakeClient(transport.base)
416
client.add_success_response('ok', '2', revid)
417
transport.mkdir('kwaak')
418
transport = transport.clone('kwaak')
419
# we do not want bzrdir to make any remote calls
420
bzrdir = RemoteBzrDir(transport, _client=False)
421
branch = RemoteBranch(bzrdir, None, _client=client)
422
result = branch.last_revision_info()
425
[('call', 'Branch.last_revision_info', ('kwaak/',))],
427
self.assertEqual((2, revid), result)
430
class TestBranchSetLastRevision(tests.TestCase):
432
def test_set_empty(self):
433
# set_revision_history([]) is translated to calling
434
# Branch.set_last_revision(path, '') on the wire.
435
transport = MemoryTransport()
436
transport.mkdir('branch')
437
transport = transport.clone('branch')
439
client = FakeClient(transport.base)
441
client.add_success_response('ok', 'branch token', 'repo token')
443
client.add_success_response('ok')
445
client.add_success_response('ok')
446
bzrdir = RemoteBzrDir(transport, _client=False)
447
branch = RemoteBranch(bzrdir, None, _client=client)
448
# This is a hack to work around the problem that RemoteBranch currently
449
# unnecessarily invokes _ensure_real upon a call to lock_write.
450
branch._ensure_real = lambda: None
453
result = branch.set_revision_history([])
455
[('call', 'Branch.set_last_revision',
456
('branch/', 'branch token', 'repo token', 'null:'))],
459
self.assertEqual(None, result)
461
def test_set_nonempty(self):
462
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
463
# Branch.set_last_revision(path, rev-idN) on the wire.
464
transport = MemoryTransport()
465
transport.mkdir('branch')
466
transport = transport.clone('branch')
468
client = FakeClient(transport.base)
470
client.add_success_response('ok', 'branch token', 'repo token')
472
client.add_success_response('ok')
474
client.add_success_response('ok')
475
bzrdir = RemoteBzrDir(transport, _client=False)
476
branch = RemoteBranch(bzrdir, None, _client=client)
477
# This is a hack to work around the problem that RemoteBranch currently
478
# unnecessarily invokes _ensure_real upon a call to lock_write.
479
branch._ensure_real = lambda: None
480
# Lock the branch, reset the record of remote calls.
484
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
486
[('call', 'Branch.set_last_revision',
487
('branch/', 'branch token', 'repo token', 'rev-id2'))],
490
self.assertEqual(None, result)
492
def test_no_such_revision(self):
493
transport = MemoryTransport()
494
transport.mkdir('branch')
495
transport = transport.clone('branch')
496
# A response of 'NoSuchRevision' is translated into an exception.
497
client = FakeClient(transport.base)
499
client.add_success_response('ok', 'branch token', 'repo token')
501
client.add_error_response('NoSuchRevision', 'rev-id')
503
client.add_success_response('ok')
505
bzrdir = RemoteBzrDir(transport, _client=False)
506
branch = RemoteBranch(bzrdir, None, _client=client)
507
branch._ensure_real = lambda: None
512
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
516
class TestBranchSetLastRevisionInfo(tests.TestCase):
518
def test_set_last_revision_info(self):
519
# set_last_revision_info(num, 'rev-id') is translated to calling
520
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
521
transport = MemoryTransport()
522
transport.mkdir('branch')
523
transport = transport.clone('branch')
524
client = FakeClient(transport.base)
526
client.add_success_response('ok', 'branch token', 'repo token')
528
client.add_success_response('ok')
530
client.add_success_response('ok')
532
bzrdir = RemoteBzrDir(transport, _client=False)
533
branch = RemoteBranch(bzrdir, None, _client=client)
534
# This is a hack to work around the problem that RemoteBranch currently
535
# unnecessarily invokes _ensure_real upon a call to lock_write.
536
branch._ensure_real = lambda: None
537
# Lock the branch, reset the record of remote calls.
540
result = branch.set_last_revision_info(1234, 'a-revision-id')
542
[('call', 'Branch.set_last_revision_info',
543
('branch/', 'branch token', 'repo token',
544
'1234', 'a-revision-id'))],
546
self.assertEqual(None, result)
548
def test_no_such_revision(self):
549
# A response of 'NoSuchRevision' is translated into an exception.
550
transport = MemoryTransport()
551
transport.mkdir('branch')
552
transport = transport.clone('branch')
553
client = FakeClient(transport.base)
555
client.add_success_response('ok', 'branch token', 'repo token')
557
client.add_error_response('NoSuchRevision', 'revid')
559
client.add_success_response('ok')
561
bzrdir = RemoteBzrDir(transport, _client=False)
562
branch = RemoteBranch(bzrdir, None, _client=client)
563
# This is a hack to work around the problem that RemoteBranch currently
564
# unnecessarily invokes _ensure_real upon a call to lock_write.
565
branch._ensure_real = lambda: None
566
# Lock the branch, reset the record of remote calls.
571
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
574
def lock_remote_branch(self, branch):
575
"""Trick a RemoteBranch into thinking it is locked."""
576
branch._lock_mode = 'w'
577
branch._lock_count = 2
578
branch._lock_token = 'branch token'
579
branch._repo_lock_token = 'repo token'
581
def test_backwards_compatibility(self):
582
"""If the server does not support the Branch.set_last_revision_info
583
verb (which is new in 1.4), then the client falls back to VFS methods.
585
# This test is a little messy. Unlike most tests in this file, it
586
# doesn't purely test what a Remote* object sends over the wire, and
587
# how it reacts to responses from the wire. It instead relies partly
588
# on asserting that the RemoteBranch will call
589
# self._real_branch.set_last_revision_info(...).
591
# First, set up our RemoteBranch with a FakeClient that raises
592
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
593
transport = MemoryTransport()
594
transport.mkdir('branch')
595
transport = transport.clone('branch')
596
client = FakeClient(transport.base)
597
client.add_unknown_method_response('Branch.set_last_revision_info')
598
bzrdir = RemoteBzrDir(transport, _client=False)
599
branch = RemoteBranch(bzrdir, None, _client=client)
600
class StubRealBranch(object):
603
def set_last_revision_info(self, revno, revision_id):
605
('set_last_revision_info', revno, revision_id))
606
real_branch = StubRealBranch()
607
branch._real_branch = real_branch
608
self.lock_remote_branch(branch)
610
# Call set_last_revision_info, and verify it behaved as expected.
611
result = branch.set_last_revision_info(1234, 'a-revision-id')
613
[('call', 'Branch.set_last_revision_info',
614
('branch/', 'branch token', 'repo token',
615
'1234', 'a-revision-id')),],
618
[('set_last_revision_info', 1234, 'a-revision-id')],
621
def test_unexpected_error(self):
622
# A response of 'NoSuchRevision' is translated into an exception.
623
transport = MemoryTransport()
624
transport.mkdir('branch')
625
transport = transport.clone('branch')
626
client = FakeClient(transport.base)
628
client.add_success_response('ok', 'branch token', 'repo token')
630
client.add_error_response('UnexpectedError')
632
client.add_success_response('ok')
634
bzrdir = RemoteBzrDir(transport, _client=False)
635
branch = RemoteBranch(bzrdir, None, _client=client)
636
# This is a hack to work around the problem that RemoteBranch currently
637
# unnecessarily invokes _ensure_real upon a call to lock_write.
638
branch._ensure_real = lambda: None
639
# Lock the branch, reset the record of remote calls.
643
err = self.assertRaises(
644
errors.ErrorFromSmartServer,
645
branch.set_last_revision_info, 123, 'revid')
646
self.assertEqual(('UnexpectedError',), err.error_tuple)
650
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
651
"""Getting the branch configuration should use an abstract method not vfs.
654
def test_get_branch_conf(self):
655
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
656
# We should see that branch.get_config() does a single rpc to get the
657
# remote configuration file, abstracting away where that is stored on
658
# the server. However at the moment it always falls back to using the
659
# vfs, and this would need some changes in config.py.
661
# in an empty branch we decode the response properly
662
client = FakeClient(self.get_url())
663
client.add_success_response_with_body('# config file body', 'ok')
664
# we need to make a real branch because the remote_branch.control_files
665
# will trigger _ensure_real.
666
branch = self.make_branch('quack')
667
transport = branch.bzrdir.root_transport
668
# we do not want bzrdir to make any remote calls
669
bzrdir = RemoteBzrDir(transport, _client=False)
670
branch = RemoteBranch(bzrdir, None, _client=client)
671
config = branch.get_config()
673
[('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
677
class TestBranchLockWrite(tests.TestCase):
679
def test_lock_write_unlockable(self):
680
transport = MemoryTransport()
681
client = FakeClient(transport.base)
682
client.add_error_response('UnlockableTransport')
683
transport.mkdir('quack')
684
transport = transport.clone('quack')
685
# we do not want bzrdir to make any remote calls
686
bzrdir = RemoteBzrDir(transport, _client=False)
687
branch = RemoteBranch(bzrdir, None, _client=client)
688
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
690
[('call', 'Branch.lock_write', ('quack/', '', ''))],
694
class TestTransportIsReadonly(tests.TestCase):
697
client = FakeClient()
698
client.add_success_response('yes')
699
transport = RemoteTransport('bzr://example.com/', medium=False,
701
self.assertEqual(True, transport.is_readonly())
703
[('call', 'Transport.is_readonly', ())],
706
def test_false(self):
707
client = FakeClient()
708
client.add_success_response('no')
709
transport = RemoteTransport('bzr://example.com/', medium=False,
711
self.assertEqual(False, transport.is_readonly())
713
[('call', 'Transport.is_readonly', ())],
716
def test_error_from_old_server(self):
717
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
719
Clients should treat it as a "no" response, because is_readonly is only
720
advisory anyway (a transport could be read-write, but then the
721
underlying filesystem could be readonly anyway).
723
client = FakeClient()
724
client.add_unknown_method_response('Transport.is_readonly')
725
transport = RemoteTransport('bzr://example.com/', medium=False,
727
self.assertEqual(False, transport.is_readonly())
729
[('call', 'Transport.is_readonly', ())],
733
class TestRemoteRepository(tests.TestCase):
734
"""Base for testing RemoteRepository protocol usage.
736
These tests contain frozen requests and responses. We want any changes to
737
what is sent or expected to be require a thoughtful update to these tests
738
because they might break compatibility with different-versioned servers.
741
def setup_fake_client_and_repository(self, transport_path):
742
"""Create the fake client and repository for testing with.
744
There's no real server here; we just have canned responses sent
747
:param transport_path: Path below the root of the MemoryTransport
748
where the repository will be created.
750
transport = MemoryTransport()
751
transport.mkdir(transport_path)
752
client = FakeClient(transport.base)
753
transport = transport.clone(transport_path)
754
# we do not want bzrdir to make any remote calls
755
bzrdir = RemoteBzrDir(transport, _client=False)
756
repo = RemoteRepository(bzrdir, None, _client=client)
760
class TestRepositoryGatherStats(TestRemoteRepository):
762
def test_revid_none(self):
763
# ('ok',), body with revisions and size
764
transport_path = 'quack'
765
repo, client = self.setup_fake_client_and_repository(transport_path)
766
client.add_success_response_with_body(
767
'revisions: 2\nsize: 18\n', 'ok')
768
result = repo.gather_stats(None)
770
[('call_expecting_body', 'Repository.gather_stats',
771
('quack/','','no'))],
773
self.assertEqual({'revisions': 2, 'size': 18}, result)
775
def test_revid_no_committers(self):
776
# ('ok',), body without committers
777
body = ('firstrev: 123456.300 3600\n'
778
'latestrev: 654231.400 0\n'
781
transport_path = 'quick'
782
revid = u'\xc8'.encode('utf8')
783
repo, client = self.setup_fake_client_and_repository(transport_path)
784
client.add_success_response_with_body(body, 'ok')
785
result = repo.gather_stats(revid)
787
[('call_expecting_body', 'Repository.gather_stats',
788
('quick/', revid, 'no'))],
790
self.assertEqual({'revisions': 2, 'size': 18,
791
'firstrev': (123456.300, 3600),
792
'latestrev': (654231.400, 0),},
795
def test_revid_with_committers(self):
796
# ('ok',), body with committers
797
body = ('committers: 128\n'
798
'firstrev: 123456.300 3600\n'
799
'latestrev: 654231.400 0\n'
802
transport_path = 'buick'
803
revid = u'\xc8'.encode('utf8')
804
repo, client = self.setup_fake_client_and_repository(transport_path)
805
client.add_success_response_with_body(body, 'ok')
806
result = repo.gather_stats(revid, True)
808
[('call_expecting_body', 'Repository.gather_stats',
809
('buick/', revid, 'yes'))],
811
self.assertEqual({'revisions': 2, 'size': 18,
813
'firstrev': (123456.300, 3600),
814
'latestrev': (654231.400, 0),},
818
class TestRepositoryGetGraph(TestRemoteRepository):
820
def test_get_graph(self):
821
# get_graph returns a graph with the repository as the
823
transport_path = 'quack'
824
repo, client = self.setup_fake_client_and_repository(transport_path)
825
graph = repo.get_graph()
826
self.assertEqual(graph._parents_provider, repo)
829
class TestRepositoryGetParentMap(TestRemoteRepository):
831
def test_get_parent_map_caching(self):
832
# get_parent_map returns from cache until unlock()
833
# setup a reponse with two revisions
834
r1 = u'\u0e33'.encode('utf8')
835
r2 = u'\u0dab'.encode('utf8')
836
lines = [' '.join([r2, r1]), r1]
837
encoded_body = bz2.compress('\n'.join(lines))
839
transport_path = 'quack'
840
repo, client = self.setup_fake_client_and_repository(transport_path)
841
client.add_success_response_with_body(encoded_body, 'ok')
842
client.add_success_response_with_body(encoded_body, 'ok')
844
graph = repo.get_graph()
845
parents = graph.get_parent_map([r2])
846
self.assertEqual({r2: (r1,)}, parents)
847
# locking and unlocking deeper should not reset
850
parents = graph.get_parent_map([r1])
851
self.assertEqual({r1: (NULL_REVISION,)}, parents)
853
[('call_with_body_bytes_expecting_body',
854
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
857
# now we call again, and it should use the second response.
859
graph = repo.get_graph()
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'),
865
('call_with_body_bytes_expecting_body',
866
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
871
def test_get_parent_map_reconnects_if_unknown_method(self):
872
transport_path = 'quack'
873
repo, client = self.setup_fake_client_and_repository(transport_path)
874
client.add_unknown_method_response('Repository,get_parent_map')
875
client.add_success_response_with_body('', 'ok')
876
self.assertTrue(client._medium._remote_is_at_least_1_2)
877
rev_id = 'revision-id'
878
expected_deprecations = [
879
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
881
parents = self.callDeprecated(
882
expected_deprecations, repo.get_parent_map, [rev_id])
884
[('call_with_body_bytes_expecting_body',
885
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
886
('disconnect medium',),
887
('call_expecting_body', 'Repository.get_revision_graph',
890
# The medium is now marked as being connected to an older server
891
self.assertFalse(client._medium._remote_is_at_least_1_2)
893
def test_get_parent_map_fallback_parentless_node(self):
894
"""get_parent_map falls back to get_revision_graph on old servers. The
895
results from get_revision_graph are tweaked to match the get_parent_map
898
Specifically, a {key: ()} result from get_revision_graph means "no
899
parents" for that key, which in get_parent_map results should be
900
represented as {key: ('null:',)}.
902
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
904
rev_id = 'revision-id'
905
transport_path = 'quack'
906
repo, client = self.setup_fake_client_and_repository(transport_path)
907
client.add_success_response_with_body(rev_id, 'ok')
908
client._medium._remote_is_at_least_1_2 = False
909
expected_deprecations = [
910
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
912
parents = self.callDeprecated(
913
expected_deprecations, repo.get_parent_map, [rev_id])
915
[('call_expecting_body', 'Repository.get_revision_graph',
918
self.assertEqual({rev_id: ('null:',)}, parents)
920
def test_get_parent_map_unexpected_response(self):
921
repo, client = self.setup_fake_client_and_repository('path')
922
client.add_success_response('something unexpected!')
924
errors.UnexpectedSmartServerResponse,
925
repo.get_parent_map, ['a-revision-id'])
928
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
930
def test_null_revision(self):
931
# a null revision has the predictable result {}, we should have no wire
932
# traffic when calling it with this argument
933
transport_path = 'empty'
934
repo, client = self.setup_fake_client_and_repository(transport_path)
935
client.add_success_response('notused')
936
result = self.applyDeprecated(one_four, repo.get_revision_graph,
938
self.assertEqual([], client._calls)
939
self.assertEqual({}, result)
941
def test_none_revision(self):
942
# with none we want the entire graph
943
r1 = u'\u0e33'.encode('utf8')
944
r2 = u'\u0dab'.encode('utf8')
945
lines = [' '.join([r2, r1]), r1]
946
encoded_body = '\n'.join(lines)
948
transport_path = 'sinhala'
949
repo, client = self.setup_fake_client_and_repository(transport_path)
950
client.add_success_response_with_body(encoded_body, 'ok')
951
result = self.applyDeprecated(one_four, repo.get_revision_graph)
953
[('call_expecting_body', 'Repository.get_revision_graph',
956
self.assertEqual({r1: (), r2: (r1, )}, result)
958
def test_specific_revision(self):
959
# with a specific revision we want the graph for that
960
# with none we want the entire graph
961
r11 = u'\u0e33'.encode('utf8')
962
r12 = u'\xc9'.encode('utf8')
963
r2 = u'\u0dab'.encode('utf8')
964
lines = [' '.join([r2, r11, r12]), r11, r12]
965
encoded_body = '\n'.join(lines)
967
transport_path = 'sinhala'
968
repo, client = self.setup_fake_client_and_repository(transport_path)
969
client.add_success_response_with_body(encoded_body, 'ok')
970
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
972
[('call_expecting_body', 'Repository.get_revision_graph',
975
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
977
def test_no_such_revision(self):
979
transport_path = 'sinhala'
980
repo, client = self.setup_fake_client_and_repository(transport_path)
981
client.add_error_response('nosuchrevision', revid)
982
# also check that the right revision is reported in the error
983
self.assertRaises(errors.NoSuchRevision,
984
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
986
[('call_expecting_body', 'Repository.get_revision_graph',
987
('sinhala/', revid))],
990
def test_unexpected_error(self):
992
transport_path = 'sinhala'
993
repo, client = self.setup_fake_client_and_repository(transport_path)
994
client.add_error_response('AnUnexpectedError')
995
e = self.assertRaises(errors.ErrorFromSmartServer,
996
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
997
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
1000
class TestRepositoryIsShared(TestRemoteRepository):
1002
def test_is_shared(self):
1003
# ('yes', ) for Repository.is_shared -> 'True'.
1004
transport_path = 'quack'
1005
repo, client = self.setup_fake_client_and_repository(transport_path)
1006
client.add_success_response('yes')
1007
result = repo.is_shared()
1009
[('call', 'Repository.is_shared', ('quack/',))],
1011
self.assertEqual(True, result)
1013
def test_is_not_shared(self):
1014
# ('no', ) for Repository.is_shared -> 'False'.
1015
transport_path = 'qwack'
1016
repo, client = self.setup_fake_client_and_repository(transport_path)
1017
client.add_success_response('no')
1018
result = repo.is_shared()
1020
[('call', 'Repository.is_shared', ('qwack/',))],
1022
self.assertEqual(False, result)
1025
class TestRepositoryLockWrite(TestRemoteRepository):
1027
def test_lock_write(self):
1028
transport_path = 'quack'
1029
repo, client = self.setup_fake_client_and_repository(transport_path)
1030
client.add_success_response('ok', 'a token')
1031
result = repo.lock_write()
1033
[('call', 'Repository.lock_write', ('quack/', ''))],
1035
self.assertEqual('a token', result)
1037
def test_lock_write_already_locked(self):
1038
transport_path = 'quack'
1039
repo, client = self.setup_fake_client_and_repository(transport_path)
1040
client.add_error_response('LockContention')
1041
self.assertRaises(errors.LockContention, repo.lock_write)
1043
[('call', 'Repository.lock_write', ('quack/', ''))],
1046
def test_lock_write_unlockable(self):
1047
transport_path = 'quack'
1048
repo, client = self.setup_fake_client_and_repository(transport_path)
1049
client.add_error_response('UnlockableTransport')
1050
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1052
[('call', 'Repository.lock_write', ('quack/', ''))],
1056
class TestRepositoryUnlock(TestRemoteRepository):
1058
def test_unlock(self):
1059
transport_path = 'quack'
1060
repo, client = self.setup_fake_client_and_repository(transport_path)
1061
client.add_success_response('ok', 'a token')
1062
client.add_success_response('ok')
1066
[('call', 'Repository.lock_write', ('quack/', '')),
1067
('call', 'Repository.unlock', ('quack/', 'a token'))],
1070
def test_unlock_wrong_token(self):
1071
# If somehow the token is wrong, unlock will raise TokenMismatch.
1072
transport_path = 'quack'
1073
repo, client = self.setup_fake_client_and_repository(transport_path)
1074
client.add_success_response('ok', 'a token')
1075
client.add_error_response('TokenMismatch')
1077
self.assertRaises(errors.TokenMismatch, repo.unlock)
1080
class TestRepositoryHasRevision(TestRemoteRepository):
1082
def test_none(self):
1083
# repo.has_revision(None) should not cause any traffic.
1084
transport_path = 'quack'
1085
repo, client = self.setup_fake_client_and_repository(transport_path)
1087
# The null revision is always there, so has_revision(None) == True.
1088
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1090
# The remote repo shouldn't be accessed.
1091
self.assertEqual([], client._calls)
1094
class TestRepositoryTarball(TestRemoteRepository):
1096
# This is a canned tarball reponse we can validate against
1098
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1099
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1100
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1101
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1102
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1103
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1104
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1105
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1106
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1107
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1108
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1109
'nWQ7QH/F3JFOFCQ0aSPfA='
1112
def test_repository_tarball(self):
1113
# Test that Repository.tarball generates the right operations
1114
transport_path = 'repo'
1115
expected_calls = [('call_expecting_body', 'Repository.tarball',
1116
('repo/', 'bz2',),),
1118
repo, client = self.setup_fake_client_and_repository(transport_path)
1119
client.add_success_response_with_body(self.tarball_content, 'ok')
1120
# Now actually ask for the tarball
1121
tarball_file = repo._get_tarball('bz2')
1123
self.assertEqual(expected_calls, client._calls)
1124
self.assertEqual(self.tarball_content, tarball_file.read())
1126
tarball_file.close()
1129
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1130
"""RemoteRepository.copy_content_into optimizations"""
1132
def test_copy_content_remote_to_local(self):
1133
self.transport_server = server.SmartTCPServer_for_testing
1134
src_repo = self.make_repository('repo1')
1135
src_repo = repository.Repository.open(self.get_url('repo1'))
1136
# At the moment the tarball-based copy_content_into can't write back
1137
# into a smart server. It would be good if it could upload the
1138
# tarball; once that works we'd have to create repositories of
1139
# different formats. -- mbp 20070410
1140
dest_url = self.get_vfs_only_url('repo2')
1141
dest_bzrdir = BzrDir.create(dest_url)
1142
dest_repo = dest_bzrdir.create_repository()
1143
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1144
self.assertTrue(isinstance(src_repo, RemoteRepository))
1145
src_repo.copy_content_into(dest_repo)
1148
class TestRepositoryStreamKnitData(TestRemoteRepository):
1150
def make_pack_file(self, records):
1151
pack_file = StringIO()
1152
pack_writer = pack.ContainerWriter(pack_file.write)
1154
for bytes, names in records:
1155
pack_writer.add_bytes_record(bytes, names)
1160
def make_pack_stream(self, records):
1161
pack_serialiser = pack.ContainerSerialiser()
1162
yield pack_serialiser.begin()
1163
for bytes, names in records:
1164
yield pack_serialiser.bytes_record(bytes, names)
1165
yield pack_serialiser.end()
1167
def test_bad_pack_from_server(self):
1168
"""A response with invalid data (e.g. it has a record with multiple
1169
names) triggers an exception.
1171
Not all possible errors will be caught at this stage, but obviously
1172
malformed data should be.
1174
record = ('bytes', [('name1',), ('name2',)])
1175
pack_stream = self.make_pack_stream([record])
1176
transport_path = 'quack'
1177
repo, client = self.setup_fake_client_and_repository(transport_path)
1178
client.add_success_response_with_body(pack_stream, 'ok')
1179
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1180
stream = repo.get_data_stream_for_search(search)
1181
self.assertRaises(errors.SmartProtocolError, list, stream)
1183
def test_backwards_compatibility(self):
1184
"""If the server doesn't recognise this request, fallback to VFS."""
1185
repo, client = self.setup_fake_client_and_repository('path')
1186
client.add_unknown_method_response(
1187
'Repository.stream_revisions_chunked')
1188
self.mock_called = False
1189
repo._real_repository = MockRealRepository(self)
1190
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1191
repo.get_data_stream_for_search(search)
1192
self.assertTrue(self.mock_called)
1193
self.failIf(client.expecting_body,
1194
"The protocol has been left in an unclean state that will cause "
1195
"TooManyConcurrentRequests errors.")
1198
class MockRealRepository(object):
1199
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
1201
def __init__(self, test):
1204
def get_data_stream_for_search(self, search):
1205
self.test.assertEqual(set(['revid']), search.get_keys())
1206
self.test.mock_called = True