Skip to content

Commit 94414cd

Browse files
Increase test coverage (#4393)
Fixes GH-3515 Increase test coverage for `attachments.py` and `utils.py`. --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent 83e0386 commit 94414cd

File tree

4 files changed

+258
-44
lines changed

4 files changed

+258
-44
lines changed

tests/test_basics.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,66 @@ def test_attachments_graceful_failure(
515515
assert envelope.items[1].payload.get_bytes() == b""
516516

517517

518+
def test_attachments_exceptions(sentry_init):
519+
sentry_init()
520+
521+
scope = sentry_sdk.get_isolation_scope()
522+
523+
# bytes and path are None
524+
with pytest.raises(TypeError) as e:
525+
scope.add_attachment()
526+
527+
assert str(e.value) == "path or raw bytes required for attachment"
528+
529+
# filename is None
530+
with pytest.raises(TypeError) as e:
531+
scope.add_attachment(bytes=b"Hello World!")
532+
533+
assert str(e.value) == "filename is required for attachment"
534+
535+
536+
def test_attachments_content_type_is_none(sentry_init, capture_envelopes):
537+
sentry_init()
538+
envelopes = capture_envelopes()
539+
540+
scope = sentry_sdk.get_isolation_scope()
541+
542+
scope.add_attachment(
543+
bytes=b"Hello World!", filename="message.txt", content_type="foo/bar"
544+
)
545+
capture_exception(ValueError())
546+
547+
(envelope,) = envelopes
548+
attachments = [x for x in envelope.items if x.type == "attachment"]
549+
(message,) = attachments
550+
551+
assert message.headers["filename"] == "message.txt"
552+
assert message.headers["content_type"] == "foo/bar"
553+
554+
555+
def test_attachments_repr(sentry_init):
556+
sentry_init()
557+
558+
scope = sentry_sdk.get_isolation_scope()
559+
560+
scope.add_attachment(bytes=b"Hello World!", filename="message.txt")
561+
562+
assert repr(scope._attachments[0]) == "<Attachment 'message.txt'>"
563+
564+
565+
def test_attachments_bytes_callable_payload(sentry_init):
566+
sentry_init()
567+
568+
scope = sentry_sdk.get_isolation_scope()
569+
570+
scope.add_attachment(bytes=bytes, filename="message.txt")
571+
572+
attachment = scope._attachments[0]
573+
item = attachment.to_envelope_item()
574+
575+
assert item.payload.bytes == b""
576+
577+
518578
def test_integration_scoping(sentry_init, capture_events):
519579
logger = logging.getLogger("test_basics")
520580

tests/test_utils.py

Lines changed: 146 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
_get_installed_modules,
3333
_generate_installed_modules,
3434
ensure_integration_enabled,
35+
to_string,
36+
exc_info_from_error,
37+
get_lines_from_file,
38+
package_version,
3539
)
3640

3741

@@ -61,55 +65,72 @@ def _normalize_distribution_name(name):
6165
return re.sub(r"[-_.]+", "-", name).lower()
6266

6367

64-
@pytest.mark.parametrize(
65-
("input_str", "expected_output"),
68+
isoformat_inputs_and_datetime_outputs = (
6669
(
67-
(
68-
"2021-01-01T00:00:00.000000Z",
69-
datetime(2021, 1, 1, tzinfo=timezone.utc),
70-
), # UTC time
71-
(
72-
"2021-01-01T00:00:00.000000",
73-
datetime(2021, 1, 1).astimezone(timezone.utc),
74-
), # No TZ -- assume local but convert to UTC
75-
(
76-
"2021-01-01T00:00:00Z",
77-
datetime(2021, 1, 1, tzinfo=timezone.utc),
78-
), # UTC - No milliseconds
79-
(
80-
"2021-01-01T00:00:00.000000+00:00",
81-
datetime(2021, 1, 1, tzinfo=timezone.utc),
82-
),
83-
(
84-
"2021-01-01T00:00:00.000000-00:00",
85-
datetime(2021, 1, 1, tzinfo=timezone.utc),
86-
),
87-
(
88-
"2021-01-01T00:00:00.000000+0000",
89-
datetime(2021, 1, 1, tzinfo=timezone.utc),
90-
),
91-
(
92-
"2021-01-01T00:00:00.000000-0000",
93-
datetime(2021, 1, 1, tzinfo=timezone.utc),
94-
),
95-
(
96-
"2020-12-31T00:00:00.000000+02:00",
97-
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))),
98-
), # UTC+2 time
99-
(
100-
"2020-12-31T00:00:00.000000-0200",
101-
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
102-
), # UTC-2 time
103-
(
104-
"2020-12-31T00:00:00-0200",
105-
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
106-
), # UTC-2 time - no milliseconds
70+
"2021-01-01T00:00:00.000000Z",
71+
datetime(2021, 1, 1, tzinfo=timezone.utc),
72+
), # UTC time
73+
(
74+
"2021-01-01T00:00:00.000000",
75+
datetime(2021, 1, 1).astimezone(timezone.utc),
76+
), # No TZ -- assume local but convert to UTC
77+
(
78+
"2021-01-01T00:00:00Z",
79+
datetime(2021, 1, 1, tzinfo=timezone.utc),
80+
), # UTC - No milliseconds
81+
(
82+
"2021-01-01T00:00:00.000000+00:00",
83+
datetime(2021, 1, 1, tzinfo=timezone.utc),
84+
),
85+
(
86+
"2021-01-01T00:00:00.000000-00:00",
87+
datetime(2021, 1, 1, tzinfo=timezone.utc),
88+
),
89+
(
90+
"2021-01-01T00:00:00.000000+0000",
91+
datetime(2021, 1, 1, tzinfo=timezone.utc),
92+
),
93+
(
94+
"2021-01-01T00:00:00.000000-0000",
95+
datetime(2021, 1, 1, tzinfo=timezone.utc),
10796
),
97+
(
98+
"2020-12-31T00:00:00.000000+02:00",
99+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))),
100+
), # UTC+2 time
101+
(
102+
"2020-12-31T00:00:00.000000-0200",
103+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
104+
), # UTC-2 time
105+
(
106+
"2020-12-31T00:00:00-0200",
107+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
108+
), # UTC-2 time - no milliseconds
109+
)
110+
111+
112+
@pytest.mark.parametrize(
113+
("input_str", "expected_output"),
114+
isoformat_inputs_and_datetime_outputs,
108115
)
109116
def test_datetime_from_isoformat(input_str, expected_output):
110117
assert datetime_from_isoformat(input_str) == expected_output, input_str
111118

112119

120+
@pytest.mark.parametrize(
121+
("input_str", "expected_output"),
122+
isoformat_inputs_and_datetime_outputs,
123+
)
124+
def test_datetime_from_isoformat_with_py_36_or_lower(input_str, expected_output):
125+
"""
126+
`fromisoformat` was added in Python version 3.7
127+
"""
128+
with mock.patch("sentry_sdk.utils.datetime") as datetime_mocked:
129+
datetime_mocked.fromisoformat.side_effect = AttributeError()
130+
datetime_mocked.strptime = datetime.strptime
131+
assert datetime_from_isoformat(input_str) == expected_output, input_str
132+
133+
113134
@pytest.mark.parametrize(
114135
"env_var_value,strict,expected",
115136
[
@@ -346,6 +367,12 @@ def test_sanitize_url_and_split(url, expected_result):
346367
assert sanitized_url.fragment == expected_result.fragment
347368

348369

370+
def test_sanitize_url_remove_authority_is_false():
371+
url = "https://usr:[email protected]"
372+
sanitized_url = sanitize_url(url, remove_authority=False)
373+
assert sanitized_url == url
374+
375+
349376
@pytest.mark.parametrize(
350377
("url", "sanitize", "expected_url", "expected_query", "expected_fragment"),
351378
[
@@ -629,6 +656,17 @@ def test_get_error_message(error, expected_result):
629656
assert get_error_message(exc_value) == expected_result(exc_value)
630657

631658

659+
def test_safe_str_fails():
660+
class ExplodingStr:
661+
def __str__(self):
662+
raise Exception
663+
664+
obj = ExplodingStr()
665+
result = safe_str(obj)
666+
667+
assert result == repr(obj)
668+
669+
632670
def test_installed_modules():
633671
try:
634672
from importlib.metadata import distributions, version
@@ -712,6 +750,20 @@ def test_default_release_empty_string():
712750
assert release is None
713751

714752

753+
def test_get_default_release_sentry_release_env(monkeypatch):
754+
monkeypatch.setenv("SENTRY_RELEASE", "sentry-env-release")
755+
assert get_default_release() == "sentry-env-release"
756+
757+
758+
def test_get_default_release_other_release_env(monkeypatch):
759+
monkeypatch.setenv("SOURCE_VERSION", "other-env-release")
760+
761+
with mock.patch("sentry_sdk.utils.get_git_revision", return_value=""):
762+
release = get_default_release()
763+
764+
assert release == "other-env-release"
765+
766+
715767
def test_ensure_integration_enabled_integration_enabled(sentry_init):
716768
def original_function():
717769
return "original"
@@ -973,3 +1025,55 @@ def test_function(): ...
9731025
sentry_sdk.utils.qualname_from_function(test_function)
9741026
== "test_qualname_from_function_none_name.<locals>.test_function"
9751027
)
1028+
1029+
1030+
def test_to_string_unicode_decode_error():
1031+
class BadStr:
1032+
def __str__(self):
1033+
raise UnicodeDecodeError("utf-8", b"", 0, 1, "reason")
1034+
1035+
obj = BadStr()
1036+
result = to_string(obj)
1037+
assert result == repr(obj)[1:-1]
1038+
1039+
1040+
def test_exc_info_from_error_dont_get_an_exc():
1041+
class NotAnException:
1042+
pass
1043+
1044+
with pytest.raises(ValueError) as exc:
1045+
exc_info_from_error(NotAnException())
1046+
1047+
assert "Expected Exception object to report, got <class" in str(exc.value)
1048+
1049+
1050+
def test_get_lines_from_file_handle_linecache_errors():
1051+
expected_result = ([], None, [])
1052+
1053+
class Loader:
1054+
@staticmethod
1055+
def get_source(module):
1056+
raise IOError("something went wrong")
1057+
1058+
result = get_lines_from_file("filename", 10, loader=Loader())
1059+
assert result == expected_result
1060+
1061+
with mock.patch(
1062+
"sentry_sdk.utils.linecache.getlines",
1063+
side_effect=OSError("something went wrong"),
1064+
):
1065+
result = get_lines_from_file("filename", 10)
1066+
assert result == expected_result
1067+
1068+
lines = ["line1", "line2", "line3"]
1069+
1070+
def fake_getlines(filename):
1071+
return lines
1072+
1073+
with mock.patch("sentry_sdk.utils.linecache.getlines", fake_getlines):
1074+
result = get_lines_from_file("filename", 10)
1075+
assert result == expected_result
1076+
1077+
1078+
def test_package_version_is_none():
1079+
assert package_version("non_existent_package") is None

tests/utils/test_contextvars.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import pytest
22
import random
33
import time
4+
from unittest import mock
45

56

6-
@pytest.mark.forked
7-
def test_leaks(maybe_monkeypatched_threading):
7+
def _run_contextvar_threaded_test():
88
import threading
99

1010
# Need to explicitly call _get_contextvars because the SDK has already
@@ -39,3 +39,14 @@ def run():
3939
t.join()
4040

4141
assert len(success) == 20
42+
43+
44+
@pytest.mark.forked
45+
def test_leaks(maybe_monkeypatched_threading):
46+
_run_contextvar_threaded_test()
47+
48+
49+
@pytest.mark.forked
50+
@mock.patch("sentry_sdk.utils._is_contextvars_broken", return_value=True)
51+
def test_leaks_when_is_contextvars_broken_is_false(maybe_monkeypatched_threading):
52+
_run_contextvar_threaded_test()

tests/utils/test_general.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,26 @@ def test_filename():
7474

7575
assert x("bogus", "bogus") == "bogus"
7676

77+
assert x("bogus", "bogus.pyc") == "bogus.py"
78+
7779
assert x("os", os.__file__) == "os.py"
7880

81+
assert x("foo.bar", "path/to/foo/bar.py") == "path/to/foo/bar.py"
82+
7983
import sentry_sdk.utils
8084

8185
assert x("sentry_sdk.utils", sentry_sdk.utils.__file__) == "sentry_sdk/utils.py"
8286

8387

88+
def test_filename_module_file_is_none():
89+
class DummyModule:
90+
__file__ = None
91+
92+
os.sys.modules["foo"] = DummyModule()
93+
94+
assert filename_for_module("foo.bar", "path/to/foo/bar.py") == "path/to/foo/bar.py"
95+
96+
8497
@pytest.mark.parametrize(
8598
"given,expected_envelope",
8699
[
@@ -120,6 +133,21 @@ def test_parse_invalid_dsn(dsn):
120133
dsn = Dsn(dsn)
121134

122135

136+
@pytest.mark.parametrize(
137+
"dsn,error_message",
138+
[
139+
("foo://[email protected]", "Unsupported scheme 'foo'"),
140+
("https://foobar@", "Missing hostname"),
141+
("https://@sentry.io", "Missing public key"),
142+
],
143+
)
144+
def test_dsn_validations(dsn, error_message):
145+
with pytest.raises(BadDsn) as e:
146+
dsn = Dsn(dsn)
147+
148+
assert str(e.value) == error_message
149+
150+
123151
@pytest.mark.parametrize(
124152
"frame,in_app_include,in_app_exclude,project_root,resulting_frame",
125153
[
@@ -578,6 +606,17 @@ def test_failed_base64_conversion(input):
578606
value="é...", metadata={"len": 8, "rem": [["!limit", "x", 2, 5]]}
579607
),
580608
],
609+
[
610+
"\udfff\udfff\udfff\udfff\udfff\udfff",
611+
5,
612+
AnnotatedValue(
613+
value="\udfff\udfff...",
614+
metadata={
615+
"len": 6,
616+
"rem": [["!limit", "x", 5 - 3, 5]],
617+
},
618+
),
619+
],
581620
],
582621
)
583622
def test_strip_string(input, max_length, result):

0 commit comments

Comments
 (0)