@@ -677,3 +677,222 @@ def test_loop_close_flushes_async_transport(sentry_init):
677677 loop .close ()
678678 if original_loop :
679679 asyncio .set_event_loop (original_loop )
680+
681+
682+ # ============================================================================
683+ # patch_loop_close edge case tests
684+ # ============================================================================
685+
686+
687+ @minimum_python_38
688+ def test_patch_loop_close_no_running_loop ():
689+ """Test patch_loop_close is a no-op when no running loop."""
690+ from sentry_sdk .integrations .asyncio import patch_loop_close
691+
692+ # Should not raise
693+ with patch ("asyncio.get_running_loop" , side_effect = RuntimeError ("no loop" )):
694+ patch_loop_close ()
695+
696+
697+ @minimum_python_38
698+ def test_patch_loop_close_already_patched ():
699+ """Test patch_loop_close skips if already patched."""
700+ from sentry_sdk .integrations .asyncio import patch_loop_close
701+
702+ mock_loop = MagicMock ()
703+ mock_loop ._sentry_flush_patched = True
704+
705+ original_close = mock_loop .close
706+
707+ with patch ("asyncio.get_running_loop" , return_value = mock_loop ):
708+ patch_loop_close ()
709+
710+ # close should not have been replaced since already patched
711+ assert mock_loop .close is original_close
712+
713+
714+ @minimum_python_38
715+ def test_patch_loop_close_patches_close ():
716+ """Test patch_loop_close replaces loop.close with patched version."""
717+ from sentry_sdk .integrations .asyncio import patch_loop_close
718+
719+ mock_loop = MagicMock ()
720+ mock_loop ._sentry_flush_patched = False
721+ # Make getattr return False for _sentry_flush_patched
722+ type(mock_loop )._sentry_flush_patched = False
723+
724+ with patch ("asyncio.get_running_loop" , return_value = mock_loop ):
725+ patch_loop_close ()
726+
727+ # close should have been replaced
728+ assert mock_loop ._sentry_flush_patched is True
729+
730+
731+ # ============================================================================
732+ # _create_task_with_factory tests
733+ # ============================================================================
734+
735+
736+ @minimum_python_38
737+ @patch ("sentry_sdk.integrations.asyncio.Task" )
738+ def test_create_task_with_factory_no_orig_factory (MockTask ):
739+ """Test _create_task_with_factory creates Task directly when no orig factory."""
740+ from sentry_sdk .integrations .asyncio import _create_task_with_factory
741+
742+ mock_loop = MagicMock ()
743+ mock_coro = MagicMock ()
744+
745+ result = _create_task_with_factory (None , mock_loop , mock_coro )
746+
747+ MockTask .assert_called_once_with (mock_coro , loop = mock_loop )
748+ assert result == MockTask .return_value
749+
750+
751+ @minimum_python_38
752+ def test_create_task_with_factory_with_orig_factory ():
753+ """Test _create_task_with_factory uses orig factory when provided."""
754+ from sentry_sdk .integrations .asyncio import _create_task_with_factory
755+
756+ mock_loop = MagicMock ()
757+ mock_coro = MagicMock ()
758+ orig_factory = MagicMock ()
759+
760+ result = _create_task_with_factory (orig_factory , mock_loop , mock_coro , name = "test" )
761+
762+ orig_factory .assert_called_once_with (mock_loop , mock_coro , name = "test" )
763+ assert result == orig_factory .return_value
764+
765+
766+ @minimum_python_38
767+ @patch ("sentry_sdk.integrations.asyncio.Task" )
768+ def test_create_task_with_factory_orig_returns_none (MockTask ):
769+ """Test _create_task_with_factory falls back to Task when orig factory returns None."""
770+ from sentry_sdk .integrations .asyncio import _create_task_with_factory
771+
772+ mock_loop = MagicMock ()
773+ mock_coro = MagicMock ()
774+ orig_factory = MagicMock (return_value = None )
775+
776+ result = _create_task_with_factory (orig_factory , mock_loop , mock_coro )
777+
778+ orig_factory .assert_called_once ()
779+ MockTask .assert_called_once_with (mock_coro , loop = mock_loop )
780+ assert result == MockTask .return_value
781+
782+
783+ # ============================================================================
784+ # _sentry_task_factory internal task detection tests
785+ # ============================================================================
786+
787+
788+ @minimum_python_39
789+ @pytest .mark .asyncio (loop_scope = "module" )
790+ async def test_sentry_task_factory_skips_internal_tasks (sentry_init ):
791+ """Test that internal tasks bypass Sentry wrapping."""
792+ sentry_init (
793+ integrations = [AsyncioIntegration ()],
794+ traces_sample_rate = 1.0 ,
795+ )
796+
797+ results = []
798+
799+ async def internal_coro ():
800+ results .append ("internal_ran" )
801+ return 42
802+
803+ with mark_sentry_task_internal ():
804+ task = asyncio .create_task (internal_coro ())
805+ result = await task
806+
807+ assert result == 42
808+ assert results == ["internal_ran" ]
809+
810+ # Verify the coroutine was NOT wrapped (internal tasks are not wrapped
811+ # with _task_with_sentry_span_creation)
812+ # The task's coro should be the original, not a wrapped one
813+ # We check by ensuring no span was created for this
814+ # (the span count test is more reliable)
815+
816+
817+ @minimum_python_39
818+ @pytest .mark .asyncio (loop_scope = "module" )
819+ async def test_sentry_task_factory_wraps_user_tasks (sentry_init , capture_events ):
820+ """Test that user tasks get wrapped with Sentry instrumentation."""
821+ sentry_init (
822+ integrations = [AsyncioIntegration ()],
823+ traces_sample_rate = 1.0 ,
824+ )
825+
826+ events = capture_events ()
827+
828+ async def user_coro ():
829+ await asyncio .sleep (0.01 )
830+
831+ with sentry_sdk .start_transaction (name = "test" ):
832+ task = asyncio .create_task (user_coro ())
833+ await task
834+
835+ sentry_sdk .flush ()
836+
837+ assert len (events ) == 1
838+ transaction = events [0 ]
839+ # User tasks should get spans
840+ user_spans = [
841+ s
842+ for s in transaction .get ("spans" , [])
843+ if "user_coro" in s .get ("description" , "" )
844+ ]
845+ assert len (user_spans ) > 0
846+
847+
848+ # ============================================================================
849+ # is_internal_task / mark_sentry_task_internal utility tests
850+ # ============================================================================
851+
852+
853+ @minimum_python_38
854+ def test_is_internal_task_default ():
855+ """Test is_internal_task returns False by default."""
856+ from sentry_sdk .utils import is_internal_task
857+
858+ assert is_internal_task () is False
859+
860+
861+ @minimum_python_38
862+ def test_mark_sentry_task_internal_context_manager ():
863+ """Test mark_sentry_task_internal sets and resets the flag."""
864+ from sentry_sdk .utils import is_internal_task , mark_sentry_task_internal
865+
866+ assert is_internal_task () is False
867+ with mark_sentry_task_internal ():
868+ assert is_internal_task () is True
869+ assert is_internal_task () is False
870+
871+
872+ @minimum_python_38
873+ def test_mark_sentry_task_internal_nested ():
874+ """Test nested mark_sentry_task_internal restores correctly."""
875+ from sentry_sdk .utils import is_internal_task , mark_sentry_task_internal
876+
877+ assert is_internal_task () is False
878+ with mark_sentry_task_internal ():
879+ assert is_internal_task () is True
880+ with mark_sentry_task_internal ():
881+ assert is_internal_task () is True
882+ assert is_internal_task () is True
883+ assert is_internal_task () is False
884+
885+
886+ @minimum_python_38
887+ def test_mark_sentry_task_internal_exception_cleanup ():
888+ """Test mark_sentry_task_internal resets flag even on exception."""
889+ from sentry_sdk .utils import is_internal_task , mark_sentry_task_internal
890+
891+ assert is_internal_task () is False
892+ try :
893+ with mark_sentry_task_internal ():
894+ assert is_internal_task () is True
895+ raise ValueError ("test exception" )
896+ except ValueError :
897+ pass
898+ assert is_internal_task () is False
0 commit comments