/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: Jelmer Vernooij
  • Date: 2008-11-25 03:08:14 UTC
  • mto: This revision was merged to the branch mainline in revision 3860.
  • Revision ID: jelmer@samba.org-20081125030814-90fce65a9h7ffmmb
Remove dpush() implementation for now.

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