~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_remote.py

  • Committer: Martin Pool
  • Date: 2008-03-16 08:25:21 UTC
  • mto: (3280.2.2 prepare-1.3)
  • mto: This revision was merged to the branch mainline in revision 3284.
  • Revision ID: mbp@sourcefrog.net-20080316082521-xmex8wq1uyj6cxyh
Fix doctest syntax

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