diff --git a/tests/conftest.py b/tests/conftest.py index faa0251d9b..082f029a03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -389,6 +389,110 @@ def render_span(span): return inner +@pytest.fixture(name="EmittedSpanMetadataEqual") +def span_metadata_matcher(): + class EmittedSpanMetadataEqual: + def __init__( + self, + span, + check_trace_id=True, + check_op=True, + check_status=True, + check_origin=True, + check_name=True, + ): + self.span = span + + self.check_trace_id = check_trace_id + self.check_op = check_op + self.check_status = check_status + self.check_origin = check_origin + self.check_name = check_name + + def __eq__(self, span): + return ( + (not self.check_trace_id or self.span.trace_id == span["trace_id"]) + and (not self.check_op or self.span.op == span["op"]) + and (not self.check_status or self.span.status == span["status"]) + and (not self.check_origin or self.span.origin == span["origin"]) + and ( + not self.check_name or self.span.description == span["description"] + ) + ) + + def __ne__(self, test_string): + return not self.__eq__(test_string) + + return EmittedSpanMetadataEqual + + +@pytest.fixture(name="SpanTreeEqualUnorderedSiblings") +def unordered_siblings_span_tree_matcher(EmittedSpanMetadataEqual): + class SpanTreeEqualUnorderedSiblings: + def __init__(self, expected_root_span, expected_span_tree, **kwargs): + self.expected_root_span = expected_root_span + self.expected_span_tree = expected_span_tree + + self.span_matcher_kwargs = kwargs + + def _construct_parent_to_spans_mapping(self, event): + by_parent = {} + for span in event["spans"]: + by_parent.setdefault(span["parent_span_id"], []).append(span) + + return by_parent + + def _subtree_eq( + self, actual_subtree_root_span, expected_subtree_root_span, by_parent + ): + if actual_subtree_root_span["span_id"] not in by_parent: + return actual_subtree_root_span == EmittedSpanMetadataEqual( + expected_subtree_root_span, **self.span_matcher_kwargs + ) + + actual_span_children = by_parent[actual_subtree_root_span["span_id"]] + expected_span_children = self.expected_span_tree[expected_subtree_root_span] + + if len(actual_span_children) != len(expected_span_children): + return False + + for expected_child in expected_span_children: + found = False + for actual_child in actual_span_children: + if actual_child == EmittedSpanMetadataEqual( + expected_child, **self.span_matcher_kwargs + ): + found = True + if not self._subtree_eq( + actual_child, expected_child, by_parent + ): + return False + continue + + if not found: + return False + + return True + + def __eq__(self, event): + by_parent = self._construct_parent_to_spans_mapping(event) + actual_root_span = event["contexts"]["trace"] + + if actual_root_span != EmittedSpanMetadataEqual( + self.expected_root_span, **self.span_matcher_kwargs + ): + return False + + return self._subtree_eq( + actual_root_span, self.expected_root_span, by_parent + ) + + def __ne__(self, test_string): + return not self.__eq__(test_string) + + return SpanTreeEqualUnorderedSiblings + + @pytest.fixture(name="StringContaining") def string_containing_matcher(): """ diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 8a30c7f5c0..ec4247805f 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -10,6 +10,7 @@ import pytest from channels.testing import HttpCommunicator from sentry_sdk import capture_message +from sentry_sdk.tracing import Span from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django.asgi import _asgi_middleware_mixin_factory from tests.integrations.django.myapp.asgi import channels_application @@ -29,7 +30,6 @@ @pytest.mark.parametrize("application", APPS) @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0" ) @@ -86,7 +86,6 @@ async def test_basic(sentry_init, capture_events, application): @pytest.mark.parametrize("application", APPS) @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -119,7 +118,6 @@ async def test_async_views(sentry_init, capture_events, application): @pytest.mark.parametrize("application", APPS) @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -165,7 +163,6 @@ async def test_active_thread_id( @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -208,7 +205,6 @@ async def test_async_views_concurrent_execution(sentry_init, settings): @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -255,12 +251,11 @@ async def test_async_middleware_that_is_function_concurrent_execution( @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) async def test_async_middleware_spans( - sentry_init, render_span_tree, capture_events, settings + sentry_init, SpanTreeEqualUnorderedSiblings, capture_events, settings ): settings.MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", @@ -286,26 +281,57 @@ async def test_async_middleware_spans( (transaction,) = events - assert ( - render_span_tree(transaction) - == """\ -- op="http.server": description=null - - op="event.django": description="django.db.reset_queries" - - op="event.django": description="django.db.close_old_connections" - - op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.__acall__" - - op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.__acall__" - - op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.__acall__" - - op="middleware.django": description="tests.integrations.django.myapp.settings.TestMiddleware.__acall__" - - op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_view" - - op="view.render": description="simple_async_view" - - op="event.django": description="django.db.close_old_connections" - - op="event.django": description="django.core.cache.close_caches" - - op="event.django": description="django.core.handlers.base.reset_urlconf\"""" + http_server_span = Span(op="http.server", name=None) + session_middleware_span = Span( + op="middleware.django", + name="django.contrib.sessions.middleware.SessionMiddleware.__acall__", + ) + authentication_middleware_span = Span( + op="middleware.django", + name="django.contrib.auth.middleware.AuthenticationMiddleware.__acall__", + ) + csrf_view_middleware_span = Span( + op="middleware.django", + name="django.middleware.csrf.CsrfViewMiddleware.__acall__", + ) + test_middleware_span = Span( + op="middleware.django", + name="tests.integrations.django.myapp.settings.TestMiddleware.__acall__", + ) + + span_tree = { + http_server_span: { + session_middleware_span, + Span(op="event.django", name="django.db.reset_queries"), + Span(op="event.django", name="django.db.close_old_connections"), + Span(op="event.django", name="django.db.close_old_connections"), + Span(op="event.django", name="django.core.cache.close_caches"), + Span(op="event.django", name="django.core.handlers.base.reset_urlconf"), + }, + session_middleware_span: {authentication_middleware_span}, + authentication_middleware_span: {csrf_view_middleware_span}, + csrf_view_middleware_span: {test_middleware_span}, + test_middleware_span: { + Span( + op="middleware.django", + name="django.middleware.csrf.CsrfViewMiddleware.process_view", + ), + Span(op="view.render", name="simple_async_view"), + }, + } + + assert transaction == SpanTreeEqualUnorderedSiblings( + http_server_span, + span_tree, + check_trace_id=False, + check_op=True, + check_status=False, + check_origin=False, + check_name=True, ) @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -333,7 +359,6 @@ async def test_has_trace_if_performance_enabled(sentry_init, capture_events): @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -364,7 +389,6 @@ async def test_has_trace_if_performance_disabled(sentry_init, capture_events): @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -398,7 +422,6 @@ async def test_trace_from_headers_if_performance_enabled(sentry_init, capture_ev @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -529,7 +552,6 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e ], ) @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" )