From 915b7af8c9a70739f70f4105680f8f795690c45c Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Thu, 15 May 2025 00:07:03 -0300 Subject: [PATCH 01/13] test(utils): Add test for `datetime_from_isoformat` Fixes GH-3515 --- tests/test_utils.py | 101 ++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b731c3e3ab..71c1063028 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -61,55 +61,72 @@ def _normalize_distribution_name(name): return re.sub(r"[-_.]+", "-", name).lower() -@pytest.mark.parametrize( - ("input_str", "expected_output"), +isoformat_inputs_and_datetime_outputs = ( ( - ( - "2021-01-01T00:00:00.000000Z", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), # UTC time - ( - "2021-01-01T00:00:00.000000", - datetime(2021, 1, 1).astimezone(timezone.utc), - ), # No TZ -- assume local but convert to UTC - ( - "2021-01-01T00:00:00Z", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), # UTC - No milliseconds - ( - "2021-01-01T00:00:00.000000+00:00", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2021-01-01T00:00:00.000000-00:00", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2021-01-01T00:00:00.000000+0000", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2021-01-01T00:00:00.000000-0000", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2020-12-31T00:00:00.000000+02:00", - datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))), - ), # UTC+2 time - ( - "2020-12-31T00:00:00.000000-0200", - datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), - ), # UTC-2 time - ( - "2020-12-31T00:00:00-0200", - datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), - ), # UTC-2 time - no milliseconds + "2021-01-01T00:00:00.000000Z", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), # UTC time + ( + "2021-01-01T00:00:00.000000", + datetime(2021, 1, 1).astimezone(timezone.utc), + ), # No TZ -- assume local but convert to UTC + ( + "2021-01-01T00:00:00Z", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), # UTC - No milliseconds + ( + "2021-01-01T00:00:00.000000+00:00", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), + ( + "2021-01-01T00:00:00.000000-00:00", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), + ( + "2021-01-01T00:00:00.000000+0000", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), + ( + "2021-01-01T00:00:00.000000-0000", + datetime(2021, 1, 1, tzinfo=timezone.utc), ), + ( + "2020-12-31T00:00:00.000000+02:00", + datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))), + ), # UTC+2 time + ( + "2020-12-31T00:00:00.000000-0200", + datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), + ), # UTC-2 time + ( + "2020-12-31T00:00:00-0200", + datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), + ), # UTC-2 time - no milliseconds +) + + +@pytest.mark.parametrize( + ("input_str", "expected_output"), + isoformat_inputs_and_datetime_outputs, ) def test_datetime_from_isoformat(input_str, expected_output): assert datetime_from_isoformat(input_str) == expected_output, input_str +@pytest.mark.parametrize( + ("input_str", "expected_output"), + isoformat_inputs_and_datetime_outputs, +) +def test_datetime_from_isoformat_with_py_36_or_lower(input_str, expected_output): + """ + `fromisoformat` was added in Python version 3.7 + """ + with mock.patch("sentry_sdk.utils.datetime") as datetime_mocked: + datetime_mocked.fromisoformat.side_effect = AttributeError() + datetime_mocked.strptime = datetime.strptime + assert datetime_from_isoformat(input_str) == expected_output, input_str + + @pytest.mark.parametrize( "env_var_value,strict,expected", [ From cdfaac7638e9a272b790ccf237443baab2f52f1f Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Thu, 15 May 2025 11:26:27 -0300 Subject: [PATCH 02/13] test(utils): Add test for `safe_str` when it fails Fixes GH-3515 --- tests/test_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 71c1063028..6fc756028e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -646,6 +646,17 @@ def test_get_error_message(error, expected_result): assert get_error_message(exc_value) == expected_result(exc_value) +def test_safe_str_fails(): + class ExplodingStr: + def __str__(self): + raise Exception + + obj = ExplodingStr() + result = safe_str(obj) + + assert result == repr(obj) + + def test_installed_modules(): try: from importlib.metadata import distributions, version From fa199277ab1a2e453bee1325a226804627038625 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Thu, 15 May 2025 16:28:03 -0300 Subject: [PATCH 03/13] test(attachments): Complete 100% coverage for `Attachment` Fixes GH-3515 --- tests/test_basics.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/test_basics.py b/tests/test_basics.py index 0fdf9f811f..2eeba78216 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -515,6 +515,66 @@ def test_attachments_graceful_failure( assert envelope.items[1].payload.get_bytes() == b"" +def test_attachments_exceptions(sentry_init): + sentry_init() + + scope = sentry_sdk.get_isolation_scope() + + # bytes and path are None + with pytest.raises(TypeError) as e: + scope.add_attachment() + + assert str(e.value) == "path or raw bytes required for attachment" + + # filename is None + with pytest.raises(TypeError) as e: + scope.add_attachment(bytes=b"Hello World!") + + assert str(e.value) == "filename is required for attachment" + + +def test_attachments_content_type_is_none(sentry_init, capture_envelopes): + sentry_init() + envelopes = capture_envelopes() + + scope = sentry_sdk.get_isolation_scope() + + scope.add_attachment( + bytes=b"Hello World!", filename="message.txt", content_type="foo/bar" + ) + capture_exception(ValueError()) + + (envelope,) = envelopes + attachments = [x for x in envelope.items if x.type == "attachment"] + (message,) = attachments + + assert message.headers["filename"] == "message.txt" + assert message.headers["content_type"] == "foo/bar" + + +def test_attachments_repr(sentry_init): + sentry_init() + + scope = sentry_sdk.get_isolation_scope() + + scope.add_attachment(bytes=b"Hello World!", filename="message.txt") + + assert repr(scope._attachments[0]) == "" + + +def test_attachments_bytes_callable_payload(sentry_init): + sentry_init() + + scope = sentry_sdk.get_isolation_scope() + + scope.add_attachment(bytes=bytes, filename="message.txt") + + attachment = scope._attachments[0] + item = attachment.to_envelope_item() + + assert item.payload.bytes == b"" + + def test_integration_scoping(sentry_init, capture_events): logger = logging.getLogger("test_basics") From 0b57e7b6707c4acd5420e335fcfc7d383724724c Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Fri, 16 May 2025 01:52:40 -0300 Subject: [PATCH 04/13] test(utils): Add test for `strip_string` Fixes: GH-3515 --- tests/utils/test_general.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index 1b689ec735..a17d552046 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -578,6 +578,17 @@ def test_failed_base64_conversion(input): value="é...", metadata={"len": 8, "rem": [["!limit", "x", 2, 5]]} ), ], + [ + "\udfff\udfff\udfff\udfff\udfff\udfff", + 5, + AnnotatedValue( + value="\udfff\udfff...", + metadata={ + "len": 6, + "rem": [["!limit", "x", 5 - 3, 5]], + }, + ), + ], ], ) def test_strip_string(input, max_length, result): From b46e6d1dfb57cbd3f170febdd2928010bd668c2a Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Fri, 16 May 2025 13:07:17 -0300 Subject: [PATCH 05/13] test(utils): Add test for `_make_threadlocal_contextvars` Fixes: GH-3515 --- tests/utils/test_contextvars.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py index a6d296bb1f..cefa4c13fd 100644 --- a/tests/utils/test_contextvars.py +++ b/tests/utils/test_contextvars.py @@ -1,10 +1,10 @@ import pytest import random import time +from unittest import mock -@pytest.mark.forked -def test_leaks(maybe_monkeypatched_threading): +def _run_contextvar_threaded_test(): import threading # Need to explicitly call _get_contextvars because the SDK has already @@ -39,3 +39,14 @@ def run(): t.join() assert len(success) == 20 + + +@pytest.mark.forked +def test_leaks(maybe_monkeypatched_threading): + _run_contextvar_threaded_test() + + +@pytest.mark.forked +@mock.patch("sentry_sdk.utils._is_contextvars_broken", return_value=True) +def test_leaks_when_is_contextvars_broken_is_false(maybe_monkeypatched_threading): + _run_contextvar_threaded_test() From a2e9dd3a9d03e3ea6c8c9f7e474aaddcb63947ba Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Fri, 16 May 2025 14:53:44 -0300 Subject: [PATCH 06/13] test(utils): Add test for `Dsn` validations Fixes: GH-3515 --- tests/utils/test_general.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index a17d552046..c95fd46d2c 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -120,6 +120,21 @@ def test_parse_invalid_dsn(dsn): dsn = Dsn(dsn) +@pytest.mark.parametrize( + "dsn,error_message", + [ + ("foo://barbaz@sentry.io", "Unsupported scheme 'foo'"), + ("https://foobar@", "Missing hostname"), + ("https://@sentry.io", "Missing public key"), + ], +) +def test_dsn_validations(dsn, error_message): + with pytest.raises(BadDsn) as e: + dsn = Dsn(dsn) + + assert str(e.value) == error_message + + @pytest.mark.parametrize( "frame,in_app_include,in_app_exclude,project_root,resulting_frame", [ From 98cf1fc559a7a5390067225a13a5398bc6b3abef Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sun, 18 May 2025 15:13:26 -0300 Subject: [PATCH 07/13] test(utils): Add test for `to_string` Fixes: GH-3515 --- tests/test_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 6fc756028e..67a8ac54d7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -32,6 +32,7 @@ _get_installed_modules, _generate_installed_modules, ensure_integration_enabled, + to_string, ) @@ -1001,3 +1002,13 @@ def test_function(): ... sentry_sdk.utils.qualname_from_function(test_function) == "test_qualname_from_function_none_name..test_function" ) + + +def test_to_string_unicode_decode_error(): + class BadStr: + def __str__(self): + raise UnicodeDecodeError("utf-8", b"", 0, 1, "reason") + + obj = BadStr() + result = to_string(obj) + assert result == repr(obj)[1:-1] From 202caa33a249fe95a1f2f3ea2dd20980f3798117 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sun, 18 May 2025 17:22:15 -0300 Subject: [PATCH 08/13] test(utils): Add test for `get_default_release` Fixes: GH-3515 --- tests/test_utils.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 67a8ac54d7..d61eb341cc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -741,6 +741,20 @@ def test_default_release_empty_string(): assert release is None +def test_get_default_release_sentry_release_env(monkeypatch): + monkeypatch.setenv("SENTRY_RELEASE", "sentry-env-release") + assert get_default_release() == "sentry-env-release" + + +def test_get_default_release_other_release_env(monkeypatch): + monkeypatch.setenv("SOURCE_VERSION", "other-env-release") + + with mock.patch("sentry_sdk.utils.get_git_revision", return_value=""): + release = get_default_release() + + assert release == "other-env-release" + + def test_ensure_integration_enabled_integration_enabled(sentry_init): def original_function(): return "original" From e4ccac45fa42efde255c817acce10a066f4238ab Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sun, 18 May 2025 21:01:09 -0300 Subject: [PATCH 09/13] test(utils): Add test for `filename_for_module` Fixes: GH-3515 --- tests/utils/test_general.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index c95fd46d2c..03182495de 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -74,13 +74,26 @@ def test_filename(): assert x("bogus", "bogus") == "bogus" + assert x("bogus", "bogus.pyc") == "bogus.py" + assert x("os", os.__file__) == "os.py" + assert x("foo.bar", "path/to/foo/bar.py") == "path/to/foo/bar.py" + import sentry_sdk.utils assert x("sentry_sdk.utils", sentry_sdk.utils.__file__) == "sentry_sdk/utils.py" +def test_filename_module_file_is_none(): + class DummyModule: + __file__ = None + + os.sys.modules["foo"] = DummyModule() + + assert filename_for_module("foo.bar", "path/to/foo/bar.py") == "path/to/foo/bar.py" + + @pytest.mark.parametrize( "given,expected_envelope", [ From 1b868c8128c7dc486d5eeebac158f1977d0856f1 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sun, 18 May 2025 21:25:33 -0300 Subject: [PATCH 10/13] test(utils): Add test for `exc_info_from_error` Fixes: GH-3515 --- tests/test_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index d61eb341cc..eef10977b3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -33,6 +33,7 @@ _generate_installed_modules, ensure_integration_enabled, to_string, + exc_info_from_error, ) @@ -1026,3 +1027,13 @@ def __str__(self): obj = BadStr() result = to_string(obj) assert result == repr(obj)[1:-1] + + +def test_exc_info_from_error_dont_get_an_exc(): + class NotAnException: + pass + + with pytest.raises(ValueError) as exc: + exc_info_from_error(NotAnException()) + + assert "Expected Exception object to report, got Date: Mon, 19 May 2025 00:29:39 -0300 Subject: [PATCH 11/13] test(utils): Add test for `get_lines_from_file` Fixes: GH-3515 --- tests/test_utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index eef10977b3..31c676c83e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -34,6 +34,7 @@ ensure_integration_enabled, to_string, exc_info_from_error, + get_lines_from_file, ) @@ -1037,3 +1038,31 @@ class NotAnException: exc_info_from_error(NotAnException()) assert "Expected Exception object to report, got Date: Mon, 19 May 2025 00:47:47 -0300 Subject: [PATCH 12/13] test(utils): Add test for `sanitize_url` Fixes: GH-3515 --- tests/test_utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 31c676c83e..4f72032a9c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -366,6 +366,12 @@ def test_sanitize_url_and_split(url, expected_result): assert sanitized_url.fragment == expected_result.fragment +def test_sanitize_url_remove_authority_is_false(): + url = "https://usr:pwd@example.com" + sanitized_url = sanitize_url(url, remove_authority=False) + assert sanitized_url == url + + @pytest.mark.parametrize( ("url", "sanitize", "expected_url", "expected_query", "expected_fragment"), [ From 3d983642bda3d204abe67bfa3c22564155418eed Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Mon, 19 May 2025 00:59:21 -0300 Subject: [PATCH 13/13] test(utils): Add test for `package_version` Fixes: GH-3515 --- tests/test_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4f72032a9c..efa2e7c068 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -35,6 +35,7 @@ to_string, exc_info_from_error, get_lines_from_file, + package_version, ) @@ -1072,3 +1073,7 @@ def fake_getlines(filename): with mock.patch("sentry_sdk.utils.linecache.getlines", fake_getlines): result = get_lines_from_file("filename", 10) assert result == expected_result + + +def test_package_version_is_none(): + assert package_version("non_existent_package") is None