Skip to content

Commit 4b086cf

Browse files
Replace Flask-Sockets with aiohttp for testing (#1012)
1 parent f150cfd commit 4b086cf

File tree

10 files changed

+97
-74
lines changed

10 files changed

+97
-74
lines changed

.github/workflows/codecov.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ jobs:
2525
run: |
2626
pip install -U pip
2727
pip install .
28-
pip install -r requirements/async.txt
2928
pip install -r requirements/adapter.txt
3029
pip install -r requirements/testing.txt
3130
pip install -r requirements/adapter_testing.txt

.github/workflows/tests.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
uses: actions/setup-python@v4
2020
with:
2121
python-version: ${{ matrix.python-version }}
22-
- name: Install dependencies
22+
- name: Install synchronous dependencies
2323
run: |
2424
pip install -U pip
2525
pip install -r requirements.txt
@@ -28,11 +28,10 @@ jobs:
2828
run: |
2929
pytest tests/slack_bolt/
3030
pytest tests/scenario_tests/
31-
- name: Run tests for Socket Mode adapters
31+
- name: Install adapter dependencies
3232
run: |
3333
pip install -r requirements/adapter.txt
3434
pip install -r requirements/adapter_testing.txt
35-
pytest tests/adapter_tests/socket_mode/
3635
- name: Run tests for HTTP Mode adapters (AWS)
3736
run: |
3837
pytest tests/adapter_tests/aws/
@@ -68,9 +67,15 @@ jobs:
6867
- name: Run tests for HTTP Mode adapters (Tornado)
6968
run: |
7069
pytest tests/adapter_tests/tornado/
71-
- name: Run tests for HTTP Mode adapters (asyncio-based libraries)
70+
- name: Install async dependencies
7271
run: |
7372
pip install -r requirements/async.txt
73+
- name: Run tests for Socket Mode adapters
74+
run: |
75+
# Requires async test dependencies
76+
pytest tests/adapter_tests/socket_mode/
77+
- name: Run tests for HTTP Mode adapters (asyncio-based libraries)
78+
run: |
7479
# Falcon supports Python 3.11 since its v3.1.1
7580
pip install "falcon>=3.1.1,<4"
7681
pytest tests/adapter_tests_async/

requirements/adapter_testing.txt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
# pip install -r requirements/adapter_testing.txt
2-
moto>=3,<4 # For AWS tests
2+
moto>=3,<5 # For AWS tests
33
docker>=5,<6 # Used by moto
44
boddle>=0.2,<0.3 # For Bottle app tests
5-
Flask>=1,<2 # TODO: Flask-Sockets is not yet compatible with Flask 2.x
6-
Werkzeug>=1,<2 # TODO: Flask-Sockets is not yet compatible with Flask 2.x
75
sanic-testing>=0.7; python_version>"3.6"
8-
requests>=2,<3 # For Starlette's TestClient

requirements/async.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
# pip install -r requirements/async.txt
22
aiohttp>=3,<4
3-
websockets>=8,<10; python_version=="3.6"
4-
websockets>=10,<11; python_version>"3.6"
3+
websockets<11

requirements/testing.txt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
# pip install -r requirements/testing.txt
22
-r testing_without_asyncio.txt
3-
4-
pytest-asyncio>=0.16.0; python_version=="3.6"
5-
pytest-asyncio>=0.18.2,<1; python_version>"3.6"
6-
aiohttp>=3,<4
3+
-r async.txt
4+
pytest-asyncio<1;
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# pip install -r requirements/testing_without_asyncio.txt
22
pytest>=6.2.5,<7
3-
pytest-cov>=3,<4
4-
Flask-Sockets>=0.2,<1 # TODO: This module is not yet Flask 2.x compatible
5-
Werkzeug>=1,<2 # TODO: Flask-Sockets is not yet compatible with Flask 2.x
6-
itsdangerous==2.0.1 # TODO: Flask-Sockets is not yet compatible with Flask 2.x
7-
Jinja2==3.0.3 # https://github.com/pallets/flask/issues/4494
3+
pytest-cov>=3,<5
84
black==22.8.0 # Until we drop Python 3.6 support, we have to stay with this version
95
click<=8.0.4 # black is affected by https://github.com/pallets/click/issues/2225
Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,107 @@
1-
import json
1+
import asyncio
22
import logging
33
import threading
44
import time
5-
import requests
6-
from typing import List
75
from unittest import TestCase
6+
from urllib.error import URLError
7+
from urllib.request import urlopen
8+
9+
from aiohttp import WSMsgType, web
810

911
socket_mode_envelopes = [
1012
"""{"envelope_id":"57d6a792-4d35-4d0b-b6aa-3361493e1caf","payload":{"type":"shortcut","token":"xxx","action_ts":"1610198080.300836","team":{"id":"T111","domain":"seratch"},"user":{"id":"U111","username":"seratch","team_id":"T111"},"is_enterprise_install":false,"enterprise":null,"callback_id":"do-something","trigger_id":"111.222.xxx"},"type":"interactive","accepts_response_payload":false}""",
1113
"""{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"token":"xxx","team_id":"T111","team_domain":"xxx","channel_id":"C111","channel_name":"random","user_id":"U111","user_name":"seratch","command":"/hello-socket-mode","text":"","api_app_id":"A111","response_url":"https://hooks.slack.com/commands/T111/111/xxx","trigger_id":"111.222.xxx"},"type":"slash_commands","accepts_response_payload":true}""",
1214
"""{"envelope_id":"08cfc559-d933-402e-a5c1-79e135afaae4","payload":{"token":"xxx","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"c9b466b5-845c-49c6-a371-57ae44359bf1","type":"message","text":"<@W111>","user":"U111","ts":"1610197986.000300","team":"T111","blocks":[{"type":"rich_text","block_id":"1HBPc","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U111"}]}]}],"channel":"C111","event_ts":"1610197986.000300","channel_type":"channel"},"type":"event_callback","event_id":"Ev111","event_time":1610197986,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U111","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-message-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":1,"retry_reason":"timeout"}""",
1315
]
1416

15-
from flask import Flask
16-
from flask_sockets import Sockets
17-
1817

1918
def start_thread_socket_mode_server(test: TestCase, port: int):
20-
def _start_thread_socket_mode_server():
21-
logger = logging.getLogger(__name__)
22-
app: Flask = Flask(__name__)
19+
logger = logging.getLogger(__name__)
20+
state = {}
21+
22+
def reset_server_state():
23+
state.update(
24+
envelopes_to_consume=list(socket_mode_envelopes),
25+
)
26+
27+
test.reset_server_state = reset_server_state
28+
29+
async def health(request: web.Request):
30+
wr = web.Response()
31+
await wr.prepare(request)
32+
wr.set_status(200)
33+
return wr
34+
35+
async def link(request: web.Request):
36+
ws = web.WebSocketResponse()
37+
await ws.prepare(request)
38+
39+
async for msg in ws:
40+
if msg.type != WSMsgType.TEXT:
41+
continue
2342

24-
@app.route("/state")
25-
def state():
26-
return json.dumps({"success": True}), 200, {"ContentType": "application/json"}
43+
if state["envelopes_to_consume"]:
44+
e = state["envelopes_to_consume"].pop(0)
45+
logger.debug(f"Send an envelope: {e}")
46+
await ws.send_str(e)
2747

28-
sockets: Sockets = Sockets(app)
48+
message = msg.data
49+
logger.debug(f"Server received a message: {message}")
2950

30-
envelopes_to_consume: List[str] = list(socket_mode_envelopes)
51+
await ws.send_str(message)
3152

32-
@sockets.route("/link")
33-
def link(ws):
34-
while not ws.closed:
35-
message = ws.read_message()
36-
if message is not None:
37-
if len(envelopes_to_consume) > 0:
38-
e = envelopes_to_consume.pop(0)
39-
logger.debug(f"Send an envelope: {e}")
40-
ws.send(e)
53+
return ws
4154

42-
logger.debug(f"Server received a message: {message}")
43-
ws.send(message)
55+
app = web.Application()
56+
app.add_routes(
57+
[
58+
web.get("/link", link),
59+
web.get("/health", health),
60+
]
61+
)
62+
runner = web.AppRunner(app)
4463

45-
from gevent import pywsgi
46-
from geventwebsocket.handler import WebSocketHandler
64+
def run_server():
65+
reset_server_state()
4766

48-
server = pywsgi.WSGIServer(("", port), app, handler_class=WebSocketHandler)
49-
test.server = server
50-
server.serve_forever(stop_timeout=1)
67+
test.loop = asyncio.new_event_loop()
68+
asyncio.set_event_loop(test.loop)
69+
test.loop.run_until_complete(runner.setup())
70+
site = web.TCPSite(runner, "127.0.0.1", port, reuse_port=True)
71+
test.loop.run_until_complete(site.start())
5172

52-
return _start_thread_socket_mode_server
73+
# run until it's stopped from the main thread
74+
test.loop.run_forever()
75+
76+
test.loop.run_until_complete(runner.cleanup())
77+
test.loop.close()
78+
79+
return run_server
5380

5481

5582
def start_socket_mode_server(test, port: int):
5683
test.sm_thread = threading.Thread(target=start_thread_socket_mode_server(test, port))
5784
test.sm_thread.daemon = True
5885
test.sm_thread.start()
59-
wait_for_socket_mode_server(port, 4) # wait for the server
86+
wait_for_socket_mode_server(port, 4)
6087

6188

62-
def wait_for_socket_mode_server(port: int, secs: int):
89+
def wait_for_socket_mode_server(port: int, timeout: int):
6390
start_time = time.time()
64-
while (time.time() - start_time) < secs:
65-
response = requests.get(url=f"http://localhost:{port}/state")
66-
if response.ok:
67-
break
68-
time.sleep(0.01)
69-
70-
71-
def stop_socket_mode_server(test):
72-
test.server.stop()
73-
test.server.close()
74-
75-
76-
async def stop_socket_mode_server_async(test: TestCase):
77-
test.server.stop()
78-
test.server.close()
91+
while (time.time() - start_time) < timeout:
92+
try:
93+
urlopen(f"http://127.0.0.1:{port}/health")
94+
return
95+
except URLError:
96+
time.sleep(0.01)
97+
98+
99+
def stop_socket_mode_server(test: TestCase):
100+
# An event loop runs in a thread and executes all callbacks and Tasks in
101+
# its thread. While a Task is running in the event loop, no other Tasks
102+
# can run in the same thread. When a Task executes an await expression, the
103+
# running Task gets suspended, and the event loop executes the next Task.
104+
# To schedule a callback from another OS thread, the loop.call_soon_threadsafe() method should be used.
105+
# https://docs.python.org/3/library/asyncio-dev.html#asyncio-multithreading
106+
test.loop.call_soon_threadsafe(test.loop.stop)
107+
test.sm_thread.join(timeout=5)

tests/adapter_tests_async/socket_mode/test_async_aiohttp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from tests.utils import remove_os_env_temporarily, restore_os_env, get_event_loop
1313
from ...adapter_tests.socket_mode.mock_socket_mode_server import (
1414
start_socket_mode_server,
15-
stop_socket_mode_server_async,
15+
stop_socket_mode_server,
1616
)
1717

1818

@@ -71,4 +71,4 @@ async def command_handler(ack):
7171
assert result["command"] is True
7272
finally:
7373
await handler.client.close()
74-
await stop_socket_mode_server_async(self)
74+
stop_socket_mode_server(self)

tests/adapter_tests_async/socket_mode/test_async_lazy_listeners.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from tests.utils import remove_os_env_temporarily, restore_os_env, get_event_loop
1313
from ...adapter_tests.socket_mode.mock_socket_mode_server import (
1414
start_socket_mode_server,
15-
stop_socket_mode_server_async,
15+
stop_socket_mode_server,
1616
)
1717

1818

@@ -80,4 +80,4 @@ async def lazy_func(body):
8080

8181
finally:
8282
await handler.client.close()
83-
await stop_socket_mode_server_async(self)
83+
stop_socket_mode_server(self)

tests/adapter_tests_async/socket_mode/test_async_websockets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from tests.utils import remove_os_env_temporarily, restore_os_env, get_event_loop
1313
from ...adapter_tests.socket_mode.mock_socket_mode_server import (
1414
start_socket_mode_server,
15-
stop_socket_mode_server_async,
15+
stop_socket_mode_server,
1616
)
1717

1818

@@ -71,4 +71,4 @@ async def command_handler(ack):
7171
assert result["command"] is True
7272
finally:
7373
await handler.client.close()
74-
await stop_socket_mode_server_async(self)
74+
stop_socket_mode_server(self)

0 commit comments

Comments
 (0)