2022-04-02 01:21:55 +00:00
|
|
|
|
import email
|
|
|
|
|
import gzip
|
|
|
|
|
import platform
|
|
|
|
|
import re
|
2022-08-16 00:48:53 +00:00
|
|
|
|
import sys
|
2023-12-18 02:39:10 +00:00
|
|
|
|
from dataclasses import dataclass
|
2024-04-30 11:39:47 +00:00
|
|
|
|
from datetime import datetime, UTC
|
2023-12-18 02:39:10 +00:00
|
|
|
|
|
|
|
|
|
import msgpack
|
2022-04-02 01:21:55 +00:00
|
|
|
|
|
2022-08-16 00:48:53 +00:00
|
|
|
|
from .conditions import is_android
|
2022-04-02 01:21:55 +00:00
|
|
|
|
|
2023-12-18 02:39:10 +00:00
|
|
|
|
VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)[-.]?(.*)")
|
2022-04-02 01:21:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def matches(actual, expected):
|
|
|
|
|
return {k: v for (k, v) in actual.items() if k in expected.keys()} == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_matches(actual, expected):
|
|
|
|
|
"""Assert two objects for equality, ignoring extra keys in ``actual``."""
|
|
|
|
|
assert {k: v for (k, v) in actual.items() if k in expected.keys()} == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_session(envelope, extra_assertion=None):
|
|
|
|
|
session = None
|
|
|
|
|
for item in envelope:
|
|
|
|
|
if item.headers.get("type") == "session" and item.payload.json is not None:
|
|
|
|
|
session = item.payload.json
|
|
|
|
|
|
|
|
|
|
assert session is not None
|
|
|
|
|
assert session["did"] == "42"
|
|
|
|
|
assert session["attrs"] == {
|
|
|
|
|
"release": "test-example-release",
|
|
|
|
|
"environment": "development",
|
|
|
|
|
}
|
|
|
|
|
if extra_assertion:
|
|
|
|
|
assert_matches(session, extra_assertion)
|
|
|
|
|
|
|
|
|
|
|
2024-04-30 11:39:47 +00:00
|
|
|
|
def assert_user_feedback(envelope):
|
|
|
|
|
user_feedback = None
|
|
|
|
|
for item in envelope:
|
|
|
|
|
if item.headers.get("type") == "user_report" and item.payload.json is not None:
|
|
|
|
|
user_feedback = item.payload.json
|
|
|
|
|
|
|
|
|
|
assert user_feedback is not None
|
|
|
|
|
assert user_feedback["name"] == "some-name"
|
|
|
|
|
assert user_feedback["email"] == "some-email"
|
|
|
|
|
assert user_feedback["comments"] == "some-comment"
|
|
|
|
|
|
|
|
|
|
|
2022-04-02 01:21:55 +00:00
|
|
|
|
def assert_meta(
|
|
|
|
|
envelope,
|
|
|
|
|
release="test-example-release",
|
|
|
|
|
integration=None,
|
|
|
|
|
transaction="test-transaction",
|
2022-08-16 00:48:53 +00:00
|
|
|
|
sdk_override=None,
|
2022-04-02 01:21:55 +00:00
|
|
|
|
):
|
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
|
|
|
|
|
expected = {
|
|
|
|
|
"platform": "native",
|
|
|
|
|
"environment": "development",
|
|
|
|
|
"release": release,
|
|
|
|
|
"user": {"id": 42, "username": "some_name"},
|
|
|
|
|
"transaction": transaction,
|
|
|
|
|
"tags": {"expected-tag": "some value"},
|
2024-04-30 11:39:47 +00:00
|
|
|
|
"extra": {
|
|
|
|
|
"extra stuff": "some value",
|
|
|
|
|
"…unicode key…": "őá…–🤮🚀¿ 한글 테스트",
|
|
|
|
|
},
|
2022-04-02 01:21:55 +00:00
|
|
|
|
}
|
|
|
|
|
expected_sdk = {
|
|
|
|
|
"name": "sentry.native",
|
2024-04-30 11:39:47 +00:00
|
|
|
|
"version": "0.7.2",
|
2022-04-02 01:21:55 +00:00
|
|
|
|
"packages": [
|
2024-04-30 11:39:47 +00:00
|
|
|
|
{"name": "github:getsentry/sentry-native", "version": "0.7.2"},
|
2022-04-02 01:21:55 +00:00
|
|
|
|
],
|
|
|
|
|
}
|
2022-08-16 00:48:53 +00:00
|
|
|
|
if is_android:
|
|
|
|
|
expected_sdk["name"] = "sentry.native.android"
|
|
|
|
|
else:
|
2022-04-02 01:21:55 +00:00
|
|
|
|
if sys.platform == "win32":
|
|
|
|
|
assert_matches(
|
|
|
|
|
event["contexts"]["os"],
|
|
|
|
|
{"name": "Windows", "version": platform.version()},
|
|
|
|
|
)
|
|
|
|
|
assert event["contexts"]["os"]["build"] is not None
|
|
|
|
|
elif sys.platform == "linux":
|
|
|
|
|
version = platform.release()
|
|
|
|
|
match = VERSION_RE.match(version)
|
|
|
|
|
version = match.group(1)
|
|
|
|
|
build = match.group(2)
|
|
|
|
|
|
|
|
|
|
assert_matches(
|
|
|
|
|
event["contexts"]["os"],
|
|
|
|
|
{"name": "Linux", "version": version, "build": build},
|
|
|
|
|
)
|
|
|
|
|
elif sys.platform == "darwin":
|
|
|
|
|
version = platform.mac_ver()[0].split(".")
|
|
|
|
|
if len(version) < 3:
|
|
|
|
|
version.append("0")
|
|
|
|
|
version = ".".join(version)
|
|
|
|
|
|
|
|
|
|
assert_matches(
|
|
|
|
|
event["contexts"]["os"],
|
|
|
|
|
{
|
|
|
|
|
"name": "macOS",
|
|
|
|
|
"version": version,
|
|
|
|
|
"kernel_version": platform.release(),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert event["contexts"]["os"]["build"] is not None
|
|
|
|
|
|
2023-12-18 02:39:10 +00:00
|
|
|
|
if sdk_override is not None:
|
2022-08-16 00:48:53 +00:00
|
|
|
|
expected_sdk["name"] = sdk_override
|
|
|
|
|
|
2022-04-02 01:21:55 +00:00
|
|
|
|
assert_matches(event, expected)
|
|
|
|
|
assert_matches(event["sdk"], expected_sdk)
|
|
|
|
|
assert_matches(
|
|
|
|
|
event["contexts"], {"runtime": {"type": "runtime", "name": "testing-runtime"}}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if integration is None:
|
|
|
|
|
assert event["sdk"].get("integrations") is None
|
|
|
|
|
else:
|
|
|
|
|
assert event["sdk"]["integrations"] == [integration]
|
|
|
|
|
if event.get("type") == "event":
|
|
|
|
|
assert any(
|
|
|
|
|
"sentry_example" in image["code_file"]
|
|
|
|
|
for image in event["debug_meta"]["images"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_stacktrace(envelope, inside_exception=False, check_size=True):
|
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
|
|
|
|
|
parent = event["exception"] if inside_exception else event["threads"]
|
|
|
|
|
frames = parent["values"][0]["stacktrace"]["frames"]
|
|
|
|
|
assert isinstance(frames, list)
|
|
|
|
|
|
|
|
|
|
if check_size:
|
|
|
|
|
assert len(frames) > 0
|
|
|
|
|
assert all(frame["instruction_addr"].startswith("0x") for frame in frames)
|
|
|
|
|
assert any(
|
|
|
|
|
frame.get("function") is not None and frame.get("package") is not None
|
|
|
|
|
for frame in frames
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_breadcrumb(envelope):
|
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
|
|
|
|
|
expected = {
|
|
|
|
|
"type": "http",
|
|
|
|
|
"message": "debug crumb",
|
|
|
|
|
"category": "example!",
|
|
|
|
|
"level": "debug",
|
2024-04-30 11:39:47 +00:00
|
|
|
|
"data": {
|
|
|
|
|
"url": "https://example.com/api/1.0/users",
|
|
|
|
|
"method": "GET",
|
|
|
|
|
"status_code": 200,
|
|
|
|
|
"reason": "OK",
|
|
|
|
|
},
|
2022-04-02 01:21:55 +00:00
|
|
|
|
}
|
|
|
|
|
assert any(matches(b, expected) for b in event["breadcrumbs"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_attachment(envelope):
|
|
|
|
|
expected = {
|
|
|
|
|
"type": "attachment",
|
|
|
|
|
"filename": "CMakeCache.txt",
|
|
|
|
|
}
|
|
|
|
|
assert any(matches(item.headers, expected) for item in envelope)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_minidump(envelope):
|
|
|
|
|
expected = {
|
|
|
|
|
"type": "attachment",
|
|
|
|
|
"attachment_type": "event.minidump",
|
|
|
|
|
}
|
|
|
|
|
minidump = next(item for item in envelope if matches(item.headers, expected))
|
|
|
|
|
assert minidump.headers["length"] > 4
|
|
|
|
|
assert minidump.payload.bytes.startswith(b"MDMP")
|
|
|
|
|
|
|
|
|
|
|
2024-04-30 11:39:47 +00:00
|
|
|
|
def assert_timestamp(ts, now=datetime.now(UTC)):
|
2022-04-02 01:21:55 +00:00
|
|
|
|
assert ts[:11] == now.isoformat()[:11]
|
|
|
|
|
|
|
|
|
|
|
2024-04-30 11:39:47 +00:00
|
|
|
|
def assert_event(envelope, message="Hello World!"):
|
2022-04-02 01:21:55 +00:00
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
expected = {
|
|
|
|
|
"level": "info",
|
|
|
|
|
"logger": "my-logger",
|
2024-04-30 11:39:47 +00:00
|
|
|
|
"message": {"formatted": message},
|
2022-04-02 01:21:55 +00:00
|
|
|
|
}
|
|
|
|
|
assert_matches(event, expected)
|
|
|
|
|
assert_timestamp(event["timestamp"])
|
|
|
|
|
|
|
|
|
|
|
2023-12-18 02:39:10 +00:00
|
|
|
|
def assert_breakpad_crash(envelope):
|
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
expected = {
|
|
|
|
|
"level": "fatal",
|
|
|
|
|
}
|
|
|
|
|
assert_matches(event, expected)
|
|
|
|
|
|
|
|
|
|
|
2022-04-02 01:21:55 +00:00
|
|
|
|
def assert_exception(envelope):
|
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
exception = {
|
|
|
|
|
"type": "ParseIntError",
|
|
|
|
|
"value": "invalid digit found in string",
|
|
|
|
|
}
|
|
|
|
|
assert_matches(event["exception"]["values"][0], exception)
|
|
|
|
|
assert_timestamp(event["timestamp"])
|
|
|
|
|
|
|
|
|
|
|
2023-12-18 02:39:10 +00:00
|
|
|
|
def assert_inproc_crash(envelope):
|
2022-04-02 01:21:55 +00:00
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
assert_matches(event, {"level": "fatal"})
|
|
|
|
|
# depending on the unwinder, we currently don’t get any stack frames from
|
|
|
|
|
# a `ucontext`
|
|
|
|
|
assert_stacktrace(envelope, inside_exception=True, check_size=False)
|
|
|
|
|
|
|
|
|
|
|
2022-08-16 00:48:53 +00:00
|
|
|
|
def assert_crash_timestamp(has_files, tmp_path):
|
|
|
|
|
# The crash file should survive a `sentry_init` and should still be there
|
|
|
|
|
# even after restarts.
|
|
|
|
|
if has_files:
|
|
|
|
|
with open("{}/.sentry-native/last_crash".format(tmp_path)) as f:
|
|
|
|
|
crash_timestamp = f.read()
|
|
|
|
|
assert_timestamp(crash_timestamp)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_before_send(envelope):
|
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
assert_matches(event, {"adapted_by": "before_send"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_no_before_send(envelope):
|
|
|
|
|
event = envelope.get_event()
|
|
|
|
|
assert ("adapted_by", "before_send") not in event.items()
|
|
|
|
|
|
|
|
|
|
|
2023-12-18 02:39:10 +00:00
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class CrashpadAttachments:
|
|
|
|
|
event: dict
|
|
|
|
|
breadcrumb1: list
|
|
|
|
|
breadcrumb2: list
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _unpack_breadcrumbs(payload):
|
|
|
|
|
unpacker = msgpack.Unpacker()
|
|
|
|
|
unpacker.feed(payload)
|
|
|
|
|
return [unpacked for unpacked in unpacker]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_crashpad_attachments(msg):
|
|
|
|
|
event = {}
|
|
|
|
|
breadcrumb1 = []
|
|
|
|
|
breadcrumb2 = []
|
|
|
|
|
for part in msg.walk():
|
2024-04-30 11:39:47 +00:00
|
|
|
|
if part.get_filename() is not None:
|
|
|
|
|
assert part.get("Content-Type") is None
|
|
|
|
|
|
2023-12-18 02:39:10 +00:00
|
|
|
|
match part.get_filename():
|
|
|
|
|
case "__sentry-event":
|
|
|
|
|
event = msgpack.unpackb(part.get_payload(decode=True))
|
|
|
|
|
case "__sentry-breadcrumb1":
|
|
|
|
|
breadcrumb1 = _unpack_breadcrumbs(part.get_payload(decode=True))
|
|
|
|
|
case "__sentry-breadcrumb2":
|
|
|
|
|
breadcrumb2 = _unpack_breadcrumbs(part.get_payload(decode=True))
|
|
|
|
|
|
|
|
|
|
return CrashpadAttachments(event, breadcrumb1, breadcrumb2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_valid_timestamp(timestamp):
|
|
|
|
|
try:
|
|
|
|
|
datetime.fromisoformat(timestamp)
|
|
|
|
|
return True
|
|
|
|
|
except ValueError:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _validate_breadcrumb_seq(seq, breadcrumb_func):
|
|
|
|
|
for i in seq:
|
|
|
|
|
breadcrumb = breadcrumb_func(i)
|
|
|
|
|
assert breadcrumb["message"] == str(i)
|
|
|
|
|
assert is_valid_timestamp(breadcrumb["timestamp"])
|
|
|
|
|
|
|
|
|
|
|
2022-04-02 01:21:55 +00:00
|
|
|
|
def assert_crashpad_upload(req):
|
|
|
|
|
multipart = gzip.decompress(req.get_data())
|
|
|
|
|
msg = email.message_from_bytes(bytes(str(req.headers), encoding="utf8") + multipart)
|
2023-12-18 02:39:10 +00:00
|
|
|
|
attachments = _load_crashpad_attachments(msg)
|
|
|
|
|
|
|
|
|
|
if len(attachments.breadcrumb1) > 3:
|
|
|
|
|
_validate_breadcrumb_seq(range(97), lambda i: attachments.breadcrumb1[3 + i])
|
|
|
|
|
_validate_breadcrumb_seq(
|
|
|
|
|
range(97, 101), lambda i: attachments.breadcrumb2[i - 97]
|
|
|
|
|
)
|
2022-04-02 01:21:55 +00:00
|
|
|
|
|
2023-12-18 02:39:10 +00:00
|
|
|
|
assert attachments.event["level"] == "fatal"
|
2022-04-02 01:21:55 +00:00
|
|
|
|
|
|
|
|
|
assert any(
|
|
|
|
|
b'name="upload_file_minidump"' in part.as_bytes()
|
|
|
|
|
and b"\n\nMDMP" in part.as_bytes()
|
|
|
|
|
for part in msg.walk()
|
|
|
|
|
)
|
2024-04-30 11:39:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_gzip_file_header(output):
|
|
|
|
|
assert output[:3] == b"\x1f\x8b\x08"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assert_gzip_content_encoding(req):
|
|
|
|
|
assert req.content_encoding == "gzip"
|