~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_remote.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-10-31 04:39:04 UTC
  • mfrom: (3565.6.16 switch_nick)
  • Revision ID: pqm@pqm.ubuntu.com-20081031043904-52fnbfrloojemvcc
(mbp) branch nickname documentation

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 the repository as the
 
1120
        # parents_provider.
 
1121
        transport_path = 'quack'
 
1122
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1123
        graph = repo.get_graph()
 
1124
        self.assertEqual(graph._parents_provider, repo)
 
1125
 
 
1126
 
 
1127
class TestRepositoryGetParentMap(TestRemoteRepository):
 
1128
 
 
1129
    def test_get_parent_map_caching(self):
 
1130
        # get_parent_map returns from cache until unlock()
 
1131
        # setup a reponse with two revisions
 
1132
        r1 = u'\u0e33'.encode('utf8')
 
1133
        r2 = u'\u0dab'.encode('utf8')
 
1134
        lines = [' '.join([r2, r1]), r1]
 
1135
        encoded_body = bz2.compress('\n'.join(lines))
 
1136
 
 
1137
        transport_path = 'quack'
 
1138
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1139
        client.add_success_response_with_body(encoded_body, 'ok')
 
1140
        client.add_success_response_with_body(encoded_body, 'ok')
 
1141
        repo.lock_read()
 
1142
        graph = repo.get_graph()
 
1143
        parents = graph.get_parent_map([r2])
 
1144
        self.assertEqual({r2: (r1,)}, parents)
 
1145
        # locking and unlocking deeper should not reset
 
1146
        repo.lock_read()
 
1147
        repo.unlock()
 
1148
        parents = graph.get_parent_map([r1])
 
1149
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
1150
        self.assertEqual(
 
1151
            [('call_with_body_bytes_expecting_body',
 
1152
              'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
 
1153
            client._calls)
 
1154
        repo.unlock()
 
1155
        # now we call again, and it should use the second response.
 
1156
        repo.lock_read()
 
1157
        graph = repo.get_graph()
 
1158
        parents = graph.get_parent_map([r1])
 
1159
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
1160
        self.assertEqual(
 
1161
            [('call_with_body_bytes_expecting_body',
 
1162
              'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
 
1163
             ('call_with_body_bytes_expecting_body',
 
1164
              'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
 
1165
            ],
 
1166
            client._calls)
 
1167
        repo.unlock()
 
1168
 
 
1169
    def test_get_parent_map_reconnects_if_unknown_method(self):
 
1170
        transport_path = 'quack'
 
1171
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1172
        client.add_unknown_method_response('Repository,get_parent_map')
 
1173
        client.add_success_response_with_body('', 'ok')
 
1174
        self.assertFalse(client._medium._is_remote_before((1, 2)))
 
1175
        rev_id = 'revision-id'
 
1176
        expected_deprecations = [
 
1177
            'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
 
1178
            'in version 1.4.']
 
1179
        parents = self.callDeprecated(
 
1180
            expected_deprecations, repo.get_parent_map, [rev_id])
 
1181
        self.assertEqual(
 
1182
            [('call_with_body_bytes_expecting_body',
 
1183
              'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
 
1184
             ('disconnect medium',),
 
1185
             ('call_expecting_body', 'Repository.get_revision_graph',
 
1186
              ('quack/', ''))],
 
1187
            client._calls)
 
1188
        # The medium is now marked as being connected to an older server
 
1189
        self.assertTrue(client._medium._is_remote_before((1, 2)))
 
1190
 
 
1191
    def test_get_parent_map_fallback_parentless_node(self):
 
1192
        """get_parent_map falls back to get_revision_graph on old servers.  The
 
1193
        results from get_revision_graph are tweaked to match the get_parent_map
 
1194
        API.
 
1195
 
 
1196
        Specifically, a {key: ()} result from get_revision_graph means "no
 
1197
        parents" for that key, which in get_parent_map results should be
 
1198
        represented as {key: ('null:',)}.
 
1199
 
 
1200
        This is the test for https://bugs.launchpad.net/bzr/+bug/214894
 
1201
        """
 
1202
        rev_id = 'revision-id'
 
1203
        transport_path = 'quack'
 
1204
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1205
        client.add_success_response_with_body(rev_id, 'ok')
 
1206
        client._medium._remember_remote_is_before((1, 2))
 
1207
        expected_deprecations = [
 
1208
            'bzrlib.remote.RemoteRepository.get_revision_graph was deprecated '
 
1209
            'in version 1.4.']
 
1210
        parents = self.callDeprecated(
 
1211
            expected_deprecations, repo.get_parent_map, [rev_id])
 
1212
        self.assertEqual(
 
1213
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1214
             ('quack/', ''))],
 
1215
            client._calls)
 
1216
        self.assertEqual({rev_id: ('null:',)}, parents)
 
1217
 
 
1218
    def test_get_parent_map_unexpected_response(self):
 
1219
        repo, client = self.setup_fake_client_and_repository('path')
 
1220
        client.add_success_response('something unexpected!')
 
1221
        self.assertRaises(
 
1222
            errors.UnexpectedSmartServerResponse,
 
1223
            repo.get_parent_map, ['a-revision-id'])
 
1224
 
 
1225
 
 
1226
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
 
1227
    
 
1228
    def test_null_revision(self):
 
1229
        # a null revision has the predictable result {}, we should have no wire
 
1230
        # traffic when calling it with this argument
 
1231
        transport_path = 'empty'
 
1232
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1233
        client.add_success_response('notused')
 
1234
        result = self.applyDeprecated(one_four, repo.get_revision_graph,
 
1235
            NULL_REVISION)
 
1236
        self.assertEqual([], client._calls)
 
1237
        self.assertEqual({}, result)
 
1238
 
 
1239
    def test_none_revision(self):
 
1240
        # with none we want the entire graph
 
1241
        r1 = u'\u0e33'.encode('utf8')
 
1242
        r2 = u'\u0dab'.encode('utf8')
 
1243
        lines = [' '.join([r2, r1]), r1]
 
1244
        encoded_body = '\n'.join(lines)
 
1245
 
 
1246
        transport_path = 'sinhala'
 
1247
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1248
        client.add_success_response_with_body(encoded_body, 'ok')
 
1249
        result = self.applyDeprecated(one_four, repo.get_revision_graph)
 
1250
        self.assertEqual(
 
1251
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1252
             ('sinhala/', ''))],
 
1253
            client._calls)
 
1254
        self.assertEqual({r1: (), r2: (r1, )}, result)
 
1255
 
 
1256
    def test_specific_revision(self):
 
1257
        # with a specific revision we want the graph for that
 
1258
        # with none we want the entire graph
 
1259
        r11 = u'\u0e33'.encode('utf8')
 
1260
        r12 = u'\xc9'.encode('utf8')
 
1261
        r2 = u'\u0dab'.encode('utf8')
 
1262
        lines = [' '.join([r2, r11, r12]), r11, r12]
 
1263
        encoded_body = '\n'.join(lines)
 
1264
 
 
1265
        transport_path = 'sinhala'
 
1266
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1267
        client.add_success_response_with_body(encoded_body, 'ok')
 
1268
        result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
 
1269
        self.assertEqual(
 
1270
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1271
             ('sinhala/', r2))],
 
1272
            client._calls)
 
1273
        self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
 
1274
 
 
1275
    def test_no_such_revision(self):
 
1276
        revid = '123'
 
1277
        transport_path = 'sinhala'
 
1278
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1279
        client.add_error_response('nosuchrevision', revid)
 
1280
        # also check that the right revision is reported in the error
 
1281
        self.assertRaises(errors.NoSuchRevision,
 
1282
            self.applyDeprecated, one_four, repo.get_revision_graph, revid)
 
1283
        self.assertEqual(
 
1284
            [('call_expecting_body', 'Repository.get_revision_graph',
 
1285
             ('sinhala/', revid))],
 
1286
            client._calls)
 
1287
 
 
1288
    def test_unexpected_error(self):
 
1289
        revid = '123'
 
1290
        transport_path = 'sinhala'
 
1291
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1292
        client.add_error_response('AnUnexpectedError')
 
1293
        e = self.assertRaises(errors.UnknownErrorFromSmartServer,
 
1294
            self.applyDeprecated, one_four, repo.get_revision_graph, revid)
 
1295
        self.assertEqual(('AnUnexpectedError',), e.error_tuple)
 
1296
 
 
1297
        
 
1298
class TestRepositoryIsShared(TestRemoteRepository):
 
1299
 
 
1300
    def test_is_shared(self):
 
1301
        # ('yes', ) for Repository.is_shared -> 'True'.
 
1302
        transport_path = 'quack'
 
1303
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1304
        client.add_success_response('yes')
 
1305
        result = repo.is_shared()
 
1306
        self.assertEqual(
 
1307
            [('call', 'Repository.is_shared', ('quack/',))],
 
1308
            client._calls)
 
1309
        self.assertEqual(True, result)
 
1310
 
 
1311
    def test_is_not_shared(self):
 
1312
        # ('no', ) for Repository.is_shared -> 'False'.
 
1313
        transport_path = 'qwack'
 
1314
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1315
        client.add_success_response('no')
 
1316
        result = repo.is_shared()
 
1317
        self.assertEqual(
 
1318
            [('call', 'Repository.is_shared', ('qwack/',))],
 
1319
            client._calls)
 
1320
        self.assertEqual(False, result)
 
1321
 
 
1322
 
 
1323
class TestRepositoryLockWrite(TestRemoteRepository):
 
1324
 
 
1325
    def test_lock_write(self):
 
1326
        transport_path = 'quack'
 
1327
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1328
        client.add_success_response('ok', 'a token')
 
1329
        result = repo.lock_write()
 
1330
        self.assertEqual(
 
1331
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
1332
            client._calls)
 
1333
        self.assertEqual('a token', result)
 
1334
 
 
1335
    def test_lock_write_already_locked(self):
 
1336
        transport_path = 'quack'
 
1337
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1338
        client.add_error_response('LockContention')
 
1339
        self.assertRaises(errors.LockContention, repo.lock_write)
 
1340
        self.assertEqual(
 
1341
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
1342
            client._calls)
 
1343
 
 
1344
    def test_lock_write_unlockable(self):
 
1345
        transport_path = 'quack'
 
1346
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1347
        client.add_error_response('UnlockableTransport')
 
1348
        self.assertRaises(errors.UnlockableTransport, repo.lock_write)
 
1349
        self.assertEqual(
 
1350
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
1351
            client._calls)
 
1352
 
 
1353
 
 
1354
class TestRepositoryUnlock(TestRemoteRepository):
 
1355
 
 
1356
    def test_unlock(self):
 
1357
        transport_path = 'quack'
 
1358
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1359
        client.add_success_response('ok', 'a token')
 
1360
        client.add_success_response('ok')
 
1361
        repo.lock_write()
 
1362
        repo.unlock()
 
1363
        self.assertEqual(
 
1364
            [('call', 'Repository.lock_write', ('quack/', '')),
 
1365
             ('call', 'Repository.unlock', ('quack/', 'a token'))],
 
1366
            client._calls)
 
1367
 
 
1368
    def test_unlock_wrong_token(self):
 
1369
        # If somehow the token is wrong, unlock will raise TokenMismatch.
 
1370
        transport_path = 'quack'
 
1371
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1372
        client.add_success_response('ok', 'a token')
 
1373
        client.add_error_response('TokenMismatch')
 
1374
        repo.lock_write()
 
1375
        self.assertRaises(errors.TokenMismatch, repo.unlock)
 
1376
 
 
1377
 
 
1378
class TestRepositoryHasRevision(TestRemoteRepository):
 
1379
 
 
1380
    def test_none(self):
 
1381
        # repo.has_revision(None) should not cause any traffic.
 
1382
        transport_path = 'quack'
 
1383
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1384
 
 
1385
        # The null revision is always there, so has_revision(None) == True.
 
1386
        self.assertEqual(True, repo.has_revision(NULL_REVISION))
 
1387
 
 
1388
        # The remote repo shouldn't be accessed.
 
1389
        self.assertEqual([], client._calls)
 
1390
 
 
1391
 
 
1392
class TestRepositoryTarball(TestRemoteRepository):
 
1393
 
 
1394
    # This is a canned tarball reponse we can validate against
 
1395
    tarball_content = (
 
1396
        'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
 
1397
        'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
 
1398
        'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
 
1399
        'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
 
1400
        'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
 
1401
        'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
 
1402
        'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
 
1403
        'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
 
1404
        '0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
 
1405
        'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
 
1406
        'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
 
1407
        'nWQ7QH/F3JFOFCQ0aSPfA='
 
1408
        ).decode('base64')
 
1409
 
 
1410
    def test_repository_tarball(self):
 
1411
        # Test that Repository.tarball generates the right operations
 
1412
        transport_path = 'repo'
 
1413
        expected_calls = [('call_expecting_body', 'Repository.tarball',
 
1414
                           ('repo/', 'bz2',),),
 
1415
            ]
 
1416
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1417
        client.add_success_response_with_body(self.tarball_content, 'ok')
 
1418
        # Now actually ask for the tarball
 
1419
        tarball_file = repo._get_tarball('bz2')
 
1420
        try:
 
1421
            self.assertEqual(expected_calls, client._calls)
 
1422
            self.assertEqual(self.tarball_content, tarball_file.read())
 
1423
        finally:
 
1424
            tarball_file.close()
 
1425
 
 
1426
 
 
1427
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
 
1428
    """RemoteRepository.copy_content_into optimizations"""
 
1429
 
 
1430
    def test_copy_content_remote_to_local(self):
 
1431
        self.transport_server = server.SmartTCPServer_for_testing
 
1432
        src_repo = self.make_repository('repo1')
 
1433
        src_repo = repository.Repository.open(self.get_url('repo1'))
 
1434
        # At the moment the tarball-based copy_content_into can't write back
 
1435
        # into a smart server.  It would be good if it could upload the
 
1436
        # tarball; once that works we'd have to create repositories of
 
1437
        # different formats. -- mbp 20070410
 
1438
        dest_url = self.get_vfs_only_url('repo2')
 
1439
        dest_bzrdir = BzrDir.create(dest_url)
 
1440
        dest_repo = dest_bzrdir.create_repository()
 
1441
        self.assertFalse(isinstance(dest_repo, RemoteRepository))
 
1442
        self.assertTrue(isinstance(src_repo, RemoteRepository))
 
1443
        src_repo.copy_content_into(dest_repo)
 
1444
 
 
1445
 
 
1446
class _StubRealPackRepository(object):
 
1447
 
 
1448
    def __init__(self, calls):
 
1449
        self._pack_collection = _StubPackCollection(calls)
 
1450
 
 
1451
 
 
1452
class _StubPackCollection(object):
 
1453
 
 
1454
    def __init__(self, calls):
 
1455
        self.calls = calls
 
1456
 
 
1457
    def autopack(self):
 
1458
        self.calls.append(('pack collection autopack',))
 
1459
 
 
1460
    def reload_pack_names(self):
 
1461
        self.calls.append(('pack collection reload_pack_names',))
 
1462
 
 
1463
    
 
1464
class TestRemotePackRepositoryAutoPack(TestRemoteRepository):
 
1465
    """Tests for RemoteRepository.autopack implementation."""
 
1466
 
 
1467
    def test_ok(self):
 
1468
        """When the server returns 'ok' and there's no _real_repository, then
 
1469
        nothing else happens: the autopack method is done.
 
1470
        """
 
1471
        transport_path = 'quack'
 
1472
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1473
        client.add_expected_call(
 
1474
            'PackRepository.autopack', ('quack/',), 'success', ('ok',))
 
1475
        repo.autopack()
 
1476
        client.finished_test()
 
1477
 
 
1478
    def test_ok_with_real_repo(self):
 
1479
        """When the server returns 'ok' and there is a _real_repository, then
 
1480
        the _real_repository's reload_pack_name's method will be called.
 
1481
        """
 
1482
        transport_path = 'quack'
 
1483
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1484
        client.add_expected_call(
 
1485
            'PackRepository.autopack', ('quack/',),
 
1486
            'success', ('ok',))
 
1487
        repo._real_repository = _StubRealPackRepository(client._calls)
 
1488
        repo.autopack()
 
1489
        self.assertEqual(
 
1490
            [('call', 'PackRepository.autopack', ('quack/',)),
 
1491
             ('pack collection reload_pack_names',)],
 
1492
            client._calls)
 
1493
        
 
1494
    def test_backwards_compatibility(self):
 
1495
        """If the server does not recognise the PackRepository.autopack verb,
 
1496
        fallback to the real_repository's implementation.
 
1497
        """
 
1498
        transport_path = 'quack'
 
1499
        repo, client = self.setup_fake_client_and_repository(transport_path)
 
1500
        client.add_unknown_method_response('PackRepository.autopack')
 
1501
        def stub_ensure_real():
 
1502
            client._calls.append(('_ensure_real',))
 
1503
            repo._real_repository = _StubRealPackRepository(client._calls)
 
1504
        repo._ensure_real = stub_ensure_real
 
1505
        repo.autopack()
 
1506
        self.assertEqual(
 
1507
            [('call', 'PackRepository.autopack', ('quack/',)),
 
1508
             ('_ensure_real',),
 
1509
             ('pack collection autopack',)],
 
1510
            client._calls)
 
1511
 
 
1512
 
 
1513
class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
 
1514
    """Base class for unit tests for bzrlib.remote._translate_error."""
 
1515
 
 
1516
    def translateTuple(self, error_tuple, **context):
 
1517
        """Call _translate_error with an ErrorFromSmartServer built from the
 
1518
        given error_tuple.
 
1519
 
 
1520
        :param error_tuple: A tuple of a smart server response, as would be
 
1521
            passed to an ErrorFromSmartServer.
 
1522
        :kwargs context: context items to call _translate_error with.
 
1523
 
 
1524
        :returns: The error raised by _translate_error.
 
1525
        """
 
1526
        # Raise the ErrorFromSmartServer before passing it as an argument,
 
1527
        # because _translate_error may need to re-raise it with a bare 'raise'
 
1528
        # statement.
 
1529
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1530
        translated_error = self.translateErrorFromSmartServer(
 
1531
            server_error, **context)
 
1532
        return translated_error
 
1533
 
 
1534
    def translateErrorFromSmartServer(self, error_object, **context):
 
1535
        """Like translateTuple, but takes an already constructed
 
1536
        ErrorFromSmartServer rather than a tuple.
 
1537
        """
 
1538
        try:
 
1539
            raise error_object
 
1540
        except errors.ErrorFromSmartServer, server_error:
 
1541
            translated_error = self.assertRaises(
 
1542
                errors.BzrError, remote._translate_error, server_error,
 
1543
                **context)
 
1544
        return translated_error
 
1545
 
 
1546
 
 
1547
class TestErrorTranslationSuccess(TestErrorTranslationBase):
 
1548
    """Unit tests for bzrlib.remote._translate_error.
 
1549
    
 
1550
    Given an ErrorFromSmartServer (which has an error tuple from a smart
 
1551
    server) and some context, _translate_error raises more specific errors from
 
1552
    bzrlib.errors.
 
1553
 
 
1554
    This test case covers the cases where _translate_error succeeds in
 
1555
    translating an ErrorFromSmartServer to something better.  See
 
1556
    TestErrorTranslationRobustness for other cases.
 
1557
    """
 
1558
 
 
1559
    def test_NoSuchRevision(self):
 
1560
        branch = self.make_branch('')
 
1561
        revid = 'revid'
 
1562
        translated_error = self.translateTuple(
 
1563
            ('NoSuchRevision', revid), branch=branch)
 
1564
        expected_error = errors.NoSuchRevision(branch, revid)
 
1565
        self.assertEqual(expected_error, translated_error)
 
1566
 
 
1567
    def test_nosuchrevision(self):
 
1568
        repository = self.make_repository('')
 
1569
        revid = 'revid'
 
1570
        translated_error = self.translateTuple(
 
1571
            ('nosuchrevision', revid), repository=repository)
 
1572
        expected_error = errors.NoSuchRevision(repository, revid)
 
1573
        self.assertEqual(expected_error, translated_error)
 
1574
 
 
1575
    def test_nobranch(self):
 
1576
        bzrdir = self.make_bzrdir('')
 
1577
        translated_error = self.translateTuple(('nobranch',), bzrdir=bzrdir)
 
1578
        expected_error = errors.NotBranchError(path=bzrdir.root_transport.base)
 
1579
        self.assertEqual(expected_error, translated_error)
 
1580
 
 
1581
    def test_LockContention(self):
 
1582
        translated_error = self.translateTuple(('LockContention',))
 
1583
        expected_error = errors.LockContention('(remote lock)')
 
1584
        self.assertEqual(expected_error, translated_error)
 
1585
 
 
1586
    def test_UnlockableTransport(self):
 
1587
        bzrdir = self.make_bzrdir('')
 
1588
        translated_error = self.translateTuple(
 
1589
            ('UnlockableTransport',), bzrdir=bzrdir)
 
1590
        expected_error = errors.UnlockableTransport(bzrdir.root_transport)
 
1591
        self.assertEqual(expected_error, translated_error)
 
1592
 
 
1593
    def test_LockFailed(self):
 
1594
        lock = 'str() of a server lock'
 
1595
        why = 'str() of why'
 
1596
        translated_error = self.translateTuple(('LockFailed', lock, why))
 
1597
        expected_error = errors.LockFailed(lock, why)
 
1598
        self.assertEqual(expected_error, translated_error)
 
1599
 
 
1600
    def test_TokenMismatch(self):
 
1601
        token = 'a lock token'
 
1602
        translated_error = self.translateTuple(('TokenMismatch',), token=token)
 
1603
        expected_error = errors.TokenMismatch(token, '(remote token)')
 
1604
        self.assertEqual(expected_error, translated_error)
 
1605
 
 
1606
    def test_Diverged(self):
 
1607
        branch = self.make_branch('a')
 
1608
        other_branch = self.make_branch('b')
 
1609
        translated_error = self.translateTuple(
 
1610
            ('Diverged',), branch=branch, other_branch=other_branch)
 
1611
        expected_error = errors.DivergedBranches(branch, other_branch)
 
1612
        self.assertEqual(expected_error, translated_error)
 
1613
 
 
1614
    def test_ReadError_no_args(self):
 
1615
        path = 'a path'
 
1616
        translated_error = self.translateTuple(('ReadError',), path=path)
 
1617
        expected_error = errors.ReadError(path)
 
1618
        self.assertEqual(expected_error, translated_error)
 
1619
 
 
1620
    def test_ReadError(self):
 
1621
        path = 'a path'
 
1622
        translated_error = self.translateTuple(('ReadError', path))
 
1623
        expected_error = errors.ReadError(path)
 
1624
        self.assertEqual(expected_error, translated_error)
 
1625
 
 
1626
    def test_PermissionDenied_no_args(self):
 
1627
        path = 'a path'
 
1628
        translated_error = self.translateTuple(('PermissionDenied',), path=path)
 
1629
        expected_error = errors.PermissionDenied(path)
 
1630
        self.assertEqual(expected_error, translated_error)
 
1631
 
 
1632
    def test_PermissionDenied_one_arg(self):
 
1633
        path = 'a path'
 
1634
        translated_error = self.translateTuple(('PermissionDenied', path))
 
1635
        expected_error = errors.PermissionDenied(path)
 
1636
        self.assertEqual(expected_error, translated_error)
 
1637
 
 
1638
    def test_PermissionDenied_one_arg_and_context(self):
 
1639
        """Given a choice between a path from the local context and a path on
 
1640
        the wire, _translate_error prefers the path from the local context.
 
1641
        """
 
1642
        local_path = 'local path'
 
1643
        remote_path = 'remote path'
 
1644
        translated_error = self.translateTuple(
 
1645
            ('PermissionDenied', remote_path), path=local_path)
 
1646
        expected_error = errors.PermissionDenied(local_path)
 
1647
        self.assertEqual(expected_error, translated_error)
 
1648
 
 
1649
    def test_PermissionDenied_two_args(self):
 
1650
        path = 'a path'
 
1651
        extra = 'a string with extra info'
 
1652
        translated_error = self.translateTuple(
 
1653
            ('PermissionDenied', path, extra))
 
1654
        expected_error = errors.PermissionDenied(path, extra)
 
1655
        self.assertEqual(expected_error, translated_error)
 
1656
 
 
1657
 
 
1658
class TestErrorTranslationRobustness(TestErrorTranslationBase):
 
1659
    """Unit tests for bzrlib.remote._translate_error's robustness.
 
1660
    
 
1661
    TestErrorTranslationSuccess is for cases where _translate_error can
 
1662
    translate successfully.  This class about how _translate_err behaves when
 
1663
    it fails to translate: it re-raises the original error.
 
1664
    """
 
1665
 
 
1666
    def test_unrecognised_server_error(self):
 
1667
        """If the error code from the server is not recognised, the original
 
1668
        ErrorFromSmartServer is propagated unmodified.
 
1669
        """
 
1670
        error_tuple = ('An unknown error tuple',)
 
1671
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1672
        translated_error = self.translateErrorFromSmartServer(server_error)
 
1673
        expected_error = errors.UnknownErrorFromSmartServer(server_error)
 
1674
        self.assertEqual(expected_error, translated_error)
 
1675
 
 
1676
    def test_context_missing_a_key(self):
 
1677
        """In case of a bug in the client, or perhaps an unexpected response
 
1678
        from a server, _translate_error returns the original error tuple from
 
1679
        the server and mutters a warning.
 
1680
        """
 
1681
        # To translate a NoSuchRevision error _translate_error needs a 'branch'
 
1682
        # in the context dict.  So let's give it an empty context dict instead
 
1683
        # to exercise its error recovery.
 
1684
        empty_context = {}
 
1685
        error_tuple = ('NoSuchRevision', 'revid')
 
1686
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1687
        translated_error = self.translateErrorFromSmartServer(server_error)
 
1688
        self.assertEqual(server_error, translated_error)
 
1689
        # In addition to re-raising ErrorFromSmartServer, some debug info has
 
1690
        # been muttered to the log file for developer to look at.
 
1691
        self.assertContainsRe(
 
1692
            self._get_log(keep_log_file=True),
 
1693
            "Missing key 'branch' in context")
 
1694
        
 
1695
    def test_path_missing(self):
 
1696
        """Some translations (PermissionDenied, ReadError) can determine the
 
1697
        'path' variable from either the wire or the local context.  If neither
 
1698
        has it, then an error is raised.
 
1699
        """
 
1700
        error_tuple = ('ReadError',)
 
1701
        server_error = errors.ErrorFromSmartServer(error_tuple)
 
1702
        translated_error = self.translateErrorFromSmartServer(server_error)
 
1703
        self.assertEqual(server_error, translated_error)
 
1704
        # In addition to re-raising ErrorFromSmartServer, some debug info has
 
1705
        # been muttered to the log file for developer to look at.
 
1706
        self.assertContainsRe(
 
1707
            self._get_log(keep_log_file=True), "Missing key 'path' in context")
 
1708
 
 
1709
 
 
1710
class TestStacking(tests.TestCaseWithTransport):
 
1711
    """Tests for operations on stacked remote repositories.
 
1712
    
 
1713
    The underlying format type must support stacking.
 
1714
    """
 
1715
 
 
1716
    def test_access_stacked_remote(self):
 
1717
        # based on <http://launchpad.net/bugs/261315>
 
1718
        # make a branch stacked on another repository containing an empty
 
1719
        # revision, then open it over hpss - we should be able to see that
 
1720
        # revision.
 
1721
        base_transport = self.get_transport()
 
1722
        base_builder = self.make_branch_builder('base', format='1.6')
 
1723
        base_builder.start_series()
 
1724
        base_revid = base_builder.build_snapshot('rev-id', None,
 
1725
            [('add', ('', None, 'directory', None))],
 
1726
            'message')
 
1727
        base_builder.finish_series()
 
1728
        stacked_branch = self.make_branch('stacked', format='1.6')
 
1729
        stacked_branch.set_stacked_on_url('../base')
 
1730
        # start a server looking at this
 
1731
        smart_server = server.SmartTCPServer_for_testing()
 
1732
        smart_server.setUp()
 
1733
        self.addCleanup(smart_server.tearDown)
 
1734
        remote_bzrdir = BzrDir.open(smart_server.get_url() + '/stacked')
 
1735
        # can get its branch and repository
 
1736
        remote_branch = remote_bzrdir.open_branch()
 
1737
        remote_repo = remote_branch.repository
 
1738
        remote_repo.lock_read()
 
1739
        try:
 
1740
            # it should have an appropriate fallback repository, which should also
 
1741
            # be a RemoteRepository
 
1742
            self.assertEquals(len(remote_repo._fallback_repositories), 1)
 
1743
            self.assertIsInstance(remote_repo._fallback_repositories[0],
 
1744
                RemoteRepository)
 
1745
            # and it has the revision committed to the underlying repository;
 
1746
            # these have varying implementations so we try several of them
 
1747
            self.assertTrue(remote_repo.has_revisions([base_revid]))
 
1748
            self.assertTrue(remote_repo.has_revision(base_revid))
 
1749
            self.assertEqual(remote_repo.get_revision(base_revid).message,
 
1750
                'message')
 
1751
        finally:
 
1752
            remote_repo.unlock()