Ctrl K
ComunidadGuías…

issues/test_stacktrace_view.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import json
import os
from html import unescape
from pathlib import Path

from django.contrib.auth import get_user_model
from django.test import tag
from django.utils.html import strip_tags

from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase
from events.factories import create_event
from issues.utils import get_type_and_value_for_data
from projects.models import Project, ProjectMembership


SAMPLES_DIR = Path(os.getenv("SAMPLES_DIR", "../event-samples"))


def load_sample(relative_path):
    with open(SAMPLES_DIR / relative_path) as sample:
        return json.load(sample)


def visible_text(response):
    return " ".join(unescape(strip_tags(response.content.decode())).split())


class StacktraceViewTests(TransactionTestCase):
    def setUp(self):
        super().setUp()
        self.user = get_user_model().objects.create_user(username="test")
        self.project = Project.objects.create(name="Stacktrace view tests")
        ProjectMembership.objects.create(project=self.project, user=self.user, accepted=True)
        self.client.force_login(self.user)

    def render_stacktrace(self, event_data):
        # create_event bypasses ingestion; fake the denormalized fields that digest_event would have stored.
        calculated_type, calculated_value = get_type_and_value_for_data(event_data)
        event = create_event(
            project=self.project,
            event_data=event_data,
            platform=event_data["platform"],
            calculated_type=calculated_type,
            calculated_value=calculated_value,
        )
        return self.client.get(f"/issues/issue/{event.issue.id}/event/{event.id}/")

    def render_frame_from_sample(self, frame_index):
        event_data = load_sample("bugsink/frames-with-missing-info.json")
        frames = event_data["exception"]["values"][0]["stacktrace"]["frames"]
        event_data["exception"]["values"][0]["stacktrace"]["frames"] = [frames[frame_index]]
        return self.render_stacktrace(event_data)

    def test_event_without_stacktrace_says_none_is_available(self):
        response = self.render_stacktrace({
            "platform": "python",
        })

        self.assertContains(response, "No stacktrace available for this event.")

    def test_exception_displays_type_and_value(self):
        response = self.render_stacktrace({
            "platform": "python",
            "exception": {
                "values": [{
                    "type": "ExampleError",
                    "value": "Something went wrong",
                }],
            },
        })

        self.assertContains(response, "ExampleError")
        self.assertContains(response, "Something went wrong")

    def test_frame_displays_location(self):
        response = self.render_stacktrace({
            "platform": "python",
            "exception": {
                "values": [{
                    "type": "ExampleError",
                    "value": "Something went wrong",
                    "stacktrace": {
                        "frames": [{
                            "filename": "example.py",
                            "function": "do_work",
                            "lineno": 421337,
                            "in_app": True,
                        }],
                    },
                }],
            },
        })

        self.assertContains(response, "example.py")
        self.assertContains(response, "do_work")
        self.assertContains(response, "421337")

    @tag("samples")
    def test_frame_displays_source_context_and_variables(self):
        response = self.render_frame_from_sample(0)
        text = visible_text(response)

        self.assertIn("main()", text)
        self.assertIn("Variable", text)
        self.assertIn("__name__", text)
        self.assertNotIn("No code context or variables available for this frame.", text)

    @tag("samples")
    def test_frame_without_source_context_displays_variables(self):
        response = self.render_frame_from_sample(1)
        text = visible_text(response)

        self.assertIn("execute_from_command_line", text)
        self.assertNotIn("No code context or variables available for this frame.", text)

    @tag("samples")
    def test_frame_without_variables_displays_source_context(self):
        response = self.render_frame_from_sample(2)
        text = visible_text(response)

        self.assertIn("utility.execute()", text)
        self.assertNotIn("Variable", text)
        self.assertNotIn("No code context or variables available for this frame.", text)

    @tag("samples")
    def test_frame_without_source_context_or_variables_says_so(self):
        response = self.render_frame_from_sample(3)

        self.assertContains(response, "No code context or variables available for this frame.")

    def test_python_frames_are_displayed_in_payload_order(self):
        response = self.render_stacktrace({
            "platform": "python",
            "exception": {
                "values": [{
                    "type": "ExampleError",
                    "value": "Something went wrong",
                    "stacktrace": {
                        "frames": [
                            {"filename": "call_started.py"},
                            {"filename": "error_raised.py"},
                        ],
                    },
                }],
            },
        })

        content = response.content.decode()
        self.assertLess(content.index("call_started.py"), content.index("error_raised.py"))

    def test_non_python_frames_are_displayed_in_reverse_payload_order(self):
        response = self.render_stacktrace({
            "platform": "java",
            "exception": {
                "values": [{
                    "type": "ExampleError",
                    "value": "Something went wrong",
                    "stacktrace": {
                        "frames": [
                            {"filename": "CallStarted.java"},
                            {"filename": "ErrorRaised.java"},
                        ],
                    },
                }],
            },
        })

        content = response.content.decode()
        self.assertLess(content.index("ErrorRaised.java"), content.index("CallStarted.java"))

    def test_exception_stacktrace_marks_beginning_and_raise(self):
        response = self.render_stacktrace({
            "platform": "python",
            "exception": {
                "values": [{
                    "type": "ExampleError",
                    "value": "Something went wrong",
                    "stacktrace": {
                        "frames": [
                            {"filename": "call_started.py"},
                            {"filename": "error_raised.py"},
                        ],
                    },
                }],
            },
        })

        content = response.content.decode()
        positions = [
            content.index("call_started.py"),
            content.index("\u2192 begin"),

            content.index("error_raised.py"),
            content.index("raise ExampleError"),
        ]
        self.assertEqual(positions, sorted(positions))

    def test_handled_exception_stacktrace_marks_try_and_handled_raise(self):
        response = self.render_stacktrace({
            "platform": "python",
            "exception": {
                "values": [
                    {
                        "type": "HandledError",
                        "value": "The first failure",
                        "stacktrace": {
                            "frames": [
                                {"filename": "handled_call.py"},
                                {"filename": "handled_raise.py"},
                            ],
                        },
                    },
                    {
                        "type": "FinalError",
                        "value": "The second failure",
                        "stacktrace": {
                            "frames": [
                                {"filename": "final_call.py"},
                                {"filename": "final_raise.py"},
                            ],
                        },
                    },
                ],
            },
        })

        content = response.content.decode()
        positions = [
            content.index("handled_call.py"),
            content.index("try\u2026"),

            content.index("handled_raise.py"),
            content.index("raise HandledError (handled)"),

            content.index("final_call.py"),
            content.index("\u2192 begin"),

            content.index("final_raise.py"),
            content.index("raise FinalError"),
        ]
        self.assertEqual(positions, sorted(positions))

    @tag("samples")
    def test_multiple_thread_snapshots_are_displayed_as_sections(self):
        event_data = load_sample("generated/sentry-java-capture-message-attach-threads.json")

        text = visible_text(self.render_stacktrace(event_data))
        message = "capture_message with all Java threads from ProbeMultipleThreads.java"

        self.assertEqual(1, text.count(message))
        self.assertLess(text.index(message), text.index("Thread #1: main"))
        self.assertIn("Thread #1: main", text)
        self.assertIn("Thread #18: Notification Thread", text)
        self.assertIn("Thread #22: bugsink-probe-background", text)
        self.assertIn("ProbeMultipleThreads.java in main line 27", text)
        self.assertIn("ProbeMultipleThreads.java in lambda$main$1 line 19", text)
        self.assertIn("errored: yes", text)
        self.assertIn("state: Runnable", text)
        self.assertNotIn("was active:", text)
        self.assertEqual(2, text.count("No stacktrace found."))
        self.assertNotIn("raise Log Message", text)
        self.assertNotIn("During handling of the above exception", text)

    @tag("samples")
    def test_exception_event_displays_thread_snapshots_after_exception(self):
        event_data = load_sample("generated/sentry-java-capture-exception-attach-threads.json")

        text = visible_text(self.render_stacktrace(event_data))

        self.assertIn("IllegalStateException", text)
        self.assertIn("capture_exception with all Java threads from ProbeExceptionMultipleThreads.java", text)
        self.assertIn("raise IllegalStateException", text)
        self.assertIn("ProbeExceptionMultipleThreads.java in captureException line 35", text)
        self.assertLess(text.index("IllegalStateException"), text.index("Threads"))
        self.assertIn("Thread #1: main", text)
        self.assertIn("Thread #22: bugsink-probe-background", text)
        self.assertIn("ProbeExceptionMultipleThreads.java in captureException line 38", text)
        self.assertIn("ProbeExceptionMultipleThreads.java in lambda$main$1 line 19", text)
        self.assertEqual(2, text.count("No stacktrace found."))
        self.assertNotIn("During handling of the above exception", text)

    def test_python_exception_chain_is_displayed_in_payload_order(self):
        response = self.render_stacktrace({
            "platform": "python",
            "exception": {
                "values": [
                    {
                        "type": "HandledError",
                        "value": "The first failure",
                    },
                    {
                        "type": "FinalError",
                        "value": "The second failure",
                    },
                ],
            },
        })

        content = response.content.decode()
        separator = "During handling of the above exception another exception occurred or was intentionally reraised:"
        self.assertLess(content.index("The first failure"), content.index(separator))
        self.assertLess(content.index(separator), content.index("The second failure"))

    def test_non_python_exception_chain_is_displayed_in_reverse_payload_order(self):
        # Less realistic than a Java sample, but identical inputs make the ordering difference easier to inspect.
        response = self.render_stacktrace({
            "platform": "java",
            "exception": {
                "values": [
                    {
                        "type": "HandledError",
                        "value": "The first failure",
                    },
                    {
                        "type": "FinalError",
                        "value": "The second failure",
                    },
                ],
            },
        })

        content = response.content.decode()
        separator = (
            "The above exception was caused by or intentially reraised during the handling of the following exception:"
        )
        self.assertLess(content.index("The second failure"), content.index(separator))
        self.assertLess(content.index(separator), content.index("The first failure"))