-
-
Notifications
You must be signed in to change notification settings - Fork 340
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Deserializing SentryFrame (#4739)
Add Decodable/Deserializing of SentryFrame, including logic for decoding NSNumbers.
- Loading branch information
1 parent
7d4acb9
commit aa9e313
Showing
5 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
Sources/Swift/Protocol/Codable/NSNumberDecodableWrapper.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
struct NSNumberDecodableWrapper: Decodable { | ||
let value: NSNumber? | ||
|
||
init(from decoder: Decoder) throws { | ||
let container = try decoder.singleValueContainer() | ||
if let intValue = try? container.decode(Int.self) { | ||
value = NSNumber(value: intValue) | ||
} | ||
// On 32-bit platforms UInt is UInt32, so we use UInt64 to cover all platforms. | ||
// We don't need UInt128 because NSNumber doesn't support it. | ||
else if let uint64Value = try? container.decode(UInt64.self) { | ||
value = NSNumber(value: uint64Value) | ||
} else if let doubleValue = try? container.decode(Double.self) { | ||
value = NSNumber(value: doubleValue) | ||
} else if let boolValue = try? container.decode(Bool.self) { | ||
value = NSNumber(value: boolValue) | ||
} else { | ||
SentryLog.warning("Failed to decode NSNumber from container for key: \(container.codingPath.last?.stringValue ?? "unknown")") | ||
value = nil | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
@_implementationOnly import _SentryPrivate | ||
import Foundation | ||
|
||
extension Frame: Decodable { | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case symbolAddress = "symbol_addr" | ||
case fileName = "filename" | ||
case function | ||
case module | ||
case package | ||
case imageAddress = "image_addr" | ||
case platform | ||
case instructionAddress = "instruction_addr" | ||
// Leaving out instruction on purpose. The event payload does not contain this field | ||
// and SentryFrame.serialize doesn't add it to the serialized dict. | ||
// We will remove the property in the next major see: | ||
// https://github.com/getsentry/sentry-cocoa/issues/4738 | ||
case lineNumber = "lineno" | ||
case columnNumber = "colno" | ||
case inApp = "in_app" | ||
case stackStart = "stack_start" | ||
} | ||
|
||
required convenience public init(from decoder: any Decoder) throws { | ||
self.init() | ||
|
||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
self.symbolAddress = try container.decodeIfPresent(String.self, forKey: .symbolAddress) | ||
self.fileName = try container.decodeIfPresent(String.self, forKey: .fileName) | ||
self.function = try container.decodeIfPresent(String.self, forKey: .function) | ||
self.module = try container.decodeIfPresent(String.self, forKey: .module) | ||
self.package = try container.decodeIfPresent(String.self, forKey: .package) | ||
self.imageAddress = try container.decodeIfPresent(String.self, forKey: .imageAddress) | ||
self.platform = try container.decodeIfPresent(String.self, forKey: .platform) | ||
self.instructionAddress = try container.decodeIfPresent(String.self, forKey: .instructionAddress) | ||
self.lineNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .lineNumber))?.value | ||
self.columnNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .columnNumber))?.value | ||
self.inApp = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .inApp))?.value | ||
self.stackStart = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .stackStart))?.value | ||
} | ||
} |
245 changes: 245 additions & 0 deletions
245
Tests/SentryTests/Protocol/Codable/NSNumberDecodableWrapperTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
@testable import Sentry | ||
import SentryTestUtils | ||
import XCTest | ||
|
||
class NSNumberDecodableWrapperTests: XCTestCase { | ||
|
||
func testDecode_BoolTrue() throws { | ||
// Arrange | ||
let jsonData = #""" | ||
{ | ||
"number": true | ||
} | ||
"""#.data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertTrue(number.boolValue) | ||
} | ||
|
||
func testDecode_BoolFalse() throws { | ||
// Arrange | ||
let jsonData = #""" | ||
{ | ||
"number": false | ||
} | ||
"""#.data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertFalse(number.boolValue) | ||
} | ||
|
||
func testDecode_PositiveInt() throws { | ||
// Arrange | ||
let jsonData = #""" | ||
{ | ||
"number": 1 | ||
} | ||
"""#.data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.intValue, 1) | ||
} | ||
|
||
func testDecode_IntMax() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": \(Int.max) | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.intValue, Int.max) | ||
} | ||
|
||
func testDecode_IntMin() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": \(Int.min) | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.intValue, Int.min) | ||
} | ||
|
||
func testDecode_UInt32Max() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": \(UInt32.max) | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.uint32Value, UInt32.max) | ||
} | ||
|
||
func testDecode_UInt64Max() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": \(UInt64.max) | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.uint64Value, UInt64.max) | ||
} | ||
|
||
// We can't use UInt128.max is only available on iOS 18 and above. | ||
// Still we would like to test if a max value bigger than UInt64.max is decoded correctly. | ||
func testDecode_UInt64MaxPlusOne_UsesDouble() throws { | ||
let UInt64MaxPlusOne = Double(UInt64.max) + 1 | ||
|
||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": \(UInt64MaxPlusOne) | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.doubleValue, UInt64MaxPlusOne) | ||
|
||
} | ||
|
||
func testDecode_Zero() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": 0.0 | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.intValue, 0) | ||
} | ||
|
||
func testDecode_Double() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": 0.1 | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.doubleValue, 0.1) | ||
} | ||
|
||
func testDecode_DoubleMax() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": \(Double.greatestFiniteMagnitude) | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.doubleValue, Double.greatestFiniteMagnitude) | ||
} | ||
|
||
func testDecode_DoubleMin() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": \(Double.leastNormalMagnitude) | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
|
||
// Assert | ||
let number = try XCTUnwrap(actual.number) | ||
XCTAssertEqual(number.doubleValue, Double.leastNormalMagnitude) | ||
} | ||
|
||
func testDecode_Nil() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": null | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
XCTAssertNil(actual.number) | ||
} | ||
|
||
func testDecode_String() throws { | ||
// Arrange | ||
let jsonData = """ | ||
{ | ||
"number": "hello" | ||
} | ||
""".data(using: .utf8)! | ||
|
||
// Act | ||
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?) | ||
XCTAssertNil(actual.number) | ||
} | ||
} | ||
|
||
private class ClassWithNSNumber: Decodable { | ||
|
||
var number: NSNumber? | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case number | ||
} | ||
|
||
required convenience public init(from decoder: any Decoder) throws { | ||
self.init() | ||
|
||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
self.number = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .number))?.value | ||
} | ||
} |
Oops, something went wrong.