~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_smart_transport.py

  • Committer: Patch Queue Manager
  • Date: 2015-12-17 18:39:00 UTC
  • mfrom: (6606.1.2 fix-float)
  • Revision ID: pqm@pqm.ubuntu.com-20151217183900-0719du2uv1kwu3lc
(vila) Inline testtools private method to fix an issue in xenial (the
 private implementation has changed in an backward incompatible way).
 (Jelmer Vernooij)

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
# all of this deals with byte strings so this is safe
20
20
from cStringIO import StringIO
21
21
import doctest
 
22
import errno
22
23
import os
23
24
import socket
 
25
import subprocess
24
26
import sys
25
27
import threading
26
28
import time
30
32
import bzrlib
31
33
from bzrlib import (
32
34
        bzrdir,
 
35
        controldir,
 
36
        debug,
33
37
        errors,
34
38
        osutils,
35
39
        tests,
59
63
        )
60
64
 
61
65
 
 
66
def create_file_pipes():
 
67
    r, w = os.pipe()
 
68
    # These must be opened without buffering, or we get undefined results
 
69
    rf = os.fdopen(r, 'rb', 0)
 
70
    wf = os.fdopen(w, 'wb', 0)
 
71
    return rf, wf
 
72
 
 
73
 
62
74
def portable_socket_pair():
63
75
    """Return a pair of TCP sockets connected to each other.
64
76
 
88
100
        return StringIOSSHConnection(self)
89
101
 
90
102
 
 
103
class FirstRejectedStringIOSSHVendor(StringIOSSHVendor):
 
104
    """The first connection will be considered closed.
 
105
 
 
106
    The second connection will succeed normally.
 
107
    """
 
108
 
 
109
    def __init__(self, read_from, write_to, fail_at_write=True):
 
110
        super(FirstRejectedStringIOSSHVendor, self).__init__(read_from,
 
111
            write_to)
 
112
        self.fail_at_write = fail_at_write
 
113
        self._first = True
 
114
 
 
115
    def connect_ssh(self, username, password, host, port, command):
 
116
        self.calls.append(('connect_ssh', username, password, host, port,
 
117
            command))
 
118
        if self._first:
 
119
            self._first = False
 
120
            return ClosedSSHConnection(self)
 
121
        return StringIOSSHConnection(self)
 
122
 
 
123
 
91
124
class StringIOSSHConnection(ssh.SSHConnection):
92
125
    """A SSH connection that uses StringIO to buffer writes and answer reads."""
93
126
 
103
136
        return 'pipes', (self.vendor.read_from, self.vendor.write_to)
104
137
 
105
138
 
 
139
class ClosedSSHConnection(ssh.SSHConnection):
 
140
    """An SSH connection that just has closed channels."""
 
141
 
 
142
    def __init__(self, vendor):
 
143
        self.vendor = vendor
 
144
 
 
145
    def close(self):
 
146
        self.vendor.calls.append(('close', ))
 
147
 
 
148
    def get_sock_or_pipes(self):
 
149
        # We create matching pipes, and then close the ssh side
 
150
        bzr_read, ssh_write = create_file_pipes()
 
151
        # We always fail when bzr goes to read
 
152
        ssh_write.close()
 
153
        if self.vendor.fail_at_write:
 
154
            # If set, we'll also fail when bzr goes to write
 
155
            ssh_read, bzr_write = create_file_pipes()
 
156
            ssh_read.close()
 
157
        else:
 
158
            bzr_write = self.vendor.write_to
 
159
        return 'pipes', (bzr_read, bzr_write)
 
160
 
 
161
 
106
162
class _InvalidHostnameFeature(features.Feature):
107
163
    """Does 'non_existent.invalid' fail to resolve?
108
164
 
198
254
        client_medium._accept_bytes('abc')
199
255
        self.assertEqual('abc', output.getvalue())
200
256
 
 
257
    def test_simple_pipes__accept_bytes_subprocess_closed(self):
 
258
        # It is unfortunate that we have to use Popen for this. However,
 
259
        # os.pipe() does not behave the same as subprocess.Popen().
 
260
        # On Windows, if you use os.pipe() and close the write side,
 
261
        # read.read() hangs. On Linux, read.read() returns the empty string.
 
262
        p = subprocess.Popen([sys.executable, '-c',
 
263
            'import sys\n'
 
264
            'sys.stdout.write(sys.stdin.read(4))\n'
 
265
            'sys.stdout.close()\n'],
 
266
            stdout=subprocess.PIPE, stdin=subprocess.PIPE)
 
267
        client_medium = medium.SmartSimplePipesClientMedium(
 
268
            p.stdout, p.stdin, 'base')
 
269
        client_medium._accept_bytes('abc\n')
 
270
        self.assertEqual('abc', client_medium._read_bytes(3))
 
271
        p.wait()
 
272
        # While writing to the underlying pipe,
 
273
        #   Windows py2.6.6 we get IOError(EINVAL)
 
274
        #   Lucid py2.6.5, we get IOError(EPIPE)
 
275
        # In both cases, it should be wrapped to ConnectionReset
 
276
        self.assertRaises(errors.ConnectionReset,
 
277
                          client_medium._accept_bytes, 'more')
 
278
 
 
279
    def test_simple_pipes__accept_bytes_pipe_closed(self):
 
280
        child_read, client_write = create_file_pipes()
 
281
        client_medium = medium.SmartSimplePipesClientMedium(
 
282
            None, client_write, 'base')
 
283
        client_medium._accept_bytes('abc\n')
 
284
        self.assertEqual('abc\n', child_read.read(4))
 
285
        # While writing to the underlying pipe,
 
286
        #   Windows py2.6.6 we get IOError(EINVAL)
 
287
        #   Lucid py2.6.5, we get IOError(EPIPE)
 
288
        # In both cases, it should be wrapped to ConnectionReset
 
289
        child_read.close()
 
290
        self.assertRaises(errors.ConnectionReset,
 
291
                          client_medium._accept_bytes, 'more')
 
292
 
 
293
    def test_simple_pipes__flush_pipe_closed(self):
 
294
        child_read, client_write = create_file_pipes()
 
295
        client_medium = medium.SmartSimplePipesClientMedium(
 
296
            None, client_write, 'base')
 
297
        client_medium._accept_bytes('abc\n')
 
298
        child_read.close()
 
299
        # Even though the pipe is closed, flush on the write side seems to be a
 
300
        # no-op, rather than a failure.
 
301
        client_medium._flush()
 
302
 
 
303
    def test_simple_pipes__flush_subprocess_closed(self):
 
304
        p = subprocess.Popen([sys.executable, '-c',
 
305
            'import sys\n'
 
306
            'sys.stdout.write(sys.stdin.read(4))\n'
 
307
            'sys.stdout.close()\n'],
 
308
            stdout=subprocess.PIPE, stdin=subprocess.PIPE)
 
309
        client_medium = medium.SmartSimplePipesClientMedium(
 
310
            p.stdout, p.stdin, 'base')
 
311
        client_medium._accept_bytes('abc\n')
 
312
        p.wait()
 
313
        # Even though the child process is dead, flush seems to be a no-op.
 
314
        client_medium._flush()
 
315
 
 
316
    def test_simple_pipes__read_bytes_pipe_closed(self):
 
317
        child_read, client_write = create_file_pipes()
 
318
        client_medium = medium.SmartSimplePipesClientMedium(
 
319
            child_read, client_write, 'base')
 
320
        client_medium._accept_bytes('abc\n')
 
321
        client_write.close()
 
322
        self.assertEqual('abc\n', client_medium._read_bytes(4))
 
323
        self.assertEqual('', client_medium._read_bytes(4))
 
324
 
 
325
    def test_simple_pipes__read_bytes_subprocess_closed(self):
 
326
        p = subprocess.Popen([sys.executable, '-c',
 
327
            'import sys\n'
 
328
            'if sys.platform == "win32":\n'
 
329
            '    import msvcrt, os\n'
 
330
            '    msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)\n'
 
331
            '    msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)\n'
 
332
            'sys.stdout.write(sys.stdin.read(4))\n'
 
333
            'sys.stdout.close()\n'],
 
334
            stdout=subprocess.PIPE, stdin=subprocess.PIPE)
 
335
        client_medium = medium.SmartSimplePipesClientMedium(
 
336
            p.stdout, p.stdin, 'base')
 
337
        client_medium._accept_bytes('abc\n')
 
338
        p.wait()
 
339
        self.assertEqual('abc\n', client_medium._read_bytes(4))
 
340
        self.assertEqual('', client_medium._read_bytes(4))
 
341
 
201
342
    def test_simple_pipes_client_disconnect_does_nothing(self):
202
343
        # calling disconnect does nothing.
203
344
        input = StringIO()
359
500
            ],
360
501
            vendor.calls)
361
502
 
 
503
    def test_ssh_client_repr(self):
 
504
        client_medium = medium.SmartSSHClientMedium(
 
505
            'base', medium.SSHParams("example.com", "4242", "username"))
 
506
        self.assertEquals(
 
507
            "SmartSSHClientMedium(bzr+ssh://username@example.com:4242/)",
 
508
            repr(client_medium))
 
509
 
 
510
    def test_ssh_client_repr_no_port(self):
 
511
        client_medium = medium.SmartSSHClientMedium(
 
512
            'base', medium.SSHParams("example.com", None, "username"))
 
513
        self.assertEquals(
 
514
            "SmartSSHClientMedium(bzr+ssh://username@example.com/)",
 
515
            repr(client_medium))
 
516
 
 
517
    def test_ssh_client_repr_no_username(self):
 
518
        client_medium = medium.SmartSSHClientMedium(
 
519
            'base', medium.SSHParams("example.com", None, None))
 
520
        self.assertEquals(
 
521
            "SmartSSHClientMedium(bzr+ssh://example.com/)",
 
522
            repr(client_medium))
 
523
 
362
524
    def test_ssh_client_ignores_disconnect_when_not_connected(self):
363
525
        # Doing a disconnect on a new (and thus unconnected) SSH medium
364
526
        # does not fail.  It's ok to disconnect an unconnected medium.
585
747
        request.finished_reading()
586
748
        self.assertRaises(errors.ReadingCompleted, request.read_bytes, None)
587
749
 
 
750
    def test_reset(self):
 
751
        server_sock, client_sock = portable_socket_pair()
 
752
        # TODO: Use SmartClientAlreadyConnectedSocketMedium for the versions of
 
753
        #       bzr where it exists.
 
754
        client_medium = medium.SmartTCPClientMedium(None, None, None)
 
755
        client_medium._socket = client_sock
 
756
        client_medium._connected = True
 
757
        req = client_medium.get_request()
 
758
        self.assertRaises(errors.TooManyConcurrentRequests,
 
759
            client_medium.get_request)
 
760
        client_medium.reset()
 
761
        # The stream should be reset, marked as disconnected, though ready for
 
762
        # us to make a new request
 
763
        self.assertFalse(client_medium._connected)
 
764
        self.assertIs(None, client_medium._socket)
 
765
        try:
 
766
            self.assertEqual('', client_sock.recv(1))
 
767
        except socket.error, e:
 
768
            if e.errno not in (errno.EBADF,):
 
769
                raise
 
770
        req = client_medium.get_request()
 
771
 
588
772
 
589
773
class RemoteTransportTests(test_smart.TestCaseWithSmartMedium):
590
774
 
1269
1453
        server._stopped.wait()
1270
1454
        # It should not be accepting another connection.
1271
1455
        self.assertRaises(socket.error, self.connect_to_server, server)
1272
 
        # It should also not be fully stopped
1273
 
        server._fully_stopped.wait(0.01)
1274
 
        self.assertFalse(server._fully_stopped.isSet())
1275
1456
        response_handler.read_body_bytes()
1276
1457
        client_sock.close()
1277
1458
        server_side_thread.join()
1445
1626
        transport = self.transport
1446
1627
        t = self.backing_transport
1447
1628
        bzrdir.BzrDirFormat.get_default_format().initialize_on_transport(t)
1448
 
        result_dir = bzrdir.BzrDir.open_containing_from_transport(transport)
 
1629
        result_dir = controldir.ControlDir.open_containing_from_transport(
 
1630
            transport)
1449
1631
 
1450
1632
 
1451
1633
class ReadOnlyEndToEndTests(SmartTCPTests):
3101
3283
            'e', # end
3102
3284
            output.getvalue())
3103
3285
 
 
3286
    def test_records_start_of_body_stream(self):
 
3287
        requester, output = self.make_client_encoder_and_output()
 
3288
        requester.set_headers({})
 
3289
        in_stream = [False]
 
3290
        def stream_checker():
 
3291
            self.assertTrue(requester.body_stream_started)
 
3292
            in_stream[0] = True
 
3293
            yield 'content'
 
3294
        flush_called = []
 
3295
        orig_flush = requester.flush
 
3296
        def tracked_flush():
 
3297
            flush_called.append(in_stream[0])
 
3298
            if in_stream[0]:
 
3299
                self.assertTrue(requester.body_stream_started)
 
3300
            else:
 
3301
                self.assertFalse(requester.body_stream_started)
 
3302
            return orig_flush()
 
3303
        requester.flush = tracked_flush
 
3304
        requester.call_with_body_stream(('one arg',), stream_checker())
 
3305
        self.assertEqual(
 
3306
            'bzr message 3 (bzr 1.6)\n' # protocol version
 
3307
            '\x00\x00\x00\x02de' # headers
 
3308
            's\x00\x00\x00\x0bl7:one arge' # args
 
3309
            'b\x00\x00\x00\x07content' # body
 
3310
            'e', output.getvalue())
 
3311
        self.assertEqual([False, True, True], flush_called)
 
3312
 
3104
3313
 
3105
3314
class StubMediumRequest(object):
3106
3315
    """A stub medium request that tracks the number of times accept_bytes is
3176
3385
    """
3177
3386
 
3178
3387
    def setUp(self):
3179
 
        tests.TestCase.setUp(self)
 
3388
        super(TestResponseEncoderBufferingProtocolThree, self).setUp()
3180
3389
        self.writes = []
3181
3390
        self.responder = protocol.ProtocolThreeResponder(self.writes.append)
3182
3391
 
3526
3735
        # encoder.
3527
3736
 
3528
3737
 
 
3738
class Test_SmartClientRequest(tests.TestCase):
 
3739
 
 
3740
    def make_client_with_failing_medium(self, fail_at_write=True, response=''):
 
3741
        response_io = StringIO(response)
 
3742
        output = StringIO()
 
3743
        vendor = FirstRejectedStringIOSSHVendor(response_io, output,
 
3744
                    fail_at_write=fail_at_write)
 
3745
        ssh_params = medium.SSHParams('a host', 'a port', 'a user', 'a pass')
 
3746
        client_medium = medium.SmartSSHClientMedium('base', ssh_params, vendor)
 
3747
        smart_client = client._SmartClient(client_medium, headers={})
 
3748
        return output, vendor, smart_client
 
3749
 
 
3750
    def make_response(self, args, body=None, body_stream=None):
 
3751
        response_io = StringIO()
 
3752
        response = _mod_request.SuccessfulSmartServerResponse(args, body=body,
 
3753
            body_stream=body_stream)
 
3754
        responder = protocol.ProtocolThreeResponder(response_io.write)
 
3755
        responder.send_response(response)
 
3756
        return response_io.getvalue()
 
3757
 
 
3758
    def test__call_doesnt_retry_append(self):
 
3759
        response = self.make_response(('appended', '8'))
 
3760
        output, vendor, smart_client = self.make_client_with_failing_medium(
 
3761
            fail_at_write=False, response=response)
 
3762
        smart_request = client._SmartClientRequest(smart_client, 'append',
 
3763
            ('foo', ''), body='content\n')
 
3764
        self.assertRaises(errors.ConnectionReset, smart_request._call, 3)
 
3765
 
 
3766
    def test__call_retries_get_bytes(self):
 
3767
        response = self.make_response(('ok',), 'content\n')
 
3768
        output, vendor, smart_client = self.make_client_with_failing_medium(
 
3769
            fail_at_write=False, response=response)
 
3770
        smart_request = client._SmartClientRequest(smart_client, 'get',
 
3771
            ('foo',))
 
3772
        response, response_handler = smart_request._call(3)
 
3773
        self.assertEqual(('ok',), response)
 
3774
        self.assertEqual('content\n', response_handler.read_body_bytes())
 
3775
 
 
3776
    def test__call_noretry_get_bytes(self):
 
3777
        debug.debug_flags.add('noretry')
 
3778
        response = self.make_response(('ok',), 'content\n')
 
3779
        output, vendor, smart_client = self.make_client_with_failing_medium(
 
3780
            fail_at_write=False, response=response)
 
3781
        smart_request = client._SmartClientRequest(smart_client, 'get',
 
3782
            ('foo',))
 
3783
        self.assertRaises(errors.ConnectionReset, smart_request._call, 3)
 
3784
 
 
3785
    def test__send_no_retry_pipes(self):
 
3786
        client_read, server_write = create_file_pipes()
 
3787
        server_read, client_write = create_file_pipes()
 
3788
        client_medium = medium.SmartSimplePipesClientMedium(client_read,
 
3789
            client_write, base='/')
 
3790
        smart_client = client._SmartClient(client_medium)
 
3791
        smart_request = client._SmartClientRequest(smart_client,
 
3792
            'hello', ())
 
3793
        # Close the server side
 
3794
        server_read.close()
 
3795
        encoder, response_handler = smart_request._construct_protocol(3)
 
3796
        self.assertRaises(errors.ConnectionReset,
 
3797
            smart_request._send_no_retry, encoder)
 
3798
 
 
3799
    def test__send_read_response_sockets(self):
 
3800
        listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
3801
        listen_sock.bind(('127.0.0.1', 0))
 
3802
        listen_sock.listen(1)
 
3803
        host, port = listen_sock.getsockname()
 
3804
        client_medium = medium.SmartTCPClientMedium(host, port, '/')
 
3805
        client_medium._ensure_connection()
 
3806
        smart_client = client._SmartClient(client_medium)
 
3807
        smart_request = client._SmartClientRequest(smart_client, 'hello', ())
 
3808
        # Accept the connection, but don't actually talk to the client.
 
3809
        server_sock, _ = listen_sock.accept()
 
3810
        server_sock.close()
 
3811
        # Sockets buffer and don't really notice that the server has closed the
 
3812
        # connection until we try to read again.
 
3813
        handler = smart_request._send(3)
 
3814
        self.assertRaises(errors.ConnectionReset,
 
3815
            handler.read_response_tuple, expect_body=False)
 
3816
 
 
3817
    def test__send_retries_on_write(self):
 
3818
        output, vendor, smart_client = self.make_client_with_failing_medium()
 
3819
        smart_request = client._SmartClientRequest(smart_client, 'hello', ())
 
3820
        handler = smart_request._send(3)
 
3821
        self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
 
3822
                         '\x00\x00\x00\x02de'   # empty headers
 
3823
                         's\x00\x00\x00\tl5:helloee',
 
3824
                         output.getvalue())
 
3825
        self.assertEqual(
 
3826
            [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
 
3827
              ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
 
3828
             ('close',),
 
3829
             ('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
 
3830
              ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
 
3831
            ],
 
3832
            vendor.calls)
 
3833
 
 
3834
    def test__send_doesnt_retry_read_failure(self):
 
3835
        output, vendor, smart_client = self.make_client_with_failing_medium(
 
3836
            fail_at_write=False)
 
3837
        smart_request = client._SmartClientRequest(smart_client, 'hello', ())
 
3838
        handler = smart_request._send(3)
 
3839
        self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
 
3840
                         '\x00\x00\x00\x02de'   # empty headers
 
3841
                         's\x00\x00\x00\tl5:helloee',
 
3842
                         output.getvalue())
 
3843
        self.assertEqual(
 
3844
            [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
 
3845
              ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
 
3846
            ],
 
3847
            vendor.calls)
 
3848
        self.assertRaises(errors.ConnectionReset, handler.read_response_tuple)
 
3849
 
 
3850
    def test__send_request_retries_body_stream_if_not_started(self):
 
3851
        output, vendor, smart_client = self.make_client_with_failing_medium()
 
3852
        smart_request = client._SmartClientRequest(smart_client, 'hello', (),
 
3853
            body_stream=['a', 'b'])
 
3854
        response_handler = smart_request._send(3)
 
3855
        # We connect, get disconnected, and notice before consuming the stream,
 
3856
        # so we try again one time and succeed.
 
3857
        self.assertEqual(
 
3858
            [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
 
3859
              ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
 
3860
             ('close',),
 
3861
             ('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
 
3862
              ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
 
3863
            ],
 
3864
            vendor.calls)
 
3865
        self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
 
3866
                         '\x00\x00\x00\x02de'   # empty headers
 
3867
                         's\x00\x00\x00\tl5:helloe'
 
3868
                         'b\x00\x00\x00\x01a'
 
3869
                         'b\x00\x00\x00\x01b'
 
3870
                         'e',
 
3871
                         output.getvalue())
 
3872
 
 
3873
    def test__send_request_stops_if_body_started(self):
 
3874
        # We intentionally use the python StringIO so that we can subclass it.
 
3875
        from StringIO import StringIO
 
3876
        response = StringIO()
 
3877
 
 
3878
        class FailAfterFirstWrite(StringIO):
 
3879
            """Allow one 'write' call to pass, fail the rest"""
 
3880
            def __init__(self):
 
3881
                StringIO.__init__(self)
 
3882
                self._first = True
 
3883
 
 
3884
            def write(self, s):
 
3885
                if self._first:
 
3886
                    self._first = False
 
3887
                    return StringIO.write(self, s)
 
3888
                raise IOError(errno.EINVAL, 'invalid file handle')
 
3889
        output = FailAfterFirstWrite()
 
3890
 
 
3891
        vendor = FirstRejectedStringIOSSHVendor(response, output,
 
3892
            fail_at_write=False)
 
3893
        ssh_params = medium.SSHParams('a host', 'a port', 'a user', 'a pass')
 
3894
        client_medium = medium.SmartSSHClientMedium('base', ssh_params, vendor)
 
3895
        smart_client = client._SmartClient(client_medium, headers={})
 
3896
        smart_request = client._SmartClientRequest(smart_client, 'hello', (),
 
3897
            body_stream=['a', 'b'])
 
3898
        self.assertRaises(errors.ConnectionReset, smart_request._send, 3)
 
3899
        # We connect, and manage to get to the point that we start consuming
 
3900
        # the body stream. The next write fails, so we just stop.
 
3901
        self.assertEqual(
 
3902
            [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
 
3903
              ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
 
3904
             ('close',),
 
3905
            ],
 
3906
            vendor.calls)
 
3907
        self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
 
3908
                         '\x00\x00\x00\x02de'   # empty headers
 
3909
                         's\x00\x00\x00\tl5:helloe',
 
3910
                         output.getvalue())
 
3911
 
 
3912
    def test__send_disabled_retry(self):
 
3913
        debug.debug_flags.add('noretry')
 
3914
        output, vendor, smart_client = self.make_client_with_failing_medium()
 
3915
        smart_request = client._SmartClientRequest(smart_client, 'hello', ())
 
3916
        self.assertRaises(errors.ConnectionReset, smart_request._send, 3)
 
3917
        self.assertEqual(
 
3918
            [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
 
3919
              ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
 
3920
             ('close',),
 
3921
            ],
 
3922
            vendor.calls)
 
3923
 
 
3924
 
3529
3925
class LengthPrefixedBodyDecoder(tests.TestCase):
3530
3926
 
3531
3927
    # XXX: TODO: make accept_reading_trailer invoke translate_response or
3865
4261
        # still work correctly.
3866
4262
        base_transport = remote.RemoteHTTPTransport('bzr+http://host/%7Ea/b')
3867
4263
        new_transport = base_transport.clone('c')
3868
 
        self.assertEqual('bzr+http://host/%7Ea/b/c/', new_transport.base)
 
4264
        self.assertEqual(base_transport.base + 'c/', new_transport.base)
3869
4265
        self.assertEqual(
3870
4266
            'c/',
3871
4267
            new_transport._client.remote_path_from_transport(new_transport))