Skip to content

Commit 580eaab

Browse files
chore: improve flaky tests (#1615)
1 parent 5c57a32 commit 580eaab

File tree

5 files changed

+85
-144
lines changed

5 files changed

+85
-144
lines changed
Lines changed: 71 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import asyncio
22
import logging
33
import os
4+
import threading
45
import time
6+
from typing import Any, Dict
7+
from urllib.error import URLError
8+
from urllib.request import urlopen
9+
from unittest import TestCase
510

611
from aiohttp import WSMsgType, web
712

@@ -28,89 +33,40 @@
2833
socket_mode_disconnect_message = """{"type":"disconnect","reason":"too_many_websockets","num_connections":2,"debug_info":{"host":"applink-111-xxx"},"connection_info":{"app_id":"A111"}}"""
2934

3035

31-
def start_socket_mode_server(self, port: int):
36+
def start_thread_socket_mode_server(self, port: int):
3237
logger = logging.getLogger(__name__)
33-
state = {}
38+
state: Dict[str, Any] = {}
3439

3540
def reset_server_state():
3641
state.update(
3742
hello_sent=False,
43+
disconnect=False,
3844
envelopes_to_consume=list(socket_mode_envelopes),
3945
)
4046

4147
self.reset_server_state = reset_server_state
4248

43-
async def link(request):
44-
ws = web.WebSocketResponse()
45-
await ws.prepare(request)
46-
47-
async for msg in ws:
48-
if msg.type != WSMsgType.TEXT:
49-
continue
50-
51-
message = msg.data
52-
logger.debug(f"Server received a message: {message}")
53-
54-
if not state["hello_sent"]:
55-
state["hello_sent"] = True
56-
await ws.send_str(socket_mode_hello_message)
57-
58-
if state["envelopes_to_consume"]:
59-
e = state["envelopes_to_consume"].pop(0)
60-
logger.debug(f"Send an envelope: {e}")
61-
await ws.send_str(e)
62-
63-
await ws.send_str(message)
64-
65-
return ws
66-
67-
app = web.Application()
68-
app.add_routes([web.get("/link", link)])
69-
runner = web.AppRunner(app)
70-
71-
def run_server():
72-
reset_server_state()
73-
74-
self.loop = loop = asyncio.new_event_loop()
75-
asyncio.set_event_loop(loop)
76-
loop.run_until_complete(runner.setup())
77-
site = web.TCPSite(runner, "127.0.0.1", port, reuse_port=True)
78-
loop.run_until_complete(site.start())
49+
async def health(request: web.Request):
50+
wr = web.Response()
51+
await wr.prepare(request)
52+
wr.set_status(200)
53+
return wr
7954

80-
# run until it's stopped from the main thread
81-
loop.run_forever()
82-
83-
loop.run_until_complete(runner.cleanup())
84-
loop.run_until_complete(asyncio.sleep(1))
85-
loop.close()
86-
87-
return run_server
88-
89-
90-
def start_socket_mode_server_with_disconnection(self, port: int):
91-
logger = logging.getLogger(__name__)
92-
state = {}
93-
94-
def reset_server_state():
95-
state.update(
96-
hello_sent=False,
97-
disconnect_sent=False,
98-
envelopes_to_consume=list(socket_mode_envelopes),
99-
)
100-
101-
self.reset_server_state = reset_server_state
55+
async def disconnect(request: web.Request):
56+
state["disconnect"] = True
57+
wr = web.Response()
58+
await wr.prepare(request)
59+
wr.set_status(200)
60+
return wr
10261

10362
async def link(request):
104-
disconnected = False
63+
connected = True
10564
ws = web.WebSocketResponse()
10665
await ws.prepare(request)
10766

10867
async for msg in ws:
109-
# To ensure disconnect message is received and handled,
110-
# need to keep this ws alive to bypass client ping-pong check.
11168
if msg.type == WSMsgType.PING:
112-
t = time.time()
113-
await ws.pong(f"sdk-ping-pong:{t}")
69+
await ws.pong(f"sdk-ping-pong:{time.time()}")
11470
continue
11571
if msg.type != WSMsgType.TEXT:
11672
continue
@@ -122,14 +78,14 @@ async def link(request):
12278
state["hello_sent"] = True
12379
await ws.send_str(socket_mode_hello_message)
12480

125-
if not state["disconnect_sent"]:
81+
if state["disconnect"]:
12682
state["hello_sent"] = False
127-
state["disconnect_sent"] = True
128-
disconnected = True
83+
state["disconnect"] = False
84+
connected = False
12985
await ws.send_str(socket_mode_disconnect_message)
130-
logger.debug(f"Disconnect message sent")
86+
logger.debug("Disconnect message sent")
13187

132-
if state["envelopes_to_consume"] and not disconnected:
88+
if state["envelopes_to_consume"] and connected:
13389
e = state["envelopes_to_consume"].pop(0)
13490
logger.debug(f"Send an envelope: {e}")
13591
await ws.send_str(e)
@@ -139,7 +95,13 @@ async def link(request):
13995
return ws
14096

14197
app = web.Application()
142-
app.add_routes([web.get("/link", link)])
98+
app.add_routes(
99+
[
100+
web.get("/link", link),
101+
web.get("/health", health),
102+
web.get("/disconnect", disconnect),
103+
]
104+
)
143105
runner = web.AppRunner(app)
144106

145107
def run_server():
@@ -155,7 +117,44 @@ def run_server():
155117
loop.run_forever()
156118

157119
loop.run_until_complete(runner.cleanup())
158-
loop.run_until_complete(asyncio.sleep(1))
159120
loop.close()
160121

161122
return run_server
123+
124+
125+
def start_socket_mode_server(test, port: int):
126+
test.sm_thread = threading.Thread(target=start_thread_socket_mode_server(test, port))
127+
test.sm_thread.daemon = True
128+
test.sm_thread.start()
129+
wait_for_socket_mode_server(port, 4)
130+
131+
132+
def stop_socket_mode_server(test: TestCase):
133+
# An event loop runs in a thread and executes all callbacks and Tasks in
134+
# its thread. While a Task is running in the event loop, no other Tasks
135+
# can run in the same thread. When a Task executes an await expression, the
136+
# running Task gets suspended, and the event loop executes the next Task.
137+
# To schedule a callback from another OS thread, the loop.call_soon_threadsafe() method should be used.
138+
# https://docs.python.org/3/library/asyncio-dev.html#asyncio-multithreading
139+
test.loop.call_soon_threadsafe(test.loop.stop)
140+
test.sm_thread.join(timeout=5)
141+
142+
143+
def wait_for_socket_mode_server(port: int, timeout: int):
144+
start_time = time.time()
145+
while (time.time() - start_time) < timeout:
146+
try:
147+
urlopen(f"http://127.0.0.1:{port}/health")
148+
return
149+
except URLError:
150+
time.sleep(0.01)
151+
152+
153+
def request_socket_mode_server_disconnect(port: int, timeout: int):
154+
start_time = time.time()
155+
while (time.time() - start_time) < timeout:
156+
try:
157+
urlopen(f"http://127.0.0.1:{port}/disconnect")
158+
return
159+
except URLError:
160+
time.sleep(0.01)

tests/slack_sdk/socket_mode/test_interactions_builtin.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import time
33
import unittest
44
from random import randint
5-
from threading import Thread
65

76
import pytest
87

@@ -17,6 +16,7 @@
1716
start_socket_mode_server,
1817
socket_mode_envelopes,
1918
socket_mode_hello_message,
19+
stop_socket_mode_server,
2020
)
2121
from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler
2222
from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server
@@ -33,9 +33,11 @@ def setUp(self):
3333
token="xoxb-api_test",
3434
base_url="http://localhost:8888",
3535
)
36+
start_socket_mode_server(self, 3011)
3637

3738
def tearDown(self):
3839
cleanup_mock_web_api_server(self)
40+
stop_socket_mode_server(self)
3941

4042
def test_buffer_size_validation(self):
4143
try:
@@ -54,11 +56,6 @@ def test_interactions(self):
5456
# we believe that the same situation never happens in the production usage.
5557
sys.setrecursionlimit(10000)
5658

57-
t = Thread(target=start_socket_mode_server(self, 3011))
58-
t.daemon = True
59-
t.start()
60-
time.sleep(2) # wait for the server
61-
6259
try:
6360
buffer_size_list = [1024, 9000, 35, 49] + list([randint(16, 128) for _ in range(10)])
6461
for buffer_size in buffer_size_list:
@@ -123,17 +120,10 @@ def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketMod
123120
# Restore the default value
124121
sys.setrecursionlimit(default_recursion_limit)
125122
client.close()
126-
self.loop.stop()
127-
t.join(timeout=5)
128123

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

131126
def test_send_message_while_disconnection(self):
132-
t = Thread(target=start_socket_mode_server(self, 3011))
133-
t.daemon = True
134-
t.start()
135-
time.sleep(2) # wait for the server
136-
137127
try:
138128
self.reset_server_state()
139129
client = SocketModeClient(
@@ -157,5 +147,3 @@ def test_send_message_while_disconnection(self):
157147
client.send_message("foo")
158148
finally:
159149
client.close()
160-
self.loop.stop()
161-
t.join(timeout=5)

tests/slack_sdk/socket_mode/test_interactions_websocket_client.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import time
33
import unittest
44
from random import randint
5-
from threading import Thread
65

76
from websocket import WebSocketException
87

@@ -17,6 +16,7 @@
1716
start_socket_mode_server,
1817
socket_mode_envelopes,
1918
socket_mode_hello_message,
19+
stop_socket_mode_server,
2020
)
2121
from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler
2222
from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server
@@ -31,15 +31,13 @@ def setUp(self):
3131
token="xoxb-api_test",
3232
base_url="http://localhost:8888",
3333
)
34+
start_socket_mode_server(self, 3012)
3435

3536
def tearDown(self):
3637
cleanup_mock_web_api_server(self)
38+
stop_socket_mode_server(self)
3739

3840
def test_interactions(self):
39-
t = Thread(target=start_socket_mode_server(self, 3012))
40-
t.daemon = True
41-
t.start()
42-
4341
received_messages = []
4442
received_socket_mode_requests = []
4543

@@ -63,7 +61,6 @@ def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketMod
6361
client.socket_mode_request_listeners.append(socket_mode_request_handler)
6462

6563
try:
66-
time.sleep(1) # wait for the server
6764
client.wss_uri = "ws://0.0.0.0:3012/link"
6865
client.connect()
6966
time.sleep(1) # wait for the message receiver
@@ -91,17 +88,11 @@ def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketMod
9188
self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests))
9289
finally:
9390
client.close()
94-
self.loop.stop()
95-
t.join(timeout=5)
9691

9792
def test_send_message_while_disconnection(self):
9893
if is_ci_unstable_test_skip_enabled():
9994
# this test tends to fail on the GitHub Actions platform
10095
return
101-
t = Thread(target=start_socket_mode_server(self, 3012))
102-
t.daemon = True
103-
t.start()
104-
time.sleep(2) # wait for the server
10596

10697
try:
10798
client = SocketModeClient(
@@ -129,5 +120,3 @@ def test_send_message_while_disconnection(self):
129120
client.send_message("foo")
130121
finally:
131122
client.close()
132-
self.loop.stop()
133-
t.join(timeout=5)

0 commit comments

Comments
 (0)