From f1f838e36bc24fb40d7121e72298da3e5a7149ff Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 02:05:04 -0400 Subject: [PATCH 01/16] Adds a realisic Werkzeug-based server for testing, and modifies one of the tests to verify that chunked encoding works correctly. --- requirements-dev.txt | 2 +- tests/test_lowlevel.py | 6 ++-- tests/testserver/werkzeug_server.py | 52 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 tests/testserver/werkzeug_server.py diff --git a/requirements-dev.txt b/requirements-dev.txt index effb0c81f5..7b5e9412a9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,4 @@ pytest-mock==2.0.0 httpbin==0.7.0 Flask>=1.0,<2.0 trustme -wheel +Werkzeug diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py index 4127fb115e..d0ea581d57 100644 --- a/tests/test_lowlevel.py +++ b/tests/test_lowlevel.py @@ -5,21 +5,21 @@ import requests from tests.testserver.server import Server, consume_socket_content +from tests.testserver.werkzeug_server import WerkzeugServer from .utils import override_environ def test_chunked_upload(): """can safely send generators""" - close_server = threading.Event() - server = Server.basic_response_server(wait_to_close_event=close_server) + server = WerkzeugServer.echo_server() data = iter([b'a', b'b', b'c']) with server as (host, port): url = 'http://{}:{}/'.format(host, port) r = requests.post(url, data=data, stream=True) - close_server.set() # release server block + assert r.content == b'abc' assert r.status_code == 200 assert r.request.headers['Transfer-Encoding'] == 'chunked' diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py new file mode 100644 index 0000000000..cd36903fc6 --- /dev/null +++ b/tests/testserver/werkzeug_server.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +import multiprocessing +import socket + +from werkzeug import Request, Response, run_simple + + +class WerkzeugServer(object): + """Realistic WSGI server for unit testing.""" + + def __init__(self, application, host='localhost', port=0): + super(WerkzeugServer, self).__init__() + + self.host = host + self.port = port + + # Werkzeug will not automatically pick a valid port for us. + if not self.port: + sock = socket.socket() + sock.bind((self.host, self.port)) + self.port = sock.getsockname()[1] + + # Try to close the socket. Ignore errors. + try: + sock.close() + except IOError: + pass + + def run_app() -> None: + run_simple(self.host, self.port, application) + + self.process = multiprocessing.Process( + target=run_app) + + @classmethod + def echo_server(cls): + @Request.application + def echo_application(request: Request) -> Response: + return Response( + request.get_data(),\ + 200, + content_type=request.content_type) + + return WerkzeugServer(echo_application) + + def __enter__(self): + self.process.start() + return self.host, self.port + + def __exit__(self, exc_type, exc_value, traceback): + self.process.terminate() + return False From b270e7336236f3ee92b3193c01a0a0af1ec0de8f Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 02:53:30 -0400 Subject: [PATCH 02/16] Remove annotations that broke the build in Python 2.7 --- tests/testserver/werkzeug_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index cd36903fc6..ce73199bdc 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -26,7 +26,7 @@ def __init__(self, application, host='localhost', port=0): except IOError: pass - def run_app() -> None: + def run_app(): run_simple(self.host, self.port, application) self.process = multiprocessing.Process( @@ -35,9 +35,9 @@ def run_app() -> None: @classmethod def echo_server(cls): @Request.application - def echo_application(request: Request) -> Response: + def echo_application(request): return Response( - request.get_data(),\ + request.get_data(), 200, content_type=request.content_type) From 7d4ce9ee6922f0861972da13b7f2d79345c550c1 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 03:02:12 -0400 Subject: [PATCH 03/16] Attempt to fix pickling error in Windows and MacOS builds. --- tests/testserver/werkzeug_server.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index ce73199bdc..4b2773acf2 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -26,11 +26,9 @@ def __init__(self, application, host='localhost', port=0): except IOError: pass - def run_app(): - run_simple(self.host, self.port, application) - self.process = multiprocessing.Process( - target=run_app) + target=run_simple, + args=(self.host, self.port, application)) @classmethod def echo_server(cls): From 8916243de299ba9d910243045d879e7fd2ff668f Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 03:08:14 -0400 Subject: [PATCH 04/16] Fix pickling error for echo_application from Windows and MacOS builds. --- tests/testserver/werkzeug_server.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index 4b2773acf2..b911c29c36 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -5,6 +5,14 @@ from werkzeug import Request, Response, run_simple +@Request.application +def echo_application(request): + return Response( + request.get_data(), + 200, + content_type=request.content_type) + + class WerkzeugServer(object): """Realistic WSGI server for unit testing.""" @@ -32,13 +40,6 @@ def __init__(self, application, host='localhost', port=0): @classmethod def echo_server(cls): - @Request.application - def echo_application(request): - return Response( - request.get_data(), - 200, - content_type=request.content_type) - return WerkzeugServer(echo_application) def __enter__(self): From 695ac3265644127501d845be83bab3c3e3b4793f Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 03:30:16 -0400 Subject: [PATCH 05/16] See if a 15-second pause when starting the server fixes the "connection refused" error in MacOS. Maybe the server simply isn't ready yet and it's a flake. --- tests/testserver/werkzeug_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index b911c29c36..2cb3d8238a 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import multiprocessing import socket +import time from werkzeug import Request, Response, run_simple @@ -44,6 +45,7 @@ def echo_server(cls): def __enter__(self): self.process.start() + time.sleep(15) return self.host, self.port def __exit__(self, exc_type, exc_value, traceback): From 6cbf092d7962e0f0fd6407add2ff1d4043844b52 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 03:42:08 -0400 Subject: [PATCH 06/16] Attempt to fix flaky tests by waiting for the Werkzeug server to be accepting connections before returning. --- tests/testserver/werkzeug_server.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index 2cb3d8238a..02f5f82f68 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import multiprocessing import socket -import time from werkzeug import Request, Response, run_simple @@ -45,7 +44,17 @@ def echo_server(cls): def __enter__(self): self.process.start() - time.sleep(15) + + # Confirm that we can actually connect to the socket before we return. + # This protects from flaky tests should the process come up too late. + sock = socket.socket() + while sock.connect_ex((self.host, self.port)): + pass + try: + sock.close() + except IOError: + pass + return self.host, self.port def __exit__(self, exc_type, exc_value, traceback): From 287f3eb3a9deeddc7142c28080498532c0d3658a Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 03:57:25 -0400 Subject: [PATCH 07/16] I wonder if the reason the MacOS errors are occurring is related to connection timeouts. Let's try adding one. --- tests/testserver/werkzeug_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index 02f5f82f68..0e75bf9a1c 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -15,6 +15,7 @@ def echo_application(request): class WerkzeugServer(object): """Realistic WSGI server for unit testing.""" + SOCKET_CONNECT_TIMEOUT = 5 def __init__(self, application, host='localhost', port=0): super(WerkzeugServer, self).__init__() @@ -48,6 +49,7 @@ def __enter__(self): # Confirm that we can actually connect to the socket before we return. # This protects from flaky tests should the process come up too late. sock = socket.socket() + sock.settimeout(5) while sock.connect_ex((self.host, self.port)): pass try: From bfc094992f2726ecbe679b4d5dcf1d6773d0f9b1 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 16:25:01 -0400 Subject: [PATCH 08/16] Try to fix the socket error in MacOS by not reusing the same socket when waiting: Instead, we close any socket that fails to connect and try again with a fresh one. --- tests/testserver/werkzeug_server.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index 0e75bf9a1c..bef84ef6ec 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -2,6 +2,7 @@ import multiprocessing import socket +from contextlib import closing from werkzeug import Request, Response, run_simple @@ -43,18 +44,20 @@ def __init__(self, application, host='localhost', port=0): def echo_server(cls): return WerkzeugServer(echo_application) + def _socket_is_ready(self): + with closing(socket.socket()) as sock: + sock.settimeout(2) + if sock.connect_ex((self.host, self.port)) == 0: + return True + else: + return False + def __enter__(self): self.process.start() # Confirm that we can actually connect to the socket before we return. # This protects from flaky tests should the process come up too late. - sock = socket.socket() - sock.settimeout(5) - while sock.connect_ex((self.host, self.port)): - pass - try: - sock.close() - except IOError: + while not self._socket_is_ready(): pass return self.host, self.port From 70b5ef98233d4a0e816664b43a0fc3dd6572d4b9 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 16:34:23 -0400 Subject: [PATCH 09/16] Reviewing the changes, I'm not sure why wheel was dropped as a test dependency. Re-add it. --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7b5e9412a9..fc64fb66cb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ httpbin==0.7.0 Flask>=1.0,<2.0 trustme Werkzeug +wheel From 9ca4848b4dca7546fcabad77823daa2b7f83ab26 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 16:37:29 -0400 Subject: [PATCH 10/16] Clean up port auto-selection using contextlib. --- tests/testserver/werkzeug_server.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index bef84ef6ec..0b5d6a60c7 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -26,15 +26,9 @@ def __init__(self, application, host='localhost', port=0): # Werkzeug will not automatically pick a valid port for us. if not self.port: - sock = socket.socket() - sock.bind((self.host, self.port)) - self.port = sock.getsockname()[1] - - # Try to close the socket. Ignore errors. - try: - sock.close() - except IOError: - pass + with closing(socket.socket()) as sock: + sock.bind((self.host, self.port)) + self.port = sock.getsockname()[1] self.process = multiprocessing.Process( target=run_simple, From 539721bc35a7a0193563f5e4aaf95b34168f4ca9 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 16:39:30 -0400 Subject: [PATCH 11/16] Actually use the SOCKET_CONNECT_TIMEOUT variable. --- tests/testserver/werkzeug_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index 0b5d6a60c7..57e7785afc 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -16,7 +16,7 @@ def echo_application(request): class WerkzeugServer(object): """Realistic WSGI server for unit testing.""" - SOCKET_CONNECT_TIMEOUT = 5 + SOCKET_CONNECT_TIMEOUT = 2 def __init__(self, application, host='localhost', port=0): super(WerkzeugServer, self).__init__() @@ -40,7 +40,7 @@ def echo_server(cls): def _socket_is_ready(self): with closing(socket.socket()) as sock: - sock.settimeout(2) + sock.settimeout(self.SOCKET_CONNECT_TIMEOUT) if sock.connect_ex((self.host, self.port)) == 0: return True else: From 5587641bd94f90b6fdd714f5dbc6e8be0abce3d0 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Fri, 27 Aug 2021 22:10:59 -0400 Subject: [PATCH 12/16] Further simplify the Werkzeug test server. --- tests/testserver/werkzeug_server.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index 57e7785afc..e69c2ae02c 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -41,10 +41,7 @@ def echo_server(cls): def _socket_is_ready(self): with closing(socket.socket()) as sock: sock.settimeout(self.SOCKET_CONNECT_TIMEOUT) - if sock.connect_ex((self.host, self.port)) == 0: - return True - else: - return False + return sock.connect_ex((self.host, self.port)) == 0 def __enter__(self): self.process.start() From 5e10520aaae53a88865c7ffcbbdfaaea37c065b2 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Thu, 28 Jul 2022 02:55:43 -0400 Subject: [PATCH 13/16] Use the example WerkzeugServer instead of the existing fake server. --- tests/test_lowlevel.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py index 210c068ce2..547fa29ea0 100644 --- a/tests/test_lowlevel.py +++ b/tests/test_lowlevel.py @@ -24,17 +24,16 @@ def echo_response_handler(sock): def test_chunked_upload(): """can safely send generators""" - close_server = threading.Event() - server = Server.basic_response_server(wait_to_close_event=close_server) - data = iter([b"a", b"b", b"c"]) + server = WerkzeugServer.echo_server() + data = iter([b'a', b'b', b'c']) with server as (host, port): - url = f"http://{host}:{port}/" + url = 'http://{}:{}/'.format(host, port) r = requests.post(url, data=data, stream=True) assert r.content == b'abc' assert r.status_code == 200 - assert r.request.headers["Transfer-Encoding"] == "chunked" + assert r.request.headers['Transfer-Encoding'] == 'chunked' def test_chunked_encoding_error(): From df89070bac534bb8ccce6a2666e612e7c1406538 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Thu, 28 Jul 2022 03:00:29 -0400 Subject: [PATCH 14/16] Linter fixes. --- tests/testserver/werkzeug_server.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index e69c2ae02c..0e9185d81a 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -1,25 +1,23 @@ # -*- coding: utf-8 -*- import multiprocessing import socket - from contextlib import closing + from werkzeug import Request, Response, run_simple @Request.application def echo_application(request): - return Response( - request.get_data(), - 200, - content_type=request.content_type) + return Response(request.get_data(), 200, content_type=request.content_type) -class WerkzeugServer(object): +class WerkzeugServer: """Realistic WSGI server for unit testing.""" + SOCKET_CONNECT_TIMEOUT = 2 - def __init__(self, application, host='localhost', port=0): - super(WerkzeugServer, self).__init__() + def __init__(self, application, host="localhost", port=0): + super().__init__() self.host = host self.port = port @@ -31,8 +29,8 @@ def __init__(self, application, host='localhost', port=0): self.port = sock.getsockname()[1] self.process = multiprocessing.Process( - target=run_simple, - args=(self.host, self.port, application)) + target=run_simple, args=(self.host, self.port, application) + ) @classmethod def echo_server(cls): @@ -48,7 +46,7 @@ def __enter__(self): # Confirm that we can actually connect to the socket before we return. # This protects from flaky tests should the process come up too late. - while not self._socket_is_ready(): + while not self._socket_is_ready(): pass return self.host, self.port From 9a6a8b2f9349c3697fc819ac17304416d9a36718 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Thu, 28 Jul 2022 03:02:44 -0400 Subject: [PATCH 15/16] Fix linter issue. --- tests/test_lowlevel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py index 547fa29ea0..2079b4d62e 100644 --- a/tests/test_lowlevel.py +++ b/tests/test_lowlevel.py @@ -28,7 +28,7 @@ def test_chunked_upload(): data = iter([b'a', b'b', b'c']) with server as (host, port): - url = 'http://{}:{}/'.format(host, port) + url = f'http://{host}:{port}/' r = requests.post(url, data=data, stream=True) assert r.content == b'abc' From 7ce0767b9ead091d5332c354272210ccf612679d Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Thu, 28 Jul 2022 03:03:14 -0400 Subject: [PATCH 16/16] Linter fix. --- tests/testserver/werkzeug_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/testserver/werkzeug_server.py b/tests/testserver/werkzeug_server.py index 0e9185d81a..ef8903a675 100644 --- a/tests/testserver/werkzeug_server.py +++ b/tests/testserver/werkzeug_server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import multiprocessing import socket from contextlib import closing