From a0066e5752ca0a5613de58c91de1273493415094 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 19 Jan 2021 15:54:20 +0200 Subject: [PATCH 1/6] Initial --- sanic/app.py | 16 +- sanic/testing.py | 284 ---------------- setup.py | 3 +- tests/test_asgi_client.py | 5 - tests/test_keep_alive_timeout.py | 564 +++++++++++++++---------------- tests/test_logging.py | 3 +- tests/test_logo.py | 3 +- tests/test_multiprocessing.py | 3 +- tests/test_request_timeout.py | 2 +- tests/test_requests.py | 11 +- tests/test_response.py | 2 +- tests/test_routes.py | 3 +- tests/test_server_events.py | 2 +- tests/test_signal_handlers.py | 3 +- tests/test_test_client_port.py | 3 +- tests/test_url_building.py | 5 +- tox.ini | 3 +- 17 files changed, 321 insertions(+), 594 deletions(-) delete mode 100644 sanic/testing.py delete mode 100644 tests/test_asgi_client.py diff --git a/sanic/app.py b/sanic/app.py index 9530ce9215..1c1d128220 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -34,7 +34,6 @@ serve_multiple, ) from sanic.static import register as static_register -from sanic.testing import SanicASGITestClient, SanicTestClient from sanic.views import CompositionView from sanic.websocket import ConnectionClosed, WebSocketProtocol @@ -87,6 +86,7 @@ def __init__( self.websocket_tasks: Set[Future] = set() self.named_request_middleware: Dict[str, MiddlewareType] = {} self.named_response_middleware: Dict[str, MiddlewareType] = {} + self._test_manager = None # Register alternative method names self.go_fast = self.run @@ -1032,11 +1032,21 @@ async def handle_request(self, request): @property def test_client(self): - return SanicTestClient(self) + if self._test_manager: + return self._test_manager.test_client + from sanic_testing import TestManager + + manager = TestManager(self) + return manager.test_client @property def asgi_client(self): - return SanicASGITestClient(self) + if self._test_manager: + return self._test_manager.asgi_client + from sanic_testing import TestManager + + manager = TestManager(self) + return manager.asgi_client # -------------------------------------------------------------------- # # Execution diff --git a/sanic/testing.py b/sanic/testing.py deleted file mode 100644 index c9bf003286..0000000000 --- a/sanic/testing.py +++ /dev/null @@ -1,284 +0,0 @@ -from json import JSONDecodeError -from socket import socket - -import httpx -import websockets - -from sanic.asgi import ASGIApp -from sanic.exceptions import MethodNotSupported -from sanic.log import logger -from sanic.response import text - - -ASGI_HOST = "mockserver" -ASGI_PORT = 1234 -ASGI_BASE_URL = f"http://{ASGI_HOST}:{ASGI_PORT}" -HOST = "127.0.0.1" -PORT = None - - -class SanicTestClient: - def __init__(self, app, port=PORT, host=HOST): - """Use port=None to bind to a random port""" - self.app = app - self.port = port - self.host = host - - @app.listener("after_server_start") - def _start_test_mode(sanic, *args, **kwargs): - sanic.test_mode = True - - @app.listener("before_server_end") - def _end_test_mode(sanic, *args, **kwargs): - sanic.test_mode = False - - def get_new_session(self): - return httpx.AsyncClient(verify=False) - - async def _local_request(self, method, url, *args, **kwargs): - logger.info(url) - raw_cookies = kwargs.pop("raw_cookies", None) - - if method == "websocket": - async with websockets.connect(url, *args, **kwargs) as websocket: - websocket.opened = websocket.open - return websocket - else: - async with self.get_new_session() as session: - - try: - if method == "request": - args = [url] + list(args) - url = kwargs.pop("http_method", "GET").upper() - response = await getattr(session, method.lower())( - url, *args, **kwargs - ) - except httpx.HTTPError as e: - if hasattr(e, "response"): - response = e.response - else: - logger.error( - f"{method.upper()} {url} received no response!", - exc_info=True, - ) - return None - - response.body = await response.aread() - response.status = response.status_code - response.content_type = response.headers.get("content-type") - - # response can be decoded as json after response._content - # is set by response.aread() - try: - response.json = response.json() - except (JSONDecodeError, UnicodeDecodeError): - response.json = None - - if raw_cookies: - response.raw_cookies = {} - - for cookie in response.cookies.jar: - response.raw_cookies[cookie.name] = cookie - - return response - - def _sanic_endpoint_test( - self, - method="get", - uri="/", - gather_request=True, - debug=False, - server_kwargs={"auto_reload": False}, - host=None, - *request_args, - **request_kwargs, - ): - results = [None, None] - exceptions = [] - if gather_request: - - def _collect_request(request): - if results[0] is None: - results[0] = request - - self.app.request_middleware.appendleft(_collect_request) - - @self.app.exception(MethodNotSupported) - async def error_handler(request, exception): - if request.method in ["HEAD", "PATCH", "PUT", "DELETE"]: - return text( - "", exception.status_code, headers=exception.headers - ) - else: - return self.app.error_handler.default(request, exception) - - if self.port: - server_kwargs = dict( - host=host or self.host, - port=self.port, - **server_kwargs, - ) - host, port = host or self.host, self.port - else: - sock = socket() - sock.bind((host or self.host, 0)) - server_kwargs = dict(sock=sock, **server_kwargs) - host, port = sock.getsockname() - self.port = port - - if uri.startswith( - ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") - ): - url = uri - else: - uri = uri if uri.startswith("/") else f"/{uri}" - scheme = "ws" if method == "websocket" else "http" - url = f"{scheme}://{host}:{port}{uri}" - # Tests construct URLs using PORT = None, which means random port not - # known until this function is called, so fix that here - url = url.replace(":None/", f":{port}/") - - @self.app.listener("after_server_start") - async def _collect_response(sanic, loop): - try: - response = await self._local_request( - method, url, *request_args, **request_kwargs - ) - results[-1] = response - except Exception as e: - logger.exception("Exception") - exceptions.append(e) - self.app.stop() - - self.app.run(debug=debug, **server_kwargs) - self.app.listeners["after_server_start"].pop() - - if exceptions: - raise ValueError(f"Exception during request: {exceptions}") - - if gather_request: - try: - request, response = results - return request, response - except BaseException: # noqa - raise ValueError( - f"Request and response object expected, got ({results})" - ) - else: - try: - return results[-1] - except BaseException: # noqa - raise ValueError(f"Request object expected, got ({results})") - - def request(self, *args, **kwargs): - return self._sanic_endpoint_test("request", *args, **kwargs) - - def get(self, *args, **kwargs): - return self._sanic_endpoint_test("get", *args, **kwargs) - - def post(self, *args, **kwargs): - return self._sanic_endpoint_test("post", *args, **kwargs) - - def put(self, *args, **kwargs): - return self._sanic_endpoint_test("put", *args, **kwargs) - - def delete(self, *args, **kwargs): - return self._sanic_endpoint_test("delete", *args, **kwargs) - - def patch(self, *args, **kwargs): - return self._sanic_endpoint_test("patch", *args, **kwargs) - - def options(self, *args, **kwargs): - return self._sanic_endpoint_test("options", *args, **kwargs) - - def head(self, *args, **kwargs): - return self._sanic_endpoint_test("head", *args, **kwargs) - - def websocket(self, *args, **kwargs): - return self._sanic_endpoint_test("websocket", *args, **kwargs) - - -class TestASGIApp(ASGIApp): - async def __call__(self): - await super().__call__() - return self.request - - -async def app_call_with_return(self, scope, receive, send): - asgi_app = await TestASGIApp.create(self, scope, receive, send) - return await asgi_app() - - -class SanicASGITestClient(httpx.AsyncClient): - def __init__( - self, - app, - base_url: str = ASGI_BASE_URL, - suppress_exceptions: bool = False, - ) -> None: - app.__class__.__call__ = app_call_with_return - app.asgi = True - - self.app = app - transport = httpx.ASGITransport(app=app, client=(ASGI_HOST, ASGI_PORT)) - super().__init__(transport=transport, base_url=base_url) - - self.last_request = None - - def _collect_request(request): - self.last_request = request - - @app.listener("after_server_start") - def _start_test_mode(sanic, *args, **kwargs): - sanic.test_mode = True - - @app.listener("before_server_end") - def _end_test_mode(sanic, *args, **kwargs): - sanic.test_mode = False - - app.request_middleware.appendleft(_collect_request) - - async def request(self, method, url, gather_request=True, *args, **kwargs): - - self.gather_request = gather_request - response = await super().request(method, url, *args, **kwargs) - response.status = response.status_code - response.body = response.content - response.content_type = response.headers.get("content-type") - - return self.last_request, response - - async def websocket(self, uri, subprotocols=None, *args, **kwargs): - scheme = "ws" - path = uri - root_path = f"{scheme}://{ASGI_HOST}" - - headers = kwargs.get("headers", {}) - headers.setdefault("connection", "upgrade") - headers.setdefault("sec-websocket-key", "testserver==") - headers.setdefault("sec-websocket-version", "13") - if subprotocols is not None: - headers.setdefault( - "sec-websocket-protocol", ", ".join(subprotocols) - ) - - scope = { - "type": "websocket", - "asgi": {"version": "3.0"}, - "http_version": "1.1", - "headers": [map(lambda y: y.encode(), x) for x in headers.items()], - "scheme": scheme, - "root_path": root_path, - "path": path, - "query_string": b"", - } - - async def receive(): - return {} - - async def send(message): - pass - - await self.app(scope, receive, send) - - return None, {} diff --git a/setup.py b/setup.py index 02649b57fb..c3f79166d4 100644 --- a/setup.py +++ b/setup.py @@ -89,15 +89,14 @@ def open_local(paths, mode="r", encoding="utf8"): "aiofiles>=0.6.0", "websockets>=8.1,<9.0", "multidict>=5.0,<6.0", - "httpx==0.15.4", ] tests_require = [ + "sanic-testing", "pytest==5.2.1", "multidict>=5.0,<6.0", "gunicorn==20.0.4", "pytest-cov", - "httpcore==0.11.*", "beautifulsoup4", uvloop, ujson, diff --git a/tests/test_asgi_client.py b/tests/test_asgi_client.py deleted file mode 100644 index d0fa1d912b..0000000000 --- a/tests/test_asgi_client.py +++ /dev/null @@ -1,5 +0,0 @@ -from sanic.testing import SanicASGITestClient - - -def test_asgi_client_instantiation(app): - assert isinstance(app.asgi_client, SanicASGITestClient) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 1b98c22974..f660d27e96 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -1,282 +1,282 @@ -import asyncio - -from asyncio import sleep as aio_sleep -from json import JSONDecodeError -from os import environ - -import httpcore -import httpx -import pytest - -from sanic import Sanic, server -from sanic.compat import OS_IS_WINDOWS -from sanic.response import text -from sanic.testing import HOST, SanicTestClient - - -CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} - -PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port - -from httpcore._async.base import ConnectionState -from httpcore._async.connection import AsyncHTTPConnection -from httpcore._types import Origin - - -class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool): - last_reused_connection = None - - async def _get_connection_from_pool(self, *args, **kwargs): - conn = await super()._get_connection_from_pool(*args, **kwargs) - self.__class__.last_reused_connection = conn - return conn - - -class ResusableSanicSession(httpx.AsyncClient): - def __init__(self, *args, **kwargs) -> None: - transport = ReusableSanicConnectionPool() - super().__init__(transport=transport, *args, **kwargs) - - -class ReuseableSanicTestClient(SanicTestClient): - def __init__(self, app, loop=None): - super().__init__(app) - if loop is None: - loop = asyncio.get_event_loop() - self._loop = loop - self._server = None - self._tcp_connector = None - self._session = None - - def get_new_session(self): - return ResusableSanicSession() - - # Copied from SanicTestClient, but with some changes to reuse the - # same loop for the same app. - def _sanic_endpoint_test( - self, - method="get", - uri="/", - gather_request=True, - debug=False, - server_kwargs=None, - *request_args, - **request_kwargs, - ): - loop = self._loop - results = [None, None] - exceptions = [] - server_kwargs = server_kwargs or {"return_asyncio_server": True} - if gather_request: - - def _collect_request(request): - if results[0] is None: - results[0] = request - - self.app.request_middleware.appendleft(_collect_request) - - if uri.startswith( - ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") - ): - url = uri - else: - uri = uri if uri.startswith("/") else f"/{uri}" - scheme = "http" - url = f"{scheme}://{HOST}:{PORT}{uri}" - - @self.app.listener("after_server_start") - async def _collect_response(loop): - try: - response = await self._local_request( - method, url, *request_args, **request_kwargs - ) - results[-1] = response - except Exception as e2: - exceptions.append(e2) - - if self._server is not None: - _server = self._server - else: - _server_co = self.app.create_server( - host=HOST, debug=debug, port=PORT, **server_kwargs - ) - - server.trigger_events( - self.app.listeners["before_server_start"], loop - ) - - try: - loop._stopping = False - _server = loop.run_until_complete(_server_co) - except Exception as e1: - raise e1 - self._server = _server - server.trigger_events(self.app.listeners["after_server_start"], loop) - self.app.listeners["after_server_start"].pop() - - if exceptions: - raise ValueError(f"Exception during request: {exceptions}") - - if gather_request: - self.app.request_middleware.pop() - try: - request, response = results - return request, response - except Exception: - raise ValueError( - f"Request and response object expected, got ({results})" - ) - else: - try: - return results[-1] - except Exception: - raise ValueError(f"Request object expected, got ({results})") - - def kill_server(self): - try: - if self._server: - self._server.close() - self._loop.run_until_complete(self._server.wait_closed()) - self._server = None - - if self._session: - self._loop.run_until_complete(self._session.aclose()) - self._session = None - - except Exception as e3: - raise e3 - - # Copied from SanicTestClient, but with some changes to reuse the - # same TCPConnection and the sane ClientSession more than once. - # Note, you cannot use the same session if you are in a _different_ - # loop, so the changes above are required too. - async def _local_request(self, method, url, *args, **kwargs): - raw_cookies = kwargs.pop("raw_cookies", None) - request_keepalive = kwargs.pop( - "request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"] - ) - if not self._session: - self._session = self.get_new_session() - try: - response = await getattr(self._session, method.lower())( - url, timeout=request_keepalive, *args, **kwargs - ) - except NameError: - raise Exception(response.status_code) - - try: - response.json = response.json() - except (JSONDecodeError, UnicodeDecodeError): - response.json = None - - response.body = await response.aread() - response.status = response.status_code - response.content_type = response.headers.get("content-type") - - if raw_cookies: - response.raw_cookies = {} - for cookie in response.cookies: - response.raw_cookies[cookie.name] = cookie - - return response - - -keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse") -keep_alive_app_client_timeout = Sanic("test_ka_client_timeout") -keep_alive_app_server_timeout = Sanic("test_ka_server_timeout") - -keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS) -keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS) -keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS) - - -@keep_alive_timeout_app_reuse.route("/1") -async def handler1(request): - return text("OK") - - -@keep_alive_app_client_timeout.route("/1") -async def handler2(request): - return text("OK") - - -@keep_alive_app_server_timeout.route("/1") -async def handler3(request): - return text("OK") - - -@pytest.mark.skipif( - bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, - reason="Not testable with current client", -) -def test_keep_alive_timeout_reuse(): - """If the server keep-alive timeout and client keep-alive timeout are - both longer than the delay, the client _and_ server will successfully - reuse the existing connection.""" - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) - headers = {"Connection": "keep-alive"} - request, response = client.get("/1", headers=headers) - assert response.status == 200 - assert response.text == "OK" - loop.run_until_complete(aio_sleep(1)) - request, response = client.get("/1") - assert response.status == 200 - assert response.text == "OK" - assert ReusableSanicConnectionPool.last_reused_connection - finally: - client.kill_server() - - -@pytest.mark.skipif( - bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, - reason="Not testable with current client", -) -def test_keep_alive_client_timeout(): - """If the server keep-alive timeout is longer than the client - keep-alive timeout, client will try to create a new connection here.""" - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) - headers = {"Connection": "keep-alive"} - request, response = client.get( - "/1", headers=headers, request_keepalive=1 - ) - assert response.status == 200 - assert response.text == "OK" - loop.run_until_complete(aio_sleep(2)) - exception = None - request, response = client.get("/1", request_keepalive=1) - assert ReusableSanicConnectionPool.last_reused_connection is None - finally: - client.kill_server() - - -@pytest.mark.skipif( - bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, - reason="Not testable with current client", -) -def test_keep_alive_server_timeout(): - """If the client keep-alive timeout is longer than the server - keep-alive timeout, the client will either a 'Connection reset' error - _or_ a new connection. Depending on how the event-loop handles the - broken server connection.""" - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) - headers = {"Connection": "keep-alive"} - request, response = client.get( - "/1", headers=headers, request_keepalive=60 - ) - assert response.status == 200 - assert response.text == "OK" - loop.run_until_complete(aio_sleep(3)) - exception = None - request, response = client.get("/1", request_keepalive=60) - assert ReusableSanicConnectionPool.last_reused_connection is None - finally: - client.kill_server() +# import asyncio + +# from asyncio import sleep as aio_sleep +# from json import JSONDecodeError +# from os import environ + +# import httpcore +# import httpx +# import pytest + +# from sanic import Sanic, server +# from sanic.compat import OS_IS_WINDOWS +# from sanic.response import text +# from sanic.testing import HOST, SanicTestClient + + +# CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} + +# PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port + +# from httpcore._async.base import ConnectionState +# from httpcore._async.connection import AsyncHTTPConnection +# from httpcore._types import Origin + + +# class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool): +# last_reused_connection = None + +# async def _get_connection_from_pool(self, *args, **kwargs): +# conn = await super()._get_connection_from_pool(*args, **kwargs) +# self.__class__.last_reused_connection = conn +# return conn + + +# class ResusableSanicSession(httpx.AsyncClient): +# def __init__(self, *args, **kwargs) -> None: +# transport = ReusableSanicConnectionPool() +# super().__init__(transport=transport, *args, **kwargs) + + +# class ReuseableSanicTestClient(SanicTestClient): +# def __init__(self, app, loop=None): +# super().__init__(app) +# if loop is None: +# loop = asyncio.get_event_loop() +# self._loop = loop +# self._server = None +# self._tcp_connector = None +# self._session = None + +# def get_new_session(self): +# return ResusableSanicSession() + +# # Copied from SanicTestClient, but with some changes to reuse the +# # same loop for the same app. +# def _sanic_endpoint_test( +# self, +# method="get", +# uri="/", +# gather_request=True, +# debug=False, +# server_kwargs=None, +# *request_args, +# **request_kwargs, +# ): +# loop = self._loop +# results = [None, None] +# exceptions = [] +# server_kwargs = server_kwargs or {"return_asyncio_server": True} +# if gather_request: + +# def _collect_request(request): +# if results[0] is None: +# results[0] = request + +# self.app.request_middleware.appendleft(_collect_request) + +# if uri.startswith( +# ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") +# ): +# url = uri +# else: +# uri = uri if uri.startswith("/") else f"/{uri}" +# scheme = "http" +# url = f"{scheme}://{HOST}:{PORT}{uri}" + +# @self.app.listener("after_server_start") +# async def _collect_response(loop): +# try: +# response = await self._local_request( +# method, url, *request_args, **request_kwargs +# ) +# results[-1] = response +# except Exception as e2: +# exceptions.append(e2) + +# if self._server is not None: +# _server = self._server +# else: +# _server_co = self.app.create_server( +# host=HOST, debug=debug, port=PORT, **server_kwargs +# ) + +# server.trigger_events( +# self.app.listeners["before_server_start"], loop +# ) + +# try: +# loop._stopping = False +# _server = loop.run_until_complete(_server_co) +# except Exception as e1: +# raise e1 +# self._server = _server +# server.trigger_events(self.app.listeners["after_server_start"], loop) +# self.app.listeners["after_server_start"].pop() + +# if exceptions: +# raise ValueError(f"Exception during request: {exceptions}") + +# if gather_request: +# self.app.request_middleware.pop() +# try: +# request, response = results +# return request, response +# except Exception: +# raise ValueError( +# f"Request and response object expected, got ({results})" +# ) +# else: +# try: +# return results[-1] +# except Exception: +# raise ValueError(f"Request object expected, got ({results})") + +# def kill_server(self): +# try: +# if self._server: +# self._server.close() +# self._loop.run_until_complete(self._server.wait_closed()) +# self._server = None + +# if self._session: +# self._loop.run_until_complete(self._session.aclose()) +# self._session = None + +# except Exception as e3: +# raise e3 + +# # Copied from SanicTestClient, but with some changes to reuse the +# # same TCPConnection and the sane ClientSession more than once. +# # Note, you cannot use the same session if you are in a _different_ +# # loop, so the changes above are required too. +# async def _local_request(self, method, url, *args, **kwargs): +# raw_cookies = kwargs.pop("raw_cookies", None) +# request_keepalive = kwargs.pop( +# "request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"] +# ) +# if not self._session: +# self._session = self.get_new_session() +# try: +# response = await getattr(self._session, method.lower())( +# url, timeout=request_keepalive, *args, **kwargs +# ) +# except NameError: +# raise Exception(response.status_code) + +# try: +# response.json = response.json() +# except (JSONDecodeError, UnicodeDecodeError): +# response.json = None + +# response.body = await response.aread() +# response.status = response.status_code +# response.content_type = response.headers.get("content-type") + +# if raw_cookies: +# response.raw_cookies = {} +# for cookie in response.cookies: +# response.raw_cookies[cookie.name] = cookie + +# return response + + +# keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse") +# keep_alive_app_client_timeout = Sanic("test_ka_client_timeout") +# keep_alive_app_server_timeout = Sanic("test_ka_server_timeout") + +# keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS) +# keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS) +# keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS) + + +# @keep_alive_timeout_app_reuse.route("/1") +# async def handler1(request): +# return text("OK") + + +# @keep_alive_app_client_timeout.route("/1") +# async def handler2(request): +# return text("OK") + + +# @keep_alive_app_server_timeout.route("/1") +# async def handler3(request): +# return text("OK") + + +# @pytest.mark.skipif( +# bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, +# reason="Not testable with current client", +# ) +# def test_keep_alive_timeout_reuse(): +# """If the server keep-alive timeout and client keep-alive timeout are +# both longer than the delay, the client _and_ server will successfully +# reuse the existing connection.""" +# try: +# loop = asyncio.new_event_loop() +# asyncio.set_event_loop(loop) +# client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) +# headers = {"Connection": "keep-alive"} +# request, response = client.get("/1", headers=headers) +# assert response.status == 200 +# assert response.text == "OK" +# loop.run_until_complete(aio_sleep(1)) +# request, response = client.get("/1") +# assert response.status == 200 +# assert response.text == "OK" +# assert ReusableSanicConnectionPool.last_reused_connection +# finally: +# client.kill_server() + + +# @pytest.mark.skipif( +# bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, +# reason="Not testable with current client", +# ) +# def test_keep_alive_client_timeout(): +# """If the server keep-alive timeout is longer than the client +# keep-alive timeout, client will try to create a new connection here.""" +# try: +# loop = asyncio.new_event_loop() +# asyncio.set_event_loop(loop) +# client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) +# headers = {"Connection": "keep-alive"} +# request, response = client.get( +# "/1", headers=headers, request_keepalive=1 +# ) +# assert response.status == 200 +# assert response.text == "OK" +# loop.run_until_complete(aio_sleep(2)) +# exception = None +# request, response = client.get("/1", request_keepalive=1) +# assert ReusableSanicConnectionPool.last_reused_connection is None +# finally: +# client.kill_server() + + +# @pytest.mark.skipif( +# bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, +# reason="Not testable with current client", +# ) +# def test_keep_alive_server_timeout(): +# """If the client keep-alive timeout is longer than the server +# keep-alive timeout, the client will either a 'Connection reset' error +# _or_ a new connection. Depending on how the event-loop handles the +# broken server connection.""" +# try: +# loop = asyncio.new_event_loop() +# asyncio.set_event_loop(loop) +# client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) +# headers = {"Connection": "keep-alive"} +# request, response = client.get( +# "/1", headers=headers, request_keepalive=60 +# ) +# assert response.status == 200 +# assert response.text == "OK" +# loop.run_until_complete(aio_sleep(3)) +# exception = None +# request, response = client.get("/1", request_keepalive=60) +# assert ReusableSanicConnectionPool.last_reused_connection is None +# finally: +# client.kill_server() diff --git a/tests/test_logging.py b/tests/test_logging.py index 069ec6046a..f5a24f08ef 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -8,13 +8,14 @@ import pytest +from sanic_testing.testing import SanicTestClient + import sanic from sanic import Sanic from sanic.compat import OS_IS_WINDOWS from sanic.log import LOGGING_CONFIG_DEFAULTS, logger from sanic.response import text -from sanic.testing import SanicTestClient logging_format = """module: %(module)s; \ diff --git a/tests/test_logo.py b/tests/test_logo.py index e8df2ea564..c3206513a4 100644 --- a/tests/test_logo.py +++ b/tests/test_logo.py @@ -1,8 +1,9 @@ import asyncio import logging +from sanic_testing.testing import PORT + from sanic.config import BASE_LOGO -from sanic.testing import PORT try: diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index ea8661ea3d..8508d4236b 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -5,9 +5,10 @@ import pytest +from sanic_testing.testing import HOST, PORT + from sanic import Blueprint from sanic.response import text -from sanic.testing import HOST, PORT @pytest.mark.skipif( diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index d750dd1d6f..f60edeaa35 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -16,10 +16,10 @@ from httpcore._exceptions import LocalProtocolError, UnsupportedProtocol from httpcore._types import TimeoutDict from httpcore._utils import url_to_origin +from sanic_testing.testing import SanicTestClient from sanic import Sanic from sanic.response import text -from sanic.testing import SanicTestClient class DelayableHTTPConnection(httpcore._async.connection.AsyncHTTPConnection): diff --git a/tests/test_requests.py b/tests/test_requests.py index ff6d068873..485b83d1b4 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -8,11 +8,7 @@ import pytest -from sanic import Blueprint, Sanic -from sanic.exceptions import ServerError -from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters -from sanic.response import html, json, text -from sanic.testing import ( +from sanic_testing.testing import ( ASGI_BASE_URL, ASGI_HOST, ASGI_PORT, @@ -21,6 +17,11 @@ SanicTestClient, ) +from sanic import Blueprint, Sanic +from sanic.exceptions import ServerError +from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters +from sanic.response import html, json, text + # ------------------------------------------------------------ # # GET diff --git a/tests/test_response.py b/tests/test_response.py index 24b209816f..7831bb70ee 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -12,6 +12,7 @@ import pytest from aiofiles import os as async_os +from sanic_testing.testing import HOST, PORT from sanic.response import ( HTTPResponse, @@ -25,7 +26,6 @@ text, ) from sanic.server import HttpProtocol -from sanic.testing import HOST, PORT JSON_DATA = {"ok": True} diff --git a/tests/test_routes.py b/tests/test_routes.py index 0c082086f4..73809518d8 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -2,11 +2,12 @@ import pytest +from sanic_testing.testing import SanicTestClient + from sanic import Sanic from sanic.constants import HTTP_METHODS from sanic.response import json, text from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists -from sanic.testing import SanicTestClient # ------------------------------------------------------------ # diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 560e941717..4b41f6fa03 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -6,7 +6,7 @@ import pytest -from sanic.testing import HOST, PORT +from sanic_testing.testing import HOST, PORT AVAILABLE_LISTENERS = [ diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index 6ac3b801e7..857b528348 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -7,9 +7,10 @@ import pytest +from sanic_testing.testing import HOST, PORT + from sanic.compat import ctrlc_workaround_for_windows from sanic.response import HTTPResponse -from sanic.testing import HOST, PORT async def stop(app, loop): diff --git a/tests/test_test_client_port.py b/tests/test_test_client_port.py index 2940ba0d54..334edde3e4 100644 --- a/tests/test_test_client_port.py +++ b/tests/test_test_client_port.py @@ -1,5 +1,6 @@ +from sanic_testing.testing import PORT, SanicTestClient + from sanic.response import json, text -from sanic.testing import PORT, SanicTestClient # ------------------------------------------------------------ # diff --git a/tests/test_url_building.py b/tests/test_url_building.py index 81fb8aaab3..de93015e24 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -4,11 +4,12 @@ import pytest as pytest +from sanic_testing.testing import HOST as test_host +from sanic_testing.testing import PORT as test_port + from sanic.blueprints import Blueprint from sanic.exceptions import URLBuildError from sanic.response import text -from sanic.testing import HOST as test_host -from sanic.testing import PORT as test_port from sanic.views import HTTPMethodView diff --git a/tox.ini b/tox.ini index e365c9fc6f..6ead9ae816 100644 --- a/tox.ini +++ b/tox.ini @@ -7,14 +7,13 @@ setenv = {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1 {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 deps = + sanic-testing coverage==5.3 pytest==5.2.1 pytest-cov pytest-sanic pytest-sugar pytest-benchmark - httpcore==0.11.* - httpx==0.15.4 chardet==3.* beautifulsoup4 gunicorn==20.0.4 From f8f215772c77c34996a7ada081335dc50e66aaaf Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 19 Jan 2021 16:11:09 +0200 Subject: [PATCH 2/6] squash --- tests/test_keep_alive_timeout.py | 564 +++++++++++++++---------------- 1 file changed, 282 insertions(+), 282 deletions(-) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index f660d27e96..1b98c22974 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -1,282 +1,282 @@ -# import asyncio - -# from asyncio import sleep as aio_sleep -# from json import JSONDecodeError -# from os import environ - -# import httpcore -# import httpx -# import pytest - -# from sanic import Sanic, server -# from sanic.compat import OS_IS_WINDOWS -# from sanic.response import text -# from sanic.testing import HOST, SanicTestClient - - -# CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} - -# PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port - -# from httpcore._async.base import ConnectionState -# from httpcore._async.connection import AsyncHTTPConnection -# from httpcore._types import Origin - - -# class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool): -# last_reused_connection = None - -# async def _get_connection_from_pool(self, *args, **kwargs): -# conn = await super()._get_connection_from_pool(*args, **kwargs) -# self.__class__.last_reused_connection = conn -# return conn - - -# class ResusableSanicSession(httpx.AsyncClient): -# def __init__(self, *args, **kwargs) -> None: -# transport = ReusableSanicConnectionPool() -# super().__init__(transport=transport, *args, **kwargs) - - -# class ReuseableSanicTestClient(SanicTestClient): -# def __init__(self, app, loop=None): -# super().__init__(app) -# if loop is None: -# loop = asyncio.get_event_loop() -# self._loop = loop -# self._server = None -# self._tcp_connector = None -# self._session = None - -# def get_new_session(self): -# return ResusableSanicSession() - -# # Copied from SanicTestClient, but with some changes to reuse the -# # same loop for the same app. -# def _sanic_endpoint_test( -# self, -# method="get", -# uri="/", -# gather_request=True, -# debug=False, -# server_kwargs=None, -# *request_args, -# **request_kwargs, -# ): -# loop = self._loop -# results = [None, None] -# exceptions = [] -# server_kwargs = server_kwargs or {"return_asyncio_server": True} -# if gather_request: - -# def _collect_request(request): -# if results[0] is None: -# results[0] = request - -# self.app.request_middleware.appendleft(_collect_request) - -# if uri.startswith( -# ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") -# ): -# url = uri -# else: -# uri = uri if uri.startswith("/") else f"/{uri}" -# scheme = "http" -# url = f"{scheme}://{HOST}:{PORT}{uri}" - -# @self.app.listener("after_server_start") -# async def _collect_response(loop): -# try: -# response = await self._local_request( -# method, url, *request_args, **request_kwargs -# ) -# results[-1] = response -# except Exception as e2: -# exceptions.append(e2) - -# if self._server is not None: -# _server = self._server -# else: -# _server_co = self.app.create_server( -# host=HOST, debug=debug, port=PORT, **server_kwargs -# ) - -# server.trigger_events( -# self.app.listeners["before_server_start"], loop -# ) - -# try: -# loop._stopping = False -# _server = loop.run_until_complete(_server_co) -# except Exception as e1: -# raise e1 -# self._server = _server -# server.trigger_events(self.app.listeners["after_server_start"], loop) -# self.app.listeners["after_server_start"].pop() - -# if exceptions: -# raise ValueError(f"Exception during request: {exceptions}") - -# if gather_request: -# self.app.request_middleware.pop() -# try: -# request, response = results -# return request, response -# except Exception: -# raise ValueError( -# f"Request and response object expected, got ({results})" -# ) -# else: -# try: -# return results[-1] -# except Exception: -# raise ValueError(f"Request object expected, got ({results})") - -# def kill_server(self): -# try: -# if self._server: -# self._server.close() -# self._loop.run_until_complete(self._server.wait_closed()) -# self._server = None - -# if self._session: -# self._loop.run_until_complete(self._session.aclose()) -# self._session = None - -# except Exception as e3: -# raise e3 - -# # Copied from SanicTestClient, but with some changes to reuse the -# # same TCPConnection and the sane ClientSession more than once. -# # Note, you cannot use the same session if you are in a _different_ -# # loop, so the changes above are required too. -# async def _local_request(self, method, url, *args, **kwargs): -# raw_cookies = kwargs.pop("raw_cookies", None) -# request_keepalive = kwargs.pop( -# "request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"] -# ) -# if not self._session: -# self._session = self.get_new_session() -# try: -# response = await getattr(self._session, method.lower())( -# url, timeout=request_keepalive, *args, **kwargs -# ) -# except NameError: -# raise Exception(response.status_code) - -# try: -# response.json = response.json() -# except (JSONDecodeError, UnicodeDecodeError): -# response.json = None - -# response.body = await response.aread() -# response.status = response.status_code -# response.content_type = response.headers.get("content-type") - -# if raw_cookies: -# response.raw_cookies = {} -# for cookie in response.cookies: -# response.raw_cookies[cookie.name] = cookie - -# return response - - -# keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse") -# keep_alive_app_client_timeout = Sanic("test_ka_client_timeout") -# keep_alive_app_server_timeout = Sanic("test_ka_server_timeout") - -# keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS) -# keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS) -# keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS) - - -# @keep_alive_timeout_app_reuse.route("/1") -# async def handler1(request): -# return text("OK") - - -# @keep_alive_app_client_timeout.route("/1") -# async def handler2(request): -# return text("OK") - - -# @keep_alive_app_server_timeout.route("/1") -# async def handler3(request): -# return text("OK") - - -# @pytest.mark.skipif( -# bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, -# reason="Not testable with current client", -# ) -# def test_keep_alive_timeout_reuse(): -# """If the server keep-alive timeout and client keep-alive timeout are -# both longer than the delay, the client _and_ server will successfully -# reuse the existing connection.""" -# try: -# loop = asyncio.new_event_loop() -# asyncio.set_event_loop(loop) -# client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) -# headers = {"Connection": "keep-alive"} -# request, response = client.get("/1", headers=headers) -# assert response.status == 200 -# assert response.text == "OK" -# loop.run_until_complete(aio_sleep(1)) -# request, response = client.get("/1") -# assert response.status == 200 -# assert response.text == "OK" -# assert ReusableSanicConnectionPool.last_reused_connection -# finally: -# client.kill_server() - - -# @pytest.mark.skipif( -# bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, -# reason="Not testable with current client", -# ) -# def test_keep_alive_client_timeout(): -# """If the server keep-alive timeout is longer than the client -# keep-alive timeout, client will try to create a new connection here.""" -# try: -# loop = asyncio.new_event_loop() -# asyncio.set_event_loop(loop) -# client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) -# headers = {"Connection": "keep-alive"} -# request, response = client.get( -# "/1", headers=headers, request_keepalive=1 -# ) -# assert response.status == 200 -# assert response.text == "OK" -# loop.run_until_complete(aio_sleep(2)) -# exception = None -# request, response = client.get("/1", request_keepalive=1) -# assert ReusableSanicConnectionPool.last_reused_connection is None -# finally: -# client.kill_server() - - -# @pytest.mark.skipif( -# bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, -# reason="Not testable with current client", -# ) -# def test_keep_alive_server_timeout(): -# """If the client keep-alive timeout is longer than the server -# keep-alive timeout, the client will either a 'Connection reset' error -# _or_ a new connection. Depending on how the event-loop handles the -# broken server connection.""" -# try: -# loop = asyncio.new_event_loop() -# asyncio.set_event_loop(loop) -# client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) -# headers = {"Connection": "keep-alive"} -# request, response = client.get( -# "/1", headers=headers, request_keepalive=60 -# ) -# assert response.status == 200 -# assert response.text == "OK" -# loop.run_until_complete(aio_sleep(3)) -# exception = None -# request, response = client.get("/1", request_keepalive=60) -# assert ReusableSanicConnectionPool.last_reused_connection is None -# finally: -# client.kill_server() +import asyncio + +from asyncio import sleep as aio_sleep +from json import JSONDecodeError +from os import environ + +import httpcore +import httpx +import pytest + +from sanic import Sanic, server +from sanic.compat import OS_IS_WINDOWS +from sanic.response import text +from sanic.testing import HOST, SanicTestClient + + +CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} + +PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port + +from httpcore._async.base import ConnectionState +from httpcore._async.connection import AsyncHTTPConnection +from httpcore._types import Origin + + +class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool): + last_reused_connection = None + + async def _get_connection_from_pool(self, *args, **kwargs): + conn = await super()._get_connection_from_pool(*args, **kwargs) + self.__class__.last_reused_connection = conn + return conn + + +class ResusableSanicSession(httpx.AsyncClient): + def __init__(self, *args, **kwargs) -> None: + transport = ReusableSanicConnectionPool() + super().__init__(transport=transport, *args, **kwargs) + + +class ReuseableSanicTestClient(SanicTestClient): + def __init__(self, app, loop=None): + super().__init__(app) + if loop is None: + loop = asyncio.get_event_loop() + self._loop = loop + self._server = None + self._tcp_connector = None + self._session = None + + def get_new_session(self): + return ResusableSanicSession() + + # Copied from SanicTestClient, but with some changes to reuse the + # same loop for the same app. + def _sanic_endpoint_test( + self, + method="get", + uri="/", + gather_request=True, + debug=False, + server_kwargs=None, + *request_args, + **request_kwargs, + ): + loop = self._loop + results = [None, None] + exceptions = [] + server_kwargs = server_kwargs or {"return_asyncio_server": True} + if gather_request: + + def _collect_request(request): + if results[0] is None: + results[0] = request + + self.app.request_middleware.appendleft(_collect_request) + + if uri.startswith( + ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") + ): + url = uri + else: + uri = uri if uri.startswith("/") else f"/{uri}" + scheme = "http" + url = f"{scheme}://{HOST}:{PORT}{uri}" + + @self.app.listener("after_server_start") + async def _collect_response(loop): + try: + response = await self._local_request( + method, url, *request_args, **request_kwargs + ) + results[-1] = response + except Exception as e2: + exceptions.append(e2) + + if self._server is not None: + _server = self._server + else: + _server_co = self.app.create_server( + host=HOST, debug=debug, port=PORT, **server_kwargs + ) + + server.trigger_events( + self.app.listeners["before_server_start"], loop + ) + + try: + loop._stopping = False + _server = loop.run_until_complete(_server_co) + except Exception as e1: + raise e1 + self._server = _server + server.trigger_events(self.app.listeners["after_server_start"], loop) + self.app.listeners["after_server_start"].pop() + + if exceptions: + raise ValueError(f"Exception during request: {exceptions}") + + if gather_request: + self.app.request_middleware.pop() + try: + request, response = results + return request, response + except Exception: + raise ValueError( + f"Request and response object expected, got ({results})" + ) + else: + try: + return results[-1] + except Exception: + raise ValueError(f"Request object expected, got ({results})") + + def kill_server(self): + try: + if self._server: + self._server.close() + self._loop.run_until_complete(self._server.wait_closed()) + self._server = None + + if self._session: + self._loop.run_until_complete(self._session.aclose()) + self._session = None + + except Exception as e3: + raise e3 + + # Copied from SanicTestClient, but with some changes to reuse the + # same TCPConnection and the sane ClientSession more than once. + # Note, you cannot use the same session if you are in a _different_ + # loop, so the changes above are required too. + async def _local_request(self, method, url, *args, **kwargs): + raw_cookies = kwargs.pop("raw_cookies", None) + request_keepalive = kwargs.pop( + "request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"] + ) + if not self._session: + self._session = self.get_new_session() + try: + response = await getattr(self._session, method.lower())( + url, timeout=request_keepalive, *args, **kwargs + ) + except NameError: + raise Exception(response.status_code) + + try: + response.json = response.json() + except (JSONDecodeError, UnicodeDecodeError): + response.json = None + + response.body = await response.aread() + response.status = response.status_code + response.content_type = response.headers.get("content-type") + + if raw_cookies: + response.raw_cookies = {} + for cookie in response.cookies: + response.raw_cookies[cookie.name] = cookie + + return response + + +keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse") +keep_alive_app_client_timeout = Sanic("test_ka_client_timeout") +keep_alive_app_server_timeout = Sanic("test_ka_server_timeout") + +keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS) +keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS) +keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS) + + +@keep_alive_timeout_app_reuse.route("/1") +async def handler1(request): + return text("OK") + + +@keep_alive_app_client_timeout.route("/1") +async def handler2(request): + return text("OK") + + +@keep_alive_app_server_timeout.route("/1") +async def handler3(request): + return text("OK") + + +@pytest.mark.skipif( + bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, + reason="Not testable with current client", +) +def test_keep_alive_timeout_reuse(): + """If the server keep-alive timeout and client keep-alive timeout are + both longer than the delay, the client _and_ server will successfully + reuse the existing connection.""" + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) + headers = {"Connection": "keep-alive"} + request, response = client.get("/1", headers=headers) + assert response.status == 200 + assert response.text == "OK" + loop.run_until_complete(aio_sleep(1)) + request, response = client.get("/1") + assert response.status == 200 + assert response.text == "OK" + assert ReusableSanicConnectionPool.last_reused_connection + finally: + client.kill_server() + + +@pytest.mark.skipif( + bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, + reason="Not testable with current client", +) +def test_keep_alive_client_timeout(): + """If the server keep-alive timeout is longer than the client + keep-alive timeout, client will try to create a new connection here.""" + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) + headers = {"Connection": "keep-alive"} + request, response = client.get( + "/1", headers=headers, request_keepalive=1 + ) + assert response.status == 200 + assert response.text == "OK" + loop.run_until_complete(aio_sleep(2)) + exception = None + request, response = client.get("/1", request_keepalive=1) + assert ReusableSanicConnectionPool.last_reused_connection is None + finally: + client.kill_server() + + +@pytest.mark.skipif( + bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, + reason="Not testable with current client", +) +def test_keep_alive_server_timeout(): + """If the client keep-alive timeout is longer than the server + keep-alive timeout, the client will either a 'Connection reset' error + _or_ a new connection. Depending on how the event-loop handles the + broken server connection.""" + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) + headers = {"Connection": "keep-alive"} + request, response = client.get( + "/1", headers=headers, request_keepalive=60 + ) + assert response.status == 200 + assert response.text == "OK" + loop.run_until_complete(aio_sleep(3)) + exception = None + request, response = client.get("/1", request_keepalive=60) + assert ReusableSanicConnectionPool.last_reused_connection is None + finally: + client.kill_server() From 933d005e5d24e6d357d21abe614f2a5f7d81509b Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 19 Jan 2021 16:17:07 +0200 Subject: [PATCH 3/6] squash --- tests/conftest.py | 2 +- tests/test_keep_alive_timeout.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3d57ac733d..cad8d75436 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -127,6 +127,6 @@ def url_param_generator(): return TYPE_TO_GENERATOR_MAP -@pytest.fixture +@pytest.fixture(scope="function") def app(request): return Sanic(request.node.name) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 1b98c22974..ebbec2b5e4 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -8,10 +8,11 @@ import httpx import pytest +from sanic_testing.testing import HOST, SanicTestClient + from sanic import Sanic, server from sanic.compat import OS_IS_WINDOWS from sanic.response import text -from sanic.testing import HOST, SanicTestClient CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} From 1f0f4ef5d56fd1e0d6913daf1adedea3c82d9c26 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 19 Jan 2021 16:34:52 +0200 Subject: [PATCH 4/6] remove testmanager --- sanic/app.py | 23 ++++++++++++----------- tests/conftest.py | 6 +++++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 1c1d128220..f8b02ef824 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -86,7 +86,8 @@ def __init__( self.websocket_tasks: Set[Future] = set() self.named_request_middleware: Dict[str, MiddlewareType] = {} self.named_response_middleware: Dict[str, MiddlewareType] = {} - self._test_manager = None + self._test_client = None + self._asgi_client = None # Register alternative method names self.go_fast = self.run @@ -1032,21 +1033,21 @@ async def handle_request(self, request): @property def test_client(self): - if self._test_manager: - return self._test_manager.test_client - from sanic_testing import TestManager + if self._test_client: + return self._test_client + from sanic_testing.testing import SanicTestClient - manager = TestManager(self) - return manager.test_client + self._test_client = SanicTestClient(self) + return self._test_client @property def asgi_client(self): - if self._test_manager: - return self._test_manager.asgi_client - from sanic_testing import TestManager + if self._asgi_client: + return self._asgi_client + from sanic_testing.testing import SanicASGITestClient - manager = TestManager(self) - return manager.asgi_client + self._asgi_client = SanicASGITestClient(self) + return self._asgi_client # -------------------------------------------------------------------- # # Execution diff --git a/tests/conftest.py b/tests/conftest.py index cad8d75436..46dddf0175 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,8 @@ import pytest +from sanic_testing import TestManager + from sanic import Sanic from sanic.router import RouteExists, Router @@ -129,4 +131,6 @@ def url_param_generator(): @pytest.fixture(scope="function") def app(request): - return Sanic(request.node.name) + app = Sanic(request.node.name) + # TestManager(app) + return app From 76ef641743cf0319248184c0e1e81b047b0c9e8b Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Mon, 25 Jan 2021 02:14:48 +0200 Subject: [PATCH 5/6] Resolve tests --- sanic/app.py | 2 +- tests/conftest.py | 5 +++++ tests/test_asgi.py | 3 +-- tests/test_logging.py | 1 + tests/test_routes.py | 8 ++++---- tests/test_url_for.py | 6 ++++-- tox.ini | 2 +- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index f8b02ef824..eb702b5dba 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1450,7 +1450,7 @@ async def _websocket_handler( pass finally: self.websocket_tasks.remove(fut) - await ws.close() + await ws.close() # -------------------------------------------------------------------- # # ASGI diff --git a/tests/conftest.py b/tests/conftest.py index 46dddf0175..96e513b8ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,11 @@ collect_ignore = ["test_worker.py"] +@pytest.fixture +def caplog(caplog): + yield caplog + + async def _handler(request): """ Dummy placeholder method used for route resolver when creating a new diff --git a/tests/test_asgi.py b/tests/test_asgi.py index 0c728493f9..92bc2fdc90 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -41,8 +41,7 @@ def transport(message_stack, receive, send): @pytest.fixture -# @pytest.mark.asyncio -def protocol(transport, loop): +def protocol(transport): return transport.get_protocol() diff --git a/tests/test_logging.py b/tests/test_logging.py index f5a24f08ef..ea02b94612 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -35,6 +35,7 @@ def test_log(app): logging.basicConfig( format=logging_format, level=logging.DEBUG, stream=log_stream ) + logging.getLogger("asyncio").setLevel(logging.WARNING) log = logging.getLogger() rand_string = str(uuid.uuid4()) diff --git a/tests/test_routes.py b/tests/test_routes.py index 73809518d8..f980411c24 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -480,21 +480,21 @@ async def handler(request, ws): results.append(ws.subprotocol) assert ws.subprotocol is not None - request, response = app.test_client.websocket("/ws", subprotocols=["bar"]) + _, response = SanicTestClient(app).websocket("/ws", subprotocols=["bar"]) assert response.opened is True assert results == ["bar"] - request, response = app.test_client.websocket( + _, response = SanicTestClient(app).websocket( "/ws", subprotocols=["bar", "foo"] ) assert response.opened is True assert results == ["bar", "bar"] - request, response = app.test_client.websocket("/ws", subprotocols=["baz"]) + _, response = SanicTestClient(app).websocket("/ws", subprotocols=["baz"]) assert response.opened is True assert results == ["bar", "bar", None] - request, response = app.test_client.websocket("/ws") + _, response = SanicTestClient(app).websocket("/ws") assert response.opened is True assert results == ["bar", "bar", None, None] diff --git a/tests/test_url_for.py b/tests/test_url_for.py index 2d692f2ef1..9ebe979a12 100644 --- a/tests/test_url_for.py +++ b/tests/test_url_for.py @@ -1,5 +1,7 @@ import asyncio +from sanic_testing.testing import SanicTestClient + from sanic.blueprints import Blueprint @@ -48,14 +50,14 @@ async def test_route3(request, ws): uri = app.url_for("test_bp.test_route") assert uri == "/bp/route" - request, response = app.test_client.websocket(uri) + request, response = SanicTestClient(app).websocket(uri) assert response.opened is True assert event.is_set() event.clear() uri = app.url_for("test_bp.test_route2") assert uri == "/bp/route2" - request, response = app.test_client.websocket(uri) + request, response = SanicTestClient(app).websocket(uri) assert response.opened is True assert event.is_set() diff --git a/tox.ini b/tox.ini index 6ead9ae816..04dec3c927 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ setenv = {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1 {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 deps = - sanic-testing + sanic-testing==0.1.2 coverage==5.3 pytest==5.2.1 pytest-cov From c32e7fd678ab55c9c5ba4ff7723cfcaa90bfe83b Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Mon, 25 Jan 2021 02:39:13 +0200 Subject: [PATCH 6/6] Resolve tests --- sanic/app.py | 4 ++-- tests/test_logo.py | 24 ++++++++---------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index eb702b5dba..fe6d270875 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1035,7 +1035,7 @@ async def handle_request(self, request): def test_client(self): if self._test_client: return self._test_client - from sanic_testing.testing import SanicTestClient + from sanic_testing.testing import SanicTestClient # type: ignore self._test_client = SanicTestClient(self) return self._test_client @@ -1044,7 +1044,7 @@ def test_client(self): def asgi_client(self): if self._asgi_client: return self._asgi_client - from sanic_testing.testing import SanicASGITestClient + from sanic_testing.testing import SanicASGITestClient # type: ignore self._asgi_client = SanicASGITestClient(self) return self._asgi_client diff --git a/tests/test_logo.py b/tests/test_logo.py index c3206513a4..3fff32db30 100644 --- a/tests/test_logo.py +++ b/tests/test_logo.py @@ -6,14 +6,6 @@ from sanic.config import BASE_LOGO -try: - import uvloop # noqa - - ROW = 0 -except BaseException: - ROW = 1 - - def test_logo_base(app, caplog): server = app.create_server( debug=True, return_asyncio_server=True, port=PORT @@ -29,8 +21,8 @@ def test_logo_base(app, caplog): loop.run_until_complete(_server.wait_closed()) app.stop() - assert caplog.record_tuples[ROW][1] == logging.DEBUG - assert caplog.record_tuples[ROW][2] == BASE_LOGO + assert caplog.record_tuples[0][1] == logging.DEBUG + assert caplog.record_tuples[0][2] == BASE_LOGO def test_logo_false(app, caplog): @@ -50,8 +42,8 @@ def test_logo_false(app, caplog): loop.run_until_complete(_server.wait_closed()) app.stop() - banner, port = caplog.record_tuples[ROW][2].rsplit(":", 1) - assert caplog.record_tuples[ROW][1] == logging.INFO + banner, port = caplog.record_tuples[0][2].rsplit(":", 1) + assert caplog.record_tuples[0][1] == logging.INFO assert banner == "Goin' Fast @ http://127.0.0.1" assert int(port) > 0 @@ -73,8 +65,8 @@ def test_logo_true(app, caplog): loop.run_until_complete(_server.wait_closed()) app.stop() - assert caplog.record_tuples[ROW][1] == logging.DEBUG - assert caplog.record_tuples[ROW][2] == BASE_LOGO + assert caplog.record_tuples[0][1] == logging.DEBUG + assert caplog.record_tuples[0][2] == BASE_LOGO def test_logo_custom(app, caplog): @@ -94,5 +86,5 @@ def test_logo_custom(app, caplog): loop.run_until_complete(_server.wait_closed()) app.stop() - assert caplog.record_tuples[ROW][1] == logging.DEBUG - assert caplog.record_tuples[ROW][2] == "My Custom Logo" + assert caplog.record_tuples[0][1] == logging.DEBUG + assert caplog.record_tuples[0][2] == "My Custom Logo"