[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