From: Robert Collins Date: Sat, 10 Oct 2009 03:42:32 +0000 (+1100) Subject: Move chunking to be \r\n based and create a dedicated module with that logic. X-Git-Url: http://git.samba.org/samba.git/?p=third_party%2Fsubunit;a=commitdiff_plain;h=40ae70b04c7c88ed80a5e5b3f340f0c523b95e59 Move chunking to be \r\n based and create a dedicated module with that logic. --- diff --git a/Makefile.am b/Makefile.am index c5f071b..8042f14 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 7dbc5a0..9740d01 100644 --- 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 diff --git a/python/subunit/__init__.py b/python/subunit/__init__.py index f06437c..a93b6eb 100644 --- a/python/subunit/__init__.py +++ b/python/subunit/__init__.py @@ -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 index 0000000..2cfc7ff --- /dev/null +++ b/python/subunit/chunked.py @@ -0,0 +1,63 @@ +# +# subunit: extensions to python unittest to get test results from subprocesses. +# Copyright (C) 2005 Robert Collins +# +# 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") diff --git a/python/subunit/tests/__init__.py b/python/subunit/tests/__init__.py index d842c7e..8869425 100644 --- a/python/subunit/tests/__init__.py +++ b/python/subunit/tests/__init__.py @@ -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 index 0000000..2bf82b2 --- /dev/null +++ b/python/subunit/tests/test_chunked.py @@ -0,0 +1,65 @@ +# +# subunit: extensions to python unittest to get test results from subprocesses. +# Copyright (C) 2005 Robert Collins +# +# 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()) diff --git a/python/subunit/tests/test_test_protocol.py b/python/subunit/tests/test_test_protocol.py index c2c62bc..41fc6b4 100644 --- a/python/subunit/tests/test_test_protocol.py +++ b/python/subunit/tests/test_test_protocol.py @@ -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():