1
# Copyright (C) 2006, 2007 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.memory import MemoryTransport
50
from bzrlib.transport.remote import RemoteTransport
53
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
56
self.transport_server = server.SmartTCPServer_for_testing
57
super(BasicRemoteObjectTests, self).setUp()
58
self.transport = self.get_transport()
59
# make a branch that can be opened over the smart transport
60
self.local_wt = BzrDir.create_standalone_workingtree('.')
63
self.transport.disconnect()
64
tests.TestCaseWithTransport.tearDown(self)
66
def test_create_remote_bzrdir(self):
67
b = remote.RemoteBzrDir(self.transport)
68
self.assertIsInstance(b, BzrDir)
70
def test_open_remote_branch(self):
71
# open a standalone branch in the working directory
72
b = remote.RemoteBzrDir(self.transport)
73
branch = b.open_branch()
74
self.assertIsInstance(branch, Branch)
76
def test_remote_repository(self):
77
b = BzrDir.open_from_transport(self.transport)
78
repo = b.open_repository()
79
revid = u'\xc823123123'.encode('utf8')
80
self.assertFalse(repo.has_revision(revid))
81
self.local_wt.commit(message='test commit', rev_id=revid)
82
self.assertTrue(repo.has_revision(revid))
84
def test_remote_branch_revision_history(self):
85
b = BzrDir.open_from_transport(self.transport).open_branch()
86
self.assertEqual([], b.revision_history())
87
r1 = self.local_wt.commit('1st commit')
88
r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
89
self.assertEqual([r1, r2], b.revision_history())
91
def test_find_correct_format(self):
92
"""Should open a RemoteBzrDir over a RemoteTransport"""
93
fmt = BzrDirFormat.find_format(self.transport)
94
self.assertTrue(RemoteBzrDirFormat
95
in BzrDirFormat._control_server_formats)
96
self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
98
def test_open_detected_smart_format(self):
99
fmt = BzrDirFormat.find_format(self.transport)
100
d = fmt.open(self.transport)
101
self.assertIsInstance(d, BzrDir)
103
def test_remote_branch_repr(self):
104
b = BzrDir.open_from_transport(self.transport).open_branch()
105
self.assertStartsWith(str(b), 'RemoteBranch(')
108
class FakeProtocol(object):
109
"""Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
111
def __init__(self, body, fake_client):
113
self._body_buffer = None
114
self._fake_client = fake_client
116
def read_body_bytes(self, count=-1):
117
if self._body_buffer is None:
118
self._body_buffer = StringIO(self.body)
119
bytes = self._body_buffer.read(count)
120
if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
121
self._fake_client.expecting_body = False
124
def cancel_read_body(self):
125
self._fake_client.expecting_body = False
127
def read_streamed_body(self):
131
class FakeClient(_SmartClient):
132
"""Lookalike for _SmartClient allowing testing."""
134
def __init__(self, responses, fake_medium_base='fake base'):
135
"""Create a FakeClient.
137
:param responses: A list of response-tuple, body-data pairs to be sent
140
self.responses = responses
142
self.expecting_body = False
143
_SmartClient.__init__(self, FakeMedium(fake_medium_base, self._calls))
145
def call(self, method, *args):
146
self._calls.append(('call', method, args))
147
return self.responses.pop(0)[0]
149
def call_expecting_body(self, method, *args):
150
self._calls.append(('call_expecting_body', method, args))
151
result = self.responses.pop(0)
152
self.expecting_body = True
153
return result[0], FakeProtocol(result[1], self)
155
def call_with_body_bytes_expecting_body(self, method, args, body):
156
self._calls.append(('call_with_body_bytes_expecting_body', method,
158
result = self.responses.pop(0)
159
self.expecting_body = True
160
return result[0], FakeProtocol(result[1], self)
163
class FakeMedium(object):
165
def __init__(self, base, client_calls):
167
self.connection = FakeConnection(client_calls)
168
self._client_calls = client_calls
171
class FakeConnection(object):
173
def __init__(self, client_calls):
174
self._remote_is_at_least_1_2 = True
175
self._client_calls = client_calls
177
def disconnect(self):
178
self._client_calls.append(('disconnect medium',))
181
class TestVfsHas(tests.TestCase):
183
def test_unicode_path(self):
184
client = FakeClient([(('yes',), )], '/')
185
transport = RemoteTransport('bzr://localhost/', _client=client)
186
filename = u'/hell\u00d8'.encode('utf8')
187
result = transport.has(filename)
189
[('call', 'has', (filename,))],
191
self.assertTrue(result)
194
class TestBzrDirOpenBranch(tests.TestCase):
196
def test_branch_present(self):
197
transport = MemoryTransport()
198
transport.mkdir('quack')
199
transport = transport.clone('quack')
200
client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
202
bzrdir = RemoteBzrDir(transport, _client=client)
203
result = bzrdir.open_branch()
205
[('call', 'BzrDir.open_branch', ('quack/',)),
206
('call', 'BzrDir.find_repositoryV2', ('quack/',))],
208
self.assertIsInstance(result, RemoteBranch)
209
self.assertEqual(bzrdir, result.bzrdir)
211
def test_branch_missing(self):
212
transport = MemoryTransport()
213
transport.mkdir('quack')
214
transport = transport.clone('quack')
215
client = FakeClient([(('nobranch',), )], transport.base)
216
bzrdir = RemoteBzrDir(transport, _client=client)
217
self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
219
[('call', 'BzrDir.open_branch', ('quack/',))],
222
def test__get_tree_branch(self):
223
# _get_tree_branch is a form of open_branch, but it should only ask for
224
# branch opening, not any other network requests.
227
calls.append("Called")
229
transport = MemoryTransport()
230
# no requests on the network - catches other api calls being made.
231
client = FakeClient([], transport.base)
232
bzrdir = RemoteBzrDir(transport, _client=client)
233
# patch the open_branch call to record that it was called.
234
bzrdir.open_branch = open_branch
235
self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
236
self.assertEqual(["Called"], calls)
237
self.assertEqual([], client._calls)
239
def test_url_quoting_of_path(self):
240
# Relpaths on the wire should not be URL-escaped. So "~" should be
241
# transmitted as "~", not "%7E".
242
transport = RemoteTransport('bzr://localhost/~hello/')
243
client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
245
bzrdir = RemoteBzrDir(transport, _client=client)
246
result = bzrdir.open_branch()
248
[('call', 'BzrDir.open_branch', ('~hello/',)),
249
('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
252
def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
253
transport = MemoryTransport()
254
transport.mkdir('quack')
255
transport = transport.clone('quack')
257
rich_response = 'yes'
261
subtree_response = 'yes'
263
subtree_response = 'no'
265
[(('ok', '', rich_response, subtree_response, external_lookup), ),],
267
bzrdir = RemoteBzrDir(transport, _client=client)
268
result = bzrdir.open_repository()
270
[('call', 'BzrDir.find_repositoryV2', ('quack/',))],
272
self.assertIsInstance(result, RemoteRepository)
273
self.assertEqual(bzrdir, result.bzrdir)
274
self.assertEqual(rich_root, result._format.rich_root_data)
275
self.assertEqual(subtrees, result._format.supports_tree_reference)
277
def test_open_repository_sets_format_attributes(self):
278
self.check_open_repository(True, True)
279
self.check_open_repository(False, True)
280
self.check_open_repository(True, False)
281
self.check_open_repository(False, False)
282
self.check_open_repository(False, False, 'yes')
284
def test_old_server(self):
285
"""RemoteBzrDirFormat should fail to probe if the server version is too
288
self.assertRaises(errors.NotBranchError,
289
RemoteBzrDirFormat.probe_transport, OldServerTransport())
292
class OldSmartClient(object):
293
"""A fake smart client for test_old_version that just returns a version one
294
response to the 'hello' (query version) command.
297
def get_request(self):
298
input_file = StringIO('ok\x011\n')
299
output_file = StringIO()
300
client_medium = medium.SmartSimplePipesClientMedium(
301
input_file, output_file)
302
return medium.SmartClientStreamMediumRequest(client_medium)
304
def protocol_version(self):
308
class OldServerTransport(object):
309
"""A fake transport for test_old_server that reports it's smart server
310
protocol version as version one.
316
def get_smart_client(self):
317
return OldSmartClient()
320
class TestBranchLastRevisionInfo(tests.TestCase):
322
def test_empty_branch(self):
323
# in an empty branch we decode the response properly
324
transport = MemoryTransport()
325
client = FakeClient([(('ok', '0', 'null:'), )], transport.base)
326
transport.mkdir('quack')
327
transport = transport.clone('quack')
328
# we do not want bzrdir to make any remote calls
329
bzrdir = RemoteBzrDir(transport, _client=False)
330
branch = RemoteBranch(bzrdir, None, _client=client)
331
result = branch.last_revision_info()
334
[('call', 'Branch.last_revision_info', ('quack/',))],
336
self.assertEqual((0, NULL_REVISION), result)
338
def test_non_empty_branch(self):
339
# in a non-empty branch we also decode the response properly
340
revid = u'\xc8'.encode('utf8')
341
transport = MemoryTransport()
342
client = FakeClient([(('ok', '2', revid), )], transport.base)
343
transport.mkdir('kwaak')
344
transport = transport.clone('kwaak')
345
# we do not want bzrdir to make any remote calls
346
bzrdir = RemoteBzrDir(transport, _client=False)
347
branch = RemoteBranch(bzrdir, None, _client=client)
348
result = branch.last_revision_info()
351
[('call', 'Branch.last_revision_info', ('kwaak/',))],
353
self.assertEqual((2, revid), result)
356
class TestBranchSetLastRevision(tests.TestCase):
358
def test_set_empty(self):
359
# set_revision_history([]) is translated to calling
360
# Branch.set_last_revision(path, '') on the wire.
361
transport = MemoryTransport()
362
transport.mkdir('branch')
363
transport = transport.clone('branch')
365
client = FakeClient([
367
(('ok', 'branch token', 'repo token'), ),
373
bzrdir = RemoteBzrDir(transport, _client=False)
374
branch = RemoteBranch(bzrdir, None, _client=client)
375
# This is a hack to work around the problem that RemoteBranch currently
376
# unnecessarily invokes _ensure_real upon a call to lock_write.
377
branch._ensure_real = lambda: None
380
result = branch.set_revision_history([])
382
[('call', 'Branch.set_last_revision',
383
('branch/', 'branch token', 'repo token', 'null:'))],
386
self.assertEqual(None, result)
388
def test_set_nonempty(self):
389
# set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
390
# Branch.set_last_revision(path, rev-idN) on the wire.
391
transport = MemoryTransport()
392
transport.mkdir('branch')
393
transport = transport.clone('branch')
395
client = FakeClient([
397
(('ok', 'branch token', 'repo token'), ),
403
bzrdir = RemoteBzrDir(transport, _client=False)
404
branch = RemoteBranch(bzrdir, None, _client=client)
405
# This is a hack to work around the problem that RemoteBranch currently
406
# unnecessarily invokes _ensure_real upon a call to lock_write.
407
branch._ensure_real = lambda: None
408
# Lock the branch, reset the record of remote calls.
412
result = branch.set_revision_history(['rev-id1', 'rev-id2'])
414
[('call', 'Branch.set_last_revision',
415
('branch/', 'branch token', 'repo token', 'rev-id2'))],
418
self.assertEqual(None, result)
420
def test_no_such_revision(self):
421
# A response of 'NoSuchRevision' is translated into an exception.
422
client = FakeClient([
424
(('ok', 'branch token', 'repo token'), ),
426
(('NoSuchRevision', 'rev-id'), ),
429
transport = MemoryTransport()
430
transport.mkdir('branch')
431
transport = transport.clone('branch')
433
bzrdir = RemoteBzrDir(transport, _client=False)
434
branch = RemoteBranch(bzrdir, None, _client=client)
435
branch._ensure_real = lambda: None
440
errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
444
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
445
"""Test branch.control_files api munging...
447
We special case RemoteBranch.control_files.get('branch.conf') to
448
call a specific API so that RemoteBranch's can intercept configuration
449
file reading, allowing them to signal to the client about things like
450
'email is configured for commits'.
453
def test_get_branch_conf(self):
454
# in an empty branch we decode the response properly
455
client = FakeClient([(('ok', ), 'config file body')], self.get_url())
456
# we need to make a real branch because the remote_branch.control_files
457
# will trigger _ensure_real.
458
branch = self.make_branch('quack')
459
transport = branch.bzrdir.root_transport
460
# we do not want bzrdir to make any remote calls
461
bzrdir = RemoteBzrDir(transport, _client=False)
462
branch = RemoteBranch(bzrdir, None, _client=client)
463
result = branch.control_files.get('branch.conf')
465
[('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
467
self.assertEqual('config file body', result.read())
470
class TestBranchLockWrite(tests.TestCase):
472
def test_lock_write_unlockable(self):
473
transport = MemoryTransport()
474
client = FakeClient([(('UnlockableTransport', ), '')], transport.base)
475
transport.mkdir('quack')
476
transport = transport.clone('quack')
477
# we do not want bzrdir to make any remote calls
478
bzrdir = RemoteBzrDir(transport, _client=False)
479
branch = RemoteBranch(bzrdir, None, _client=client)
480
self.assertRaises(errors.UnlockableTransport, branch.lock_write)
482
[('call', 'Branch.lock_write', ('quack/', '', ''))],
486
class TestTransportIsReadonly(tests.TestCase):
489
client = FakeClient([(('yes',), '')])
490
transport = RemoteTransport('bzr://example.com/', medium=False,
492
self.assertEqual(True, transport.is_readonly())
494
[('call', 'Transport.is_readonly', ())],
497
def test_false(self):
498
client = FakeClient([(('no',), '')])
499
transport = RemoteTransport('bzr://example.com/', medium=False,
501
self.assertEqual(False, transport.is_readonly())
503
[('call', 'Transport.is_readonly', ())],
506
def test_error_from_old_server(self):
507
"""bzr 0.15 and earlier servers don't recognise the is_readonly verb.
509
Clients should treat it as a "no" response, because is_readonly is only
510
advisory anyway (a transport could be read-write, but then the
511
underlying filesystem could be readonly anyway).
513
client = FakeClient([(
514
('error', "Generic bzr smart protocol error: "
515
"bad request 'Transport.is_readonly'"), '')])
516
transport = RemoteTransport('bzr://example.com/', medium=False,
518
self.assertEqual(False, transport.is_readonly())
520
[('call', 'Transport.is_readonly', ())],
523
def test_error_from_old_0_11_server(self):
524
"""Same as test_error_from_old_server, but with the slightly different
525
error message from bzr 0.11 servers.
527
client = FakeClient([(
528
('error', "Generic bzr smart protocol error: "
529
"bad request u'Transport.is_readonly'"), '')])
530
transport = RemoteTransport('bzr://example.com/', medium=False,
532
self.assertEqual(False, transport.is_readonly())
534
[('call', 'Transport.is_readonly', ())],
538
class TestRemoteRepository(tests.TestCase):
539
"""Base for testing RemoteRepository protocol usage.
541
These tests contain frozen requests and responses. We want any changes to
542
what is sent or expected to be require a thoughtful update to these tests
543
because they might break compatibility with different-versioned servers.
546
def setup_fake_client_and_repository(self, responses, transport_path):
547
"""Create the fake client and repository for testing with.
549
There's no real server here; we just have canned responses sent
552
:param transport_path: Path below the root of the MemoryTransport
553
where the repository will be created.
555
transport = MemoryTransport()
556
transport.mkdir(transport_path)
557
client = FakeClient(responses, transport.base)
558
transport = transport.clone(transport_path)
559
# we do not want bzrdir to make any remote calls
560
bzrdir = RemoteBzrDir(transport, _client=False)
561
repo = RemoteRepository(bzrdir, None, _client=client)
565
class TestRepositoryGatherStats(TestRemoteRepository):
567
def test_revid_none(self):
568
# ('ok',), body with revisions and size
569
responses = [(('ok', ), 'revisions: 2\nsize: 18\n')]
570
transport_path = 'quack'
571
repo, client = self.setup_fake_client_and_repository(
572
responses, transport_path)
573
result = repo.gather_stats(None)
575
[('call_expecting_body', 'Repository.gather_stats',
576
('quack/','','no'))],
578
self.assertEqual({'revisions': 2, 'size': 18}, result)
580
def test_revid_no_committers(self):
581
# ('ok',), body without committers
582
responses = [(('ok', ),
583
'firstrev: 123456.300 3600\n'
584
'latestrev: 654231.400 0\n'
587
transport_path = 'quick'
588
revid = u'\xc8'.encode('utf8')
589
repo, client = self.setup_fake_client_and_repository(
590
responses, transport_path)
591
result = repo.gather_stats(revid)
593
[('call_expecting_body', 'Repository.gather_stats',
594
('quick/', revid, 'no'))],
596
self.assertEqual({'revisions': 2, 'size': 18,
597
'firstrev': (123456.300, 3600),
598
'latestrev': (654231.400, 0),},
601
def test_revid_with_committers(self):
602
# ('ok',), body with committers
603
responses = [(('ok', ),
605
'firstrev: 123456.300 3600\n'
606
'latestrev: 654231.400 0\n'
609
transport_path = 'buick'
610
revid = u'\xc8'.encode('utf8')
611
repo, client = self.setup_fake_client_and_repository(
612
responses, transport_path)
613
result = repo.gather_stats(revid, True)
615
[('call_expecting_body', 'Repository.gather_stats',
616
('buick/', revid, 'yes'))],
618
self.assertEqual({'revisions': 2, 'size': 18,
620
'firstrev': (123456.300, 3600),
621
'latestrev': (654231.400, 0),},
625
class TestRepositoryGetGraph(TestRemoteRepository):
627
def test_get_graph(self):
628
# get_graph returns a graph with the repository as the
631
transport_path = 'quack'
632
repo, client = self.setup_fake_client_and_repository(
633
responses, transport_path)
634
graph = repo.get_graph()
635
self.assertEqual(graph._parents_provider, repo)
638
class TestRepositoryGetParentMap(TestRemoteRepository):
640
def test_get_parent_map_caching(self):
641
# get_parent_map returns from cache until unlock()
642
# setup a reponse with two revisions
643
r1 = u'\u0e33'.encode('utf8')
644
r2 = u'\u0dab'.encode('utf8')
645
lines = [' '.join([r2, r1]), r1]
646
encoded_body = bz2.compress('\n'.join(lines))
647
responses = [(('ok', ), encoded_body), (('ok', ), encoded_body)]
649
transport_path = 'quack'
650
repo, client = self.setup_fake_client_and_repository(
651
responses, transport_path)
653
graph = repo.get_graph()
654
parents = graph.get_parent_map([r2])
655
self.assertEqual({r2: (r1,)}, parents)
656
# locking and unlocking deeper should not reset
659
parents = graph.get_parent_map([r1])
660
self.assertEqual({r1: (NULL_REVISION,)}, parents)
662
[('call_with_body_bytes_expecting_body',
663
'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
666
# now we call again, and it should use the second response.
668
graph = repo.get_graph()
669
parents = graph.get_parent_map([r1])
670
self.assertEqual({r1: (NULL_REVISION,)}, parents)
672
[('call_with_body_bytes_expecting_body',
673
'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
674
('call_with_body_bytes_expecting_body',
675
'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
680
def test_get_parent_map_reconnects_if_unknown_method(self):
682
"Generic bzr smart protocol error: "
683
"bad request 'Repository.get_parent_map'")
685
(('error', error_msg), ''),
687
transport_path = 'quack'
688
repo, client = self.setup_fake_client_and_repository(
689
responses, transport_path)
690
rev_id = 'revision-id'
691
parents = repo.get_parent_map([rev_id])
693
[('call_with_body_bytes_expecting_body',
694
'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
695
('disconnect medium',),
696
('call_expecting_body', 'Repository.get_revision_graph',
702
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
704
def test_null_revision(self):
705
# a null revision has the predictable result {}, we should have no wire
706
# traffic when calling it with this argument
707
responses = [(('notused', ), '')]
708
transport_path = 'empty'
709
repo, client = self.setup_fake_client_and_repository(
710
responses, transport_path)
711
result = self.applyDeprecated(one_four, repo.get_revision_graph,
713
self.assertEqual([], client._calls)
714
self.assertEqual({}, result)
716
def test_none_revision(self):
717
# with none we want the entire graph
718
r1 = u'\u0e33'.encode('utf8')
719
r2 = u'\u0dab'.encode('utf8')
720
lines = [' '.join([r2, r1]), r1]
721
encoded_body = '\n'.join(lines)
723
responses = [(('ok', ), encoded_body)]
724
transport_path = 'sinhala'
725
repo, client = self.setup_fake_client_and_repository(
726
responses, transport_path)
727
result = self.applyDeprecated(one_four, repo.get_revision_graph)
729
[('call_expecting_body', 'Repository.get_revision_graph',
732
self.assertEqual({r1: (), r2: (r1, )}, result)
734
def test_specific_revision(self):
735
# with a specific revision we want the graph for that
736
# with none we want the entire graph
737
r11 = u'\u0e33'.encode('utf8')
738
r12 = u'\xc9'.encode('utf8')
739
r2 = u'\u0dab'.encode('utf8')
740
lines = [' '.join([r2, r11, r12]), r11, r12]
741
encoded_body = '\n'.join(lines)
743
responses = [(('ok', ), encoded_body)]
744
transport_path = 'sinhala'
745
repo, client = self.setup_fake_client_and_repository(
746
responses, transport_path)
747
result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
749
[('call_expecting_body', 'Repository.get_revision_graph',
752
self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
754
def test_no_such_revision(self):
756
responses = [(('nosuchrevision', revid), '')]
757
transport_path = 'sinhala'
758
repo, client = self.setup_fake_client_and_repository(
759
responses, transport_path)
760
# also check that the right revision is reported in the error
761
self.assertRaises(errors.NoSuchRevision,
762
self.applyDeprecated, one_four, repo.get_revision_graph, revid)
764
[('call_expecting_body', 'Repository.get_revision_graph',
765
('sinhala/', revid))],
769
class TestRepositoryIsShared(TestRemoteRepository):
771
def test_is_shared(self):
772
# ('yes', ) for Repository.is_shared -> 'True'.
773
responses = [(('yes', ), )]
774
transport_path = 'quack'
775
repo, client = self.setup_fake_client_and_repository(
776
responses, transport_path)
777
result = repo.is_shared()
779
[('call', 'Repository.is_shared', ('quack/',))],
781
self.assertEqual(True, result)
783
def test_is_not_shared(self):
784
# ('no', ) for Repository.is_shared -> 'False'.
785
responses = [(('no', ), )]
786
transport_path = 'qwack'
787
repo, client = self.setup_fake_client_and_repository(
788
responses, transport_path)
789
result = repo.is_shared()
791
[('call', 'Repository.is_shared', ('qwack/',))],
793
self.assertEqual(False, result)
796
class TestRepositoryLockWrite(TestRemoteRepository):
798
def test_lock_write(self):
799
responses = [(('ok', 'a token'), '')]
800
transport_path = 'quack'
801
repo, client = self.setup_fake_client_and_repository(
802
responses, transport_path)
803
result = repo.lock_write()
805
[('call', 'Repository.lock_write', ('quack/', ''))],
807
self.assertEqual('a token', result)
809
def test_lock_write_already_locked(self):
810
responses = [(('LockContention', ), '')]
811
transport_path = 'quack'
812
repo, client = self.setup_fake_client_and_repository(
813
responses, transport_path)
814
self.assertRaises(errors.LockContention, repo.lock_write)
816
[('call', 'Repository.lock_write', ('quack/', ''))],
819
def test_lock_write_unlockable(self):
820
responses = [(('UnlockableTransport', ), '')]
821
transport_path = 'quack'
822
repo, client = self.setup_fake_client_and_repository(
823
responses, transport_path)
824
self.assertRaises(errors.UnlockableTransport, repo.lock_write)
826
[('call', 'Repository.lock_write', ('quack/', ''))],
830
class TestRepositoryUnlock(TestRemoteRepository):
832
def test_unlock(self):
833
responses = [(('ok', 'a token'), ''),
835
transport_path = 'quack'
836
repo, client = self.setup_fake_client_and_repository(
837
responses, transport_path)
841
[('call', 'Repository.lock_write', ('quack/', '')),
842
('call', 'Repository.unlock', ('quack/', 'a token'))],
845
def test_unlock_wrong_token(self):
846
# If somehow the token is wrong, unlock will raise TokenMismatch.
847
responses = [(('ok', 'a token'), ''),
848
(('TokenMismatch',), '')]
849
transport_path = 'quack'
850
repo, client = self.setup_fake_client_and_repository(
851
responses, transport_path)
853
self.assertRaises(errors.TokenMismatch, repo.unlock)
856
class TestRepositoryHasRevision(TestRemoteRepository):
859
# repo.has_revision(None) should not cause any traffic.
860
transport_path = 'quack'
862
repo, client = self.setup_fake_client_and_repository(
863
responses, transport_path)
865
# The null revision is always there, so has_revision(None) == True.
866
self.assertEqual(True, repo.has_revision(NULL_REVISION))
868
# The remote repo shouldn't be accessed.
869
self.assertEqual([], client._calls)
872
class TestRepositoryTarball(TestRemoteRepository):
874
# This is a canned tarball reponse we can validate against
876
'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
877
'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
878
'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
879
'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
880
'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
881
'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
882
'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
883
'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
884
'0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
885
'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
886
'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
887
'nWQ7QH/F3JFOFCQ0aSPfA='
890
def test_repository_tarball(self):
891
# Test that Repository.tarball generates the right operations
892
transport_path = 'repo'
893
expected_responses = [(('ok',), self.tarball_content),
895
expected_calls = [('call_expecting_body', 'Repository.tarball',
898
remote_repo, client = self.setup_fake_client_and_repository(
899
expected_responses, transport_path)
900
# Now actually ask for the tarball
901
tarball_file = remote_repo._get_tarball('bz2')
903
self.assertEqual(expected_calls, client._calls)
904
self.assertEqual(self.tarball_content, tarball_file.read())
909
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
910
"""RemoteRepository.copy_content_into optimizations"""
912
def test_copy_content_remote_to_local(self):
913
self.transport_server = server.SmartTCPServer_for_testing
914
src_repo = self.make_repository('repo1')
915
src_repo = repository.Repository.open(self.get_url('repo1'))
916
# At the moment the tarball-based copy_content_into can't write back
917
# into a smart server. It would be good if it could upload the
918
# tarball; once that works we'd have to create repositories of
919
# different formats. -- mbp 20070410
920
dest_url = self.get_vfs_only_url('repo2')
921
dest_bzrdir = BzrDir.create(dest_url)
922
dest_repo = dest_bzrdir.create_repository()
923
self.assertFalse(isinstance(dest_repo, RemoteRepository))
924
self.assertTrue(isinstance(src_repo, RemoteRepository))
925
src_repo.copy_content_into(dest_repo)
928
class TestRepositoryStreamKnitData(TestRemoteRepository):
930
def make_pack_file(self, records):
931
pack_file = StringIO()
932
pack_writer = pack.ContainerWriter(pack_file.write)
934
for bytes, names in records:
935
pack_writer.add_bytes_record(bytes, names)
940
def make_pack_stream(self, records):
941
pack_serialiser = pack.ContainerSerialiser()
942
yield pack_serialiser.begin()
943
for bytes, names in records:
944
yield pack_serialiser.bytes_record(bytes, names)
945
yield pack_serialiser.end()
947
def test_bad_pack_from_server(self):
948
"""A response with invalid data (e.g. it has a record with multiple
949
names) triggers an exception.
951
Not all possible errors will be caught at this stage, but obviously
952
malformed data should be.
954
record = ('bytes', [('name1',), ('name2',)])
955
pack_stream = self.make_pack_stream([record])
956
responses = [(('ok',), pack_stream), ]
957
transport_path = 'quack'
958
repo, client = self.setup_fake_client_and_repository(
959
responses, transport_path)
960
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
961
stream = repo.get_data_stream_for_search(search)
962
self.assertRaises(errors.SmartProtocolError, list, stream)
964
def test_backwards_compatibility(self):
965
"""If the server doesn't recognise this request, fallback to VFS."""
967
"Generic bzr smart protocol error: "
968
"bad request 'Repository.stream_revisions_chunked'")
970
(('error', error_msg), '')]
971
repo, client = self.setup_fake_client_and_repository(
973
self.mock_called = False
974
repo._real_repository = MockRealRepository(self)
975
search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
976
repo.get_data_stream_for_search(search)
977
self.assertTrue(self.mock_called)
978
self.failIf(client.expecting_body,
979
"The protocol has been left in an unclean state that will cause "
980
"TooManyConcurrentRequests errors.")
983
class MockRealRepository(object):
984
"""Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
986
def __init__(self, test):
989
def get_data_stream_for_search(self, search):
990
self.test.assertEqual(set(['revid']), search.get_keys())
991
self.test.mock_called = True