diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a7505ad3d..b35c779777a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add extension for `Data` to track file I/O operations with Sentry (#4862) - New user feedback API and Widget (#4874) ### Improvements diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d0fa45469f6..aeabbb6dd84 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -813,6 +813,7 @@ D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */; }; D43647EF2D5CF9E3001468E0 /* SentrySpanDataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647EE2D5CF9DC001468E0 /* SentrySpanDataKey.swift */; }; D43647F12D5CFB71001468E0 /* SentrySpanKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */; }; + D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; @@ -822,8 +823,10 @@ D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; + D4C5F59A2D4249E6002A9BF6 /* DataSentryTracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */; }; D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; }; D4E3F35E2D4A877300F79E2B /* SentryNSDictionarySanitize+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */; }; + D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; @@ -1951,6 +1954,9 @@ D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryNSDictionarySanitize+Tests.m"; sourceTree = ""; }; D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSDictionarySanitizeTests.swift; sourceTree = ""; }; + D43647EE2D5CF9DC001468E0 /* SentrySpanDataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanDataKey.swift; sourceTree = ""; }; + D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentrySpanKeyTests.swift; sourceTree = ""; }; + D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = ""; }; D46D45E12D5F3FD600A1CB35 /* Sentry_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Sentry_Base.xctestplan; sourceTree = ""; }; D46D45E32D5F402100A1CB35 /* iOS-Swift_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "iOS-Swift_Base.xctestplan"; sourceTree = ""; }; D46D45E42D5F403E00A1CB35 /* iOS-Swift-Benchmarking_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "iOS-Swift-Benchmarking_Base.xctestplan"; path = "Plans/iOS-Swift-Benchmarking_Base.xctestplan"; sourceTree = SOURCE_ROOT; }; @@ -1960,8 +1966,6 @@ D46D45E82D5F40FA00A1CB35 /* SentryProfilerTests_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = SentryProfilerTests_Base.xctestplan; path = Plans/SentryProfilerTests_Base.xctestplan; sourceTree = SOURCE_ROOT; }; D46D45E92D5F411700A1CB35 /* SentrySwiftUI_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = SentrySwiftUI_Base.xctestplan; path = Plans/SentrySwiftUI_Base.xctestplan; sourceTree = SOURCE_ROOT; }; D46D45EA2D5F412100A1CB35 /* SentryTests_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = SentryTests_Base.xctestplan; path = Plans/SentryTests_Base.xctestplan; sourceTree = SOURCE_ROOT; }; - D43647EE2D5CF9DC001468E0 /* SentrySpanDataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanDataKey.swift; sourceTree = ""; }; - D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentrySpanKeyTests.swift; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; @@ -1979,6 +1983,8 @@ D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; + D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSentryTracingIntegrationTests.swift; sourceTree = ""; }; + D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = ""; }; D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; @@ -3802,6 +3808,15 @@ name = Transaction; sourceTree = ""; }; + D468C0602D36699700964230 /* IO */ = { + isa = PBXGroup; + children = ( + D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */, + D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */, + ); + path = IO; + sourceTree = ""; + }; D46D45E22D5F3FD600A1CB35 /* Plans */ = { isa = PBXGroup; children = ( @@ -4048,6 +4063,7 @@ D8739CF72BECFF92007D2F66 /* Performance */ = { isa = PBXGroup; children = ( + D468C0602D36699700964230 /* IO */, 6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */, D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */, ); @@ -4057,6 +4073,7 @@ D875ED09276CC83200422FAC /* IO */ = { isa = PBXGroup; children = ( + D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */, D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */, D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */, D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */, @@ -5063,6 +5080,7 @@ D84D2CC32C29AD120011AF8A /* SentrySessionReplay.swift in Sources */, 849B8F9B2C6E906900148E1F /* SentryUserFeedbackIntegrationDriver.swift in Sources */, 63FE70DF20DA4C1000CDBAE8 /* SentryCrashMonitorType.c in Sources */, + D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */, 7BF9EF7E2722B91F00B5BBEF /* SentryDefaultObjCRuntimeWrapper.m in Sources */, 7BC3936E25B1AB72004F03D3 /* SentryLevelMapper.m in Sources */, 6304360B1EC0595B00C4D3FA /* SentryNSDataUtils.m in Sources */, @@ -5082,6 +5100,7 @@ 7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */, 7D5C441A237C2E1F00DAB0A3 /* SentrySDK.m in Sources */, 7D65260E237F649E00113EA2 /* SentryScope.m in Sources */, + D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */, 84281C472A57905700EE88F2 /* SentrySample.m in Sources */, 84AC61D329F7541E009EEF61 /* SentryDispatchSourceWrapper.m in Sources */, 62A456E52B0370E0003F19A1 /* SentryUIEventTrackerTransactionMode.m in Sources */, @@ -5381,6 +5400,7 @@ 8E70B0FD25CB72BE002B3155 /* SentrySpanTests.swift in Sources */, 7BBD188F2448469A00427C76 /* HttpDateFormatter.swift in Sources */, 63FE720C20DA66EC00CDBAE8 /* SentryCrashMonitor_Tests.m in Sources */, + D4C5F59A2D4249E6002A9BF6 /* DataSentryTracingIntegrationTests.swift in Sources */, D855B3EA27D652C700BCED76 /* TestCoreDataStack.swift in Sources */, D8AE48C12C57B1550092A2A6 /* SentryLevelTests.swift in Sources */, 63FE721820DA66EC00CDBAE8 /* TestThread.m in Sources */, diff --git a/SentryTestUtils/TestCurrentDateProvider.swift b/SentryTestUtils/TestCurrentDateProvider.swift index 751c0ec1922..5a4d990bd6b 100644 --- a/SentryTestUtils/TestCurrentDateProvider.swift +++ b/SentryTestUtils/TestCurrentDateProvider.swift @@ -8,12 +8,11 @@ public class TestCurrentDateProvider: SentryCurrentDateProvider { public var driftTimeForEveryRead = false public var driftTimeInterval = 0.1 private var _systemUptime: TimeInterval = 0 - + public init() { } - - public func date() -> Date { + public func date() -> Date { defer { if driftTimeForEveryRead { internalDate = internalDate.addingTimeInterval(driftTimeInterval) @@ -28,7 +27,7 @@ public class TestCurrentDateProvider: SentryCurrentDateProvider { setDate(date: TestCurrentDateProvider.defaultStartingDate) internalSystemTime = 0 } - + public func setDate(date: Date) { internalDate = date } @@ -43,7 +42,7 @@ public class TestCurrentDateProvider: SentryCurrentDateProvider { setDate(date: date().addingTimeInterval(nanoseconds.toTimeInterval())) internalSystemTime += nanoseconds } - + public func advanceBy(interval: TimeInterval) { setDate(date: date().addingTimeInterval(interval)) internalSystemTime += interval.toNanoSeconds() diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index 5d7d565f75f..872080d790a 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -178,6 +178,13 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path return result; } +- (nullable id)spanForPath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation +{ + return [self spanForPath:path origin:origin operation:operation size:0]; +} + - (nullable id)spanForPath:(NSString *)path origin:(NSString *)origin operation:(NSString *)operation @@ -209,6 +216,10 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path ioSpan.origin = origin; [ioSpan setDataValue:path forKey:SentrySpanDataKey.filePath]; + if (size > 0) { + [ioSpan setDataValue:[NSNumber numberWithUnsignedInteger:size] + forKey:SentrySpanDataKey.fileSize]; + } SENTRY_LOG_DEBUG( @"Automatically started a new span with description: %@, operation: %@, origin: %@", diff --git a/Sources/Sentry/include/SentryFileIOTracker.h b/Sources/Sentry/include/SentryFileIOTracker.h index 126a1cfbf48..5ade68e09b8 100644 --- a/Sources/Sentry/include/SentryFileIOTracker.h +++ b/Sources/Sentry/include/SentryFileIOTracker.h @@ -80,6 +80,17 @@ SENTRY_NO_INIT method:(BOOL (^)(NSString *, NSData *, NSDictionary *))method; +// MARK: - Internal Methods available for Swift Extension + +- (nullable id)spanForPath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation; + +- (nullable id)spanForPath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation + size:(NSUInteger)size; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift new file mode 100644 index 00000000000..255ca273827 --- /dev/null +++ b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift @@ -0,0 +1,52 @@ +@_implementationOnly import _SentryPrivate + +/// A ``Data`` extension that tracks read and write operations with Sentry. +/// +/// - Note: Methods provided by this extension reflect the same functionality as the original ``Data`` methods, but they track the operation with Sentry. +public extension Data { + + // MARK: - Reading Data from a File + + /// Creates a data object from the data at the specified file URL, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.experimental.enableDataSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - url: The location on disk of the data to read. + /// - options: The mask specifying the options to use when reading the data. For more information, see ``NSData.ReadingOptions``. + /// - Note: See ``Data.init(contentsOf:options:)`` for more information. + init(contentsOfWithSentryTracing url: URL, options: Data.ReadingOptions = []) throws { + let tracker = SentryFileIOTracker.sharedInstance() + self = try tracker + .measureReadingData( + from: url, + options: options, + origin: SentryTraceOrigin.manualFileData) { url, options in + try Data(contentsOf: url, options: options) + } + } + + // MARK: - Writing Data to a File + + /// Write the contents of the `Data` to a location, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.experimental.enableDataSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - url: The location to write the data into. + /// - options: Options for writing the data. Default value is `[]`. + /// - Note: See ``Data.write(to:options:)`` for more information. + func writeWithSentryTracing(to url: URL, options: Data.WritingOptions = []) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker + .measureWritingData( + self, + to: url, + options: options, + origin: SentryTraceOrigin.manualFileData) { data, url, options in + try data.write(to: url, options: options) + } + } +} diff --git a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift new file mode 100644 index 00000000000..92f5fcd8853 --- /dev/null +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -0,0 +1,54 @@ +@_implementationOnly import _SentryPrivate + +extension SentryFileIOTracker { + func measureReadingData( + from url: URL, + options: Data.ReadingOptions, + origin: String, + method: (_ url: URL, _ options: Data.ReadingOptions) throws -> Data + ) rethrows -> Data { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. + guard url.scheme == NSURLFileScheme else { + return try method(url, options) + } + guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileRead) else { + return try method(url, options) + } + do { + let data = try method(url, options) + span.setData(value: data.count, key: SentrySpanDataKey.fileSize) + span.finish() + return data + } catch { + span.finish(status: .internalError) + throw error + } + } + + func measureWritingData( + _ data: Data, + to url: URL, + options: Data.WritingOptions, + origin: String, + method: (_ data: Data, _ url: URL, _ options: Data.WritingOptions) throws -> Void + ) rethrows { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. + guard url.scheme == NSURLFileScheme else { + return try method(data, url, options) + } + guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileWrite, size: UInt(data.count)) else { + return try method(data, url, options) + } + do { + try method(data, url, options) + span.finish() + } catch { + span.finish(status: .internalError) + throw error + } + } +} diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift new file mode 100644 index 00000000000..26f0dd2cf7f --- /dev/null +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -0,0 +1,404 @@ +@testable import Sentry +import SentryTestUtils +import XCTest + +class DataSentryTracingIntegrationTests: XCTestCase { + private class Fixture { + let mockDateProvider: TestCurrentDateProvider = { + let provider = TestCurrentDateProvider() + provider.driftTimeForEveryRead = true + provider.driftTimeInterval = 0.25 + return provider + }() + + let data = "SOME DATA".data(using: .utf8)! + + var fileUrlToRead: URL! + var fileUrlToWrite: URL! + var ignoredFileUrl: URL! + + init() {} + + func getSut(testName: String, isSDKEnabled: Bool = true, isEnabled: Bool = true) throws -> Data { + if isSDKEnabled { + SentryDependencyContainer.sharedInstance().dateProvider = mockDateProvider + + SentrySDK.start { options in + options.dsn = TestConstants.dsnAsString(username: "DataSentryTracingIntegrationTests") + options.removeAllIntegrations() + + // Configure options required by File I/O tracking integration + options.enableAutoPerformanceTracing = true + options.enableFileIOTracing = isEnabled + options.setIntegrations(isEnabled ? [SentryFileIOTrackingIntegration.self] : []) + + // Configure the tracing sample rate to record all traces + options.tracesSampleRate = 1.0 + + // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. + // Instead we are recommending to disable swizzling and use manual tracing. + options.enableSwizzling = true + options.experimental.enableDataSwizzling = false + options.experimental.enableFileManagerSwizzling = false + } + + // Get the working directory of the SDK, as the path is using the DSN hash to avoid conflicts + guard let sentryBasePath = SentrySDK.currentHub().getClient()?.fileManager.basePath else { + preconditionFailure("Sentry base path is nil, but should be configured for test cases.") + } + let sentryBasePathUrl = URL(fileURLWithPath: sentryBasePath) + + fileUrlToRead = sentryBasePathUrl.appendingPathComponent("file-to-read") + try data.write(to: fileUrlToRead) + + fileUrlToWrite = sentryBasePathUrl.appendingPathComponent("file-to-write") + + // Get the working directory of the SDK, as these files are ignored by default + guard let sentryPath = SentrySDK.currentHub().getClient()?.fileManager.sentryPath else { + preconditionFailure("Sentry path is nil, but should be configured for test cases.") + } + let sentryPathUrl = URL(fileURLWithPath: sentryPath) + + ignoredFileUrl = sentryPathUrl.appendingPathComponent("ignored-file") + try data.write(to: ignoredFileUrl) + } else { + let basePathUrl = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("test-\(testName.hashValue.description)") + try! FileManager.default + .createDirectory(at: basePathUrl, withIntermediateDirectories: true) + + fileUrlToRead = basePathUrl.appendingPathComponent("file-to-read") + try data.write(to: fileUrlToRead) + + fileUrlToWrite = basePathUrl.appendingPathComponent("file-to-write") + } + return data + } + + var invalidFileUrlToRead: URL { + URL(fileURLWithPath: "/dev/null") + } + + var invalidFileUrlToWrite: URL { + URL(fileURLWithPath: "/path/that/does/not/exist") + } + + var nonFileUrl: URL { + // URL to a file that is not a file but should exist at all times + URL(string: "https://raw.githubusercontent.com/getsentry/sentry-cocoa/refs/heads/main/.gitignore")! + } + } + + private var fixture: Fixture! + + override func setUp() { + super.setUp() + fixture = Fixture() + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + // MARK: - Data.init(contentsOfWithSentryTracing:) + + func testInitContentsOfWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let expectedData = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let refTimestamp = fixture.mockDateProvider.date() + let data = try Data(contentsOfWithSentryTracing: fixture.fileUrlToRead) + + // -- Assert -- + XCTAssertEqual(data, expectedData) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + + XCTAssertEqual(span.status, SentrySpanStatus.ok) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToRead.path) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + + // As the date provider is used by multiple internal components, it is not possible to pin-point the exact timestamp. + // Therefore, we can only assert relative timestamps as the date provider uses an internal drift. + let startTimestamp = try XCTUnwrap(span.startTimestamp) + let endTimestamp = try XCTUnwrap(span.timestamp) + XCTAssertGreaterThan(startTimestamp.timeIntervalSince1970, refTimestamp.timeIntervalSince1970) + XCTAssertGreaterThan(endTimestamp.timeIntervalSince1970, startTimestamp.timeIntervalSince1970) + } + + func testInitContentsOfWithSentryTracingWithOptions_shouldPassOptionsToSystemImplementation() throws { + // -- Arrange -- + let expectedData = try fixture.getSut(testName: self.name) + + // To verify that the option is passed, we are using the `alwaysMapped` option. + // We expect the option to read the data differently when set. + // + // Due to the current implementation of the `Data(contentsOf:options:)` initializer, it is not possible to detect if the file was mapped or not. + // Therefore the mapped and unmapped data will look exactly the same, and no assertions can be made on the data. + // + // Ref: https://github.com/swiftlang/swift-foundation/blob/c64dcd8347554db347492e0643d1e5fbc4ccfd2b/Sources/FoundationEssentials/Data/Data%2BReading.swift#L333-L337 + + // Assert expected implementation behavior by writing the same file twice without the option set. + let unmappedData = try Data(contentsOf: fixture.fileUrlToRead) + let mappedData = try Data(contentsOf: fixture.fileUrlToRead, options: [.alwaysMapped]) + XCTAssertEqual(unmappedData, expectedData) + XCTAssertEqual(mappedData, expectedData) + + // -- Act -- + let unmappedSentryData = try Data(contentsOfWithSentryTracing: fixture.fileUrlToRead) + let mappedSentryData = try Data(contentsOfWithSentryTracing: fixture.fileUrlToRead, options: [.alwaysMapped]) + + // -- Assert -- + XCTAssertEqual(unmappedSentryData, expectedData) + XCTAssertEqual(mappedSentryData, expectedData) + } + + func testInitContentsOfWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + let refTimestamp = fixture.mockDateProvider.date() + XCTAssertThrowsError(try Data(contentsOfWithSentryTracing: fixture.invalidFileUrlToRead)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + + XCTAssertEqual(span.status, SentrySpanStatus.internalError) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToRead.path) + XCTAssertNil(span.data["file.size"]) + + // As the date provider is used by multiple internal components, it is not possible to pin-point the exact timestamp. + // Therefore, we can only assert relative timestamps as the date provider uses an internal drift. + let startTimestamp = try XCTUnwrap(span.startTimestamp) + let endTimestamp = try XCTUnwrap(span.timestamp) + XCTAssertGreaterThan(startTimestamp.timeIntervalSince1970, refTimestamp.timeIntervalSince1970) + XCTAssertGreaterThan(endTimestamp.timeIntervalSince1970, startTimestamp.timeIntervalSince1970) + } + + func testInitContentsOfWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfWithSentryTracing: fixture.nonFileUrl) + + // -- Assert -- + XCTAssertGreaterThan(data.count, 0) + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testInitContentsOfWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfWithSentryTracing: fixture.fileUrlToRead) + + // -- Assert -- + XCTAssertEqual(data, fixture.data) + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testInitContentsOfWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfWithSentryTracing: fixture.ignoredFileUrl) + + // -- Assert -- + XCTAssertEqual(data, fixture.data) + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testInitContentsOfWithSentryTracing_SDKIsNotStarted_shouldReadData() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name, isSDKEnabled: false) + + // -- Act -- + let data = try Data(contentsOfWithSentryTracing: fixture.fileUrlToRead) + + // -- Assert -- + XCTAssertFalse(SentrySDK.isEnabled) + XCTAssertEqual(data, fixture.data) + } + + func testInitContentsOfWithSentryTracing_SDKIsClosed_shouldReadData() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name) + SentrySDK.close() + + // -- Act -- + let data = try Data(contentsOfWithSentryTracing: fixture.fileUrlToRead) + + // -- Assert -- + XCTAssertFalse(SentrySDK.isEnabled) + XCTAssertEqual(data, fixture.data) + } + + // MARK: - Data.writeWithSentryTracing(to:) + + func testWriteWithSentryTracing_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let refTimestamp = fixture.mockDateProvider.date() + try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + + XCTAssertEqual(span.status, SentrySpanStatus.ok) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToWrite.path) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + + // Reading the written data will create a span, so do it after asserting the transaction + let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) + XCTAssertEqual(writtenData, fixture.data) + + // As the date provider is used by multiple internal components, it is not possible to pin-point the exact timestamp. + // Therefore, we can only assert relative timestamps as the date provider uses an internal drift. + let startTimestamp = try XCTUnwrap(span.startTimestamp) + let endTimestamp = try XCTUnwrap(span.timestamp) + XCTAssertGreaterThan(startTimestamp.timeIntervalSince1970, refTimestamp.timeIntervalSince1970) + XCTAssertGreaterThan(endTimestamp.timeIntervalSince1970, startTimestamp.timeIntervalSince1970) + } + + func testWriteWithSentryTracingWithOptions_shouldPassOptionsToSystemImplementation() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + + // To verify that the option is passed, we are using the `withoutOverwriting` option. + // We expect the default write implementation to not fail when writing the same file twice without the option set. + // When setting the option, we expect the write operation to fail as the file is already written. + + // Assert expected implementation behavior by writing the same file twice without the option set. + XCTAssertNoThrow(try sut.write(to: fixture.fileUrlToWrite, options: [])) + XCTAssertNoThrow(try sut.write(to: fixture.fileUrlToWrite, options: [])) + XCTAssertThrowsError(try sut.write(to: fixture.fileUrlToWrite, options: [.withoutOverwriting])) + + // Cleanup by deleting the file + try FileManager.default.removeItem(at: fixture.fileUrlToWrite) + + // -- Act -- + // The traced implementation should behave the same way as the default implementation. + XCTAssertNoThrow(try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite, options: [])) + XCTAssertNoThrow(try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite, options: [])) + XCTAssertThrowsError(try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite, options: [.withoutOverwriting])) + + // -- Assert -- + let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) + XCTAssertEqual(writtenData, sut) + } + + func testWriteWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + let refTimestamp = fixture.mockDateProvider.date() + XCTAssertThrowsError(try sut.writeWithSentryTracing(to: fixture.invalidFileUrlToWrite)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + + XCTAssertEqual(span.status, SentrySpanStatus.internalError) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToWrite.path) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + + // As the date provider is used by multiple internal components, it is not possible to pin-point the exact timestamp. + // Therefore, we can only assert relative timestamps as the date provider uses an internal drift. + let startTimestamp = try XCTUnwrap(span.startTimestamp) + let endTimestamp = try XCTUnwrap(span.timestamp) + XCTAssertGreaterThan(startTimestamp.timeIntervalSince1970, refTimestamp.timeIntervalSince1970) + XCTAssertGreaterThan(endTimestamp.timeIntervalSince1970, startTimestamp.timeIntervalSince1970) + } + + func testWriteWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.writeWithSentryTracing(to: fixture.nonFileUrl)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testWriteWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testWriteWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + try sut.writeWithSentryTracing(to: fixture.ignoredFileUrl) + + // -- Assert -- + let writtenData = try Data(contentsOf: fixture.ignoredFileUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testWriteWithSentryTracing_SDKIsNotStarted_shouldWriteFile() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name, isSDKEnabled: false) + SentrySDK.close() + + // -- Act -- + try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite) + + // -- Assert -- + XCTAssertFalse(SentrySDK.isEnabled) + let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) + XCTAssertEqual(writtenData, fixture.data) + } + + func testWriteWithSentryTracing_SDKIsClosed_shouldWriteFile() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + SentrySDK.close() + + // -- Act -- + try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite) + + // -- Assert -- + XCTAssertFalse(SentrySDK.isEnabled) + let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) + XCTAssertEqual(writtenData, fixture.data) + } +} diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift index 33fea1a4a06..af47ea4b995 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift @@ -215,7 +215,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertNil(span) wait(for: [expect], timeout: 0.1) } - + func testReadFromString() { let sut = fixture.getSut() let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index 413e3e18f8c..9c157641bf2 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -29,6 +29,18 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { fileURL = fileDirectory.appendingPathComponent("TestFile") filePath = fileURL?.path } + + func assertDataWritten(toUrl url: URL, file: StaticString = #file, line: UInt = #line) { + guard let data = try? Data(contentsOf: url) else { + XCTFail("Could not load written resource file", file: file, line: line) + return + } + XCTAssertEqual(self.data, data, file: file, line: line) + } + + var invalidFileUrlToRead: URL { + URL(fileURLWithPath: "/dev/null") + } } private var fixture: Fixture! @@ -57,125 +69,188 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } func test_WritingTrackingDisabled_forIOOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableFileIOTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_WritingTrackingDisabled_forSwizzlingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableSwizzling: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_WritingTrackingDisabled_forAutoPerformanceTrackingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableAutoPerformanceTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_WritingTrackingDisabled_TracingDisabled() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(tracesSampleRate: 0)) - + + // -- Assert -- assertWriteWithNoSpans() } - func test_Writing_Tracking() throws { - if #available(iOS 18, macOS 15, tvOS 15, *) { - throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") + func testData_Writing_Tracking() { + // -- Arrange -- + let expectedSpanCount: Int + if #available(iOS 18, macOS 15, tvOS 18, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 } + + // -- Act -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.write") { + + // -- Assert -- + assertSpans(expectedSpanCount, "file.write") { try? fixture.data.write(to: fixture.fileURL) } } - - func test_WritingWithOption_Tracking() throws { - if #available(iOS 18, macOS 15, tvOS 15, *) { - throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") + + func testData_WritingWithOption_Tracking() { + // -- Arrange -- + let expectedSpanCount: Int + if #available(iOS 18, macOS 15, tvOS 18, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 } + + // -- Act -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.write") { + + // -- Assert -- + assertSpans(expectedSpanCount, "file.write") { try? fixture.data.write(to: fixture.fileURL, options: .atomic) } } - + func test_ReadingTrackingDisabled_forIOOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableFileIOTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_ReadingTrackingDisabled_forSwizzlingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableSwizzling: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_ReadingTrackingDisabled_forAutoPerformanceTrackingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableAutoPerformanceTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_ReadingTrackingDisabled_TracingDisabled() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(tracesSampleRate: 0)) - + + // -- Assert -- assertWriteWithNoSpans() } - func test_ReadingURL_Tracking() throws { - if #available(iOS 18, macOS 15, tvOS 15, *) { - throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") + func testData_ReadingURL_Tracking() { + // -- Arrange -- + let expectedSpanCount: Int + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 } + + // -- Act & Assert -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { + assertSpans(expectedSpanCount, "file.read") { let _ = try? Data(contentsOf: fixture.fileURL) } } - - func test_ReadingURLWithOption_Tracking() throws { - if #available(iOS 18, macOS 15, tvOS 15, *) { - throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") + + func testData_ReadingURLWithOption_Tracking() throws { + // -- Arrange -- + let expectedSpanCount: Int + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 } + + // -- Act & Assert -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { - let data = try? Data(contentsOf: fixture.fileURL, options: .uncached) - XCTAssertEqual(data?.count, fixture.data.count) + let data = assertSpans(expectedSpanCount, "file.read") { + try? Data(contentsOf: fixture.fileURL, options: .uncached) } + XCTAssertEqual(data?.count, fixture.data.count) } - + func test_ReadingFile_Tracking() { + // -- Arrange -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { - let data = NSData(contentsOfFile: fixture.filePath) - XCTAssertEqual(data?.count, fixture.data.count) + + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + NSData(contentsOfFile: fixture.filePath) } + XCTAssertEqual(data?.count, fixture.data.count) } func test_ReadingFileWithOptions_Tracking() { + // -- Arrange -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { - let data = try? NSData(contentsOfFile: fixture.filePath, options: .uncached) - XCTAssertEqual(data?.count, fixture.data.count) + + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + try? NSData(contentsOfFile: fixture.filePath, options: .uncached) } + XCTAssertEqual(data?.count, fixture.data.count) } func test_ReadingBigFile() { + // -- Arrange -- SentrySDK.start(options: fixture.getOptions()) - guard let jsonFile = getBigFilePath() else { XCTFail("Could not open Resource") return } - - assertSpans(1, "file.read") { - let data = try? NSData(contentsOfFile: jsonFile, options: .uncached) - XCTAssertEqual(data?.count, 295_760) + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + try? NSData(contentsOfFile: jsonFile, options: .uncached) } + XCTAssertEqual(data?.count, 295_760) } func test_WritingBigFile() { + // -- Arrange -- guard let jsonFile = getBigFilePath() else { XCTFail("Could not open Resource") return @@ -187,14 +262,13 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } SentrySDK.start(options: fixture.getOptions()) - + + // -- Act & Assert -- assertSpans(1, "file.write") { try? data.write(to: fixture.fileURL, options: .atomic) - - let size = try? fixture.fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 - - XCTAssertEqual(size, 295_760) } + let size = try? fixture.fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 + XCTAssertEqual(size, 295_760) } private func getBigFilePath() -> String? { @@ -275,22 +349,32 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { try? fixture.data.write(to: fixture.fileURL) } } - - private func assertSpans( _ spansCount: Int, _ operation: String, _ description: String = "TestFile", _ block: () -> Void) { + + @discardableResult + private func assertSpans( + _ spansCount: Int, + _ operation: String, + _ description: String = "TestFile", + _ block: () -> ReturnValue, + file: StaticString = #file, + line: UInt = #line + ) -> ReturnValue { let parentTransaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - block() - + let result = block() + let childrenSelector = NSSelectorFromString("children") guard let children = parentTransaction.perform(childrenSelector).takeUnretainedValue() as? [Span] else { - XCTFail("Did not found children property from transaction.") - return + XCTFail("Did not found children property from transaction.", file: file, line: line) + return result } - XCTAssertEqual(children.count, spansCount) + XCTAssertEqual(children.count, spansCount, "Actual span count is not equal to expected count", file: file, line: line) if let first = children.first { - XCTAssertEqual(first.operation, operation) + XCTAssertEqual(first.operation, operation, "Operation for span is not equal to expected operation", file: file, line: line) } + + return result } }