diff --git a/pyproject.toml b/pyproject.toml index 712f19340..8a74a0d93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,8 @@ filterwarnings = [ "ignore:slack.* package is deprecated. Please use slack_sdk.* package instead.*:UserWarning", ] asyncio_mode = "auto" +timeout = 180 +timeout_method = "thread" [tool.mypy] diff --git a/requirements/testing.txt b/requirements/testing.txt index c7d5cb680..c024e4169 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -15,6 +15,10 @@ pytest>=9.1.1,<10; python_version >= "3.10" # Note: for async. pytest-asyncio<2 +# pytest-timeout +# Note: per-test timeout so a hung test fails fast with a thread dump instead of stalling CI. +pytest-timeout>=2.2,<3 + # pytest-cov # Note: pytest-cov 7.1+ requires Python >=3.9; cap older interpreters below it. pytest-cov>=4,<7.1.0; python_version < "3.9" diff --git a/tests/mock_web_api_server/__init__.py b/tests/mock_web_api_server/__init__.py index afb5761c4..0c582ae40 100644 --- a/tests/mock_web_api_server/__init__.py +++ b/tests/mock_web_api_server/__init__.py @@ -15,7 +15,10 @@ def setup_mock_web_api_server(test: TestCase, handler: Type[SimpleHTTPRequestHan test.received_requests = ReceivedRequests(Queue()) test.thread = MockServerThread(queue=test.received_requests.queue, test=test, handler=handler, port=port) test.thread.start() - test.server_started.wait() + if not test.server_started.wait(timeout=5): + raise RuntimeError( + f"Mock web API server failed to start on port {test.thread.port} within 5s (port already in use?)" + ) def cleanup_mock_web_api_server(test: TestCase): @@ -56,11 +59,14 @@ def setup_mock_web_api_server_async(test: TestCase, handler: Type[SimpleHTTPRequ test.received_requests = ReceivedRequests(asyncio.Queue()) test.thread = MockServerThread(queue=test.received_requests.queue, test=test, handler=handler, port=port) test.thread.start() - test.server_started.wait() + if not test.server_started.wait(timeout=5): + raise RuntimeError( + f"Mock web API server failed to start on port {test.thread.port} within 5s (port already in use?)" + ) def cleanup_mock_web_api_server_async(test: TestCase): - test.thread.stop_unsafe() + test.thread.stop() test.thread = None diff --git a/tests/mock_web_api_server/mock_server_thread.py b/tests/mock_web_api_server/mock_server_thread.py index 0888cc4ea..6022bb14b 100644 --- a/tests/mock_web_api_server/mock_server_thread.py +++ b/tests/mock_web_api_server/mock_server_thread.py @@ -1,25 +1,33 @@ -from asyncio import Queue import asyncio -from http.server import HTTPServer, SimpleHTTPRequestHandler +import logging import threading -from typing import Type, Union +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from queue import Queue +from typing import Optional, Type, Union from unittest import TestCase +logger = logging.getLogger(__name__) + class MockServerThread(threading.Thread): def __init__( - self, queue: Union[Queue, asyncio.Queue], test: TestCase, handler: Type[SimpleHTTPRequestHandler], port: int = 8888 + self, + test: TestCase, + handler: Type[SimpleHTTPRequestHandler], + queue: Optional[Union[Queue, asyncio.Queue]] = None, + port: int = 8888, ): - threading.Thread.__init__(self) + threading.Thread.__init__(self, daemon=True) self.handler = handler self.test = test self.queue = queue self.port = port def run(self): - self.server = HTTPServer(("localhost", self.port), self.handler) - self.server.queue = self.queue - self.test.server_url = f"http://localhost:{str(self.port)}" + self.server = ThreadingHTTPServer(("localhost", self.port), self.handler) + if self.queue is not None: + self.server.queue = self.queue + self.test.server_url = f"http://localhost:{self.port}" self.test.host, self.test.port = self.server.socket.getsockname() self.test.server_started.set() # threading.Event() @@ -30,12 +38,9 @@ def run(self): self.server.server_close() def stop(self): - with self.server.queue.mutex: - del self.server.queue self.server.shutdown() - self.join() - - def stop_unsafe(self): - del self.server.queue - self.server.shutdown() - self.join() + self.join(timeout=5) + if self.is_alive(): + logger.warning(f"Mock web API server thread on port {self.port} did not stop within 5s") + if getattr(self.server, "queue", None) is not None: + del self.server.queue diff --git a/tests/rtm/mock_web_api_server.py b/tests/rtm/mock_web_api_server.py index 9d03ac837..2cb279887 100644 --- a/tests/rtm/mock_web_api_server.py +++ b/tests/rtm/mock_web_api_server.py @@ -2,10 +2,11 @@ import logging import threading from http import HTTPStatus -from http.server import HTTPServer, SimpleHTTPRequestHandler -from typing import Type +from http.server import SimpleHTTPRequestHandler from unittest import TestCase +from tests.mock_web_api_server.mock_server_thread import MockServerThread + class MockHandler(SimpleHTTPRequestHandler): protocol_version = "HTTP/1.1" @@ -62,34 +63,14 @@ def do_POST(self): self._handle() -class MockServerThread(threading.Thread): - def __init__(self, test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler): - threading.Thread.__init__(self) - self.handler = handler - self.test = test - - def run(self): - self.server = HTTPServer(("localhost", 8888), self.handler) - self.test.server_url = "http://localhost:8888" - self.test.host, self.test.port = self.server.socket.getsockname() - self.test.server_started.set() # threading.Event() - - self.test = None - try: - self.server.serve_forever(0.05) - finally: - self.server.server_close() - - def stop(self): - self.server.shutdown() - self.join() - - def setup_mock_web_api_server(test: TestCase): test.server_started = threading.Event() - test.thread = MockServerThread(test) + test.thread = MockServerThread(test=test, handler=MockHandler) test.thread.start() - test.server_started.wait() + if not test.server_started.wait(timeout=5): + raise RuntimeError( + f"Mock web API server failed to start on port {test.thread.port} within 5s (port already in use?)" + ) def cleanup_mock_web_api_server(test: TestCase): diff --git a/tests/slack_sdk/socket_mode/test_interactions_builtin.py b/tests/slack_sdk/socket_mode/test_interactions_builtin.py index 4ff576fcf..3f53e4cd1 100644 --- a/tests/slack_sdk/socket_mode/test_interactions_builtin.py +++ b/tests/slack_sdk/socket_mode/test_interactions_builtin.py @@ -112,14 +112,12 @@ def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketMod self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests)) finally: - pass - # client.close() + client.close() self.logger.info(f"Passed with buffer size: {buffer_size}") finally: # Restore the default value sys.setrecursionlimit(default_recursion_limit) - client.close() self.logger.info(f"Passed with buffer size: {buffer_size_list}") diff --git a/tests/slack_sdk/web/mock_web_api_handler.py b/tests/slack_sdk/web/mock_web_api_handler.py index 12a487e05..5416f4d31 100644 --- a/tests/slack_sdk/web/mock_web_api_handler.py +++ b/tests/slack_sdk/web/mock_web_api_handler.py @@ -1,14 +1,9 @@ -import asyncio import json import logging -from queue import Queue import re -import threading import time from http import HTTPStatus -from http.server import HTTPServer, SimpleHTTPRequestHandler -from typing import Type, Union -from unittest import TestCase +from http.server import SimpleHTTPRequestHandler from urllib.parse import urlparse, parse_qs @@ -258,37 +253,3 @@ def do_GET(self): def do_POST(self): self._handle() - - -class MockServerThread(threading.Thread): - def __init__( - self, queue: Union[Queue, asyncio.Queue], test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler - ): - threading.Thread.__init__(self) - self.handler = handler - self.test = test - self.queue = queue - - def run(self): - self.server = HTTPServer(("localhost", 8888), self.handler) - self.server.queue = self.queue - self.test.server_url = "http://localhost:8888" - self.test.host, self.test.port = self.server.socket.getsockname() - self.test.server_started.set() # threading.Event() - - self.test = None - try: - self.server.serve_forever(0.05) - finally: - self.server.server_close() - - def stop(self): - with self.server.queue.mutex: - del self.server.queue - self.server.shutdown() - self.join() - - def stop_unsafe(self): - del self.server.queue - self.server.shutdown() - self.join() diff --git a/tests/slack_sdk_async/socket_mode/test_interactions_websockets.py b/tests/slack_sdk_async/socket_mode/test_interactions_websockets.py index 5b408dbcf..4209195e8 100644 --- a/tests/slack_sdk_async/socket_mode/test_interactions_websockets.py +++ b/tests/slack_sdk_async/socket_mode/test_interactions_websockets.py @@ -17,6 +17,7 @@ start_socket_mode_server, socket_mode_envelopes, socket_mode_hello_message, + stop_socket_mode_server, ) from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async @@ -35,7 +36,10 @@ def setUp(self): start_socket_mode_server(self, 3001) def tearDown(self): - cleanup_mock_web_api_server_async(self) + try: + cleanup_mock_web_api_server_async(self) + finally: + stop_socket_mode_server(self) @async_test async def test_interactions(self):