/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

 * ``Repository.get_data_stream`` is now deprecated in favour of
   ``Repository.get_data_stream_for_search`` which allows less network
   traffic when requesting data streams over a smart server. (Robert Collins)

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