~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_remote.py

  • Committer: Ian Clatworthy
  • Date: 2009-01-19 02:24:15 UTC
  • mto: This revision was merged to the branch mainline in revision 3944.
  • Revision ID: ian.clatworthy@canonical.com-20090119022415-mo0mcfeiexfktgwt
apply jam's log --short fix (Ian Clatworthy)

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