diff options
author | Tres Seaver <tseaver@palladion.com> | 2014-03-10 11:24:39 -0400 |
---|---|---|
committer | Tres Seaver <tseaver@palladion.com> | 2014-03-10 11:24:39 -0400 |
commit | 78417e17ee193befba1b1bc2c5b4b1a1629d8320 (patch) | |
tree | be95173fd29f90a59e8cfe36fe47bd1ae0951182 | |
parent | 7b2ff7fed602a4dc00d1a64e617deb7a1ce529bb (diff) | |
parent | 169558586d477f6f22402300422b90b5334b3654 (diff) | |
download | waitress-78417e17ee193befba1b1bc2c5b4b1a1629d8320.tar.gz |
Merge from master.
-rw-r--r-- | CHANGES.txt | 51 | ||||
-rw-r--r-- | docs/conf.py | 2 | ||||
-rw-r--r-- | docs/runner.rst | 2 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | waitress/adjustments.py | 24 | ||||
-rw-r--r-- | waitress/buffers.py | 16 | ||||
-rw-r--r-- | waitress/channel.py | 9 | ||||
-rw-r--r-- | waitress/compat.py | 5 | ||||
-rw-r--r-- | waitress/parser.py | 17 | ||||
-rw-r--r-- | waitress/receiver.py | 6 | ||||
-rw-r--r-- | waitress/task.py | 39 | ||||
-rw-r--r-- | waitress/tests/test_adjustments.py | 10 | ||||
-rw-r--r-- | waitress/tests/test_buffers.py | 35 | ||||
-rw-r--r-- | waitress/tests/test_channel.py | 32 | ||||
-rw-r--r-- | waitress/tests/test_functional.py | 4 | ||||
-rw-r--r-- | waitress/tests/test_parser.py | 24 | ||||
-rw-r--r-- | waitress/tests/test_receiver.py | 23 | ||||
-rw-r--r-- | waitress/tests/test_runner.py | 4 | ||||
-rw-r--r-- | waitress/tests/test_task.py | 22 |
19 files changed, 283 insertions, 44 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 3317251..1722bf9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,7 +3,56 @@ Unreleased - Allow override of ``wsgi.url_scheme`` via a request header, ``X_FORWARDED_PROTO``. Allows proxies which serve mixed HTTP / HTTPS - requests to control signal which are served as HTTPS. + requests to control signal which are served as HTTPS.See + https://github.com/Pylons/waitress/pull/42. + +0.8.8 (2013-11-30) +------------------ + +- Fix some cases where the creation of extremely large output buffers (greater + than 2GB, suspected to be buffers added via ``wsgi.file_wrapper``) might + cause an OverflowError on Python 2. See + https://github.com/Pylons/waitress/issues/47. + +- When the ``url_prefix`` adjustment starts with more than one slash, all + slashes except one will be stripped from its beginning. This differs from + older behavior where more than one leading slash would be preserved in + ``url_prefix``. + +- If a client somehow manages to send an empty path, we no longer convert the + empty path to a single slash in ``PATH_INFO``. Instead, the path remains + empty. According to RFC 2616 section "5.1.2 Request-URI", the scenario of a + client sending an empty path is actually not possible because the request URI + portion cannot be empty. + +- If the ``url_prefix`` adjustment matches the request path exactly, we now + compute ``SCRIPT_NAME`` and ``PATH_INFO`` properly. Previously, if the + ``url_prefix`` was ``/foo`` and the path received from a client was ``/foo``, + we would set *both* ``SCRIPT_NAME`` and ``PATH_INFO`` to ``/foo``. This was + incorrect. Now in such a case we set ``PATH_INFO`` to the empty string and + we set ``SCRIPT_NAME`` to ``/foo``. Note that the change we made has no + effect on paths that do not match the ``url_prefix`` exactly (such as + ``/foo/bar``); these continue to operate as they did. See + https://github.com/Pylons/waitress/issues/46 + +- Preserve header ordering of headers with the same name as per RFC 2616. See + https://github.com/Pylons/waitress/pull/44 + +- When waitress receives a ``Transfer-Encoding: chunked`` request, we no longer + send the ``TRANSFER_ENCODING`` nor the ``HTTP_TRANSFER_ENCODING`` value to + the application in the environment. Instead, we pop this header. Since we + cope with chunked requests by buffering the data in the server, we also know + when a chunked request has ended, and therefore we know the content length. + We set the content-length header in the environment, such that applications + effectively never know the original request was a T-E: chunked request; it + will appear to them as if the request is a non-chunked request with an + accurate content-length. + +- Cope with the fact that the ``Transfer-Encoding`` value is case-insensitive. + +- When the ``--unix-socket-perms`` option was used as an argument to + ``waitress-serve``, a ``TypeError`` would be raised. See + https://github.com/Pylons/waitress/issues/50. 0.8.7 (2013-08-29) ------------------ diff --git a/docs/conf.py b/docs/conf.py index fa9f9a7..1df17f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ copyright = '2012, Agendaless Consulting <chrism@plope.com>' # other places throughout the built documents. # # The short X.Y version. -version = '0.8.7' +version = '0.8.8' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/runner.rst b/docs/runner.rst index 3fb3814..9799a17 100644 --- a/docs/runner.rst +++ b/docs/runner.rst @@ -121,7 +121,7 @@ Tuning options: Number of bytes to request when calling ``socket.recv()``. Default is 8192. -``--send-bytes=INT``` +``--send-bytes=INT`` Number of bytes to send to socket.send(). Default is 18000. Multiples of 9000 should avoid partly-filled TCP packets. @@ -37,7 +37,7 @@ if sys.version_info[:2] == (2, 6): setup( name='waitress', - version='0.8.7', + version='0.8.8', author='Zope Foundation and Contributors', author_email='zope-dev@zope.org', maintainer="Chris McDonough", diff --git a/waitress/adjustments.py b/waitress/adjustments.py index fb5f7a6..2835e97 100644 --- a/waitress/adjustments.py +++ b/waitress/adjustments.py @@ -36,8 +36,13 @@ def asoctal(s): """Convert the given octal string to an actual number.""" return int(s, 8) -def slash_suffix_stripped_str(s): - return s.rstrip('/') +def slash_fixed_str(s): + s = s.strip() + if s: + # always have a leading slash, replace any number of leading slashes + # with a single slash, and strip any trailing slashes + s = '/' + s.lstrip('/').rstrip('/') + return s class Adjustments(object): """This class contains tunable parameters. @@ -48,7 +53,7 @@ class Adjustments(object): ('port', int), ('threads', int), ('url_scheme', str), - ('url_prefix', slash_suffix_stripped_str), + ('url_prefix', slash_fixed_str), ('backlog', int), ('recv_bytes', int), ('send_bytes', int), @@ -176,7 +181,10 @@ class Adjustments(object): @classmethod def parse_args(cls, argv): - """Parse command line arguments. + """Pre-parse command line arguments for input into __init__. Note that + this does not cast values into adjustment types, it just creates a + dictionary suitable for passing into __init__, where __init__ does the + casting. """ long_opts = ['help', 'call'] for opt, cast in cls._params: @@ -196,9 +204,11 @@ class Adjustments(object): param = opt.lstrip('-').replace('-', '_') if param.startswith('no_'): param = param[3:] - kw[param] = False - elif param in ('help', 'call') or cls._param_map[param] is asbool: + kw[param] = 'false' + elif param in ('help', 'call'): kw[param] = True + elif cls._param_map[param] is asbool: + kw[param] = 'true' else: - kw[param] = cls._param_map[param](value) + kw[param] = value return kw, args diff --git a/waitress/buffers.py b/waitress/buffers.py index bb5a530..0009444 100644 --- a/waitress/buffers.py +++ b/waitress/buffers.py @@ -186,8 +186,8 @@ class OverflowableBuffer(object): """ This buffer implementation has four stages: - No data - - String-based buffer - - StringIO-based buffer + - Bytes-based buffer + - BytesIO-based buffer - Temporary file storage The first two stages are fastest for simple transfers. """ @@ -203,11 +203,15 @@ class OverflowableBuffer(object): def __len__(self): buf = self.buf if buf is not None: + # use buf.__len__ rather than len(buf) FBO of not getting + # OverflowError on Python 2 return buf.__len__() else: return self.strbuf.__len__() def __nonzero__(self): + # use self.__len__ rather than len(self) FBO of not getting + # OverflowError on Python 2 return self.__len__() > 0 __bool__ = __nonzero__ # py3 @@ -241,7 +245,9 @@ class OverflowableBuffer(object): return buf = self._create_buffer() buf.append(s) - sz = len(buf) + # use buf.__len__ rather than len(buf) FBO of not getting + # OverflowError on Python 2 + sz = buf.__len__() if not self.overflowed: if sz >= self.overflow: self._set_large_buffer() @@ -278,7 +284,9 @@ class OverflowableBuffer(object): return buf.prune() if self.overflowed: - sz = len(buf) + # use buf.__len__ rather than len(buf) FBO of not getting + # OverflowError on Python 2 + sz = buf.__len__() if sz < self.overflow: # Revert to a faster buffer. self._set_small_buffer() diff --git a/waitress/channel.py b/waitress/channel.py index 5d16c74..11ac140 100644 --- a/waitress/channel.py +++ b/waitress/channel.py @@ -89,7 +89,10 @@ class HTTPChannel(logging_dispatcher, object): return False def total_outbufs_len(self): - return sum([len(b) for b in self.outbufs]) # genexpr == more funccalls + # genexpr == more funccalls + # use b.__len__ rather than len(b) FBO of not getting OverflowError + # on Python 2 + return sum([b.__len__() for b in self.outbufs]) def writable(self): # if there's data in the out buffer or we've been instructed to close @@ -233,7 +236,9 @@ class HTTPChannel(logging_dispatcher, object): while True: outbuf = self.outbufs[0] - outbuflen = len(outbuf) + # use outbuf.__len__ rather than len(outbuf) FBO of not getting + # OverflowError on Python 2 + outbuflen = outbuf.__len__() if outbuflen <= 0: # self.outbufs[-1] must always be a writable outbuf if len(self.outbufs) > 1: diff --git a/waitress/compat.py b/waitress/compat.py index 1796480..a90ee56 100644 --- a/waitress/compat.py +++ b/waitress/compat.py @@ -109,3 +109,8 @@ try: import httplib except ImportError: # pragma: no cover from http import client as httplib + +try: + MAXINT = sys.maxint +except AttributeError: # pragma: no cover + MAXINT = sys.maxsize diff --git a/waitress/parser.py b/waitress/parser.py index 83e2424..dc3ec47 100644 --- a/waitress/parser.py +++ b/waitress/parser.py @@ -148,7 +148,16 @@ class HTTPRequestParser(object): self.error = br.error self.completed = True elif br.completed: + # The request (with the body) is ready to use. self.completed = True + if self.chunked: + # We've converted the chunked transfer encoding request + # body into a normal request body, so we know its content + # length; set the header here. We already popped the + # TRANSFER_ENCODING header in parse_header, so this will + # appear to the client to be an entirely non-chunked HTTP + # request with a valid content-length. + self.headers['CONTENT_LENGTH'] = str(br.__len__()) return consumed def parse_header(self, header_plus): @@ -203,8 +212,12 @@ class HTTPRequestParser(object): self.connection_close = True if version == '1.1': - te = headers.get('TRANSFER_ENCODING', '') - if te == 'chunked': + # since the server buffers data from chunked transfers and clients + # never need to deal with chunked requests, downstream clients + # should not see the HTTP_TRANSFER_ENCODING header; we pop it + # here + te = headers.pop('TRANSFER_ENCODING', '') + if te.lower() == 'chunked': self.chunked = True buf = OverflowableBuffer(self.adj.inbuf_overflow) self.body_rcv = ChunkedReceiver(buf) diff --git a/waitress/receiver.py b/waitress/receiver.py index f362f08..594ae97 100644 --- a/waitress/receiver.py +++ b/waitress/receiver.py @@ -28,6 +28,9 @@ class FixedStreamReceiver(object): self.remain = cl self.buf = buf + def __len__(self): + return self.buf.__len__() + def received(self, data): 'See IStreamConsumer' rm = self.remain @@ -66,6 +69,9 @@ class ChunkedReceiver(object): def __init__(self, buf): self.buf = buf + def __len__(self): + return self.buf.__len__() + def received(self, s): # Returns the number of bytes consumed. if self.completed: diff --git a/waitress/task.py b/waitress/task.py index c541dfd..7dcd8a4 100644 --- a/waitress/task.py +++ b/waitress/task.py @@ -255,7 +255,11 @@ class Task(object): response_headers.append(('Date', build_http_date(self.start_time))) first_line = 'HTTP/%s %s' % (self.version, self.status) - next_lines = ['%s: %s' % hv for hv in sorted(self.response_headers)] + # NB: sorting headers needs to preserve same-named-header order + # as per RFC 2616 section 4.2; thus the key=lambda x: x[0] here; + # rely on stable sort to keep relative position of same-named headers + next_lines = ['%s: %s' % hv for hv in sorted( + self.response_headers, key=lambda x: x[0])] lines = [first_line] + next_lines res = '%s\r\n\r\n' % '\r\n'.join(lines) return tobytes(res) @@ -447,13 +451,28 @@ class WSGITask(Task): path = request.path channel = self.channel server = channel.server - - path = path.lstrip('/') - - url_prefix_with_slash = server.adj.url_prefix.lstrip('/') + '/' - - if url_prefix_with_slash and path.startswith(url_prefix_with_slash): - path = path[len(url_prefix_with_slash):] + url_prefix = server.adj.url_prefix + + if path.startswith('/'): + # strip extra slashes at the beginning of a path that starts + # with any number of slashes + path = '/' + path.lstrip('/') + + if url_prefix: + # NB: url_prefix is guaranteed by the configuration machinery to + # be either the empty string or a string that starts with a single + # slash and ends without any slashes + if path == url_prefix: + # if the path is the same as the url prefix, the SCRIPT_NAME + # should be the url_prefix and PATH_INFO should be empty + path = '' + else: + # if the path starts with the url prefix plus a slash, + # the SCRIPT_NAME should be the url_prefix and PATH_INFO should + # the value of path from the slash until its end + url_prefix_with_trailing_slash = url_prefix + '/' + if path.startswith(url_prefix_with_trailing_slash): + path = path[len(url_prefix):] environ = {} environ['REQUEST_METHOD'] = request.command.upper() @@ -461,8 +480,8 @@ class WSGITask(Task): environ['SERVER_NAME'] = server.server_name environ['SERVER_SOFTWARE'] = server.adj.ident environ['SERVER_PROTOCOL'] = 'HTTP/%s' % self.version - environ['SCRIPT_NAME'] = server.adj.url_prefix - environ['PATH_INFO'] = '/' + path + environ['SCRIPT_NAME'] = url_prefix + environ['PATH_INFO'] = path environ['QUERY_STRING'] = request.query environ['REMOTE_ADDR'] = channel.addr[0] diff --git a/waitress/tests/test_adjustments.py b/waitress/tests/test_adjustments.py index 26d7b04..fe390dc 100644 --- a/waitress/tests/test_adjustments.py +++ b/waitress/tests/test_adjustments.py @@ -59,7 +59,7 @@ class TestAdjustments(unittest.TestCase): max_request_header_size='1300', max_request_body_size='1400', expose_tracebacks='true', ident='abc', asyncore_loop_timeout='5', asyncore_use_poll=True, unix_socket='/tmp/waitress.sock', - unix_socket_perms='777', url_prefix='/foo') + unix_socket_perms='777', url_prefix='///foo/') self.assertEqual(inst.host, 'host') self.assertEqual(inst.port, 8080) self.assertEqual(inst.threads, 5) @@ -114,12 +114,12 @@ class TestCLI(unittest.TestCase): def test_positive_boolean(self): opts, args = self.parse(['--expose-tracebacks']) - self.assertDictContainsSubset({'expose_tracebacks': True}, opts) + self.assertDictContainsSubset({'expose_tracebacks': 'true'}, opts) self.assertSequenceEqual(args, []) def test_negative_boolean(self): opts, args = self.parse(['--no-expose-tracebacks']) - self.assertDictContainsSubset({'expose_tracebacks': False}, opts) + self.assertDictContainsSubset({'expose_tracebacks': 'false'}, opts) self.assertSequenceEqual(args, []) def test_cast_params(self): @@ -130,8 +130,8 @@ class TestCLI(unittest.TestCase): ]) self.assertDictContainsSubset({ 'host': 'localhost', - 'port': 80, - 'unix_socket_perms': 0o777, + 'port': '80', + 'unix_socket_perms':'777', }, opts) self.assertSequenceEqual(args, []) diff --git a/waitress/tests/test_buffers.py b/waitress/tests/test_buffers.py index b4ed7eb..f7c90b4 100644 --- a/waitress/tests/test_buffers.py +++ b/waitress/tests/test_buffers.py @@ -284,6 +284,17 @@ class TestOverflowableBuffer(unittest.TestCase): self.assertEqual(inst.buf.get(100), b'x' * 5) self.assertEqual(inst.strbuf, b'') + def test_append_with_len_more_than_max_int(self): + from waitress.compat import MAXINT + inst = self._makeOne() + inst.overflowed = True + buf = DummyBuffer(length=MAXINT) + inst.buf = buf + result = inst.append(b'x') + # we don't want this to throw an OverflowError on Python 2 (see + # https://github.com/Pylons/waitress/issues/47) + self.assertEqual(result, None) + def test_append_buf_None_not_longer_than_srtbuf_limit(self): inst = self._makeOne() inst.strbuf = b'x' * 5 @@ -373,6 +384,17 @@ class TestOverflowableBuffer(unittest.TestCase): inst.prune() self.assertNotEqual(inst.buf, buf) + def test_prune_with_buflen_more_than_max_int(self): + from waitress.compat import MAXINT + inst = self._makeOne() + inst.overflowed = True + buf = DummyBuffer(length=MAXINT+1) + inst.buf = buf + result = inst.prune() + # we don't want this to throw an OverflowError on Python 2 (see + # https://github.com/Pylons/waitress/issues/47) + self.assertEqual(result, None) + def test_getfile_buf_None(self): inst = self._makeOne() f = inst.getfile() @@ -417,3 +439,16 @@ class Filelike(KindaFilelike): def tell(self): v = self.tellresults.pop(0) return v + +class DummyBuffer(object): + def __init__(self, length=0): + self.length = length + + def __len__(self): + return self.length + + def append(self, s): + self.length = self.length + len(s) + + def prune(self): + pass diff --git a/waitress/tests/test_channel.py b/waitress/tests/test_channel.py index 33286c2..3ca97f0 100644 --- a/waitress/tests/test_channel.py +++ b/waitress/tests/test_channel.py @@ -22,6 +22,18 @@ class TestHTTPChannel(unittest.TestCase): self.assertEqual(inst.addr, '127.0.0.1') self.assertEqual(map[100], inst) + def test_total_outbufs_len_an_outbuf_size_gt_sys_maxint(self): + from waitress.compat import MAXINT + inst, _, map = self._makeOneWithMap() + class DummyHugeBuffer(object): + def __len__(self): + return MAXINT + 1 + inst.outbufs = [DummyHugeBuffer()] + result = inst.total_outbufs_len() + # we are testing that this method does not raise an OverflowError + # (see https://github.com/Pylons/waitress/issues/47) + self.assertEqual(result, MAXINT+1) + def test_writable_something_in_outbuf(self): inst, sock, map = self._makeOneWithMap() inst.outbufs[0].append(b'abc') @@ -256,6 +268,26 @@ class TestHTTPChannel(unittest.TestCase): self.assertEqual(inst.outbufs, [buffer]) self.assertEqual(len(inst.logger.exceptions), 1) + def test__flush_some_outbuf_len_gt_sys_maxint(self): + from waitress.compat import MAXINT + inst, sock, map = self._makeOneWithMap() + class DummyHugeOutbuffer(object): + def __init__(self): + self.length = MAXINT + 1 + def __len__(self): + return self.length + def get(self, numbytes): + self.length = 0 + return b'123' + def skip(self, *args): pass + buf = DummyHugeOutbuffer() + inst.outbufs = [buf] + inst.send = lambda *arg: 0 + result = inst._flush_some() + # we are testing that _flush_some doesn't raise an OverflowError + # when one of its outbufs has a __len__ that returns gt sys.maxint + self.assertEqual(result, False) + def test_handle_close(self): inst, sock, map = self._makeOneWithMap() inst.handle_close() diff --git a/waitress/tests/test_functional.py b/waitress/tests/test_functional.py index afe0929..942ef0a 100644 --- a/waitress/tests/test_functional.py +++ b/waitress/tests/test_functional.py @@ -258,6 +258,8 @@ class EchoTests(object): line, headers, response_body = read_http(fp) self.assertline(line, '200', 'OK', 'HTTP/1.1') self.assertEqual(response_body, b'') + self.assertEqual(headers['content-length'], '0') + self.assertFalse('transfer-encoding' in headers) def test_chunking_request_with_content(self): control_line = b"20;\r\n" # 20 hex = 32 dec @@ -277,6 +279,8 @@ class EchoTests(object): line, headers, response_body = read_http(fp) self.assertline(line, '200', 'OK', 'HTTP/1.1') self.assertEqual(response_body, expected) + self.assertEqual(headers['content-length'], str(len(expected))) + self.assertFalse('transfer-encoding' in headers) def test_broken_chunked_encoding(self): control_line = "20;\r\n" # 20 hex = 32 dec diff --git a/waitress/tests/test_parser.py b/waitress/tests/test_parser.py index d51e705..72e3a01 100644 --- a/waitress/tests/test_parser.py +++ b/waitress/tests/test_parser.py @@ -121,8 +121,8 @@ GET /foobar HTTP/1.1 Transfer-Encoding: chunked X-Foo: 1 -20;\r -This string has 32 characters\r +20;\r\n +This string has 32 characters\r\n 0\r\n\r\n""" result = self.parser.received(data) self.assertEqual(result, 58) @@ -149,6 +149,23 @@ garbage self.assertTrue(isinstance(self.parser.error, BadRequest)) + def test_received_chunked_completed_sets_content_length(self): + data = b"""\ +GET /foobar HTTP/1.1 +Transfer-Encoding: chunked +X-Foo: 1 + +20;\r\n +This string has 32 characters\r\n +0\r\n\r\n""" + result = self.parser.received(data) + self.assertEqual(result, 58) + data = data[result:] + result = self.parser.received(data) + self.assertTrue(self.parser.completed) + self.assertTrue(self.parser.error is None) + self.assertEqual(self.parser.headers['CONTENT_LENGTH'], '32') + def test_parse_header_gardenpath(self): data = b"""\ GET /foobar HTTP/8.4 @@ -168,7 +185,8 @@ foo: bar""" self.assertEqual(self.parser.body_rcv, None) def test_parse_header_11_te_chunked(self): - data = b"GET /foobar HTTP/1.1\ntransfer-encoding: chunked" + # NB: test that capitalization of header value is unimportant + data = b"GET /foobar HTTP/1.1\ntransfer-encoding: ChUnKed" self.parser.parse_header(data) self.assertEqual(self.parser.body_rcv.__class__.__name__, 'ChunkedReceiver') diff --git a/waitress/tests/test_receiver.py b/waitress/tests/test_receiver.py index 961d1c5..707f328 100644 --- a/waitress/tests/test_receiver.py +++ b/waitress/tests/test_receiver.py @@ -2,9 +2,9 @@ import unittest class TestFixedStreamReceiver(unittest.TestCase): - def _makeOne(self, buf, cl): + def _makeOne(self, cl, buf): from waitress.receiver import FixedStreamReceiver - return FixedStreamReceiver(buf, cl) + return FixedStreamReceiver(cl, buf) def test_received_remain_lt_1(self): buf = DummyBuffer() @@ -42,6 +42,11 @@ class TestFixedStreamReceiver(unittest.TestCase): inst = self._makeOne(10, buf) self.assertEqual(inst.getbuf(), buf) + def test___len__(self): + buf = DummyBuffer(['1', '2']) + inst = self._makeOne(10, buf) + self.assertEqual(inst.__len__(), 2) + class TestChunkedReceiver(unittest.TestCase): def _makeOne(self, buf): @@ -142,13 +147,23 @@ class TestChunkedReceiver(unittest.TestCase): inst = self._makeOne(buf) self.assertEqual(inst.getbuf(), buf) + def test___len__(self): + buf = DummyBuffer(['1', '2']) + inst = self._makeOne(buf) + self.assertEqual(inst.__len__(), 2) + class DummyBuffer(object): - def __init__(self): - self.data = [] + def __init__(self, data=None): + if data is None: + data = [] + self.data = data def append(self, s): self.data.append(s) def getfile(self): return self + + def __len__(self): + return len(self.data) diff --git a/waitress/tests/test_runner.py b/waitress/tests/test_runner.py index 50f6154..23da82e 100644 --- a/waitress/tests/test_runner.py +++ b/waitress/tests/test_runner.py @@ -126,7 +126,7 @@ class Test_run(unittest.TestCase): import waitress.tests.fixtureapps.runner as _apps def check_server(app, **kw): self.assertIs(app, _apps.app) - self.assertDictEqual(kw, {'port': 80}) + self.assertDictEqual(kw, {'port': '80'}) argv = [ 'waitress-serve', '--port=80', @@ -138,7 +138,7 @@ class Test_run(unittest.TestCase): import waitress.tests.fixtureapps.runner as _apps def check_server(app, **kw): self.assertIs(app, _apps.app) - self.assertDictEqual(kw, {'port': 80}) + self.assertDictEqual(kw, {'port': '80'}) argv = [ 'waitress-serve', '--port=80', diff --git a/waitress/tests/test_task.py b/waitress/tests/test_task.py index 19ff471..8abb1e5 100644 --- a/waitress/tests/test_task.py +++ b/waitress/tests/test_task.py @@ -394,6 +394,16 @@ class TestWSGITask(unittest.TestCase): inst.channel.server.application = app self.assertRaises(AssertionError, inst.execute) + def test_preserve_header_value_order(self): + def app(environ, start_response): + write = start_response('200 OK', [('C', 'b'), ('A', 'b'), ('A', 'a')]) + write(b'abc') + return [] + inst = self._makeOne() + inst.channel.server.application = app + inst.execute() + self.assertTrue(b'A: b\r\nA: a\r\nC: b\r\n' in inst.channel.written) + def test_execute_bad_status_value(self): def app(environ, start_response): start_response(None, []) @@ -548,7 +558,7 @@ class TestWSGITask(unittest.TestCase): request.path = '' inst.request = request environ = inst.get_environment() - self.assertEqual(environ['PATH_INFO'], '/') + self.assertEqual(environ['PATH_INFO'], '') def test_get_environment_no_query(self): inst = self._makeOne() @@ -585,6 +595,16 @@ class TestWSGITask(unittest.TestCase): self.assertEqual(environ['PATH_INFO'], '/fuz') self.assertEqual(environ['SCRIPT_NAME'], '/foo') + def test_get_environ_with_url_prefix_empty_path(self): + inst = self._makeOne() + inst.channel.server.adj.url_prefix = '/foo' + request = DummyParser() + request.path = '/foo' + inst.request = request + environ = inst.get_environment() + self.assertEqual(environ['PATH_INFO'], '') + self.assertEqual(environ['SCRIPT_NAME'], '/foo') + def test_get_environment_values(self): import sys inst = self._makeOne() |