Skip to content

Commit 76aae83

Browse files
committed
feat(test): Add tests for specific async transport functionality
Add tests that test specific async functionality for the async transport, such as concurrency. GH-4601
1 parent f21e2ea commit 76aae83

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed

tests/test_transport.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os
44
import socket
55
import sys
6+
import asyncio
7+
import threading
68
from collections import defaultdict
79
from datetime import datetime, timedelta, timezone
810
from unittest import mock
@@ -153,6 +155,7 @@ def test_transport_works(
153155
@pytest.mark.parametrize("use_pickle", (True, False))
154156
@pytest.mark.parametrize("compression_level", (0, 9, None))
155157
@pytest.mark.parametrize("compression_algo", ("gzip", "br", "<invalid>", None))
158+
@pytest.mark.skipif(not PY38, reason="Async transport only supported in Python 3.8+")
156159
async def test_transport_works_async(
157160
capturing_server,
158161
request,
@@ -753,3 +756,118 @@ def close(self):
753756
client.flush()
754757

755758
assert seen == ["status_500"]
759+
760+
761+
@pytest.mark.asyncio
762+
@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+")
763+
async def test_async_transport_background_thread_capture(
764+
capturing_server, make_client, caplog
765+
):
766+
"""Test capture_envelope from background threads uses run_coroutine_threadsafe"""
767+
caplog.set_level(logging.DEBUG)
768+
experiments = {"transport_async": True}
769+
client = make_client(_experiments=experiments)
770+
assert isinstance(client.transport, AsyncHttpTransport)
771+
sentry_sdk.get_global_scope().set_client(client)
772+
captured_from_thread = []
773+
exception_from_thread = []
774+
775+
def background_thread_work():
776+
try:
777+
# This should use run_coroutine_threadsafe path
778+
capture_message("from background thread")
779+
captured_from_thread.append(True)
780+
except Exception as e:
781+
exception_from_thread.append(e)
782+
783+
thread = threading.Thread(target=background_thread_work)
784+
thread.start()
785+
thread.join()
786+
assert not exception_from_thread
787+
assert captured_from_thread
788+
await client.close(timeout=2.0)
789+
assert capturing_server.captured
790+
791+
792+
@pytest.mark.asyncio
793+
@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+")
794+
async def test_async_transport_event_loop_closed_scenario(
795+
capturing_server, make_client, caplog
796+
):
797+
"""Test behavior when trying to capture after event loop context ends"""
798+
caplog.set_level(logging.DEBUG)
799+
experiments = {"transport_async": True}
800+
client = make_client(_experiments=experiments)
801+
sentry_sdk.get_global_scope().set_client(client)
802+
original_loop = client.transport.loop
803+
804+
with mock.patch("asyncio.get_running_loop", side_effect=RuntimeError("no loop")):
805+
with mock.patch.object(client.transport.loop, "is_running", return_value=False):
806+
with mock.patch("sentry_sdk.transport.logger") as mock_logger:
807+
# This should trigger the "no_async_context" path
808+
capture_message("after loop closed")
809+
810+
mock_logger.warning.assert_called_with(
811+
"Async Transport is not running in an event loop."
812+
)
813+
814+
client.transport.loop = original_loop
815+
await client.close(timeout=2.0)
816+
817+
818+
@pytest.mark.asyncio
819+
@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+")
820+
async def test_async_transport_concurrent_requests(
821+
capturing_server, make_client, caplog
822+
):
823+
"""Test multiple simultaneous envelope submissions"""
824+
caplog.set_level(logging.DEBUG)
825+
experiments = {"transport_async": True}
826+
client = make_client(_experiments=experiments)
827+
assert isinstance(client.transport, AsyncHttpTransport)
828+
sentry_sdk.get_global_scope().set_client(client)
829+
830+
num_messages = 15
831+
832+
async def send_message(i):
833+
capture_message(f"concurrent message {i}")
834+
835+
tasks = [send_message(i) for i in range(num_messages)]
836+
await asyncio.gather(*tasks)
837+
transport = client.transport
838+
await client.close(timeout=2.0)
839+
assert len(transport.background_tasks) == 0
840+
assert len(capturing_server.captured) == num_messages
841+
842+
843+
@pytest.mark.asyncio
844+
@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+")
845+
async def test_async_transport_rate_limiting_with_concurrency(
846+
capturing_server, make_client, request
847+
):
848+
"""Test async transport rate limiting with concurrent requests"""
849+
experiments = {"transport_async": True}
850+
client = make_client(_experiments=experiments)
851+
852+
assert isinstance(client.transport, AsyncHttpTransport)
853+
sentry_sdk.get_global_scope().set_client(client)
854+
request.addfinalizer(lambda: sentry_sdk.get_global_scope().set_client(None))
855+
capturing_server.respond_with(
856+
code=429, headers={"X-Sentry-Rate-Limits": "60:error:organization"}
857+
)
858+
859+
# Send one request first to trigger rate limiting
860+
capture_message("initial message")
861+
await asyncio.sleep(0.1) # Wait for request to execute
862+
assert client.transport._check_disabled("error") is True
863+
capturing_server.clear_captured()
864+
865+
async def send_message(i):
866+
capture_message(f"message {i}")
867+
await asyncio.sleep(0.01)
868+
869+
await asyncio.gather(*[send_message(i) for i in range(5)])
870+
await asyncio.sleep(0.1)
871+
# New request should be dropped due to rate limiting
872+
assert len(capturing_server.captured) == 0
873+
await client.close(timeout=2.0)

0 commit comments

Comments
 (0)