/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: Andrew Bennetts
  • Date: 2008-03-28 08:05:51 UTC
  • mto: This revision was merged to the branch mainline in revision 3321.
  • Revision ID: andrew.bennetts@canonical.com-20080328080551-n7f6rejuycnzn0p8
Change _SmartClient's API to accept a medium and a base, rather than a _SharedConnection.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007 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
    errors,
 
31
    graph,
 
32
    pack,
 
33
    remote,
 
34
    repository,
 
35
    tests,
 
36
    )
 
37
from bzrlib.branch import Branch
 
38
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
39
from bzrlib.remote import (
 
40
    RemoteBranch,
 
41
    RemoteBzrDir,
 
42
    RemoteBzrDirFormat,
 
43
    RemoteRepository,
 
44
    )
 
45
from bzrlib.revision import NULL_REVISION
 
46
from bzrlib.smart import server, medium
 
47
from bzrlib.smart.client import _SmartClient
 
48
from bzrlib.symbol_versioning import one_four
 
49
from bzrlib.transport.memory import MemoryTransport
 
50
from bzrlib.transport.remote import RemoteTransport
 
51
 
 
52
 
 
53
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
 
54
 
 
55
    def setUp(self):
 
56
        self.transport_server = server.SmartTCPServer_for_testing
 
57
        super(BasicRemoteObjectTests, self).setUp()
 
58
        self.transport = self.get_transport()
 
59
        # make a branch that can be opened over the smart transport
 
60
        self.local_wt = BzrDir.create_standalone_workingtree('.')
 
61
 
 
62
    def tearDown(self):
 
63
        self.transport.disconnect()
 
64
        tests.TestCaseWithTransport.tearDown(self)
 
65
 
 
66
    def test_create_remote_bzrdir(self):
 
67
        b = remote.RemoteBzrDir(self.transport)
 
68
        self.assertIsInstance(b, BzrDir)
 
69
 
 
70
    def test_open_remote_branch(self):
 
71
        # open a standalone branch in the working directory
 
72
        b = remote.RemoteBzrDir(self.transport)
 
73
        branch = b.open_branch()
 
74
        self.assertIsInstance(branch, Branch)
 
75
 
 
76
    def test_remote_repository(self):
 
77
        b = BzrDir.open_from_transport(self.transport)
 
78
        repo = b.open_repository()
 
79
        revid = u'\xc823123123'.encode('utf8')
 
80
        self.assertFalse(repo.has_revision(revid))
 
81
        self.local_wt.commit(message='test commit', rev_id=revid)
 
82
        self.assertTrue(repo.has_revision(revid))
 
83
 
 
84
    def test_remote_branch_revision_history(self):
 
85
        b = BzrDir.open_from_transport(self.transport).open_branch()
 
86
        self.assertEqual([], b.revision_history())
 
87
        r1 = self.local_wt.commit('1st commit')
 
88
        r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
 
89
        self.assertEqual([r1, r2], b.revision_history())
 
90
 
 
91
    def test_find_correct_format(self):
 
92
        """Should open a RemoteBzrDir over a RemoteTransport"""
 
93
        fmt = BzrDirFormat.find_format(self.transport)
 
94
        self.assertTrue(RemoteBzrDirFormat
 
95
                        in BzrDirFormat._control_server_formats)
 
96
        self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
 
97
 
 
98
    def test_open_detected_smart_format(self):
 
99
        fmt = BzrDirFormat.find_format(self.transport)
 
100
        d = fmt.open(self.transport)
 
101
        self.assertIsInstance(d, BzrDir)
 
102
 
 
103
    def test_remote_branch_repr(self):
 
104
        b = BzrDir.open_from_transport(self.transport).open_branch()
 
105
        self.assertStartsWith(str(b), 'RemoteBranch(')
 
106
 
 
107
 
 
108
class FakeProtocol(object):
 
109
    """Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
 
110
 
 
111
    def __init__(self, body, fake_client):
 
112
        self.body = body
 
113
        self._body_buffer = None
 
114
        self._fake_client = fake_client
 
115
 
 
116
    def read_body_bytes(self, count=-1):
 
117
        if self._body_buffer is None:
 
118
            self._body_buffer = StringIO(self.body)
 
119
        bytes = self._body_buffer.read(count)
 
120
        if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
 
121
            self._fake_client.expecting_body = False
 
122
        return bytes
 
123
 
 
124
    def cancel_read_body(self):
 
125
        self._fake_client.expecting_body = False
 
126
 
 
127
    def read_streamed_body(self):
 
128
        return self.body
 
129
 
 
130
 
 
131
class FakeClient(_SmartClient):
 
132
    """Lookalike for _SmartClient allowing testing."""
 
133
    
 
134
    def __init__(self, responses, fake_medium_base='fake base'):
 
135
        """Create a FakeClient.
 
136
 
 
137
        :param responses: A list of response-tuple, body-data pairs to be sent
 
138
            back to callers.
 
139
        """
 
140
        self.responses = responses
 
141
        self._calls = []
 
142
        self.expecting_body = False
 
143
        _SmartClient.__init__(self, FakeMedium(self._calls), fake_medium_base)
 
144
 
 
145
    def call(self, method, *args):
 
146
        self._calls.append(('call', method, args))
 
147
        return self.responses.pop(0)[0]
 
148
 
 
149
    def call_expecting_body(self, method, *args):
 
150
        self._calls.append(('call_expecting_body', method, args))
 
151
        result = self.responses.pop(0)
 
152
        self.expecting_body = True
 
153
        return result[0], FakeProtocol(result[1], self)
 
154
 
 
155
    def call_with_body_bytes_expecting_body(self, method, args, body):
 
156
        self._calls.append(('call_with_body_bytes_expecting_body', method,
 
157
            args, body))
 
158
        result = self.responses.pop(0)
 
159
        self.expecting_body = True
 
160
        return result[0], FakeProtocol(result[1], self)
 
161
 
 
162
 
 
163
class FakeMedium(object):
 
164
 
 
165
    def __init__(self, client_calls):
 
166
        self._remote_is_at_least_1_2 = True
 
167
        self._client_calls = client_calls
 
168
 
 
169
    def disconnect(self):
 
170
        self._client_calls.append(('disconnect medium',))
 
171
 
 
172
 
 
173
class TestVfsHas(tests.TestCase):
 
174
 
 
175
    def test_unicode_path(self):
 
176
        client = FakeClient([(('yes',), )], '/')
 
177
        transport = RemoteTransport('bzr://localhost/', _client=client)
 
178
        filename = u'/hell\u00d8'.encode('utf8')
 
179
        result = transport.has(filename)
 
180
        self.assertEqual(
 
181
            [('call', 'has', (filename,))],
 
182
            client._calls)
 
183
        self.assertTrue(result)
 
184
 
 
185
 
 
186
class TestBzrDirOpenBranch(tests.TestCase):
 
187
 
 
188
    def test_branch_present(self):
 
189
        transport = MemoryTransport()
 
190
        transport.mkdir('quack')
 
191
        transport = transport.clone('quack')
 
192
        client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
 
193
                            transport.base)
 
194
        bzrdir = RemoteBzrDir(transport, _client=client)
 
195
        result = bzrdir.open_branch()
 
196
        self.assertEqual(
 
197
            [('call', 'BzrDir.open_branch', ('quack/',)),
 
198
             ('call', 'BzrDir.find_repositoryV2', ('quack/',))],
 
199
            client._calls)
 
200
        self.assertIsInstance(result, RemoteBranch)
 
201
        self.assertEqual(bzrdir, result.bzrdir)
 
202
 
 
203
    def test_branch_missing(self):
 
204
        transport = MemoryTransport()
 
205
        transport.mkdir('quack')
 
206
        transport = transport.clone('quack')
 
207
        client = FakeClient([(('nobranch',), )], transport.base)
 
208
        bzrdir = RemoteBzrDir(transport, _client=client)
 
209
        self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
 
210
        self.assertEqual(
 
211
            [('call', 'BzrDir.open_branch', ('quack/',))],
 
212
            client._calls)
 
213
 
 
214
    def test__get_tree_branch(self):
 
215
        # _get_tree_branch is a form of open_branch, but it should only ask for
 
216
        # branch opening, not any other network requests.
 
217
        calls = []
 
218
        def open_branch():
 
219
            calls.append("Called")
 
220
            return "a-branch"
 
221
        transport = MemoryTransport()
 
222
        # no requests on the network - catches other api calls being made.
 
223
        client = FakeClient([], transport.base)
 
224
        bzrdir = RemoteBzrDir(transport, _client=client)
 
225
        # patch the open_branch call to record that it was called.
 
226
        bzrdir.open_branch = open_branch
 
227
        self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
 
228
        self.assertEqual(["Called"], calls)
 
229
        self.assertEqual([], client._calls)
 
230
 
 
231
    def test_url_quoting_of_path(self):
 
232
        # Relpaths on the wire should not be URL-escaped.  So "~" should be
 
233
        # transmitted as "~", not "%7E".
 
234
        transport = RemoteTransport('bzr://localhost/~hello/')
 
235
        client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
 
236
                            transport.base)
 
237
        bzrdir = RemoteBzrDir(transport, _client=client)
 
238
        result = bzrdir.open_branch()
 
239
        self.assertEqual(
 
240
            [('call', 'BzrDir.open_branch', ('~hello/',)),
 
241
             ('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
 
242
            client._calls)
 
243
 
 
244
    def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
 
245
        transport = MemoryTransport()
 
246
        transport.mkdir('quack')
 
247
        transport = transport.clone('quack')
 
248
        if rich_root:
 
249
            rich_response = 'yes'
 
250
        else:
 
251
            rich_response = 'no'
 
252
        if subtrees:
 
253
            subtree_response = 'yes'
 
254
        else:
 
255
            subtree_response = 'no'
 
256
        client = FakeClient(
 
257
            [(('ok', '', rich_response, subtree_response, external_lookup), ),],
 
258
            transport.base)
 
259
        bzrdir = RemoteBzrDir(transport, _client=client)
 
260
        result = bzrdir.open_repository()
 
261
        self.assertEqual(
 
262
            [('call', 'BzrDir.find_repositoryV2', ('quack/',))],
 
263
            client._calls)
 
264
        self.assertIsInstance(result, RemoteRepository)
 
265
        self.assertEqual(bzrdir, result.bzrdir)
 
266
        self.assertEqual(rich_root, result._format.rich_root_data)
 
267
        self.assertEqual(subtrees, result._format.supports_tree_reference)
 
268
 
 
269
    def test_open_repository_sets_format_attributes(self):
 
270
        self.check_open_repository(True, True)
 
271
        self.check_open_repository(False, True)
 
272
        self.check_open_repository(True, False)
 
273
        self.check_open_repository(False, False)
 
274
        self.check_open_repository(False, False, 'yes')
 
275
 
 
276
    def test_old_server(self):
 
277
        """RemoteBzrDirFormat should fail to probe if the server version is too
 
278
        old.
 
279
        """
 
280
        self.assertRaises(errors.NotBranchError,
 
281
            RemoteBzrDirFormat.probe_transport, OldServerTransport())
 
282
 
 
283
 
 
284
class OldSmartClient(object):
 
285
    """A fake smart client for test_old_version that just returns a version one
 
286
    response to the 'hello' (query version) command.
 
287
    """
 
288
 
 
289
    def get_request(self):
 
290
        input_file = StringIO('ok\x011\n')
 
291
        output_file = StringIO()
 
292
        client_medium = medium.SmartSimplePipesClientMedium(
 
293
            input_file, output_file)
 
294
        return medium.SmartClientStreamMediumRequest(client_medium)
 
295
 
 
296
    def protocol_version(self):
 
297
        return 1
 
298
 
 
299
 
 
300
class OldServerTransport(object):
 
301
    """A fake transport for test_old_server that reports it's smart server
 
302
    protocol version as version one.
 
303
    """
 
304
 
 
305
    def __init__(self):
 
306
        self.base = 'fake:'
 
307
 
 
308
    def get_smart_client(self):
 
309
        return OldSmartClient()
 
310
 
 
311
 
 
312
class TestBranchLastRevisionInfo(tests.TestCase):
 
313
 
 
314
    def test_empty_branch(self):
 
315
        # in an empty branch we decode the response properly
 
316
        transport = MemoryTransport()
 
317
        client = FakeClient([(('ok', '0', 'null:'), )], transport.base)
 
318
        transport.mkdir('quack')
 
319
        transport = transport.clone('quack')
 
320
        # we do not want bzrdir to make any remote calls
 
321
        bzrdir = RemoteBzrDir(transport, _client=False)
 
322
        branch = RemoteBranch(bzrdir, None, _client=client)
 
323
        result = branch.last_revision_info()
 
324
 
 
325
        self.assertEqual(
 
326
            [('call', 'Branch.last_revision_info', ('quack/',))],
 
327
            client._calls)
 
328
        self.assertEqual((0, NULL_REVISION), result)
 
329
 
 
330
    def test_non_empty_branch(self):
 
331
        # in a non-empty branch we also decode the response properly
 
332
        revid = u'\xc8'.encode('utf8')
 
333
        transport = MemoryTransport()
 
334
        client = FakeClient([(('ok', '2', revid), )], transport.base)
 
335
        transport.mkdir('kwaak')
 
336
        transport = transport.clone('kwaak')
 
337
        # we do not want bzrdir to make any remote calls
 
338
        bzrdir = RemoteBzrDir(transport, _client=False)
 
339
        branch = RemoteBranch(bzrdir, None, _client=client)
 
340
        result = branch.last_revision_info()
 
341
 
 
342
        self.assertEqual(
 
343
            [('call', 'Branch.last_revision_info', ('kwaak/',))],
 
344
            client._calls)
 
345
        self.assertEqual((2, revid), result)
 
346
 
 
347
 
 
348
class TestBranchSetLastRevision(tests.TestCase):
 
349
 
 
350
    def test_set_empty(self):
 
351
        # set_revision_history([]) is translated to calling
 
352
        # Branch.set_last_revision(path, '') on the wire.
 
353
        transport = MemoryTransport()
 
354
        transport.mkdir('branch')
 
355
        transport = transport.clone('branch')
 
356
 
 
357
        client = FakeClient([
 
358
            # lock_write
 
359
            (('ok', 'branch token', 'repo token'), ),
 
360
            # set_last_revision
 
361
            (('ok',), ),
 
362
            # unlock
 
363
            (('ok',), )],
 
364
            transport.base)
 
365
        bzrdir = RemoteBzrDir(transport, _client=False)
 
366
        branch = RemoteBranch(bzrdir, None, _client=client)
 
367
        # This is a hack to work around the problem that RemoteBranch currently
 
368
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
369
        branch._ensure_real = lambda: None
 
370
        branch.lock_write()
 
371
        client._calls = []
 
372
        result = branch.set_revision_history([])
 
373
        self.assertEqual(
 
374
            [('call', 'Branch.set_last_revision',
 
375
                ('branch/', 'branch token', 'repo token', 'null:'))],
 
376
            client._calls)
 
377
        branch.unlock()
 
378
        self.assertEqual(None, result)
 
379
 
 
380
    def test_set_nonempty(self):
 
381
        # set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
 
382
        # Branch.set_last_revision(path, rev-idN) on the wire.
 
383
        transport = MemoryTransport()
 
384
        transport.mkdir('branch')
 
385
        transport = transport.clone('branch')
 
386
 
 
387
        client = FakeClient([
 
388
            # lock_write
 
389
            (('ok', 'branch token', 'repo token'), ),
 
390
            # set_last_revision
 
391
            (('ok',), ),
 
392
            # unlock
 
393
            (('ok',), )],
 
394
            transport.base)
 
395
        bzrdir = RemoteBzrDir(transport, _client=False)
 
396
        branch = RemoteBranch(bzrdir, None, _client=client)
 
397
        # This is a hack to work around the problem that RemoteBranch currently
 
398
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
399
        branch._ensure_real = lambda: None
 
400
        # Lock the branch, reset the record of remote calls.
 
401
        branch.lock_write()
 
402
        client._calls = []
 
403
 
 
404
        result = branch.set_revision_history(['rev-id1', 'rev-id2'])
 
405
        self.assertEqual(
 
406
            [('call', 'Branch.set_last_revision',
 
407
                ('branch/', 'branch token', 'repo token', 'rev-id2'))],
 
408
            client._calls)
 
409
        branch.unlock()
 
410
        self.assertEqual(None, result)
 
411
 
 
412
    def test_no_such_revision(self):
 
413
        # A response of 'NoSuchRevision' is translated into an exception.
 
414
        client = FakeClient([
 
415
            # lock_write
 
416
            (('ok', 'branch token', 'repo token'), ),
 
417
            # set_last_revision
 
418
            (('NoSuchRevision', 'rev-id'), ),
 
419
            # unlock
 
420
            (('ok',), )])
 
421
        transport = MemoryTransport()
 
422
        transport.mkdir('branch')
 
423
        transport = transport.clone('branch')
 
424
 
 
425
        bzrdir = RemoteBzrDir(transport, _client=False)
 
426
        branch = RemoteBranch(bzrdir, None, _client=client)
 
427
        branch._ensure_real = lambda: None
 
428
        branch.lock_write()
 
429
        client._calls = []
 
430
 
 
431
        self.assertRaises(
 
432
            errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
 
433
        branch.unlock()
 
434
 
 
435
 
 
436
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
 
437
    """Test branch.control_files api munging...
 
438
 
 
439
    We special case RemoteBranch.control_files.get('branch.conf') to
 
440
    call a specific API so that RemoteBranch's can intercept configuration
 
441
    file reading, allowing them to signal to the client about things like
 
442
    'email is configured for commits'.
 
443
    """
 
444
 
 
445
    def test_get_branch_conf(self):
 
446
        # in an empty branch we decode the response properly
 
447
        client = FakeClient([(('ok', ), 'config file body')], self.get_url())
 
448
        # we need to make a real branch because the remote_branch.control_files
 
449
        # will trigger _ensure_real.
 
450
        branch = self.make_branch('quack')
 
451
        transport = branch.bzrdir.root_transport
 
452
        # we do not want bzrdir to make any remote calls
 
453
        bzrdir = RemoteBzrDir(transport, _client=False)
 
454
        branch = RemoteBranch(bzrdir, None, _client=client)
 
455
        result = branch.control_files.get('branch.conf')
 
456
        self.assertEqual(
 
457
            [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
 
458
            client._calls)
 
459
        self.assertEqual('config file body', result.read())
 
460
 
 
461
 
 
462
class TestBranchLockWrite(tests.TestCase):
 
463
 
 
464
    def test_lock_write_unlockable(self):
 
465
        transport = MemoryTransport()
 
466
        client = FakeClient([(('UnlockableTransport', ), '')], transport.base)
 
467
        transport.mkdir('quack')
 
468
        transport = transport.clone('quack')
 
469
        # we do not want bzrdir to make any remote calls
 
470
        bzrdir = RemoteBzrDir(transport, _client=False)
 
471
        branch = RemoteBranch(bzrdir, None, _client=client)
 
472
        self.assertRaises(errors.UnlockableTransport, branch.lock_write)
 
473
        self.assertEqual(
 
474
            [('call', 'Branch.lock_write', ('quack/', '', ''))],
 
475
            client._calls)
 
476
 
 
477
 
 
478
class TestTransportIsReadonly(tests.TestCase):
 
479
 
 
480
    def test_true(self):
 
481
        client = FakeClient([(('yes',), '')])
 
482
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
483
                                    _client=client)
 
484
        self.assertEqual(True, transport.is_readonly())
 
485
        self.assertEqual(
 
486
            [('call', 'Transport.is_readonly', ())],
 
487
            client._calls)
 
488
 
 
489
    def test_false(self):
 
490
        client = FakeClient([(('no',), '')])
 
491
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
492
                                    _client=client)
 
493
        self.assertEqual(False, transport.is_readonly())
 
494
        self.assertEqual(
 
495
            [('call', 'Transport.is_readonly', ())],
 
496
            client._calls)
 
497
 
 
498
    def test_error_from_old_server(self):
 
499
        """bzr 0.15 and earlier servers don't recognise the is_readonly verb.
 
500
        
 
501
        Clients should treat it as a "no" response, because is_readonly is only
 
502
        advisory anyway (a transport could be read-write, but then the
 
503
        underlying filesystem could be readonly anyway).
 
504
        """
 
505
        client = FakeClient([(
 
506
            ('error', "Generic bzr smart protocol error: "
 
507
                      "bad request 'Transport.is_readonly'"), '')])
 
508
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
509
                                    _client=client)
 
510
        self.assertEqual(False, transport.is_readonly())
 
511
        self.assertEqual(
 
512
            [('call', 'Transport.is_readonly', ())],
 
513
            client._calls)
 
514
 
 
515
    def test_error_from_old_0_11_server(self):
 
516
        """Same as test_error_from_old_server, but with the slightly different
 
517
        error message from bzr 0.11 servers.
 
518
        """
 
519
        client = FakeClient([(
 
520
            ('error', "Generic bzr smart protocol error: "
 
521
                      "bad request u'Transport.is_readonly'"), '')])
 
522
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
523
                                    _client=client)
 
524
        self.assertEqual(False, transport.is_readonly())
 
525
        self.assertEqual(
 
526
            [('call', 'Transport.is_readonly', ())],
 
527
            client._calls)
 
528
 
 
529
 
 
530
class TestRemoteRepository(tests.TestCase):
 
531
    """Base for testing RemoteRepository protocol usage.
 
532
    
 
533
    These tests contain frozen requests and responses.  We want any changes to 
 
534
    what is sent or expected to be require a thoughtful update to these tests
 
535
    because they might break compatibility with different-versioned servers.
 
536
    """
 
537
 
 
538
    def setup_fake_client_and_repository(self, responses, transport_path):
 
539
        """Create the fake client and repository for testing with.
 
540
        
 
541
        There's no real server here; we just have canned responses sent
 
542
        back one by one.
 
543
        
 
544
        :param transport_path: Path below the root of the MemoryTransport
 
545
            where the repository will be created.
 
546
        """
 
547
        transport = MemoryTransport()
 
548
        transport.mkdir(transport_path)
 
549
        client = FakeClient(responses, transport.base)
 
550
        transport = transport.clone(transport_path)
 
551
        # we do not want bzrdir to make any remote calls
 
552
        bzrdir = RemoteBzrDir(transport, _client=False)
 
553
        repo = RemoteRepository(bzrdir, None, _client=client)
 
554
        return repo, client
 
555
 
 
556
 
 
557
class TestRepositoryGatherStats(TestRemoteRepository):
 
558
 
 
559
    def test_revid_none(self):
 
560
        # ('ok',), body with revisions and size
 
561
        responses = [(('ok', ), 'revisions: 2\nsize: 18\n')]
 
562
        transport_path = 'quack'
 
563
        repo, client = self.setup_fake_client_and_repository(
 
564
            responses, transport_path)
 
565
        result = repo.gather_stats(None)
 
566
        self.assertEqual(
 
567
            [('call_expecting_body', 'Repository.gather_stats',
 
568
             ('quack/','','no'))],
 
569
            client._calls)
 
570
        self.assertEqual({'revisions': 2, 'size': 18}, result)
 
571
 
 
572
    def test_revid_no_committers(self):
 
573
        # ('ok',), body without committers
 
574
        responses = [(('ok', ),
 
575
                      'firstrev: 123456.300 3600\n'
 
576
                      'latestrev: 654231.400 0\n'
 
577
                      'revisions: 2\n'
 
578
                      'size: 18\n')]
 
579
        transport_path = 'quick'
 
580
        revid = u'\xc8'.encode('utf8')
 
581
        repo, client = self.setup_fake_client_and_repository(
 
582
            responses, transport_path)
 
583
        result = repo.gather_stats(revid)
 
584
        self.assertEqual(
 
585
            [('call_expecting_body', 'Repository.gather_stats',
 
586
              ('quick/', revid, 'no'))],
 
587
            client._calls)
 
588
        self.assertEqual({'revisions': 2, 'size': 18,
 
589
                          'firstrev': (123456.300, 3600),
 
590
                          'latestrev': (654231.400, 0),},
 
591
                         result)
 
592
 
 
593
    def test_revid_with_committers(self):
 
594
        # ('ok',), body with committers
 
595
        responses = [(('ok', ),
 
596
                      'committers: 128\n'
 
597
                      'firstrev: 123456.300 3600\n'
 
598
                      'latestrev: 654231.400 0\n'
 
599
                      'revisions: 2\n'
 
600
                      'size: 18\n')]
 
601
        transport_path = 'buick'
 
602
        revid = u'\xc8'.encode('utf8')
 
603
        repo, client = self.setup_fake_client_and_repository(
 
604
            responses, transport_path)
 
605
        result = repo.gather_stats(revid, True)
 
606
        self.assertEqual(
 
607
            [('call_expecting_body', 'Repository.gather_stats',
 
608
              ('buick/', revid, 'yes'))],
 
609
            client._calls)
 
610
        self.assertEqual({'revisions': 2, 'size': 18,
 
611
                          'committers': 128,
 
612
                          'firstrev': (123456.300, 3600),
 
613
                          'latestrev': (654231.400, 0),},
 
614
                         result)
 
615
 
 
616
 
 
617
class TestRepositoryGetGraph(TestRemoteRepository):
 
618
 
 
619
    def test_get_graph(self):
 
620
        # get_graph returns a graph with the repository as the
 
621
        # parents_provider.
 
622
        responses = []
 
623
        transport_path = 'quack'
 
624
        repo, client = self.setup_fake_client_and_repository(
 
625
            responses, transport_path)
 
626
        graph = repo.get_graph()
 
627
        self.assertEqual(graph._parents_provider, repo)
 
628
 
 
629
 
 
630
class TestRepositoryGetParentMap(TestRemoteRepository):
 
631
 
 
632
    def test_get_parent_map_caching(self):
 
633
        # get_parent_map returns from cache until unlock()
 
634
        # setup a reponse with two revisions
 
635
        r1 = u'\u0e33'.encode('utf8')
 
636
        r2 = u'\u0dab'.encode('utf8')
 
637
        lines = [' '.join([r2, r1]), r1]
 
638
        encoded_body = bz2.compress('\n'.join(lines))
 
639
        responses = [(('ok', ), encoded_body), (('ok', ), encoded_body)]
 
640
 
 
641
        transport_path = 'quack'
 
642
        repo, client = self.setup_fake_client_and_repository(
 
643
            responses, transport_path)
 
644
        repo.lock_read()
 
645
        graph = repo.get_graph()
 
646
        parents = graph.get_parent_map([r2])
 
647
        self.assertEqual({r2: (r1,)}, parents)
 
648
        # locking and unlocking deeper should not reset
 
649
        repo.lock_read()
 
650
        repo.unlock()
 
651
        parents = graph.get_parent_map([r1])
 
652
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
653
        self.assertEqual(
 
654
            [('call_with_body_bytes_expecting_body',
 
655
              'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
 
656
            client._calls)
 
657
        repo.unlock()
 
658
        # now we call again, and it should use the second response.
 
659
        repo.lock_read()
 
660
        graph = repo.get_graph()
 
661
        parents = graph.get_parent_map([r1])
 
662
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
663
        self.assertEqual(
 
664
            [('call_with_body_bytes_expecting_body',
 
665
              'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
 
666
             ('call_with_body_bytes_expecting_body',
 
667
              'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
 
668
            ],
 
669
            client._calls)
 
670
        repo.unlock()
 
671
 
 
672
    def test_get_parent_map_reconnects_if_unknown_method(self):
 
673
        error_msg = (
 
674
            "Generic bzr smart protocol error: "
 
675
            "bad request 'Repository.get_parent_map'")
 
676
        responses = [
 
677
            (('error', error_msg), ''),
 
678
            (('ok',), '')]
 
679
        transport_path = 'quack'
 
680
        repo, client = self.setup_fake_client_and_repository(
 
681
            responses, transport_path)
 
682
        rev_id = 'revision-id'
 
683
        parents = repo.get_parent_map([rev_id])
 
684
        self.assertEqual(
 
685
            [('call_with_body_bytes_expecting_body',
 
686
              'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
 
687
             ('disconnect medium',),
 
688
             ('call_expecting_body', 'Repository.get_revision_graph',
 
689
              ('quack/', ''))],
 
690
            client._calls)
 
691
 
 
692
 
 
693
 
 
694
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
 
695
    
 
696
    def test_null_revision(self):
 
697
        # a null revision has the predictable result {}, we should have no wire
 
698
        # traffic when calling it with this argument
 
699
        responses = [(('notused', ), '')]
 
700
        transport_path = 'empty'
 
701
        repo, client = self.setup_fake_client_and_repository(
 
702
            responses, transport_path)
 
703
        result = self.applyDeprecated(one_four, repo.get_revision_graph,
 
704
            NULL_REVISION)
 
705
        self.assertEqual([], client._calls)
 
706
        self.assertEqual({}, result)
 
707
 
 
708
    def test_none_revision(self):
 
709
        # with none we want the entire graph
 
710
        r1 = u'\u0e33'.encode('utf8')
 
711
        r2 = u'\u0dab'.encode('utf8')
 
712
        lines = [' '.join([r2, r1]), r1]
 
713
        encoded_body = '\n'.join(lines)
 
714
 
 
715
        responses = [(('ok', ), encoded_body)]
 
716
        transport_path = 'sinhala'
 
717
        repo, client = self.setup_fake_client_and_repository(
 
718
            responses, transport_path)
 
719
        result = self.applyDeprecated(one_four, repo.get_revision_graph)
 
720
        self.assertEqual(
 
721
            [('call_expecting_body', 'Repository.get_revision_graph',
 
722
             ('sinhala/', ''))],
 
723
            client._calls)
 
724
        self.assertEqual({r1: (), r2: (r1, )}, result)
 
725
 
 
726
    def test_specific_revision(self):
 
727
        # with a specific revision we want the graph for that
 
728
        # with none we want the entire graph
 
729
        r11 = u'\u0e33'.encode('utf8')
 
730
        r12 = u'\xc9'.encode('utf8')
 
731
        r2 = u'\u0dab'.encode('utf8')
 
732
        lines = [' '.join([r2, r11, r12]), r11, r12]
 
733
        encoded_body = '\n'.join(lines)
 
734
 
 
735
        responses = [(('ok', ), encoded_body)]
 
736
        transport_path = 'sinhala'
 
737
        repo, client = self.setup_fake_client_and_repository(
 
738
            responses, transport_path)
 
739
        result = self.applyDeprecated(one_four, repo.get_revision_graph, r2)
 
740
        self.assertEqual(
 
741
            [('call_expecting_body', 'Repository.get_revision_graph',
 
742
             ('sinhala/', r2))],
 
743
            client._calls)
 
744
        self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
 
745
 
 
746
    def test_no_such_revision(self):
 
747
        revid = '123'
 
748
        responses = [(('nosuchrevision', revid), '')]
 
749
        transport_path = 'sinhala'
 
750
        repo, client = self.setup_fake_client_and_repository(
 
751
            responses, transport_path)
 
752
        # also check that the right revision is reported in the error
 
753
        self.assertRaises(errors.NoSuchRevision,
 
754
            self.applyDeprecated, one_four, repo.get_revision_graph, revid)
 
755
        self.assertEqual(
 
756
            [('call_expecting_body', 'Repository.get_revision_graph',
 
757
             ('sinhala/', revid))],
 
758
            client._calls)
 
759
 
 
760
        
 
761
class TestRepositoryIsShared(TestRemoteRepository):
 
762
 
 
763
    def test_is_shared(self):
 
764
        # ('yes', ) for Repository.is_shared -> 'True'.
 
765
        responses = [(('yes', ), )]
 
766
        transport_path = 'quack'
 
767
        repo, client = self.setup_fake_client_and_repository(
 
768
            responses, transport_path)
 
769
        result = repo.is_shared()
 
770
        self.assertEqual(
 
771
            [('call', 'Repository.is_shared', ('quack/',))],
 
772
            client._calls)
 
773
        self.assertEqual(True, result)
 
774
 
 
775
    def test_is_not_shared(self):
 
776
        # ('no', ) for Repository.is_shared -> 'False'.
 
777
        responses = [(('no', ), )]
 
778
        transport_path = 'qwack'
 
779
        repo, client = self.setup_fake_client_and_repository(
 
780
            responses, transport_path)
 
781
        result = repo.is_shared()
 
782
        self.assertEqual(
 
783
            [('call', 'Repository.is_shared', ('qwack/',))],
 
784
            client._calls)
 
785
        self.assertEqual(False, result)
 
786
 
 
787
 
 
788
class TestRepositoryLockWrite(TestRemoteRepository):
 
789
 
 
790
    def test_lock_write(self):
 
791
        responses = [(('ok', 'a token'), '')]
 
792
        transport_path = 'quack'
 
793
        repo, client = self.setup_fake_client_and_repository(
 
794
            responses, transport_path)
 
795
        result = repo.lock_write()
 
796
        self.assertEqual(
 
797
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
798
            client._calls)
 
799
        self.assertEqual('a token', result)
 
800
 
 
801
    def test_lock_write_already_locked(self):
 
802
        responses = [(('LockContention', ), '')]
 
803
        transport_path = 'quack'
 
804
        repo, client = self.setup_fake_client_and_repository(
 
805
            responses, transport_path)
 
806
        self.assertRaises(errors.LockContention, repo.lock_write)
 
807
        self.assertEqual(
 
808
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
809
            client._calls)
 
810
 
 
811
    def test_lock_write_unlockable(self):
 
812
        responses = [(('UnlockableTransport', ), '')]
 
813
        transport_path = 'quack'
 
814
        repo, client = self.setup_fake_client_and_repository(
 
815
            responses, transport_path)
 
816
        self.assertRaises(errors.UnlockableTransport, repo.lock_write)
 
817
        self.assertEqual(
 
818
            [('call', 'Repository.lock_write', ('quack/', ''))],
 
819
            client._calls)
 
820
 
 
821
 
 
822
class TestRepositoryUnlock(TestRemoteRepository):
 
823
 
 
824
    def test_unlock(self):
 
825
        responses = [(('ok', 'a token'), ''),
 
826
                     (('ok',), '')]
 
827
        transport_path = 'quack'
 
828
        repo, client = self.setup_fake_client_and_repository(
 
829
            responses, transport_path)
 
830
        repo.lock_write()
 
831
        repo.unlock()
 
832
        self.assertEqual(
 
833
            [('call', 'Repository.lock_write', ('quack/', '')),
 
834
             ('call', 'Repository.unlock', ('quack/', 'a token'))],
 
835
            client._calls)
 
836
 
 
837
    def test_unlock_wrong_token(self):
 
838
        # If somehow the token is wrong, unlock will raise TokenMismatch.
 
839
        responses = [(('ok', 'a token'), ''),
 
840
                     (('TokenMismatch',), '')]
 
841
        transport_path = 'quack'
 
842
        repo, client = self.setup_fake_client_and_repository(
 
843
            responses, transport_path)
 
844
        repo.lock_write()
 
845
        self.assertRaises(errors.TokenMismatch, repo.unlock)
 
846
 
 
847
 
 
848
class TestRepositoryHasRevision(TestRemoteRepository):
 
849
 
 
850
    def test_none(self):
 
851
        # repo.has_revision(None) should not cause any traffic.
 
852
        transport_path = 'quack'
 
853
        responses = None
 
854
        repo, client = self.setup_fake_client_and_repository(
 
855
            responses, transport_path)
 
856
 
 
857
        # The null revision is always there, so has_revision(None) == True.
 
858
        self.assertEqual(True, repo.has_revision(NULL_REVISION))
 
859
 
 
860
        # The remote repo shouldn't be accessed.
 
861
        self.assertEqual([], client._calls)
 
862
 
 
863
 
 
864
class TestRepositoryTarball(TestRemoteRepository):
 
865
 
 
866
    # This is a canned tarball reponse we can validate against
 
867
    tarball_content = (
 
868
        'QlpoOTFBWSZTWdGkj3wAAWF/k8aQACBIB//A9+8cIX/v33AACEAYABAECEACNz'
 
869
        'JqsgJJFPTSnk1A3qh6mTQAAAANPUHkagkSTEkaA09QaNAAAGgAAAcwCYCZGAEY'
 
870
        'mJhMJghpiaYBUkKammSHqNMZQ0NABkNAeo0AGneAevnlwQoGzEzNVzaYxp/1Uk'
 
871
        'xXzA1CQX0BJMZZLcPBrluJir5SQyijWHYZ6ZUtVqqlYDdB2QoCwa9GyWwGYDMA'
 
872
        'OQYhkpLt/OKFnnlT8E0PmO8+ZNSo2WWqeCzGB5fBXZ3IvV7uNJVE7DYnWj6qwB'
 
873
        'k5DJDIrQ5OQHHIjkS9KqwG3mc3t+F1+iujb89ufyBNIKCgeZBWrl5cXxbMGoMs'
 
874
        'c9JuUkg5YsiVcaZJurc6KLi6yKOkgCUOlIlOpOoXyrTJjK8ZgbklReDdwGmFgt'
 
875
        'dkVsAIslSVCd4AtACSLbyhLHryfb14PKegrVDba+U8OL6KQtzdM5HLjAc8/p6n'
 
876
        '0lgaWU8skgO7xupPTkyuwheSckejFLK5T4ZOo0Gda9viaIhpD1Qn7JqqlKAJqC'
 
877
        'QplPKp2nqBWAfwBGaOwVrz3y1T+UZZNismXHsb2Jq18T+VaD9k4P8DqE3g70qV'
 
878
        'JLurpnDI6VS5oqDDPVbtVjMxMxMg4rzQVipn2Bv1fVNK0iq3Gl0hhnnHKm/egy'
 
879
        'nWQ7QH/F3JFOFCQ0aSPfA='
 
880
        ).decode('base64')
 
881
 
 
882
    def test_repository_tarball(self):
 
883
        # Test that Repository.tarball generates the right operations
 
884
        transport_path = 'repo'
 
885
        expected_responses = [(('ok',), self.tarball_content),
 
886
            ]
 
887
        expected_calls = [('call_expecting_body', 'Repository.tarball',
 
888
                           ('repo/', 'bz2',),),
 
889
            ]
 
890
        remote_repo, client = self.setup_fake_client_and_repository(
 
891
            expected_responses, transport_path)
 
892
        # Now actually ask for the tarball
 
893
        tarball_file = remote_repo._get_tarball('bz2')
 
894
        try:
 
895
            self.assertEqual(expected_calls, client._calls)
 
896
            self.assertEqual(self.tarball_content, tarball_file.read())
 
897
        finally:
 
898
            tarball_file.close()
 
899
 
 
900
 
 
901
class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
 
902
    """RemoteRepository.copy_content_into optimizations"""
 
903
 
 
904
    def test_copy_content_remote_to_local(self):
 
905
        self.transport_server = server.SmartTCPServer_for_testing
 
906
        src_repo = self.make_repository('repo1')
 
907
        src_repo = repository.Repository.open(self.get_url('repo1'))
 
908
        # At the moment the tarball-based copy_content_into can't write back
 
909
        # into a smart server.  It would be good if it could upload the
 
910
        # tarball; once that works we'd have to create repositories of
 
911
        # different formats. -- mbp 20070410
 
912
        dest_url = self.get_vfs_only_url('repo2')
 
913
        dest_bzrdir = BzrDir.create(dest_url)
 
914
        dest_repo = dest_bzrdir.create_repository()
 
915
        self.assertFalse(isinstance(dest_repo, RemoteRepository))
 
916
        self.assertTrue(isinstance(src_repo, RemoteRepository))
 
917
        src_repo.copy_content_into(dest_repo)
 
918
 
 
919
 
 
920
class TestRepositoryStreamKnitData(TestRemoteRepository):
 
921
 
 
922
    def make_pack_file(self, records):
 
923
        pack_file = StringIO()
 
924
        pack_writer = pack.ContainerWriter(pack_file.write)
 
925
        pack_writer.begin()
 
926
        for bytes, names in records:
 
927
            pack_writer.add_bytes_record(bytes, names)
 
928
        pack_writer.end()
 
929
        pack_file.seek(0)
 
930
        return pack_file
 
931
 
 
932
    def make_pack_stream(self, records):
 
933
        pack_serialiser = pack.ContainerSerialiser()
 
934
        yield pack_serialiser.begin()
 
935
        for bytes, names in records:
 
936
            yield pack_serialiser.bytes_record(bytes, names)
 
937
        yield pack_serialiser.end()
 
938
 
 
939
    def test_bad_pack_from_server(self):
 
940
        """A response with invalid data (e.g. it has a record with multiple
 
941
        names) triggers an exception.
 
942
        
 
943
        Not all possible errors will be caught at this stage, but obviously
 
944
        malformed data should be.
 
945
        """
 
946
        record = ('bytes', [('name1',), ('name2',)])
 
947
        pack_stream = self.make_pack_stream([record])
 
948
        responses = [(('ok',), pack_stream), ]
 
949
        transport_path = 'quack'
 
950
        repo, client = self.setup_fake_client_and_repository(
 
951
            responses, transport_path)
 
952
        search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
 
953
        stream = repo.get_data_stream_for_search(search)
 
954
        self.assertRaises(errors.SmartProtocolError, list, stream)
 
955
    
 
956
    def test_backwards_compatibility(self):
 
957
        """If the server doesn't recognise this request, fallback to VFS."""
 
958
        error_msg = (
 
959
            "Generic bzr smart protocol error: "
 
960
            "bad request 'Repository.stream_revisions_chunked'")
 
961
        responses = [
 
962
            (('error', error_msg), '')]
 
963
        repo, client = self.setup_fake_client_and_repository(
 
964
            responses, 'path')
 
965
        self.mock_called = False
 
966
        repo._real_repository = MockRealRepository(self)
 
967
        search = graph.SearchResult(set(['revid']), set(), 1, set(['revid']))
 
968
        repo.get_data_stream_for_search(search)
 
969
        self.assertTrue(self.mock_called)
 
970
        self.failIf(client.expecting_body,
 
971
            "The protocol has been left in an unclean state that will cause "
 
972
            "TooManyConcurrentRequests errors.")
 
973
 
 
974
 
 
975
class MockRealRepository(object):
 
976
    """Helper class for TestRepositoryStreamKnitData.test_unknown_method."""
 
977
 
 
978
    def __init__(self, test):
 
979
        self.test = test
 
980
 
 
981
    def get_data_stream_for_search(self, search):
 
982
        self.test.assertEqual(set(['revid']), search.get_keys())
 
983
        self.test.mock_called = True
 
984
 
 
985