Files
2024-02-15 17:01:50 +00:00

487 lines
18 KiB
Python

from unittest.mock import patch
import pytest
from sentry.integrations.utils.code_mapping import (
CodeMapping,
CodeMappingTreesHelper,
FrameFilename,
Repo,
RepoTree,
UnsupportedFrameFilename,
convert_stacktrace_frame_path_to_source_path,
filter_source_code_files,
get_extension,
get_sorted_code_mapping_configs,
should_include,
stacktrace_buckets,
)
from sentry.models.integrations.organization_integration import OrganizationIntegration
from sentry.silo.base import SiloMode
from sentry.testutils.cases import TestCase
from sentry.testutils.silo import assume_test_silo_mode
from sentry.utils.event_frames import EventFrame
sentry_files = [
"bin/__init__.py",
"bin/example1.py",
"bin/example2.py",
"docs-ui/.eslintrc.js",
"src/sentry/identity/oauth2.py",
"src/sentry/integrations/slack/client.py",
"src/sentry/web/urls.py",
"src/sentry/wsgi.py",
"src/sentry_plugins/slack/client.py",
]
UNSUPPORTED_FRAME_FILENAMES = [
"async https://s1.sentry-cdn.com/_static/dist/sentry/entrypoints/app.js",
"/gtm.js", # Top source; starts with backslash
"<anonymous>",
"<frozen importlib._bootstrap>",
"[native code]",
"O$t",
"async https://s1.sentry-cdn.com/_static/dist/sentry/entrypoints/app.js",
"/foo/bar/baz", # no extension
"README", # no extension
"ssl.py",
# XXX: The following will need to be supported
"C:\\Users\\Donia\\AppData\\Roaming\\Adobe\\UXP\\Plugins\\External\\452f92d2_0.13.0\\main.js",
"initialization.dart",
"backburner.js",
]
class TestRepoFiles(TestCase):
"""These evaluate which files should be included as part of a repo."""
def test_filter_source_code_files(self):
source_code_files = filter_source_code_files(sentry_files)
assert source_code_files.index("bin/__init__.py") == 0
assert source_code_files.index("docs-ui/.eslintrc.js") == 3
with pytest.raises(ValueError):
source_code_files.index("README.md")
def test_filter_source_code_files_not_supported(self):
source_code_files = filter_source_code_files([])
assert source_code_files == []
source_code_files = filter_source_code_files([".env", "README"])
assert source_code_files == []
def test_should_not_include(self):
for file in [
"static/app/views/organizationRoot.spec.jsx",
"tests/foo.py",
]:
assert should_include(file) is False
def test_get_extension():
assert get_extension("") == ""
assert get_extension("f.py") == "py"
assert get_extension("f.xx") == "xx"
assert get_extension("./app/utils/handleXhrErrorResponse.tsx") == "tsx"
assert get_extension("[native code]") == ""
assert get_extension("/foo/bar/baz") == ""
assert get_extension("/gtm.js") == "js"
def test_buckets_logic():
stacktraces = [
"app://foo.js",
"./app/utils/handleXhrErrorResponse.tsx",
"getsentry/billing/tax/manager.py",
"/cronscripts/monitoringsync.php",
] + UNSUPPORTED_FRAME_FILENAMES
buckets = stacktrace_buckets(stacktraces)
assert buckets == {
"./app": [FrameFilename("./app/utils/handleXhrErrorResponse.tsx")],
"app:": [FrameFilename("app://foo.js")],
"cronscripts": [FrameFilename("/cronscripts/monitoringsync.php")],
"getsentry": [FrameFilename("getsentry/billing/tax/manager.py")],
}
class TestFrameFilename:
def test_frame_filename_package_and_more_than_one_level(self):
ff = FrameFilename("getsentry/billing/tax/manager.py")
assert f"{ff.root}/{ff.dir_path}/{ff.file_name}" == "getsentry/billing/tax/manager.py"
assert f"{ff.dir_path}/{ff.file_name}" == ff.file_and_dir_path
def test_frame_filename_package_and_no_levels(self):
ff = FrameFilename("root/bar.py")
assert f"{ff.root}/{ff.file_name}" == "root/bar.py"
assert f"{ff.root}/{ff.file_and_dir_path}" == "root/bar.py"
assert ff.dir_path == ""
def test_frame_filename_repr(self):
path = "getsentry/billing/tax/manager.py"
assert FrameFilename(path).__repr__() == f"FrameFilename: {path}"
def test_raises_unsupported(self):
for filepath in UNSUPPORTED_FRAME_FILENAMES:
with pytest.raises(UnsupportedFrameFilename):
FrameFilename(filepath)
class TestDerivedCodeMappings(TestCase):
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog):
self._caplog = caplog
def setUp(self):
super().setUp()
self.foo_repo = Repo("Test-Organization/foo", "master")
self.bar_repo = Repo("Test-Organization/bar", "main")
self.code_mapping_helper = CodeMappingTreesHelper(
{
self.foo_repo.name: RepoTree(self.foo_repo, files=sentry_files),
self.bar_repo.name: RepoTree(self.bar_repo, files=["sentry/web/urls.py"]),
}
)
self.expected_code_mappings = [
CodeMapping(repo=self.foo_repo, stacktrace_root="sentry/", source_path="src/sentry/"),
CodeMapping(
repo=self.foo_repo,
stacktrace_root="sentry_plugins/",
source_path="src/sentry_plugins/",
),
]
def test_package_also_matches(self):
repo_tree = RepoTree(self.foo_repo, files=["apostello/views/base.py"])
# We create a new tree helper in order to improve the understability of this test
cmh = CodeMappingTreesHelper({self.foo_repo.name: repo_tree})
cm = cmh._generate_code_mapping_from_tree(
repo_tree=repo_tree, frame_filename=FrameFilename("raven/base.py")
)
# We should not derive a code mapping since the package name does not match
assert cm == []
def test_no_matches(self):
stacktraces = [
"getsentry/billing/tax/manager.py",
"requests/models.py",
"urllib3/connectionpool.py",
"ssl.py",
]
code_mappings = self.code_mapping_helper.generate_code_mappings(stacktraces)
assert code_mappings == []
@patch("sentry.integrations.utils.code_mapping.logger")
def test_matches_top_src_file(self, logger):
stacktraces = ["setup.py"]
code_mappings = self.code_mapping_helper.generate_code_mappings(stacktraces)
assert code_mappings == []
def test_no_dir_depth_match(self):
code_mappings = self.code_mapping_helper.generate_code_mappings(["sentry/wsgi.py"])
assert code_mappings == [
CodeMapping(
repo=Repo(name="Test-Organization/foo", branch="master"),
stacktrace_root="sentry/",
source_path="src/sentry/",
)
]
def test_more_than_one_match_does_derive(self):
stacktraces = [
# More than one file matches for this, however, the package name is taken into account
# - "src/sentry_plugins/slack/client.py",
# - "src/sentry/integrations/slack/client.py",
"sentry_plugins/slack/client.py",
]
code_mappings = self.code_mapping_helper.generate_code_mappings(stacktraces)
assert code_mappings == [
CodeMapping(
repo=self.foo_repo,
stacktrace_root="sentry_plugins/",
source_path="src/sentry_plugins/",
)
]
def test_no_stacktraces_to_process(self):
code_mappings = self.code_mapping_helper.generate_code_mappings([])
assert code_mappings == []
def test_more_than_one_match_works_when_code_mapping_excludes_other_match(self):
stacktraces = [
"sentry/identity/oauth2.py",
"sentry_plugins/slack/client.py",
]
code_mappings = self.code_mapping_helper.generate_code_mappings(stacktraces)
assert code_mappings == self.expected_code_mappings
def test_more_than_one_match_works_with_different_order(self):
stacktraces = [
# This file matches twice files in the repo, however, the reprocessing
# feature allows deriving both code mappings
"sentry_plugins/slack/client.py",
"sentry/identity/oauth2.py",
]
code_mappings = self.code_mapping_helper.generate_code_mappings(stacktraces)
assert sorted(code_mappings) == sorted(self.expected_code_mappings)
@patch("sentry.integrations.utils.code_mapping.logger")
def test_more_than_one_repo_match(self, logger):
# XXX: There's a chance that we could infer package names but that is risky
# repo 1: src/sentry/web/urls.py
# repo 2: sentry/web/urls.py
stacktraces = ["sentry/web/urls.py"]
code_mappings = self.code_mapping_helper.generate_code_mappings(stacktraces)
# The file appears in more than one repo, thus, we are unable to determine the code mapping
assert code_mappings == []
logger.warning.assert_called_with("More than one repo matched %s", "sentry/web/urls.py")
def test_list_file_matches_single(self):
frame_filename = FrameFilename("sentry_plugins/slack/client.py")
matches = self.code_mapping_helper.list_file_matches(frame_filename)
expected_matches = [
{
"filename": "src/sentry_plugins/slack/client.py",
"repo_name": "Test-Organization/foo",
"repo_branch": "master",
"stacktrace_root": "sentry_plugins/",
"source_path": "src/sentry_plugins/",
}
]
assert matches == expected_matches
def test_list_file_matches_multiple(self):
frame_filename = FrameFilename("sentry/web/urls.py")
matches = self.code_mapping_helper.list_file_matches(frame_filename)
expected_matches = [
{
"filename": "src/sentry/web/urls.py",
"repo_name": "Test-Organization/foo",
"repo_branch": "master",
"stacktrace_root": "sentry/",
"source_path": "src/sentry/",
},
{
"filename": "sentry/web/urls.py",
"repo_name": "Test-Organization/bar",
"repo_branch": "main",
"stacktrace_root": "sentry/",
"source_path": "sentry/",
},
]
assert matches == expected_matches
def test_normalized_stack_and_source_roots_starts_with_period_slash(self):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"./app/", "static/app/"
)
assert stacktrace_root == "./"
assert source_path == "static/"
def test_normalized_stack_and_source_roots_starts_with_period_slash_no_containing_directory(
self,
):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"./app/", "app/"
)
assert stacktrace_root == "./"
assert source_path == ""
def test_normalized_stack_and_source_not_matching(self):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"sentry/", "src/sentry/"
)
assert stacktrace_root == "sentry/"
assert source_path == "src/sentry/"
def test_normalized_stack_and_source_roots_equal(self):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"source/", "source/"
)
assert stacktrace_root == ""
assert source_path == ""
def test_normalized_stack_and_source_roots_starts_with_period_slash_two_levels(self):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"./app/", "app/foo/app/"
)
assert stacktrace_root == "./"
assert source_path == "app/foo/"
def test_normalized_stack_and_source_roots_starts_with_app(self):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"app:///utils/", "utils/"
)
assert stacktrace_root == "app:///"
assert source_path == ""
def test_normalized_stack_and_source_roots_starts_with_multiple_dot_dot_slash(self):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"../../../../../../packages/", "packages/"
)
assert stacktrace_root == "../../../../../../"
assert source_path == ""
def test_normalized_stack_and_source_roots_starts_with_app_dot_dot_slash(self):
stacktrace_root, source_path = self.code_mapping_helper._normalized_stack_and_source_roots(
"app:///../services/", "services/"
)
assert stacktrace_root == "app:///../"
assert source_path == ""
class TestConvertStacktraceFramePathToSourcePath(TestCase):
def setUp(self):
super()
self.integration, self.oi = self.create_provider_integration_for(
self.organization, self.user, provider="example", name="Example"
)
self.repo = self.create_repo(
project=self.project,
name="getsentry/sentry",
)
self.code_mapping_empty = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="",
source_root="src/",
)
self.code_mapping_abs_path = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="/Users/Foo/src/sentry/",
source_root="src/sentry/",
)
self.code_mapping_file = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="sentry/",
source_root="src/sentry/",
)
def test_convert_stacktrace_frame_path_to_source_path_empty(self):
assert (
convert_stacktrace_frame_path_to_source_path(
frame=EventFrame(filename="sentry/file.py"),
code_mapping=self.code_mapping_empty,
platform="python",
sdk_name="sentry.python",
)
== "src/sentry/file.py"
)
def test_convert_stacktrace_frame_path_to_source_path_abs_path(self):
assert (
convert_stacktrace_frame_path_to_source_path(
frame=EventFrame(
filename="file.py", abs_path="/Users/Foo/src/sentry/folder/file.py"
),
code_mapping=self.code_mapping_abs_path,
platform="python",
sdk_name="sentry.python",
)
== "src/sentry/folder/file.py"
)
def test_convert_stacktrace_frame_path_to_source_path_java(self):
assert (
convert_stacktrace_frame_path_to_source_path(
frame=EventFrame(filename="File.java", module="sentry.module.File"),
code_mapping=self.code_mapping_file,
platform="java",
sdk_name="sentry.java",
)
== "src/sentry/module/File.java"
)
class TestGetSortedCodeMappingConfigs(TestCase):
def setUp(self):
super()
with assume_test_silo_mode(SiloMode.CONTROL):
self.integration = self.create_provider_integration(provider="example", name="Example")
self.integration.add_organization(self.organization, self.user)
self.oi = OrganizationIntegration.objects.get(integration_id=self.integration.id)
self.repo = self.create_repo(
project=self.project,
name="getsentry/sentry",
)
self.repo.integration_id = self.integration.id
self.repo.provider = "example"
self.repo.save()
def test_get_sorted_code_mapping_configs(self):
# Created by the user, not well defined stack root
code_mapping1 = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="",
source_root="",
automatically_generated=False,
)
# Created by automation, not as well defined stack root
code_mapping2 = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="usr/src/getsentry/src/",
source_root="",
automatically_generated=True,
)
# Created by the user, well defined stack root
code_mapping3 = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="usr/src/getsentry/",
source_root="",
automatically_generated=False,
)
# Created by the user, not as well defined stack root
code_mapping4 = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="usr/src/",
source_root="",
automatically_generated=False,
)
# Created by automation, well defined stack root
code_mapping5 = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="usr/src/getsentry/src/sentry/",
source_root="",
automatically_generated=True,
)
# Created by user, well defined stack root that references abs_path
code_mapping6 = self.create_code_mapping(
organization_integration=self.oi,
project=self.project,
repo=self.repo,
stack_root="/Users/User/code/src/getsentry/src/sentry/",
source_root="",
automatically_generated=False,
)
# Expected configs: stack_root, automatically_generated
expected_config_order = [
code_mapping6, # "/Users/User/code/src/getsentry/src/sentry/", False
code_mapping3, # "usr/src/getsentry/", False
code_mapping4, # "usr/src/", False
code_mapping1, # "", False
code_mapping5, # "usr/src/getsentry/src/sentry/", True
code_mapping2, # "usr/src/getsentry/src/", True
]
sorted_configs = get_sorted_code_mapping_configs(self.project)
assert sorted_configs == expected_config_order