diff --git a/tests/conftest.py b/tests/conftest.py index 6a33029d11..01b1e9a81f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,12 +2,18 @@ import os import socket import warnings +import brotli +import gzip +import io from threading import Thread from contextlib import contextmanager from http.server import BaseHTTPRequestHandler, HTTPServer from unittest import mock +from collections import namedtuple import pytest +from pytest_localserver.http import WSGIServer +from werkzeug.wrappers import Request, Response import jsonschema @@ -23,7 +29,7 @@ import sentry_sdk import sentry_sdk.utils -from sentry_sdk.envelope import Envelope +from sentry_sdk.envelope import Envelope, parse_json from sentry_sdk.integrations import ( # noqa: F401 _DEFAULT_INTEGRATIONS, _installed_integrations, @@ -663,3 +669,57 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) + + +CapturedData = namedtuple("CapturedData", ["path", "event", "envelope", "compressed"]) + + +class CapturingServer(WSGIServer): + def __init__(self, host="127.0.0.1", port=0, ssl_context=None): + WSGIServer.__init__(self, host, port, self, ssl_context=ssl_context) + self.code = 204 + self.headers = {} + self.captured = [] + + def respond_with(self, code=200, headers=None): + self.code = code + if headers: + self.headers = headers + + def clear_captured(self): + del self.captured[:] + + def __call__(self, environ, start_response): + """ + This is the WSGI application. + """ + request = Request(environ) + event = envelope = None + content_encoding = request.headers.get("content-encoding") + if content_encoding == "gzip": + rdr = gzip.GzipFile(fileobj=io.BytesIO(request.data)) + compressed = True + elif content_encoding == "br": + rdr = io.BytesIO(brotli.decompress(request.data)) + compressed = True + else: + rdr = io.BytesIO(request.data) + compressed = False + + if request.mimetype == "application/json": + event = parse_json(rdr.read()) + else: + envelope = Envelope.deserialize_from(rdr) + + self.captured.append( + CapturedData( + path=request.path, + event=event, + envelope=envelope, + compressed=compressed, + ) + ) + + response = Response(status=self.code) + response.headers.extend(self.headers) + return response(environ, start_response) diff --git a/tests/test_gevent.py b/tests/test_gevent.py new file mode 100644 index 0000000000..d330760adf --- /dev/null +++ b/tests/test_gevent.py @@ -0,0 +1,118 @@ +import logging +import pickle +from datetime import datetime, timezone + +import sentry_sdk +from sentry_sdk._compat import PY37, PY38 + +import pytest +from tests.conftest import CapturingServer + +pytest.importorskip("gevent") + + +@pytest.fixture(scope="module") +def monkeypatched_gevent(): + try: + import gevent + + gevent.monkey.patch_all() + except Exception as e: + if "_RLock__owner" in str(e): + pytest.skip("https://github.com/gevent/gevent/issues/1380") + else: + raise + + +@pytest.fixture +def capturing_server(request): + server = CapturingServer() + server.start() + request.addfinalizer(server.stop) + return server + + +@pytest.fixture +def make_client(request, capturing_server): + def inner(**kwargs): + return sentry_sdk.Client( + "http://foobar@{}/132".format(capturing_server.url[len("http://") :]), + **kwargs, + ) + + return inner + + +@pytest.mark.forked +@pytest.mark.parametrize("debug", (True, False)) +@pytest.mark.parametrize("client_flush_method", ["close", "flush"]) +@pytest.mark.parametrize("use_pickle", (True, False)) +@pytest.mark.parametrize("compression_level", (0, 9, None)) +@pytest.mark.parametrize( + "compression_algo", + (("gzip", "br", "", None) if PY37 else ("gzip", "", None)), +) +@pytest.mark.parametrize("http2", [True, False] if PY38 else [False]) +def test_transport_works_gevent( + capturing_server, + request, + capsys, + caplog, + debug, + make_client, + client_flush_method, + use_pickle, + compression_level, + compression_algo, + http2, +): + caplog.set_level(logging.DEBUG) + + experiments = {} + if compression_level is not None: + experiments["transport_compression_level"] = compression_level + + if compression_algo is not None: + experiments["transport_compression_algo"] = compression_algo + + if http2: + experiments["transport_http2"] = True + + client = make_client( + debug=debug, + _experiments=experiments, + ) + + if use_pickle: + client = pickle.loads(pickle.dumps(client)) + + sentry_sdk.get_global_scope().set_client(client) + request.addfinalizer(lambda: sentry_sdk.get_global_scope().set_client(None)) + + sentry_sdk.add_breadcrumb( + level="info", message="i like bread", timestamp=datetime.now(timezone.utc) + ) + sentry_sdk.capture_message("löl") + + getattr(client, client_flush_method)() + + out, err = capsys.readouterr() + assert not err and not out + assert capturing_server.captured + should_compress = ( + # default is to compress with brotli if available, gzip otherwise + (compression_level is None) + or ( + # setting compression level to 0 means don't compress + compression_level + > 0 + ) + ) and ( + # if we couldn't resolve to a known algo, we don't compress + compression_algo + != "" + ) + + assert capturing_server.captured[0].compressed == should_compress + + assert any("Sending envelope" in record.msg for record in caplog.records) == debug diff --git a/tests/test_transport.py b/tests/test_transport.py index 6eb7cdf829..c6a1a0a7a7 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1,29 +1,20 @@ import logging import pickle -import gzip -import io import os import socket import sys -from collections import defaultdict, namedtuple +from collections import defaultdict from datetime import datetime, timedelta, timezone from unittest import mock -import brotli import pytest -from pytest_localserver.http import WSGIServer -from werkzeug.wrappers import Request, Response +from tests.conftest import CapturingServer try: import httpcore except (ImportError, ModuleNotFoundError): httpcore = None -try: - import gevent -except ImportError: - gevent = None - import sentry_sdk from sentry_sdk import ( Client, @@ -42,65 +33,22 @@ ) from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger -CapturedData = namedtuple("CapturedData", ["path", "event", "envelope", "compressed"]) - - -class CapturingServer(WSGIServer): - def __init__(self, host="127.0.0.1", port=0, ssl_context=None): - WSGIServer.__init__(self, host, port, self, ssl_context=ssl_context) - self.code = 204 - self.headers = {} - self.captured = [] - - def respond_with(self, code=200, headers=None): - self.code = code - if headers: - self.headers = headers - - def clear_captured(self): - del self.captured[:] - - def __call__(self, environ, start_response): - """ - This is the WSGI application. - """ - request = Request(environ) - event = envelope = None - content_encoding = request.headers.get("content-encoding") - if content_encoding == "gzip": - rdr = gzip.GzipFile(fileobj=io.BytesIO(request.data)) - compressed = True - elif content_encoding == "br": - rdr = io.BytesIO(brotli.decompress(request.data)) - compressed = True - else: - rdr = io.BytesIO(request.data) - compressed = False - - if request.mimetype == "application/json": - event = parse_json(rdr.read()) - else: - envelope = Envelope.deserialize_from(rdr) - - self.captured.append( - CapturedData( - path=request.path, - event=event, - envelope=envelope, - compressed=compressed, - ) - ) - response = Response(status=self.code) - response.headers.extend(self.headers) - return response(environ, start_response) +server = None -@pytest.fixture -def capturing_server(request): +@pytest.fixture(scope="module", autouse=True) +def make_capturing_server(request): + global server server = CapturingServer() server.start() request.addfinalizer(server.stop) + + +@pytest.fixture +def capturing_server(): + global server + server.clear_captured() return server @@ -129,18 +77,13 @@ def mock_transaction_envelope(span_count): return envelope -@pytest.mark.forked @pytest.mark.parametrize("debug", (True, False)) @pytest.mark.parametrize("client_flush_method", ["close", "flush"]) @pytest.mark.parametrize("use_pickle", (True, False)) @pytest.mark.parametrize("compression_level", (0, 9, None)) @pytest.mark.parametrize( "compression_algo", - ( - ("gzip", "br", "", None) - if PY37 or gevent is None - else ("gzip", "", None) - ), + (("gzip", "br", "", None) if PY37 else ("gzip", "", None)), ) @pytest.mark.parametrize("http2", [True, False] if PY38 else [False]) def test_transport_works( @@ -155,7 +98,6 @@ def test_transport_works( compression_level, compression_algo, http2, - maybe_monkeypatched_threading, ): caplog.set_level(logging.DEBUG)