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, 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):
190
self._remote_is_at_least_1_2 = True
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_SmartClient_remote_path_from_transport(tests.TestCase):
212
"""Tests for the behaviour of _SmartClient.remote_path_from_transport."""
214
def assertRemotePath(self, expected, client_base, transport_base):
215
"""Assert that the result of _SmartClient.remote_path_from_transport
216
is the expected value for a given client_base and transport_base.
218
dummy_medium = 'dummy medium'
219
client = _SmartClient(dummy_medium, client_base)
220
transport = get_transport(transport_base)
221
result = client.remote_path_from_transport(transport)
222
self.assertEqual(expected, result)
224
def test_remote_path_from_transport(self):
225
"""_SmartClient.remote_path_from_transport calculates a URL for the
226
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 test_remote_path_from_transport_http(self):
233
"""Remote paths for HTTP transports are calculated differently to other
234
transports. They are just relative to the client base, not the root
235
directory of the host.
237
for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
238
self.assertRemotePath(
239
'../xyz/', scheme + '//host/path', scheme + '//host/xyz')
240
self.assertRemotePath(
241
'xyz/', scheme + '//host/path', scheme + '//host/path/xyz')
244
class TestBzrDirOpenBranch(tests.TestCase):
246
def test_branch_present(self):
247
transport = MemoryTransport()
248
transport.mkdir('quack')
249
transport = transport.clone('quack')
250
client = FakeClient(transport.base)
251
client.add_success_response('ok', '')
252
client.add_success_response('ok', '', 'no', 'no', 'no')
253
bzrdir = RemoteBzrDir(transport, _client=client)
254
result = bzrdir.open_branch()
256
[('call', 'BzrDir.open_branch', ('quack/',)),
257
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
259
self.assertIsInstance(result, RemoteBranch)
260
self.assertEqual(bzrdir, result.bzrdir)
262
def test_branch_missing(self):
263
transport = MemoryTransport()
264
transport.mkdir('quack')
265
transport = transport.clone('quack')
266
client = FakeClient(transport.base)
267
client.add_error_response('nobranch')
268
bzrdir = RemoteBzrDir(transport, _client=client)
269
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
271
[('call', 'BzrDir.open_branch', ('quack/',))],
274
def test__get_tree_branch(self):
275
# _get_tree_branch is a form of open_branch, but it should only ask for
276
# branch opening, not any other network requests.
279
calls.append("Called")
281
transport = MemoryTransport()
282
# no requests on the network - catches other api calls being made.
283
client = FakeClient(transport.base)
284
bzrdir = RemoteBzrDir(transport, _client=client)
285
# patch the open_branch call to record that it was called.
286
bzrdir.open_branch = open_branch
287
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
288
self.assertEqual(["Called"], calls)
289
self.assertEqual([], client._calls)
291
def test_url_quoting_of_path(self):
292
# Relpaths on the wire should not be URL-escaped. So "~" should be
293
# transmitted as "~", not "%7E".
294
transport = RemoteTransport('bzr://localhost/~hello/')
295
client = FakeClient(transport.base)
296
client.add_success_response('ok', '')
297
client.add_success_response('ok', '', 'no', 'no', 'no')
298
bzrdir = RemoteBzrDir(transport, _client=client)
299
result = bzrdir.open_branch()
301
[('call', 'BzrDir.open_branch', ('~hello/',)),
302
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
305
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
306
transport = MemoryTransport()
307
transport.mkdir('quack')
308
transport = transport.clone('quack')
310
rich_response = 'yes'
314
subtree_response = 'yes'
316
subtree_response = 'no'
317
client = FakeClient(transport.base)
318
client.add_success_response(
319
'ok', '', rich_response, subtree_response, external_lookup)
320
bzrdir = RemoteBzrDir(transport, _client=client)
321
result = bzrdir.open_repository()
323
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
325
self.assertIsInstance(result, RemoteRepository)
326
self.assertEqual(bzrdir, result.bzrdir)
327
self.assertEqual(rich_root, result._format.rich_root_data)
328
self.assertEqual(subtrees, result._format.supports_tree_reference)
330
def test_open_repository_sets_format_attributes(self):
331
self.check_open_repository(True, True)
332
self.check_open_repository(False, True)
333
self.check_open_repository(True, False)
334
self.check_open_repository(False, False)
335
self.check_open_repository(False, False, 'yes')
337
def test_old_server(self):
338
"""RemoteBzrDirFormat should fail to probe if the server version is too
341
self.assertRaises(errors.NotBranchError,
342
RemoteBzrDirFormat.probe_transport, OldServerTransport())
345
class TestBzrDirOpenRepository(tests.TestCase):
347
def test_backwards_compat_1_2(self):
348
transport = MemoryTransport()
349
transport.mkdir('quack')
350
transport = transport.clone('quack')
351
client = FakeClient(transport.base)
352
client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
353
client.add_success_response('ok', '', 'no', 'no')
354
bzrdir = RemoteBzrDir(transport, _client=client)
355
repo = bzrdir.open_repository()
357
[('call', 'BzrDir.find_repositoryV2', ('quack/',)),
358
('call', 'BzrDir.find_repository', ('quack/',))],
362
class OldSmartClient(object):
363
"""A fake smart client for test_old_version that just returns a version one
364
response to the 'hello' (query version) command.
367
def get_request(self):
368
input_file = StringIO('ok\x011\n')
369
output_file = StringIO()
370
client_medium = medium.SmartSimplePipesClientMedium(
371
input_file, output_file)
372
return medium.SmartClientStreamMediumRequest(client_medium)
374
def protocol_version(self):
378
class OldServerTransport(object):
379
"""A fake transport for test_old_server that reports it's smart server
380
protocol version as version one.
386
def get_smart_client(self):
387
return OldSmartClient()
390
class TestBranchLastRevisionInfo(tests.TestCase):
392
def test_empty_branch(self):
393
# in an empty branch we decode the response properly
394
transport = MemoryTransport()
395
client = FakeClient(transport.base)
396
client.add_success_response('ok', '0', 'null:')
397
transport.mkdir('quack')
398
transport = transport.clone('quack')
399
# we do not want bzrdir to make any remote calls
400
bzrdir = RemoteBzrDir(transport, _client=False)
401
branch = RemoteBranch(bzrdir, None, _client=client)
402
result = branch.last_revision_info()
405
[('call', 'Branch.last_revision_info', ('quack/',))],
407
self.assertEqual((0, NULL_REVISION), result)
409
def test_non_empty_branch(self):
410
# in a non-empty branch we also decode the response properly
411
revid = u'\xc8'.encode('utf8')
412
transport = MemoryTransport()
413
client = FakeClient(transport.base)
414
client.add_success_response('ok', '2', revid)
415
transport.mkdir('kwaak')
416
transport = transport.clone('kwaak')
417
# we do not want bzrdir to make any remote calls
418
bzrdir = RemoteBzrDir(transport, _client=False)
419
branch = RemoteBranch(bzrdir, None, _client=client)
420
result = branch.last_revision_info()
423
[('call', 'Branch.last_revision_info', ('kwaak/',))],
425
self.assertEqual((2, revid), result)
428
class TestBranchSetLastRevision(tests.TestCase):
430
def test_set_empty(self):
431
# set_revision_history([]) is translated to calling
432
# Branch.set_last_revision(path, '') on the wire.
433
transport = MemoryTransport()
434
transport.mkdir('branch')
435
transport = transport.clone('branch')
437
client = FakeClient(transport.base)
439
client.add_success_response('ok', 'branch token', 'repo token')
441
client.add_success_response('ok')
443
client.add_success_response('ok')
444
bzrdir = RemoteBzrDir(transport, _client=False)
445
branch = RemoteBranch(bzrdir, None, _client=client)
446
# This is a hack to work around the problem that RemoteBranch currently
447
# unnecessarily invokes _ensure_real upon a call to lock_write.
448
branch._ensure_real = lambda: None
451
result = branch.set_revision_history([])
453
[('call', 'Branch.set_last_revision',
454
('branch/', 'branch token', 'repo token', 'null:'))],
457
self.assertEqual(None, result)
459
def test_set_nonempty(self):
460
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
461
# Branch.set_last_revision(path, rev-idN) on the wire.
462
transport = MemoryTransport()
463
transport.mkdir('branch')
464
transport = transport.clone('branch')
466
client = FakeClient(transport.base)
468
client.add_success_response('ok', 'branch token', 'repo token')
470
client.add_success_response('ok')
472
client.add_success_response('ok')
473
bzrdir = RemoteBzrDir(transport, _client=False)
474
branch = RemoteBranch(bzrdir, None, _client=client)
475
# This is a hack to work around the problem that RemoteBranch currently
476
# unnecessarily invokes _ensure_real upon a call to lock_write.
477
branch._ensure_real = lambda: None
478
# Lock the branch, reset the record of remote calls.
482
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
484
[('call', 'Branch.set_last_revision',
485
('branch/', 'branch token', 'repo token', 'rev-id2'))],
488
self.assertEqual(None, result)
490
def test_no_such_revision(self):
491
transport = MemoryTransport()
492
transport.mkdir('branch')
493
transport = transport.clone('branch')
494
# A response of 'NoSuchRevision' is translated into an exception.
495
client = FakeClient(transport.base)
497
client.add_success_response('ok', 'branch token', 'repo token')
499
client.add_error_response('NoSuchRevision', 'rev-id')
501
client.add_success_response('ok')
503
bzrdir = RemoteBzrDir(transport, _client=False)
504
branch = RemoteBranch(bzrdir, None, _client=client)
505
branch._ensure_real = lambda: None
510
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
514
class TestBranchSetLastRevisionInfo(tests.TestCase):
516
def test_set_last_revision_info(self):
517
# set_last_revision_info(num, 'rev-id') is translated to calling
518
# Branch.set_last_revision_info(num, 'rev-id') on the wire.
519
transport = MemoryTransport()
520
transport.mkdir('branch')
521
transport = transport.clone('branch')
522
client = FakeClient(transport.base)
524
client.add_success_response('ok', 'branch token', 'repo token')
526
client.add_success_response('ok')
528
client.add_success_response('ok')
530
bzrdir = RemoteBzrDir(transport, _client=False)
531
branch = RemoteBranch(bzrdir, None, _client=client)
532
# This is a hack to work around the problem that RemoteBranch currently
533
# unnecessarily invokes _ensure_real upon a call to lock_write.
534
branch._ensure_real = lambda: None
535
# Lock the branch, reset the record of remote calls.
538
result = branch.set_last_revision_info(1234, 'a-revision-id')
540
[('call', 'Branch.set_last_revision_info',
541
('branch/', 'branch token', 'repo token',
542
'1234', 'a-revision-id'))],
544
self.assertEqual(None, result)
546
def test_no_such_revision(self):
547
# A response of 'NoSuchRevision' is translated into an exception.
548
transport = MemoryTransport()
549
transport.mkdir('branch')
550
transport = transport.clone('branch')
551
client = FakeClient(transport.base)
553
client.add_success_response('ok', 'branch token', 'repo token')
555
client.add_error_response('NoSuchRevision', 'revid')
557
client.add_success_response('ok')
559
bzrdir = RemoteBzrDir(transport, _client=False)
560
branch = RemoteBranch(bzrdir, None, _client=client)
561
# This is a hack to work around the problem that RemoteBranch currently
562
# unnecessarily invokes _ensure_real upon a call to lock_write.
563
branch._ensure_real = lambda: None
564
# Lock the branch, reset the record of remote calls.
569
errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
572
def lock_remote_branch(self, branch):
573
"""Trick a RemoteBranch into thinking it is locked."""
574
branch._lock_mode = 'w'
575
branch._lock_count = 2
576
branch._lock_token = 'branch token'
577
branch._repo_lock_token = 'repo token'
579
def test_backwards_compatibility(self):
580
"""If the server does not support the Branch.set_last_revision_info
581
verb (which is new in 1.4), then the client falls back to VFS methods.
583
# This test is a little messy. Unlike most tests in this file, it
584
# doesn't purely test what a Remote* object sends over the wire, and
585
# how it reacts to responses from the wire. It instead relies partly
586
# on asserting that the RemoteBranch will call
587
# self._real_branch.set_last_revision_info(...).
589
# First, set up our RemoteBranch with a FakeClient that raises
590
# UnknownSmartMethod, and a StubRealBranch that logs how it is called.
591
transport = MemoryTransport()
592
transport.mkdir('branch')
593
transport = transport.clone('branch')
594
client = FakeClient(transport.base)
595
client.add_unknown_method_response('Branch.set_last_revision_info')
596
bzrdir = RemoteBzrDir(transport, _client=False)
597
branch = RemoteBranch(bzrdir, None, _client=client)
598
class StubRealBranch(object):
601
def set_last_revision_info(self, revno, revision_id):
603
('set_last_revision_info', revno, revision_id))
604
real_branch = StubRealBranch()
605
branch._real_branch = real_branch
606
self.lock_remote_branch(branch)
608
# Call set_last_revision_info, and verify it behaved as expected.
609
result = branch.set_last_revision_info(1234, 'a-revision-id')
611
[('call', 'Branch.set_last_revision_info',
612
('branch/', 'branch token', 'repo token',
613
'1234', 'a-revision-id')),],
616
[('set_last_revision_info', 1234, 'a-revision-id')],
619
def test_unexpected_error(self):
620
# A response of 'NoSuchRevision' is translated into an exception.
621
transport = MemoryTransport()
622
transport.mkdir('branch')
623
transport = transport.clone('branch')
624
client = FakeClient(transport.base)
626
client.add_success_response('ok', 'branch token', 'repo token')
628
client.add_error_response('UnexpectedError')
630
client.add_success_response('ok')
632
bzrdir = RemoteBzrDir(transport, _client=False)
633
branch = RemoteBranch(bzrdir, None, _client=client)
634
# This is a hack to work around the problem that RemoteBranch currently
635
# unnecessarily invokes _ensure_real upon a call to lock_write.
636
branch._ensure_real = lambda: None
637
# Lock the branch, reset the record of remote calls.
641
err = self.assertRaises(
642
errors.ErrorFromSmartServer,
643
branch.set_last_revision_info, 123, 'revid')
644
self.assertEqual(('UnexpectedError',), err.error_tuple)
648
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
649
"""Getting the branch configuration should use an abstract method not vfs.
652
def test_get_branch_conf(self):
653
raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
654
# We should see that branch.get_config() does a single rpc to get the
655
# remote configuration file, abstracting away where that is stored on
656
# the server. However at the moment it always falls back to using the
657
# vfs, and this would need some changes in config.py.
659
# in an empty branch we decode the response properly
660
client = FakeClient(self.get_url())
661
client.add_success_response_with_body('# config file body', 'ok')
662
# we need to make a real branch because the remote_branch.control_files
663
# will trigger _ensure_real.
664
branch = self.make_branch('quack')
665
transport = branch.bzrdir.root_transport
666
# we do not want bzrdir to make any remote calls
667
bzrdir = RemoteBzrDir(transport, _client=False)
668
branch = RemoteBranch(bzrdir, None, _client=client)
669
config = branch.get_config()
671
[('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
675
class TestBranchLockWrite(tests.TestCase):
677
def test_lock_write_unlockable(self):
678
transport = MemoryTransport()
679
client = FakeClient(transport.base)
680
client.add_error_response('UnlockableTransport')
681
transport.mkdir('quack')
682
transport = transport.clone('quack')
683
# we do not want bzrdir to make any remote calls
684
bzrdir = RemoteBzrDir(transport, _client=False)
685
branch = RemoteBranch(bzrdir, None, _client=client)
686
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
688
[('call', 'Branch.lock_write', ('quack/', '', ''))],
692
class TestTransportIsReadonly(tests.TestCase):
695
client = FakeClient()
696
client.add_success_response('yes')
697
transport = RemoteTransport('bzr://example.com/', medium=False,
699
self.assertEqual(True, transport.is_readonly())
701
[('call', 'Transport.is_readonly', ())],
704
def test_false(self):
705
client = FakeClient()
706
client.add_success_response('no')
707
transport = RemoteTransport('bzr://example.com/', medium=False,
709
self.assertEqual(False, transport.is_readonly())
711
[('call', 'Transport.is_readonly', ())],
714
def test_error_from_old_server(self):
715
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
717
Clients should treat it as a "no" response, because is_readonly is only
718
advisory anyway (a transport could be read-write, but then the
719
underlying filesystem could be readonly anyway).
721
client = FakeClient()
722
client.add_unknown_method_response('Transport.is_readonly')
723
transport = RemoteTransport('bzr://example.com/', medium=False,
725
self.assertEqual(False, transport.is_readonly())
727
[('call', 'Transport.is_readonly', ())],
731
class TestRemoteRepository(tests.TestCase):
732
"""Base for testing RemoteRepository protocol usage.
734
These tests contain frozen requests and responses. We want any changes to
735
what is sent or expected to be require a thoughtful update to these tests
736
because they might break compatibility with different-versioned servers.
739
def setup_fake_client_and_repository(self, transport_path):
740
"""Create the fake client and repository for testing with.
742
There's no real server here; we just have canned responses sent
745
:param transport_path: Path below the root of the MemoryTransport
746
where the repository will be created.
748
transport = MemoryTransport()
749
transport.mkdir(transport_path)
750
client = FakeClient(transport.base)
751
transport = transport.clone(transport_path)
752
# we do not want bzrdir to make any remote calls
753
bzrdir = RemoteBzrDir(transport, _client=False)
754
repo = RemoteRepository(bzrdir, None, _client=client)
758
class TestRepositoryGatherStats(TestRemoteRepository):
760
def test_revid_none(self):
761
# ('ok',), body with revisions and size
762
transport_path = 'quack'
763
repo, client = self.setup_fake_client_and_repository(transport_path)
764
client.add_success_response_with_body(
765
'revisions: 2\nsize: 18\n', 'ok')
766
result = repo.gather_stats(None)
768
[('call_expecting_body', 'Repository.gather_stats',
769
('quack/','','no'))],
771
self.assertEqual({'revisions': 2, 'size': 18}, result)
773
def test_revid_no_committers(self):
774
# ('ok',), body without committers
775
body = ('firstrev: 123456.300 3600\n'
776
'latestrev: 654231.400 0\n'
779
transport_path = 'quick'
780
revid = u'\xc8'.encode('utf8')
781
repo, client = self.setup_fake_client_and_repository(transport_path)
782
client.add_success_response_with_body(body, 'ok')
783
result = repo.gather_stats(revid)
785
[('call_expecting_body', 'Repository.gather_stats',
786
('quick/', revid, 'no'))],
788
self.assertEqual({'revisions': 2, 'size': 18,
789
'firstrev': (123456.300, 3600),
790
'latestrev': (654231.400, 0),},
793
def test_revid_with_committers(self):
794
# ('ok',), body with committers
795
body = ('committers: 128\n'
796
'firstrev: 123456.300 3600\n'
797
'latestrev: 654231.400 0\n'
800
transport_path = 'buick'
801
revid = u'\xc8'.encode('utf8')
802
repo, client = self.setup_fake_client_and_repository(transport_path)
803
client.add_success_response_with_body(body, 'ok')
804
result = repo.gather_stats(revid, True)
806
[('call_expecting_body', 'Repository.gather_stats',
807
('buick/', revid, 'yes'))],
809
self.assertEqual({'revisions': 2, 'size': 18,
811
'firstrev': (123456.300, 3600),
812
'latestrev': (654231.400, 0),},
816
class TestRepositoryGetGraph(TestRemoteRepository):
818
def test_get_graph(self):
819
# get_graph returns a graph with the repository as the
821
transport_path = 'quack'
822
repo, client = self.setup_fake_client_and_repository(transport_path)
823
graph = repo.get_graph()
824
self.assertEqual(graph._parents_provider, repo)
827
class TestRepositoryGetParentMap(TestRemoteRepository):
829
def test_get_parent_map_caching(self):
830
# get_parent_map returns from cache until unlock()
831
# setup a reponse with two revisions
832
r1 = u'\u0e33'.encode('utf8')
833
r2 = u'\u0dab'.encode('utf8')
834
lines = [' '.join([r2, r1]), r1]
835
encoded_body = bz2.compress('\n'.join(lines))
837
transport_path = 'quack'
838
repo, client = self.setup_fake_client_and_repository(transport_path)
839
client.add_success_response_with_body(encoded_body, 'ok')
840
client.add_success_response_with_body(encoded_body, 'ok')
842
graph = repo.get_graph()
843
parents = graph.get_parent_map([r2])
844
self.assertEqual({r2: (r1,)}, parents)
845
# locking and unlocking deeper should not reset
848
parents = graph.get_parent_map([r1])
849
self.assertEqual({r1: (NULL_REVISION,)}, parents)
851
[('call_with_body_bytes_expecting_body',
852
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
855
# now we call again, and it should use the second response.
857
graph = repo.get_graph()
858
parents = graph.get_parent_map([r1])
859
self.assertEqual({r1: (NULL_REVISION,)}, parents)
861
[('call_with_body_bytes_expecting_body',
862
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
863
('call_with_body_bytes_expecting_body',
864
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
869
def test_get_parent_map_reconnects_if_unknown_method(self):
870
transport_path = 'quack'
871
repo, client = self.setup_fake_client_and_repository(transport_path)
872
client.add_unknown_method_response('Repository,get_parent_map')
873
client.add_success_response_with_body('', 'ok')
874
self.assertTrue(client._medium._remote_is_at_least_1_2)
875
rev_id = 'revision-id'
876
expected_deprecations = [
877
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
879
parents = self.callDeprecated(
880
expected_deprecations, repo.get_parent_map, [rev_id])
882
[('call_with_body_bytes_expecting_body',
883
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
884
('disconnect medium',),
885
('call_expecting_body', 'Repository.get_revision_graph',
888
# The medium is now marked as being connected to an older server
889
self.assertFalse(client._medium._remote_is_at_least_1_2)
891
def test_get_parent_map_fallback_parentless_node(self):
892
"""get_parent_map falls back to get_revision_graph on old servers. The
893
results from get_revision_graph are tweaked to match the get_parent_map
896
Specifically, a {key: ()} result from get_revision_graph means "no
897
parents" for that key, which in get_parent_map results should be
898
represented as {key: ('null:',)}.
900
This is the test for https://bugs.launchpad.net/bzr/+bug/214894
902
rev_id = 'revision-id'
903
transport_path = 'quack'
904
repo, client = self.setup_fake_client_and_repository(transport_path)
905
client.add_success_response_with_body(rev_id, 'ok')
906
client._medium._remote_is_at_least_1_2 = False
907
expected_deprecations = [
908
'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
910
parents = self.callDeprecated(
911
expected_deprecations, repo.get_parent_map, [rev_id])
913
[('call_expecting_body', 'Repository.get_revision_graph',
916
self.assertEqual({rev_id: ('null:',)}, parents)
918
def test_get_parent_map_unexpected_response(self):
919
repo, client = self.setup_fake_client_and_repository('path')
920
client.add_success_response('something unexpected!')
922
errors.UnexpectedSmartServerResponse,
923
repo.get_parent_map, ['a-revision-id'])
926
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
928
def test_null_revision(self):
929
# a null revision has the predictable result {}, we should have no wire
930
# traffic when calling it with this argument
931
transport_path = 'empty'
932
repo, client = self.setup_fake_client_and_repository(transport_path)
933
client.add_success_response('notused')
934
result = self.applyDeprecated(one_four, repo.get_revision_graph,
936
self.assertEqual([], client._calls)
937
self.assertEqual({}, result)
939
def test_none_revision(self):
940
# with none we want the entire graph
941
r1 = u'\u0e33'.encode('utf8')
942
r2 = u'\u0dab'.encode('utf8')
943
lines = [' '.join([r2, r1]), r1]
944
encoded_body = '\n'.join(lines)
946
transport_path = 'sinhala'
947
repo, client = self.setup_fake_client_and_repository(transport_path)
948
client.add_success_response_with_body(encoded_body, 'ok')
949
result = self.applyDeprecated(one_four, repo.get_revision_graph)
951
[('call_expecting_body', 'Repository.get_revision_graph',
954
self.assertEqual({r1: (), r2: (r1, )}, result)
956
def test_specific_revision(self):
957
# with a specific revision we want the graph for that
958
# with none we want the entire graph
959
r11 = u'\u0e33'.encode('utf8')
960
r12 = u'\xc9'.encode('utf8')
961
r2 = u'\u0dab'.encode('utf8')
962
lines = [' '.join([r2, r11, r12]), r11, r12]
963
encoded_body = '\n'.join(lines)
965
transport_path = 'sinhala'
966
repo, client = self.setup_fake_client_and_repository(transport_path)
967
client.add_success_response_with_body(encoded_body, 'ok')
968
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
970
[('call_expecting_body', 'Repository.get_revision_graph',
973
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
975
def test_no_such_revision(self):
977
transport_path = 'sinhala'
978
repo, client = self.setup_fake_client_and_repository(transport_path)
979
client.add_error_response('nosuchrevision', revid)
980
# also check that the right revision is reported in the error
981
self.assertRaises(errors.NoSuchRevision,
982
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
984
[('call_expecting_body', 'Repository.get_revision_graph',
985
('sinhala/', revid))],
988
def test_unexpected_error(self):
990
transport_path = 'sinhala'
991
repo, client = self.setup_fake_client_and_repository(transport_path)
992
client.add_error_response('AnUnexpectedError')
993
e = self.assertRaises(errors.ErrorFromSmartServer,
994
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
995
self.assertEqual(('AnUnexpectedError',), e.error_tuple)
998
class TestRepositoryIsShared(TestRemoteRepository):
1000
def test_is_shared(self):
1001
# ('yes', ) for Repository.is_shared -> 'True'.
1002
transport_path = 'quack'
1003
repo, client = self.setup_fake_client_and_repository(transport_path)
1004
client.add_success_response('yes')
1005
result = repo.is_shared()
1007
[('call', 'Repository.is_shared', ('quack/',))],
1009
self.assertEqual(True, result)
1011
def test_is_not_shared(self):
1012
# ('no', ) for Repository.is_shared -> 'False'.
1013
transport_path = 'qwack'
1014
repo, client = self.setup_fake_client_and_repository(transport_path)
1015
client.add_success_response('no')
1016
result = repo.is_shared()
1018
[('call', 'Repository.is_shared', ('qwack/',))],
1020
self.assertEqual(False, result)
1023
class TestRepositoryLockWrite(TestRemoteRepository):
1025
def test_lock_write(self):
1026
transport_path = 'quack'
1027
repo, client = self.setup_fake_client_and_repository(transport_path)
1028
client.add_success_response('ok', 'a token')
1029
result = repo.lock_write()
1031
[('call', 'Repository.lock_write', ('quack/', ''))],
1033
self.assertEqual('a token', result)
1035
def test_lock_write_already_locked(self):
1036
transport_path = 'quack'
1037
repo, client = self.setup_fake_client_and_repository(transport_path)
1038
client.add_error_response('LockContention')
1039
self.assertRaises(errors.LockContention, repo.lock_write)
1041
[('call', 'Repository.lock_write', ('quack/', ''))],
1044
def test_lock_write_unlockable(self):
1045
transport_path = 'quack'
1046
repo, client = self.setup_fake_client_and_repository(transport_path)
1047
client.add_error_response('UnlockableTransport')
1048
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
1050
[('call', 'Repository.lock_write', ('quack/', ''))],
1054
class TestRepositoryUnlock(TestRemoteRepository):
1056
def test_unlock(self):
1057
transport_path = 'quack'
1058
repo, client = self.setup_fake_client_and_repository(transport_path)
1059
client.add_success_response('ok', 'a token')
1060
client.add_success_response('ok')
1064
[('call', 'Repository.lock_write', ('quack/', '')),
1065
('call', 'Repository.unlock', ('quack/', 'a token'))],
1068
def test_unlock_wrong_token(self):
1069
# If somehow the token is wrong, unlock will raise TokenMismatch.
1070
transport_path = 'quack'
1071
repo, client = self.setup_fake_client_and_repository(transport_path)
1072
client.add_success_response('ok', 'a token')
1073
client.add_error_response('TokenMismatch')
1075
self.assertRaises(errors.TokenMismatch, repo.unlock)
1078
class TestRepositoryHasRevision(TestRemoteRepository):
1080
def test_none(self):
1081
# repo.has_revision(None) should not cause any traffic.
1082
transport_path = 'quack'
1083
repo, client = self.setup_fake_client_and_repository(transport_path)
1085
# The null revision is always there, so has_revision(None) == True.
1086
self.assertEqual(True, repo.has_revision(NULL_REVISION))
1088
# The remote repo shouldn't be accessed.
1089
self.assertEqual([], client._calls)
1092
class TestRepositoryTarball(TestRemoteRepository):
1094
# This is a canned tarball reponse we can validate against
1096
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
1097
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
1098
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
1099
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
1100
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
1101
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
1102
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
1103
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
1104
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
1105
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
1106
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
1107
'nWQ7QH/F3JFOFCQ0aSPfA='
1110
def test_repository_tarball(self):
1111
# Test that Repository.tarball generates the right operations
1112
transport_path = 'repo'
1113
expected_calls = [('call_expecting_body', 'Repository.tarball',
1114
('repo/', 'bz2',),),
1116
repo, client = self.setup_fake_client_and_repository(transport_path)
1117
client.add_success_response_with_body(self.tarball_content, 'ok')
1118
# Now actually ask for the tarball
1119
tarball_file = repo._get_tarball('bz2')
1121
self.assertEqual(expected_calls, client._calls)
1122
self.assertEqual(self.tarball_content, tarball_file.read())
1124
tarball_file.close()
1127
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
1128
"""RemoteRepository.copy_content_into optimizations"""
1130
def test_copy_content_remote_to_local(self):
1131
self.transport_server = server.SmartTCPServer_for_testing
1132
src_repo = self.make_repository('repo1')
1133
src_repo = repository.Repository.open(self.get_url('repo1'))
1134
# At the moment the tarball-based copy_content_into can't write back
1135
# into a smart server. It would be good if it could upload the
1136
# tarball; once that works we'd have to create repositories of
1137
# different formats. -- mbp 20070410
1138
dest_url = self.get_vfs_only_url('repo2')
1139
dest_bzrdir = BzrDir.create(dest_url)
1140
dest_repo = dest_bzrdir.create_repository()
1141
self.assertFalse(isinstance(dest_repo, RemoteRepository))
1142
self.assertTrue(isinstance(src_repo, RemoteRepository))
1143
src_repo.copy_content_into(dest_repo)
1146
class TestRepositoryStreamKnitData(TestRemoteRepository):
1148
def make_pack_file(self, records):
1149
pack_file = StringIO()
1150
pack_writer = pack.ContainerWriter(pack_file.write)
1152
for bytes, names in records:
1153
pack_writer.add_bytes_record(bytes, names)
1158
def make_pack_stream(self, records):
1159
pack_serialiser = pack.ContainerSerialiser()
1160
yield pack_serialiser.begin()
1161
for bytes, names in records:
1162
yield pack_serialiser.bytes_record(bytes, names)
1163
yield pack_serialiser.end()
1165
def test_bad_pack_from_server(self):
1166
"""A response with invalid data (e.g. it has a record with multiple
1167
names) triggers an exception.
1169
Not all possible errors will be caught at this stage, but obviously
1170
malformed data should be.
1172
record = ('bytes', [('name1',), ('name2',)])
1173
pack_stream = self.make_pack_stream([record])
1174
transport_path = 'quack'
1175
repo, client = self.setup_fake_client_and_repository(transport_path)
1176
client.add_success_response_with_body(pack_stream, 'ok')
1177
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1178
stream = repo.get_data_stream_for_search(search)
1179
self.assertRaises(errors.SmartProtocolError, list, stream)
1181
def test_backwards_compatibility(self):
1182
"""If the server doesn't recognise this request, fallback to VFS."""
1183
repo, client = self.setup_fake_client_and_repository('path')
1184
client.add_unknown_method_response(
1185
'Repository.stream_revisions_chunked')
1186
self.mock_called = False
1187
repo._real_repository = MockRealRepository(self)
1188
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
1189
repo.get_data_stream_for_search(search)
1190
self.assertTrue(self.mock_called)
1191
self.failIf(client.expecting_body,
1192
"The protocol has been left in an unclean state that will cause "
1193
"TooManyConcurrentRequests errors.")
1196
class MockRealRepository(object):
1197
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
1199
def __init__(self, test):
1202
def get_data_stream_for_search(self, search):
1203
self.test.assertEqual(set(['revid']), search.get_keys())
1204
self.test.mock_called = True