[MERGE] Implement chunked body encoding for the smart protocol.
Andrew Bennetts
andrew at canonical.com
Mon Sep 3 18:21:53 BST 2007
This bundle adds infrastructure for streaming results in chunks over the smart
protocol, rather than in one big length-prefixed lump.
See the doc/developers/network-protocol.txt already submitted for the protocol
details.
Server-side request handlers can use this simply by doing:
def do(self, *args):
...
return SuccessfulSmartServerResponse(
('ok',), body_stream=self.make_stream())
(assuming make_stream returns an iterator of strings, e.g. a generator
function)
Client-side streamed bodies are read by calling “read_streamed_body” instead of
“read_body_bytes”.
The protocol allows for a stream to be interrupted by an error.
(This bundle includes the doc/developers/network-protocol.txt changes I've
submitted separately. This inflates the changes a bit, sorry about the noise.)
-Andrew.
-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: andrew.bennetts at canonical.com-20070903171210-\
# 5wjqh4lq24wn9qfp
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev
# testament_sha1: 5ed6592a7d5099a125cf67e47d26f9f2496dbd43
# timestamp: 2007-09-04 03:12:27 +1000
# source_branch: http://people.ubuntu.com/~andrew/bzr/hpss-streaming
# base_revision_id: pqm at pqm.ubuntu.com-20070903130729-qdcrag0a7vcpzfgm
#
# 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-08 14:34:56 +0000
+++ bzrlib/smart/protocol.py 2007-09-03 07:53:54 +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 BzrError(
+ 'Chunks must be str or FailedSmartServerResponse, got %r'
+ % chunks)
+
+
+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-04-26 09:07:38 +0000
+++ bzrlib/smart/request.py 2007-09-03 06:53:49 +0000
@@ -83,17 +83,30 @@
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.is_successful(),
+ return "<SmartServerResponse %r args=%r body=%r>" % (self.is_successful(),
self.args, self.body)
=== modified file 'bzrlib/tests/test_smart.py'
--- bzrlib/tests/test_smart.py 2007-07-17 06:57:01 +0000
+++ bzrlib/tests/test_smart.py 2007-08-30 06:17:47 +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-08-02 06:40:58 +0000
+++ bzrlib/tests/test_smart_transport.py 2007-09-03 07:53:54 +0000
@@ -1614,6 +1614,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(
@@ -1700,6 +1751,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(
@@ -1833,7 +1892,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)
@@ -1849,6 +1907,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.
@@ -1954,16 +2049,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