/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: Robert Collins
  • Date: 2008-03-28 03:54:40 UTC
  • mto: This revision was merged to the branch mainline in revision 3313.
  • Revision ID: robertc@robertcollins.net-20080328035440-4cflvryqujresn2g
Reduce code duplication as per review.

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
        self.client = self.transport.get_smart_client()
 
60
        # make a branch that can be opened over the smart transport
 
61
        self.local_wt = BzrDir.create_standalone_workingtree('.')
 
62
 
 
63
    def tearDown(self):
 
64
        self.transport.disconnect()
 
65
        tests.TestCaseWithTransport.tearDown(self)
 
66
 
 
67
    def test_create_remote_bzrdir(self):
 
68
        b = remote.RemoteBzrDir(self.transport)
 
69
        self.assertIsInstance(b, BzrDir)
 
70
 
 
71
    def test_open_remote_branch(self):
 
72
        # open a standalone branch in the working directory
 
73
        b = remote.RemoteBzrDir(self.transport)
 
74
        branch = b.open_branch()
 
75
        self.assertIsInstance(branch, Branch)
 
76
 
 
77
    def test_remote_repository(self):
 
78
        b = BzrDir.open_from_transport(self.transport)
 
79
        repo = b.open_repository()
 
80
        revid = u'\xc823123123'.encode('utf8')
 
81
        self.assertFalse(repo.has_revision(revid))
 
82
        self.local_wt.commit(message='test commit', rev_id=revid)
 
83
        self.assertTrue(repo.has_revision(revid))
 
84
 
 
85
    def test_remote_branch_revision_history(self):
 
86
        b = BzrDir.open_from_transport(self.transport).open_branch()
 
87
        self.assertEqual([], b.revision_history())
 
88
        r1 = self.local_wt.commit('1st commit')
 
89
        r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8'.encode('utf8'))
 
90
        self.assertEqual([r1, r2], b.revision_history())
 
91
 
 
92
    def test_find_correct_format(self):
 
93
        """Should open a RemoteBzrDir over a RemoteTransport"""
 
94
        fmt = BzrDirFormat.find_format(self.transport)
 
95
        self.assertTrue(RemoteBzrDirFormat
 
96
                        in BzrDirFormat._control_server_formats)
 
97
        self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
 
98
 
 
99
    def test_open_detected_smart_format(self):
 
100
        fmt = BzrDirFormat.find_format(self.transport)
 
101
        d = fmt.open(self.transport)
 
102
        self.assertIsInstance(d, BzrDir)
 
103
 
 
104
    def test_remote_branch_repr(self):
 
105
        b = BzrDir.open_from_transport(self.transport).open_branch()
 
106
        self.assertStartsWith(str(b), 'RemoteBranch(')
 
107
 
 
108
 
 
109
class FakeProtocol(object):
 
110
    """Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
 
111
 
 
112
    def __init__(self, body, fake_client):
 
113
        self.body = body
 
114
        self._body_buffer = None
 
115
        self._fake_client = fake_client
 
116
 
 
117
    def read_body_bytes(self, count=-1):
 
118
        if self._body_buffer is None:
 
119
            self._body_buffer = StringIO(self.body)
 
120
        bytes = self._body_buffer.read(count)
 
121
        if self._body_buffer.tell() == len(self._body_buffer.getvalue()):
 
122
            self._fake_client.expecting_body = False
 
123
        return bytes
 
124
 
 
125
    def cancel_read_body(self):
 
126
        self._fake_client.expecting_body = False
 
127
 
 
128
    def read_streamed_body(self):
 
129
        return self.body
 
130
 
 
131
 
 
132
class FakeClient(_SmartClient):
 
133
    """Lookalike for _SmartClient allowing testing."""
 
134
    
 
135
    def __init__(self, responses, fake_medium_base='fake base'):
 
136
        """Create a FakeClient.
 
137
 
 
138
        :param responses: A list of response-tuple, body-data pairs to be sent
 
139
            back to callers.
 
140
        """
 
141
        self.responses = responses
 
142
        self._calls = []
 
143
        self.expecting_body = False
 
144
        _SmartClient.__init__(self, FakeMedium(fake_medium_base, self._calls))
 
145
 
 
146
    def call(self, method, *args):
 
147
        self._calls.append(('call', method, args))
 
148
        return self.responses.pop(0)[0]
 
149
 
 
150
    def call_expecting_body(self, method, *args):
 
151
        self._calls.append(('call_expecting_body', method, args))
 
152
        result = self.responses.pop(0)
 
153
        self.expecting_body = True
 
154
        return result[0], FakeProtocol(result[1], self)
 
155
 
 
156
    def call_with_body_bytes_expecting_body(self, method, args, body):
 
157
        self._calls.append(('call_with_body_bytes_expecting_body', method,
 
158
            args, body))
 
159
        result = self.responses.pop(0)
 
160
        self.expecting_body = True
 
161
        return result[0], FakeProtocol(result[1], self)
 
162
 
 
163
 
 
164
class FakeMedium(object):
 
165
 
 
166
    def __init__(self, base, client_calls):
 
167
        self.base = base
 
168
        self.connection = FakeConnection(client_calls)
 
169
        self._client_calls = client_calls
 
170
 
 
171
 
 
172
class FakeConnection(object):
 
173
 
 
174
    def __init__(self, client_calls):
 
175
        self._remote_is_at_least_1_2 = True
 
176
        self._client_calls = client_calls
 
177
 
 
178
    def disconnect(self):
 
179
        self._client_calls.append(('disconnect medium',))
 
180
 
 
181
 
 
182
class TestVfsHas(tests.TestCase):
 
183
 
 
184
    def test_unicode_path(self):
 
185
        client = FakeClient([(('yes',), )], '/')
 
186
        transport = RemoteTransport('bzr://localhost/', _client=client)
 
187
        filename = u'/hell\u00d8'.encode('utf8')
 
188
        result = transport.has(filename)
 
189
        self.assertEqual(
 
190
            [('call', 'has', (filename,))],
 
191
            client._calls)
 
192
        self.assertTrue(result)
 
193
 
 
194
 
 
195
class TestBzrDirOpenBranch(tests.TestCase):
 
196
 
 
197
    def test_branch_present(self):
 
198
        transport = MemoryTransport()
 
199
        transport.mkdir('quack')
 
200
        transport = transport.clone('quack')
 
201
        client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
 
202
                            transport.base)
 
203
        bzrdir = RemoteBzrDir(transport, _client=client)
 
204
        result = bzrdir.open_branch()
 
205
        self.assertEqual(
 
206
            [('call', 'BzrDir.open_branch', ('quack/',)),
 
207
             ('call', 'BzrDir.find_repositoryV2', ('quack/',))],
 
208
            client._calls)
 
209
        self.assertIsInstance(result, RemoteBranch)
 
210
        self.assertEqual(bzrdir, result.bzrdir)
 
211
 
 
212
    def test_branch_missing(self):
 
213
        transport = MemoryTransport()
 
214
        transport.mkdir('quack')
 
215
        transport = transport.clone('quack')
 
216
        client = FakeClient([(('nobranch',), )], transport.base)
 
217
        bzrdir = RemoteBzrDir(transport, _client=client)
 
218
        self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
 
219
        self.assertEqual(
 
220
            [('call', 'BzrDir.open_branch', ('quack/',))],
 
221
            client._calls)
 
222
 
 
223
    def test__get_tree_branch(self):
 
224
        # _get_tree_branch is a form of open_branch, but it should only ask for
 
225
        # branch opening, not any other network requests.
 
226
        calls = []
 
227
        def open_branch():
 
228
            calls.append("Called")
 
229
            return "a-branch"
 
230
        transport = MemoryTransport()
 
231
        # no requests on the network - catches other api calls being made.
 
232
        client = FakeClient([], transport.base)
 
233
        bzrdir = RemoteBzrDir(transport, _client=client)
 
234
        # patch the open_branch call to record that it was called.
 
235
        bzrdir.open_branch = open_branch
 
236
        self.assertEqual((None, "a-branch"), bzrdir._get_tree_branch())
 
237
        self.assertEqual(["Called"], calls)
 
238
        self.assertEqual([], client._calls)
 
239
 
 
240
    def test_url_quoting_of_path(self):
 
241
        # Relpaths on the wire should not be URL-escaped.  So "~" should be
 
242
        # transmitted as "~", not "%7E".
 
243
        transport = RemoteTransport('bzr://localhost/~hello/')
 
244
        client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no', 'no'), )],
 
245
                            transport.base)
 
246
        bzrdir = RemoteBzrDir(transport, _client=client)
 
247
        result = bzrdir.open_branch()
 
248
        self.assertEqual(
 
249
            [('call', 'BzrDir.open_branch', ('~hello/',)),
 
250
             ('call', 'BzrDir.find_repositoryV2', ('~hello/',))],
 
251
            client._calls)
 
252
 
 
253
    def check_open_repository(self, rich_root, subtrees, external_lookup='no'):
 
254
        transport = MemoryTransport()
 
255
        transport.mkdir('quack')
 
256
        transport = transport.clone('quack')
 
257
        if rich_root:
 
258
            rich_response = 'yes'
 
259
        else:
 
260
            rich_response = 'no'
 
261
        if subtrees:
 
262
            subtree_response = 'yes'
 
263
        else:
 
264
            subtree_response = 'no'
 
265
        client = FakeClient(
 
266
            [(('ok', '', rich_response, subtree_response, external_lookup), ),],
 
267
            transport.base)
 
268
        bzrdir = RemoteBzrDir(transport, _client=client)
 
269
        result = bzrdir.open_repository()
 
270
        self.assertEqual(
 
271
            [('call', 'BzrDir.find_repositoryV2', ('quack/',))],
 
272
            client._calls)
 
273
        self.assertIsInstance(result, RemoteRepository)
 
274
        self.assertEqual(bzrdir, result.bzrdir)
 
275
        self.assertEqual(rich_root, result._format.rich_root_data)
 
276
        self.assertEqual(subtrees, result._format.supports_tree_reference)
 
277
 
 
278
    def test_open_repository_sets_format_attributes(self):
 
279
        self.check_open_repository(True, True)
 
280
        self.check_open_repository(False, True)
 
281
        self.check_open_repository(True, False)
 
282
        self.check_open_repository(False, False)
 
283
        self.check_open_repository(False, False, 'yes')
 
284
 
 
285
    def test_old_server(self):
 
286
        """RemoteBzrDirFormat should fail to probe if the server version is too
 
287
        old.
 
288
        """
 
289
        self.assertRaises(errors.NotBranchError,
 
290
            RemoteBzrDirFormat.probe_transport, OldServerTransport())
 
291
 
 
292
 
 
293
class OldSmartClient(object):
 
294
    """A fake smart client for test_old_version that just returns a version one
 
295
    response to the 'hello' (query version) command.
 
296
    """
 
297
 
 
298
    def get_request(self):
 
299
        input_file = StringIO('ok\x011\n')
 
300
        output_file = StringIO()
 
301
        client_medium = medium.SmartSimplePipesClientMedium(
 
302
            input_file, output_file)
 
303
        return medium.SmartClientStreamMediumRequest(client_medium)
 
304
 
 
305
 
 
306
class OldServerTransport(object):
 
307
    """A fake transport for test_old_server that reports it's smart server
 
308
    protocol version as version one.
 
309
    """
 
310
 
 
311
    def __init__(self):
 
312
        self.base = 'fake:'
 
313
 
 
314
    def get_smart_client(self):
 
315
        return OldSmartClient()
 
316
 
 
317
 
 
318
class TestBranchLastRevisionInfo(tests.TestCase):
 
319
 
 
320
    def test_empty_branch(self):
 
321
        # in an empty branch we decode the response properly
 
322
        transport = MemoryTransport()
 
323
        client = FakeClient([(('ok', '0', 'null:'), )], transport.base)
 
324
        transport.mkdir('quack')
 
325
        transport = transport.clone('quack')
 
326
        # we do not want bzrdir to make any remote calls
 
327
        bzrdir = RemoteBzrDir(transport, _client=False)
 
328
        branch = RemoteBranch(bzrdir, None, _client=client)
 
329
        result = branch.last_revision_info()
 
330
 
 
331
        self.assertEqual(
 
332
            [('call', 'Branch.last_revision_info', ('quack/',))],
 
333
            client._calls)
 
334
        self.assertEqual((0, NULL_REVISION), result)
 
335
 
 
336
    def test_non_empty_branch(self):
 
337
        # in a non-empty branch we also decode the response properly
 
338
        revid = u'\xc8'.encode('utf8')
 
339
        transport = MemoryTransport()
 
340
        client = FakeClient([(('ok', '2', revid), )], transport.base)
 
341
        transport.mkdir('kwaak')
 
342
        transport = transport.clone('kwaak')
 
343
        # we do not want bzrdir to make any remote calls
 
344
        bzrdir = RemoteBzrDir(transport, _client=False)
 
345
        branch = RemoteBranch(bzrdir, None, _client=client)
 
346
        result = branch.last_revision_info()
 
347
 
 
348
        self.assertEqual(
 
349
            [('call', 'Branch.last_revision_info', ('kwaak/',))],
 
350
            client._calls)
 
351
        self.assertEqual((2, revid), result)
 
352
 
 
353
 
 
354
class TestBranchSetLastRevision(tests.TestCase):
 
355
 
 
356
    def test_set_empty(self):
 
357
        # set_revision_history([]) is translated to calling
 
358
        # Branch.set_last_revision(path, '') on the wire.
 
359
        transport = MemoryTransport()
 
360
        transport.mkdir('branch')
 
361
        transport = transport.clone('branch')
 
362
 
 
363
        client = FakeClient([
 
364
            # lock_write
 
365
            (('ok', 'branch token', 'repo token'), ),
 
366
            # set_last_revision
 
367
            (('ok',), ),
 
368
            # unlock
 
369
            (('ok',), )],
 
370
            transport.base)
 
371
        bzrdir = RemoteBzrDir(transport, _client=False)
 
372
        branch = RemoteBranch(bzrdir, None, _client=client)
 
373
        # This is a hack to work around the problem that RemoteBranch currently
 
374
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
375
        branch._ensure_real = lambda: None
 
376
        branch.lock_write()
 
377
        client._calls = []
 
378
        result = branch.set_revision_history([])
 
379
        self.assertEqual(
 
380
            [('call', 'Branch.set_last_revision',
 
381
                ('branch/', 'branch token', 'repo token', 'null:'))],
 
382
            client._calls)
 
383
        branch.unlock()
 
384
        self.assertEqual(None, result)
 
385
 
 
386
    def test_set_nonempty(self):
 
387
        # set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
 
388
        # Branch.set_last_revision(path, rev-idN) on the wire.
 
389
        transport = MemoryTransport()
 
390
        transport.mkdir('branch')
 
391
        transport = transport.clone('branch')
 
392
 
 
393
        client = FakeClient([
 
394
            # lock_write
 
395
            (('ok', 'branch token', 'repo token'), ),
 
396
            # set_last_revision
 
397
            (('ok',), ),
 
398
            # unlock
 
399
            (('ok',), )],
 
400
            transport.base)
 
401
        bzrdir = RemoteBzrDir(transport, _client=False)
 
402
        branch = RemoteBranch(bzrdir, None, _client=client)
 
403
        # This is a hack to work around the problem that RemoteBranch currently
 
404
        # unnecessarily invokes _ensure_real upon a call to lock_write.
 
405
        branch._ensure_real = lambda: None
 
406
        # Lock the branch, reset the record of remote calls.
 
407
        branch.lock_write()
 
408
        client._calls = []
 
409
 
 
410
        result = branch.set_revision_history(['rev-id1', 'rev-id2'])
 
411
        self.assertEqual(
 
412
            [('call', 'Branch.set_last_revision',
 
413
                ('branch/', 'branch token', 'repo token', 'rev-id2'))],
 
414
            client._calls)
 
415
        branch.unlock()
 
416
        self.assertEqual(None, result)
 
417
 
 
418
    def test_no_such_revision(self):
 
419
        # A response of 'NoSuchRevision' is translated into an exception.
 
420
        client = FakeClient([
 
421
            # lock_write
 
422
            (('ok', 'branch token', 'repo token'), ),
 
423
            # set_last_revision
 
424
            (('NoSuchRevision', 'rev-id'), ),
 
425
            # unlock
 
426
            (('ok',), )])
 
427
        transport = MemoryTransport()
 
428
        transport.mkdir('branch')
 
429
        transport = transport.clone('branch')
 
430
 
 
431
        bzrdir = RemoteBzrDir(transport, _client=False)
 
432
        branch = RemoteBranch(bzrdir, None, _client=client)
 
433
        branch._ensure_real = lambda: None
 
434
        branch.lock_write()
 
435
        client._calls = []
 
436
 
 
437
        self.assertRaises(
 
438
            errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
 
439
        branch.unlock()
 
440
 
 
441
 
 
442
class TestBranchControlGetBranchConf(tests.TestCaseWithMemoryTransport):
 
443
    """Test branch.control_files api munging...
 
444
 
 
445
    We special case RemoteBranch.control_files.get('branch.conf') to
 
446
    call a specific API so that RemoteBranch's can intercept configuration
 
447
    file reading, allowing them to signal to the client about things like
 
448
    'email is configured for commits'.
 
449
    """
 
450
 
 
451
    def test_get_branch_conf(self):
 
452
        # in an empty branch we decode the response properly
 
453
        client = FakeClient([(('ok', ), 'config file body')], self.get_url())
 
454
        # we need to make a real branch because the remote_branch.control_files
 
455
        # will trigger _ensure_real.
 
456
        branch = self.make_branch('quack')
 
457
        transport = branch.bzrdir.root_transport
 
458
        # we do not want bzrdir to make any remote calls
 
459
        bzrdir = RemoteBzrDir(transport, _client=False)
 
460
        branch = RemoteBranch(bzrdir, None, _client=client)
 
461
        result = branch.control_files.get('branch.conf')
 
462
        self.assertEqual(
 
463
            [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
 
464
            client._calls)
 
465
        self.assertEqual('config file body', result.read())
 
466
 
 
467
 
 
468
class TestBranchLockWrite(tests.TestCase):
 
469
 
 
470
    def test_lock_write_unlockable(self):
 
471
        transport = MemoryTransport()
 
472
        client = FakeClient([(('UnlockableTransport', ), '')], transport.base)
 
473
        transport.mkdir('quack')
 
474
        transport = transport.clone('quack')
 
475
        # we do not want bzrdir to make any remote calls
 
476
        bzrdir = RemoteBzrDir(transport, _client=False)
 
477
        branch = RemoteBranch(bzrdir, None, _client=client)
 
478
        self.assertRaises(errors.UnlockableTransport, branch.lock_write)
 
479
        self.assertEqual(
 
480
            [('call', 'Branch.lock_write', ('quack/', '', ''))],
 
481
            client._calls)
 
482
 
 
483
 
 
484
class TestTransportIsReadonly(tests.TestCase):
 
485
 
 
486
    def test_true(self):
 
487
        client = FakeClient([(('yes',), '')])
 
488
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
489
                                    _client=client)
 
490
        self.assertEqual(True, transport.is_readonly())
 
491
        self.assertEqual(
 
492
            [('call', 'Transport.is_readonly', ())],
 
493
            client._calls)
 
494
 
 
495
    def test_false(self):
 
496
        client = FakeClient([(('no',), '')])
 
497
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
498
                                    _client=client)
 
499
        self.assertEqual(False, transport.is_readonly())
 
500
        self.assertEqual(
 
501
            [('call', 'Transport.is_readonly', ())],
 
502
            client._calls)
 
503
 
 
504
    def test_error_from_old_server(self):
 
505
        """bzr 0.15 and earlier servers don't recognise the is_readonly verb.
 
506
        
 
507
        Clients should treat it as a "no" response, because is_readonly is only
 
508
        advisory anyway (a transport could be read-write, but then the
 
509
        underlying filesystem could be readonly anyway).
 
510
        """
 
511
        client = FakeClient([(
 
512
            ('error', "Generic bzr smart protocol error: "
 
513
                      "bad request 'Transport.is_readonly'"), '')])
 
514
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
515
                                    _client=client)
 
516
        self.assertEqual(False, transport.is_readonly())
 
517
        self.assertEqual(
 
518
            [('call', 'Transport.is_readonly', ())],
 
519
            client._calls)
 
520
 
 
521
    def test_error_from_old_0_11_server(self):
 
522
        """Same as test_error_from_old_server, but with the slightly different
 
523
        error message from bzr 0.11 servers.
 
524
        """
 
525
        client = FakeClient([(
 
526
            ('error', "Generic bzr smart protocol error: "
 
527
                      "bad request u'Transport.is_readonly'"), '')])
 
528
        transport = RemoteTransport('bzr://example.com/', medium=False,
 
529
                                    _client=client)
 
530
        self.assertEqual(False, transport.is_readonly())
 
531
        self.assertEqual(
 
532
            [('call', 'Transport.is_readonly', ())],
 
533
            client._calls)
 
534
 
 
535
 
 
536
class TestRemoteRepository(tests.TestCase):
 
537
    """Base for testing RemoteRepository protocol usage.
 
538
    
 
539
    These tests contain frozen requests and responses.  We want any changes to 
 
540
    what is sent or expected to be require a thoughtful update to these tests
 
541
    because they might break compatibility with different-versioned servers.
 
542
    """
 
543
 
 
544
    def setup_fake_client_and_repository(self, responses, transport_path):
 
545
        """Create the fake client and repository for testing with.
 
546
        
 
547
        There's no real server here; we just have canned responses sent
 
548
        back one by one.
 
549
        
 
550
        :param transport_path: Path below the root of the MemoryTransport
 
551
            where the repository will be created.
 
552
        """
 
553
        transport = MemoryTransport()
 
554
        transport.mkdir(transport_path)
 
555
        client = FakeClient(responses, transport.base)
 
556
        transport = transport.clone(transport_path)
 
557
        # we do not want bzrdir to make any remote calls
 
558
        bzrdir = RemoteBzrDir(transport, _client=False)
 
559
        repo = RemoteRepository(bzrdir, None, _client=client)
 
560
        return repo, client
 
561
 
 
562
 
 
563
class TestRepositoryGatherStats(TestRemoteRepository):
 
564
 
 
565
    def test_revid_none(self):
 
566
        # ('ok',), body with revisions and size
 
567
        responses = [(('ok', ), 'revisions: 2\nsize: 18\n')]
 
568
        transport_path = 'quack'
 
569
        repo, client = self.setup_fake_client_and_repository(
 
570
            responses, transport_path)
 
571
        result = repo.gather_stats(None)
 
572
        self.assertEqual(
 
573
            [('call_expecting_body', 'Repository.gather_stats',
 
574
             ('quack/','','no'))],
 
575
            client._calls)
 
576
        self.assertEqual({'revisions': 2, 'size': 18}, result)
 
577
 
 
578
    def test_revid_no_committers(self):
 
579
        # ('ok',), body without committers
 
580
        responses = [(('ok', ),
 
581
                      'firstrev: 123456.300 3600\n'
 
582
                      'latestrev: 654231.400 0\n'
 
583
                      'revisions: 2\n'
 
584
                      'size: 18\n')]
 
585
        transport_path = 'quick'
 
586
        revid = u'\xc8'.encode('utf8')
 
587
        repo, client = self.setup_fake_client_and_repository(
 
588
            responses, transport_path)
 
589
        result = repo.gather_stats(revid)
 
590
        self.assertEqual(
 
591
            [('call_expecting_body', 'Repository.gather_stats',
 
592
              ('quick/', revid, 'no'))],
 
593
            client._calls)
 
594
        self.assertEqual({'revisions': 2, 'size': 18,
 
595
                          'firstrev': (123456.300, 3600),
 
596
                          'latestrev': (654231.400, 0),},
 
597
                         result)
 
598
 
 
599
    def test_revid_with_committers(self):
 
600
        # ('ok',), body with committers
 
601
        responses = [(('ok', ),
 
602
                      'committers: 128\n'
 
603
                      'firstrev: 123456.300 3600\n'
 
604
                      'latestrev: 654231.400 0\n'
 
605
                      'revisions: 2\n'
 
606
                      'size: 18\n')]
 
607
        transport_path = 'buick'
 
608
        revid = u'\xc8'.encode('utf8')
 
609
        repo, client = self.setup_fake_client_and_repository(
 
610
            responses, transport_path)
 
611
        result = repo.gather_stats(revid, True)
 
612
        self.assertEqual(
 
613
            [('call_expecting_body', 'Repository.gather_stats',
 
614
              ('buick/', revid, 'yes'))],
 
615
            client._calls)
 
616
        self.assertEqual({'revisions': 2, 'size': 18,
 
617
                          'committers': 128,
 
618
                          'firstrev': (123456.300, 3600),
 
619
                          'latestrev': (654231.400, 0),},
 
620
                         result)
 
621
 
 
622
 
 
623
class TestRepositoryGetGraph(TestRemoteRepository):
 
624
 
 
625
    def test_get_graph(self):
 
626
        # get_graph returns a graph with the repository as the
 
627
        # parents_provider.
 
628
        responses = []
 
629
        transport_path = 'quack'
 
630
        repo, client = self.setup_fake_client_and_repository(
 
631
            responses, transport_path)
 
632
        graph = repo.get_graph()
 
633
        self.assertEqual(graph._parents_provider, repo)
 
634
 
 
635
 
 
636
class TestRepositoryGetParentMap(TestRemoteRepository):
 
637
 
 
638
    def test_get_parent_map_caching(self):
 
639
        # get_parent_map returns from cache until unlock()
 
640
        # setup a reponse with two revisions
 
641
        r1 = u'\u0e33'.encode('utf8')
 
642
        r2 = u'\u0dab'.encode('utf8')
 
643
        lines = [' '.join([r2, r1]), r1]
 
644
        encoded_body = bz2.compress('\n'.join(lines))
 
645
        responses = [(('ok', ), encoded_body), (('ok', ), encoded_body)]
 
646
 
 
647
        transport_path = 'quack'
 
648
        repo, client = self.setup_fake_client_and_repository(
 
649
            responses, transport_path)
 
650
        repo.lock_read()
 
651
        graph = repo.get_graph()
 
652
        parents = graph.get_parent_map([r2])
 
653
        self.assertEqual({r2: (r1,)}, parents)
 
654
        # locking and unlocking deeper should not reset
 
655
        repo.lock_read()
 
656
        repo.unlock()
 
657
        parents = graph.get_parent_map([r1])
 
658
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
659
        self.assertEqual(
 
660
            [('call_with_body_bytes_expecting_body',
 
661
              'Repository.get_parent_map', ('quack/', r2), '\n\n0')],
 
662
            client._calls)
 
663
        repo.unlock()
 
664
        # now we call again, and it should use the second response.
 
665
        repo.lock_read()
 
666
        graph = repo.get_graph()
 
667
        parents = graph.get_parent_map([r1])
 
668
        self.assertEqual({r1: (NULL_REVISION,)}, parents)
 
669
        self.assertEqual(
 
670
            [('call_with_body_bytes_expecting_body',
 
671
              'Repository.get_parent_map', ('quack/', r2), '\n\n0'),
 
672
             ('call_with_body_bytes_expecting_body',
 
673
              'Repository.get_parent_map', ('quack/', r1), '\n\n0'),
 
674
            ],
 
675
            client._calls)
 
676
        repo.unlock()
 
677
 
 
678
    def test_get_parent_map_reconnects_if_unknown_method(self):
 
679
        error_msg = (
 
680
            "Generic bzr smart protocol error: "
 
681
            "bad request 'Repository.get_parent_map'")
 
682
        responses = [
 
683
            (('error', error_msg), ''),
 
684
            (('ok',), '')]
 
685
        transport_path = 'quack'
 
686
        repo, client = self.setup_fake_client_and_repository(
 
687
            responses, transport_path)
 
688
        rev_id = 'revision-id'
 
689
        parents = repo.get_parent_map([rev_id])
 
690
        self.assertEqual(
 
691
            [('call_with_body_bytes_expecting_body',
 
692
              'Repository.get_parent_map', ('quack/', rev_id), '\n\n0'),
 
693
             ('disconnect medium',),
 
694
             ('call_expecting_body', 'Repository.get_revision_graph',
 
695
              ('quack/', ''))],
 
696
            client._calls)
 
697
 
 
698
 
 
699
 
 
700
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
 
701
    
 
702
    def test_null_revision(self):
 
703
        # a null revision has the predictable result {}, we should have no wire
 
704
        # traffic when calling it with this argument
 
705
        responses = [(('notused', ), '')]
 
706
        transport_path = 'empty'
 
707
        repo, client = self.setup_fake_client_and_repository(
 
708
            responses, transport_path)
 
709
        result = self.applyDeprecated(one_four, repo.get_revision_graph,
 
710
            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 = self.applyDeprecated(one_four, 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 = self.applyDeprecated(one_four, 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
            self.applyDeprecated, one_four, 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