Skip to content

Commit ebdfe45

Browse files
authored
Replace Flask-Sockets with aiohttp for testing (#1445)
Flask-Sockets is deprecated and it won't receive any fixes in the future, so it seems reasonable to replace it with some up-to-date library. aiohttp has been chosen since it was already presented as an optional requirement. These changes also: * move format checking for tests from run_validation.sh to setup.py * remove duplicated dependency installations from setup.py * add a codegen step on CI * force colors on CI
1 parent 56f7127 commit ebdfe45

File tree

9 files changed

+111
-98
lines changed

9 files changed

+111
-98
lines changed

.github/workflows/ci-build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
env:
2727
PYTHON_SLACK_SDK_MOCK_SERVER_MODE: 'threading'
2828
CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: '1'
29+
FORCE_COLOR: '1'
2930
steps:
3031
- uses: actions/checkout@v3
3132
- name: Set up Python ${{ matrix.python-version }}
@@ -35,9 +36,12 @@ jobs:
3536
cache: pip
3637
- name: Install dependencies
3738
run: |
38-
pip install -U pip wheel
39+
pip install -U pip setuptools wheel
3940
pip install -r requirements/testing.txt
4041
pip install -r requirements/optional.txt
42+
- name: Run codegen
43+
run: |
44+
python setup.py codegen
4145
- name: Run validation (black/flake8/pytest)
4246
run: |
4347
python setup.py validate

requirements/testing.txt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
# pip install -r requirements/testing.txt
2+
aiohttp<4 # used for a WebSocket server mock
23
pytest>=7.0.1,<8
34
pytest-asyncio<1 # for async
4-
Flask-Sockets>=0.2,<1
5-
Flask>=1,<2 # TODO: Flask-Sockets is not yet compatible with Flask 2.x
6-
Werkzeug<2 # TODO: Flask-Sockets is not yet compatible with Flask 2.x
7-
itsdangerous==1.1.0 # TODO: Flask-Sockets is not yet compatible with Flask 2.x
8-
Jinja2==3.0.3 # https://github.com/pallets/flask/issues/4494
95
pytest-cov>=2,<3
106
# while flake8 5.x have issues with Python 3.12, flake8 6.x requires Python >= 3.8.1,
117
# so 5.x should be kept in order to stay compatible with Python 3.6/3.7

scripts/run_validation.sh

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@ set -e
55

66
script_dir=`dirname $0`
77
cd ${script_dir}/..
8-
pip install -U pip
8+
9+
pip install -U pip setuptools wheel
910
pip install -r requirements/testing.txt \
1011
-r requirements/optional.txt
1112

12-
black --check tests/ integration_tests/
13-
# TODO: resolve linting errors for tests
14-
# flake8 tests/ integration_tests/
15-
1613
python setup.py codegen
1714
python setup.py validate

setup.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@
1111

1212
here = os.path.abspath(os.path.dirname(__file__))
1313

14-
codegen_dependencies = [
15-
# Don't change this version without running CI builds;
16-
# The latest version may not be available for older Python runtime
17-
"black==22.10.0",
18-
]
1914

2015
class BaseCommand(Command):
2116
user_options = []
@@ -41,11 +36,6 @@ def _run(self, s, command):
4136

4237
class CodegenCommand(BaseCommand):
4338
def run(self):
44-
self._run(
45-
"Installing required dependencies ...",
46-
[sys.executable, "-m", "pip", "install"] + codegen_dependencies,
47-
)
48-
4939
header = (
5040
"# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
5141
"#\n"
@@ -149,16 +139,28 @@ def initialize_options(self):
149139
self.test_target = ""
150140

151141
def run(self):
152-
self._run(
153-
"Installing test dependencies ...",
154-
[sys.executable, "-m", "pip", "install", "-r", "requirements/testing.txt"],
155-
)
142+
def run_black(target, target_name=None):
143+
self._run(
144+
f"Running black for {target_name or target} ...",
145+
[sys.executable, "-m", "black", "--check", f"{here}/{target}"],
146+
)
156147

157-
self._run("Running black for legacy packages ...", [sys.executable, "-m", "black", f"{here}/slack"])
158-
self._run("Running black for slack_sdk package ...", [sys.executable, "-m", "black", f"{here}/slack_sdk"])
148+
run_black("slack", "legacy packages")
149+
run_black("slack_sdk", "slack_sdk package")
150+
run_black("tests")
151+
run_black("integration_tests")
159152

160-
self._run("Running flake8 for legacy packages ...", [sys.executable, "-m", "flake8", f"{here}/slack"])
161-
self._run("Running flake8 for slack_sdk package ...", [sys.executable, "-m", "flake8", f"{here}/slack_sdk"])
153+
def run_flake8(target, target_name=None):
154+
self._run(
155+
f"Running flake8 for {target_name or target} ...",
156+
[sys.executable, "-m", "flake8", f"{here}/{target}"],
157+
)
158+
159+
run_flake8("slack", "legacy packages")
160+
run_flake8("slack_sdk", "slack_sdk package")
161+
# TODO: resolve linting errors for tests
162+
# run_flake8("tests")
163+
# run_flake8("integration_tests")
162164

163165
target = self.test_target.replace("tests/", "", 1)
164166
self._run(
@@ -175,7 +177,7 @@ def run(self):
175177

176178

177179
class UnitTestsCommand(BaseCommand):
178-
"""Support setup.py validate."""
180+
"""Support setup.py unit_tests."""
179181

180182
description = "Run unit tests (pytest)."
181183
user_options = [("test-target=", "i", "tests/{test-target}")]
Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import asyncio
12
import logging
23
import os
34

5+
from aiohttp import WSMsgType, web
6+
7+
48
socket_mode_envelopes = [
59
"""{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"token":"verification-token","team_id":"T111","team_domain":"xxx","channel_id":"C111","channel_name":"random","user_id":"U111","user_name":"testxyz","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}""",
610
"""{"envelope_id":"cda4159a-72a5-4744-aba3-4d66eb52682b","payload":{"token":"verification-token","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"f0582a78-72db-4feb-b2f3-1e47d66365c8","type":"app_mention","text":"<@U111>","user":"U222","ts":"1610241741.000200","team":"T111","blocks":[{"type":"rich_text","block_id":"Sesm","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U111"}]}]}],"channel":"C111","event_ts":"1610241741.000200"},"type":"event_callback","event_id":"Ev111","event_time":1610241741,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U222","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-app_mention-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":0,"retry_reason":""}""",
@@ -20,50 +24,61 @@
2024

2125
socket_mode_hello_message = """{"type":"hello","num_connections":2,"debug_info":{"host":"applink-111-xxx","build_number":10,"approximate_connection_time":18060},"connection_info":{"app_id":"A111"}}"""
2226

23-
from flask import Flask
24-
from flask_sockets import Sockets
25-
2627

2728
def start_socket_mode_server(self, port: int):
28-
def _start_socket_mode_server():
29-
logger = logging.getLogger(__name__)
30-
app: Flask = Flask(__name__)
31-
sockets: Sockets = Sockets(app)
29+
logger = logging.getLogger(__name__)
30+
state = {}
31+
32+
def reset_server_state():
33+
state.update(
34+
hello_sent=False,
35+
envelopes_to_consume=list(socket_mode_envelopes),
36+
)
37+
38+
self.reset_server_state = reset_server_state
39+
40+
async def link(request):
41+
ws = web.WebSocketResponse()
42+
await ws.prepare(request)
43+
44+
async for msg in ws:
45+
if msg.type != WSMsgType.TEXT:
46+
continue
47+
48+
message = msg.data
49+
logger.debug(f"Server received a message: {message}")
3250

33-
state = {
34-
"hello_sent": False,
35-
"envelopes_to_consume": list(socket_mode_envelopes),
36-
}
51+
if not state["hello_sent"]:
52+
state["hello_sent"] = True
53+
await ws.send_str(socket_mode_hello_message)
3754

38-
@sockets.route("/link")
39-
def link(ws):
40-
while not ws.closed:
41-
message = ws.read_message()
42-
if message is not None:
43-
if not state["hello_sent"]:
44-
ws.send(socket_mode_hello_message)
45-
state["hello_sent"] = True
55+
if state["envelopes_to_consume"]:
56+
e = state["envelopes_to_consume"].pop(0)
57+
logger.debug(f"Send an envelope: {e}")
58+
await ws.send_str(e)
4659

47-
if len(state.get("envelopes_to_consume")) > 0:
48-
e = state.get("envelopes_to_consume").pop(0)
49-
logger.debug(f"Send an envelope: {e}")
50-
ws.send(e)
60+
await ws.send_str(message)
5161

52-
logger.debug(f"Server received a message: {message}")
53-
ws.send(message)
62+
return ws
5463

55-
from gevent import pywsgi
56-
from geventwebsocket.handler import WebSocketHandler
64+
app = web.Application()
65+
app.add_routes([web.get("/link", link)])
66+
runner = web.AppRunner(app)
5767

58-
server = pywsgi.WSGIServer(("", port), app, handler_class=WebSocketHandler)
59-
self.server = server
68+
def run_server():
69+
reset_server_state()
6070

61-
def reset_sever_state():
62-
state["hello_sent"] = False
63-
state["envelopes_to_consume"] = list(socket_mode_envelopes)
71+
self.loop = loop = asyncio.new_event_loop()
72+
asyncio.set_event_loop(loop)
73+
loop.run_until_complete(runner.setup())
74+
site = web.TCPSite(runner, "127.0.0.1", port, reuse_port=True)
75+
loop.run_until_complete(site.start())
6476

65-
self.reset_sever_state = reset_sever_state
77+
# run until it's stopped from the main thread
78+
loop.run_forever()
6679

67-
server.serve_forever(stop_timeout=1)
80+
loop.run_until_complete(runner.cleanup())
81+
loop.run_until_complete(asyncio.sleep(1))
82+
loop.close()
6883

69-
return _start_socket_mode_server
84+
return run_server

tests/slack_sdk/socket_mode/test_interactions_builtin.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from random import randint
55
from threading import Thread
66

7+
import pytest
8+
79
from slack_sdk.errors import SlackClientConfigurationError, SlackClientNotConnectedError
810
from slack_sdk.socket_mode.request import SocketModeRequest
911

@@ -66,7 +68,7 @@ def test_interactions(self):
6668
try:
6769
buffer_size_list = [1024, 9000, 35, 49] + list([randint(16, 128) for _ in range(10)])
6870
for buffer_size in buffer_size_list:
69-
self.reset_sever_state()
71+
self.reset_server_state()
7072

7173
received_messages = []
7274
received_socket_mode_requests = []
@@ -127,8 +129,8 @@ def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketMod
127129
# Restore the default value
128130
sys.setrecursionlimit(default_recursion_limit)
129131
client.close()
130-
self.server.stop()
131-
self.server.close()
132+
self.loop.stop()
133+
t.join(timeout=5)
132134

133135
self.logger.info(f"Passed with buffer size: {buffer_size_list}")
134136

@@ -141,7 +143,7 @@ def test_send_message_while_disconnection(self):
141143
time.sleep(2) # wait for the server
142144

143145
try:
144-
self.reset_sever_state()
146+
self.reset_server_state()
145147
client = SocketModeClient(
146148
app_token="xapp-A111-222-xyz",
147149
web_client=self.web_client,
@@ -155,16 +157,13 @@ def test_send_message_while_disconnection(self):
155157

156158
client.disconnect()
157159
time.sleep(1) # wait for the connection
158-
try:
160+
with pytest.raises(SlackClientNotConnectedError):
159161
client.send_message("foo")
160-
self.fail("SlackClientNotConnectedError is expected here")
161-
except SlackClientNotConnectedError as _:
162-
pass
163162

164163
client.connect()
165164
time.sleep(1) # wait for the connection
166165
client.send_message("foo")
167166
finally:
168167
client.close()
169-
self.server.stop()
170-
self.server.close()
168+
self.loop.stop()
169+
t.join(timeout=5)

tests/slack_sdk/socket_mode/test_interactions_websocket_client.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketMod
8383
expected.sort()
8484

8585
count = 0
86-
while count < 10 and len(received_messages) < len(expected):
86+
while count < 10 and (
87+
len(received_messages) < len(expected) or len(received_socket_mode_requests) < len(socket_mode_envelopes)
88+
):
8789
time.sleep(0.2)
8890
count += 0.2
8991

@@ -93,8 +95,8 @@ def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketMod
9395
self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests))
9496
finally:
9597
client.close()
96-
self.server.stop()
97-
self.server.close()
98+
self.loop.stop()
99+
t.join(timeout=5)
98100

99101
def test_send_message_while_disconnection(self):
100102
if is_ci_unstable_test_skip_enabled():
@@ -105,7 +107,6 @@ def test_send_message_while_disconnection(self):
105107
time.sleep(2) # wait for the server
106108

107109
try:
108-
self.reset_sever_state()
109110
client = SocketModeClient(
110111
app_token="xapp-A111-222-xyz",
111112
web_client=self.web_client,
@@ -131,5 +132,5 @@ def test_send_message_while_disconnection(self):
131132
client.send_message("foo")
132133
finally:
133134
client.close()
134-
self.server.stop()
135-
self.server.close()
135+
self.loop.stop()
136+
t.join(timeout=5)

tests/slack_sdk_async/socket_mode/test_interactions_aiohttp.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from random import randint
66
from threading import Thread
77

8+
import pytest
89
from aiohttp import WSMessage
10+
911
from slack_sdk.socket_mode.request import SocketModeRequest
1012

1113
from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient
@@ -87,7 +89,9 @@ async def socket_mode_listener(
8789
expected.sort()
8890

8991
count = 0
90-
while count < 10 and len(received_messages) < len(expected):
92+
while count < 10 and (
93+
len(received_messages) < len(expected) or len(received_socket_mode_requests) < len(socket_mode_envelopes)
94+
):
9195
await asyncio.sleep(0.2)
9296
count += 0.2
9397

@@ -97,8 +101,8 @@ async def socket_mode_listener(
97101
self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests))
98102
finally:
99103
await client.close()
100-
self.server.stop()
101-
self.server.close()
104+
self.loop.stop()
105+
t.join(timeout=5)
102106

103107
@async_test
104108
async def test_send_message_while_disconnection(self):
@@ -124,16 +128,13 @@ async def test_send_message_while_disconnection(self):
124128

125129
await client.disconnect()
126130
await asyncio.sleep(1) # wait for the message receiver
127-
try:
131+
with pytest.raises(ConnectionError):
128132
await client.send_message("foo")
129-
self.fail("ConnectionError is expected here")
130-
except ConnectionError as _:
131-
pass
132133

133134
await client.connect()
134135
await asyncio.sleep(1) # wait for the message receiver
135136
await client.send_message("foo")
136137
finally:
137138
await client.close()
138-
self.server.stop()
139-
self.server.close()
139+
self.loop.stop()
140+
t.join(timeout=5)

0 commit comments

Comments
 (0)