[MERGE] Implement chunked body encoding for the smart protocol.
Andrew Bennetts
andrew at canonical.com
Mon Oct 22 08:19:33 BST 2007
John Arbash Meinel wrote:
> John Arbash Meinel has voted comment.
> Status is now: Semi-approved
> Comment:
> Another PING. Did something like this get merged as part of the other
> streaming changes?
>
> If not, then I'd expect the streaming to fail on first branch for some
> of the larger repositories (caching 500MB isn't very nice for people,
> not to mention the lag of having to wait until everything is downloaded
> before any processing begins.)
No, this wasn't part of the work that already landed. This work still needs
review.
Here's an updated bundle against bzr.dev.
-Andrew.
-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: andrew.bennetts at canonical.com-20071022071549-\
# s79n24hdx7nb0zi5
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev
# testament_sha1: 63b57377d3aab094ce36c2fe56c365159a6194cc
# timestamp: 2007-10-22 17:18:21 +1000
# source_branch: http://people.ubuntu.com/~andrew/bzr/chunked-\
# body/hpss-streaming
# base_revision_id: pqm at pqm.ubuntu.com-20071018040514-3hc1k2nj1umg3tig
#
# Begin patch
=== added file 'doc/developers/network-protocol.txt'
--- doc/developers/network-protocol.txt 1970-01-01 00:00:00 +0000
+++ doc/developers/network-protocol.txt 2007-09-03 17:09:56 +0000
@@ -0,0 +1,235 @@
+================
+Network Protocol
+================
+
+:Date: 2007-09-03
+
+
+.. contents::
+
+
+Overview
+========
+
+The smart protocol provides a way to send a requests and corresponding
+responses to communicate with a remote bzr process.
+
+Layering
+========
+
+Medium
+------
+
+At the bottom level there is either a socket, pipes, or an HTTP
+request/response. We call this layer the *medium*. It is responsible for
+carrying bytes between a client and server. For sockets, we have the idea
+that you have multiple requests and get a read error because the other
+side did shutdown. For pipes we have read pipe which will have a zero
+read which marks end-of-file. For HTTP server environment there is no
+end-of-stream because each request coming into the server is independent.
+
+So we need a wrapper around pipes and sockets to seperate out requests
+from substrate and this will give us a single model which is consistent
+for HTTP, sockets and pipes.
+
+Protocol
+--------
+
+On top of the medium is the *protocol*. This is the layer that
+deserialises bytes into the structured data that requests and responses
+consist of.
+
+Request/Response processing
+---------------------------
+
+On top of the protocol is the logic for processing requests (on the
+server) or responses (on the client).
+
+Server-side
+-----------
+
+Sketch::
+
+ MEDIUM (factory for protocol, reads bytes & pushes to protocol,
+ uses protocol to detect end-of-request, sends written
+ bytes to client) e.g. socket, pipe, HTTP request handler.
+ ^
+ | bytes.
+ v
+
+ PROTOCOL(serialization, deserialization) accepts bytes for one
+ request, decodes according to internal state, pushes
+ structured data to handler. accepts structured data from
+ handler and encodes and writes to the medium. factory for
+ handler.
+ ^
+ | structured data
+ v
+
+ HANDLER (domain logic) accepts structured data, operates state
+ machine until the request can be satisfied,
+ sends structured data to the protocol.
+
+Request handlers are registered in the `bzrlib.smart.request` module.
+
+
+Client-side
+-----------
+
+Sketch::
+
+ CLIENT domain logic, accepts domain requests, generated structured
+ data, reads structured data from responses and turns into
+ domain data. Sends structured data to the protocol.
+ Operates state machines until the request can be delivered
+ (e.g. reading from a bundle generated in bzrlib to deliver a
+ complete request).
+
+ This is RemoteBzrDir, RemoteRepository, etc.
+ ^
+ | structured data
+ v
+
+ PROTOCOL (serialization, deserialization) accepts structured data for one
+ request, encodes and writes to the medium. Reads bytes from the
+ medium, decodes and allows the client to read structured data.
+ ^
+ | bytes.
+ v
+
+ MEDIUM accepts bytes from the protocol & delivers to the remote server.
+ Allows the protocol to read bytes e.g. socket, pipe, HTTP request.
+
+The domain logic is in `bzrlib.remote`: `RemoteBzrDir`, `RemoteBranch`,
+and so on.
+
+There is also an plain file-level transport that calls remote methods to
+manipulate files on the server in `bzrlib.transport.remote`.
+
+Protocol description
+====================
+
+Version one
+-----------
+
+Version one of the protocol was introduced in Bazaar 0.11.
+
+The protocol (for both requests and responses) is described by::
+
+ REQUEST := MESSAGE_V1
+ RESPONSE := MESSAGE_V1
+ MESSAGE_V1 := ARGS BODY
+
+ ARGS := ARG [MORE_ARGS] NEWLINE
+ MORE_ARGS := SEP ARG [MORE_ARGS]
+ SEP := 0x01
+
+ BODY := LENGTH NEWLINE BODY_BYTES TRAILER
+ LENGTH := decimal integer
+ TRAILER := "done" NEWLINE
+
+That is, a tuple of arguments separated by Ctrl-A and terminated with a
+newline, followed by length prefixed body with a constant trailer. Note
+that although arguments are not 8-bit safe (they cannot include 0x01 or
+0x0a bytes without breaking the protocol encoding), the body is.
+
+Version two
+-----------
+
+Version two was introduced in Bazaar 0.16.
+
+The request protocol is::
+
+ REQUEST_V2 := "bzr request 2" NEWLINE MESSAGE_V2
+
+The response protocol is::
+
+ RESPONSE_V2 := "bzr response 2" NEWLINE MESSAGE_V2
+
+Future versions should follow this structure, like version two does::
+
+ FUTURE_MESSAGE := VERSION_STRING NEWLINE REST_OF_MESSAGE
+
+This is so that clients and servers can read bytes up to the first newline
+byte to determine what version a message is.
+
+For compatibility will all versions (past and future) of bzr clients,
+servers that receive a request in an unknown protocol version should
+respond with a single-line error terminated with 0x0a (NEWLINE), rather
+than structured response prefixed with a version string.
+
+Version two of the message protocol is::
+
+ MESSAGE_V2 := ARGS BODY
+ BODY_V2 := BODY | STREAMED_BODY
+
+That is, a version one length-prefixed body, or a version two streamed
+body.
+
+Version two with streamed bodies
+--------------------------------
+
+An extension to version two allows streamed bodies. A streamed body looks
+a lot like HTTP's chunked encoding::
+
+ STREAMED_BODY := "chunked" NEWLINE CHUNKS TERMINATOR
+ CHUNKS := CHUNK [CHUNKS]
+ CHUNK := CHUNK_LENGTH CHUNK_CONTENT
+ CHUNK_LENGTH := HEX_DIGITS NEWLINE
+ CHUNK_CONTENT := bytes
+
+ TERMINATOR := SUCCESS_TERMINATOR | ERROR_TERMINATOR
+ SUCCESS_TERMINATOR := 'END' NEWLINE
+ ERROR_TERMINATOR := 'ERR' NEWLINE CHUNKS SUCCESS_TERMINATOR
+
+That is, the body consists of a series of chunks. Each chunk starts with
+a length prefix in hexadecimal digits, followed by an ASCII newline byte.
+The end of the body is signaled by 'END\\n', or by 'ERR\\n' followed by
+error args, one per chunk. Note that this allows an 8-bit clean error
+response.
+
+A streamed body starts with the string "chunked" so that legacy clients
+and servers will not mistake the first chunk as the start of a version one
+body.
+
+The type of body (length-prefixed or chunked) in a response is always the
+same for a given request method. Only new request methods introduced in
+Bazaar 0.91 and later use streamed bodies.
+
+Paths
+=====
+
+Paths are passed across the network. The client needs to see a namespace
+that includes any repository that might need to be referenced, and the
+client needs to know about a root directory beyond which it cannot ascend.
+
+Servers run over ssh will typically want to be able to access any path the
+user can access. Public servers on the other hand (which might be over
+http, ssh or tcp) will typically want to restrict access to only a
+particular directory and its children, so will want to do a software
+virtual root at that level. In other words they'll want to rewrite
+incoming paths to be under that level (and prevent escaping using ../
+tricks). The default implementation in bzrlib does this using the
+`bzrlib.transport.chroot` module.
+
+URLs that include ~ should probably be passed across to the server
+verbatim and the server can expand them. This will proably not be
+meaningful when limited to a directory? See `bug 109143`_.
+
+.. _bug 109143: https://bugs.launchpad.net/bzr/+bug/109143
+
+
+Requests
+========
+
+The first argument of a request specifies the request method.
+
+The available request methods are registered in `bzrlib.smart.request`.
+
+**XXX**: ideally the request methods should be documented here.
+Contributions welcome!
+
+
+..
+ vim: ft=rst tw=74 ai
+
=== modified file 'bzrlib/smart/__init__.py'
--- bzrlib/smart/__init__.py 2007-04-26 04:32:44 +0000
+++ bzrlib/smart/__init__.py 2007-09-03 04:43:27 +0000
@@ -20,160 +20,17 @@
rather than being a single large module. Refer to the individual module
docstrings for details.
-Overview
-========
-
-The smart protocol provides a way to send a requests and corresponding
-responses to communicate with a remote bzr process.
-
-Layering
-========
-
-Medium
-------
-
-At the bottom level there is either a socket, pipes, or an HTTP
-request/response. We call this layer the *medium*. It is responsible for
-carrying bytes between a client and server. For sockets, we have the
-idea that you have multiple requests and get a read error because the other side
-did shutdown. For pipes we have read pipe which will have a zero read which
-marks end-of-file. For HTTP server environment there is no end-of-stream
-because each request coming into the server is independent.
-
-So we need a wrapper around pipes and sockets to seperate out requests from
-substrate and this will give us a single model which is consistent for HTTP,
-sockets and pipes.
-
-Protocol
---------
-
-On top of the medium is the *protocol*. This is the layer that deserialises
-bytes into the structured data that requests and responses consist of.
-
-Version one of the protocol (for requests and responses) is described by::
-
- REQUEST := MESSAGE_V1
- RESPONSE := MESSAGE_V1
- MESSAGE_V1 := ARGS BODY
-
- ARGS := ARG [MORE_ARGS] NEWLINE
- MORE_ARGS := SEP ARG [MORE_ARGS]
- SEP := 0x01
-
- BODY := LENGTH NEWLINE BODY_BYTES TRAILER
- LENGTH := decimal integer
- TRAILER := "done" NEWLINE
-
-That is, a tuple of arguments separated by Ctrl-A and terminated with a newline,
-followed by length prefixed body with a constant trailer. Note that although
-arguments are not 8-bit safe (they cannot include 0x01 or 0x0a bytes without
-breaking the protocol encoding), the body is.
-
-Version two of the request protocol is::
-
- REQUEST_V2 := "bzr request 2" NEWLINE MESSAGE_V1
-
-Version two of the response protocol is::
-
- RESPONSE_V2 := "bzr request 2" NEWLINE MESSAGE_V1
-
-Future versions should follow this structure, like version two does::
-
- FUTURE_MESSAGE := VERSION_STRING NEWLINE REST_OF_MESSAGE
-
-This is that clients and servers can read bytes up to the first newline byte to
-determine what version a message is.
-
-For compatibility will all versions (past and future) of bzr clients, servers
-that receive a request in an unknown protocol version should respond with a
-single-line error terminated with 0x0a (NEWLINE), rather than structured
-response prefixed with a version string.
-
-Request/Response processing
----------------------------
-
-On top of the protocol is the logic for processing requests (on the server) or
-responses (on the client).
-
-Server-side
------------
-
-Sketch::
-
- MEDIUM (factory for protocol, reads bytes & pushes to protocol,
- uses protocol to detect end-of-request, sends written
- bytes to client) e.g. socket, pipe, HTTP request handler.
- ^
- | bytes.
- v
-
- PROTOCOL(serialization, deserialization) accepts bytes for one
- request, decodes according to internal state, pushes
- structured data to handler. accepts structured data from
- handler and encodes and writes to the medium. factory for
- handler.
- ^
- | structured data
- v
-
- HANDLER (domain logic) accepts structured data, operates state
- machine until the request can be satisfied,
- sends structured data to the protocol.
-
-Request handlers are registered in `bzrlib.smart.request`.
-
-
-Client-side
------------
-
-Sketch::
-
- CLIENT domain logic, accepts domain requests, generated structured
- data, reads structured data from responses and turns into
- domain data. Sends structured data to the protocol.
- Operates state machines until the request can be delivered
- (e.g. reading from a bundle generated in bzrlib to deliver a
- complete request).
-
- Possibly this should just be RemoteBzrDir, RemoteTransport,
- ...
- ^
- | structured data
- v
-
-PROTOCOL (serialization, deserialization) accepts structured data for one
- request, encodes and writes to the medium. Reads bytes from the
- medium, decodes and allows the client to read structured data.
- ^
- | bytes.
- v
-
- MEDIUM (accepts bytes from the protocol & delivers to the remote server.
- Allows the potocol to read bytes e.g. socket, pipe, HTTP request.
-
-The domain logic is in `bzrlib.remote`: `RemoteBzrDir`, `RemoteBranch`, and so
-on.
+Server-side request handlers are registered in the `bzrlib.smart.request`
+module.
+
+The domain logic is in `bzrlib.remote`: `RemoteBzrDir`, `RemoteBranch`,
+and so on.
There is also an plain file-level transport that calls remote methods to
manipulate files on the server in `bzrlib.transport.remote`.
-Paths
-=====
-
-Paths are passed across the network. The client needs to see a namespace that
-includes any repository that might need to be referenced, and the client needs
-to know about a root directory beyond which it cannot ascend.
-
-Servers run over ssh will typically want to be able to access any path the user
-can access. Public servers on the other hand (which might be over http, ssh
-or tcp) will typically want to restrict access to only a particular directory
-and its children, so will want to do a software virtual root at that level.
-In other words they'll want to rewrite incoming paths to be under that level
-(and prevent escaping using ../ tricks.)
-
-URLs that include ~ should probably be passed across to the server verbatim
-and the server can expand them. This will proably not be meaningful when
-limited to a directory?
+The protocol is described in doc/developers/network-protocol.txt.
+
"""
# TODO: _translate_error should be on the client, not the transport because
=== modified file 'bzrlib/smart/protocol.py'
--- bzrlib/smart/protocol.py 2007-08-13 01:04:05 +0000
+++ bzrlib/smart/protocol.py 2007-10-22 07:15:49 +0000
@@ -18,6 +18,7 @@
client and server.
"""
+import collections
from cStringIO import StringIO
import time
@@ -197,19 +198,48 @@
"""
self._write_func(RESPONSE_VERSION_TWO)
-
-class LengthPrefixedBodyDecoder(object):
- """Decodes the length-prefixed bulk data."""
-
+ def _send_response(self, response):
+ """Send a smart server response down the output stream."""
+ assert not self._finished, 'response already sent'
+ self._finished = True
+ self._write_protocol_version()
+ self._write_success_or_failure_prefix(response)
+ self._write_func(_encode_tuple(response.args))
+ if response.body is not None:
+ assert isinstance(response.body, str), 'body must be a str'
+ bytes = self._encode_bulk_data(response.body)
+ self._write_func(bytes)
+ elif response.body_stream is not None:
+ _send_stream(response.body_stream, self._write_func)
+
+
+def _send_stream(stream, write_func):
+ _send_chunks(stream, write_func)
+ write_func('END\n')
+
+
+def _send_chunks(stream, write_func):
+ for chunk in stream:
+ if isinstance(chunk, str):
+ bytes = "%x\n%s" % (len(chunk), chunk)
+ write_func(bytes)
+ elif isinstance(chunk, request.FailedSmartServerResponse):
+ write_func('ERR\n')
+ _send_chunks(chunk.args, write_func)
+ return
+ else:
+ raise errors.BzrError(
+ 'Chunks must be str or FailedSmartServerResponse, got %r'
+ % chunk)
+
+
+class _StatefulDecoder(object):
+
def __init__(self):
- self.bytes_left = None
self.finished_reading = False
self.unused_data = ''
- self.state_accept = self._state_accept_expecting_length
- self.state_read = self._state_read_no_data
- self._in_buffer = ''
- self._trailer_buffer = ''
-
+ self.bytes_left = None
+
def accept_bytes(self, bytes):
"""Decode as much of bytes as possible.
@@ -226,6 +256,124 @@
current_state = self.state_accept
self.state_accept('')
+
+class ChunkedBodyDecoder(_StatefulDecoder):
+ """Decoder for chunked body data.
+
+ This is very similar the HTTP's chunked encoding. See the description of
+ streamed body data in `doc/developers/network-protocol.txt` for details.
+ """
+
+ def __init__(self):
+ _StatefulDecoder.__init__(self)
+ self.state_accept = self._state_accept_expecting_length
+ self._in_buffer = ''
+ self.chunk_in_progress = None
+ self.chunks = collections.deque()
+ self.error = False
+ self.error_in_progress = None
+
+ def next_read_size(self):
+ # Note: the shortest possible chunk is 2 bytes: '0\n', and the
+ # end-of-body marker is 4 bytes: 'END\n'.
+ if self.state_accept == self._state_accept_reading_chunk:
+ # We're expecting more chunk content. So we're expecting at least
+ # the rest of this chunk plus an END chunk.
+ return self.bytes_left + 4
+ elif self.state_accept == self._state_accept_expecting_length:
+ if self._in_buffer == '':
+ # We're expecting a chunk length. There's at least two bytes
+ # left: a digit plus '\n'.
+ return 2
+ else:
+ # We're in the middle of reading a chunk length. So there's at
+ # least one byte left, the '\n' that terminates the length.
+ return 1
+ elif self.state_accept == self._state_accept_reading_unused:
+ return 1
+ else:
+ raise AssertionError("Impossible state: %r" % (self.state_accept,))
+
+ def read_next_chunk(self):
+ try:
+ return self.chunks.popleft()
+ except IndexError:
+ return None
+
+ def _extract_line(self):
+ pos = self._in_buffer.find('\n')
+ if pos == -1:
+ # We haven't read a complete length prefix yet, so there's nothing
+ # to do.
+ return None
+ line = self._in_buffer[:pos]
+ # Trim the prefix (including '\n' delimiter) from the _in_buffer.
+ self._in_buffer = self._in_buffer[pos+1:]
+ return line
+
+ def _finished(self):
+ self.unused_data = self._in_buffer
+ self._in_buffer = None
+ self.state_accept = self._state_accept_reading_unused
+ if self.error:
+ error_args = tuple(self.error_in_progress)
+ self.chunks.append(request.FailedSmartServerResponse(error_args))
+ self.error_in_progress = None
+ self.finished_reading = True
+
+ def _state_accept_expecting_length(self, bytes):
+ self._in_buffer += bytes
+ prefix = self._extract_line()
+ if prefix is None:
+ # We haven't read a complete length prefix yet, so there's nothing
+ # to do.
+ return
+ elif prefix == 'ERR':
+ self.error = True
+ self.error_in_progress = []
+ self._state_accept_expecting_length('')
+ return
+ elif prefix == 'END':
+ # We've read the end-of-body marker.
+ # Any further bytes are unused data, including the bytes left in
+ # the _in_buffer.
+ self._finished()
+ return
+ else:
+ self.bytes_left = int(prefix, 16)
+ self.chunk_in_progress = ''
+ self.state_accept = self._state_accept_reading_chunk
+
+ def _state_accept_reading_chunk(self, bytes):
+ self._in_buffer += bytes
+ in_buffer_len = len(self._in_buffer)
+ self.chunk_in_progress += self._in_buffer[:self.bytes_left]
+ self._in_buffer = self._in_buffer[self.bytes_left:]
+ self.bytes_left -= in_buffer_len
+ if self.bytes_left <= 0:
+ # Finished with chunk
+ self.bytes_left = None
+ if self.error:
+ self.error_in_progress.append(self.chunk_in_progress)
+ else:
+ self.chunks.append(self.chunk_in_progress)
+ self.chunk_in_progress = None
+ self.state_accept = self._state_accept_expecting_length
+
+ def _state_accept_reading_unused(self, bytes):
+ self.unused_data += bytes
+
+
+class LengthPrefixedBodyDecoder(_StatefulDecoder):
+ """Decodes the length-prefixed bulk data."""
+
+ def __init__(self):
+ _StatefulDecoder.__init__(self)
+ self.state_accept = self._state_accept_expecting_length
+ self.state_read = self._state_read_no_data
+ self._in_buffer = ''
+ self._trailer_buffer = ''
+
def next_read_size(self):
if self.bytes_left is not None:
# Ideally we want to read all the remainder of the body and the
@@ -448,9 +596,24 @@
return SmartClientRequestProtocolOne.read_response_tuple(self, expect_body)
def _write_protocol_version(self):
- r"""Write any prefixes this protocol requires.
+ """Write any prefixes this protocol requires.
Version two sends the value of REQUEST_VERSION_TWO.
"""
self._request.accept_bytes(REQUEST_VERSION_TWO)
+ def read_streamed_body(self):
+ """Read bytes from the body, decoding into a byte stream.
+ """
+ _body_decoder = ChunkedBodyDecoder()
+ while not _body_decoder.finished_reading:
+ bytes_wanted = _body_decoder.next_read_size()
+ bytes = self._request.read_bytes(bytes_wanted)
+ _body_decoder.accept_bytes(bytes)
+ for body_bytes in iter(_body_decoder.read_next_chunk, None):
+ if 'hpss' in debug.debug_flags:
+ mutter(' %d byte chunk read',
+ len(body_bytes))
+ yield body_bytes
+ self._request.finished_reading()
+
=== modified file 'bzrlib/smart/request.py'
--- bzrlib/smart/request.py 2007-10-12 05:26:46 +0000
+++ bzrlib/smart/request.py 2007-10-19 05:38:36 +0000
@@ -83,18 +83,31 @@
SuccessfulSmartServerResponse and FailedSmartServerResponse as appropriate.
"""
- def __init__(self, args, body=None):
+ def __init__(self, args, body=None, body_stream=None):
+ """Constructor.
+
+ :param args: tuple of response arguments.
+ :param body: string of a response body.
+ :param body_stream: iterable of bytestrings to be streamed to the
+ client.
+ """
self.args = args
+ if body is not None and body_stream is not None:
+ raise errors.BzrError(
+ "'body' and 'body_stream' are mutually exclusive.")
self.body = body
+ self.body_stream = body_stream
def __eq__(self, other):
if other is None:
return False
- return other.args == self.args and other.body == self.body
+ return (other.args == self.args and
+ other.body == self.body and
+ other.body_stream is self.body_stream)
def __repr__(self):
- return "<SmartServerResponse args=%r body=%r>" % (
- self.args, self.body)
+ return "<SmartServerResponse %r args=%r body=%r>" % (
+ self.is_successful(), self.args, self.body)
class FailedSmartServerResponse(SmartServerResponse):
=== modified file 'bzrlib/tests/test_smart.py'
--- bzrlib/tests/test_smart.py 2007-10-12 05:26:46 +0000
+++ bzrlib/tests/test_smart.py 2007-10-19 05:38:36 +0000
@@ -14,7 +14,13 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-"""Tests for the smart wire/domain protocol."""
+"""Tests for the smart wire/domain protocol.
+
+This module contains tests for the domain-level smart requests and responses,
+such as the 'Branch.lock_write' request.
+
+Tests for low-level protocol encoding are found in test_smart_transport.
+"""
from StringIO import StringIO
import tempfile
=== modified file 'bzrlib/tests/test_smart_transport.py'
--- bzrlib/tests/test_smart_transport.py 2007-10-12 05:26:46 +0000
+++ bzrlib/tests/test_smart_transport.py 2007-10-19 05:38:36 +0000
@@ -1648,6 +1648,57 @@
self.assertOffsetSerialisation([(1,2), (3,4), (100, 200)],
'1,2\n3,4\n100,200', self.client_protocol)
+ def assertBodyStreamSerialisation(self, expected_serialisation,
+ body_stream):
+ """Assert that body_stream is serialised as expected_serialisation."""
+ out_stream = StringIO()
+ protocol._send_stream(body_stream, out_stream.write)
+ self.assertEqual(expected_serialisation, out_stream.getvalue())
+
+ def assertBodyStreamRoundTrips(self, body_stream):
+ """Assert that body_stream is the same after being serialised and
+ deserialised.
+ """
+ out_stream = StringIO()
+ protocol._send_stream(body_stream, out_stream.write)
+ decoder = protocol.ChunkedBodyDecoder()
+ decoder.accept_bytes(out_stream.getvalue())
+ decoded_stream = list(iter(decoder.read_next_chunk, None))
+ self.assertEqual(body_stream, decoded_stream)
+
+ def test_body_stream_serialisation_empty(self):
+ """A body_stream with no bytes can be serialised."""
+ self.assertBodyStreamSerialisation('END\n', [])
+ self.assertBodyStreamRoundTrips([])
+
+ def test_body_stream_serialisation(self):
+ stream = ['chunk one', 'chunk two', 'chunk three']
+ self.assertBodyStreamSerialisation(
+ '9\nchunk one' + '9\nchunk two' + 'b\nchunk three' + 'END\n',
+ stream)
+ self.assertBodyStreamRoundTrips(stream)
+
+ def test_body_stream_with_empty_element_serialisation(self):
+ """A body stream can include ''.
+
+ The empty string can be transmitted like any other string.
+ """
+ stream = ['', 'chunk']
+ self.assertBodyStreamSerialisation(
+ '0\n' + '5\nchunk' + 'END\n', stream)
+ self.assertBodyStreamRoundTrips(stream)
+
+ def test_body_stream_error_serialistion(self):
+ stream = ['first chunk',
+ request.FailedSmartServerResponse(
+ ('FailureName', 'failure arg'))]
+ expected_bytes = (
+ 'b\nfirst chunk' +
+ 'ERR\n' + 'b\nFailureName' + 'b\nfailure arg' +
+ 'END\n')
+ self.assertBodyStreamSerialisation(expected_bytes, stream)
+ self.assertBodyStreamRoundTrips(stream)
+
def test_accept_bytes_of_bad_request_to_protocol(self):
out_stream = StringIO()
smart_protocol = protocol.SmartServerRequestProtocolTwo(
@@ -1734,6 +1785,14 @@
request.SuccessfulSmartServerResponse(('x',)))
self.assertEqual(0, smart_protocol.next_read_size())
+ def test__send_response_with_body_stream_sets_finished_reading(self):
+ smart_protocol = protocol.SmartServerRequestProtocolTwo(
+ None, lambda x: None)
+ self.assertEqual(1, smart_protocol.next_read_size())
+ smart_protocol._send_response(
+ request.SuccessfulSmartServerResponse(('x',), body_stream=[]))
+ self.assertEqual(0, smart_protocol.next_read_size())
+
def test__send_response_errors_with_base_response(self):
"""Ensure that only the Successful/Failed subclasses are used."""
smart_protocol = protocol.SmartServerRequestProtocolTwo(
@@ -1867,7 +1926,6 @@
def test_client_cancel_read_body_does_not_eat_body_bytes(self):
# cancelling the expected body needs to finish the request, but not
# read any more bytes.
- expected_bytes = "1234567"
server_bytes = (protocol.RESPONSE_VERSION_TWO +
"success\nok\n7\n1234567done\n")
input = StringIO(server_bytes)
@@ -1883,6 +1941,43 @@
self.assertRaises(
errors.ReadingCompleted, smart_protocol.read_body_bytes)
+ def test_streamed_body_bytes(self):
+ two_body_chunks = "4\n1234" + "3\n567"
+ body_terminator = "END\n"
+ server_bytes = (protocol.RESPONSE_VERSION_TWO +
+ "success\nok\n" + two_body_chunks + body_terminator)
+ input = StringIO(server_bytes)
+ output = StringIO()
+ client_medium = medium.SmartSimplePipesClientMedium(input, output)
+ request = client_medium.get_request()
+ smart_protocol = protocol.SmartClientRequestProtocolTwo(request)
+ smart_protocol.call('foo')
+ smart_protocol.read_response_tuple(True)
+ stream = smart_protocol.read_streamed_body()
+ self.assertEqual(['1234', '567'], list(stream))
+
+ def test_read_streamed_body_error(self):
+ """When a stream is interrupted by an error..."""
+ a_body_chunk = '4\naaaa'
+ err_signal = 'ERR\n'
+ err_chunks = 'a\nerror arg1' + '4\narg2'
+ finish = 'END\n'
+ body = a_body_chunk + err_signal + err_chunks + finish
+ server_bytes = (protocol.RESPONSE_VERSION_TWO +
+ "success\nok\n" + body)
+ input = StringIO(server_bytes)
+ output = StringIO()
+ client_medium = medium.SmartSimplePipesClientMedium(input, output)
+ smart_request = client_medium.get_request()
+ smart_protocol = protocol.SmartClientRequestProtocolTwo(smart_request)
+ smart_protocol.call('foo')
+ smart_protocol.read_response_tuple(True)
+ expected_chunks = [
+ 'aaaa',
+ request.FailedSmartServerResponse(('error arg1', 'arg2'))]
+ stream = smart_protocol.read_streamed_body()
+ self.assertEqual(expected_chunks, list(stream))
+
class TestSmartClientUnicode(tests.TestCase):
"""_SmartClient tests for unicode arguments.
@@ -1988,16 +2083,175 @@
self.assertEqual('', decoder.unused_data)
+class TestChunkedBodyDecoder(tests.TestCase):
+ """Tests for ChunkedBodyDecoder."""
+
+ def test_construct(self):
+ decoder = protocol.ChunkedBodyDecoder()
+ self.assertFalse(decoder.finished_reading)
+ self.assertEqual(2, decoder.next_read_size())
+ self.assertEqual(None, decoder.read_next_chunk())
+ self.assertEqual('', decoder.unused_data)
+
+ def test_empty_content(self):
+ """'0' + LF is the complete chunked encoding of a zero-length body."""
+ decoder = protocol.ChunkedBodyDecoder()
+ decoder.accept_bytes('END\n')
+ self.assertTrue(decoder.finished_reading)
+ self.assertEqual(None, decoder.read_next_chunk())
+ self.assertEqual('', decoder.unused_data)
+
+ def test_one_chunk(self):
+ """A body in a single chunk is decoded correctly."""
+ decoder = protocol.ChunkedBodyDecoder()
+ chunk_length = 'f\n'
+ chunk_content = '123456789abcdef'
+ finish = 'END\n'
+ decoder.accept_bytes(chunk_length + chunk_content + finish)
+ self.assertTrue(decoder.finished_reading)
+ self.assertEqual(chunk_content, decoder.read_next_chunk())
+ self.assertEqual('', decoder.unused_data)
+
+ def test_incomplete_chunk(self):
+ """When there are less bytes in the chunk than declared by the length,
+ then we haven't finished reading yet.
+ """
+ decoder = protocol.ChunkedBodyDecoder()
+ chunk_length = '8\n'
+ three_bytes = '123'
+ decoder.accept_bytes(chunk_length + three_bytes)
+ self.assertFalse(decoder.finished_reading)
+ self.assertEqual(
+ 5 + 4, decoder.next_read_size(),
+ "The next_read_size hint should be the number of missing bytes in "
+ "this chunk plus 4 (the length of the end-of-body marker: "
+ "'END\\n')")
+ self.assertEqual(None, decoder.read_next_chunk())
+
+ def test_incomplete_length(self):
+ """A chunk length hasn't been read until a newline byte has been read.
+ """
+ decoder = protocol.ChunkedBodyDecoder()
+ decoder.accept_bytes('9')
+ self.assertEqual(
+ 1, decoder.next_read_size(),
+ "The next_read_size hint should be 1, because we don't know the "
+ "length yet.")
+ decoder.accept_bytes('\n')
+ self.assertEqual(
+ 9 + 4, decoder.next_read_size(),
+ "The next_read_size hint should be the length of the chunk plus 4 "
+ "(the length of the end-of-body marker: 'END\\n')")
+ self.assertFalse(decoder.finished_reading)
+ self.assertEqual(None, decoder.read_next_chunk())
+
+ def test_two_chunks(self):
+ """Content from multiple chunks is concatenated."""
+ decoder = protocol.ChunkedBodyDecoder()
+ chunk_one = '3\naaa'
+ chunk_two = '5\nbbbbb'
+ finish = 'END\n'
+ decoder.accept_bytes(chunk_one + chunk_two + finish)
+ self.assertTrue(decoder.finished_reading)
+ self.assertEqual('aaa', decoder.read_next_chunk())
+ self.assertEqual('bbbbb', decoder.read_next_chunk())
+ self.assertEqual(None, decoder.read_next_chunk())
+ self.assertEqual('', decoder.unused_data)
+
+ def test_excess_bytes(self):
+ """Bytes after the chunked body are reported as unused bytes."""
+ decoder = protocol.ChunkedBodyDecoder()
+ chunked_body = "5\naaaaaEND\n"
+ excess_bytes = "excess bytes"
+ decoder.accept_bytes(chunked_body + excess_bytes)
+ self.assertTrue(decoder.finished_reading)
+ self.assertEqual('aaaaa', decoder.read_next_chunk())
+ self.assertEqual(excess_bytes, decoder.unused_data)
+ self.assertEqual(
+ 1, decoder.next_read_size(),
+ "next_read_size hint should be 1 when finished_reading.")
+
+ def test_multidigit_length(self):
+ """Lengths in the chunk prefixes can have multiple digits."""
+ decoder = protocol.ChunkedBodyDecoder()
+ length = 0x123
+ chunk_prefix = hex(length) + '\n'
+ chunk_bytes = 'z' * length
+ finish = 'END\n'
+ decoder.accept_bytes(chunk_prefix + chunk_bytes + finish)
+ self.assertTrue(decoder.finished_reading)
+ self.assertEqual(chunk_bytes, decoder.read_next_chunk())
+
+ def test_byte_at_a_time(self):
+ """A complete body fed to the decoder one byte at a time should not
+ confuse the decoder. That is, it should give the same result as if the
+ bytes had been received in one batch.
+
+ This test is the same as test_one_chunk apart from the way accept_bytes
+ is called.
+ """
+ decoder = protocol.ChunkedBodyDecoder()
+ chunk_length = 'f\n'
+ chunk_content = '123456789abcdef'
+ finish = 'END\n'
+ for byte in (chunk_length + chunk_content + finish):
+ decoder.accept_bytes(byte)
+ self.assertTrue(decoder.finished_reading)
+ self.assertEqual(chunk_content, decoder.read_next_chunk())
+ self.assertEqual('', decoder.unused_data)
+
+ def test_read_pending_data_resets(self):
+ """read_pending_data does not return the same bytes twice."""
+ decoder = protocol.ChunkedBodyDecoder()
+ chunk_one = '3\naaa'
+ chunk_two = '3\nbbb'
+ finish = 'END\n'
+ decoder.accept_bytes(chunk_one)
+ self.assertEqual('aaa', decoder.read_next_chunk())
+ decoder.accept_bytes(chunk_two)
+ self.assertEqual('bbb', decoder.read_next_chunk())
+ self.assertEqual(None, decoder.read_next_chunk())
+
+ def test_decode_error(self):
+ decoder = protocol.ChunkedBodyDecoder()
+ chunk_one = 'b\nfirst chunk'
+ error_signal = 'ERR\n'
+ error_chunks = '5\npart1' + '5\npart2'
+ finish = 'END\n'
+ decoder.accept_bytes(chunk_one + error_signal + error_chunks + finish)
+ self.assertTrue(decoder.finished_reading)
+ self.assertEqual('first chunk', decoder.read_next_chunk())
+ expected_failure = request.FailedSmartServerResponse(
+ ('part1', 'part2'))
+ self.assertEqual(expected_failure, decoder.read_next_chunk())
+
+
class TestSuccessfulSmartServerResponse(tests.TestCase):
- def test_construct(self):
+ def test_construct_no_body(self):
response = request.SuccessfulSmartServerResponse(('foo', 'bar'))
self.assertEqual(('foo', 'bar'), response.args)
self.assertEqual(None, response.body)
- response = request.SuccessfulSmartServerResponse(('foo', 'bar'), 'bytes')
+
+ def test_construct_with_body(self):
+ response = request.SuccessfulSmartServerResponse(
+ ('foo', 'bar'), 'bytes')
self.assertEqual(('foo', 'bar'), response.args)
self.assertEqual('bytes', response.body)
+ def test_construct_with_body_stream(self):
+ bytes_iterable = ['abc']
+ response = request.SuccessfulSmartServerResponse(
+ ('foo', 'bar'), body_stream=bytes_iterable)
+ self.assertEqual(('foo', 'bar'), response.args)
+ self.assertEqual(bytes_iterable, response.body_stream)
+
+ def test_construct_rejects_body_and_body_stream(self):
+ """'body' and 'body_stream' are mutually exclusive."""
+ self.assertRaises(
+ errors.BzrError,
+ request.SuccessfulSmartServerResponse, (), 'body', ['stream'])
+
def test_is_successful(self):
"""is_successful should return True for SuccessfulSmartServerResponse."""
response = request.SuccessfulSmartServerResponse(('error',))
=== modified file 'doc/developers/index.txt'
--- doc/developers/index.txt 2007-08-13 10:20:11 +0000
+++ doc/developers/index.txt 2007-09-03 05:11:38 +0000
@@ -31,5 +31,6 @@
* `Repositories <repository.html>`_ |--| What repositories do and are used for.
+* `Network protocol <network-protocol.html>`_ |--| Custom network protocol.
.. |--| unicode:: U+2014
# Begin bundle
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
More information about the bazaar
mailing list