Move chunking to be \r\n based and create a dedicated module with that logic.
authorRobert Collins <robertc@robertcollins.net>
Sat, 10 Oct 2009 03:42:32 +0000 (14:42 +1100)
committerRobert Collins <robertc@robertcollins.net>
Sat, 10 Oct 2009 03:42:32 +0000 (14:42 +1100)
Makefile.am
README
python/subunit/__init__.py
python/subunit/chunked.py [new file with mode: 0644]
python/subunit/tests/__init__.py
python/subunit/tests/test_chunked.py [new file with mode: 0644]
python/subunit/tests/test_test_protocol.py

index c5f071b9cfe7d85c0b13f2bb53c200a86395558c..8042f14e7cf204c71405c727ec651255fa50a80c 100644 (file)
@@ -16,6 +16,7 @@ EXTRA_DIST =  \
        python/subunit/tests/__init__.py \
        python/subunit/tests/sample-script.py \
        python/subunit/tests/sample-two-script.py \
+       python/subunit/tests/test_chunked.py \
        python/subunit/tests/test_content.py \
        python/subunit/tests/test_content_type.py \
        python/subunit/tests/test_subunit_filter.py \
@@ -64,6 +65,7 @@ pcdata_DATA = \
 
 pkgpython_PYTHON = \
        python/subunit/__init__.py \
+       python/subunit/chunked.py \
        python/subunit/content.py \
        python/subunit/content_type.py \
        python/subunit/iso8601.py \
diff --git a/README b/README
index 7dbc5a00312e00a3a0f568b86dfa01dcd7f765af..9740d013a53487cff602abe9f06867a588fa9ad9 100644 (file)
--- a/README
+++ b/README
@@ -164,7 +164,7 @@ BRACKETED ::= '[' CR lines ']' CR
 MULTIPART ::= '[ multipart' CR PART* ']' CR
 PART ::= PART_TYPE CR NAME CR PART_BYTES CR
 PART_TYPE ::= Content-Type: type/sub-type(;parameter=value,parameter=value)
-PART_BYTES ::= (DIGITS CR BYTE{DIGITS})* '0' CR
+PART_BYTES ::= (DIGITS CR LF BYTE{DIGITS})* '0' CR LF
 
 unexpected output on stdout -> stdout.
 exit w/0 or last test completing -> error
index f06437c2053aec15df5fa89f03ba6b4ce6186b09..a93b6eb44243bbf7d6ed9d7c82154da862e1aa89 100644 (file)
@@ -107,6 +107,13 @@ result object::
  # And run your suite as normal, Subunit will exec each external script as
  # needed and report to your result object.
  suite.run(result)
+
+Utility modules
+---------------
+
+* subunit.chunked contains HTTP chunked encoding/decoding logic.
+* subunit.content contains a minimal assumptions MIME content representation.
+* subunit.content_type contains a MIME Content-Type representation.
 """
 
 import datetime
@@ -119,7 +126,7 @@ import unittest
 
 import iso8601
 
-import content, content_type
+import chunked, content, content_type
 
 
 PROGRESS_SET = 0
@@ -618,9 +625,9 @@ class TestProtocolClient(unittest.TestResult):
                     param_strs.append("%s=%s" % (param, value))
                 self._stream.write(",".join(param_strs))
             self._stream.write("\n%s\n" % name)
-            for bytes in content.iter_bytes():
-                self._stream.write("%d\n%s" % (len(bytes), bytes))
-            self._stream.write("0\n")
+            encoder = chunked.Encoder(self._stream)
+            map(encoder.write, content.iter_bytes())
+            encoder.close()
 
     def done(self):
         """Obey the testtools result.done() interface."""
diff --git a/python/subunit/chunked.py b/python/subunit/chunked.py
new file mode 100644 (file)
index 0000000..2cfc7ff
--- /dev/null
@@ -0,0 +1,63 @@
+#
+#  subunit: extensions to python unittest to get test results from subprocesses.
+#  Copyright (C) 2005  Robert Collins <robertc@robertcollins.net>
+#
+#  Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
+#  license at the users choice. A copy of both licenses are available in the
+#  project source as Apache-2.0 and BSD. You may not use this file except in
+#  compliance with one of these two licences.
+#  
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+#  license you chose for the specific language governing permissions and
+#  limitations under that license.
+#
+
+"""Encoder/decoder for http style chunked encoding."""
+
+class Encoder(object):
+    """Encode content to a stream using HTTP Chunked coding."""
+
+    def __init__(self, output):
+        """Create an encoder encoding to output.
+
+        :param output: A file-like object. Bytes written  to the Encoder
+            will be encoded using HTTP chunking. Small writes may be buffered
+            and the ``close`` method must be called to finish the stream.
+        """
+        self.output = output
+        self.buffered_bytes = []
+        self.buffer_size = 0
+
+    def flush(self, extra_len=0):
+        """Flush the encoder to the output stream.
+        
+        :param extra_len: Increase the size of the chunk by this many bytes
+            to allow for a subsequent write.
+        """
+        if not self.buffer_size and not extra_len:
+            return
+        buffered_bytes = self.buffered_bytes
+        buffer_size = self.buffer_size
+        self.buffered_bytes = []
+        self.buffer_size = 0
+        self.output.write("%X\r\n" % (buffer_size + extra_len))
+        if buffer_size:
+            self.output.write(''.join(buffered_bytes))
+        return True
+
+    def write(self, bytes):
+        """Encode bytes to the output stream."""
+        bytes_len = len(bytes)
+        if self.buffer_size + bytes_len >= 65536:
+            self.flush(bytes_len)
+            self.output.write(bytes)
+        else:
+            self.buffered_bytes.append(bytes)
+            self.buffer_size += bytes_len
+
+    def close(self):
+        """Finish the stream. This does not close the output stream."""
+        self.flush()
+        self.output.write("0\r\n")
index d842c7e6ae8010764cb16984527f533322a427d9..88694258ae3f5b20ea20fb145212228d17ffd89d 100644 (file)
@@ -16,6 +16,7 @@
 
 from subunit.tests import (
     TestUtil,
+    test_chunked,
     test_content_type,
     test_content,
     test_progress_model,
@@ -29,6 +30,7 @@ from subunit.tests import (
 
 def test_suite():
     result = TestUtil.TestSuite()
+    result.addTest(test_chunked.test_suite())
     result.addTest(test_content_type.test_suite())
     result.addTest(test_content.test_suite())
     result.addTest(test_progress_model.test_suite())
diff --git a/python/subunit/tests/test_chunked.py b/python/subunit/tests/test_chunked.py
new file mode 100644 (file)
index 0000000..2bf82b2
--- /dev/null
@@ -0,0 +1,65 @@
+#
+#  subunit: extensions to python unittest to get test results from subprocesses.
+#  Copyright (C) 2005  Robert Collins <robertc@robertcollins.net>
+#
+#  Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
+#  license at the users choice. A copy of both licenses are available in the
+#  project source as Apache-2.0 and BSD. You may not use this file except in
+#  compliance with one of these two licences.
+#  
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+#  license you chose for the specific language governing permissions and
+#  limitations under that license.
+#
+
+from cStringIO import StringIO
+import unittest
+
+import subunit.chunked
+
+
+def test_suite():
+    loader = subunit.tests.TestUtil.TestLoader()
+    result = loader.loadTestsFromName(__name__)
+    return result
+
+
+class TestEncode(unittest.TestCase):
+
+    def setUp(self):
+        self.output = StringIO()
+        self.encoder = subunit.chunked.Encoder(self.output)
+
+    def test_encode_nothing(self):
+        self.encoder.close()
+        self.assertEqual('0\r\n', self.output.getvalue())
+
+    def test_encode_empty(self):
+        self.encoder.write('')
+        self.encoder.close()
+        self.assertEqual('0\r\n', self.output.getvalue())
+
+    def test_encode_short(self):
+        self.encoder.write('abc')
+        self.encoder.close()
+        self.assertEqual('3\r\nabc0\r\n', self.output.getvalue())
+
+    def test_encode_combines_short(self):
+        self.encoder.write('abc')
+        self.encoder.write('def')
+        self.encoder.close()
+        self.assertEqual('6\r\nabcdef0\r\n', self.output.getvalue())
+
+    def test_encode_over_9_is_in_hex(self):
+        self.encoder.write('1234567890')
+        self.encoder.close()
+        self.assertEqual('A\r\n12345678900\r\n', self.output.getvalue())
+
+    def test_encode_long_ranges_not_combined(self):
+        self.encoder.write('1' * 65536)
+        self.encoder.write('2' * 65536)
+        self.encoder.close()
+        self.assertEqual('10000\r\n' + '1' * 65536 + '10000\r\n' +
+            '2' * 65536 + '0\r\n', self.output.getvalue())
index c2c62bc318f26b1649248c4d89bc7aa447968faf..41fc6b48fc3b06d7cc971975dd7e9b94e8f739f1 100644 (file)
@@ -1083,7 +1083,7 @@ class TestTestProtocolClient(unittest.TestCase):
             self.io.getvalue(), "successful: %s [ multipart\n"
                 "Content-Type: text/plain\n"
                 "something\n"
-                "15\nserialised\nform0\n]\n" % self.test.id())
+                "F\r\nserialised\nform0\r\n]\n" % self.test.id())
 
     def test_add_failure(self):
         """Test addFailure on a TestProtocolClient."""
@@ -1102,10 +1102,10 @@ class TestTestProtocolClient(unittest.TestCase):
             "failure: %s [ multipart\n"
             "Content-Type: text/plain\n"
             "something\n"
-            "15\nserialised\nform0\n"
+            "F\r\nserialised\nform0\r\n"
             "Content-Type: text/x-traceback;language=python\n"
             "traceback\n"
-            "25\nRemoteException: boo qux\n0\n"
+            "19\r\nRemoteException: boo qux\n0\r\n"
             "]\n" % self.test.id())
 
     def test_add_error(self):
@@ -1127,10 +1127,10 @@ class TestTestProtocolClient(unittest.TestCase):
             "error: %s [ multipart\n"
             "Content-Type: text/plain\n"
             "something\n"
-            "15\nserialised\nform0\n"
+            "F\r\nserialised\nform0\r\n"
             "Content-Type: text/x-traceback;language=python\n"
             "traceback\n"
-            "25\nRemoteException: boo qux\n0\n"
+            "19\r\nRemoteException: boo qux\n0\r\n"
             "]\n" % self.test.id())
 
     def test_add_expected_failure(self):
@@ -1152,10 +1152,10 @@ class TestTestProtocolClient(unittest.TestCase):
             "xfail: %s [ multipart\n"
             "Content-Type: text/plain\n"
             "something\n"
-            "15\nserialised\nform0\n"
+            "F\r\nserialised\nform0\r\n"
             "Content-Type: text/x-traceback;language=python\n"
             "traceback\n"
-            "25\nRemoteException: boo qux\n0\n"
+            "19\r\nRemoteException: boo qux\n0\r\n"
             "]\n" % self.test.id())
 
     def test_add_skip(self):
@@ -1177,7 +1177,7 @@ class TestTestProtocolClient(unittest.TestCase):
             "skip: %s [ multipart\n"
             "Content-Type: text/plain\n"
             "reason\n"
-            "14\nHas it really?0\n"
+            "E\r\nHas it really?0\r\n"
             "]\n" % self.test.id())
 
     def test_progress_set(self):
@@ -1221,7 +1221,7 @@ class TestTestProtocolClient(unittest.TestCase):
             self.io.getvalue(), "successful: %s [ multipart\n"
                 "Content-Type: text/plain\n"
                 "something\n"
-                "15\nserialised\nform0\n]\n" % self.test.id())
+                "F\r\nserialised\nform0\r\n]\n" % self.test.id())
 
 
 def test_suite():