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
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, responses, 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
144
self.responses = responses
146
self.expecting_body = False
147
_SmartClient.__init__(self, FakeMedium(self._calls), fake_medium_base)
149
def _get_next_response(self):
150
response_tuple = self.responses.pop(0)
151
if response_tuple[0][0] == 'unknown verb':
152
raise errors.UnknownSmartMethod(response_tuple[0][1])
153
return response_tuple
155
def call(self, method, *args):
156
self._calls.append(('call', method, args))
157
return self._get_next_response()[0]
159
def call_expecting_body(self, method, *args):
160
self._calls.append(('call_expecting_body', method, args))
161
result = self._get_next_response()
162
self.expecting_body = True
163
return result[0], FakeProtocol(result[1], self)
165
def call_with_body_bytes_expecting_body(self, method, args, body):
166
self._calls.append(('call_with_body_bytes_expecting_body', method,
168
result = self._get_next_response()
169
self.expecting_body = True
170
return result[0], FakeProtocol(result[1], self)
173
class FakeMedium(object):
175
def __init__(self, client_calls):
176
self._remote_is_at_least_1_2 = True
177
self._client_calls = client_calls
179
def disconnect(self):
180
self._client_calls.append(('disconnect medium',))
183
class TestVfsHas(tests.TestCase):
185
def test_unicode_path(self):
186
client = FakeClient([(('yes',), )], '/')
187
transport = RemoteTransport('bzr://localhost/', _client=client)
188
filename = u'/hell\u00d8'.encode('utf8')
189
result = transport.has(filename)
191
[('call', 'has', (filename,))],
193
self.assertTrue(result)
196
class Test_SmartClient_remote_path_from_transport(tests.TestCase):
197
"""Tests for the behaviour of _SmartClient.remote_path_from_transport."""
199
def assertRemotePath(self, expected, client_base, transport_base):
200
"""Assert that the result of _SmartClient.remote_path_from_transport
201
is the expected value for a given client_base and transport_base.
203
dummy_medium = 'dummy medium'
204
client = _SmartClient(dummy_medium, client_base)
205
transport = get_transport(transport_base)
206
result = client.remote_path_from_transport(transport)
207
self.assertEqual(expected, result)
209
def test_remote_path_from_transport(self):
210
"""_SmartClient.remote_path_from_transport calculates a URL for the
211
given transport relative to the root of the client base URL.
213
self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
214
self.assertRemotePath(
215
'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
217
def test_remote_path_from_transport_http(self):
218
"""Remote paths for HTTP transports are calculated differently to other
219
transports. They are just relative to the client base, not the root
220
directory of the host.
222
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
223
self.assertRemotePath(
224
'../xyz/', scheme + '//host/path', scheme + '//host/xyz')
225
self.assertRemotePath(
226
'xyz/', scheme + '//host/path', scheme + '//host/path/xyz')
229
class TestBzrDirOpenBranch(tests.TestCase):
231
def test_branch_present(self):
232
transport = MemoryTransport()
233
transport.mkdir('quack')
234
transport = transport.clone('quack')
235
client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
237
bzrdir = RemoteBzrDir(transport, _client=client)
238
result = bzrdir.open_branch()
240
[('call', 'BzrDir.open_branch', ('quack/',)),
241
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
243
self.assertIsInstance(result, RemoteBranch)
244
self.assertEqual(bzrdir, result.bzrdir)
246
def test_branch_missing(self):
247
transport = MemoryTransport()
248
transport.mkdir('quack')
249
transport = transport.clone('quack')
250
client = FakeClient([(('nobranch',), )], transport.base)
251
bzrdir = RemoteBzrDir(transport, _client=client)
252
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
254
[('call', 'BzrDir.open_branch', ('quack/',))],
257
def test__get_tree_branch(self):
258
# _get_tree_branch is a form of open_branch, but it should only ask for
259
# branch opening, not any other network requests.
262
calls.append("Called")
264
transport = MemoryTransport()
265
# no requests on the network - catches other api calls being made.
266
client = FakeClient([], transport.base)
267
bzrdir = RemoteBzrDir(transport, _client=client)
268
# patch the open_branch call to record that it was called.
269
bzrdir.open_branch = open_branch
270
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
271
self.assertEqual(["Called"], calls)
272
self.assertEqual([], client._calls)
274
def test_url_quoting_of_path(self):
275
# Relpaths on the wire should not be URL-escaped. So "~" should be
276
# transmitted as "~", not "%7E".
277
transport = RemoteTransport('bzr://localhost/~hello/')
278
client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
280
bzrdir = RemoteBzrDir(transport, _client=client)
281
result = bzrdir.open_branch()
283
[('call', 'BzrDir.open_branch', ('~hello/',)),
284
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
287
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
288
transport = MemoryTransport()
289
transport.mkdir('quack')
290
transport = transport.clone('quack')
292
rich_response = 'yes'
296
subtree_response = 'yes'
298
subtree_response = 'no'
300
[(('ok', '', rich_response, subtree_response, external_lookup), ),],
302
bzrdir = RemoteBzrDir(transport, _client=client)
303
result = bzrdir.open_repository()
305
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
307
self.assertIsInstance(result, RemoteRepository)
308
self.assertEqual(bzrdir, result.bzrdir)
309
self.assertEqual(rich_root, result._format.rich_root_data)
310
self.assertEqual(subtrees, result._format.supports_tree_reference)
312
def test_open_repository_sets_format_attributes(self):
313
self.check_open_repository(True, True)
314
self.check_open_repository(False, True)
315
self.check_open_repository(True, False)
316
self.check_open_repository(False, False)
317
self.check_open_repository(False, False, 'yes')
319
def test_old_server(self):
320
"""RemoteBzrDirFormat should fail to probe if the server version is too
323
self.assertRaises(errors.NotBranchError,
324
RemoteBzrDirFormat.probe_transport, OldServerTransport())
327
class TestBzrDirOpenRepository(tests.TestCase):
329
def test_backwards_compat_1_2(self):
330
transport = MemoryTransport()
331
transport.mkdir('quack')
332
transport = transport.clone('quack')
333
client = FakeClient([
334
(('unknown verb', 'RemoteRepository.find_repositoryV2'), ''),
335
(('ok', '', 'no', 'no'), ''),],
337
bzrdir = RemoteBzrDir(transport, _client=client)
338
repo = bzrdir.open_repository()
340
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
341
('call', 'BzrDir.find_repository', ('quack/',))],
345
class OldSmartClient(object):
346
"""A fake smart client for test_old_version that just returns a version one
347
response to the 'hello' (query version) command.
350
def get_request(self):
351
input_file = StringIO('ok\x011\n')
352
output_file = StringIO()
353
client_medium = medium.SmartSimplePipesClientMedium(
354
input_file, output_file)
355
return medium.SmartClientStreamMediumRequest(client_medium)
357
def protocol_version(self):
361
class OldServerTransport(object):
362
"""A fake transport for test_old_server that reports it's smart server
363
protocol version as version one.
369
def get_smart_client(self):
370
return OldSmartClient()
373
class TestBranchLastRevisionInfo(tests.TestCase):
375
def test_empty_branch(self):
376
# in an empty branch we decode the response properly
377
transport = MemoryTransport()
378
client = FakeClient([(('ok', '0', 'null:'), )], transport.base)
379
transport.mkdir('quack')
380
transport = transport.clone('quack')
381
# we do not want bzrdir to make any remote calls
382
bzrdir = RemoteBzrDir(transport, _client=False)
383
branch = RemoteBranch(bzrdir, None, _client=client)
384
result = branch.last_revision_info()
387
[('call', 'Branch.last_revision_info', ('quack/',))],
389
self.assertEqual((0, NULL_REVISION), result)
391
def test_non_empty_branch(self):
392
# in a non-empty branch we also decode the response properly
393
revid = u'\xc8'.encode('utf8')
394
transport = MemoryTransport()
395
client = FakeClient([(('ok', '2', revid), )], transport.base)
396
transport.mkdir('kwaak')
397
transport = transport.clone('kwaak')
398
# we do not want bzrdir to make any remote calls
399
bzrdir = RemoteBzrDir(transport, _client=False)
400
branch = RemoteBranch(bzrdir, None, _client=client)
401
result = branch.last_revision_info()
404
[('call', 'Branch.last_revision_info', ('kwaak/',))],
406
self.assertEqual((2, revid), result)
409
class TestBranchSetLastRevision(tests.TestCase):
411
def test_set_empty(self):
412
# set_revision_history([]) is translated to calling
413
# Branch.set_last_revision(path, '') on the wire.
414
transport = MemoryTransport()
415
transport.mkdir('branch')
416
transport = transport.clone('branch')
418
client = FakeClient([
420
(('ok', 'branch token', 'repo token'), ),
426
bzrdir = RemoteBzrDir(transport, _client=False)
427
branch = RemoteBranch(bzrdir, None, _client=client)
428
# This is a hack to work around the problem that RemoteBranch currently
429
# unnecessarily invokes _ensure_real upon a call to lock_write.
430
branch._ensure_real = lambda: None
433
result = branch.set_revision_history([])
435
[('call', 'Branch.set_last_revision',
436
('branch/', 'branch token', 'repo token', 'null:'))],
439
self.assertEqual(None, result)
441
def test_set_nonempty(self):
442
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
443
# Branch.set_last_revision(path, rev-idN) on the wire.
444
transport = MemoryTransport()
445
transport.mkdir('branch')
446
transport = transport.clone('branch')
448
client = FakeClient([
450
(('ok', 'branch token', 'repo token'), ),
456
bzrdir = RemoteBzrDir(transport, _client=False)
457
branch = RemoteBranch(bzrdir, None, _client=client)
458
# This is a hack to work around the problem that RemoteBranch currently
459
# unnecessarily invokes _ensure_real upon a call to lock_write.
460
branch._ensure_real = lambda: None
461
# Lock the branch, reset the record of remote calls.
465
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
467
[('call', 'Branch.set_last_revision',
468
('branch/', 'branch token', 'repo token', 'rev-id2'))],
471
self.assertEqual(None, result)
473
def test_no_such_revision(self):
474
# A response of 'NoSuchRevision' is translated into an exception.
475
client = FakeClient([
477
(('ok', 'branch token', 'repo token'), ),
479
(('NoSuchRevision', 'rev-id'), ),
482
transport = MemoryTransport()
483
transport.mkdir('branch')
484
transport = transport.clone('branch')
486
bzrdir = RemoteBzrDir(transport, _client=False)
487
branch = RemoteBranch(bzrdir, None, _client=client)
488
branch._ensure_real = lambda: None
493
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
497
class TestBranchSetLastRevisionInfo(tests.TestCase):
499
def test_set_last_revision_info(self):
500
# set_last_revision_info(num, 'rev-id') is translated to calling
501
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
502
transport = MemoryTransport()
503
transport.mkdir('branch')
504
transport = transport.clone('branch')
505
client = FakeClient([
507
(('ok', 'branch token', 'repo token'), ),
508
# set_last_revision_info
511
(('ok',), )], transport.base)
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.
521
result = branch.set_last_revision_info(1234, 'a-revision-id')
523
[('call', 'Branch.set_last_revision_info',
524
('branch/', 'branch token', 'repo token',
525
'1234', 'a-revision-id'))],
527
self.assertEqual(None, result)
529
def test_no_such_revision(self):
530
# A response of 'NoSuchRevision' is translated into an exception.
531
client = FakeClient([
533
(('ok', 'branch token', 'repo token'), ),
534
# set_last_revision_info
535
(('NoSuchRevision', 'revid'), ),
539
transport = MemoryTransport()
540
transport.mkdir('branch')
541
transport = transport.clone('branch')
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.
553
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
556
def lock_remote_branch(self, branch):
557
"""Trick a RemoteBranch into thinking it is locked."""
558
branch._lock_mode = 'w'
559
branch._lock_count = 2
560
branch._lock_token = 'branch token'
561
branch._repo_lock_token = 'repo token'
563
def test_backwards_compatibility(self):
564
"""If the server does not support the Branch.set_last_revision_info
565
verb (which is new in 1.4), then the client falls back to VFS methods.
567
# This test is a little messy. Unlike most tests in this file, it
568
# doesn't purely test what a Remote* object sends over the wire, and
569
# how it reacts to responses from the wire. It instead relies partly
570
# on asserting that the RemoteBranch will call
571
# self._real_branch.set_last_revision_info(...).
573
# First, set up our RemoteBranch with a FakeClient that raises
574
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
575
transport = MemoryTransport()
576
transport.mkdir('branch')
577
transport = transport.clone('branch')
579
[(('unknown verb', 'Branch.set_last_revision_info',), ),],
581
bzrdir = RemoteBzrDir(transport, _client=False)
582
branch = RemoteBranch(bzrdir, None, _client=client)
583
class StubRealBranch(object):
586
def set_last_revision_info(self, revno, revision_id):
588
('set_last_revision_info', revno, revision_id))
589
real_branch = StubRealBranch()
590
branch._real_branch = real_branch
591
self.lock_remote_branch(branch)
593
# Call set_last_revision_info, and verify it behaved as expected.
594
result = branch.set_last_revision_info(1234, 'a-revision-id')
596
[('call', 'Branch.set_last_revision_info',
597
('branch/', 'branch token', 'repo token',
598
'1234', 'a-revision-id')),],
601
[('set_last_revision_info', 1234, 'a-revision-id')],
605
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
606
"""Getting the branch configuration should use an abstract method not vfs.
609
def test_get_branch_conf(self):
610
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
611
# We should see that branch.get_config() does a single rpc to get the
612
# remote configuration file, abstracting away where that is stored on
613
# the server. However at the moment it always falls back to using the
614
# vfs, and this would need some changes in config.py.
616
# in an empty branch we decode the response properly
617
client = FakeClient([(('ok', ), '# config file body')], self.get_url())
618
# we need to make a real branch because the remote_branch.control_files
619
# will trigger _ensure_real.
620
branch = self.make_branch('quack')
621
transport = branch.bzrdir.root_transport
622
# we do not want bzrdir to make any remote calls
623
bzrdir = RemoteBzrDir(transport, _client=False)
624
branch = RemoteBranch(bzrdir, None, _client=client)
625
config = branch.get_config()
627
[('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
631
class TestBranchLockWrite(tests.TestCase):
633
def test_lock_write_unlockable(self):
634
transport = MemoryTransport()
635
client = FakeClient([(('UnlockableTransport', ), '')], transport.base)
636
transport.mkdir('quack')
637
transport = transport.clone('quack')
638
# we do not want bzrdir to make any remote calls
639
bzrdir = RemoteBzrDir(transport, _client=False)
640
branch = RemoteBranch(bzrdir, None, _client=client)
641
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
643
[('call', 'Branch.lock_write', ('quack/', '', ''))],
647
class TestTransportIsReadonly(tests.TestCase):
650
client = FakeClient([(('yes',), '')])
651
transport = RemoteTransport('bzr://example.com/', medium=False,
653
self.assertEqual(True, transport.is_readonly())
655
[('call', 'Transport.is_readonly', ())],
658
def test_false(self):
659
client = FakeClient([(('no',), '')])
660
transport = RemoteTransport('bzr://example.com/', medium=False,
662
self.assertEqual(False, transport.is_readonly())
664
[('call', 'Transport.is_readonly', ())],
667
def test_error_from_old_server(self):
668
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
670
Clients should treat it as a "no" response, because is_readonly is only
671
advisory anyway (a transport could be read-write, but then the
672
underlying filesystem could be readonly anyway).
674
client = FakeClient([(('unknown verb', 'Transport.is_readonly'), '')])
675
transport = RemoteTransport('bzr://example.com/', medium=False,
677
self.assertEqual(False, transport.is_readonly())
679
[('call', 'Transport.is_readonly', ())],
683
class TestRemoteRepository(tests.TestCase):
684
"""Base for testing RemoteRepository protocol usage.
686
These tests contain frozen requests and responses. We want any changes to
687
what is sent or expected to be require a thoughtful update to these tests
688
because they might break compatibility with different-versioned servers.
691
def setup_fake_client_and_repository(self, responses, transport_path):
692
"""Create the fake client and repository for testing with.
694
There's no real server here; we just have canned responses sent
697
:param transport_path: Path below the root of the MemoryTransport
698
where the repository will be created.
700
transport = MemoryTransport()
701
transport.mkdir(transport_path)
702
client = FakeClient(responses, transport.base)
703
transport = transport.clone(transport_path)
704
# we do not want bzrdir to make any remote calls
705
bzrdir = RemoteBzrDir(transport, _client=False)
706
repo = RemoteRepository(bzrdir, None, _client=client)
710
class TestRepositoryGatherStats(TestRemoteRepository):
712
def test_revid_none(self):
713
# ('ok',), body with revisions and size
714
responses = [(('ok', ), 'revisions: 2\nsize: 18\n')]
715
transport_path = 'quack'
716
repo, client = self.setup_fake_client_and_repository(
717
responses, transport_path)
718
result = repo.gather_stats(None)
720
[('call_expecting_body', 'Repository.gather_stats',
721
('quack/','','no'))],
723
self.assertEqual({'revisions': 2, 'size': 18}, result)
725
def test_revid_no_committers(self):
726
# ('ok',), body without committers
727
responses = [(('ok', ),
728
'firstrev: 123456.300 3600\n'
729
'latestrev: 654231.400 0\n'
732
transport_path = 'quick'
733
revid = u'\xc8'.encode('utf8')
734
repo, client = self.setup_fake_client_and_repository(
735
responses, transport_path)
736
result = repo.gather_stats(revid)
738
[('call_expecting_body', 'Repository.gather_stats',
739
('quick/', revid, 'no'))],
741
self.assertEqual({'revisions': 2, 'size': 18,
742
'firstrev': (123456.300, 3600),
743
'latestrev': (654231.400, 0),},
746
def test_revid_with_committers(self):
747
# ('ok',), body with committers
748
responses = [(('ok', ),
750
'firstrev: 123456.300 3600\n'
751
'latestrev: 654231.400 0\n'
754
transport_path = 'buick'
755
revid = u'\xc8'.encode('utf8')
756
repo, client = self.setup_fake_client_and_repository(
757
responses, transport_path)
758
result = repo.gather_stats(revid, True)
760
[('call_expecting_body', 'Repository.gather_stats',
761
('buick/', revid, 'yes'))],
763
self.assertEqual({'revisions': 2, 'size': 18,
765
'firstrev': (123456.300, 3600),
766
'latestrev': (654231.400, 0),},
770
class TestRepositoryGetGraph(TestRemoteRepository):
772
def test_get_graph(self):
773
# get_graph returns a graph with the repository as the
776
transport_path = 'quack'
777
repo, client = self.setup_fake_client_and_repository(
778
responses, transport_path)
779
graph = repo.get_graph()
780
self.assertEqual(graph._parents_provider, repo)
783
class TestRepositoryGetParentMap(TestRemoteRepository):
785
def test_get_parent_map_caching(self):
786
# get_parent_map returns from cache until unlock()
787
# setup a reponse with two revisions
788
r1 = u'\u0e33'.encode('utf8')
789
r2 = u'\u0dab'.encode('utf8')
790
lines = [' '.join([r2, r1]), r1]
791
encoded_body = bz2.compress('\n'.join(lines))
792
responses = [(('ok', ), encoded_body), (('ok', ), encoded_body)]
794
transport_path = 'quack'
795
repo, client = self.setup_fake_client_and_repository(
796
responses, transport_path)
798
graph = repo.get_graph()
799
parents = graph.get_parent_map([r2])
800
self.assertEqual({r2: (r1,)}, parents)
801
# locking and unlocking deeper should not reset
804
parents = graph.get_parent_map([r1])
805
self.assertEqual({r1: (NULL_REVISION,)}, parents)
807
[('call_with_body_bytes_expecting_body',
808
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
811
# now we call again, and it should use the second response.
813
graph = repo.get_graph()
814
parents = graph.get_parent_map([r1])
815
self.assertEqual({r1: (NULL_REVISION,)}, parents)
817
[('call_with_body_bytes_expecting_body',
818
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
819
('call_with_body_bytes_expecting_body',
820
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
825
def test_get_parent_map_reconnects_if_unknown_method(self):
827
(('unknown verb', 'Repository.get_parent_map'), ''),
829
transport_path = 'quack'
830
repo, client = self.setup_fake_client_and_repository(
831
responses, transport_path)
832
self.assertTrue(client._medium._remote_is_at_least_1_2)
833
rev_id = 'revision-id'
834
expected_deprecations = [
835
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
837
parents = self.callDeprecated(
838
expected_deprecations, repo.get_parent_map, [rev_id])
840
[('call_with_body_bytes_expecting_body',
841
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
842
('disconnect medium',),
843
('call_expecting_body', 'Repository.get_revision_graph',
846
# The medium is now marked as being connected to an older server
847
self.assertFalse(client._medium._remote_is_at_least_1_2)
849
def test_get_parent_map_fallback_parentless_node(self):
850
"""get_parent_map falls back to get_revision_graph on old servers. The
851
results from get_revision_graph are tweaked to match the get_parent_map
854
Specifically, a {key: ()} result from get_revision_graph means "no
855
parents" for that key, which in get_parent_map results should be
856
represented as {key: ('null:',)}.
858
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
860
rev_id = 'revision-id'
861
responses = [(('ok',), rev_id)]
862
transport_path = 'quack'
863
repo, client = self.setup_fake_client_and_repository(
864
responses, transport_path)
865
client._medium._remote_is_at_least_1_2 = False
866
expected_deprecations = [
867
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
869
parents = self.callDeprecated(
870
expected_deprecations, repo.get_parent_map, [rev_id])
872
[('call_expecting_body', 'Repository.get_revision_graph',
875
self.assertEqual({rev_id: ('null:',)}, parents)
877
def test_get_parent_map_unexpected_response(self):
879
(('something unexpected!',), '')]
880
repo, client = self.setup_fake_client_and_repository(responses, 'path')
882
errors.UnexpectedSmartServerResponse,
883
repo.get_parent_map, ['a-revision-id'])
886
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
888
def test_null_revision(self):
889
# a null revision has the predictable result {}, we should have no wire
890
# traffic when calling it with this argument
891
responses = [(('notused', ), '')]
892
transport_path = 'empty'
893
repo, client = self.setup_fake_client_and_repository(
894
responses, transport_path)
895
result = self.applyDeprecated(one_four, repo.get_revision_graph,
897
self.assertEqual([], client._calls)
898
self.assertEqual({}, result)
900
def test_none_revision(self):
901
# with none we want the entire graph
902
r1 = u'\u0e33'.encode('utf8')
903
r2 = u'\u0dab'.encode('utf8')
904
lines = [' '.join([r2, r1]), r1]
905
encoded_body = '\n'.join(lines)
907
responses = [(('ok', ), encoded_body)]
908
transport_path = 'sinhala'
909
repo, client = self.setup_fake_client_and_repository(
910
responses, transport_path)
911
result = self.applyDeprecated(one_four, repo.get_revision_graph)
913
[('call_expecting_body', 'Repository.get_revision_graph',
916
self.assertEqual({r1: (), r2: (r1, )}, result)
918
def test_specific_revision(self):
919
# with a specific revision we want the graph for that
920
# with none we want the entire graph
921
r11 = u'\u0e33'.encode('utf8')
922
r12 = u'\xc9'.encode('utf8')
923
r2 = u'\u0dab'.encode('utf8')
924
lines = [' '.join([r2, r11, r12]), r11, r12]
925
encoded_body = '\n'.join(lines)
927
responses = [(('ok', ), encoded_body)]
928
transport_path = 'sinhala'
929
repo, client = self.setup_fake_client_and_repository(
930
responses, transport_path)
931
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
933
[('call_expecting_body', 'Repository.get_revision_graph',
936
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
938
def test_no_such_revision(self):
940
responses = [(('nosuchrevision', revid), '')]
941
transport_path = 'sinhala'
942
repo, client = self.setup_fake_client_and_repository(
943
responses, transport_path)
944
# also check that the right revision is reported in the error
945
self.assertRaises(errors.NoSuchRevision,
946
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
948
[('call_expecting_body', 'Repository.get_revision_graph',
949
('sinhala/', revid))],
953
class TestRepositoryIsShared(TestRemoteRepository):
955
def test_is_shared(self):
956
# ('yes', ) for Repository.is_shared -> 'True'.
957
responses = [(('yes', ), )]
958
transport_path = 'quack'
959
repo, client = self.setup_fake_client_and_repository(
960
responses, transport_path)
961
result = repo.is_shared()
963
[('call', 'Repository.is_shared', ('quack/',))],
965
self.assertEqual(True, result)
967
def test_is_not_shared(self):
968
# ('no', ) for Repository.is_shared -> 'False'.
969
responses = [(('no', ), )]
970
transport_path = 'qwack'
971
repo, client = self.setup_fake_client_and_repository(
972
responses, transport_path)
973
result = repo.is_shared()
975
[('call', 'Repository.is_shared', ('qwack/',))],
977
self.assertEqual(False, result)
980
class TestRepositoryLockWrite(TestRemoteRepository):
982
def test_lock_write(self):
983
responses = [(('ok', 'a token'), '')]
984
transport_path = 'quack'
985
repo, client = self.setup_fake_client_and_repository(
986
responses, transport_path)
987
result = repo.lock_write()
989
[('call', 'Repository.lock_write', ('quack/', ''))],
991
self.assertEqual('a token', result)
993
def test_lock_write_already_locked(self):
994
responses = [(('LockContention', ), '')]
995
transport_path = 'quack'
996
repo, client = self.setup_fake_client_and_repository(
997
responses, transport_path)
998
self.assertRaises(errors.LockContention, repo.lock_write)
1000
[('call', 'Repository.lock_write', ('quack/', ''))],
1003
def test_lock_write_unlockable(self):
1004
responses = [(('UnlockableTransport', ), '')]
1005
transport_path = 'quack'
1006
repo, client = self.setup_fake_client_and_repository(
1007
responses, transport_path)
1008
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1010
[('call', 'Repository.lock_write', ('quack/', ''))],
1014
class TestRepositoryUnlock(TestRemoteRepository):
1016
def test_unlock(self):
1017
responses = [(('ok', 'a token'), ''),
1019
transport_path = 'quack'
1020
repo, client = self.setup_fake_client_and_repository(
1021
responses, transport_path)
1025
[('call', 'Repository.lock_write', ('quack/', '')),
1026
('call', 'Repository.unlock', ('quack/', 'a token'))],
1029
def test_unlock_wrong_token(self):
1030
# If somehow the token is wrong, unlock will raise TokenMismatch.
1031
responses = [(('ok', 'a token'), ''),
1032
(('TokenMismatch',), '')]
1033
transport_path = 'quack'
1034
repo, client = self.setup_fake_client_and_repository(
1035
responses, transport_path)
1037
self.assertRaises(errors.TokenMismatch, repo.unlock)
1040
class TestRepositoryHasRevision(TestRemoteRepository):
1042
def test_none(self):
1043
# repo.has_revision(None) should not cause any traffic.
1044
transport_path = 'quack'
1046
repo, client = self.setup_fake_client_and_repository(
1047
responses, transport_path)
1049
# The null revision is always there, so has_revision(None) == True.
1050
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1052
# The remote repo shouldn't be accessed.
1053
self.assertEqual([], client._calls)
1056
class TestRepositoryTarball(TestRemoteRepository):
1058
# This is a canned tarball reponse we can validate against
1060
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1061
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1062
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1063
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1064
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1065
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1066
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1067
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1068
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1069
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1070
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1071
'nWQ7QH/F3JFOFCQ0aSPfA='
1074
def test_repository_tarball(self):
1075
# Test that Repository.tarball generates the right operations
1076
transport_path = 'repo'
1077
expected_responses = [(('ok',), self.tarball_content),
1079
expected_calls = [('call_expecting_body', 'Repository.tarball',
1080
('repo/', 'bz2',),),
1082
remote_repo, client = self.setup_fake_client_and_repository(
1083
expected_responses, transport_path)
1084
# Now actually ask for the tarball
1085
tarball_file = remote_repo._get_tarball('bz2')
1087
self.assertEqual(expected_calls, client._calls)
1088
self.assertEqual(self.tarball_content, tarball_file.read())
1090
tarball_file.close()
1093
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1094
"""RemoteRepository.copy_content_into optimizations"""
1096
def test_copy_content_remote_to_local(self):
1097
self.transport_server = server.SmartTCPServer_for_testing
1098
src_repo = self.make_repository('repo1')
1099
src_repo = repository.Repository.open(self.get_url('repo1'))
1100
# At the moment the tarball-based copy_content_into can't write back
1101
# into a smart server. It would be good if it could upload the
1102
# tarball; once that works we'd have to create repositories of
1103
# different formats. -- mbp 20070410
1104
dest_url = self.get_vfs_only_url('repo2')
1105
dest_bzrdir = BzrDir.create(dest_url)
1106
dest_repo = dest_bzrdir.create_repository()
1107
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1108
self.assertTrue(isinstance(src_repo, RemoteRepository))
1109
src_repo.copy_content_into(dest_repo)
1112
class TestRepositoryStreamKnitData(TestRemoteRepository):
1114
def make_pack_file(self, records):
1115
pack_file = StringIO()
1116
pack_writer = pack.ContainerWriter(pack_file.write)
1118
for bytes, names in records:
1119
pack_writer.add_bytes_record(bytes, names)
1124
def make_pack_stream(self, records):
1125
pack_serialiser = pack.ContainerSerialiser()
1126
yield pack_serialiser.begin()
1127
for bytes, names in records:
1128
yield pack_serialiser.bytes_record(bytes, names)
1129
yield pack_serialiser.end()
1131
def test_bad_pack_from_server(self):
1132
"""A response with invalid data (e.g. it has a record with multiple
1133
names) triggers an exception.
1135
Not all possible errors will be caught at this stage, but obviously
1136
malformed data should be.
1138
record = ('bytes', [('name1',), ('name2',)])
1139
pack_stream = self.make_pack_stream([record])
1140
responses = [(('ok',), pack_stream), ]
1141
transport_path = 'quack'
1142
repo, client = self.setup_fake_client_and_repository(
1143
responses, transport_path)
1144
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1145
stream = repo.get_data_stream_for_search(search)
1146
self.assertRaises(errors.SmartProtocolError, list, stream)
1148
def test_backwards_compatibility(self):
1149
"""If the server doesn't recognise this request, fallback to VFS."""
1151
(('unknown verb', 'Repository.stream_revisions_chunked'), '')]
1152
repo, client = self.setup_fake_client_and_repository(
1154
self.mock_called = False
1155
repo._real_repository = MockRealRepository(self)
1156
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1157
repo.get_data_stream_for_search(search)
1158
self.assertTrue(self.mock_called)
1159
self.failIf(client.expecting_body,
1160
"The protocol has been left in an unclean state that will cause "
1161
"TooManyConcurrentRequests errors.")
1164
class MockRealRepository(object):
1165
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
1167
def __init__(self, test):
1170
def get_data_stream_for_search(self, search):
1171
self.test.assertEqual(set(['revid']), search.get_keys())
1172
self.test.mock_called = True