Skip to content

Commit aa9529c

Browse files
test: improve the mock serve experience
1 parent a5b9db6 commit aa9529c

File tree

64 files changed

+704
-1826
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

+704
-1826
lines changed

.github/workflows/ci-build.yml

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: CI Build
33

44
on:
55
push:
6-
branches: [ main ]
6+
branches: [main]
77
pull_request:
88

99
jobs:
@@ -15,47 +15,46 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
python-version:
18-
- '3.12'
19-
- '3.11'
20-
- '3.10'
21-
- '3.9'
22-
- '3.8'
23-
- '3.7'
24-
- '3.6'
25-
- 'pypy3.10'
18+
- "3.12"
19+
- "3.11"
20+
- "3.10"
21+
- "3.9"
22+
- "3.8"
23+
- "3.7"
24+
- "3.6"
25+
- "pypy3.10"
2626
env:
27-
PYTHON_SLACK_SDK_MOCK_SERVER_MODE: 'threading'
28-
CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: '1'
29-
CI_UNSTABLE_TESTS_SKIP_ENABLED: '1'
30-
FORCE_COLOR: '1'
27+
CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: "1"
28+
CI_UNSTABLE_TESTS_SKIP_ENABLED: "1"
29+
FORCE_COLOR: "1"
3130
steps:
32-
- uses: actions/checkout@v4
33-
- name: Set up Python ${{ matrix.python-version }}
34-
uses: actions/setup-python@v5
35-
with:
36-
python-version: ${{ matrix.python-version }}
37-
cache: pip
38-
- name: Install dependencies
39-
run: |
40-
pip install -U pip setuptools wheel
41-
pip install -r requirements/testing.txt
42-
pip install -r requirements/optional.txt
43-
- name: Run codegen
44-
run: |
45-
python setup.py codegen
46-
- name: Run validation (black/flake8/pytest)
47-
run: |
48-
python setup.py validate
49-
- name: Run tests for SQLAlchemy v1.4 (backward-compatibility)
50-
run: |
51-
# Install v1.4 for testing
52-
pip install "SQLAlchemy>=1.4,<2"
53-
python setup.py unit_tests --test-target tests/slack_sdk/oauth/installation_store/test_sqlalchemy.py && \
54-
python setup.py unit_tests --test-target tests/slack_sdk/oauth/state_store/test_sqlalchemy.py
55-
- name: Run codecov (only 3.9)
56-
if: startsWith(matrix.python-version, '3.9')
57-
uses: codecov/codecov-action@v4
58-
with:
59-
token: ${{ secrets.CODECOV_TOKEN }}
60-
# python setup.py validate generates the coverage file
61-
files: ./coverage.xml
31+
- uses: actions/checkout@v4
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v5
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
cache: pip
37+
- name: Install dependencies
38+
run: |
39+
pip install -U pip setuptools wheel
40+
pip install -r requirements/testing.txt
41+
pip install -r requirements/optional.txt
42+
- name: Run codegen
43+
run: |
44+
python setup.py codegen
45+
- name: Run validation (black/flake8/pytest)
46+
run: |
47+
python setup.py validate
48+
- name: Run tests for SQLAlchemy v1.4 (backward-compatibility)
49+
run: |
50+
# Install v1.4 for testing
51+
pip install "SQLAlchemy>=1.4,<2"
52+
python setup.py unit_tests --test-target tests/slack_sdk/oauth/installation_store/test_sqlalchemy.py && \
53+
python setup.py unit_tests --test-target tests/slack_sdk/oauth/state_store/test_sqlalchemy.py
54+
- name: Run codecov (only 3.9)
55+
if: startsWith(matrix.python-version, '3.9')
56+
uses: codecov/codecov-action@v4
57+
with:
58+
token: ${{ secrets.CODECOV_TOKEN }}
59+
# python setup.py validate generates the coverage file
60+
files: ./coverage.xml

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)

0 commit comments

Comments
 (0)