/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: Ian Clatworthy
  • Date: 2008-03-27 07:51:10 UTC
  • mto: (3311.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3312.
  • Revision ID: ian.clatworthy@canonical.com-20080327075110-afgd7x03ybju06ez
Reduce evangelism in the User Guide

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