/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_remote.py

  • Committer: Martin Pool
  • Date: 2008-11-21 04:06:25 UTC
  • mto: This revision was merged to the branch mainline in revision 3845.
  • Revision ID: mbp@sourcefrog.net-20081121040625-zvsu9pu1pen8pqfr
Review cleanups on readdir

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
"""Tests for remote bzrdir/branch/repo/etc
 
18
 
 
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. 
 
22
 
 
23
These tests correspond to tests.test_smart, which exercises the server side.
 
24
"""
 
25
 
 
26
import bz2
 
27
from cStringIO import StringIO
 
28
 
 
29
from bzrlib import (
 
30
    config,
 
31
    errors,
 
32
    graph,
 
33
    pack,
 
34
    remote,
 
35
    repository,
 
36
    tests,
 
37
    urlutils,
 
38
    )
 
39
from bzrlib.branch import Branch
 
40
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
41
from bzrlib.remote import (
 
42
    RemoteBranch,
 
43
    RemoteBzrDir,
 
44
    RemoteBzrDirFormat,
 
45
    RemoteRepository,
 
46
    )
 
47
from bzrlib.revision import NULL_REVISION
 
48
from bzrlib.smart import server, medium
 
49
from bzrlib.smart.client import _SmartClient
 
50
from bzrlib.symbol_versioning import one_four
 
51
from bzrlib.transport import get_transport, http
 
52
from bzrlib.transport.memory import MemoryTransport
 
53
from bzrlib.transport.remote import (
 
54
    RemoteTransport,
 
55
    RemoteSSHTransport,
 
56
    RemoteTCPTransport,
 
57
)
 
58
 
 
59
 
 
60
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
 
61
 
 
62
    def setUp(self):
 
63
        self.transport_server = server.SmartTCPServer_for_testing
 
64
        super(BasicRemoteObjectTests, self).setUp()
 
65
        self.transport = self.get_transport()
 
66
        # make a branch that can be opened over the smart transport
 
67
        self.local_wt = BzrDir.create_standalone_workingtree('.')
 
68
 
 
69
    def tearDown(self):
 
70
        self.transport.disconnect()
 
71
        tests.TestCaseWithTransport.tearDown(self)
 
72
 
 
73
    def test_create_remote_bzrdir(self):
 
74
        b = remote.RemoteBzrDir(self.transport)
 
75
        self.assertIsInstance(b, BzrDir)
 
76
 
 
77
    def test_open_remote_branch(self):
 
78
        # open a standalone branch in the working directory
 
79
        b = remote.RemoteBzrDir(self.transport)
 
80
        branch = b.open_branch()
 
81
        self.assertIsInstance(branch, Branch)
 
82
 
 
83
    def test_remote_repository(self):
 
84
        b = BzrDir.open_from_transport(self.transport)
 
85
        repo = b.open_repository()
 
86
        revid = u'\xc823123123'.encode('utf8')
 
87
        self.assertFalse(repo.has_revision(revid))
 
88
        self.local_wt.commit(message='test commit', rev_id=revid)
 
89
        self.assertTrue(repo.has_revision(revid))
 
90
 
 
91
    def test_remote_branch_revision_history(self):
 
92
        b = BzrDir.open_from_transport(self.transport).open_branch()
 
93
        self.assertEqual([], b.revision_history())
 
94
        r1 = self.local_wt.commit('1st commit')
 
95
        r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
 
96
        self.assertEqual([r1, r2], b.revision_history())
 
97
 
 
98
    def test_find_correct_format(self):
 
99
        """Should open a RemoteBzrDir over a RemoteTransport"""
 
100
        fmt = BzrDirFormat.find_format(self.transport)
 
101
        self.assertTrue(RemoteBzrDirFormat
 
102
                        in BzrDirFormat._control_server_formats)
 
103
        self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
 
104
 
 
105
    def test_open_detected_smart_format(self):
 
106
        fmt = BzrDirFormat.find_format(self.transport)
 
107
        d = fmt.open(self.transport)
 
108
        self.assertIsInstance(d, BzrDir)
 
109
 
 
110
    def test_remote_branch_repr(self):
 
111
        b = BzrDir.open_from_transport(self.transport).open_branch()
 
112
        self.assertStartsWith(str(b), 'RemoteBranch(')
 
113
 
 
114
 
 
115
class FakeRemoteTransport(object):
 
116
    """This class provides the minimum support for use in place of a RemoteTransport.
 
117
    
 
118
    It doesn't actually transmit requests, but rather expects them to be
 
119
    handled by a FakeClient which holds canned responses.  It does not allow
 
120
    any vfs access, therefore is not suitable for testing any operation that
 
121
    will fallback to vfs access.  Backing the test by an instance of this
 
122
    class guarantees that it's - done using non-vfs operations.
 
123
    """
 
124
 
 
125
    _default_url = 'fakeremotetransport://host/path/'
 
126
 
 
127
    def __init__(self, url=None):
 
128
        if url is None:
 
129
            url = self._default_url
 
130
        self.base = url
 
131
 
 
132
    def __repr__(self):
 
133
        return "%r(%r)" % (self.__class__.__name__,
 
134
            self.base)
 
135
 
 
136
    def clone(self, relpath):
 
137
        return FakeRemoteTransport(urlutils.join(self.base, relpath))
 
138
 
 
139
    def get(self, relpath):
 
140
        # only get is specifically stubbed out, because it's usually the first
 
141
        # thing we do.  anything else will fail with an AttributeError.
 
142
        raise AssertionError("%r doesn't support file access to %r"
 
143
            % (self, relpath))
 
144
 
 
145
 
 
146
 
 
147
class FakeProtocol(object):
 
148
    """Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
 
149
 
 
150
    def __init__(self, body, fake_client):
 
151
        self.body = body
 
152
        self._body_buffer = None
 
153
        self._fake_client = fake_client
 
154
 
 
155
    def read_body_bytes(self, count=-1):
 
156
        if self._body_buffer is None:
 
157
            self._body_buffer = StringIO(self.body)
 
158
        bytes = self._body_buffer.read(count)
 
159
        if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
 
160
            self._fake_client.expecting_body = False
 
161
        return bytes
 
162
 
 
163
    def cancel_read_body(self):
 
164
        self._fake_client.expecting_body = False
 
165
 
 
166
    def read_streamed_body(self):
 
167
        return self.body
 
168
 
 
169
 
 
170
class FakeClient(_SmartClient):
 
171
    """Lookalike for _SmartClient allowing testing."""
 
172
    
 
173
    def __init__(self, fake_medium_base='fake base'):
 
174
        """Create a FakeClient."""
 
175
        self.responses = []
 
176
        self._calls = []
 
177
        self.expecting_body = False
 
178
        # if non-None, this is the list of expected calls, with only the
 
179
        # method name and arguments included.  the body might be hard to
 
180
        # compute so is not included
 
181
        self._expected_calls = None
 
182
        _SmartClient.__init__(self, FakeMedium(self._calls, fake_medium_base))
 
183
 
 
184
    def add_expected_call(self, call_name, call_args, response_type,
 
185
        response_args, response_body=None):
 
186
        if self._expected_calls is None:
 
187
            self._expected_calls = []
 
188
        self._expected_calls.append((call_name, call_args))
 
189
        self.responses.append((response_type, response_args, response_body))
 
190
 
 
191
    def add_success_response(self, *args):
 
192
        self.responses.append(('success', args, None))
 
193
 
 
194
    def add_success_response_with_body(self, body, *args):
 
195
        self.responses.append(('success', args, body))
 
196
 
 
197
    def add_error_response(self, *args):
 
198
        self.responses.append(('error', args))
 
199
 
 
200
    def add_unknown_method_response(self, verb):
 
201
        self.responses.append(('unknown', verb))
 
202
 
 
203
    def finished_test(self):
 
204
        if self._expected_calls:
 
205
            raise AssertionError("%r finished but was still expecting %r"
 
206
                % (self, self._expected_calls[0]))
 
207
 
 
208
    def _get_next_response(self):
 
209
        try:
 
210
            response_tuple = self.responses.pop(0)
 
211
        except IndexError, e:
 
212
            raise AssertionError("%r didn't expect any more calls"
 
213
                % (self,))
 
214
        if response_tuple[0] == 'unknown':
 
215
            raise errors.UnknownSmartMethod(response_tuple[1])
 
216
        elif response_tuple[0] == 'error':
 
217
            raise errors.ErrorFromSmartServer(response_tuple[1])
 
218
        return response_tuple
 
219
 
 
220
    def _check_call(self, method, args):
 
221
        if self._expected_calls is None:
 
222
            # the test should be updated to say what it expects
 
223
            return
 
224
        try:
 
225
            next_call = self._expected_calls.pop(0)
 
226
        except IndexError:
 
227
            raise AssertionError("%r didn't expect any more calls "
 
228
                "but got %r%r"
 
229
                % (self, method, args,))
 
230
        if method != next_call[0] or args != next_call[1]:
 
231
            raise AssertionError("%r expected %r%r "
 
232
                "but got %r%r"
 
233
                % (self, next_call[0], next_call[1], method, args,))
 
234
 
 
235
    def call(self, method, *args):
 
236
        self._check_call(method, args)
 
237
        self._calls.append(('call', method, args))
 
238
        return self._get_next_response()[1]
 
239
 
 
240
    def call_expecting_body(self, method, *args):
 
241
        self._check_call(method, args)
 
242
        self._calls.append(('call_expecting_body', method, args))
 
243
        result = self._get_next_response()
 
244
        self.expecting_body = True
 
245
        return result[1], FakeProtocol(result[2], self)
 
246
 
 
247
    def call_with_body_bytes_expecting_body(self, method, args, body):
 
248
        self._check_call(method, args)
 
249
        self._calls.append(('call_with_body_bytes_expecting_body', method,
 
250
            args, body))
 
251
        result = self._get_next_response()
 
252
        self.expecting_body = True
 
253
        return result[1], FakeProtocol(result[2], self)
 
254
 
 
255
 
 
256
class FakeMedium(medium.SmartClientMedium):
 
257
 
 
258
    def __init__(self, client_calls, base):
 
259
        medium.SmartClientMedium.__init__(self, base)
 
260
        self._client_calls = client_calls
 
261
 
 
262
    def disconnect(self):
 
263
        self._client_calls.append(('disconnect medium',))
 
264
 
 
265
 
 
266
class TestVfsHas(tests.TestCase):
 
267
 
 
268
    def test_unicode_path(self):
 
269
        client = FakeClient('/')
 
270
        client.add_success_response('yes',)
 
271
        transport = RemoteTransport('bzr://localhost/', _client=client)
 
272
        filename = u'/hell\u00d8'.encode('utf8')
 
273
        result = transport.has(filename)
 
274
        self.assertEqual(
 
275
            [('call', 'has', (filename,))],
 
276
            client._calls)
 
277
        self.assertTrue(result)
 
278
 
 
279
 
 
280
class Test_ClientMedium_remote_path_from_transport(tests.TestCase):
 
281
    """Tests for the behaviour of client_medium.remote_path_from_transport."""
 
282
 
 
283
    def assertRemotePath(self, expected, client_base, transport_base):
 
284
        """Assert that the result of
 
285
        SmartClientMedium.remote_path_from_transport is the expected value for
 
286
        a given client_base and transport_base.
 
287
        """
 
288
        client_medium = medium.SmartClientMedium(client_base)
 
289
        transport = get_transport(transport_base)
 
290
        result = client_medium.remote_path_from_transport(transport)
 
291
        self.assertEqual(expected, result)
 
292
 
 
293
    def test_remote_path_from_transport(self):
 
294
        """SmartClientMedium.remote_path_from_transport calculates a URL for
 
295
        the given transport relative to the root of the client base URL.
 
296
        """
 
297
        self.assertRemotePath('xyz/', 'bzr://host/path', 'bzr://host/xyz')
 
298
        self.assertRemotePath(
 
299
            'path/xyz/', 'bzr://host/path', 'bzr://host/path/xyz')
 
300
 
 
301
    def assertRemotePathHTTP(self, expected, transport_base, relpath):
 
302
        """Assert that the result of
 
303
        HttpTransportBase.remote_path_from_transport is the expected value for
 
304
        a given transport_base and relpath of that transport.  (Note that
 
305
        HttpTransportBase is a subclass of SmartClientMedium)
 
306
        """
 
307
        base_transport = get_transport(transport_base)
 
308
        client_medium = base_transport.get_smart_medium()
 
309
        cloned_transport = base_transport.clone(relpath)
 
310
        result = client_medium.remote_path_from_transport(cloned_transport)
 
311
        self.assertEqual(expected, result)
 
312
        
 
313
    def test_remote_path_from_transport_http(self):
 
314
        """Remote paths for HTTP transports are calculated differently to other
 
315
        transports.  They are just relative to the client base, not the root
 
316
        directory of the host.
 
317
        """
 
318
        for scheme in ['http:', 'https:', 'bzr+http:', 'bzr+https:']:
 
319
            self.assertRemotePathHTTP(
 
320
                '../xyz/', scheme + '//host/path', '../xyz/')
 
321
            self.assertRemotePathHTTP(
 
322
                'xyz/', scheme + '//host/path', 'xyz/')
 
323
 
 
324
 
 
325
class Test_ClientMedium_remote_is_at_least(tests.TestCase):
 
326
    """Tests for the behaviour of client_medium.remote_is_at_least."""
 
327
 
 
328
    def test_initially_unlimited(self):
 
329
        """A fresh medium assumes that the remote side supports all
 
330
        versions.
 
331
        """
 
332
        client_medium = medium.SmartClientMedium('dummy base')
 
333
        self.assertFalse(client_medium._is_remote_before((99, 99)))
 
334
    
 
335
    def test__remember_remote_is_before(self):
 
336
        """Calling _remember_remote_is_before ratchets down the known remote
 
337
        version.
 
338
        """
 
339
        client_medium = medium.SmartClientMedium('dummy base')
 
340
        # Mark the remote side as being less than 1.6.  The remote side may
 
341
        # still be 1.5.
 
342
        client_medium._remember_remote_is_before((1, 6))
 
343
        self.assertTrue(client_medium._is_remote_before((1, 6)))
 
344
        self.assertFalse(client_medium._is_remote_before((1, 5)))
 
345
        # Calling _remember_remote_is_before again with a lower value works.
 
346
        client_medium._remember_remote_is_before((1, 5))
 
347
        self.assertTrue(client_medium._is_remote_before((1, 5)))
 
348
        # You cannot call _remember_remote_is_before with a larger value.
 
349
        self.assertRaises(
 
350
            AssertionError, client_medium._remember_remote_is_before, (1, 9))
 
351
 
 
352
 
 
353
class TestBzrDirOpenBranch(tests.TestCase):
 
354
 
 
355
    def test_branch_present(self):
 
356
        transport = MemoryTransport()
 
357
        transport.mkdir('quack')
 
358
        transport = transport.clone('quack')
 
359
        client = FakeClient(transport.base)
 
360
        client.add_expected_call(
 
361
            'BzrDir.open_branch', ('quack/',),
 
362
            'success', ('ok', ''))
 
363
        client.add_expected_call(
 
364
            'BzrDir.find_repositoryV2', ('quack/',),
 
365
            'success', ('ok', '', 'no', 'no', 'no'))
 
366
        client.add_expected_call(
 
367
            'Branch.get_stacked_on_url', ('quack/',),
 
368
            'error', ('NotStacked',))
 
369
        bzrdir = RemoteBzrDir(transport, _client=client)
 
370
        result = bzrdir.open_branch()
 
371
        self.assertIsInstance(result, RemoteBranch)
 
372
        self.assertEqual(bzrdir, result.bzrdir)
 
373
        client.finished_test()
 
374
 
 
375
    def test_branch_missing(self):
 
376
        transport = MemoryTransport()
 
377
        transport.mkdir('quack')
 
378
        transport = transport.clone('quack')
 
379
        client = FakeClient(transport.base)
 
380
        client.add_error_response('nobranch')
 
381
        bzrdir = RemoteBzrDir(transport, _client=client)
 
382
        self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
 
383
        self.assertEqual(
 
384
            [('call', 'BzrDir.open_branch', ('quack/',))],
 
385
            client._calls)
 
386
 
 
387
    def test__get_tree_branch(self):
 
388
        # _get_tree_branch is a form of open_branch, but it should only ask for
 
389
        # branch opening, not any other network requests.
 
390
        calls = []
 
391
        def open_branch():
 
392
            calls.append("Called")
 
393
            return "a-branch"
 
394
        transport = MemoryTransport()
 
395
        # no requests on the network - catches other api calls being made.
 
396
        client = FakeClient(transport.base)
 
397
        bzrdir = RemoteBzrDir(transport, _client=client)
 
398
        # patch the open_branch call to record that it was called.
 
399
        bzrdir.open_branch = open_branch
 
400
        self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
 
401
        self.assertEqual(["Called"], calls)
 
402
        self.assertEqual([], client._calls)
 
403
 
 
404
    def test_url_quoting_of_path(self):
 
405
        # Relpaths on the wire should not be URL-escaped.  So "~" should be
 
406
        # transmitted as "~", not "%7E".
 
407
        transport = RemoteTCPTransport('bzr://localhost/~hello/')
 
408
        client = FakeClient(transport.base)
 
409
        client.add_expected_call(
 
410
            'BzrDir.open_branch', ('~hello/',),
 
411
            'success', ('ok', ''))
 
412
        client.add_expected_call(
 
413
            'BzrDir.find_repositoryV2', ('~hello/',),
 
414
            'success', ('ok', '', 'no', 'no', 'no'))
 
415
        client.add_expected_call(
 
416
            'Branch.get_stacked_on_url', ('~hello/',),
 
417
            'error', ('NotStacked',))
 
418
        bzrdir = RemoteBzrDir(transport, _client=client)
 
419
        result = bzrdir.open_branch()
 
420
        client.finished_test()
 
421
 
 
422
    def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
 
423
        transport = MemoryTransport()
 
424
        transport.mkdir('quack')
 
425
        transport = transport.clone('quack')
 
426
        if rich_root:
 
427
            rich_response = 'yes'
 
428
        else:
 
429
            rich_response = 'no'
 
430
        if subtrees:
 
431
            subtree_response = 'yes'
 
432
        else:
 
433
            subtree_response = 'no'
 
434
        client = FakeClient(transport.base)
 
435
        client.add_success_response(
 
436
            'ok', '', rich_response, subtree_response, external_lookup)
 
437
        bzrdir = RemoteBzrDir(transport, _client=client)
 
438
        result = bzrdir.open_repository()
 
439
        self.assertEqual(
 
440
            [('call', 'BzrDir.find_repositoryV2', ('quack/',))],
 
441
            client._calls)
 
442
        self.assertIsInstance(result, RemoteRepository)
 
443
        self.assertEqual(bzrdir, result.bzrdir)
 
444
        self.assertEqual(rich_root, result._format.rich_root_data)
 
445
        self.assertEqual(subtrees, result._format.supports_tree_reference)
 
446
 
 
447
    def test_open_repository_sets_format_attributes(self):
 
448
        self.check_open_repository(True, True)
 
449
        self.check_open_repository(False, True)
 
450
        self.check_open_repository(True, False)
 
451
        self.check_open_repository(False, False)
 
452
        self.check_open_repository(False, False, 'yes')
 
453
 
 
454
    def test_old_server(self):
 
455
        """RemoteBzrDirFormat should fail to probe if the server version is too
 
456
        old.
 
457
        """
 
458
        self.assertRaises(errors.NotBranchError,
 
459
            RemoteBzrDirFormat.probe_transport, OldServerTransport())
 
460
 
 
461
 
 
462
class TestBzrDirOpenRepository(tests.TestCase):
 
463
 
 
464
    def test_backwards_compat_1_2(self):
 
465
        transport = MemoryTransport()
 
466
        transport.mkdir('quack')
 
467
        transport = transport.clone('quack')
 
468
        client = FakeClient(transport.base)
 
469
        client.add_unknown_method_response('RemoteRepository.find_repositoryV2')
 
470
        client.add_success_response('ok', '', 'no', 'no')
 
471
        bzrdir = RemoteBzrDir(transport, _client=client)
 
472
        repo = bzrdir.open_repository()
 
473
        self.assertEqual(
 
474
            [('call', 'BzrDir.find_repositoryV2', ('quack/',)),
 
475
             ('call', 'BzrDir.find_repository', ('quack/',))],
 
476
            client._calls)
 
477
 
 
478
 
 
479
class OldSmartClient(object):
 
480
    """A fake smart client for test_old_version that just returns a version one
 
481
    response to the 'hello' (query version) command.
 
482
    """
 
483
 
 
484
    def get_request(self):
 
485
        input_file = StringIO('ok\x011\n')
 
486
        output_file = StringIO()
 
487
        client_medium = medium.SmartSimplePipesClientMedium(
 
488
            input_file, output_file)
 
489
        return medium.SmartClientStreamMediumRequest(client_medium)
 
490
 
 
491
    def protocol_version(self):
 
492
        return 1
 
493
 
 
494
 
 
495
class OldServerTransport(object):
 
496
    """A fake transport for test_old_server that reports it's smart server
 
497
    protocol version as version one.
 
498
    """
 
499
 
 
500
    def __init__(self):
 
501
        self.base = 'fake:'
 
502
 
 
503
    def get_smart_client(self):
 
504
        return OldSmartClient()
 
505
 
 
506
 
 
507
class RemoteBranchTestCase(tests.TestCase):
 
508
 
 
509
    def make_remote_branch(self, transport, client):
 
510
        """Make a RemoteBranch using 'client' as its _SmartClient.
 
511
        
 
512
        A RemoteBzrDir and RemoteRepository will also be created to fill out
 
513
        the RemoteBranch, albeit with stub values for some of their attributes.
 
514
        """
 
515
        # we do not want bzrdir to make any remote calls, so use False as its
 
516
        # _client.  If it tries to make a remote call, this will fail
 
517
        # immediately.
 
518
        bzrdir = RemoteBzrDir(transport, _client=False)
 
519
        repo = RemoteRepository(bzrdir, None, _client=client)
 
520
        return RemoteBranch(bzrdir, repo, _client=client)
 
521
 
 
522
 
 
523
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
 
524
 
 
525
    def test_empty_branch(self):
 
526
        # in an empty branch we decode the response properly
 
527
        transport = MemoryTransport()
 
528
        client = FakeClient(transport.base)
 
529
        client.add_expected_call(
 
530
            'Branch.get_stacked_on_url', ('quack/',),
 
531
            'error', ('NotStacked',))
 
532
        client.add_expected_call(
 
533
            'Branch.last_revision_info', ('quack/',),
 
534
            'success', ('ok', '0', 'null:'))
 
535
        transport.mkdir('quack')
 
536
        transport = transport.clone('quack')
 
537
        branch = self.make_remote_branch(transport, client)
 
538
        result = branch.last_revision_info()
 
539
        client.finished_test()
 
540
        self.assertEqual((0, NULL_REVISION), result)
 
541
 
 
542
    def test_non_empty_branch(self):
 
543
        # in a non-empty branch we also decode the response properly
 
544
        revid = u'\xc8'.encode('utf8')
 
545
        transport = MemoryTransport()
 
546
        client = FakeClient(transport.base)
 
547
        client.add_expected_call(
 
548
            'Branch.get_stacked_on_url', ('kwaak/',),
 
549
            'error', ('NotStacked',))
 
550
        client.add_expected_call(
 
551
            'Branch.last_revision_info', ('kwaak/',),
 
552
            'success', ('ok', '2', revid))
 
553
        transport.mkdir('kwaak')
 
554
        transport = transport.clone('kwaak')
 
555
        branch = self.make_remote_branch(transport, client)
 
556
        result = branch.last_revision_info()
 
557
        self.assertEqual((2, revid), result)
 
558
 
 
559
 
 
560
class TestBranch_get_stacked_on_url(tests.TestCaseWithMemoryTransport):
 
561
    """Test Branch._get_stacked_on_url rpc"""
 
562
 
 
563
    def test_get_stacked_on_invalid_url(self):
 
564
        raise tests.KnownFailure('opening a branch requires the server to open the fallback repository')
 
565
        transport = FakeRemoteTransport('fakeremotetransport:///')
 
566
        client = FakeClient(transport.base)
 
567
        client.add_expected_call(
 
568
            'Branch.get_stacked_on_url', ('.',),
 
569
            'success', ('ok', 'file:///stacked/on'))
 
570
        bzrdir = RemoteBzrDir(transport, _client=client)
 
571
        branch = RemoteBranch(bzrdir, None, _client=client)
 
572
        result = branch.get_stacked_on_url()
 
573
        self.assertEqual(
 
574
            'file:///stacked/on', result)
 
575
 
 
576
    def test_backwards_compatible(self):
 
577
        # like with bzr1.6 with no Branch.get_stacked_on_url rpc
 
578
        base_branch = self.make_branch('base', format='1.6')
 
579
        stacked_branch = self.make_branch('stacked', format='1.6')
 
580
        stacked_branch.set_stacked_on_url('../base')
 
581
        client = FakeClient(self.get_url())
 
582
        client.add_expected_call(
 
583
            'BzrDir.open_branch', ('stacked/',),
 
584
            'success', ('ok', ''))
 
585
        client.add_expected_call(
 
586
            'BzrDir.find_repositoryV2', ('stacked/',),
 
587
            'success', ('ok', '', 'no', 'no', 'no'))
 
588
        # called twice, once from constructor and then again by us
 
589
        client.add_expected_call(
 
590
            'Branch.get_stacked_on_url', ('stacked/',),
 
591
            'unknown', ('Branch.get_stacked_on_url',))
 
592
        client.add_expected_call(
 
593
            'Branch.get_stacked_on_url', ('stacked/',),
 
594
            'unknown', ('Branch.get_stacked_on_url',))
 
595
        # this will also do vfs access, but that goes direct to the transport
 
596
        # and isn't seen by the FakeClient.
 
597
        bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
 
598
        branch = bzrdir.open_branch()
 
599
        result = branch.get_stacked_on_url()
 
600
        self.assertEqual('../base', result)
 
601
        client.finished_test()
 
602
        # it's in the fallback list both for the RemoteRepository and its vfs
 
603
        # repository
 
604
        self.assertEqual(1, len(branch.repository._fallback_repositories))
 
605
        self.assertEqual(1,
 
606
            len(branch.repository._real_repository._fallback_repositories))
 
607
 
 
608
    def test_get_stacked_on_real_branch(self):
 
609
        base_branch = self.make_branch('base', format='1.6')
 
610
        stacked_branch = self.make_branch('stacked', format='1.6')
 
611
        stacked_branch.set_stacked_on_url('../base')
 
612
        client = FakeClient(self.get_url())
 
613
        client.add_expected_call(
 
614
            'BzrDir.open_branch', ('stacked/',),
 
615
            'success', ('ok', ''))
 
616
        client.add_expected_call(
 
617
            'BzrDir.find_repositoryV2', ('stacked/',),
 
618
            'success', ('ok', '', 'no', 'no', 'no'))
 
619
        # called twice, once from constructor and then again by us
 
620
        client.add_expected_call(
 
621
            'Branch.get_stacked_on_url', ('stacked/',),
 
622
            'success', ('ok', '../base'))
 
623
        client.add_expected_call(
 
624
            'Branch.get_stacked_on_url', ('stacked/',),
 
625
            'success', ('ok', '../base'))
 
626
        bzrdir = RemoteBzrDir(self.get_transport('stacked'), _client=client)
 
627
        branch = bzrdir.open_branch()
 
628
        result = branch.get_stacked_on_url()
 
629
        self.assertEqual('../base', result)
 
630
        client.finished_test()
 
631
        # it's in the fallback list both for the RemoteRepository and its vfs
 
632
        # repository
 
633
        self.assertEqual(1, len(branch.repository._fallback_repositories))
 
634
        self.assertEqual(1,
 
635
            len(branch.repository._real_repository._fallback_repositories))
 
636
 
 
637
 
 
638
class TestBranchSetLastRevision(RemoteBranchTestCase):
 
639
 
 
640
    def test_set_empty(self):
 
641
        # set_revision_history([]) is translated to calling
 
642
        # Branch.set_last_revision(path, '') on the wire.
 
643
        transport = MemoryTransport()
 
644
        transport.mkdir('branch')
 
645
        transport = transport.clone('branch')
 
646
 
 
647
        client = FakeClient(transport.base)
 
648
        client.add_expected_call(
 
649
            'Branch.get_stacked_on_url', ('branch/',),
 
650
            'error', ('NotStacked',))
 
651
        client.add_expected_call(
 
652
            'Branch.lock_write', ('branch/', '', ''),
 
653
            'success', ('ok', 'branch token', 'repo token'))
 
654
        client.add_expected_call(
 
655
            'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'null:',),
 
656
            'success', ('ok',))
 
657
        client.add_expected_call(
 
658
            'Branch.unlock', ('branch/', 'branch token', 'repo token'),
 
659
            'success', ('ok',))
 
660
        branch = self.make_remote_branch(transport, client)
 
661
        # This is a hack to work around the problem that RemoteBranch currently
 
662
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
663
        branch._ensure_real = lambda: None
 
664
        branch.lock_write()
 
665
        result = branch.set_revision_history([])
 
666
        branch.unlock()
 
667
        self.assertEqual(None, result)
 
668
        client.finished_test()
 
669
 
 
670
    def test_set_nonempty(self):
 
671
        # set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
 
672
        # Branch.set_last_revision(path, rev-idN) on the wire.
 
673
        transport = MemoryTransport()
 
674
        transport.mkdir('branch')
 
675
        transport = transport.clone('branch')
 
676
 
 
677
        client = FakeClient(transport.base)
 
678
        client.add_expected_call(
 
679
            'Branch.get_stacked_on_url', ('branch/',),
 
680
            'error', ('NotStacked',))
 
681
        client.add_expected_call(
 
682
            'Branch.lock_write', ('branch/', '', ''),
 
683
            'success', ('ok', 'branch token', 'repo token'))
 
684
        client.add_expected_call(
 
685
            'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id2',),
 
686
            'success', ('ok',))
 
687
        client.add_expected_call(
 
688
            'Branch.unlock', ('branch/', 'branch token', 'repo token'),
 
689
            'success', ('ok',))
 
690
        branch = self.make_remote_branch(transport, client)
 
691
        # This is a hack to work around the problem that RemoteBranch currently
 
692
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
693
        branch._ensure_real = lambda: None
 
694
        # Lock the branch, reset the record of remote calls.
 
695
        branch.lock_write()
 
696
        result = branch.set_revision_history(['rev-id1', 'rev-id2'])
 
697
        branch.unlock()
 
698
        self.assertEqual(None, result)
 
699
        client.finished_test()
 
700
 
 
701
    def test_no_such_revision(self):
 
702
        transport = MemoryTransport()
 
703
        transport.mkdir('branch')
 
704
        transport = transport.clone('branch')
 
705
        # A response of 'NoSuchRevision' is translated into an exception.
 
706
        client = FakeClient(transport.base)
 
707
        client.add_expected_call(
 
708
            'Branch.get_stacked_on_url', ('branch/',),
 
709
            'error', ('NotStacked',))
 
710
        client.add_expected_call(
 
711
            'Branch.lock_write', ('branch/', '', ''),
 
712
            'success', ('ok', 'branch token', 'repo token'))
 
713
        client.add_expected_call(
 
714
            'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
 
715
            'error', ('NoSuchRevision', 'rev-id'))
 
716
        client.add_expected_call(
 
717
            'Branch.unlock', ('branch/', 'branch token', 'repo token'),
 
718
            'success', ('ok',))
 
719
 
 
720
        branch = self.make_remote_branch(transport, client)
 
721
        branch.lock_write()
 
722
        self.assertRaises(
 
723
            errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
 
724
        branch.unlock()
 
725
        client.finished_test()
 
726
 
 
727
    def test_tip_change_rejected(self):
 
728
        """TipChangeRejected responses cause a TipChangeRejected exception to
 
729
        be raised.
 
730
        """
 
731
        transport = MemoryTransport()
 
732
        transport.mkdir('branch')
 
733
        transport = transport.clone('branch')
 
734
        client = FakeClient(transport.base)
 
735
        rejection_msg_unicode = u'rejection message\N{INTERROBANG}'
 
736
        rejection_msg_utf8 = rejection_msg_unicode.encode('utf8')
 
737
        client.add_expected_call(
 
738
            'Branch.get_stacked_on_url', ('branch/',),
 
739
            'error', ('NotStacked',))
 
740
        client.add_expected_call(
 
741
            'Branch.lock_write', ('branch/', '', ''),
 
742
            'success', ('ok', 'branch token', 'repo token'))
 
743
        client.add_expected_call(
 
744
            'Branch.set_last_revision', ('branch/', 'branch token', 'repo token', 'rev-id',),
 
745
            'error', ('TipChangeRejected', rejection_msg_utf8))
 
746
        client.add_expected_call(
 
747
            'Branch.unlock', ('branch/', 'branch token', 'repo token'),
 
748
            'success', ('ok',))
 
749
        branch = self.make_remote_branch(transport, client)
 
750
        branch._ensure_real = lambda: None
 
751
        branch.lock_write()
 
752
        self.addCleanup(branch.unlock)
 
753
        # The 'TipChangeRejected' error response triggered by calling
 
754
        # set_revision_history causes a TipChangeRejected exception.
 
755
        err = self.assertRaises(
 
756
            errors.TipChangeRejected, branch.set_revision_history, ['rev-id'])
 
757
        # The UTF-8 message from the response has been decoded into a unicode
 
758
        # object.
 
759
        self.assertIsInstance(err.msg, unicode)
 
760
        self.assertEqual(rejection_msg_unicode, err.msg)
 
761
        branch.unlock()
 
762
        client.finished_test()
 
763
 
 
764
 
 
765
class TestBranchSetLastRevisionInfo(RemoteBranchTestCase):
 
766
 
 
767
    def test_set_last_revision_info(self):
 
768
        # set_last_revision_info(num, 'rev-id') is translated to calling
 
769
        # Branch.set_last_revision_info(num, 'rev-id') on the wire.
 
770
        transport = MemoryTransport()
 
771
        transport.mkdir('branch')
 
772
        transport = transport.clone('branch')
 
773
        client = FakeClient(transport.base)
 
774
        # get_stacked_on_url
 
775
        client.add_error_response('NotStacked')
 
776
        # lock_write
 
777
        client.add_success_response('ok', 'branch token', 'repo token')
 
778
        # set_last_revision
 
779
        client.add_success_response('ok')
 
780
        # unlock
 
781
        client.add_success_response('ok')
 
782
 
 
783
        branch = self.make_remote_branch(transport, client)
 
784
        # Lock the branch, reset the record of remote calls.
 
785
        branch.lock_write()
 
786
        client._calls = []
 
787
        result = branch.set_last_revision_info(1234, 'a-revision-id')
 
788
        self.assertEqual(
 
789
            [('call', 'Branch.set_last_revision_info',
 
790
                ('branch/', 'branch token', 'repo token',
 
791
                 '1234', 'a-revision-id'))],
 
792
            client._calls)
 
793
        self.assertEqual(None, result)
 
794
 
 
795
    def test_no_such_revision(self):
 
796
        # A response of 'NoSuchRevision' is translated into an exception.
 
797
        transport = MemoryTransport()
 
798
        transport.mkdir('branch')
 
799
        transport = transport.clone('branch')
 
800
        client = FakeClient(transport.base)
 
801
        # get_stacked_on_url
 
802
        client.add_error_response('NotStacked')
 
803
        # lock_write
 
804
        client.add_success_response('ok', 'branch token', 'repo token')
 
805
        # set_last_revision
 
806
        client.add_error_response('NoSuchRevision', 'revid')
 
807
        # unlock
 
808
        client.add_success_response('ok')
 
809
 
 
810
        branch = self.make_remote_branch(transport, client)
 
811
        # Lock the branch, reset the record of remote calls.
 
812
        branch.lock_write()
 
813
        client._calls = []
 
814
 
 
815
        self.assertRaises(
 
816
            errors.NoSuchRevision, branch.set_last_revision_info, 123, 'revid')
 
817
        branch.unlock()
 
818
 
 
819
    def lock_remote_branch(self, branch):
 
820
        """Trick a RemoteBranch into thinking it is locked."""
 
821
        branch._lock_mode = 'w'
 
822
        branch._lock_count = 2
 
823
        branch._lock_token = 'branch token'
 
824
        branch._repo_lock_token = 'repo token'
 
825
        branch.repository._lock_mode = 'w'
 
826
        branch.repository._lock_count = 2
 
827
        branch.repository._lock_token = 'repo token'
 
828
 
 
829
    def test_backwards_compatibility(self):
 
830
        """If the server does not support the Branch.set_last_revision_info
 
831
        verb (which is new in 1.4), then the client falls back to VFS methods.
 
832
        """
 
833
        # This test is a little messy.  Unlike most tests in this file, it
 
834
        # doesn't purely test what a Remote* object sends over the wire, and
 
835
        # how it reacts to responses from the wire.  It instead relies partly
 
836
        # on asserting that the RemoteBranch will call
 
837
        # self._real_branch.set_last_revision_info(...).
 
838
 
 
839
        # First, set up our RemoteBranch with a FakeClient that raises
 
840
        # UnknownSmartMethod, and a StubRealBranch that logs how it is called.
 
841
        transport = MemoryTransport()
 
842
        transport.mkdir('branch')
 
843
        transport = transport.clone('branch')
 
844
        client = FakeClient(transport.base)
 
845
        client.add_expected_call(
 
846
            'Branch.get_stacked_on_url', ('branch/',),
 
847
            'error', ('NotStacked',))
 
848
        client.add_expected_call(
 
849
            'Branch.set_last_revision_info',
 
850
            ('branch/', 'branch token', 'repo token', '1234', 'a-revision-id',),
 
851
            'unknown', 'Branch.set_last_revision_info')
 
852
 
 
853
        branch = self.make_remote_branch(transport, client)
 
854
        class StubRealBranch(object):
 
855
            def __init__(self):
 
856
                self.calls = []
 
857
            def set_last_revision_info(self, revno, revision_id):
 
858
                self.calls.append(
 
859
                    ('set_last_revision_info', revno, revision_id))
 
860
            def _clear_cached_state(self):
 
861
                pass
 
862
        real_branch = StubRealBranch()
 
863
        branch._real_branch = real_branch
 
864
        self.lock_remote_branch(branch)
 
865
 
 
866
        # Call set_last_revision_info, and verify it behaved as expected.
 
867
        result = branch.set_last_revision_info(1234, 'a-revision-id')
 
868
        self.assertEqual(
 
869
            [('set_last_revision_info', 1234, 'a-revision-id')],
 
870
            real_branch.calls)
 
871
        client.finished_test()
 
872
 
 
873
    def test_unexpected_error(self):
 
874
        # If the server sends an error the client doesn't understand, it gets
 
875
        # turned into an UnknownErrorFromSmartServer, which is presented as a
 
876
        # non-internal error to the user.
 
877
        transport = MemoryTransport()
 
878
        transport.mkdir('branch')
 
879
        transport = transport.clone('branch')
 
880
        client = FakeClient(transport.base)
 
881
        # get_stacked_on_url
 
882
        client.add_error_response('NotStacked')
 
883
        # lock_write
 
884
        client.add_success_response('ok', 'branch token', 'repo token')
 
885
        # set_last_revision
 
886
        client.add_error_response('UnexpectedError')
 
887
        # unlock
 
888
        client.add_success_response('ok')
 
889
 
 
890
        branch = self.make_remote_branch(transport, client)
 
891
        # Lock the branch, reset the record of remote calls.
 
892
        branch.lock_write()
 
893
        client._calls = []
 
894
 
 
895
        err = self.assertRaises(
 
896
            errors.UnknownErrorFromSmartServer,
 
897
            branch.set_last_revision_info, 123, 'revid')
 
898
        self.assertEqual(('UnexpectedError',), err.error_tuple)
 
899
        branch.unlock()
 
900
 
 
901
    def test_tip_change_rejected(self):
 
902
        """TipChangeRejected responses cause a TipChangeRejected exception to
 
903
        be raised.
 
904
        """
 
905
        transport = MemoryTransport()
 
906
        transport.mkdir('branch')
 
907
        transport = transport.clone('branch')
 
908
        client = FakeClient(transport.base)
 
909
        # get_stacked_on_url
 
910
        client.add_error_response('NotStacked')
 
911
        # lock_write
 
912
        client.add_success_response('ok', 'branch token', 'repo token')
 
913
        # set_last_revision
 
914
        client.add_error_response('TipChangeRejected', 'rejection message')
 
915
        # unlock
 
916
        client.add_success_response('ok')
 
917
 
 
918
        branch = self.make_remote_branch(transport, client)
 
919
        # Lock the branch, reset the record of remote calls.
 
920
        branch.lock_write()
 
921
        self.addCleanup(branch.unlock)
 
922
        client._calls = []
 
923
 
 
924
        # The 'TipChangeRejected' error response triggered by calling
 
925
        # set_last_revision_info causes a TipChangeRejected exception.
 
926
        err = self.assertRaises(
 
927
            errors.TipChangeRejected,
 
928
            branch.set_last_revision_info, 123, 'revid')
 
929
        self.assertEqual('rejection message', err.msg)
 
930
 
 
931
 
 
932
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
 
933
    """Getting the branch configuration should use an abstract method not vfs.
 
934
    """
 
935
 
 
936
    def test_get_branch_conf(self):
 
937
        raise tests.KnownFailure('branch.conf is not retrieved by get_config_file')
 
938
        ## # We should see that branch.get_config() does a single rpc to get the
 
939
        ## # remote configuration file, abstracting away where that is stored on
 
940
        ## # the server.  However at the moment it always falls back to using the
 
941
        ## # vfs, and this would need some changes in config.py.
 
942
 
 
943
        ## # in an empty branch we decode the response properly
 
944
        ## client = FakeClient([(('ok', ), '# config file body')], self.get_url())
 
945
        ## # we need to make a real branch because the remote_branch.control_files
 
946
        ## # will trigger _ensure_real.
 
947
        ## branch = self.make_branch('quack')
 
948
        ## transport = branch.bzrdir.root_transport
 
949
        ## # we do not want bzrdir to make any remote calls
 
950
        ## bzrdir = RemoteBzrDir(transport, _client=False)
 
951
        ## branch = RemoteBranch(bzrdir, None, _client=client)
 
952
        ## config = branch.get_config()
 
953
        ## self.assertEqual(
 
954
        ##     [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
 
955
        ##     client._calls)
 
956
 
 
957
 
 
958
class TestBranchLockWrite(RemoteBranchTestCase):
 
959
 
 
960
    def test_lock_write_unlockable(self):
 
961
        transport = MemoryTransport()
 
962
        client = FakeClient(transport.base)
 
963
        client.add_expected_call(
 
964
            'Branch.get_stacked_on_url', ('quack/',),
 
965
            'error', ('NotStacked',),)
 
966
        client.add_expected_call(
 
967
            'Branch.lock_write', ('quack/', '', ''),
 
968
            'error', ('UnlockableTransport',))
 
969
        transport.mkdir('quack')
 
970
        transport = transport.clone('quack')
 
971
        branch = self.make_remote_branch(transport, client)
 
972
        self.assertRaises(errors.UnlockableTransport, branch.lock_write)
 
973
        client.finished_test()
 
974
 
 
975
 
 
976
class TestTransportIsReadonly(tests.TestCase):
 
977
 
 
978
    def test_true(self):
 
979
        client = FakeClient()
 
980
        client.add_success_response('yes')
 
981
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
982
                                    _client=client)
 
983
        self.assertEqual(True, transport.is_readonly())
 
984
        self.assertEqual(
 
985
            [('call', 'Transport.is_readonly', ())],
 
986
            client._calls)
 
987
 
 
988
    def test_false(self):
 
989
        client = FakeClient()
 
990
        client.add_success_response('no')
 
991
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
992
                                    _client=client)
 
993
        self.assertEqual(False, transport.is_readonly())
 
994
        self.assertEqual(
 
995
            [('call', 'Transport.is_readonly', ())],
 
996
            client._calls)
 
997
 
 
998
    def test_error_from_old_server(self):
 
999
        """bzr 0.15 and earlier servers don't recognise the is_readonly verb.
 
1000
        
 
1001
        Clients should treat it as a "no" response, because is_readonly is only
 
1002
        advisory anyway (a transport could be read-write, but then the
 
1003
        underlying filesystem could be readonly anyway).
 
1004
        """
 
1005
        client = FakeClient()
 
1006
        client.add_unknown_method_response('Transport.is_readonly')
 
1007
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
1008
                                    _client=client)
 
1009
        self.assertEqual(False, transport.is_readonly())
 
1010
        self.assertEqual(
 
1011
            [('call', 'Transport.is_readonly', ())],
 
1012
            client._calls)
 
1013
 
 
1014
 
 
1015
class TestRemoteSSHTransportAuthentication(tests.TestCaseInTempDir):
 
1016
 
 
1017
    def test_defaults_to_none(self):
 
1018
        t = RemoteSSHTransport('bzr+ssh://example.com')
 
1019
        self.assertIs(None, t._get_credentials()[0])
 
1020
 
 
1021
    def test_uses_authentication_config(self):
 
1022
        conf = config.AuthenticationConfig()
 
1023
        conf._get_config().update(
 
1024
            {'bzr+sshtest': {'scheme': 'ssh', 'user': 'bar', 'host':
 
1025
            'example.com'}})
 
1026
        conf._save()
 
1027
        t = RemoteSSHTransport('bzr+ssh://example.com')
 
1028
        self.assertEqual('bar', t._get_credentials()[0])
 
1029
 
 
1030
 
 
1031
class TestRemoteRepository(tests.TestCase):
 
1032
    """Base for testing RemoteRepository protocol usage.
 
1033
    
 
1034
    These tests contain frozen requests and responses.  We want any changes to 
 
1035
    what is sent or expected to be require a thoughtful update to these tests
 
1036
    because they might break compatibility with different-versioned servers.
 
1037
    """
 
1038
 
 
1039
    def setup_fake_client_and_repository(self, transport_path):
 
1040
        """Create the fake client and repository for testing with.
 
1041
        
 
1042
        There's no real server here; we just have canned responses sent
 
1043
        back one by one.
 
1044
        
 
1045
        :param transport_path: Path below the root of the MemoryTransport
 
1046
            where the repository will be created.
 
1047
        """
 
1048
        transport = MemoryTransport()
 
1049
        transport.mkdir(transport_path)
 
1050
        client = FakeClient(transport.base)
 
1051
        transport = transport.clone(transport_path)
 
1052
        # we do not want bzrdir to make any remote calls
 
1053
        bzrdir = RemoteBzrDir(transport, _client=False)
 
1054
        repo = RemoteRepository(bzrdir, None, _client=client)
 
1055
        return repo, client
 
1056
 
 
1057
 
 
1058
class TestRepositoryGatherStats(TestRemoteRepository):
 
1059
 
 
1060
    def test_revid_none(self):
 
1061
        # ('ok',), body with revisions and size
 
1062
        transport_path = 'quack'
 
1063
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1064
        client.add_success_response_with_body(
 
1065
            'revisions: 2\nsize: 18\n', 'ok')
 
1066
        result = repo.gather_stats(None)
 
1067
        self.assertEqual(
 
1068
            [('call_expecting_body', 'Repository.gather_stats',
 
1069
             ('quack/','','no'))],
 
1070
            client._calls)
 
1071
        self.assertEqual({'revisions': 2, 'size': 18}, result)
 
1072
 
 
1073
    def test_revid_no_committers(self):
 
1074
        # ('ok',), body without committers
 
1075
        body = ('firstrev: 123456.300 3600\n'
 
1076
                'latestrev: 654231.400 0\n'
 
1077
                'revisions: 2\n'
 
1078
                'size: 18\n')
 
1079
        transport_path = 'quick'
 
1080
        revid = u'\xc8'.encode('utf8')
 
1081
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1082
        client.add_success_response_with_body(body, 'ok')
 
1083
        result = repo.gather_stats(revid)
 
1084
        self.assertEqual(
 
1085
            [('call_expecting_body', 'Repository.gather_stats',
 
1086
              ('quick/', revid, 'no'))],
 
1087
            client._calls)
 
1088
        self.assertEqual({'revisions': 2, 'size': 18,
 
1089
                          'firstrev': (123456.300, 3600),
 
1090
                          'latestrev': (654231.400, 0),},
 
1091
                         result)
 
1092
 
 
1093
    def test_revid_with_committers(self):
 
1094
        # ('ok',), body with committers
 
1095
        body = ('committers: 128\n'
 
1096
                'firstrev: 123456.300 3600\n'
 
1097
                'latestrev: 654231.400 0\n'
 
1098
                'revisions: 2\n'
 
1099
                'size: 18\n')
 
1100
        transport_path = 'buick'
 
1101
        revid = u'\xc8'.encode('utf8')
 
1102
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1103
        client.add_success_response_with_body(body, 'ok')
 
1104
        result = repo.gather_stats(revid, True)
 
1105
        self.assertEqual(
 
1106
            [('call_expecting_body', 'Repository.gather_stats',
 
1107
              ('buick/', revid, 'yes'))],
 
1108
            client._calls)
 
1109
        self.assertEqual({'revisions': 2, 'size': 18,
 
1110
                          'committers': 128,
 
1111
                          'firstrev': (123456.300, 3600),
 
1112
                          'latestrev': (654231.400, 0),},
 
1113
                         result)
 
1114
 
 
1115
 
 
1116
class TestRepositoryGetGraph(TestRemoteRepository):
 
1117
 
 
1118
    def test_get_graph(self):
 
1119
        # get_graph returns a graph with a custom parents provider.
 
1120
        transport_path = 'quack'
 
1121
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1122
        graph = repo.get_graph()
 
1123
        self.assertNotEqual(graph._parents_provider, repo)
 
1124
 
 
1125
 
 
1126
class TestRepositoryGetParentMap(TestRemoteRepository):
 
1127
 
 
1128
    def test_get_parent_map_caching(self):
 
1129
        # get_parent_map returns from cache until unlock()
 
1130
        # setup a reponse with two revisions
 
1131
        r1 = u'\u0e33'.encode('utf8')
 
1132
        r2 = u'\u0dab'.encode('utf8')
 
1133
        lines = [' '.join([r2, r1]), r1]
 
1134
        encoded_body = bz2.compress('\n'.join(lines))
 
1135
 
 
1136
        transport_path = 'quack'
 
1137
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1138
        client.add_success_response_with_body(encoded_body, 'ok')
 
1139
        client.add_success_response_with_body(encoded_body, 'ok')
 
1140
        repo.lock_read()
 
1141
        graph = repo.get_graph()
 
1142
        parents = graph.get_parent_map([r2])
 
1143
        self.assertEqual({r2: (r1,)}, parents)
 
1144
        # locking and unlocking deeper should not reset
 
1145
        repo.lock_read()
 
1146
        repo.unlock()
 
1147
        parents = graph.get_parent_map([r1])
 
1148
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
1149
        self.assertEqual(
 
1150
            [('call_with_body_bytes_expecting_body',
 
1151
              'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
 
1152
            client._calls)
 
1153
        repo.unlock()
 
1154
        # now we call again, and it should use the second response.
 
1155
        repo.lock_read()
 
1156
        graph = repo.get_graph()
 
1157
        parents = graph.get_parent_map([r1])
 
1158
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
1159
        self.assertEqual(
 
1160
            [('call_with_body_bytes_expecting_body',
 
1161
              'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
 
1162
             ('call_with_body_bytes_expecting_body',
 
1163
              'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
 
1164
            ],
 
1165
            client._calls)
 
1166
        repo.unlock()
 
1167
 
 
1168
    def test_get_parent_map_reconnects_if_unknown_method(self):
 
1169
        transport_path = 'quack'
 
1170
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1171
        client.add_unknown_method_response('Repository,get_parent_map')
 
1172
        client.add_success_response_with_body('', 'ok')
 
1173
        self.assertFalse(client._medium._is_remote_before((1, 2)))
 
1174
        rev_id = 'revision-id'
 
1175
        expected_deprecations = [
 
1176
            'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
 
1177
            'in version 1.4.']
 
1178
        parents = self.callDeprecated(
 
1179
            expected_deprecations, repo.get_parent_map, [rev_id])
 
1180
        self.assertEqual(
 
1181
            [('call_with_body_bytes_expecting_body',
 
1182
              'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
 
1183
             ('disconnect medium',),
 
1184
             ('call_expecting_body', 'Repository.get_revision_graph',
 
1185
              ('quack/', ''))],
 
1186
            client._calls)
 
1187
        # The medium is now marked as being connected to an older server
 
1188
        self.assertTrue(client._medium._is_remote_before((1, 2)))
 
1189
 
 
1190
    def test_get_parent_map_fallback_parentless_node(self):
 
1191
        """get_parent_map falls back to get_revision_graph on old servers.  The
 
1192
        results from get_revision_graph are tweaked to match the get_parent_map
 
1193
        API.
 
1194
 
 
1195
        Specifically, a {key: ()} result from get_revision_graph means "no
 
1196
        parents" for that key, which in get_parent_map results should be
 
1197
        represented as {key: ('null:',)}.
 
1198
 
 
1199
        This is the test for https://bugs.launchpad.net/bzr/+bug/214894
 
1200
        """
 
1201
        rev_id = 'revision-id'
 
1202
        transport_path = 'quack'
 
1203
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1204
        client.add_success_response_with_body(rev_id, 'ok')
 
1205
        client._medium._remember_remote_is_before((1, 2))
 
1206
        expected_deprecations = [
 
1207
            'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
 
1208
            'in version 1.4.']
 
1209
        parents = self.callDeprecated(
 
1210
            expected_deprecations, repo.get_parent_map, [rev_id])
 
1211
        self.assertEqual(
 
1212
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1213
             ('quack/', ''))],
 
1214
            client._calls)
 
1215
        self.assertEqual({rev_id: ('null:',)}, parents)
 
1216
 
 
1217
    def test_get_parent_map_unexpected_response(self):
 
1218
        repo, client = self.setup_fake_client_and_repository('path')
 
1219
        client.add_success_response('something unexpected!')
 
1220
        self.assertRaises(
 
1221
            errors.UnexpectedSmartServerResponse,
 
1222
            repo.get_parent_map, ['a-revision-id'])
 
1223
 
 
1224
 
 
1225
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
 
1226
    
 
1227
    def test_null_revision(self):
 
1228
        # a null revision has the predictable result {}, we should have no wire
 
1229
        # traffic when calling it with this argument
 
1230
        transport_path = 'empty'
 
1231
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1232
        client.add_success_response('notused')
 
1233
        result = self.applyDeprecated(one_four, repo.get_revision_graph,
 
1234
            NULL_REVISION)
 
1235
        self.assertEqual([], client._calls)
 
1236
        self.assertEqual({}, result)
 
1237
 
 
1238
    def test_none_revision(self):
 
1239
        # with none we want the entire graph
 
1240
        r1 = u'\u0e33'.encode('utf8')
 
1241
        r2 = u'\u0dab'.encode('utf8')
 
1242
        lines = [' '.join([r2, r1]), r1]
 
1243
        encoded_body = '\n'.join(lines)
 
1244
 
 
1245
        transport_path = 'sinhala'
 
1246
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1247
        client.add_success_response_with_body(encoded_body, 'ok')
 
1248
        result = self.applyDeprecated(one_four, repo.get_revision_graph)
 
1249
        self.assertEqual(
 
1250
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1251
             ('sinhala/', ''))],
 
1252
            client._calls)
 
1253
        self.assertEqual({r1: (), r2: (r1, )}, result)
 
1254
 
 
1255
    def test_specific_revision(self):
 
1256
        # with a specific revision we want the graph for that
 
1257
        # with none we want the entire graph
 
1258
        r11 = u'\u0e33'.encode('utf8')
 
1259
        r12 = u'\xc9'.encode('utf8')
 
1260
        r2 = u'\u0dab'.encode('utf8')
 
1261
        lines = [' '.join([r2, r11, r12]), r11, r12]
 
1262
        encoded_body = '\n'.join(lines)
 
1263
 
 
1264
        transport_path = 'sinhala'
 
1265
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1266
        client.add_success_response_with_body(encoded_body, 'ok')
 
1267
        result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
 
1268
        self.assertEqual(
 
1269
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1270
             ('sinhala/', r2))],
 
1271
            client._calls)
 
1272
        self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
 
1273
 
 
1274
    def test_no_such_revision(self):
 
1275
        revid = '123'
 
1276
        transport_path = 'sinhala'
 
1277
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1278
        client.add_error_response('nosuchrevision', revid)
 
1279
        # also check that the right revision is reported in the error
 
1280
        self.assertRaises(errors.NoSuchRevision,
 
1281
            self.applyDeprecated, one_four, repo.get_revision_graph, revid)
 
1282
        self.assertEqual(
 
1283
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1284
             ('sinhala/', revid))],
 
1285
            client._calls)
 
1286
 
 
1287
    def test_unexpected_error(self):
 
1288
        revid = '123'
 
1289
        transport_path = 'sinhala'
 
1290
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1291
        client.add_error_response('AnUnexpectedError')
 
1292
        e = self.assertRaises(errors.UnknownErrorFromSmartServer,
 
1293
            self.applyDeprecated, one_four, repo.get_revision_graph, revid)
 
1294
        self.assertEqual(('AnUnexpectedError',), e.error_tuple)
 
1295
 
 
1296
        
 
1297
class TestRepositoryIsShared(TestRemoteRepository):
 
1298
 
 
1299
    def test_is_shared(self):
 
1300
        # ('yes', ) for Repository.is_shared -> 'True'.
 
1301
        transport_path = 'quack'
 
1302
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1303
        client.add_success_response('yes')
 
1304
        result = repo.is_shared()
 
1305
        self.assertEqual(
 
1306
            [('call', 'Repository.is_shared', ('quack/',))],
 
1307
            client._calls)
 
1308
        self.assertEqual(True, result)
 
1309
 
 
1310
    def test_is_not_shared(self):
 
1311
        # ('no', ) for Repository.is_shared -> 'False'.
 
1312
        transport_path = 'qwack'
 
1313
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1314
        client.add_success_response('no')
 
1315
        result = repo.is_shared()
 
1316
        self.assertEqual(
 
1317
            [('call', 'Repository.is_shared', ('qwack/',))],
 
1318
            client._calls)
 
1319
        self.assertEqual(False, result)
 
1320
 
 
1321
 
 
1322
class TestRepositoryLockWrite(TestRemoteRepository):
 
1323
 
 
1324
    def test_lock_write(self):
 
1325
        transport_path = 'quack'
 
1326
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1327
        client.add_success_response('ok', 'a token')
 
1328
        result = repo.lock_write()
 
1329
        self.assertEqual(
 
1330
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
1331
            client._calls)
 
1332
        self.assertEqual('a token', result)
 
1333
 
 
1334
    def test_lock_write_already_locked(self):
 
1335
        transport_path = 'quack'
 
1336
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1337
        client.add_error_response('LockContention')
 
1338
        self.assertRaises(errors.LockContention, repo.lock_write)
 
1339
        self.assertEqual(
 
1340
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
1341
            client._calls)
 
1342
 
 
1343
    def test_lock_write_unlockable(self):
 
1344
        transport_path = 'quack'
 
1345
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1346
        client.add_error_response('UnlockableTransport')
 
1347
        self.assertRaises(errors.UnlockableTransport, repo.lock_write)
 
1348
        self.assertEqual(
 
1349
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
1350
            client._calls)
 
1351
 
 
1352
 
 
1353
class TestRepositoryUnlock(TestRemoteRepository):
 
1354
 
 
1355
    def test_unlock(self):
 
1356
        transport_path = 'quack'
 
1357
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1358
        client.add_success_response('ok', 'a token')
 
1359
        client.add_success_response('ok')
 
1360
        repo.lock_write()
 
1361
        repo.unlock()
 
1362
        self.assertEqual(
 
1363
            [('call', 'Repository.lock_write', ('quack/', '')),
 
1364
             ('call', 'Repository.unlock', ('quack/', 'a token'))],
 
1365
            client._calls)
 
1366
 
 
1367
    def test_unlock_wrong_token(self):
 
1368
        # If somehow the token is wrong, unlock will raise TokenMismatch.
 
1369
        transport_path = 'quack'
 
1370
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1371
        client.add_success_response('ok', 'a token')
 
1372
        client.add_error_response('TokenMismatch')
 
1373
        repo.lock_write()
 
1374
        self.assertRaises(errors.TokenMismatch, repo.unlock)
 
1375
 
 
1376
 
 
1377
class TestRepositoryHasRevision(TestRemoteRepository):
 
1378
 
 
1379
    def test_none(self):
 
1380
        # repo.has_revision(None) should not cause any traffic.
 
1381
        transport_path = 'quack'
 
1382
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1383
 
 
1384
        # The null revision is always there, so has_revision(None) == True.
 
1385
        self.assertEqual(True, repo.has_revision(NULL_REVISION))
 
1386
 
 
1387
        # The remote repo shouldn't be accessed.
 
1388
        self.assertEqual([], client._calls)
 
1389
 
 
1390
 
 
1391
class TestRepositoryTarball(TestRemoteRepository):
 
1392
 
 
1393
    # This is a canned tarball reponse we can validate against
 
1394
    tarball_content = (
 
1395
        'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
 
1396
        'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
 
1397
        'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
 
1398
        'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
 
1399
        'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
 
1400
        'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
 
1401
        'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
 
1402
        'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
 
1403
        '0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
 
1404
        'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
 
1405
        'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
 
1406
        'nWQ7QH/F3JFOFCQ0aSPfA='
 
1407
        ).decode('base64')
 
1408
 
 
1409
    def test_repository_tarball(self):
 
1410
        # Test that Repository.tarball generates the right operations
 
1411
        transport_path = 'repo'
 
1412
        expected_calls = [('call_expecting_body', 'Repository.tarball',
 
1413
                           ('repo/', 'bz2',),),
 
1414
            ]
 
1415
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1416
        client.add_success_response_with_body(self.tarball_content, 'ok')
 
1417
        # Now actually ask for the tarball
 
1418
        tarball_file = repo._get_tarball('bz2')
 
1419
        try:
 
1420
            self.assertEqual(expected_calls, client._calls)
 
1421
            self.assertEqual(self.tarball_content, tarball_file.read())
 
1422
        finally:
 
1423
            tarball_file.close()
 
1424
 
 
1425
 
 
1426
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
 
1427
    """RemoteRepository.copy_content_into optimizations"""
 
1428
 
 
1429
    def test_copy_content_remote_to_local(self):
 
1430
        self.transport_server = server.SmartTCPServer_for_testing
 
1431
        src_repo = self.make_repository('repo1')
 
1432
        src_repo = repository.Repository.open(self.get_url('repo1'))
 
1433
        # At the moment the tarball-based copy_content_into can't write back
 
1434
        # into a smart server.  It would be good if it could upload the
 
1435
        # tarball; once that works we'd have to create repositories of
 
1436
        # different formats. -- mbp 20070410
 
1437
        dest_url = self.get_vfs_only_url('repo2')
 
1438
        dest_bzrdir = BzrDir.create(dest_url)
 
1439
        dest_repo = dest_bzrdir.create_repository()
 
1440
        self.assertFalse(isinstance(dest_repo, RemoteRepository))
 
1441
        self.assertTrue(isinstance(src_repo, RemoteRepository))
 
1442
        src_repo.copy_content_into(dest_repo)
 
1443
 
 
1444
 
 
1445
class _StubRealPackRepository(object):
 
1446
 
 
1447
    def __init__(self, calls):
 
1448
        self._pack_collection = _StubPackCollection(calls)
 
1449
 
 
1450
 
 
1451
class _StubPackCollection(object):
 
1452
 
 
1453
    def __init__(self, calls):
 
1454
        self.calls = calls
 
1455
 
 
1456
    def autopack(self):
 
1457
        self.calls.append(('pack collection autopack',))
 
1458
 
 
1459
    def reload_pack_names(self):
 
1460
        self.calls.append(('pack collection reload_pack_names',))
 
1461
 
 
1462
    
 
1463
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
 
1464
    """Tests for RemoteRepository.autopack implementation."""
 
1465
 
 
1466
    def test_ok(self):
 
1467
        """When the server returns 'ok' and there's no _real_repository, then
 
1468
        nothing else happens: the autopack method is done.
 
1469
        """
 
1470
        transport_path = 'quack'
 
1471
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1472
        client.add_expected_call(
 
1473
            'PackRepository.autopack', ('quack/',), 'success', ('ok',))
 
1474
        repo.autopack()
 
1475
        client.finished_test()
 
1476
 
 
1477
    def test_ok_with_real_repo(self):
 
1478
        """When the server returns 'ok' and there is a _real_repository, then
 
1479
        the _real_repository's reload_pack_name's method will be called.
 
1480
        """
 
1481
        transport_path = 'quack'
 
1482
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1483
        client.add_expected_call(
 
1484
            'PackRepository.autopack', ('quack/',),
 
1485
            'success', ('ok',))
 
1486
        repo._real_repository = _StubRealPackRepository(client._calls)
 
1487
        repo.autopack()
 
1488
        self.assertEqual(
 
1489
            [('call', 'PackRepository.autopack', ('quack/',)),
 
1490
             ('pack collection reload_pack_names',)],
 
1491
            client._calls)
 
1492
        
 
1493
    def test_backwards_compatibility(self):
 
1494
        """If the server does not recognise the PackRepository.autopack verb,
 
1495
        fallback to the real_repository's implementation.
 
1496
        """
 
1497
        transport_path = 'quack'
 
1498
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1499
        client.add_unknown_method_response('PackRepository.autopack')
 
1500
        def stub_ensure_real():
 
1501
            client._calls.append(('_ensure_real',))
 
1502
            repo._real_repository = _StubRealPackRepository(client._calls)
 
1503
        repo._ensure_real = stub_ensure_real
 
1504
        repo.autopack()
 
1505
        self.assertEqual(
 
1506
            [('call', 'PackRepository.autopack', ('quack/',)),
 
1507
             ('_ensure_real',),
 
1508
             ('pack collection autopack',)],
 
1509
            client._calls)
 
1510
 
 
1511
 
 
1512
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
 
1513
    """Base class for unit tests for bzrlib.remote._translate_error."""
 
1514
 
 
1515
    def translateTuple(self, error_tuple, **context):
 
1516
        """Call _translate_error with an ErrorFromSmartServer built from the
 
1517
        given error_tuple.
 
1518
 
 
1519
        :param error_tuple: A tuple of a smart server response, as would be
 
1520
            passed to an ErrorFromSmartServer.
 
1521
        :kwargs context: context items to call _translate_error with.
 
1522
 
 
1523
        :returns: The error raised by _translate_error.
 
1524
        """
 
1525
        # Raise the ErrorFromSmartServer before passing it as an argument,
 
1526
        # because _translate_error may need to re-raise it with a bare 'raise'
 
1527
        # statement.
 
1528
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1529
        translated_error = self.translateErrorFromSmartServer(
 
1530
            server_error, **context)
 
1531
        return translated_error
 
1532
 
 
1533
    def translateErrorFromSmartServer(self, error_object, **context):
 
1534
        """Like translateTuple, but takes an already constructed
 
1535
        ErrorFromSmartServer rather than a tuple.
 
1536
        """
 
1537
        try:
 
1538
            raise error_object
 
1539
        except errors.ErrorFromSmartServer, server_error:
 
1540
            translated_error = self.assertRaises(
 
1541
                errors.BzrError, remote._translate_error, server_error,
 
1542
                **context)
 
1543
        return translated_error
 
1544
 
 
1545
 
 
1546
class TestErrorTranslationSuccess(TestErrorTranslationBase):
 
1547
    """Unit tests for bzrlib.remote._translate_error.
 
1548
    
 
1549
    Given an ErrorFromSmartServer (which has an error tuple from a smart
 
1550
    server) and some context, _translate_error raises more specific errors from
 
1551
    bzrlib.errors.
 
1552
 
 
1553
    This test case covers the cases where _translate_error succeeds in
 
1554
    translating an ErrorFromSmartServer to something better.  See
 
1555
    TestErrorTranslationRobustness for other cases.
 
1556
    """
 
1557
 
 
1558
    def test_NoSuchRevision(self):
 
1559
        branch = self.make_branch('')
 
1560
        revid = 'revid'
 
1561
        translated_error = self.translateTuple(
 
1562
            ('NoSuchRevision', revid), branch=branch)
 
1563
        expected_error = errors.NoSuchRevision(branch, revid)
 
1564
        self.assertEqual(expected_error, translated_error)
 
1565
 
 
1566
    def test_nosuchrevision(self):
 
1567
        repository = self.make_repository('')
 
1568
        revid = 'revid'
 
1569
        translated_error = self.translateTuple(
 
1570
            ('nosuchrevision', revid), repository=repository)
 
1571
        expected_error = errors.NoSuchRevision(repository, revid)
 
1572
        self.assertEqual(expected_error, translated_error)
 
1573
 
 
1574
    def test_nobranch(self):
 
1575
        bzrdir = self.make_bzrdir('')
 
1576
        translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
 
1577
        expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
 
1578
        self.assertEqual(expected_error, translated_error)
 
1579
 
 
1580
    def test_LockContention(self):
 
1581
        translated_error = self.translateTuple(('LockContention',))
 
1582
        expected_error = errors.LockContention('(remote lock)')
 
1583
        self.assertEqual(expected_error, translated_error)
 
1584
 
 
1585
    def test_UnlockableTransport(self):
 
1586
        bzrdir = self.make_bzrdir('')
 
1587
        translated_error = self.translateTuple(
 
1588
            ('UnlockableTransport',), bzrdir=bzrdir)
 
1589
        expected_error = errors.UnlockableTransport(bzrdir.root_transport)
 
1590
        self.assertEqual(expected_error, translated_error)
 
1591
 
 
1592
    def test_LockFailed(self):
 
1593
        lock = 'str() of a server lock'
 
1594
        why = 'str() of why'
 
1595
        translated_error = self.translateTuple(('LockFailed', lock, why))
 
1596
        expected_error = errors.LockFailed(lock, why)
 
1597
        self.assertEqual(expected_error, translated_error)
 
1598
 
 
1599
    def test_TokenMismatch(self):
 
1600
        token = 'a lock token'
 
1601
        translated_error = self.translateTuple(('TokenMismatch',), token=token)
 
1602
        expected_error = errors.TokenMismatch(token, '(remote token)')
 
1603
        self.assertEqual(expected_error, translated_error)
 
1604
 
 
1605
    def test_Diverged(self):
 
1606
        branch = self.make_branch('a')
 
1607
        other_branch = self.make_branch('b')
 
1608
        translated_error = self.translateTuple(
 
1609
            ('Diverged',), branch=branch, other_branch=other_branch)
 
1610
        expected_error = errors.DivergedBranches(branch, other_branch)
 
1611
        self.assertEqual(expected_error, translated_error)
 
1612
 
 
1613
    def test_ReadError_no_args(self):
 
1614
        path = 'a path'
 
1615
        translated_error = self.translateTuple(('ReadError',), path=path)
 
1616
        expected_error = errors.ReadError(path)
 
1617
        self.assertEqual(expected_error, translated_error)
 
1618
 
 
1619
    def test_ReadError(self):
 
1620
        path = 'a path'
 
1621
        translated_error = self.translateTuple(('ReadError', path))
 
1622
        expected_error = errors.ReadError(path)
 
1623
        self.assertEqual(expected_error, translated_error)
 
1624
 
 
1625
    def test_PermissionDenied_no_args(self):
 
1626
        path = 'a path'
 
1627
        translated_error = self.translateTuple(('PermissionDenied',), path=path)
 
1628
        expected_error = errors.PermissionDenied(path)
 
1629
        self.assertEqual(expected_error, translated_error)
 
1630
 
 
1631
    def test_PermissionDenied_one_arg(self):
 
1632
        path = 'a path'
 
1633
        translated_error = self.translateTuple(('PermissionDenied', path))
 
1634
        expected_error = errors.PermissionDenied(path)
 
1635
        self.assertEqual(expected_error, translated_error)
 
1636
 
 
1637
    def test_PermissionDenied_one_arg_and_context(self):
 
1638
        """Given a choice between a path from the local context and a path on
 
1639
        the wire, _translate_error prefers the path from the local context.
 
1640
        """
 
1641
        local_path = 'local path'
 
1642
        remote_path = 'remote path'
 
1643
        translated_error = self.translateTuple(
 
1644
            ('PermissionDenied', remote_path), path=local_path)
 
1645
        expected_error = errors.PermissionDenied(local_path)
 
1646
        self.assertEqual(expected_error, translated_error)
 
1647
 
 
1648
    def test_PermissionDenied_two_args(self):
 
1649
        path = 'a path'
 
1650
        extra = 'a string with extra info'
 
1651
        translated_error = self.translateTuple(
 
1652
            ('PermissionDenied', path, extra))
 
1653
        expected_error = errors.PermissionDenied(path, extra)
 
1654
        self.assertEqual(expected_error, translated_error)
 
1655
 
 
1656
 
 
1657
class TestErrorTranslationRobustness(TestErrorTranslationBase):
 
1658
    """Unit tests for bzrlib.remote._translate_error's robustness.
 
1659
    
 
1660
    TestErrorTranslationSuccess is for cases where _translate_error can
 
1661
    translate successfully.  This class about how _translate_err behaves when
 
1662
    it fails to translate: it re-raises the original error.
 
1663
    """
 
1664
 
 
1665
    def test_unrecognised_server_error(self):
 
1666
        """If the error code from the server is not recognised, the original
 
1667
        ErrorFromSmartServer is propagated unmodified.
 
1668
        """
 
1669
        error_tuple = ('An unknown error tuple',)
 
1670
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1671
        translated_error = self.translateErrorFromSmartServer(server_error)
 
1672
        expected_error = errors.UnknownErrorFromSmartServer(server_error)
 
1673
        self.assertEqual(expected_error, translated_error)
 
1674
 
 
1675
    def test_context_missing_a_key(self):
 
1676
        """In case of a bug in the client, or perhaps an unexpected response
 
1677
        from a server, _translate_error returns the original error tuple from
 
1678
        the server and mutters a warning.
 
1679
        """
 
1680
        # To translate a NoSuchRevision error _translate_error needs a 'branch'
 
1681
        # in the context dict.  So let's give it an empty context dict instead
 
1682
        # to exercise its error recovery.
 
1683
        empty_context = {}
 
1684
        error_tuple = ('NoSuchRevision', 'revid')
 
1685
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1686
        translated_error = self.translateErrorFromSmartServer(server_error)
 
1687
        self.assertEqual(server_error, translated_error)
 
1688
        # In addition to re-raising ErrorFromSmartServer, some debug info has
 
1689
        # been muttered to the log file for developer to look at.
 
1690
        self.assertContainsRe(
 
1691
            self._get_log(keep_log_file=True),
 
1692
            "Missing key 'branch' in context")
 
1693
        
 
1694
    def test_path_missing(self):
 
1695
        """Some translations (PermissionDenied, ReadError) can determine the
 
1696
        'path' variable from either the wire or the local context.  If neither
 
1697
        has it, then an error is raised.
 
1698
        """
 
1699
        error_tuple = ('ReadError',)
 
1700
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1701
        translated_error = self.translateErrorFromSmartServer(server_error)
 
1702
        self.assertEqual(server_error, translated_error)
 
1703
        # In addition to re-raising ErrorFromSmartServer, some debug info has
 
1704
        # been muttered to the log file for developer to look at.
 
1705
        self.assertContainsRe(
 
1706
            self._get_log(keep_log_file=True), "Missing key 'path' in context")
 
1707
 
 
1708
 
 
1709
class TestStacking(tests.TestCaseWithTransport):
 
1710
    """Tests for operations on stacked remote repositories.
 
1711
    
 
1712
    The underlying format type must support stacking.
 
1713
    """
 
1714
 
 
1715
    def test_access_stacked_remote(self):
 
1716
        # based on <http://launchpad.net/bugs/261315>
 
1717
        # make a branch stacked on another repository containing an empty
 
1718
        # revision, then open it over hpss - we should be able to see that
 
1719
        # revision.
 
1720
        base_transport = self.get_transport()
 
1721
        base_builder = self.make_branch_builder('base', format='1.6')
 
1722
        base_builder.start_series()
 
1723
        base_revid = base_builder.build_snapshot('rev-id', None,
 
1724
            [('add', ('', None, 'directory', None))],
 
1725
            'message')
 
1726
        base_builder.finish_series()
 
1727
        stacked_branch = self.make_branch('stacked', format='1.6')
 
1728
        stacked_branch.set_stacked_on_url('../base')
 
1729
        # start a server looking at this
 
1730
        smart_server = server.SmartTCPServer_for_testing()
 
1731
        smart_server.setUp()
 
1732
        self.addCleanup(smart_server.tearDown)
 
1733
        remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
 
1734
        # can get its branch and repository
 
1735
        remote_branch = remote_bzrdir.open_branch()
 
1736
        remote_repo = remote_branch.repository
 
1737
        remote_repo.lock_read()
 
1738
        try:
 
1739
            # it should have an appropriate fallback repository, which should also
 
1740
            # be a RemoteRepository
 
1741
            self.assertEquals(len(remote_repo._fallback_repositories), 1)
 
1742
            self.assertIsInstance(remote_repo._fallback_repositories[0],
 
1743
                RemoteRepository)
 
1744
            # and it has the revision committed to the underlying repository;
 
1745
            # these have varying implementations so we try several of them
 
1746
            self.assertTrue(remote_repo.has_revisions([base_revid]))
 
1747
            self.assertTrue(remote_repo.has_revision(base_revid))
 
1748
            self.assertEqual(remote_repo.get_revision(base_revid).message,
 
1749
                'message')
 
1750
        finally:
 
1751
            remote_repo.unlock()
 
1752
 
 
1753
    def prepare_stacked_remote_branch(self):
 
1754
        smart_server = server.SmartTCPServer_for_testing()
 
1755
        smart_server.setUp()
 
1756
        self.addCleanup(smart_server.tearDown)
 
1757
        tree1 = self.make_branch_and_tree('tree1')
 
1758
        tree1.commit('rev1', rev_id='rev1')
 
1759
        tree2 = self.make_branch_and_tree('tree2', format='1.6')
 
1760
        tree2.branch.set_stacked_on_url(tree1.branch.base)
 
1761
        branch2 = Branch.open(smart_server.get_url() + '/tree2')
 
1762
        branch2.lock_read()
 
1763
        self.addCleanup(branch2.unlock)
 
1764
        return branch2
 
1765
 
 
1766
    def test_stacked_get_parent_map(self):
 
1767
        # the public implementation of get_parent_map obeys stacking
 
1768
        branch = self.prepare_stacked_remote_branch()
 
1769
        repo = branch.repository
 
1770
        self.assertEqual(['rev1'], repo.get_parent_map(['rev1']).keys())
 
1771
 
 
1772
    def test_stacked__get_parent_map(self):
 
1773
        # the private variant of _get_parent_map ignores stacking
 
1774
        branch = self.prepare_stacked_remote_branch()
 
1775
        repo = branch.repository
 
1776
        self.assertEqual([], repo._get_parent_map(['rev1']).keys())