Skip to content

Commit fe598cd

Browse files
test: improve the mock server experience (#1602)
1 parent a5b9db6 commit fe598cd

File tree

64 files changed

+663
-1784
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+663
-1784
lines changed

.github/workflows/ci-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ jobs:
2424
- '3.6'
2525
- 'pypy3.10'
2626
env:
27-
PYTHON_SLACK_SDK_MOCK_SERVER_MODE: 'threading'
2827
CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: '1'
2928
CI_UNSTABLE_TESTS_SKIP_ENABLED: '1'
3029
FORCE_COLOR: '1'
@@ -59,3 +58,4 @@ jobs:
5958
token: ${{ secrets.CODECOV_TOKEN }}
6059
# python setup.py validate generates the coverage file
6160
files: ./coverage.xml
61+

tests/helpers.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,5 @@ def restore_os_env(old_env: dict) -> None:
2626
os.environ.update(old_env)
2727

2828

29-
def get_mock_server_mode() -> str:
30-
"""Returns a str representing the mode.
31-
32-
:return: threading/multiprocessing
33-
"""
34-
mode = os.environ.get("PYTHON_SLACK_SDK_MOCK_SERVER_MODE")
35-
if mode is None:
36-
# We used to use "multiprocessing"" for macOS until Big Sur 11.1
37-
# Since 11.1, the "multiprocessing" mode started failing a lot...
38-
# Therefore, we switched the default mode back to "threading".
39-
return "threading"
40-
else:
41-
return mode
42-
43-
4429
def is_ci_unstable_test_skip_enabled() -> bool:
4530
return os.environ.get("CI_UNSTABLE_TESTS_SKIP_ENABLED") == "1"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import asyncio
2+
from http.server import SimpleHTTPRequestHandler
3+
from queue import Queue
4+
import threading
5+
import time
6+
from typing import Type
7+
from unittest import TestCase
8+
9+
from tests.mock_web_api_server.received_requests import ReceivedRequests
10+
from tests.mock_web_api_server.mock_server_thread import MockServerThread
11+
12+
13+
def setup_mock_web_api_server(test: TestCase, handler: Type[SimpleHTTPRequestHandler], port: int = 8888):
14+
test.server_started = threading.Event()
15+
test.received_requests = ReceivedRequests(Queue())
16+
test.thread = MockServerThread(queue=test.received_requests.queue, test=test, handler=handler, port=port)
17+
test.thread.start()
18+
test.server_started.wait()
19+
20+
21+
def cleanup_mock_web_api_server(test: TestCase):
22+
test.thread.stop()
23+
test.thread = None
24+
25+
26+
def assert_received_request_count(test: TestCase, path: str, min_count: int, timeout: float = 1):
27+
start_time = time.time()
28+
error = None
29+
while time.time() - start_time < timeout:
30+
try:
31+
received_count = test.received_requests.get(path, 0)
32+
assert (
33+
received_count == min_count
34+
), f"Expected {min_count} '{path}' {'requests' if min_count > 1 else 'request'}, but got {received_count}!"
35+
return
36+
except Exception as e:
37+
error = e
38+
# waiting for some requests to be received
39+
time.sleep(0.05)
40+
41+
if error is not None:
42+
raise error
43+
44+
45+
def assert_auth_test_count(test: TestCase, expected_count: int):
46+
assert_received_request_count(test, "/auth.test", expected_count, 0.5)
47+
48+
49+
#########
50+
# async #
51+
#########
52+
53+
54+
def setup_mock_web_api_server_async(test: TestCase, handler: Type[SimpleHTTPRequestHandler], port: int = 8888):
55+
test.server_started = threading.Event()
56+
test.received_requests = ReceivedRequests(asyncio.Queue())
57+
test.thread = MockServerThread(queue=test.received_requests.queue, test=test, handler=handler, port=port)
58+
test.thread.start()
59+
test.server_started.wait()
60+
61+
62+
def cleanup_mock_web_api_server_async(test: TestCase):
63+
test.thread.stop_unsafe()
64+
test.thread = None
65+
66+
67+
async def assert_received_request_count_async(test: TestCase, path: str, min_count: int, timeout: float = 1):
68+
start_time = time.time()
69+
error = None
70+
while time.time() - start_time < timeout:
71+
try:
72+
received_count = await test.received_requests.get_async(path, 0)
73+
assert (
74+
received_count == min_count
75+
), f"Expected {min_count} '{path}' {'requests' if min_count > 1 else 'request'}, but got {received_count}!"
76+
return
77+
except Exception as e:
78+
error = e
79+
# waiting for mock_received_requests updates
80+
await asyncio.sleep(0.05)
81+
82+
if error is not None:
83+
raise error
84+
85+
86+
async def assert_auth_test_count_async(test: TestCase, expected_count: int):
87+
await assert_received_request_count_async(test, "/auth.test", expected_count, 0.5)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from asyncio import Queue
2+
import asyncio
3+
from http.server import HTTPServer, SimpleHTTPRequestHandler
4+
import threading
5+
from typing import Type, Union
6+
from unittest import TestCase
7+
8+
9+
class MockServerThread(threading.Thread):
10+
def __init__(
11+
self, queue: Union[Queue, asyncio.Queue], test: TestCase, handler: Type[SimpleHTTPRequestHandler], port: int = 8888
12+
):
13+
threading.Thread.__init__(self)
14+
self.handler = handler
15+
self.test = test
16+
self.queue = queue
17+
self.port = port
18+
19+
def run(self):
20+
self.server = HTTPServer(("localhost", self.port), self.handler)
21+
self.server.queue = self.queue
22+
self.test.server_url = f"http://localhost:{str(self.port)}"
23+
self.test.host, self.test.port = self.server.socket.getsockname()
24+
self.test.server_started.set() # threading.Event()
25+
26+
self.test = None
27+
try:
28+
self.server.serve_forever(0.05)
29+
finally:
30+
self.server.server_close()
31+
32+
def stop(self):
33+
with self.server.queue.mutex:
34+
del self.server.queue
35+
self.server.shutdown()
36+
self.join()
37+
38+
def stop_unsafe(self):
39+
del self.server.queue
40+
self.server.shutdown()
41+
self.join()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import asyncio
2+
from queue import Queue
3+
from typing import Optional, Union
4+
5+
6+
class ReceivedRequests:
7+
def __init__(self, queue: Union[Queue, asyncio.Queue]):
8+
self.queue = queue
9+
self.received_requests: dict = {}
10+
11+
def get(self, key: str, default: Optional[int] = None) -> Optional[int]:
12+
while not self.queue.empty():
13+
path = self.queue.get()
14+
self.received_requests[path] = self.received_requests.get(path, 0) + 1
15+
return self.received_requests.get(key, default)
16+
17+
async def get_async(self, key: str, default: Optional[int] = None) -> Optional[int]:
18+
while not self.queue.empty():
19+
path = await self.queue.get()
20+
self.received_requests[path] = self.received_requests.get(path, 0) + 1
21+
return self.received_requests.get(key, default)
Lines changed: 3 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
import json
22
import logging
33
import re
4-
import sys
5-
import threading
64
import time
75
from http import HTTPStatus
8-
from http.server import HTTPServer, SimpleHTTPRequestHandler
9-
from multiprocessing.context import Process
10-
from typing import Type
11-
from unittest import TestCase
12-
from urllib.request import Request, urlopen
13-
14-
from tests.helpers import get_mock_server_mode
6+
from http.server import SimpleHTTPRequestHandler
157

168

179
class MockHandler(SimpleHTTPRequestHandler):
@@ -32,6 +24,8 @@ def set_common_headers(self):
3224
self.end_headers()
3325

3426
def do_GET(self):
27+
# put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited
28+
self.server.queue.put_nowait(self.path)
3529
if self.path == "/received_requests.json":
3630
self.send_response(200)
3731
self.set_common_headers()
@@ -97,123 +91,3 @@ def do_GET(self):
9791
except Exception as e:
9892
self.logger.error(str(e), exc_info=True)
9993
raise
100-
101-
102-
class MockServerProcessTarget:
103-
def __init__(self, handler: Type[SimpleHTTPRequestHandler] = MockHandler):
104-
self.handler = handler
105-
106-
def run(self):
107-
self.handler.received_requests = {}
108-
self.server = HTTPServer(("localhost", 8888), self.handler)
109-
try:
110-
self.server.serve_forever(0.05)
111-
finally:
112-
self.server.server_close()
113-
114-
def stop(self):
115-
self.handler.received_requests = {}
116-
self.server.shutdown()
117-
self.join()
118-
119-
120-
class MonitorThread(threading.Thread):
121-
def __init__(self, test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler):
122-
threading.Thread.__init__(self, daemon=True)
123-
self.handler = handler
124-
self.test = test
125-
self.test.mock_received_requests = None
126-
self.is_running = True
127-
128-
def run(self) -> None:
129-
while self.is_running:
130-
try:
131-
req = Request(f"{self.test.server_url}/received_requests.json")
132-
resp = urlopen(req, timeout=1)
133-
self.test.mock_received_requests = json.loads(resp.read().decode("utf-8"))
134-
except Exception as e:
135-
# skip logging for the initial request
136-
if self.test.mock_received_requests is not None:
137-
logging.getLogger(__name__).exception(e)
138-
time.sleep(0.01)
139-
140-
def stop(self):
141-
self.is_running = False
142-
self.join()
143-
144-
145-
class MockServerThread(threading.Thread):
146-
def __init__(self, test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler):
147-
threading.Thread.__init__(self)
148-
self.handler = handler
149-
self.test = test
150-
151-
def run(self):
152-
self.server = HTTPServer(("localhost", 8888), self.handler)
153-
self.test.server_url = "http://localhost:8888"
154-
self.test.host, self.test.port = self.server.socket.getsockname()
155-
self.test.server_started.set() # threading.Event()
156-
157-
self.test = None
158-
try:
159-
self.server.serve_forever()
160-
finally:
161-
self.server.server_close()
162-
163-
def stop(self):
164-
self.server.shutdown()
165-
self.join()
166-
167-
168-
def setup_mock_web_api_server(test: TestCase):
169-
if get_mock_server_mode() == "threading":
170-
test.server_started = threading.Event()
171-
test.thread = MockServerThread(test)
172-
test.thread.start()
173-
test.server_started.wait()
174-
else:
175-
# start a mock server as another process
176-
target = MockServerProcessTarget()
177-
test.server_url = "http://localhost:8888"
178-
test.host, test.port = "localhost", 8888
179-
test.process = Process(target=target.run, daemon=True)
180-
test.process.start()
181-
182-
time.sleep(0.1)
183-
184-
# start a thread in the current process
185-
# this thread fetches mock_received_requests from the remote process
186-
test.monitor_thread = MonitorThread(test)
187-
test.monitor_thread.start()
188-
count = 0
189-
# wait until the first successful data retrieval
190-
while test.mock_received_requests is None:
191-
time.sleep(0.01)
192-
count += 1
193-
if count >= 100:
194-
raise Exception("The mock server is not yet running!")
195-
196-
197-
def cleanup_mock_web_api_server(test: TestCase):
198-
if get_mock_server_mode() == "threading":
199-
test.thread.stop()
200-
test.thread = None
201-
else:
202-
# stop the thread to fetch mock_received_requests from the remote process
203-
test.monitor_thread.stop()
204-
205-
retry_count = 0
206-
# terminate the process
207-
while test.process.is_alive():
208-
test.process.terminate()
209-
time.sleep(0.01)
210-
retry_count += 1
211-
if retry_count >= 100:
212-
raise Exception("Failed to stop the mock server!")
213-
214-
# Python 3.6 does not have this method
215-
if sys.version_info.major == 3 and sys.version_info.minor > 6:
216-
# cleanup the process's resources
217-
test.process.close()
218-
219-
test.process = None

tests/slack_sdk/audit_logs/test_client.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22
from urllib.error import URLError
33

44
from slack_sdk.audit_logs import AuditLogsClient, AuditLogsResponse
5-
from tests.slack_sdk.audit_logs.mock_web_api_server import (
6-
cleanup_mock_web_api_server,
7-
setup_mock_web_api_server,
8-
)
5+
from tests.slack_sdk.audit_logs.mock_web_api_handler import MockHandler
6+
from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server
97

108

119
class TestAuditLogsClient(unittest.TestCase):
1210
def setUp(self):
1311
self.client = AuditLogsClient(token="xoxp-", base_url="http://localhost:8888/")
14-
setup_mock_web_api_server(self)
12+
setup_mock_web_api_server(self, MockHandler)
1513

1614
def tearDown(self):
1715
cleanup_mock_web_api_server(self)

tests/slack_sdk/audit_logs/test_client_http_retry.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
from slack_sdk.audit_logs import AuditLogsClient
44
from slack_sdk.http_retry import RateLimitErrorRetryHandler
5-
from tests.slack_sdk.audit_logs.mock_web_api_server import (
6-
cleanup_mock_web_api_server,
7-
setup_mock_web_api_server,
8-
)
5+
from tests.slack_sdk.audit_logs.mock_web_api_handler import MockHandler
6+
from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server
97
from ..my_retry_handler import MyRetryHandler
108

119

1210
class TestAuditLogsClient_HttpRetries(unittest.TestCase):
1311
def setUp(self):
14-
setup_mock_web_api_server(self)
12+
setup_mock_web_api_server(self, MockHandler)
1513

1614
def tearDown(self):
1715
cleanup_mock_web_api_server(self)

tests/slack_sdk/oauth/token_rotation/test_token_rotator.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
from slack_sdk.oauth.installation_store import Installation
55
from slack_sdk.oauth.token_rotation import TokenRotator
66
from slack_sdk.web import WebClient
7-
from tests.slack_sdk.web.mock_web_api_server import (
8-
setup_mock_web_api_server,
9-
cleanup_mock_web_api_server,
10-
)
7+
from tests.slack_sdk.web.mock_web_api_handler import MockHandler
8+
from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server
119

1210

1311
class TestTokenRotator(unittest.TestCase):
1412
def setUp(self):
15-
setup_mock_web_api_server(self)
13+
setup_mock_web_api_server(self, MockHandler)
1614
self.token_rotator = TokenRotator(
1715
client=WebClient(base_url="http://localhost:8888", token=None),
1816
client_id="111.222",

0 commit comments

Comments
 (0)